Compare commits

...

45 Commits

Author SHA1 Message Date
Ajay Bura
a6fdf9010b v2.0.2 2022-05-14 09:38:58 +05:30
Ajay Bura
941dae0625 Remove globally exposed var 2022-05-14 08:28:31 +05:30
Ajay Bura
4a715bfd17 Fix pasting not working #551 2022-05-14 08:24:21 +05:30
Ajay Bura
0b70c7e490 v2.0.1 2022-05-13 16:39:54 +05:30
Ajay Bura
0539836714 Fix space and enter focus message field 2022-05-13 15:38:18 +05:30
Ash
c08b0e654b Add allowCustomHomeservers config option (#525)
* feat: Add allowCustomHomeservers config option

* fix: Do not lock the homeserver input when the selection is changed
2022-05-12 17:13:14 +05:30
Dean Bassett
b3cb48319a Add the ability to focus on paste (#545)
* pasting should focus the message field

also refactored a small amount to use KeyEvent.code
instead of KeyEvent.keyCode, which is deprecated.

fixes ajbura/cinny#544

* fix lint

* comments
2022-05-12 16:58:19 +05:30
Ajay Bura
44553cc375 Fix crash in room without create state event (#546) 2022-05-12 16:32:39 +05:30
Ajay Bura
fbe287a702 Fix message edit isn't reflected in reply #421 2022-05-12 13:45:23 +05:30
Ajay Bura
5863dcdf67 Fix join with alias (#533) 2022-05-11 20:56:49 +05:30
Ajay Bura
f77bee25ef Remove forget room on leave 2022-05-11 20:53:21 +05:30
Ajay Bura
c11328a064 Fix crash on leaving room (#532) 2022-05-11 20:25:54 +05:30
Ajay Bura
d04de2fba0 Add badges 2022-05-08 13:52:05 +05:30
Ajay Bura
d2b435618c v2.0.0 2022-05-08 13:23:31 +05:30
Ajay Bura
7525bb78e5 Fix emoji verificaition not working with some client 2022-05-08 12:26:31 +05:30
Ajay Bura
2075a572fe Fixed cinny verified device failed to verify other 2022-05-08 11:55:41 +05:30
Ajay Bura
73723ba6ba Fix own cross siging trust before verification without key #514 2022-05-07 09:50:29 +05:30
Ajay Bura
0791820a6c Merge branch 'dev' of https://github.com/ajbura/cinny into dev 2022-05-05 19:58:29 +05:30
Ajay Bura
931f352873 Fix space path visible in DM's 2022-05-05 19:58:16 +05:30
dependabot[bot]
7c7d2e0fa4 Bump webpack-dev-server from 4.8.1 to 4.9.0 (#524)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.8.1 to 4.9.0.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.8.1...v4.9.0)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 10:47:11 +05:30
Ajay Bura
3372fb6f74 Fix public room showing leaved room as joined 2022-05-04 14:54:43 +05:30
Ajay Bura
bc856269ff Merge branch 'dev' of https://github.com/ajbura/cinny into dev 2022-05-04 14:22:20 +05:30
Ajay Bura
06bae231ef Fix bugs in dm tab 2022-05-04 14:22:16 +05:30
Rubin Elezi
65a0edc3a6 Don't enable e2ee for bridged platform (#476)
* Don't enable e2ee for bridged platform

* remove comments

* Change function name

Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
2022-05-04 10:58:30 +05:30
Ajay Bura
b7c322d473 Sign release tarball with PGP key (#392) 2022-05-03 16:43:16 +05:30
dependabot[bot]
0776a04362 Bump sass from 1.50.1 to 1.51.0 (#522)
Bumps [sass](https://github.com/sass/dart-sass) from 1.50.1 to 1.51.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.50.1...1.51.0)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 16:02:14 +05:30
Ajay Bura
e51fc5a585 Add join with address option (#420, #447) 2022-05-03 16:01:50 +05:30
Ajay Bura
3afc068a02 Fixes #430, #434, #455 2022-05-03 14:05:56 +05:30
Ajay Bura
5cdad44abf Load sound file on startup (#444) 2022-05-03 13:18:27 +05:30
dependabot[bot]
43762df998 Bump @babel/core from 7.17.9 to 7.17.10 (#521)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.9 to 7.17.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.10/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 13:07:51 +05:30
dependabot[bot]
95228c6dd9 Bump @babel/preset-env from 7.16.11 to 7.17.10 (#520)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.11 to 7.17.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.10/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 13:02:49 +05:30
dependabot[bot]
205fcf8487 Bump @fontsource/inter from 4.5.7 to 4.5.10 (#519)
Bumps [@fontsource/inter](https://github.com/fontsource/fontsource/tree/HEAD/fonts/google/inter) from 4.5.7 to 4.5.10.
- [Release notes](https://github.com/fontsource/fontsource/releases)
- [Changelog](https://github.com/fontsource/fontsource/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/fontsource/commits/HEAD/fonts/google/inter)

---
updated-dependencies:
- dependency-name: "@fontsource/inter"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 13:00:30 +05:30
dependabot[bot]
336e8921ee Bump react-modal from 3.14.4 to 3.15.1 (#518)
Bumps [react-modal](https://github.com/reactjs/react-modal) from 3.14.4 to 3.15.1.
- [Release notes](https://github.com/reactjs/react-modal/releases)
- [Changelog](https://github.com/reactjs/react-modal/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reactjs/react-modal/compare/v3.14.4...v3.15.1)

---
updated-dependencies:
- dependency-name: react-modal
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 12:58:25 +05:30
dependabot[bot]
ef149b9fcf Bump matrix-js-sdk from 17.0.0 to 17.1.0 (#517)
Bumps [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk) from 17.0.0 to 17.1.0.
- [Release notes](https://github.com/matrix-org/matrix-js-sdk/releases)
- [Changelog](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/matrix-org/matrix-js-sdk/compare/v17.0.0...v17.1.0)

---
updated-dependencies:
- dependency-name: matrix-js-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 12:56:42 +05:30
dependabot[bot]
766b4c13c3 Bump eslint-plugin-react-hooks from 4.4.0 to 4.5.0 (#516)
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 12:54:22 +05:30
Ajay Bura
f5605258e3 Merge branch 'dev' of https://github.com/ajbura/cinny into dev 2022-05-03 12:52:33 +05:30
Ajay Bura
2ba4d2f2b7 Bug fixes in emoji verificaiton 2022-05-03 12:52:26 +05:30
dependabot[bot]
2e050c066e Bump docker/metadata-action from 3.7.0 to 3.8.0 (#523)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 3.7.0 to 3.8.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v3.7.0...v3.8.0)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-03 12:36:14 +05:30
Ajay Bura
3f83514427 Fix #514 2022-05-01 20:56:30 +05:30
Ajay Bura
8c227843c9 Show error on wrong security key 2022-05-01 17:40:47 +05:30
Ajay Bura
ba084c0a10 Fix key backup not working without phrase 2022-05-01 17:32:29 +05:30
Ajay Bura
3fdd42706d Fix branch name in readme 2022-05-01 13:38:31 +05:30
Ajay Bura
b49b51a671 Fix link to screenshot 2022-05-01 13:37:29 +05:30
Ajay Bura
e5bb386dd2 Use SHA instead of tag for 3rd party actions (#498) 2022-05-01 13:23:42 +05:30
Ajay Bura
2867bb3bc3 Session verification by emojis (#513)
* Add option to verify with security key/phrase

* Manually merge #311 by @ginnyTheCat
2022-05-01 13:22:55 +05:30
38 changed files with 1115 additions and 570 deletions

View File

@@ -56,7 +56,7 @@ jobs:
console.log(`::set-output name=prnumber::${pr.number}`); console.log(`::set-output name=prnumber::${pr.number}`);
- name: Deploy to Netlify - name: Deploy to Netlify
id: netlify id: netlify
uses: nwtgck/actions-netlify@v1.2.3 uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99
with: with:
publish-dir: dist publish-dir: dist
deploy-message: "Deploy from GitHub Actions" deploy-message: "Deploy from GitHub Actions"
@@ -68,7 +68,7 @@ jobs:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
timeout-minutes: 1 timeout-minutes: 1
- name: Edit PR Description - name: Edit PR Description
uses: Beakyn/gha-comment-pull-request@v1.0.2 uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:

View File

@@ -14,7 +14,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Build and deploy to Netlify - name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v1.7.2 uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
with: with:
install_command: "npm ci" install_command: "npm ci"
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -12,7 +12,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Build and deploy to Netlify - name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v1.7.2 uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
with: with:
install_command: "npm ci" install_command: "npm ci"
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
@@ -25,11 +25,19 @@ jobs:
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
- name: Create tar.gz - name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Sign tar.gz
uses: actionhippie/gpgsign@4e28208b142cae93e1582401dcda1cf79e4f72c0
with:
private_key: ${{ secrets.GNUPG_KEY }}
passphrase: ${{ secrets.GNUPG_PASSPHRASE }}
detach_sign: true
files: cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release - name: Upload tagged release
uses: softprops/action-gh-release@v0.1.14 uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5
with: with:
files: | files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
push_to_dockerhub: push_to_dockerhub:
name: Push Docker image to Docker Hub name: Push Docker image to Docker Hub
@@ -44,7 +52,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v3.7.0 uses: docker/metadata-action@v3.8.0
with: with:
images: ajbura/cinny images: ajbura/cinny
- name: Build and push Docker image - name: Build and push Docker image

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021 Ajay Bura (ajbura) and contributors Copyright (c) 2021 Ajay Bura (ajbura)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,5 +1,10 @@
# Cinny # Cinny
[![Star](https://img.shields.io/github/stars/ajbura/cinny)](https://github.com/ajbura/cinny/tree/dev)
[![Chat](https://img.shields.io/badge/chat-on%20matrix-orange)](https://matrix.to/#/#cinny:matrix.org)
[![Twitter](https://img.shields.io/twitter/url?url=https://twitter.com/@cinnyapp)](https://twitter.com/@cinnyapp)
[![Support](https://img.shields.io/badge/sponsor-open%20collective-blue.svg)](https://opencollective.com/cinny)
## Table of Contents ## Table of Contents
- [About](#about) - [About](#about)
@@ -11,7 +16,7 @@
Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, elegant and secure interface. 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) ![preview](https://github.com/cinnyapp/cinny-site/blob/main/assets/preview-light.png)
## Building and Running ## Building and Running
@@ -59,7 +64,7 @@ To set default Homeserver on login and register page, place a customized [`confi
## License ## License
Copyright (c) 2021 Ajay Bura (ajbura) and contributors Copyright (c) 2021 Ajay Bura (ajbura)
Code licensed under the MIT License: <http://opensource.org/licenses/MIT> Code licensed under the MIT License: <http://opensource.org/licenses/MIT>

View File

@@ -7,5 +7,6 @@
"kde.org", "kde.org",
"matrix.org", "matrix.org",
"chat.mozilla.org" "chat.mozilla.org"
] ],
"allowCustomHomeservers": true
} }

685
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "1.8.2", "version": "2.0.2",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
@@ -15,7 +15,7 @@
"author": "Ajay Bura", "author": "Ajay Bura",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource/inter": "^4.5.7", "@fontsource/inter": "^4.5.10",
"@fontsource/roboto": "^4.5.5", "@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", "@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", "@tippyjs/react": "^4.2.6",
@@ -29,7 +29,7 @@
"html-react-parser": "^1.4.12", "html-react-parser": "^1.4.12",
"katex": "^0.15.3", "katex": "^0.15.3",
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"matrix-js-sdk": "^17.0.0", "matrix-js-sdk": "^17.1.0",
"micromark": "^3.0.10", "micromark": "^3.0.10",
"micromark-extension-gfm": "^2.0.1", "micromark-extension-gfm": "^2.0.1",
"micromark-extension-math": "^2.0.2", "micromark-extension-math": "^2.0.2",
@@ -43,14 +43,14 @@
"react-dnd-html5-backend": "^15.1.3", "react-dnd-html5-backend": "^15.1.3",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-google-recaptcha": "^2.1.0", "react-google-recaptcha": "^2.1.0",
"react-modal": "^3.14.4", "react-modal": "^3.15.1",
"sanitize-html": "^2.7.0", "sanitize-html": "^2.7.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"twemoji": "^14.0.2" "twemoji": "^14.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.17.9", "@babel/core": "^7.17.10",
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.17.10",
"@babel/preset-react": "^7.16.7", "@babel/preset-react": "^7.16.7",
"assert": "^2.0.0", "assert": "^2.0.0",
"babel-loader": "^8.2.5", "babel-loader": "^8.2.5",
@@ -66,14 +66,14 @@
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.29.4", "eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0", "eslint-plugin-react-hooks": "^4.5.0",
"favicons": "^6.2.2", "favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2", "favicons-webpack-plugin": "^5.0.2",
"html-loader": "^3.1.0", "html-loader": "^3.1.0",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"sass": "^1.50.1", "sass": "^1.51.0",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
@@ -81,7 +81,7 @@
"util": "^0.12.4", "util": "^0.12.4",
"webpack": "^5.72.0", "webpack": "^5.72.0",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1", "webpack-dev-server": "^4.9.0",
"webpack-merge": "^5.7.3" "webpack-merge": "^5.7.3"
} }
} }

View File

@@ -18,5 +18,11 @@
</head> </head>
<body id="appBody"> <body id="appBody">
<div id="root"></div> <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> </body>
</html> </html>

View File

@@ -123,17 +123,26 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
const eTimeline = await mx.getEventTimeline(timelineSet, eventId); const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
await roomTimeline.decryptAllEventsOfTimeline(eTimeline); await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
const mEvent = eTimeline.getTimelineSet().findEventById(eventId); let mEvent = eTimeline.getTimelineSet().findEventById(eventId);
const editedList = roomTimeline.editedTimeline.get(mEvent.getId());
if (editedList) {
mEvent = editedList[editedList.length - 1];
}
const rawBody = mEvent.getContent().body; const rawBody = mEvent.getContent().body;
const username = getUsernameOfRoomMember(mEvent.sender); const username = getUsernameOfRoomMember(mEvent.sender);
if (isMountedRef.current === false) return; if (isMountedRef.current === false) return;
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***'; 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({ setReply({
to: username, to: username,
color: colorMXID(mEvent.getSender()), color: colorMXID(mEvent.getSender()),
body: parseReply(rawBody)?.body ?? rawBody ?? fallbackBody, body: parsedBody,
event: mEvent, event: mEvent,
}); });
} catch { } catch {

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './RoomIntro.scss'; import './RoomIntro.scss';
import { twemojify } from '../../../util/twemojify';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
@@ -15,8 +14,8 @@ function RoomIntro({
<div className="room-intro"> <div className="room-intro">
<Avatar imageSrc={avatarSrc} text={name} bgColor={colorMXID(roomId)} size="large" /> <Avatar imageSrc={avatarSrc} text={name} bgColor={colorMXID(roomId)} size="large" />
<div className="room-intro__content"> <div className="room-intro__content">
<Text className="room-intro__name" variant="h1" weight="medium" primary>{twemojify(heading)}</Text> <Text className="room-intro__name" variant="h1" weight="medium" primary>{heading}</Text>
<Text className="room-intro__desc" variant="b1">{twemojify(desc, undefined, true)}</Text> <Text className="room-intro__desc" variant="b1">{desc}</Text>
{ time !== null && <Text className="room-intro__time" variant="b3">{time}</Text>} { time !== null && <Text className="room-intro__time" variant="b3">{time}</Text>}
</div> </div>
</div> </div>
@@ -35,9 +34,9 @@ RoomIntro.propTypes = {
PropTypes.bool, PropTypes.bool,
]), ]),
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
heading: PropTypes.string.isRequired, heading: PropTypes.node.isRequired,
desc: PropTypes.string.isRequired, desc: PropTypes.node.isRequired,
time: PropTypes.string, time: PropTypes.node,
}; };
export default RoomIntro; export default RoomIntro;

View File

@@ -70,7 +70,7 @@ function RoomVisibility({ roomId }) {
const noSpaceParent = currentState.getStateEvents('m.space.parent').length === 0; const noSpaceParent = currentState.getStateEvents('m.space.parent').length === 0;
const mCreate = currentState.getStateEvents('m.room.create')[0]?.getContent(); const mCreate = currentState.getStateEvents('m.room.create')[0]?.getContent();
const roomVersion = Number(mCreate.room_version); const roomVersion = Number(mCreate?.room_version ?? 0);
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0; const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel); const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);

View File

@@ -0,0 +1,200 @@
/* 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;

View File

@@ -0,0 +1,35 @@
@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);
}
}

View File

@@ -56,9 +56,10 @@ function InviteList({ isOpen, onRequestClose }) {
function renderRoomTile(roomId) { function renderRoomTile(roomId) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const myRoom = mx.getRoom(roomId); const myRoom = mx.getRoom(roomId);
if (!myRoom) return null;
const roomName = myRoom.name; const roomName = myRoom.name;
let roomAlias = myRoom.getCanonicalAlias(); let roomAlias = myRoom.getCanonicalAlias();
if (roomAlias === null) roomAlias = myRoom.roomId; if (!roomAlias) roomAlias = myRoom.roomId;
const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? ''; const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? '';
return ( return (
<RoomTile <RoomTile
@@ -97,12 +98,13 @@ function InviteList({ isOpen, onRequestClose }) {
{ {
Array.from(initMatrix.roomList.inviteDirects).map((roomId) => { Array.from(initMatrix.roomList.inviteDirects).map((roomId) => {
const myRoom = initMatrix.matrixClient.getRoom(roomId); const myRoom = initMatrix.matrixClient.getRoom(roomId);
if (myRoom === null) return null;
const roomName = myRoom.name; const roomName = myRoom.name;
return ( return (
<RoomTile <RoomTile
key={myRoom.roomId} key={myRoom.roomId}
name={roomName} name={roomName}
id={myRoom.getDMInviter()} id={myRoom.getDMInviter() || roomId}
options={ options={
procInvite.has(myRoom.roomId) procInvite.has(myRoom.roomId)
? (<Spinner size="small" />) ? (<Spinner size="small" />)

View File

@@ -103,6 +103,18 @@ function InviteUser({
updateIsSearching(false); 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) { async function createDM(userId) {
if (mx.getUserId() === userId) return; if (mx.getUserId() === userId) return;
const dmRoomId = hasDMWith(userId); const dmRoomId = hasDMWith(userId);
@@ -117,7 +129,7 @@ function InviteUser({
procUserError.delete(userId); procUserError.delete(userId);
updateUserProcError(getMapCopy(procUserError)); updateUserProcError(getMapCopy(procUserError));
const result = await roomActions.createDM(userId); const result = await roomActions.createDM(userId, await hasDevices(userId));
roomIdToUserId.set(result.room_id, userId); roomIdToUserId.set(result.room_id, userId);
updateRoomIdToUserId(getMapCopy(roomIdToUserId)); updateRoomIdToUserId(getMapCopy(roomIdToUserId));
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,155 @@
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;

View File

@@ -0,0 +1,20 @@
@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);
}
}

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
@@ -9,12 +10,12 @@ import { roomIdByActivity } from '../../../util/sort';
import RoomsCategory from './RoomsCategory'; import RoomsCategory from './RoomsCategory';
const drawerPostie = new Postie(); const drawerPostie = new Postie();
function Directs() { function Directs({ size }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const { roomList, notifications } = initMatrix; const { roomList, notifications } = initMatrix;
const [directIds, setDirectIds] = useState([]); const [directIds, setDirectIds] = useState([]);
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), []); useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), [size]);
useEffect(() => { useEffect(() => {
const handleTimeline = (event, room, toStartOfTimeline, removed, data) => { const handleTimeline = (event, room, toStartOfTimeline, removed, data) => {
@@ -63,5 +64,8 @@ function Directs() {
return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />; return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />;
} }
Directs.propTypes = {
size: PropTypes.number.isRequired,
};
export default Directs; export default Directs;

View File

@@ -42,12 +42,15 @@ function Drawer() {
const [spaceId] = useSelectedSpace(); const [spaceId] = useSelectedSpace();
const [, forceUpdate] = useForceUpdate(); const [, forceUpdate] = useForceUpdate();
const scrollRef = useRef(null); const scrollRef = useRef(null);
const { roomList } = initMatrix;
useEffect(() => { useEffect(() => {
const { roomList } = initMatrix; const handleUpdate = () => {
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, forceUpdate); forceUpdate();
};
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate);
return () => { return () => {
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, forceUpdate); roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, handleUpdate);
}; };
}, []); }, []);
@@ -61,14 +64,16 @@ function Drawer() {
<div className="drawer"> <div className="drawer">
<DrawerHeader selectedTab={selectedTab} spaceId={spaceId} /> <DrawerHeader selectedTab={selectedTab} spaceId={spaceId} />
<div className="drawer__content-wrapper"> <div className="drawer__content-wrapper">
{navigation.selectedSpacePath.length > 1 && <DrawerBreadcrumb spaceId={spaceId} />} {navigation.selectedSpacePath.length > 1 && selectedTab !== cons.tabs.DIRECTS && (
<DrawerBreadcrumb spaceId={spaceId} />
)}
<div className="rooms__wrapper"> <div className="rooms__wrapper">
<ScrollView ref={scrollRef} autoHide> <ScrollView ref={scrollRef} autoHide>
<div className="rooms-container"> <div className="rooms-container">
{ {
selectedTab !== cons.tabs.DIRECTS selectedTab !== cons.tabs.DIRECTS
? <Home spaceId={spaceId} /> ? <Home spaceId={spaceId} />
: <Directs /> : <Directs size={roomList.directs.size} />
} }
</div> </div>
</ScrollView> </ScrollView>

View File

@@ -7,7 +7,7 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import { import {
openPublicRooms, openCreateRoom, openSpaceManage, openPublicRooms, openCreateRoom, openSpaceManage, openJoinAlias,
openSpaceAddExisting, openInviteUser, openReusableContextMenu, openSpaceAddExisting, openInviteUser, openReusableContextMenu,
} from '../../../client/action/navigation'; } from '../../../client/action/navigation';
import { getEventCords } from '../../../util/common'; import { getEventCords } from '../../../util/common';
@@ -60,6 +60,14 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
Join public room Join public room
</MenuItem> </MenuItem>
)} )}
{ !spaceId && (
<MenuItem
iconSrc={PlusIC}
onClick={() => { afterOptionSelect(); openJoinAlias(); }}
>
Join with address
</MenuItem>
)}
{ spaceId && ( { spaceId && (
<MenuItem <MenuItem
iconSrc={PlusIC} iconSrc={PlusIC}

View File

@@ -195,7 +195,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
return rooms.map((room) => { return rooms.map((room) => {
const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id; const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id;
const name = typeof room.name === 'string' ? room.name : alias; const name = typeof room.name === 'string' ? room.name : alias;
const isJoined = initMatrix.matrixClient.getRoom(room.room_id) !== null; const isJoined = initMatrix.matrixClient.getRoom(room.room_id)?.getMyMembership() === 'join';
return ( return (
<RoomTile <RoomTile
key={room.room_id} key={room.room_id}

View File

@@ -7,6 +7,8 @@ import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExistin
import Search from '../search/Search'; import Search from '../search/Search';
import ViewSource from '../view-source/ViewSource'; import ViewSource from '../view-source/ViewSource';
import CreateRoom from '../create-room/CreateRoom'; import CreateRoom from '../create-room/CreateRoom';
import JoinAlias from '../join-alias/JoinAlias';
import EmojiVerification from '../emoji-verification/EmojiVerification';
import ReusableDialog from '../../molecules/dialog/ReusableDialog'; import ReusableDialog from '../../molecules/dialog/ReusableDialog';
@@ -18,8 +20,10 @@ function Dialogs() {
<ProfileViewer /> <ProfileViewer />
<ShortcutSpaces /> <ShortcutSpaces />
<CreateRoom /> <CreateRoom />
<JoinAlias />
<SpaceAddExisting /> <SpaceAddExisting />
<Search /> <Search />
<EmojiVerification />
<ReusableDialog /> <ReusableDialog />
</> </>

View File

@@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
import './RoomViewContent.scss'; import './RoomViewContent.scss';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
@@ -50,21 +51,54 @@ function loadingMsgPlaceholders(key, count = 2) {
); );
} }
function genRoomIntro(mEvent, roomTimeline) { function RoomIntroContainer({ event, timeline }) {
const [, nameForceUpdate] = useForceUpdate();
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic; const { roomList } = initMatrix;
const isDM = initMatrix.roomList.directs.has(roomTimeline.roomId); const { room } = timeline;
let avatarSrc = roomTimeline.room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop'); const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
avatarSrc = isDM ? roomTimeline.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc; 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);
};
}, []);
return ( return (
<RoomIntro <RoomIntro
key={mEvent ? mEvent.getId() : 'room-intro'} roomId={timeline.roomId}
roomId={roomTimeline.roomId}
avatarSrc={avatarSrc} avatarSrc={avatarSrc}
name={roomTimeline.room.name} name={room.name}
heading={`Welcome to ${roomTimeline.room.name}`} heading={twemojify(heading)}
desc={`This is the beginning of the ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`} desc={desc}
time={mEvent ? `Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null} time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
/> />
); );
} }
@@ -199,7 +233,7 @@ function usePaginate(
}; };
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer); roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
return () => { return () => {
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer); roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
}; };
}, [roomTimeline]); }, [roomTimeline]);
@@ -470,12 +504,14 @@ function RoomViewContent({ eventId, roomTimeline }) {
if (i === 0 && !roomTimeline.canPaginateBackward()) { if (i === 0 && !roomTimeline.canPaginateBackward()) {
if (mEvent.getType() === 'm.room.create') { if (mEvent.getType() === 'm.room.create') {
tl.push(genRoomIntro(mEvent, roomTimeline)); tl.push(
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
);
itemCountIndex += 1; itemCountIndex += 1;
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
continue; continue;
} else { } else {
tl.push(genRoomIntro(undefined, roomTimeline)); tl.push(<RoomIntroContainer key="room-intro" event={null} timeline={roomTimeline} />);
itemCountIndex += 1; itemCountIndex += 1;
} }
} }

View File

@@ -4,7 +4,7 @@ import dateFormat from 'dateformat';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import { isCrossVerified } from '../../../util/matrixUtil'; import { isCrossVerified } from '../../../util/matrixUtil';
import { openReusableDialog } from '../../../client/action/navigation'; import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
@@ -25,6 +25,7 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useDeviceList } from '../../hooks/useDeviceList'; import { useDeviceList } from '../../hooks/useDeviceList';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
import { accessSecretStorage } from './SecretStorageAccess';
const promptDeviceName = async (deviceName) => new Promise((resolve) => { const promptDeviceName = async (deviceName) => new Promise((resolve) => {
let isCompleted = false; let isCompleted = false;
@@ -69,6 +70,7 @@ function DeviceManage() {
const [truncated, setTruncated] = useState(true); const [truncated, setTruncated] = useState(true);
const mountStore = useStore(); const mountStore = useStore();
mountStore.setItem(true); mountStore.setItem(true);
const isMeVerified = isCrossVerified(mx.deviceId);
useEffect(() => { useEffect(() => {
setProcessing([]); setProcessing([]);
@@ -127,18 +129,41 @@ function DeviceManage() {
removeFromProcessing(device); 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 renderDevice = (device, isVerified) => {
const deviceId = device.device_id; const deviceId = device.device_id;
const displayName = device.display_name; const displayName = device.display_name;
const lastIP = device.last_seen_ip; const lastIP = device.last_seen_ip;
const lastTS = device.last_seen_ts; const lastTS = device.last_seen_ts;
const isCurrentDevice = mx.deviceId === deviceId;
return ( return (
<SettingTile <SettingTile
key={deviceId} key={deviceId}
title={( title={(
<Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}> <Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
{displayName} {displayName}
<Text variant="b3" span>{`${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text> <Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
{isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
</Text> </Text>
)} )}
options={ options={
@@ -146,12 +171,14 @@ function DeviceManage() {
? <Spinner size="small" /> ? <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={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" /> <IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
</> </>
) )
} }
content={( content={(
<>
<Text variant="b3"> <Text variant="b3">
Last activity Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}> <span style={{ color: 'var(--tc-surface-normal)' }}>
@@ -159,6 +186,12 @@ function DeviceManage() {
</span> </span>
{lastIP ? ` at ${lastIP}` : ''} {lastIP ? ` at ${lastIP}` : ''}
</Text> </Text>
{isCurrentDevice && (
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
{`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
</Text>
)}
</>
)} )}
/> />
); );
@@ -200,7 +233,7 @@ function DeviceManage() {
{noEncryption.length > 0 && ( {noEncryption.length > 0 && (
<div> <div>
<MenuHeader>Sessions without encryption support</MenuHeader> <MenuHeader>Sessions without encryption support</MenuHeader>
{noEncryption.map((device) => renderDevice(device, true))} {noEncryption.map((device) => renderDevice(device, null))}
</div> </div>
)} )}
<div> <div>
@@ -211,7 +244,7 @@ function DeviceManage() {
if (truncated && index >= TRUNCATED_COUNT) return null; if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true); return renderDevice(device, true);
}) })
: <Text className="device-manage__info">No verified session</Text> : <Text className="device-manage__info">No verified sessions</Text>
} }
{ verified.length > TRUNCATED_COUNT && ( { verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}> <Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>

View File

@@ -15,6 +15,23 @@
& .setting-tile:last-of-type { & .setting-tile:last-of-type {
border-bottom: none; 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 { &__rename {
padding: var(--sp-normal); padding: var(--sp-normal);

View File

@@ -159,9 +159,9 @@ function DeleteKeyBackupDialog({ requestClose }) {
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const mountStore = useStore(); const mountStore = useStore();
mountStore.setItem(true);
const deleteBackup = async () => { const deleteBackup = async () => {
mountStore.setItem(true);
setIsDeleting(true); setIsDeleting(true);
try { try {
const backupInfo = await mx.getKeyBackupVersion(); const backupInfo = await mx.getKeyBackupVersion();

View File

@@ -24,14 +24,14 @@ function SecretStorageAccess({ onComplete }) {
const [process, setProcess] = useState(false); const [process, setProcess] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const mountStore = useStore(); const mountStore = useStore();
mountStore.setItem(true);
const toggleWithPhrase = () => setWithPhrase(!withPhrase); const toggleWithPhrase = () => setWithPhrase(!withPhrase);
const processInput = async ({ key, phrase }) => { const processInput = async ({ key, phrase }) => {
mountStore.setItem(true);
setProcess(true); setProcess(true);
try { try {
const { salt, iterations } = sSKeyInfo.passphrase; const { salt, iterations } = sSKeyInfo.passphrase || {};
const privateKey = key const privateKey = key
? mx.keyBackupKeyFromRecoveryKey(key) ? mx.keyBackupKeyFromRecoveryKey(key)
: await deriveKey(phrase, salt, iterations); : await deriveKey(phrase, salt, iterations);

View File

@@ -93,12 +93,13 @@ function Homeserver({ onChange }) {
const result = await (await fetch(configFileUrl, { method: 'GET' })).json(); const result = await (await fetch(configFileUrl, { method: 'GET' })).json();
const selectedHs = result?.defaultHomeserver; const selectedHs = result?.defaultHomeserver;
const hsList = result?.homeserverList; const hsList = result?.homeserverList;
const allowCustom = result?.allowCustomHomeservers ?? true;
if (!hsList?.length > 0 || selectedHs < 0 || selectedHs >= hsList?.length) { if (!hsList?.length > 0 || selectedHs < 0 || selectedHs >= hsList?.length) {
throw new Error(); throw new Error();
} }
setHs({ selected: hsList[selectedHs], list: hsList }); setHs({ selected: hsList[selectedHs], list: hsList, allowCustom: allowCustom });
} catch { } catch {
setHs({ selected: 'matrix.org', list: ['matrix.org'] }); setHs({ selected: 'matrix.org', list: ['matrix.org'], allowCustom: true });
} }
}, []); }, []);
@@ -106,14 +107,15 @@ function Homeserver({ onChange }) {
const { value } = e.target; const { value } = e.target;
setProcess({ isLoading: false }); setProcess({ isLoading: false });
debounce._(async () => { debounce._(async () => {
setHs({ selected: value.trim(), list: hs.list }); setHs({ ...hs, selected: value.trim() });
}, 700)(); }, 700)();
}; };
return ( return (
<> <>
<div className="homeserver-form"> <div className="homeserver-form">
<Input name="homeserver" onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} label="Homeserver" /> <Input name="homeserver" onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} label="Homeserver"
disabled={hs === null || !hs.allowCustom} />
<ContextMenu <ContextMenu
placement="right" placement="right"
content={(hideMenu) => ( content={(hideMenu) => (
@@ -126,7 +128,7 @@ function Homeserver({ onChange }) {
onClick={() => { onClick={() => {
hideMenu(); hideMenu();
hsRef.current.value = hsName; hsRef.current.value = hsName;
setHs({ selected: hsName, list: hs.list }); setHs({ ...hs, selected: hsName });
}} }}
> >
{hsName} {hsName}

View File

@@ -86,6 +86,13 @@ 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) { export function openInviteUser(roomId, searchTerm) {
appDispatcher.dispatch({ appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_INVITE_USER, type: cons.actions.navigation.OPEN_INVITE_USER,
@@ -166,3 +173,11 @@ export function openReusableDialog(title, render, afterClose) {
afterClose, afterClose,
}); });
} }
export function openEmojiVerification(request, targetDevice) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_EMOJI_VERIFICATION,
request,
targetDevice,
});
}

View File

@@ -113,17 +113,19 @@ async function join(roomIdOrAlias, isDM, via) {
* @param {string} roomId * @param {string} roomId
* @param {boolean} isDM * @param {boolean} isDM
*/ */
function leave(roomId) { async function leave(roomId) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const isDM = initMatrix.roomList.directs.has(roomId); const isDM = initMatrix.roomList.directs.has(roomId);
mx.leave(roomId) try {
.then(() => { await mx.leave(roomId);
appDispatcher.dispatch({ appDispatcher.dispatch({
type: cons.actions.room.LEAVE, type: cons.actions.room.LEAVE,
roomId, roomId,
isDM, isDM,
}); });
}).catch(); } catch {
console.error('Unable to leave room.');
}
} }
async function create(options, isDM = false) { async function create(options, isDM = false) {

View File

@@ -2,25 +2,57 @@ import { openSearch, toggleRoomSettings } from '../action/navigation';
import navigation from '../state/navigation'; import navigation from '../state/navigation';
import { markAsRead } from '../action/notifications'; 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) { function listenKeyboard(event) {
// Ctrl/Cmd + // Ctrl/Cmd +
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
// k - for search Modal // open search modal
if (event.keyCode === 75) { if (event.code === 'KeyK') {
event.preventDefault(); event.preventDefault();
if (navigation.isRawModalVisible) return; if (navigation.isRawModalVisible) return;
openSearch(); openSearch();
} }
// focus message field on paste
if (event.code === 'KeyV') {
if (navigation.isRawModalVisible) return;
const msgTextarea = document.getElementById('message-textarea');
if (document.activeElement !== msgTextarea && document.activeElement.tagName.toLowerCase() === 'input') return;
msgTextarea?.focus();
}
} }
if (!event.ctrlKey && !event.altKey) { if (!event.ctrlKey && !event.altKey && !event.metaKey) {
if (navigation.isRawModalVisible) return; if (navigation.isRawModalVisible) return;
if (['text', 'textarea'].includes(document.activeElement.type)) { if (document.activeElement.tagName.toLowerCase() === 'input') {
return; return;
} }
// esc if (event.code === 'Escape') {
if (event.keyCode === 27) {
if (navigation.isRoomSettings) { if (navigation.isRoomSettings) {
toggleRoomSettings(); toggleRoomSettings();
return; return;
@@ -31,18 +63,14 @@ function listenKeyboard(event) {
} }
} }
// Don't allow these keys to type/focus message field // focus the text field on most keypresses
if ((event.keyCode !== 8 && event.keyCode < 48) if (shouldFocusMessageField(event.code)) {
|| (event.keyCode >= 91 && event.keyCode <= 93)
|| (event.keyCode >= 112 && event.keyCode <= 183)) {
return;
}
// press any key to focus and type in message field // press any key to focus and type in message field
const msgTextarea = document.getElementById('message-textarea'); const msgTextarea = document.getElementById('message-textarea');
msgTextarea?.focus(); msgTextarea?.focus();
} }
} }
}
function initHotkeys() { function initHotkeys() {
document.body.addEventListener('keydown', listenKeyboard); document.body.addEventListener('keydown', listenKeyboard);

View File

@@ -38,6 +38,9 @@ class InitMatrix extends EventEmitter {
deviceId: secret.deviceId, deviceId: secret.deviceId,
timelineSupport: true, timelineSupport: true,
cryptoCallbacks, cryptoCallbacks,
verificationMethods: [
'm.sas.v1',
],
}); });
await this.matrixClient.initCrypto(); await this.matrixClient.initCrypto();

View File

@@ -6,9 +6,6 @@ import cons from './cons';
import navigation from './navigation'; import navigation from './navigation';
import settings from './settings'; import settings from './settings';
import NotificationSound from '../../../public/sound/notification.ogg';
import InviteSound from '../../../public/sound/invite.ogg';
function isNotifEvent(mEvent) { function isNotifEvent(mEvent) {
const eType = mEvent.getType(); const eType = mEvent.getType();
if (!cons.supportEventTypes.includes(eType)) return false; if (!cons.supportEventTypes.includes(eType)) return false;
@@ -238,14 +235,14 @@ class Notifications extends EventEmitter {
_playNotiSound() { _playNotiSound() {
if (!this._notiAudio) { if (!this._notiAudio) {
this._notiAudio = new Audio(NotificationSound); this._notiAudio = document.getElementById('notificationSound');
} }
this._notiAudio.play(); this._notiAudio.play();
} }
_playInviteSound() { _playInviteSound() {
if (!this._inviteAudio) { if (!this._inviteAudio) {
this._inviteAudio = new Audio(InviteSound); this._inviteAudio = document.getElementById('inviteSound');
} }
this._inviteAudio.play(); this._inviteAudio.play();
} }

View File

@@ -6,6 +6,21 @@ function isMEventSpaceChild(mEvent) {
return mEvent.getType() === 'm.space.child' && Object.keys(mEvent.getContent()).length > 0; 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 { class RoomList extends EventEmitter {
constructor(matrixClient) { constructor(matrixClient) {
super(); super();
@@ -228,6 +243,7 @@ class RoomList extends EventEmitter {
} }
_isDMInvite(room) { _isDMInvite(room) {
if (this.mDirects.has(room.roomId)) return true;
const me = room.getMember(this.matrixClient.getUserId()); const me = room.getMember(this.matrixClient.getUserId());
const myEventContent = me.events.member.getContent(); const myEventContent = me.events.member.getContent();
return myEventContent.membership === 'invite' && myEventContent.is_direct; return myEventContent.membership === 'invite' && myEventContent.is_direct;
@@ -243,22 +259,11 @@ class RoomList extends EventEmitter {
latestMDirects.forEach((directId) => { latestMDirects.forEach((directId) => {
const myRoom = this.matrixClient.getRoom(directId); const myRoom = this.matrixClient.getRoom(directId);
if (this.mDirects.has(directId)) return; if (this.mDirects.has(directId)) return;
// Update mDirects
this.mDirects.add(directId); this.mDirects.add(directId);
if (myRoom === null) return; 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); 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.rooms.delete(directId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED); this.emit(cons.events.roomList.ROOMLIST_UPDATED);
} }
@@ -298,23 +303,17 @@ class RoomList extends EventEmitter {
} }
}); });
this.matrixClient.on('Room.myMembership', (room, membership, prevMembership) => { this.matrixClient.on('Room.myMembership', async (room, membership, prevMembership) => {
// room => prevMembership = null | invite | join | leave | kick | ban | unban // room => prevMembership = null | invite | join | leave | kick | ban | unban
// room => membership = invite | join | leave | kick | ban | unban // room => membership = invite | join | leave | kick | ban | unban
const { roomId } = room; 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; 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 (membership === 'invite') {
if (this._isDMInvite(room)) this.inviteDirects.add(roomId); if (this._isDMInvite(room)) this.inviteDirects.add(roomId);
else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId); else if (room.isSpaceRoom()) this.inviteSpaces.add(roomId);
@@ -324,13 +323,26 @@ class RoomList extends EventEmitter {
return; return;
} }
// When user join room (first time) or start DM. if (prevMembership === 'invite') {
if ((prevMembership === null || prevMembership === 'invite') && membership === 'join') { 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);
}
if (['leave', 'kick', 'ban'].includes(membership)) {
if (this.directs.has(roomId)) this.directs.delete(roomId);
else if (this.spaces.has(roomId)) 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. // when user create room/DM OR accept room/dm invite from this client.
// we will update this.rooms/this.directs with user action // we will update this.rooms/this.directs with user action
if (this.directs.has(roomId) || this.spaces.has(roomId) || this.rooms.has(roomId)) return; if (membership === 'join' && this.processingRooms.has(roomId)) {
if (this.processingRooms.has(roomId)) {
const procRoomInfo = this.processingRooms.get(roomId); const procRoomInfo = this.processingRooms.get(roomId);
if (procRoomInfo.isDM) this.directs.add(roomId); if (procRoomInfo.isDM) this.directs.add(roomId);
@@ -344,68 +356,20 @@ class RoomList extends EventEmitter {
this.processingRooms.delete(roomId); this.processingRooms.delete(roomId);
return; return;
} }
if (room.isSpaceRoom()) {
this.addToSpaces(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId); if (this.mDirects.has(roomId) && membership === 'join') {
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;
}
// 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.directs.add(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId); this.emit(cons.events.roomList.ROOM_JOINED, roomId);
}
this.emit(cons.events.roomList.ROOMLIST_UPDATED); this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return; 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);
}
if (membership === 'join') { if (membership === 'join') {
if (room.isSpaceRoom()) this.addToSpaces(roomId); if (room.isSpaceRoom()) this.addToSpaces(roomId);
else this.rooms.add(roomId); else this.rooms.add(roomId);
this.emit(cons.events.roomList.ROOM_JOINED, roomId); this.emit(cons.events.roomList.ROOM_JOINED, roomId);
}
this.emit(cons.events.roomList.ROOMLIST_UPDATED); this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
}); });
} }
} }

View File

@@ -1,5 +1,5 @@
const cons = { const cons = {
version: '1.8.2', version: '2.0.2',
secretKey: { secretKey: {
ACCESS_TOKEN: 'cinny_access_token', ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id', DEVICE_ID: 'cinny_device_id',
@@ -38,6 +38,7 @@ const cons = {
OPEN_INVITE_LIST: 'OPEN_INVITE_LIST', OPEN_INVITE_LIST: 'OPEN_INVITE_LIST',
OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS', OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS',
OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM', OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM',
OPEN_JOIN_ALIAS: 'OPEN_JOIN_ALIAS',
OPEN_INVITE_USER: 'OPEN_INVITE_USER', OPEN_INVITE_USER: 'OPEN_INVITE_USER',
OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER', OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER',
OPEN_SETTINGS: 'OPEN_SETTINGS', OPEN_SETTINGS: 'OPEN_SETTINGS',
@@ -49,6 +50,7 @@ const cons = {
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU', OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
OPEN_NAVIGATION: 'OPEN_NAVIGATION', OPEN_NAVIGATION: 'OPEN_NAVIGATION',
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG', OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
OPEN_EMOJI_VERIFICATION: 'OPEN_EMOJI_VERIFICATION',
}, },
room: { room: {
JOIN: 'JOIN', JOIN: 'JOIN',
@@ -85,6 +87,7 @@ const cons = {
INVITE_LIST_OPENED: 'INVITE_LIST_OPENED', INVITE_LIST_OPENED: 'INVITE_LIST_OPENED',
PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED', PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED',
CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED', CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
JOIN_ALIAS_OPENED: 'JOIN_ALIAS_OPENED',
INVITE_USER_OPENED: 'INVITE_USER_OPENED', INVITE_USER_OPENED: 'INVITE_USER_OPENED',
SETTINGS_OPENED: 'SETTINGS_OPENED', SETTINGS_OPENED: 'SETTINGS_OPENED',
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED', PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
@@ -96,6 +99,7 @@ const cons = {
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED', REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
NAVIGATION_OPENED: 'NAVIGATION_OPENED', NAVIGATION_OPENED: 'NAVIGATION_OPENED',
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED', REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
EMOJI_VERIFICATION_OPENED: 'EMOJI_VERIFICATION_OPENED',
}, },
roomList: { roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED', ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',

View File

@@ -14,7 +14,7 @@ class Navigation extends EventEmitter {
this.isRoomSettings = false; this.isRoomSettings = false;
this.recentRooms = []; this.recentRooms = [];
this.isRawModalVisible = false; this.rawModelStack = [];
} }
_setSpacePath(roomId) { _setSpacePath(roomId) {
@@ -47,8 +47,13 @@ class Navigation extends EventEmitter {
} }
} }
get isRawModalVisible() {
return this.rawModelStack.length > 0;
}
setIsRawModalVisible(visible) { setIsRawModalVisible(visible) {
this.isRawModalVisible = visible; if (visible) this.rawModelStack.push(true);
else this.rawModelStack.pop();
} }
navigate(action) { navigate(action) {
@@ -122,6 +127,12 @@ class Navigation extends EventEmitter {
action.parentId, action.parentId,
); );
}, },
[cons.actions.navigation.OPEN_JOIN_ALIAS]: () => {
this.emit(
cons.events.navigation.JOIN_ALIAS_OPENED,
action.term,
);
},
[cons.actions.navigation.OPEN_INVITE_USER]: () => { [cons.actions.navigation.OPEN_INVITE_USER]: () => {
this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm); this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm);
}, },
@@ -185,6 +196,13 @@ class Navigation extends EventEmitter {
action.afterClose, action.afterClose,
); );
}, },
[cons.actions.navigation.OPEN_EMOJI_VERIFICATION]: () => {
this.emit(
cons.events.navigation.EMOJI_VERIFICATION_OPENED,
action.request,
action.targetDevice,
);
},
}; };
actions[action.type]?.(); actions[action.type]?.();
} }

View File

@@ -475,6 +475,10 @@ textarea {
supported by Chrome, Edge, Opera and Firefox */ supported by Chrome, Edge, Opera and Firefox */
} }
audio:not([controls]) {
display: none !important;
}
.flex--center { .flex--center {
display: flex; display: flex;
justify-content: center; justify-content: center;