Compare commits

..
Author SHA1 Message Date
Ajay Bura 1ce6cfd487 Replace confirm and prompt with custom dialogs 2022-04-25 20:12:06 +05:30
48 changed files with 1769 additions and 2669 deletions

No files matched your search

+1 -2
View File
@@ -1,3 +1,2 @@
github: ajbura
liberapay: ajbura
open_collective: cinny
liberapay: ajbura
+4 -4
View File
@@ -11,23 +11,23 @@ jobs:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.2
uses: actions/checkout@v3.0.1
- name: Build app
run: npm ci && npm run build
- name: Upload artifact
uses: actions/upload-artifact@v3.1.0
uses: actions/upload-artifact@v3.0.0
with:
name: previewbuild
path: dist
retention-days: 1
- name: Get PR info
uses: actions/github-script@v6.1.0
uses: actions/github-script@v6.0.0
with:
script: |
var fs = require('fs');
fs.writeFileSync('${{github.workspace}}/pr.json', JSON.stringify(context.payload.pull_request));
- name: Upload PR Info
uses: actions/upload-artifact@v3.1.0
uses: actions/upload-artifact@v3.0.0
with:
name: pr.json
path: pr.json
+4 -7
View File
@@ -6,9 +6,6 @@ on:
- completed
jobs:
get-build-and-deploy:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
if: >
${{ github.event.workflow_run.conclusion == 'success' }}
@@ -17,7 +14,7 @@ jobs:
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
# so instead we get this mess:
- name: Download artifact
uses: actions/github-script@v6.1.0
uses: actions/github-script@v6.0.0
with:
script: |
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -51,7 +48,7 @@ jobs:
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
- name: Read PR Info
id: readctx
uses: actions/github-script@v6.1.0
uses: actions/github-script@v6.0.0
with:
script: |
var fs = require('fs');
@@ -59,7 +56,7 @@ jobs:
console.log(`::set-output name=prnumber::${pr.number}`);
- name: Deploy to Netlify
id: netlify
uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99
uses: nwtgck/actions-netlify@v1.2.3
with:
publish-dir: dist
deploy-message: "Deploy from GitHub Actions"
@@ -71,7 +68,7 @@ jobs:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
timeout-minutes: 1
- name: Edit PR Description
uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409
uses: Beakyn/gha-comment-pull-request@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+2 -2
View File
@@ -13,9 +13,9 @@ jobs:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.2
uses: actions/checkout@v3.0.1
- name: Build Docker image
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v2.10.0
with:
context: .
push: false
+3 -4
View File
@@ -9,13 +9,12 @@ jobs:
deploy-to-netlify:
name: 'Deploy'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.2
uses: actions/checkout@v3.0.1
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
uses: jsmrcaga/action-netlify-deploy@v1.7.2
with:
install_command: "npm ci"
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+17 -48
View File
@@ -5,48 +5,14 @@ on:
types: [published]
jobs:
create-release-tar:
name: 'Create release tar'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.2
- name: Build
run: |
npm ci
npm run build
- name: Get version from tag
id: vars
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
- name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Sign tar.gz
run: |
echo '${{ secrets.GNUPG_KEY }}' | gpg --batch --import
# Sadly a few lines in the private key match a few lines in the public key,
# As a result just --export --armor gives us a few lines replaced with ***
# making it useless for importing the signing key. Instead, we dump it as
# non-armored and hex-encode it so that its printable.
echo "PGP Signing key, in raw PGP format in hex. Import with cat ... | xxd -r -p - | gpg --import"
gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5
with:
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
deploy-to-netlify:
name: 'Deploy to Netlify'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.2
uses: actions/checkout@v3.0.1
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
uses: jsmrcaga/action-netlify-deploy@v1.7.2
with:
install_command: "npm ci"
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
@@ -54,34 +20,37 @@ jobs:
BUILD_DIRECTORY: "dist"
NETLIFY_DEPLOY_MESSAGE: "Prod deploy v${{ github.ref }}"
NETLIFY_DEPLOY_TO_PROD: true
- name: Get version from tag
id: vars
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
- name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Upload tagged release
uses: softprops/action-gh-release@v0.1.14
with:
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
push-to-dockerhub:
push_to_dockerhub:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
uses: actions/checkout@v3.0.1
- name: Login to Docker Hub
uses: docker/login-action@v2.0.0
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4.0.1
uses: docker/metadata-action@v3.7.0
with:
images: ajbura/cinny
- name: Build and push Docker image
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v2.10.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Ajay Bura (ajbura)
Copyright (c) 2021 Ajay Bura (ajbura) and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+12 -71
View File
@@ -1,27 +1,18 @@
<p align="center">
<img src="https://raw.githubusercontent.com/ajbura/cinny/dev/public/res/svg/cinny.svg?sanitize=true"
height="16">
<span><b>Cinny</b></span>
</p>
<p align="center">
<a href="https://github.com/ajbura/cinny/releases">
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
<a href="https://hub.docker.com/r/ajbura/cinny">
<img alt="DockerHub downloads" src="https://img.shields.io/docker/pulls/ajbura/cinny?logo=docker&style=social"></a>
<a href="https://fosstodon.org/@cinnyapp">
<img alt="Follow on Mastodon" src="https://img.shields.io/mastodon/follow/106845779685925461?domain=https%3A%2F%2Ffosstodon.org&logo=mastodon&style=social"></a>
<a href="https://twitter.com/intent/follow?screen_name=cinnyapp">
<img alt="Follow on Twitter" src="https://img.shields.io/twitter/follow/cinnyapp?logo=twitter&style=social"></a>
<a href="https://cinny.in/#sponsor">
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
</p>
# Cinny
**Cinny** is a Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have a client that is easy on end user
and feels a modern chat application.
## Table of Contents
- [About](#about)
- [Getting Started](https://cinny.in)
- [Contributing](./CONTRIBUTING.md)
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
## About <a name = "about"></a>
Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, elegant and secure interface.
![preview](https://github.com/ajbura/cinny-site/blob/master/assets/preview-light.png)
## Building and Running
### Running pre-compiled
@@ -29,57 +20,7 @@ and feels a modern chat application.
A tarball of pre-compiled version of the app is provided with each [release](https://github.com/ajbura/cinny/releases).
You can serve the application with a webserver of your choosing by simply copying `dist/` directory to the webroot.
<details>
<summary>PGP Public Key to verify pre-compiled tarball</summary>
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGJw/g0BDAC8qQeLqDMzYzfPyOmRlHVEoguVTo+eo1aVdQH2X7OELdjjBlyj
6d6c1adv/uF2g83NNMoQY7GEeHjRnXE4m8kYSaarb840pxrYUagDc0dAbJOGaCBY
FKTo7U1Kvg0vdiaRuus0pvc1NVdXSxRNQbFXBSwduD+zn66TI3HfcEHNN62FG1cE
K1jWDwLAU0P3kKmj8+CAc3h9ZklPu0k/+t5bf/LJkvdBJAUzGZpehbPL5f3u3BZ0
leZLIrR8uV7PiV5jKFahxlKR5KQHld8qQm+qVhYbUzpuMBGmh419I6UvTzxuRcvU
Frn9ttCEzV55Y+so4X2e4ZnB+5gOnNw+ecifGVdj/+UyWnqvqqDvLrEjjK890nLb
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
AdQEEwEIAD4WIQSRri2MHidaaZv+vvuUMwx6UK/M8wUCYnD+DQIbAwUJA8JnAAUL
CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCUMwx6UK/M88ApC/9HAdbum1lYBC0s
1k7GwP2A7B4sQtBWjy771BzybWlHeaeG+BGJwg4YiuowXZMm5dubFJFoI/CfeY07
B5aK40/bmT6Xcfkp0VA74c1wUpubBUEJN7tH5HG/OGd9BKeq9E/HHtVaJLVT1k3w
Rhv9VuHO6nR30EEp7IDthftotl5S4lio3+W0pKk4TAKV8vjaCNp3y/lAHzoP1BU9
bUSao+7GXVeArKBjuqxN+t1uuiaxPH4L0oe2pMVjTig04zGJM5fTVoly859MEcC/
R7Taq9RWGfXFmgCXy8Dviz3eOD90vqpCzhX4+ypK0cp2X0UwhMH4dpKUzExmdbhl
eBO5GcHB4VxvloRBNf9/Lr7YOTgWejMUw+MlhZE2RE8unfW1LnM/cjL4dhXzO/XB
FUHHNq8d6d4e02rfWqw7mZo2/NVJgFRcvzw2rgx7w7CKtCNwF4lNjUetB2waZzDb
fAE0kwhK4Iuwvy12JOBzL0Yy9MxANtwUryr/LQz9AmdT4Rwnp0S5AY0EYnD+DQEM
ANOu/d6ZMF8bW+Df9RDCUQKytbaZfa+ZbIHBus7whCD/SQMOhPKntv3HX7SmMCs+
5i27kJMu4YN623JCS7hdCoXVO1R5kXCEcneW/rPBMDutaM472YvIWMIqK9Wwl5+0
Piu2N+uTkKhe9uS2u7eN+Khef3d7xfjGRxoppM+xI9dZO+jhYiy8LuC0oBohTjJq
QPqfGDpowBwRkkOsGz/XVcesJ1Pzg4bKivTS9kZjZSyT9RRSY8As0sVUN57AwYul
s1+eh00n/tVpi2Jj9pCm7S0csSXvXj8v2OTdK1jt4YjpzR0/rwh4+/xlOjDjZEqH
vMPhpzpbgnwkxZ3X8BFne9dJ3maC5zQ3LAeCP5m1W0hXzagYhfyjo74slJgD1O8c
LDf2Oxc5MyM8Y/UK497zfqSPfgT3NhQmhHzk83DjXw3I6Z3A3U+Jp61w0eBRI1nx
H1UIG+gldcAKUTcfwL0lghoT3nmi9JAbvek0Smhz00Bbo8/dx8vwQRxDUxlt7Exx
NwARAQABiQG8BBgBCAAmFiEEka4tjB4nWmmb/r77lDMMelCvzPMFAmJw/g0CGwwF
CQPCZwAACgkQlDMMelCvzPPT7Qv8CjXUEhphZFLwpBfaNOzRNfIXJST9aDit8zHW
IMmfSpORVfpU71IyIB3o/DtTUPwCeb8nvNJs7aj1QT1ZUSsqFa3yY2S16V/g8+WN
sHca6oDSc1J+A0eEpEL1HbG1b5OPBC0AeGvvMOoqrbqThBZVKg1Jc/0SD3cvKElv
aHeCZCNNmfcZ2Ib4HYhhc8//ZtC9TeI+5J/YesctY1M12EoWMxMrc27Y3P5Pa0BI
Uc3qxWggPq1vOFYsEshL0w99HyJvREJmQA7Fa0crV+rICxyrBxJeNnEvjH/0KCBU
LCkEonLY1QwrxyeeV3VpxGE3zHHE3azOdAjTIoAdzX5f/qhbgYlM68GL2f8xdDkp
O0igSGHWhO4F8BfmE7IOTx1Bi7daczp8nCFxh73cKpKB0RUsd9xxrqYpovjmEAlo
w7aHpdzt64NQcsrbK10OSVDF3gFa9Vz20/NQvdUrp8jGmAb/8+nYqI94Jsc28H36
UeGsouhyuITLwEhScounZDqop+Dx
=Zg+6
-----END PGP PUBLIC KEY BLOCK-----
```
</details>
### Building from source
> We recommend using a version manager as versions change very quickly. You will likely need to switch
between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Also recommended nodejs version is 16.15.0 LTS.
Execute the following commands to compile the app from its source code:
@@ -90,7 +31,7 @@ npm run build # Compiles the app into the dist/ directory
You can then copy the files to a webserver's webroot of your choice.
To serve a development version of the app locally for testing, you need to use the command `npm start`.
To serve a development version of the app locally for testing, you may also use the command `npm start`.
### Running with Docker
@@ -118,7 +59,7 @@ To set default Homeserver on login and register page, place a customized [`confi
## License
Copyright (c) 2021 Ajay Bura (ajbura)
Copyright (c) 2021 Ajay Bura (ajbura) and contributors
Code licensed under the MIT License: <http://opensource.org/licenses/MIT>
+1 -2
View File
@@ -7,6 +7,5 @@
"kde.org",
"matrix.org",
"chat.mozilla.org"
],
"allowCustomHomeservers": true
]
}
+1510 -1599
View File
File diff suppressed because it is too large. Load diff
+21 -23
View File
@@ -1,11 +1,11 @@
{
"name": "cinny",
"version": "2.0.3",
"version": "1.8.2",
"description": "Yet another matrix client",
"main": "index.js",
"engines": {
"npm": ">=6.14.8 <=8.5.5",
"node": ">=14.15.0 <=17.9.0"
"npm": ">=6.14.11",
"node": ">=14.6.0"
},
"scripts": {
"start": "webpack serve --config ./webpack.dev.js --open",
@@ -15,8 +15,8 @@
"author": "Ajay Bura",
"license": "MIT",
"dependencies": {
"@fontsource/inter": "^4.5.10",
"@fontsource/roboto": "^4.5.7",
"@fontsource/inter": "^4.5.7",
"@fontsource/roboto": "^4.5.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@tippyjs/react": "^4.2.6",
"babel-polyfill": "^6.26.0",
@@ -26,13 +26,11 @@
"file-saver": "^2.0.5",
"flux": "^4.0.3",
"formik": "^2.2.9",
"html-react-parser": "^1.4.12",
"katex": "^0.15.6",
"html-react-parser": "^1.4.11",
"linkifyjs": "^2.1.9",
"matrix-js-sdk": "^17.2.0",
"matrix-js-sdk": "^17.0.0",
"micromark": "^3.0.10",
"micromark-extension-gfm": "^2.0.1",
"micromark-extension-math": "^2.0.2",
"micromark-util-chunked": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0",
"micromark-util-symbol": "^1.0.1",
@@ -43,45 +41,45 @@
"react-dnd-html5-backend": "^15.1.3",
"react-dom": "^17.0.2",
"react-google-recaptcha": "^2.1.0",
"react-modal": "^3.15.1",
"react-modal": "^3.14.4",
"sanitize-html": "^2.7.0",
"tippy.js": "^6.3.7",
"twemoji": "^14.0.2"
},
"devDependencies": {
"@babel/core": "^7.18.0",
"@babel/preset-env": "^7.18.0",
"@babel/preset-react": "^7.17.12",
"@babel/core": "^7.17.9",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"assert": "^2.0.0",
"babel-loader": "^8.2.5",
"babel-loader": "^8.2.4",
"browserify-fs": "^1.0.0",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"copy-webpack-plugin": "^10.2.4",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^8.16.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"eslint": "^8.13.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.30.0",
"eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"html-loader": "^3.1.0",
"html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "^2.6.0",
"path-browserify": "^1.0.1",
"sass": "^1.52.1",
"sass-loader": "^13.0.0",
"sass": "^1.50.0",
"sass-loader": "^12.6.0",
"stream-browserify": "^3.0.0",
"style-loader": "^3.3.1",
"url": "^0.11.0",
"util": "^0.12.4",
"webpack": "^5.72.1",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.0",
"webpack-dev-server": "^4.8.1",
"webpack-merge": "^5.7.3"
}
}
-6
View File
@@ -18,11 +18,5 @@
</head>
<body id="appBody">
<div id="root"></div>
<audio id="notificationSound">
<source src="./sound/notification.ogg" type="audio/ogg" />
</audio>
<audio id="inviteSound">
<source src="./sound/invite.ogg" type="audio/ogg" />
</audio>
</body>
</html>
-33
View File
@@ -1,33 +0,0 @@
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import katex from 'katex';
import 'katex/dist/katex.min.css';
import 'katex/dist/contrib/copy-tex';
import 'katex/dist/contrib/copy-tex.css';
const Math = React.memo(({
content, throwOnError, errorColor, displayMode,
}) => {
const ref = useRef(null);
useEffect(() => {
katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
}, [content, throwOnError, errorColor, displayMode]);
return <span ref={ref} />;
});
Math.defaultProps = {
throwOnError: null,
errorColor: null,
displayMode: null,
};
Math.propTypes = {
content: PropTypes.string.isRequired,
throwOnError: PropTypes.bool,
errorColor: PropTypes.string,
displayMode: PropTypes.bool,
};
export default Math;
+3 -12
View File
@@ -123,26 +123,17 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
let mEvent = eTimeline.getTimelineSet().findEventById(eventId);
const editedList = roomTimeline.editedTimeline.get(mEvent.getId());
if (editedList) {
mEvent = editedList[editedList.length - 1];
}
const mEvent = eTimeline.getTimelineSet().findEventById(eventId);
const rawBody = mEvent.getContent().body;
const username = getUsernameOfRoomMember(mEvent.sender);
if (isMountedRef.current === false) return;
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
if (editedList && parsedBody.startsWith(' * ')) {
parsedBody = parsedBody.slice(3);
}
setReply({
to: username,
color: colorMXID(mEvent.getSender()),
body: parsedBody,
body: parseReply(rawBody)?.body ?? rawBody ?? fallbackBody,
event: mEvent,
});
} catch {
@@ -200,7 +191,7 @@ const MessageBody = React.memo(({
let content = null;
if (isCustomHTML) {
try {
content = twemojify(sanitizeCustomHtml(body), undefined, true, false, true);
content = twemojify(sanitizeCustomHtml(body), undefined, true, false);
} catch {
console.error('Malformed custom html: ', body);
content = twemojify(body, undefined);
+6 -5
View File
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import './RoomIntro.scss';
import { twemojify } from '../../../util/twemojify';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
@@ -14,8 +15,8 @@ function RoomIntro({
<div className="room-intro">
<Avatar imageSrc={avatarSrc} text={name} bgColor={colorMXID(roomId)} size="large" />
<div className="room-intro__content">
<Text className="room-intro__name" variant="h1" weight="medium" primary>{heading}</Text>
<Text className="room-intro__desc" variant="b1">{desc}</Text>
<Text className="room-intro__name" variant="h1" weight="medium" primary>{twemojify(heading)}</Text>
<Text className="room-intro__desc" variant="b1">{twemojify(desc, undefined, true)}</Text>
{ time !== null && <Text className="room-intro__time" variant="b3">{time}</Text>}
</div>
</div>
@@ -34,9 +35,9 @@ RoomIntro.propTypes = {
PropTypes.bool,
]),
name: PropTypes.string.isRequired,
heading: PropTypes.node.isRequired,
desc: PropTypes.node.isRequired,
time: PropTypes.node,
heading: PropTypes.string.isRequired,
desc: PropTypes.string.isRequired,
time: PropTypes.string,
};
export default RoomIntro;
@@ -70,7 +70,7 @@ function RoomVisibility({ roomId }) {
const noSpaceParent = currentState.getStateEvents('m.space.parent').length === 0;
const mCreate = currentState.getStateEvents('m.room.create')[0]?.getContent();
const roomVersion = Number(mCreate?.room_version ?? 0);
const roomVersion = Number(mCreate.room_version);
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
@@ -1,200 +0,0 @@
/* eslint-disable react/prop-types */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './EmojiVerification.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { hasPrivateKey } from '../../../client/state/secretStorageKeys';
import { getDefaultSSKey, isCrossVerified } from '../../../util/matrixUtil';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Spinner from '../../atoms/spinner/Spinner';
import Dialog from '../../molecules/dialog/Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useStore } from '../../hooks/useStore';
import { accessSecretStorage } from '../settings/SecretStorageAccess';
function EmojiVerificationContent({ data, requestClose }) {
const [sas, setSas] = useState(null);
const [process, setProcess] = useState(false);
const { request, targetDevice } = data;
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const beginStore = useStore();
const beginVerification = async () => {
if (
isCrossVerified(mx.deviceId)
&& (mx.getCrossSigningId() === null || await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing') === false)
) {
if (!hasPrivateKey(getDefaultSSKey())) {
const keyData = await accessSecretStorage('Emoji verification');
if (!keyData) {
request.cancel();
return;
}
}
await mx.checkOwnCrossSigningTrust();
}
setProcess(true);
await request.accept();
const verifier = request.beginKeyVerification('m.sas.v1', targetDevice);
const handleVerifier = (sasData) => {
verifier.off('show_sas', handleVerifier);
if (!mountStore.getItem()) return;
setSas(sasData);
setProcess(false);
};
verifier.on('show_sas', handleVerifier);
await verifier.verify();
};
const sasMismatch = () => {
sas.mismatch();
setProcess(true);
};
const sasConfirm = () => {
sas.confirm();
setProcess(true);
};
useEffect(() => {
mountStore.setItem(true);
const handleChange = () => {
if (request.done || request.cancelled) {
requestClose();
return;
}
if (targetDevice && !beginStore.getItem()) {
beginStore.setItem(true);
beginVerification();
}
};
if (request === null) return null;
const req = request;
req.on('change', handleChange);
return () => {
req.off('change', handleChange);
if (req.cancelled === false && req.done === false) {
req.cancel();
}
};
}, [request]);
const renderWait = () => (
<>
<Spinner size="small" />
<Text>Waiting for response from other device...</Text>
</>
);
if (sas !== null) {
return (
<div className="emoji-verification__content">
<Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
<div className="emoji-verification__emojis">
{sas.sas.emoji.map((emoji, i) => (
// eslint-disable-next-line react/no-array-index-key
<div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
<Text variant="h1">{twemojify(emoji[0])}</Text>
<Text>{emoji[1]}</Text>
</div>
))}
</div>
<div className="emoji-verification__buttons">
{process ? renderWait() : (
<>
<Button variant="primary" onClick={sasConfirm}>They match</Button>
<Button onClick={sasMismatch}>{'They don\'t match'}</Button>
</>
)}
</div>
</div>
);
}
if (targetDevice) {
return (
<div className="emoji-verification__content">
<Text>Please accept the request from other device.</Text>
<div className="emoji-verification__buttons">
{renderWait()}
</div>
</div>
);
}
return (
<div className="emoji-verification__content">
<Text>Click accept to start the verification process.</Text>
<div className="emoji-verification__buttons">
{
process
? renderWait()
: <Button variant="primary" onClick={beginVerification}>Accept</Button>
}
</div>
</div>
);
}
EmojiVerificationContent.propTypes = {
data: PropTypes.shape({}).isRequired,
requestClose: PropTypes.func.isRequired,
};
function useVisibilityToggle() {
const [data, setData] = useState(null);
const mx = initMatrix.matrixClient;
useEffect(() => {
const handleOpen = (request, targetDevice) => {
setData({ request, targetDevice });
};
navigation.on(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
mx.on('crypto.verification.request', handleOpen);
return () => {
navigation.removeListener(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
mx.removeListener('crypto.verification.request', handleOpen);
};
}, []);
const requestClose = () => setData(null);
return [data, requestClose];
}
function EmojiVerification() {
const [data, requestClose] = useVisibilityToggle();
return (
<Dialog
isOpen={data !== null}
className="emoji-verification"
title={(
<Text variant="s1" weight="medium" primary>
Emoji verification
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{
data !== null
? <EmojiVerificationContent data={data} requestClose={requestClose} />
: <div />
}
</Dialog>
);
}
export default EmojiVerification;
@@ -1,35 +0,0 @@
@use '../../partials/flex';
@use '../../partials/dir';
.emoji-verification {
&__content {
padding: var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
display: flex;
flex-direction: column;
gap: var(--sp-normal);
}
&__emojis {
margin: var(--sp-loose) 0;
display: flex;
align-items: center;
justify-content: space-around;
gap: var(--sp-extra-tight);
flex-wrap: wrap;
}
&__emoji-block {
@extend .cp-fx__column;
flex: 1;
align-items: center;
gap: var(--sp-extra-tight);
white-space: nowrap;
text-transform: capitalize;
}
&__buttons {
display: flex;
gap: var(--sp-normal);
}
}
+2 -4
View File
@@ -56,10 +56,9 @@ function InviteList({ isOpen, onRequestClose }) {
function renderRoomTile(roomId) {
const mx = initMatrix.matrixClient;
const myRoom = mx.getRoom(roomId);
if (!myRoom) return null;
const roomName = myRoom.name;
let roomAlias = myRoom.getCanonicalAlias();
if (!roomAlias) roomAlias = myRoom.roomId;
if (roomAlias === null) roomAlias = myRoom.roomId;
const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? '';
return (
<RoomTile
@@ -98,13 +97,12 @@ function InviteList({ isOpen, onRequestClose }) {
{
Array.from(initMatrix.roomList.inviteDirects).map((roomId) => {
const myRoom = initMatrix.matrixClient.getRoom(roomId);
if (myRoom === null) return null;
const roomName = myRoom.name;
return (
<RoomTile
key={myRoom.roomId}
name={roomName}
id={myRoom.getDMInviter() || roomId}
id={myRoom.getDMInviter()}
options={
procInvite.has(myRoom.roomId)
? (<Spinner size="small" />)
+1 -13
View File
@@ -103,18 +103,6 @@ function InviteUser({
updateIsSearching(false);
}
async function hasDevices(userId) {
try {
const usersDeviceMap = await mx.downloadKeys([userId, mx.getUserId()]);
return Object.values(usersDeviceMap).every((userDevices) =>
Object.keys(userDevices).length > 0,
);
} catch (e) {
console.error("Error determining if it's possible to encrypt to all users: ", e);
return false;
}
}
async function createDM(userId) {
if (mx.getUserId() === userId) return;
const dmRoomId = hasDMWith(userId);
@@ -129,7 +117,7 @@ function InviteUser({
procUserError.delete(userId);
updateUserProcError(getMapCopy(procUserError));
const result = await roomActions.createDM(userId, await hasDevices(userId));
const result = await roomActions.createDM(userId);
roomIdToUserId.set(result.room_id, userId);
updateRoomIdToUserId(getMapCopy(roomIdToUserId));
} catch (e) {
-155
View File
@@ -1,155 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './JoinAlias.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { join } from '../../../client/action/room';
import { selectRoom, selectSpace } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import Dialog from '../../molecules/dialog/Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useStore } from '../../hooks/useStore';
const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/;
function JoinAliasContent({ term, requestClose }) {
const [process, setProcess] = useState(false);
const [error, setError] = useState(undefined);
const [lastJoinId, setLastJoinId] = useState(undefined);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const openRoom = (roomId) => {
const room = mx.getRoom(roomId);
if (!room) return;
if (room.isSpaceRoom()) selectSpace(roomId);
else selectRoom(roomId);
requestClose();
};
useEffect(() => {
const handleJoin = (roomId) => {
if (lastJoinId !== roomId) return;
openRoom(roomId);
};
initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleJoin);
return () => {
initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleJoin);
};
}, [lastJoinId]);
const handleSubmit = async (e) => {
e.preventDefault();
mountStore.setItem(true);
const alias = e.target.alias.value;
if (alias?.trim() === '') return;
if (alias.match(ALIAS_OR_ID_REG) === null) {
setError('Invalid address.');
return;
}
setProcess('Looking for address...');
setError(undefined);
let via;
if (alias.startsWith('#')) {
try {
const aliasData = await mx.resolveRoomAlias(alias);
via = aliasData?.servers.slice(0, 3) || [];
if (mountStore.getItem()) {
setProcess(`Joining ${alias}...`);
}
} catch (err) {
if (!mountStore.getItem()) return;
setProcess(false);
setError(`Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`);
}
}
try {
const roomId = await join(alias, false, via);
if (!mountStore.getItem()) return;
setLastJoinId(roomId);
openRoom(roomId);
} catch {
if (!mountStore.getItem()) return;
setProcess(false);
setError(`Unable to join ${alias}. Either room/space is private or doesn't exist.`);
}
};
return (
<form className="join-alias" onSubmit={handleSubmit}>
<Input
label="Address"
value={term}
name="alias"
required
/>
{error && <Text className="join-alias__error" variant="b3">{error}</Text>}
<div className="join-alias__btn">
{
process
? (
<>
<Spinner size="small" />
<Text>{process}</Text>
</>
)
: <Button variant="primary" type="submit">Join</Button>
}
</div>
</form>
);
}
JoinAliasContent.defaultProps = {
term: undefined,
};
JoinAliasContent.propTypes = {
term: PropTypes.string,
requestClose: PropTypes.func.isRequired,
};
function useWindowToggle() {
const [data, setData] = useState(null);
useEffect(() => {
const handleOpen = (term) => {
setData({ term });
};
navigation.on(cons.events.navigation.JOIN_ALIAS_OPENED, handleOpen);
return () => {
navigation.removeListener(cons.events.navigation.JOIN_ALIAS_OPENED, handleOpen);
};
}, []);
const onRequestClose = () => setData(null);
return [data, onRequestClose];
}
function JoinAlias() {
const [data, requestClose] = useWindowToggle();
return (
<Dialog
isOpen={data !== null}
title={(
<Text variant="s1" weight="medium" primary>Join with address</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{ data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div /> }
</Dialog>
);
}
export default JoinAlias;
@@ -1,20 +0,0 @@
@use '../../partials/dir';
.join-alias {
padding: var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
& > *:not(:first-child) {
margin-top: var(--sp-normal);
}
&__error {
color: var(--tc-danger-high);
margin-top: var(--sp-extra-tight) !important;
}
&__btn {
display: flex;
gap: var(--sp-normal);
}
}
+2 -6
View File
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
@@ -10,12 +9,12 @@ import { roomIdByActivity } from '../../../util/sort';
import RoomsCategory from './RoomsCategory';
const drawerPostie = new Postie();
function Directs({ size }) {
function Directs() {
const mx = initMatrix.matrixClient;
const { roomList, notifications } = initMatrix;
const [directIds, setDirectIds] = useState([]);
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), [size]);
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), []);
useEffect(() => {
const handleTimeline = (event, room, toStartOfTimeline, removed, data) => {
@@ -64,8 +63,5 @@ function Directs({ size }) {
return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />;
}
Directs.propTypes = {
size: PropTypes.number.isRequired,
};
export default Directs;
+5 -10
View File
@@ -42,15 +42,12 @@ function Drawer() {
const [spaceId] = useSelectedSpace();
const [, forceUpdate] = useForceUpdate();
const scrollRef = useRef(null);
const { roomList } = initMatrix;
useEffect(() => {
const handleUpdate = () => {
forceUpdate();
};
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate);
const { roomList } = initMatrix;
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, forceUpdate);
return () => {
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate);
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, forceUpdate);
};
}, []);
@@ -64,16 +61,14 @@ function Drawer() {
<div className="drawer">
<DrawerHeader selectedTab={selectedTab} spaceId={spaceId} />
<div className="drawer__content-wrapper">
{navigation.selectedSpacePath.length > 1 && selectedTab !== cons.tabs.DIRECTS && (
<DrawerBreadcrumb spaceId={spaceId} />
)}
{navigation.selectedSpacePath.length > 1 && <DrawerBreadcrumb spaceId={spaceId} />}
<div className="rooms__wrapper">
<ScrollView ref={scrollRef} autoHide>
<div className="rooms-container">
{
selectedTab !== cons.tabs.DIRECTS
? <Home spaceId={spaceId} />
: <Directs size={roomList.directs.size} />
: <Directs />
}
</div>
</ScrollView>
@@ -7,7 +7,7 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import {
openPublicRooms, openCreateRoom, openSpaceManage, openJoinAlias,
openPublicRooms, openCreateRoom, openSpaceManage,
openSpaceAddExisting, openInviteUser, openReusableContextMenu,
} from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common';
@@ -60,14 +60,6 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
Join public room
</MenuItem>
)}
{ !spaceId && (
<MenuItem
iconSrc={PlusIC}
onClick={() => { afterOptionSelect(); openJoinAlias(); }}
>
Join with address
</MenuItem>
)}
{ spaceId && (
<MenuItem
iconSrc={PlusIC}
+1 -1
View File
@@ -92,7 +92,7 @@ function ProfileAvatarMenu() {
function CrossSigninAlert() {
const deviceList = useDeviceList();
const unverified = deviceList?.filter((device) => isCrossVerified(device.device_id) === false);
const unverified = deviceList?.filter((device) => !isCrossVerified(device.device_id));
if (!unverified?.length) return null;
@@ -195,7 +195,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
return rooms.map((room) => {
const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id;
const name = typeof room.name === 'string' ? room.name : alias;
const isJoined = initMatrix.matrixClient.getRoom(room.room_id)?.getMyMembership() === 'join';
const isJoined = initMatrix.matrixClient.getRoom(room.room_id) !== null;
return (
<RoomTile
key={room.room_id}
-4
View File
@@ -7,8 +7,6 @@ import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExistin
import Search from '../search/Search';
import ViewSource from '../view-source/ViewSource';
import CreateRoom from '../create-room/CreateRoom';
import JoinAlias from '../join-alias/JoinAlias';
import EmojiVerification from '../emoji-verification/EmojiVerification';
import ReusableDialog from '../../molecules/dialog/ReusableDialog';
@@ -20,10 +18,8 @@ function Dialogs() {
<ProfileViewer />
<ShortcutSpaces />
<CreateRoom />
<JoinAlias />
<SpaceAddExisting />
<Search />
<EmojiVerification />
<ReusableDialog />
</>
+14 -50
View File
@@ -8,7 +8,6 @@ import PropTypes from 'prop-types';
import './RoomViewContent.scss';
import dateFormat from 'dateformat';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
@@ -51,54 +50,21 @@ function loadingMsgPlaceholders(key, count = 2) {
);
}
function RoomIntroContainer({ event, timeline }) {
const [, nameForceUpdate] = useForceUpdate();
function genRoomIntro(mEvent, roomTimeline) {
const mx = initMatrix.matrixClient;
const { roomList } = initMatrix;
const { room } = timeline;
const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
const isDM = roomList.directs.has(timeline.roomId);
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
const heading = isDM ? room.name : `Welcome to ${room.name}`;
const topic = twemojify(roomTopic || '', undefined, true);
const nameJsx = twemojify(room.name);
const desc = isDM
? (
<>
This is the beginning of your direct message history with @
<b>{nameJsx}</b>
{'. '}
{topic}
</>
)
: (
<>
{'This is the beginning of the '}
<b>{nameJsx}</b>
{' room. '}
{topic}
</>
);
useEffect(() => {
const handleUpdate = () => nameForceUpdate();
roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
return () => {
roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
};
}, []);
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
const isDM = initMatrix.roomList.directs.has(roomTimeline.roomId);
let avatarSrc = roomTimeline.room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
avatarSrc = isDM ? roomTimeline.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
return (
<RoomIntro
roomId={timeline.roomId}
key={mEvent ? mEvent.getId() : 'room-intro'}
roomId={roomTimeline.roomId}
avatarSrc={avatarSrc}
name={room.name}
heading={twemojify(heading)}
desc={desc}
time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
name={roomTimeline.room.name}
heading={`Welcome to ${roomTimeline.room.name}`}
desc={`This is the beginning of the ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
time={mEvent ? `Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
/>
);
}
@@ -233,7 +199,7 @@ function usePaginate(
};
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
return () => {
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
};
}, [roomTimeline]);
@@ -504,14 +470,12 @@ function RoomViewContent({ eventId, roomTimeline }) {
if (i === 0 && !roomTimeline.canPaginateBackward()) {
if (mEvent.getType() === 'm.room.create') {
tl.push(
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
);
tl.push(genRoomIntro(mEvent, roomTimeline));
itemCountIndex += 1;
// eslint-disable-next-line no-continue
continue;
} else {
tl.push(<RoomIntroContainer key="room-intro" event={null} timeline={roomTimeline} />);
tl.push(genRoomIntro(undefined, roomTimeline));
itemCountIndex += 1;
}
}
+14 -60
View File
@@ -4,7 +4,7 @@ import dateFormat from 'dateformat';
import initMatrix from '../../../client/initMatrix';
import { isCrossVerified } from '../../../util/matrixUtil';
import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation';
import { openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -25,7 +25,6 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useStore } from '../../hooks/useStore';
import { useDeviceList } from '../../hooks/useDeviceList';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
import { accessSecretStorage } from './SecretStorageAccess';
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
let isCompleted = false;
@@ -70,7 +69,6 @@ function DeviceManage() {
const [truncated, setTruncated] = useState(true);
const mountStore = useStore();
mountStore.setItem(true);
const isMeVerified = isCrossVerified(mx.deviceId);
useEffect(() => {
setProcessing([]);
@@ -129,41 +127,18 @@ function DeviceManage() {
removeFromProcessing(device);
};
const verifyWithKey = async (device) => {
const keyData = await accessSecretStorage('Session verification');
if (!keyData) return;
addToProcessing(device);
await mx.checkOwnCrossSigningTrust();
};
const verifyWithEmojis = async (deviceId) => {
const req = await mx.requestVerification(mx.getUserId(), [deviceId]);
openEmojiVerification(req, { userId: mx.getUserId(), deviceId });
};
const verify = (deviceId, isCurrentDevice) => {
if (isCurrentDevice) {
verifyWithKey(deviceId);
return;
}
verifyWithEmojis(deviceId);
};
const renderDevice = (device, isVerified) => {
const deviceId = device.device_id;
const displayName = device.display_name;
const lastIP = device.last_seen_ip;
const lastTS = device.last_seen_ts;
const isCurrentDevice = mx.deviceId === deviceId;
return (
<SettingTile
key={deviceId}
title={(
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
<Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}>
{displayName}
<Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
{isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
<Text variant="b3" span>{` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text>
</Text>
)}
options={
@@ -171,27 +146,19 @@ function DeviceManage() {
? <Spinner size="small" />
: (
<>
{((isMeVerified && isVerified === false) || (isCurrentDevice && isVerified === false)) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">Verify</Button>}
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
</>
)
}
content={(
<>
<Text variant="b3">
Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}>
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
</span>
{lastIP ? ` at ${lastIP}` : ''}
</Text>
{isCurrentDevice && (
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
{`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
</Text>
)}
</>
<Text variant="b3">
Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}>
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
</span>
{lastIP ? ` at ${lastIP}` : ''}
</Text>
)}
/>
);
@@ -199,16 +166,9 @@ function DeviceManage() {
const unverified = [];
const verified = [];
const noEncryption = [];
deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
const isVerified = isCrossVerified(device.device_id);
if (isVerified === true) {
verified.push(device);
} else if (isVerified === false) {
unverified.push(device);
} else {
noEncryption.push(device);
}
if (isCrossVerified(device.device_id)) verified.push(device);
else unverified.push(device);
});
return (
<div className="device-manage">
@@ -227,15 +187,9 @@ function DeviceManage() {
{
unverified.length > 0
? unverified.map((device) => renderDevice(device, false))
: <Text className="device-manage__info">No unverified sessions</Text>
: <Text className="device-manage__info">No unverified session</Text>
}
</div>
{noEncryption.length > 0 && (
<div>
<MenuHeader>Sessions without encryption support</MenuHeader>
{noEncryption.map((device) => renderDevice(device, null))}
</div>
)}
<div>
<MenuHeader>Verified sessions</MenuHeader>
{
@@ -244,7 +198,7 @@ function DeviceManage() {
if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true);
})
: <Text className="device-manage__info">No verified sessions</Text>
: <Text className="device-manage__info">No verified session</Text>
}
{ verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
@@ -15,23 +15,6 @@
& .setting-tile:last-of-type {
border-bottom: none;
}
& .setting-tile__options {
display: flex;
align-items: center;
gap: var(--sp-ultra-tight);
& .btn-positive {
padding: 6px var(--sp-tight);
min-width: 0;
}
}
&__current-label {
margin: 0 var(--sp-extra-tight);
padding: 2px var(--sp-ultra-tight);
color: var(--bg-surface);
background-color: var(--tc-surface-low);
border-radius: 4px;
}
&__rename {
padding: var(--sp-normal);
+1 -1
View File
@@ -159,9 +159,9 @@ function DeleteKeyBackupDialog({ requestClose }) {
const [isDeleting, setIsDeleting] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
mountStore.setItem(true);
const deleteBackup = async () => {
mountStore.setItem(true);
setIsDeleting(true);
try {
const backupInfo = await mx.getKeyBackupVersion();
@@ -24,14 +24,14 @@ function SecretStorageAccess({ onComplete }) {
const [process, setProcess] = useState(false);
const [error, setError] = useState(null);
const mountStore = useStore();
mountStore.setItem(true);
const toggleWithPhrase = () => setWithPhrase(!withPhrase);
const processInput = async ({ key, phrase }) => {
mountStore.setItem(true);
setProcess(true);
try {
const { salt, iterations } = sSKeyInfo.passphrase || {};
const { salt, iterations } = sSKeyInfo.passphrase;
const privateKey = key
? mx.keyBackupKeyFromRecoveryKey(key)
: await deriveKey(phrase, salt, iterations);
+5 -7
View File
@@ -93,13 +93,12 @@ function Homeserver({ onChange }) {
const result = await (await fetch(configFileUrl, { method: 'GET' })).json();
const selectedHs = result?.defaultHomeserver;
const hsList = result?.homeserverList;
const allowCustom = result?.allowCustomHomeservers ?? true;
if (!hsList?.length > 0 || selectedHs < 0 || selectedHs >= hsList?.length) {
throw new Error();
}
setHs({ selected: hsList[selectedHs], list: hsList, allowCustom: allowCustom });
setHs({ selected: hsList[selectedHs], list: hsList });
} catch {
setHs({ selected: 'matrix.org', list: ['matrix.org'], allowCustom: true });
setHs({ selected: 'matrix.org', list: ['matrix.org'] });
}
}, []);
@@ -107,15 +106,14 @@ function Homeserver({ onChange }) {
const { value } = e.target;
setProcess({ isLoading: false });
debounce._(async () => {
setHs({ ...hs, selected: value.trim() });
setHs({ selected: value.trim(), list: hs.list });
}, 700)();
};
return (
<>
<div className="homeserver-form">
<Input name="homeserver" onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} label="Homeserver"
disabled={hs === null || !hs.allowCustom} />
<Input name="homeserver" onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} label="Homeserver" />
<ContextMenu
placement="right"
content={(hideMenu) => (
@@ -128,7 +126,7 @@ function Homeserver({ onChange }) {
onClick={() => {
hideMenu();
hsRef.current.value = hsName;
setHs({ ...hs, selected: hsName });
setHs({ selected: hsName, list: hs.list });
}}
>
{hsName}
-15
View File
@@ -86,13 +86,6 @@ export function openCreateRoom(isSpace = false, parentId = null) {
});
}
export function openJoinAlias(term) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_JOIN_ALIAS,
term,
});
}
export function openInviteUser(roomId, searchTerm) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_INVITE_USER,
@@ -173,11 +166,3 @@ export function openReusableDialog(title, render, afterClose) {
afterClose,
});
}
export function openEmojiVerification(request, targetDevice) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_EMOJI_VERIFICATION,
request,
targetDevice,
});
}
+9 -11
View File
@@ -113,19 +113,17 @@ async function join(roomIdOrAlias, isDM, via) {
* @param {string} roomId
* @param {boolean} isDM
*/
async function leave(roomId) {
function leave(roomId) {
const mx = initMatrix.matrixClient;
const isDM = initMatrix.roomList.directs.has(roomId);
try {
await mx.leave(roomId);
appDispatcher.dispatch({
type: cons.actions.room.LEAVE,
roomId,
isDM,
});
} catch {
console.error('Unable to leave room.');
}
mx.leave(roomId)
.then(() => {
appDispatcher.dispatch({
type: cons.actions.room.LEAVE,
roomId,
isDM,
});
}).catch();
}
async function create(options, isDM = false) {
+15 -46
View File
@@ -2,60 +2,25 @@ import { openSearch, toggleRoomSettings } from '../action/navigation';
import navigation from '../state/navigation';
import { markAsRead } from '../action/notifications';
function shouldFocusMessageField(code) {
// do not focus on F keys
if (/^F\d+$/.test(code)) return false;
// do not focus on numlock/scroll lock
if (
code.metaKey
|| code.startsWith('OS')
|| code.startsWith('Meta')
|| code.startsWith('Shift')
|| code.startsWith('Alt')
|| code.startsWith('Control')
|| code.startsWith('Arrow')
|| code === 'Tab'
|| code === 'Space'
|| code === 'Enter'
|| code === 'NumLock'
|| code === 'ScrollLock'
) {
return false;
}
return true;
}
function listenKeyboard(event) {
// Ctrl/Cmd +
if (event.ctrlKey || event.metaKey) {
// open search modal
if (event.code === 'KeyK') {
// k - for search Modal
if (event.keyCode === 75) {
event.preventDefault();
if (navigation.isRawModalVisible) return;
openSearch();
}
// focus message field on paste
if (event.code === 'KeyV') {
if (navigation.isRawModalVisible) return;
const msgTextarea = document.getElementById('message-textarea');
const { activeElement } = document;
if (activeElement !== msgTextarea
&& ['input', 'textarea'].includes(activeElement.tagName.toLowerCase())
) return;
msgTextarea?.focus();
}
}
if (!event.ctrlKey && !event.altKey && !event.metaKey) {
if (!event.ctrlKey && !event.altKey) {
if (navigation.isRawModalVisible) return;
if (['input', 'textarea'].includes(document.activeElement.tagName.toLowerCase())) {
if (['text', 'textarea'].includes(document.activeElement.type)) {
return;
}
if (event.code === 'Escape') {
// esc
if (event.keyCode === 27) {
if (navigation.isRoomSettings) {
toggleRoomSettings();
return;
@@ -66,12 +31,16 @@ function listenKeyboard(event) {
}
}
// focus the text field on most keypresses
if (shouldFocusMessageField(event.code)) {
// press any key to focus and type in message field
const msgTextarea = document.getElementById('message-textarea');
msgTextarea?.focus();
// Don't allow these keys to type/focus message field
if ((event.keyCode !== 8 && event.keyCode < 48)
|| (event.keyCode >= 91 && event.keyCode <= 93)
|| (event.keyCode >= 112 && event.keyCode <= 183)) {
return;
}
// press any key to focus and type in message field
const msgTextarea = document.getElementById('message-textarea');
msgTextarea?.focus();
}
}
-3
View File
@@ -38,9 +38,6 @@ class InitMatrix extends EventEmitter {
deviceId: secret.deviceId,
timelineSupport: true,
cryptoCallbacks,
verificationMethods: [
'm.sas.v1',
],
});
await this.matrixClient.initCrypto();
+5 -2
View File
@@ -6,6 +6,9 @@ import cons from './cons';
import navigation from './navigation';
import settings from './settings';
import NotificationSound from '../../../public/sound/notification.ogg';
import InviteSound from '../../../public/sound/invite.ogg';
function isNotifEvent(mEvent) {
const eType = mEvent.getType();
if (!cons.supportEventTypes.includes(eType)) return false;
@@ -235,14 +238,14 @@ class Notifications extends EventEmitter {
_playNotiSound() {
if (!this._notiAudio) {
this._notiAudio = document.getElementById('notificationSound');
this._notiAudio = new Audio(NotificationSound);
}
this._notiAudio.play();
}
_playInviteSound() {
if (!this._inviteAudio) {
this._inviteAudio = document.getElementById('inviteSound');
this._inviteAudio = new Audio(InviteSound);
}
this._inviteAudio.play();
}
+94 -58
View File
@@ -6,21 +6,6 @@ function isMEventSpaceChild(mEvent) {
return mEvent.getType() === 'm.space.child' && Object.keys(mEvent.getContent()).length > 0;
}
/**
* @param {() => boolean} callback if return true wait will over else callback will be called again.
* @param {number} timeout timeout to callback
* @param {number} maxTry maximum callback try > 0. -1 means no limit
*/
async function waitFor(callback, timeout = 400, maxTry = -1) {
if (maxTry === 0) return false;
const isOver = async () => new Promise((resolve) => {
setTimeout(() => resolve(callback()), timeout);
});
if (await isOver()) return true;
return waitFor(callback, timeout, maxTry - 1);
}
class RoomList extends EventEmitter {
constructor(matrixClient) {
super();
@@ -243,7 +228,6 @@ class RoomList extends EventEmitter {
}
_isDMInvite(room) {
if (this.mDirects.has(room.roomId)) return true;
const me = room.getMember(this.matrixClient.getUserId());
const myEventContent = me.events.member.getContent();
return myEventContent.membership === 'invite' && myEventContent.is_direct;
@@ -259,11 +243,22 @@ class RoomList extends EventEmitter {
latestMDirects.forEach((directId) => {
const myRoom = this.matrixClient.getRoom(directId);
if (this.mDirects.has(directId)) return;
// Update mDirects
this.mDirects.add(directId);
if (myRoom === null) return;
if (myRoom.getMyMembership() === 'join') {
if (this._isDMInvite(myRoom)) return;
if (myRoom.getMyMembership === 'join' && !this.directs.has(directId)) {
this.directs.add(directId);
}
// Newly added room.
// at this time my membership can be invite | join
if (myRoom.getMyMembership() === 'join' && this.rooms.has(directId)) {
// found a DM which accidentally gets added to this.rooms
this.rooms.delete(directId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
@@ -303,17 +298,23 @@ class RoomList extends EventEmitter {
}
});
this.matrixClient.on('Room.myMembership', async (room, membership, prevMembership) => {
this.matrixClient.on('Room.myMembership', (room, membership, prevMembership) => {
// room => prevMembership = null | invite | join | leave | kick | ban | unban
// room => membership = invite | join | leave | kick | ban | unban
const { roomId } = room;
const isRoomReady = () => this.matrixClient.getRoom(roomId) !== null;
if (['join', 'invite'].includes(membership) && isRoomReady() === false) {
if (await waitFor(isRoomReady, 200, 100) === false) return;
}
if (membership === 'unban') return;
// When user_reject/sender_undo room invite
if (prevMembership === 'invite') {
if (this.inviteDirects.has(roomId)) this.inviteDirects.delete(roomId);
else if (this.inviteSpaces.has(roomId)) this.inviteSpaces.delete(roomId);
else this.inviteRooms.delete(roomId);
this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId);
}
// When user get invited
if (membership === 'invite') {
if (this._isDMInvite(room)) this.inviteDirects.add(roomId);
else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId);
@@ -323,53 +324,88 @@ class RoomList extends EventEmitter {
return;
}
if (prevMembership === 'invite') {
if (this.inviteDirects.has(roomId)) this.inviteDirects.delete(roomId);
else if (this.inviteSpaces.has(roomId)) this.inviteSpaces.delete(roomId);
else this.inviteRooms.delete(roomId);
// When user join room (first time) or start DM.
if ((prevMembership === null || prevMembership === 'invite') && membership === 'join') {
// when user create room/DM OR accept room/dm invite from this client.
// we will update this.rooms/this.directs with user action
if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return;
this.emit(cons.events.roomList.INVITELIST_UPDATED, roomId);
if (this.processingRooms.has(roomId)) {
const procRoomInfo = this.processingRooms.get(roomId);
if (procRoomInfo.isDM) this.directs.add(roomId);
else if (room.isSpaceRoom()) this.addToSpaces(roomId);
else this.rooms.add(roomId);
if (procRoomInfo.task === 'CREATE') this.emit(cons.events.roomList.ROOM_CREATED, roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
this.processingRooms.delete(roomId);
return;
}
if (room.isSpaceRoom()) {
this.addToSpaces(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
// below code intented to work when user create room/DM
// OR accept room/dm invite from other client.
// and we have to update our client. (it's ok to have 10sec delay)
// create a buffer of 10sec and HOPE client.accoundData get updated
// then accoundData event listener will update this.mDirects.
// and we will be able to know if it's a DM.
// ----------
// less likely situation:
// if we don't get accountData with 10sec then:
// we will temporary add it to this.rooms.
// and in future when accountData get updated
// accountData listener will automatically goona REMOVE it from this.rooms
// and will ADD it to this.directs
// and emit the cons.events.roomList.ROOMLIST_UPDATED to update the UI.
setTimeout(() => {
if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return;
if (this.mDirects.has(roomId)) this.directs.add(roomId);
else this.rooms.add(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}, 10000);
return;
}
if (['leave', 'kick', 'ban'].includes(membership)) {
if (this.directs.has(roomId)) this.directs.delete(roomId);
else if (this.spaces.has(roomId)) this.deleteFromSpaces(roomId);
// when room is a DM add/remove it from DM's and return.
if (this.directs.has(roomId)) {
if (membership === 'leave' || membership === 'kick' || membership === 'ban') {
this.directs.delete(roomId);
this.emit(cons.events.roomList.ROOM_LEAVED, roomId);
}
}
if (this.mDirects.has(roomId)) {
if (membership === 'join') {
this.directs.add(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
}
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
// when room is not a DM add/remove it from rooms.
if (membership === 'leave' || membership === 'kick' || membership === 'ban') {
if (room.isSpaceRoom()) this.deleteFromSpaces(roomId);
else this.rooms.delete(roomId);
this.emit(cons.events.roomList.ROOM_LEAVED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
// when user create room/DM OR accept room/dm invite from this client.
// we will update this.rooms/this.directs with user action
if (membership === 'join' && this.processingRooms.has(roomId)) {
const procRoomInfo = this.processingRooms.get(roomId);
if (procRoomInfo.isDM) this.directs.add(roomId);
else if (room.isSpaceRoom()) this.addToSpaces(roomId);
else this.rooms.add(roomId);
if (procRoomInfo.task === 'CREATE') this.emit(cons.events.roomList.ROOM_CREATED, roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
this.processingRooms.delete(roomId);
return;
}
if (this.mDirects.has(roomId) && membership === 'join') {
this.directs.add(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
if (membership === 'join') {
if (room.isSpaceRoom()) this.addToSpaces(roomId);
else this.rooms.add(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
});
}
}
+3 -4
View File
@@ -2,9 +2,8 @@ import EventEmitter from 'events';
import { micromark } from 'micromark';
import { gfm, gfmHtml } from 'micromark-extension-gfm';
import encrypt from 'browser-encrypt-attachment';
import { math } from 'micromark-extension-math';
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
import { mathExtensionHtml, spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
import { spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
import cons from './cons';
import settings from './settings';
@@ -86,8 +85,8 @@ function getVideoThumbnail(video, width, height, mimeType) {
function getFormattedBody(markdown) {
const result = micromark(markdown, {
extensions: [gfm(), spoilerExtension(), math()],
htmlExtensions: [gfmHtml(), spoilerExtensionHtml, mathExtensionHtml],
extensions: [gfm(), spoilerExtension()],
htmlExtensions: [gfmHtml(), spoilerExtensionHtml],
});
const bodyParts = result.match(/^(<p>)(.*)(<\/p>)$/);
if (bodyParts === null) return result;
+1 -5
View File
@@ -1,5 +1,5 @@
const cons = {
version: '2.0.3',
version: '1.8.2',
secretKey: {
ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id',
@@ -38,7 +38,6 @@ const cons = {
OPEN_INVITE_LIST: 'OPEN_INVITE_LIST',
OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS',
OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM',
OPEN_JOIN_ALIAS: 'OPEN_JOIN_ALIAS',
OPEN_INVITE_USER: 'OPEN_INVITE_USER',
OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER',
OPEN_SETTINGS: 'OPEN_SETTINGS',
@@ -50,7 +49,6 @@ const cons = {
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
OPEN_NAVIGATION: 'OPEN_NAVIGATION',
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
OPEN_EMOJI_VERIFICATION: 'OPEN_EMOJI_VERIFICATION',
},
room: {
JOIN: 'JOIN',
@@ -87,7 +85,6 @@ const cons = {
INVITE_LIST_OPENED: 'INVITE_LIST_OPENED',
PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED',
CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
JOIN_ALIAS_OPENED: 'JOIN_ALIAS_OPENED',
INVITE_USER_OPENED: 'INVITE_USER_OPENED',
SETTINGS_OPENED: 'SETTINGS_OPENED',
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
@@ -99,7 +96,6 @@ const cons = {
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
NAVIGATION_OPENED: 'NAVIGATION_OPENED',
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
EMOJI_VERIFICATION_OPENED: 'EMOJI_VERIFICATION_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
+2 -20
View File
@@ -14,7 +14,7 @@ class Navigation extends EventEmitter {
this.isRoomSettings = false;
this.recentRooms = [];
this.rawModelStack = [];
this.isRawModalVisible = false;
}
_setSpacePath(roomId) {
@@ -47,13 +47,8 @@ class Navigation extends EventEmitter {
}
}
get isRawModalVisible() {
return this.rawModelStack.length > 0;
}
setIsRawModalVisible(visible) {
if (visible) this.rawModelStack.push(true);
else this.rawModelStack.pop();
this.isRawModalVisible = visible;
}
navigate(action) {
@@ -127,12 +122,6 @@ class Navigation extends EventEmitter {
action.parentId,
);
},
[cons.actions.navigation.OPEN_JOIN_ALIAS]: () => {
this.emit(
cons.events.navigation.JOIN_ALIAS_OPENED,
action.term,
);
},
[cons.actions.navigation.OPEN_INVITE_USER]: () => {
this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm);
},
@@ -196,13 +185,6 @@ class Navigation extends EventEmitter {
action.afterClose,
);
},
[cons.actions.navigation.OPEN_EMOJI_VERIFICATION]: () => {
this.emit(
cons.events.navigation.EMOJI_VERIFICATION_OPENED,
action.request,
action.targetDevice,
);
},
};
actions[action.type]?.();
}
-4
View File
@@ -475,10 +475,6 @@ textarea {
supported by Chrome, Edge, Opera and Firefox */
}
audio:not([controls]) {
display: none !important;
}
.flex--center {
display: flex;
justify-content: center;
+1 -56
View File
@@ -140,59 +140,4 @@ const spoilerExtensionHtml = {
},
};
const mathExtensionHtml = {
enter: {
mathFlow() {
this.lineEndingIfNeeded();
},
mathFlowFenceMeta() {
this.buffer();
},
mathText() {
this.buffer();
},
},
exit: {
mathFlow() {
const value = this.encode(this.resume().replace(/(?:\r?\n|\r)$/, ''));
this.tag('<div data-mx-maths="');
this.tag(value);
this.tag('"><code>');
this.raw(value);
this.tag('</code></div>');
this.setData('mathFlowOpen');
this.setData('slurpOneLineEnding');
},
mathFlowFence() {
// After the first fence.
if (!this.getData('mathFlowOpen')) {
this.setData('mathFlowOpen', true);
this.setData('slurpOneLineEnding', true);
this.buffer();
}
},
mathFlowFenceMeta() {
this.resume();
},
mathFlowValue(token) {
this.raw(this.sliceSerialize(token));
},
mathText() {
const value = this.encode(this.resume());
this.tag('<span data-mx-maths="');
this.tag(value);
this.tag('"><code>');
this.raw(value);
this.tag('</code></span>');
},
mathTextData(token) {
this.raw(this.sliceSerialize(token));
},
},
};
export {
inlineExtension,
spoilerExtension, spoilerExtensionHtml,
mathExtensionHtml,
};
export { inlineExtension, spoilerExtension, spoilerExtensionHtml };
+1 -2
View File
@@ -171,8 +171,7 @@ export function isCrossVerified(deviceId) {
const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true);
return deviceTrust.isCrossSigningVerified();
} catch {
// device does not support encryption
return null;
return false;
}
}
+1 -2
View File
@@ -15,8 +15,7 @@ const urlSchemes = ['https', 'http', 'ftp', 'mailto', 'magnet'];
const permittedTagToAttributes = {
font: ['style', 'data-mx-bg-color', 'data-mx-color', 'color'],
span: ['style', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'data-mx-maths', 'data-mx-pill', 'data-mx-ping'],
div: ['data-mx-maths'],
span: ['style', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'data-mx-pill', 'data-mx-ping'],
a: ['name', 'target', 'href', 'rel'],
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
o: ['start'],
@@ -1,41 +1,17 @@
/* eslint-disable import/prefer-default-export */
import React, { lazy, Suspense } from 'react';
import linkifyHtml from 'linkifyjs/html';
import parse from 'html-react-parser';
import twemoji from 'twemoji';
import { sanitizeText } from './sanitize';
const Math = lazy(() => import('../app/atoms/math/Math'));
const mathOptions = {
replace: (node) => {
const maths = node.attribs?.['data-mx-maths'];
if (maths) {
return (
<Suspense fallback={<code>{maths}</code>}>
<Math
content={maths}
throwOnError={false}
errorColor="var(--tc-danger-normal)"
displayMode={node.name === 'div'}
/>
</Suspense>
);
}
return null;
},
};
/**
* @param {string} text - text to twemojify
* @param {object|undefined} opts - options for tweomoji.parse
* @param {boolean} [linkify=false] - convert links to html tags (default: false)
* @param {boolean} [sanitize=true] - sanitize html text (default: true)
* @param {boolean} [maths=false] - render maths (default: false)
* @returns React component
*/
export function twemojify(text, opts, linkify = false, sanitize = true, maths = false) {
export function twemojify(text, opts, linkify = false, sanitize = true) {
if (typeof text !== 'string') return text;
let content = text;
@@ -49,5 +25,5 @@ export function twemojify(text, opts, linkify = false, sanitize = true, maths =
rel: 'noreferrer noopener',
});
}
return parse(content, maths ? mathOptions : null);
return parse(content);
}