Compare commits

...
Author SHA1 Message Date
Ajay Bura 101134e313 Add SSSS and key backup 2022-04-24 13:17:36 +05:30
Ajay Bura 89f69ed5b4 Show progress when restoring key backup 2022-04-24 09:49:48 +05:30
Ajay Bura 848cbb1066 Merge branch 'dev' into feat/key-backup 2022-04-23 19:12:11 +05:30
Ajay Bura 8f987ea86a WIP 2022-04-23 19:08:46 +05:30
Ajay Bura e7d4f203c3 WIP 2022-04-21 15:51:22 +05:30
Krishan ec26c03d58 Run docker check when someone change the action too (#495) 2022-04-20 08:58:58 +05:30
Krishan 3b1b3387e7 Replace forked action with original one (#496)
The reason to replace is that previous action fails when pull request content is empty and Beakyn/gha-comment-pull-request@v1.0.2 fixed this.
2022-04-20 08:58:47 +05:30
dependabot[bot] 62c03d1334 Bump html-react-parser from 1.4.9 to 1.4.11 (#490)
Bumps [html-react-parser](https://github.com/remarkablemark/html-react-parser) from 1.4.9 to 1.4.11.
- [Release notes](https://github.com/remarkablemark/html-react-parser/releases)
- [Changelog](https://github.com/remarkablemark/html-react-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/remarkablemark/html-react-parser/compare/v1.4.9...v1.4.11)

---
updated-dependencies:
- dependency-name: html-react-parser
  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-04-19 09:39:14 +05:30
dependabot[bot] 51e12184d7 Bump eslint-plugin-react-hooks from 4.3.0 to 4.4.0 (#448)
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 4.3.0 to 4.4.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-04-19 09:37:00 +05:30
dependabot[bot] 8c01eb9c00 Bump node from 17.8.0-alpine3.15 to 17.9.0-alpine3.15 (#492)
Bumps node from 17.8.0-alpine3.15 to 17.9.0-alpine3.15.

---
updated-dependencies:
- dependency-name: node
  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-04-19 09:35:09 +05:30
dependabot[bot] bf264d5add Bump actions/checkout from 3.0.0 to 3.0.1 (#491)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: actions/checkout
  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-04-19 09:34:33 +05:30
Krishan 8b3bd38bad Improve strings (#488)
* Update SpaceOptions.jsx

* Update RoomOptions.jsx

* Update RoomProfile.jsx

* Update ProfileEditor.jsx

* Update RoomSettings.jsx

* Update SpaceSettings.jsx

* Update Message.jsx

* Update DeviceManage.jsx

* Update Message.jsx

* Update RoomProfile.jsx
2022-04-18 08:55:16 +05:30
Ajay Bura 35ba92ae20 WIP 2022-04-16 21:36:01 +05:30
Ajay Bura 5c4b843b49 Merge branch 'dev' into feat/key-backup 2022-04-16 21:29:52 +05:30
Ajay Bura a494082086 WIP 2022-04-16 21:29:10 +05:30
Ajay Bura ba0de8800a Fix crash on unable to getContent of tombstoned room 2022-04-16 20:53:24 +05:30
Ajay Bura 88e0321d28 Add key backup
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-04-15 19:20:23 +05:30
dependabot[bot] cd5ae4cb7f Bump matrix-js-sdk from 16.0.1 to 17.0.0 (#486)
Bumps [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk) from 16.0.1 to 17.0.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/v16.0.1...v17.0.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-12 10:35:38 +05:30
dependabot[bot] 6575542281 Bump sass from 1.49.11 to 1.50.0 (#484)
Bumps [sass](https://github.com/sass/dart-sass) from 1.49.11 to 1.50.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.49.11...1.50.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-04-12 10:31:57 +05:30
dependabot[bot] bf2559da80 Bump webpack-dev-server from 4.7.4 to 4.8.1 (#483)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.7.4 to 4.8.1.
- [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.7.4...v4.8.1)

---
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-04-12 10:27:09 +05:30
dependabot[bot] fedc207de2 Bump eslint from 8.12.0 to 8.13.0 (#481)
Bumps [eslint](https://github.com/eslint/eslint) from 8.12.0 to 8.13.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.12.0...v8.13.0)

---
updated-dependencies:
- dependency-name: eslint
  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-04-12 10:23:53 +05:30
dependabot[bot] 0d58478a73 Bump @babel/core from 7.17.8 to 7.17.9 (#482)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.8 to 7.17.9.
- [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.9/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-04-12 10:20:49 +05:30
dependabot[bot] abf24d1942 Bump eslint-plugin-import from 2.25.4 to 2.26.0 (#472)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.25.4 to 2.26.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.25.4...v2.26.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  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-04-12 10:17:55 +05:30
dependabot[bot] afe3f2f3f3 Bump docker/metadata-action from 3.6.2 to 3.7.0 (#487)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 3.6.2 to 3.7.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v3.6.2...v3.7.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-04-12 10:14:31 +05:30
dependabot[bot] 1d1cb567da Bump webpack from 5.71.0 to 5.72.0 (#485)
Bumps [webpack](https://github.com/webpack/webpack) from 5.71.0 to 5.72.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.71.0...v5.72.0)

---
updated-dependencies:
- dependency-name: webpack
  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-04-12 10:14:10 +05:30
Krishan 699bbee544 Fix max power level in room permissions (#480)
* Fix power level in permissions

Fix allowed value of power level in room permissions, earlier the max value was 100 even if room members have power level more than 100.

* Update RoomPermissions.jsx
2022-04-11 21:29:39 +05:30
Ajay Bura d0cec1108a Add SecretStorageAccess component
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-04-10 09:45:54 +05:30
Ajay Bura ebd581d173 Add cross signing set/reset
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-04-09 10:08:53 +05:30
Krishan 9c54915e73 Revert "Bump react-dnd-html5-backend from 15.1.3 to 16.0.0 (#469)" (#473)
This reverts commit ed4390b99d.
2022-04-06 11:39:00 +05:30
dependabot[bot] ed4390b99d Bump react-dnd-html5-backend from 15.1.3 to 16.0.0 (#469)
Bumps [react-dnd-html5-backend](https://github.com/react-dnd/react-dnd) from 15.1.3 to 16.0.0.
- [Release notes](https://github.com/react-dnd/react-dnd/releases)
- [Changelog](https://github.com/react-dnd/react-dnd/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react-dnd/react-dnd/commits/v16.0.0)

---
updated-dependencies:
- dependency-name: react-dnd-html5-backend
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-06 10:36:01 +05:30
dependabot[bot] 039d9bae68 Bump react-dnd-html5-backend from 15.1.2 to 15.1.3 (#464)
Bumps [react-dnd-html5-backend](https://github.com/react-dnd/react-dnd) from 15.1.2 to 15.1.3.
- [Release notes](https://github.com/react-dnd/react-dnd/releases)
- [Changelog](https://github.com/react-dnd/react-dnd/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react-dnd/react-dnd/commits)

---
updated-dependencies:
- dependency-name: react-dnd-html5-backend
  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-04-05 09:11:33 +05:30
dependabot[bot] 93ab48ac9a Bump react-dnd from 15.1.1 to 15.1.2 (#466)
Bumps [react-dnd](https://github.com/react-dnd/react-dnd) from 15.1.1 to 15.1.2.
- [Release notes](https://github.com/react-dnd/react-dnd/releases)
- [Changelog](https://github.com/react-dnd/react-dnd/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react-dnd/react-dnd/commits/v15.1.2)

---
updated-dependencies:
- dependency-name: react-dnd
  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-04-05 09:07:23 +05:30
dependabot[bot] c95e312acb Bump webpack from 5.70.0 to 5.71.0 (#463)
Bumps [webpack](https://github.com/webpack/webpack) from 5.70.0 to 5.71.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.70.0...v5.71.0)

---
updated-dependencies:
- dependency-name: webpack
  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-04-05 09:00:14 +05:30
dependabot[bot] 9279bc7060 Bump sass from 1.49.9 to 1.49.11 (#465)
Bumps [sass](https://github.com/sass/dart-sass) from 1.49.9 to 1.49.11.
- [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.49.9...1.49.11)

---
updated-dependencies:
- dependency-name: sass
  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-04-05 08:56:39 +05:30
dependabot[bot] fe61576dcd Bump twemoji from 14.0.1 to 14.0.2 (#457)
Bumps [twemoji](https://github.com/twitter/twemoji) from 14.0.1 to 14.0.2.
- [Release notes](https://github.com/twitter/twemoji/releases)
- [Commits](https://github.com/twitter/twemoji/compare/v14.0.1...v14.0.2)

---
updated-dependencies:
- dependency-name: twemoji
  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-04-05 08:55:16 +05:30
Ajay Bura ce367f2b67 Add cross singing dialogs
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-04-01 19:41:30 +05:30
Ajay Bura 3c73cbe67f Merge branch 'dev' into feat/key-backup
To use reusable dialog in cross signing
2022-03-31 20:40:35 +05:30
Ajay Bura 3f6e3074f2 Add reusable dialog (#459)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 20:39:05 +05:30
Ajay Bura 289a386bf7 WIP
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 18:30:53 +05:30
Krishan 53a8e2aa57 Increase Pull request limit for dependabot npm updates (#454)
* Increase PR limit for dependabot

* Make the invisible visible
2022-03-31 15:42:37 +05:30
Ajay Bura ebec39bdd1 Fix typo
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 14:52:33 +05:30
Ajay Bura 575eacae92 Add cross signing and key backup component
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 14:44:32 +05:30
Ajay Bura db4bb13e8e Add cross signin info card in device manage component
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 14:42:59 +05:30
Ajay Bura 4bbdf3196e Add hasCrossSigninAccountData function
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 14:41:19 +05:30
Ajay Bura d978776993 Add cross signin status hook
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 14:40:38 +05:30
Ajay Bura c423ba853a Add css variables
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 14:39:47 +05:30
Ajay Bura 9acac02f0b Add info card component
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 14:38:58 +05:30
Ajay Bura a30b4b9d98 Add unverified session indicator in sidebar
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-30 18:48:53 +05:30
Ajay Bura 0bdb14f5b7 Add className prop in sidebar avatar comp
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-30 18:46:55 +05:30
Krishan 44ab6f181c Fix docker check to only run on Dockerfile change (#452) 2022-03-30 18:38:52 +05:30
Ajay Bura c27f2d6268 Add isCrossVerified func to matrixUtil
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-30 17:14:04 +05:30
Ajay Bura d1d545ed55 Add useDeviceList hook
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-30 17:10:02 +05:30
Krishan 3a3a830706 Reverting .yaml as some things doesn't work (#451) 2022-03-30 15:01:41 +05:30
Krishan 1a6e3e73c5 String update and file extension name consistency (#436)
* Fixes #434

* Fixes #433

* Prtially fixes #432

* Disable auto labelling of issues

* Use yaml instead of yml as recommended by yaml.org

* shortened the strings

* simplified option description
2022-03-30 13:42:52 +05:30
dependabot[bot] 7d508e5a7d Bump matrix-js-sdk from 15.6.0 to 16.0.1 (#440)
Bumps [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk) from 15.6.0 to 16.0.1.
- [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/v15.6.0...v16.0.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-29 13:03:05 +05:30
dependabot[bot] 370c224d3a Bump @fontsource/inter from 4.5.5 to 4.5.7 (#443)
Bumps [@fontsource/inter](https://github.com/fontsource/fontsource/tree/HEAD/fonts/google/inter) from 4.5.5 to 4.5.7.
- [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-03-29 12:44:22 +05:30
dependabot[bot] 7bce501069 Bump babel-loader from 8.2.3 to 8.2.4 (#437)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 8.2.3 to 8.2.4.
- [Release notes](https://github.com/babel/babel-loader/releases)
- [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel-loader/compare/v8.2.3...v8.2.4)

---
updated-dependencies:
- dependency-name: babel-loader
  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-03-29 12:33:35 +05:30
dependabot[bot] 9d15445eba Bump @fontsource/roboto from 4.5.3 to 4.5.5 (#438)
Bumps [@fontsource/roboto](https://github.com/fontsource/fontsource/tree/HEAD/fonts/google/roboto) from 4.5.3 to 4.5.5.
- [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/roboto)

---
updated-dependencies:
- dependency-name: "@fontsource/roboto"
  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-03-29 12:33:20 +05:30
dependabot[bot] 16ee13f1f7 Bump eslint from 8.11.0 to 8.12.0 (#439)
Bumps [eslint](https://github.com/eslint/eslint) from 8.11.0 to 8.12.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.11.0...v8.12.0)

---
updated-dependencies:
- dependency-name: eslint
  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-03-29 12:33:01 +05:30
dependabot[bot] 8d25eb0acd Bump node from 17.7.2-alpine3.15 to 17.8.0-alpine3.15 (#442)
Bumps node from 17.7.2-alpine3.15 to 17.8.0-alpine3.15.

---
updated-dependencies:
- dependency-name: node
  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-03-29 12:32:46 +05:30
Ajay Bura 9cb13a91cd Fix incorrect custom html crashing app (#429)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-25 10:37:35 +05:30
Ajay Bura 30f8930773 Fix style and string
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-24 18:47:53 +05:30
Krishan 05eaa8d3e0 General fix and consistency changes (#428) 2022-03-23 19:40:39 +05:30
dependabot[bot] a32ffdf6d4 Bump node-forge from 1.2.1 to 1.3.0 (#416)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.2.1 to 1.3.0.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.2.1...v1.3.0)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-23 18:46:35 +05:30
Ajay Bura 0f97de1b09 Fix can't open spaces from public room list (#419)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-23 18:45:13 +05:30
Ajay Bura e8d6ccec9a Fix crash on load and room creation (#418)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-23 18:44:53 +05:30
Ajay Bura 005434f79b Add support for managing sessions (#415)
* Allow node type prop in setting tile

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Update popup window max height

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add device management setting

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add password based login

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* truncate long list of verified devices

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-23 18:44:38 +05:30
Krishan fe997d8b01 Fix loading on older browsers (#424) 2022-03-23 18:33:19 +05:30
dependabot[bot] 7291932a0b Bump @babel/core from 7.17.7 to 7.17.8 (#407)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.7 to 7.17.8.
- [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.8/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-03-22 19:03:38 +05:30
dependabot[bot] 49ade03a9a Bump html-react-parser from 1.4.8 to 1.4.9 (#408)
Bumps [html-react-parser](https://github.com/remarkablemark/html-react-parser) from 1.4.8 to 1.4.9.
- [Release notes](https://github.com/remarkablemark/html-react-parser/releases)
- [Changelog](https://github.com/remarkablemark/html-react-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/remarkablemark/html-react-parser/compare/v1.4.8...v1.4.9)

---
updated-dependencies:
- dependency-name: html-react-parser
  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-03-22 19:03:15 +05:30
dependabot[bot] dd6dbd25da Bump node from 17.7.1-alpine3.15 to 17.7.2-alpine3.15 (#410)
Bumps node from 17.7.1-alpine3.15 to 17.7.2-alpine3.15.

---
updated-dependencies:
- dependency-name: node
  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-03-22 19:02:26 +05:30
Ajay Bura 50bf90fada Redesign app/user settings (#404)
* Redesign app settings

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Redesign user profile in settings

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Update string

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix bug

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-21 09:46:11 +05:30
Ajay Bura abb81b6390 Remove opacity from jump to unread and latest btn (#402)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 09:19:20 +05:30
Ajay Bura 36da3d3cba Add sound on incoming invites (#400)
* Add invite sound

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add credits

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Change invite sound to google material
2022-03-19 18:55:57 +05:30
Ajay Bura ae71a99aa4 Sort search results by activity (#399)
* Sort search result by activity

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Optimize generateResults function codes

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-19 18:55:38 +05:30
Ajay Bura a7034d6351 Improve jump to bottom btn
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 11:37:31 +05:30
Ajay Bura 6010b4c252 Fix cyclic dependency in initMatrix
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 09:22:16 +05:30
Ajay Bura 8330f4fba9 Improve jump to unread button (#396)
* Improve jump to unread button

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Remove unused cod

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix mark as read not hidding jump to unread btn

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add notification mark as read action

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add esc as hotkey to mark room as read

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add message icons

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Change jump to unread icon

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-18 09:09:14 +05:30
ginnyTheCat dc6e153b92 Notification sounds (#367)
* Basic notification sound support

* Add settings option for notification sounds

* Allow sound without desktop notifications
2022-03-18 09:07:11 +05:30
Nitan Alexandru Marcel a2655ee6a5 Fix loading on older browsers (#397) 2022-03-18 09:06:48 +05:30
Ajay Bura 13248962af Update recent clock icon
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 17:35:50 +05:30
ginnyTheCatandKrishan b698982186 Add recently used section to emoji board (#373)
* Add recent section to emoji board

* Add section to emoji board sidebar

* Add emoji limit like element web has

* Ignore custom emojis

* Filter out invalid emojis

* Update heart icon with clock

Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
2022-03-17 17:19:14 +05:30
Ajay Bura 5a299b21c5 Sort direct messages by activity (#393)
* Add sort util

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Use sort util for members

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Sort dms by activity

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Sort dms activily

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Chanege roomIdByLastActive func name

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 16:55:16 +05:30
Krishan bb90f11ec8 Formify issue templates and improve existing (#395)
* Update bug_report.md

* Rename bug_report.md to bug_report.yml

* Update and rename feature_request.md to feature_request.yml

* Create config.yml

* Update PULL_REQUEST_TEMPLATE.md
2022-03-17 16:47:42 +05:30
Ajay Bura 12369ba2ec v1.8.2
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 16:44:50 +05:30
101 changed files with 3155 additions and 1636 deletions

No files matched your search

-33
View File
@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
#### Describe the bug
A clear and concise description of what the bug is.
#### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
#### Expected behavior
A clear and concise description of what you expected to happen.
#### Screenshots
If applicable, add screenshots to help explain your problem.
#### Desktop (please complete the following information):
- OS: [e.g. Windows, MacOS]
- Browser: [e.g. chrome, firefox]
- Version: [e.g. 3.22]
- Matrix homeserver: [e.g. matrix.org]
#### Additional context
Add any other context about the problem here.
+57
View File
@@ -0,0 +1,57 @@
name: 🐞 Bug Report
description: Report a bug
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Browser: [e.g. chrome 99.5, firefox 97.2]
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
4. Matrix homeserver: [e.g. matrix.org]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+4
View File
@@ -0,0 +1,4 @@
contact_links:
- name: 💬 Matrix Chat
url: https://matrix.to/#/#cinny:matrix.org
about: Ask questions and talk to other Cinny users and the maintainers
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
#### Describe the solution you'd like
A clear and concise description of what you want to happen.
#### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
#### Additional context
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,33 @@
name: 💡 Feature Request
description: Suggest an idea
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.
+2 -4
View File
@@ -1,15 +1,13 @@
<!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
Fixes #
#### Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+14 -12
View File
@@ -1,28 +1,30 @@
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
version: 2
updates:
- package-ecosystem: github-actions
- package-ecosystem: npm
directory: /
schedule:
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 15
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
- package-ecosystem: npm
directory: /
schedule:
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
+2 -7
View File
@@ -10,8 +10,8 @@ jobs:
env:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Check out the repo
uses: actions/checkout@v3.0.0
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Build app
run: npm ci && npm run build
- name: Upload artifact
@@ -32,8 +32,3 @@ jobs:
name: pr.json
path: pr.json
retention-days: 1
- name: Build Docker image
uses: docker/build-push-action@v2.10.0
with:
context: .
push: false
+3 -3
View File
@@ -13,7 +13,7 @@ jobs:
# There's a 'download artifact' action but it hasn't been updated for the
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
# so instead we get this mess:
- name: 'Download artifact'
- name: Download artifact
uses: actions/github-script@v6.0.0
with:
script: |
@@ -46,7 +46,7 @@ jobs:
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
- name: Extract Artifacts
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
- name: 'Read PR Info'
- name: Read PR Info
id: readctx
uses: actions/github-script@v6.0.0
with:
@@ -68,7 +68,7 @@ jobs:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
timeout-minutes: 1
- name: Edit PR Description
uses: velas/pr-description@v1.0.1
uses: Beakyn/gha-comment-pull-request@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+21
View File
@@ -0,0 +1,21 @@
name: 'Docker check'
on:
pull_request:
paths:
- 'Dockerfile'
- '.github/workflows/docker-pr.yml'
jobs:
docker-build:
runs-on: ubuntu-latest
env:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Build Docker image
uses: docker/build-push-action@v2.10.0
with:
context: .
push: false
@@ -6,13 +6,15 @@ on:
- dev
jobs:
deploy:
deploy-to-netlify:
name: 'Deploy'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.0
- uses: jsmrcaga/action-netlify-deploy@v1.7.2
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v1.7.2
with:
install_command: "npm ci"
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
@@ -9,8 +9,8 @@ jobs:
name: 'Deploy to Netlify'
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3.0.0
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v1.7.2
with:
@@ -26,7 +26,7 @@ jobs:
- name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Upload tagged release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v0.1.14
with:
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
@@ -35,8 +35,8 @@ jobs:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3.0.0
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Login to Docker Hub
uses: docker/login-action@v1.14.1
with:
@@ -44,7 +44,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3.6.2
uses: docker/metadata-action@v3.7.0
with:
images: ajbura/cinny
- name: Build and push Docker image
+1 -1
View File
@@ -1,5 +1,5 @@
## Builder
FROM node:17.7.1-alpine3.15 as builder
FROM node:17.9.0-alpine3.15 as builder
WORKDIR /src
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Ajay Bura (ajbura) and other contributors
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
+656 -947
View File
File diff suppressed because it is too large. Load diff
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "cinny",
"version": "1.8.1",
"version": "1.8.2",
"description": "Yet another matrix client",
"main": "index.js",
"engines": {
@@ -15,8 +15,8 @@
"author": "Ajay Bura",
"license": "MIT",
"dependencies": {
"@fontsource/inter": "^4.5.5",
"@fontsource/roboto": "^4.5.3",
"@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,9 +26,9 @@
"file-saver": "^2.0.5",
"flux": "^4.0.3",
"formik": "^2.2.9",
"html-react-parser": "^1.4.8",
"html-react-parser": "^1.4.11",
"linkifyjs": "^2.1.9",
"matrix-js-sdk": "^15.6.0",
"matrix-js-sdk": "^17.0.0",
"micromark": "^3.0.10",
"micromark-extension-gfm": "^2.0.1",
"micromark-util-chunked": "^1.0.0",
@@ -37,21 +37,21 @@
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-autosize-textarea": "^7.1.0",
"react-dnd": "^15.1.1",
"react-dnd-html5-backend": "^15.1.2",
"react-dnd": "^15.1.2",
"react-dnd-html5-backend": "^15.1.3",
"react-dom": "^17.0.2",
"react-google-recaptcha": "^2.1.0",
"react-modal": "^3.14.4",
"sanitize-html": "^2.7.0",
"tippy.js": "^6.3.7",
"twemoji": "^14.0.1"
"twemoji": "^14.0.2"
},
"devDependencies": {
"@babel/core": "^7.17.7",
"@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.2",
"babel-loader": "^8.2.4",
"browserify-fs": "^1.0.0",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
@@ -59,27 +59,27 @@
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"eslint": "^8.11.0",
"eslint": "^8.13.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.2.0",
"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.49.9",
"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.70.0",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4",
"webpack-dev-server": "^4.8.1",
"webpack-merge": "^5.7.3"
}
}
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<rect x="7" y="12" fill="#010101" width="10" height="2"/>
<g>
<circle fill="#010101" cx="19" cy="6" r="4"/>
</g>
<path fill="#010101" d="M13.3,8H7v2h7.5C14,9.4,13.6,8.7,13.3,8z"/>
<path fill="#010101" d="M19,12v5.6l-2.4-1.3L16.1,16h-0.5H5V6h8c0-0.7,0.1-1.4,0.3-2H4.8C3.8,4,3,4.9,3,6v10c0,1.1,0.8,2,1.8,2
h10.8l5.4,3v-9.3C20.4,11.9,19.7,12,19,12z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 811 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<path fill="#010101" d="M19,6v11.6l-2.4-1.3L16.1,16h-0.5H5V6H19 M19.2,4H4.8C3.8,4,3,4.9,3,6v10c0,1.1,0.8,2,1.8,2h10.8l5.4,3V6
C21,4.9,20.2,4,19.2,4L19.2,4z"/>
<rect x="7" y="8" fill="#010101" width="10" height="2"/>
<rect x="7" y="12" fill="#010101" width="10" height="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 732 B

+7 -6
View File
@@ -4,13 +4,14 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g>
<polygon fill="#010101" points="11,6 11,12 15.2,16.2 16.7,14.8 13,11.2 13,6 "/>
<path fill="#010101" d="M12,2C6.5,2,2,6.5,2,12H0.2L3,14.8L5.8,12H4c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8c-1.9,0-3.7-0.7-5-1.8
l-1.2,1.6C7.4,21.2,9.6,22,12,22c5.5,0,10-4.5,10-10S17.5,2,12,2z"/>
<polygon points="11,7 11,13 14.5,16.5 15.9,15.1 13,12.2 13,7 "/>
<path d="M12,2C8.7,2,5.8,3.6,4,6V4H2.5v5h5V7.5H5.4C6.9,5.4,9.3,4,12,4c4.4,0,8,3.6,8,8s-3.6,8-8,8s-8-3.6-8-8H2
c0,5.5,4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z"/>
</g>
<g>
<polygon fill="#010101" points="49,44 49,50 53.2,54.2 54.7,52.8 51,49.2 51,44 "/>
<path fill="#010101" d="M50,40c-5.5,0-10,4.5-10,10h-1.8l2.8,2.8l2.8-2.8H42c0-4.4,3.6-8,8-8s8,3.6,8,8s-3.6,8-8,8
c-1.9,0-3.7-0.7-5-1.8l-1.2,1.6c1.7,1.4,3.9,2.2,6.3,2.2c5.5,0,10-4.5,10-10S55.5,40,50,40z"/>
<polygon points="49,44 49,50 53.2,54.2 54.7,52.8 51,49.2 51,44 "/>
<polygon points="45.5,47 40.5,47 40.5,42 42,42 42,45.5 45.5,45.5 "/>
<path d="M50,40c-4.1,0-7.6,2.5-9.2,6h2.2c1.4-2.4,4-4,6.9-4c4.4,0,8,3.6,8,8s-3.6,8-8,8s-8-3.6-8-8h-2c0,5.5,4.5,10,10,10
s10-4.5,10-10S55.5,40,50,40z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 985 B

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+59
View File
@@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import './InfoCard.scss';
import Text from '../text/Text';
import RawIcon from '../system-icons/RawIcon';
import IconButton from '../button/IconButton';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function InfoCard({
className, style,
variant, iconSrc,
title, content,
rounded, requestClose,
}) {
const classes = [`info-card info-card--${variant}`];
if (rounded) classes.push('info-card--rounded');
if (className) classes.push(className);
return (
<div className={classes.join(' ')} style={style}>
{iconSrc && (
<div className="info-card__icon">
<RawIcon color={`var(--ic-${variant}-high)`} src={iconSrc} />
</div>
)}
<div className="info-card__content">
<Text>{title}</Text>
{content}
</div>
{requestClose && (
<IconButton src={CrossIC} variant={variant} onClick={requestClose} />
)}
</div>
);
}
InfoCard.defaultProps = {
className: null,
style: null,
variant: 'surface',
iconSrc: null,
content: null,
rounded: false,
requestClose: null,
};
InfoCard.propTypes = {
className: PropTypes.string,
style: PropTypes.shape({}),
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
iconSrc: PropTypes.string,
title: PropTypes.string.isRequired,
content: PropTypes.node,
rounded: PropTypes.bool,
requestClose: PropTypes.func,
};
export default InfoCard;
+79
View File
@@ -0,0 +1,79 @@
@use '.././../partials/flex';
@use '.././../partials/dir';
.info-card {
display: flex;
align-items: flex-start;
line-height: 0;
padding: var(--sp-tight);
@include dir.prop(border-left, 4px solid transparent, none);
@include dir.prop(border-right, none, 4px solid transparent);
& > .ic-btn {
padding: 0;
border-radius: 4;
}
&__content {
margin: 0 var(--sp-tight);
@extend .cp-fx__item-one;
& > *:nth-child(2) {
margin-top: var(--sp-ultra-tight);
}
}
&--rounded {
@include dir.prop(
border-radius,
0 var(--bo-radius) var(--bo-radius) 0,
var(--bo-radius) 0 0 var(--bo-radius)
);
}
&--surface {
border-color: var(--bg-surface-border);
background-color: var(--bg-surface-hover);
}
&--primary {
border-color: var(--bg-primary);
background-color: var(--bg-primary-hover);
& .text {
color: var(--tc-primary-high);
&-b3 {
color: var(--tc-primary-normal);
}
}
}
&--positive {
border-color: var(--bg-positive-border);
background-color: var(--bg-positive-hover);
& .text {
color: var(--tc-positive-high);
&-b3 {
color: var(--tc-positive-normal);
}
}
}
&--caution {
border-color: var(--bg-caution-border);
background-color: var(--bg-caution-hover);
& .text {
color: var(--tc-caution-high);
&-b3 {
color: var(--tc-caution-normal);
}
}
}
&--danger {
border-color: var(--bg-danger-border);
background-color: var(--bg-danger-hover);
& .text {
color: var(--tc-danger-high);
&-b3 {
color: var(--tc-danger-normal);
}
}
}
}
+2 -2
View File
@@ -74,7 +74,7 @@ Tabs.defaultProps = {
Tabs.propTypes = {
items: PropTypes.arrayOf(
PropTypes.exact({
PropTypes.shape({
iconSrc: PropTypes.string,
text: PropTypes.string,
disabled: PropTypes.bool,
@@ -84,4 +84,4 @@ Tabs.propTypes = {
onSelect: PropTypes.func.isRequired,
};
export { Tabs as default };
export default Tabs;
+25
View File
@@ -0,0 +1,25 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from 'react';
import initMatrix from '../../client/initMatrix';
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
export function useCrossSigningStatus() {
const mx = initMatrix.matrixClient;
const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData());
useEffect(() => {
if (isCSEnabled) return null;
const handleAccountData = (event) => {
if (event.getType() === 'm.cross_signing.master') {
setIsCSEnabled(true);
}
};
mx.on('accountData', handleAccountData);
return () => {
mx.removeListener('accountData', handleAccountData);
};
}, [isCSEnabled === false]);
return isCSEnabled;
}
+32
View File
@@ -0,0 +1,32 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from 'react';
import initMatrix from '../../client/initMatrix';
export function useDeviceList() {
const mx = initMatrix.matrixClient;
const [deviceList, setDeviceList] = useState(null);
useEffect(() => {
let isMounted = true;
const updateDevices = () => mx.getDevices().then((data) => {
if (!isMounted) return;
setDeviceList(data.devices || []);
});
updateDevices();
const handleDevicesUpdate = (users) => {
if (users.includes(mx.getUserId())) {
updateDevices();
}
};
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
return () => {
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
isMounted = false;
};
}, []);
return deviceList;
}
+4 -1
View File
@@ -12,6 +12,7 @@ import RawModal from '../../atoms/modal/RawModal';
function Dialog({
className, isOpen, title, onAfterOpen, onAfterClose,
contentOptions, onRequestClose, closeFromOutside, children,
invisibleScroll,
}) {
return (
<RawModal
@@ -36,7 +37,7 @@ function Dialog({
{contentOptions}
</Header>
<div className="dialog__content__wrapper">
<ScrollView autoHide>
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
<div className="dialog__content-container">
{children}
</div>
@@ -55,6 +56,7 @@ Dialog.defaultProps = {
onAfterClose: null,
onRequestClose: null,
closeFromOutside: true,
invisibleScroll: false,
};
Dialog.propTypes = {
@@ -67,6 +69,7 @@ Dialog.propTypes = {
onRequestClose: PropTypes.func,
closeFromOutside: PropTypes.bool,
children: PropTypes.node.isRequired,
invisibleScroll: PropTypes.bool,
};
export default Dialog;
-5
View File
@@ -21,8 +21,3 @@
flex-direction: column;
}
}
.dialog__content-container {
padding-top: var(--sp-extra-tight);
padding-bottom: var(--sp-extra-loose);
}
@@ -0,0 +1,49 @@
import React, { useState, useEffect } from 'react';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import IconButton from '../../atoms/button/IconButton';
import Dialog from './Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function ReusableDialog() {
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const handleOpen = (title, render, afterClose) => {
setIsOpen(true);
setData({ title, render, afterClose });
};
navigation.on(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
return () => {
navigation.removeListener(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
};
}, []);
const handleAfterClose = () => {
data.afterClose?.();
setData(null);
};
const handleRequestClose = () => {
setIsOpen(false);
};
return (
<Dialog
isOpen={isOpen}
title={data?.title || ''}
onAfterClose={handleAfterClose}
onRequestClose={handleRequestClose}
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip="Close" />}
invisibleScroll
>
{data?.render(handleRequestClose) || <div />}
</Dialog>
);
}
export default ReusableDialog;
+12 -4
View File
@@ -186,9 +186,17 @@ const MessageBody = React.memo(({
// if body is not string it is a React element.
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
let content = isCustomHTML
? twemojify(sanitizeCustomHtml(body), undefined, true, false)
: twemojify(body, undefined, true);
let content = null;
if (isCustomHTML) {
try {
content = twemojify(sanitizeCustomHtml(body), undefined, true, false);
} catch {
console.error('Malformed custom html: ', body);
content = twemojify(body, undefined);
}
} else {
content = twemojify(body, undefined, true);
}
// Determine if this message should render with large emojis
// Criteria:
@@ -539,7 +547,7 @@ const MessageOptions = React.memo(({
variant="danger"
iconSrc={BinIC}
onClick={() => {
if (window.confirm('Are you sure you want to delete this event')) {
if (window.confirm('Are you sure that you want to delete this event?')) {
redactEvent(roomId, mEvent.getId());
}
}}
@@ -1,7 +1,7 @@
@use '../../partials/dir';
.pw-model {
--modal-height: 656px;
--modal-height: 774px;
max-height: var(--modal-height) !important;
height: 100%;
}
@@ -17,19 +17,19 @@ const visibility = {
const items = [{
iconSrc: null,
text: 'World readable (anyone can read)',
text: 'Anyone (including guests)',
type: visibility.WORLD_READABLE,
}, {
iconSrc: null,
text: 'Member shared (since the point in time of selecting this option)',
text: 'Members (all messages)',
type: visibility.SHARED,
}, {
iconSrc: null,
text: 'Member invited (since they were invited)',
text: 'Members (messages after invite)',
type: visibility.INVITED,
}, {
iconSrc: null,
text: 'Member joined (since they joined)',
text: 'Members (messages after join)',
type: visibility.JOINED,
}];
@@ -87,7 +87,7 @@ function RoomHistoryVisibility({ roomId }) {
</MenuItem>
))
}
<Text variant="b3">Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.</Text>
<Text variant="b3">Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect.</Text>
</div>
);
}
+2 -21
View File
@@ -9,6 +9,7 @@ import colorMXID from '../../../util/colorMXID';
import { openProfileViewer } from '../../../client/action/navigation';
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
import AsyncSearch from '../../../util/AsyncSearch';
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -19,26 +20,6 @@ import PeopleSelector from '../people-selector/PeopleSelector';
const PER_PAGE_MEMBER = 50;
function AtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
function sortByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
function normalizeMembers(members) {
const mx = initMatrix.matrixClient;
return members.map((member) => ({
@@ -65,7 +46,7 @@ function useMemberOfMembership(roomId, membership) {
if (event && event?.getRoomId() !== roomId) return;
const memberOfMembership = normalizeMembers(
room.getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel),
.sort(memberByAtoZ).sort(memberByPowerLevel),
);
setMembers(memberOfMembership);
};
@@ -35,7 +35,12 @@ const items = [{
function setRoomNotifType(roomId, newType) {
const mx = initMatrix.matrixClient;
const { notifications } = initMatrix;
const roomPushRule = mx.getRoomPushRule('global', roomId);
let roomPushRule;
try {
roomPushRule = mx.getRoomPushRule('global', roomId);
} catch {
roomPushRule = undefined;
}
const promises = [];
if (newType === cons.notifs.MUTE) {
@@ -6,6 +6,7 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openInviteUser } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room';
import { markAsRead } from '../../../client/action/notifications';
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import RoomNotification from '../room-notification/RoomNotification';
@@ -20,10 +21,8 @@ function RoomOptions({ roomId, afterOptionSelect }) {
const canInvite = room?.canInvite(mx.getUserId());
const handleMarkAsRead = () => {
markAsRead(roomId);
afterOptionSelect();
if (!room) return;
const events = room.getLiveTimeline().getEvents();
mx.sendReadReceipt(events[events.length - 1]);
};
const handleInviteClick = () => {
@@ -31,7 +30,7 @@ function RoomOptions({ roomId, afterOptionSelect }) {
afterOptionSelect();
};
const handleLeaveClick = () => {
if (confirm('Are you really want to leave this room?')) {
if (confirm('Are you sure that you want to leave this room?')) {
roomActions.leave(roomId);
afterOptionSelect();
}
@@ -179,6 +179,7 @@ function RoomPermissions({ roomId }) {
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
const permissions = pLEvent.getContent();
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
const handlePowerLevelChange = (newPowerLevel) => {
@@ -208,7 +209,7 @@ function RoomPermissions({ roomId }) {
(closeMenu) => (
<PowerLevelSelector
value={powerLevel}
max={100}
max={myPowerLevel}
onSelect={(pl) => {
closeMenu();
handlePowerLevelChange(pl);
@@ -117,7 +117,7 @@ function RoomProfile({ roomId }) {
const handleAvatarUpload = async (url) => {
if (url === null) {
if (confirm('Are you sure you want to remove avatar?')) {
if (confirm('Are you sure that you want to remove room avatar?')) {
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
}
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
@@ -9,7 +9,11 @@ function SettingTile({ title, options, content }) {
<div className="setting-tile">
<div className="setting-tile__content">
<div className="setting-tile__title">
<Text variant="b1">{title}</Text>
{
typeof title === 'string'
? <Text variant="b1">{title}</Text>
: title
}
</div>
{content}
</div>
@@ -24,7 +28,7 @@ SettingTile.defaultProps = {
};
SettingTile.propTypes = {
title: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
options: PropTypes.node,
content: PropTypes.node,
};
@@ -9,11 +9,12 @@ import Tooltip from '../../atoms/tooltip/Tooltip';
import { blurOnBubbling } from '../../atoms/button/script';
const SidebarAvatar = React.forwardRef(({
tooltip, active, onClick, onContextMenu,
avatar, notificationBadge,
className, tooltip, active, onClick,
onContextMenu, avatar, notificationBadge,
}, ref) => {
let activeClass = '';
if (active) activeClass = ' sidebar-avatar--active';
const classes = ['sidebar-avatar'];
if (active) classes.push('sidebar-avatar--active');
if (className) classes.push(className);
return (
<Tooltip
content={<Text variant="b1">{twemojify(tooltip)}</Text>}
@@ -21,7 +22,7 @@ const SidebarAvatar = React.forwardRef(({
>
<button
ref={ref}
className={`sidebar-avatar${activeClass}`}
className={classes.join(' ')}
type="button"
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
onClick={onClick}
@@ -34,6 +35,7 @@ const SidebarAvatar = React.forwardRef(({
);
});
SidebarAvatar.defaultProps = {
className: null,
active: false,
onClick: null,
onContextMenu: null,
@@ -41,6 +43,7 @@ SidebarAvatar.defaultProps = {
};
SidebarAvatar.propTypes = {
className: PropTypes.string,
tooltip: PropTypes.string.isRequired,
active: PropTypes.bool,
onClick: PropTypes.func,
@@ -88,7 +88,7 @@ function SpaceAddExistingContent({ roomId }) {
};
const handleSearch = (ev) => {
const term = ev.target.value.toLocaleLowerCase().replaceAll(' ', '');
const term = ev.target.value.toLocaleLowerCase().replace(/\s/g, '');
if (term === '') {
setSearchIds(null);
return;
@@ -100,7 +100,7 @@ function SpaceAddExistingContent({ roomId }) {
if (!name) return false;
name = name.normalize('NFKC')
.toLocaleLowerCase()
.replaceAll(' ', '');
.replace(/\s/g, '');
return name.includes(term);
});
setSearchIds(searchedIds);
@@ -55,7 +55,7 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
};
const handleLeaveClick = () => {
if (confirm('Are you really want to leave this space?')) {
if (confirm('Are you sure that you want to leave this space?')) {
leave(roomId);
afterOptionSelect();
}
+1 -1
View File
@@ -210,7 +210,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
/>
)}
content={(
<Text variant="b3">Override the default (100) power level.</Text>
<Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</Text>
)}
/>
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
@@ -2,6 +2,7 @@
@use '../../partials/dir';
.create-room {
margin: var(--sp-normal);
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
&__form > * {
+25 -6
View File
@@ -12,6 +12,7 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { addRecentEmoji, getRecentEmojis } from './recent';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -20,6 +21,7 @@ import Input from '../../atoms/input/Input';
import ScrollView from '../../atoms/scroll/ScrollView';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import RecentClockIC from '../../../../public/res/ic/outlined/recent-clock.svg';
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
import CupIC from '../../../../public/res/ic/outlined/cup.svg';
@@ -29,10 +31,11 @@ import BulbIC from '../../../../public/res/ic/outlined/bulb.svg';
import PeaceIC from '../../../../public/res/ic/outlined/peace.svg';
import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
const ROW_EMOJIS_COUNT = 7;
const EmojiGroup = React.memo(({ name, groupEmojis }) => {
function getEmojiBoard() {
const emojiBoard = [];
const ROW_EMOJIS_COUNT = 7;
const totalEmojis = groupEmojis.length;
for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
@@ -147,8 +150,9 @@ function EmojiBoard({ onSelect, searchRef }) {
function selectEmoji(e) {
if (isTargetNotEmoji(e.target)) return;
const emoji = e.target;
onSelect(getEmojiDataFromTarget(emoji));
const emoji = getEmojiDataFromTarget(e.target);
onSelect(emoji);
if (emoji.hexcode) addRecentEmoji(emoji.unicode);
}
function setEmojiInfo(emoji) {
@@ -188,6 +192,9 @@ function EmojiBoard({ onSelect, searchRef }) {
}
const [availableEmojis, setAvailableEmojis] = useState([]);
const [recentEmojis, setRecentEmojis] = useState([]);
const recentOffset = recentEmojis.length > 0 ? 1 : 0;
useEffect(() => {
const updateAvailableEmoji = (selectedRoomId) => {
@@ -215,6 +222,9 @@ function EmojiBoard({ onSelect, searchRef }) {
const onOpen = () => {
searchRef.current.value = '';
handleSearchChange();
// only update when board is getting opened to prevent shifting UI
setRecentEmojis(getRecentEmojis(3 * ROW_EMOJIS_COUNT));
};
navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
@@ -230,7 +240,7 @@ function EmojiBoard({ onSelect, searchRef }) {
const $emojiContent = scrollEmojisRef.current.firstElementChild;
const groupCount = $emojiContent.childElementCount;
if (groupCount > emojiGroups.length) {
tabIndex += groupCount - emojiGroups.length - availableEmojis.length;
tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset;
}
$emojiContent.children[tabIndex].scrollIntoView();
}
@@ -246,6 +256,7 @@ function EmojiBoard({ onSelect, searchRef }) {
<ScrollView ref={scrollEmojisRef} autoHide>
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
<SearchedEmoji />
{recentEmojis.length > 0 && <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />}
{
availableEmojis.map((pack) => (
<EmojiGroup
@@ -271,13 +282,21 @@ function EmojiBoard({ onSelect, searchRef }) {
</div>
<ScrollView invisible>
<div className="emoji-board__nav">
{recentEmojis.length > 0 && (
<IconButton
onClick={() => openGroup(0)}
src={RecentClockIC}
tooltip="Recent"
tooltipPlacement="right"
/>
)}
<div className="emoji-board__nav-custom">
{
availableEmojis.map((pack) => {
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
return (
<IconButton
onClick={() => openGroup(pack.packIndex)}
onClick={() => openGroup(recentOffset + pack.packIndex)}
src={src}
key={pack.packIndex}
tooltip={pack.displayName}
@@ -301,7 +320,7 @@ function EmojiBoard({ onSelect, searchRef }) {
[7, FlagIC, 'Flags'],
].map(([indx, ico, name]) => (
<IconButton
onClick={() => openGroup(availableEmojis.length + indx)}
onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
key={indx}
src={ico}
tooltip={name}
@@ -91,6 +91,10 @@
}
}
.emoji-row {
display: flex;
}
.emoji-group {
--emoji-padding: 6px;
position: relative;
+36
View File
@@ -0,0 +1,36 @@
import initMatrix from '../../../client/initMatrix';
import { emojis } from './emoji';
const eventType = 'io.element.recent_emoji';
function getRecentEmojisRaw() {
return initMatrix.matrixClient.getAccountData(eventType).getContent().recent_emoji ?? [];
}
export function getRecentEmojis(limit) {
const res = [];
getRecentEmojisRaw()
.sort((a, b) => b[1] - a[1])
.find(([unicode]) => {
const emoji = emojis.find((e) => e.unicode === unicode);
if (emoji) return res.push(emoji) >= limit;
return false;
});
return res;
}
export function addRecentEmoji(unicode) {
const recent = getRecentEmojisRaw();
const i = recent.findIndex(([u]) => u === unicode);
let entry;
if (i < 0) {
entry = [unicode, 1];
} else {
[entry] = recent.splice(i, 1);
entry[1] += 1;
}
recent.unshift(entry);
initMatrix.matrixClient.setAccountData(eventType, {
recent_emoji: recent.slice(0, 100),
});
}
+24 -4
View File
@@ -1,18 +1,38 @@
import React, { useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import Postie from '../../../util/Postie';
import { roomIdByActivity } from '../../../util/sort';
import RoomsCategory from './RoomsCategory';
import { AtoZ } from './common';
const drawerPostie = new Postie();
function Directs() {
const mx = initMatrix.matrixClient;
const { roomList, notifications } = initMatrix;
const directIds = [...roomList.directs].sort(AtoZ);
const [directIds, setDirectIds] = useState([]);
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), []);
useEffect(() => {
const handleTimeline = (event, room, toStartOfTimeline, removed, data) => {
if (!roomList.directs.has(room.roomId)) return;
if (!data.liveEvent) return;
if (directIds[0] === room.roomId) return;
const newDirectIds = [room.roomId];
directIds.forEach((id) => {
if (id === room.roomId) return;
newDirectIds.push(id);
});
setDirectIds(newDirectIds);
};
mx.on('Room.timeline', handleTimeline);
return () => {
mx.removeListener('Room.timeline', handleTimeline);
};
}, [directIds]);
useEffect(() => {
const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
+23 -17
View File
@@ -5,11 +5,11 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import Postie from '../../../util/Postie';
import { roomIdByActivity, roomIdByAtoZ } from '../../../util/sort';
import RoomsCategory from './RoomsCategory';
import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces';
import { AtoZ, RoomToDM } from './common';
const drawerPostie = new Postie();
function Home({ spaceId }) {
@@ -34,10 +34,6 @@ function Home({ spaceId }) {
roomIds = roomList.getOrphanRooms();
}
spaceIds.sort(AtoZ);
roomIds.sort(AtoZ);
directIds.sort(AtoZ);
if (isCategorized) {
categories = roomList.getCategorizedSpaces(spaceIds);
categories.delete(spaceId);
@@ -73,26 +69,36 @@ function Home({ spaceId }) {
return (
<>
{ !isCategorized && spaceIds.length !== 0 && (
<RoomsCategory name="Spaces" roomIds={spaceIds} drawerPostie={drawerPostie} />
<RoomsCategory name="Spaces" roomIds={spaceIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
)}
{ roomIds.length !== 0 && (
<RoomsCategory name="Rooms" roomIds={roomIds} drawerPostie={drawerPostie} />
<RoomsCategory name="Rooms" roomIds={roomIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
)}
{ directIds.length !== 0 && (
<RoomsCategory name="People" roomIds={directIds} drawerPostie={drawerPostie} />
<RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
)}
{ isCategorized && [...categories].map(([catId, childIds]) => (
<RoomsCategory
key={catId}
spaceId={catId}
name={mx.getRoom(catId).name}
roomIds={[...childIds].sort(AtoZ).sort(RoomToDM)}
drawerPostie={drawerPostie}
/>
))}
{ isCategorized && [...categories].map(([catId, childIds]) => {
const rms = [];
const dms = [];
childIds.forEach((id) => {
if (directs.has(id)) dms.push(id);
else rms.push(id);
});
rms.sort(roomIdByAtoZ);
dms.sort(roomIdByActivity);
return (
<RoomsCategory
key={catId}
spaceId={catId}
name={mx.getRoom(catId).name}
roomIds={rms.concat(dms)}
drawerPostie={drawerPostie}
/>
);
})}
</>
);
}
+23 -1
View File
@@ -14,6 +14,7 @@ import {
} from '../../../client/action/navigation';
import { moveSpaceShortcut } from '../../../client/action/accountData';
import { abbreviateNumber, getEventCords } from '../../../util/common';
import { isCrossVerified } from '../../../util/matrixUtil';
import Avatar from '../../atoms/avatar/Avatar';
import NotificationBadge from '../../atoms/badge/NotificationBadge';
@@ -26,8 +27,12 @@ import UserIC from '../../../../public/res/ic/outlined/user.svg';
import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
import { useSelectedTab } from '../../hooks/useSelectedTab';
import { useDeviceList } from '../../hooks/useDeviceList';
import { tabText as settingTabText } from '../settings/Settings';
function useNotificationUpdate() {
const { notifications } = initMatrix;
@@ -72,7 +77,7 @@ function ProfileAvatarMenu() {
return (
<SidebarAvatar
onClick={openSettings}
tooltip={profile.displayName}
tooltip="Settings"
avatar={(
<Avatar
text={profile.displayName}
@@ -85,6 +90,22 @@ function ProfileAvatarMenu() {
);
}
function CrossSigninAlert() {
const deviceList = useDeviceList();
const unverified = deviceList?.filter((device) => !isCrossVerified(device.device_id));
if (!unverified?.length) return null;
return (
<SidebarAvatar
className="sidebar__cross-signin-alert"
tooltip={`${unverified.length} unverified sessions`}
onClick={() => openSettings(settingTabText.SECURITY)}
avatar={<Avatar iconSrc={ShieldUserIC} iconColor="var(--ic-danger-normal)" size="normal" />}
/>
);
}
function FeaturedTab() {
const { roomList, accountData, notifications } = initMatrix;
const [selectedTab] = useSelectedTab();
@@ -358,6 +379,7 @@ function SideBar() {
notificationBadge={<NotificationBadge alert content={totalInvites} />}
/>
)}
<CrossSigninAlert />
<ProfileAvatarMenu />
</div>
</div>
+17
View File
@@ -57,4 +57,21 @@
width: 24px;
height: 1px;
background-color: var(--bg-surface-border);
}
.sidebar__cross-signin-alert .avatar-container {
box-shadow: var(--bs-danger-border);
animation-name: pushRight;
animation-duration: 400ms;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes pushRight {
from {
transform: translateX(4px) scale(1);
}
to {
transform: translateX(0) scale(1);
}
}
-30
View File
@@ -1,30 +0,0 @@
import initMatrix from '../../../client/initMatrix';
function AtoZ(aId, bId) {
let aName = initMatrix.matrixClient.getRoom(aId).name;
let bName = initMatrix.matrixClient.getRoom(bId).name;
// remove "#" from the room name
// To ignore it in sorting
aName = aName.replaceAll('#', '');
bName = bName.replaceAll('#', '');
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
const RoomToDM = (aId, bId) => {
const { directs } = initMatrix.roomList;
const aIsDm = directs.has(aId);
const bIsDm = directs.has(bId);
if (aIsDm && !bIsDm) return 1;
if (!aIsDm && bIsDm) return -1;
return 0;
};
export { AtoZ, RoomToDM };
@@ -1,37 +1,46 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import ImageUpload from '../../molecules/image-upload/ImageUpload';
import Input from '../../atoms/input/Input';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import './ProfileEditor.scss';
// TODO Fix bug that prevents 'Save' button from enabling up until second changed.
function ProfileEditor({
userId,
}) {
function ProfileEditor({ userId }) {
const [isEditing, setIsEditing] = useState(false);
const mx = initMatrix.matrixClient;
const user = mx.getUser(mx.getUserId());
const displayNameRef = useRef(null);
const bgColor = colorMXID(userId);
const [avatarSrc, setAvatarSrc] = useState(null);
const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
const [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true);
let username = mx.getUser(mx.getUserId()).displayName;
useEffect(() => {
let isMounted = true;
mx.getProfileInfo(mx.getUserId()).then((info) => {
if (!isMounted) return;
setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null);
setUsername(info.displayname);
});
return () => {
isMounted = false;
};
}, [userId]);
// Sets avatar URL and updates the avatar component in profile editor to reflect new upload
function handleAvatarUpload(url) {
const handleAvatarUpload = (url) => {
if (url === null) {
if (confirm('Are you sure you want to remove avatar?')) {
if (confirm('Are you sure that you want to remove avatar?')) {
mx.setAvatarUrl('');
setAvatarSrc(null);
}
@@ -39,48 +48,72 @@ function ProfileEditor({
}
mx.setAvatarUrl(url);
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
}
};
function saveDisplayName() {
const saveDisplayName = () => {
const newDisplayName = displayNameRef.current.value;
if (newDisplayName !== null && newDisplayName !== username) {
mx.setDisplayName(newDisplayName);
username = newDisplayName;
setUsername(newDisplayName);
setDisabled(true);
setIsEditing(false);
}
}
};
function onDisplayNameInputChange() {
const onDisplayNameInputChange = () => {
setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
}
function cancelDisplayNameChanges() {
};
const cancelDisplayNameChanges = () => {
displayNameRef.current.value = username;
onDisplayNameInputChange();
}
setIsEditing(false);
};
return (
const renderForm = () => (
<form
className="profile-editor"
className="profile-editor__form"
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
>
<Input
label={`Display name of ${mx.getUserId()}`}
onChange={onDisplayNameInputChange}
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</form>
);
const renderInfo = () => (
<div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
<div>
<Text variant="h2" primary weight="medium">{twemojify(username)}</Text>
<IconButton
src={PencilIC}
size="extra-small"
tooltip="Edit"
onClick={() => setIsEditing(true)}
/>
</div>
<Text variant="b2">{mx.getUserId()}</Text>
</div>
);
return (
<div className="profile-editor">
<ImageUpload
text={username}
bgColor={bgColor}
bgColor={colorMXID(userId)}
imageSrc={avatarSrc}
onUpload={handleAvatarUpload}
onRequestRemove={() => handleAvatarUpload(null)}
/>
<div className="profile-editor__input-wrapper">
<Input
label={`Display name of ${mx.getUserId()}`}
onChange={onDisplayNameInputChange}
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</div>
</form>
{
isEditing ? renderForm() : renderInfo()
}
</div>
);
}
@@ -1,28 +1,41 @@
@use '../../partials/dir';
@use '../../partials/flex';
.profile-editor {
display: flex;
align-items: flex-start;
align-items: flex-end;
}
.profile-editor__input-wrapper {
flex: 1;
min-width: 0;
margin-top: 10px;
.profile-editor__info,
.profile-editor__form {
@extend .cp-fx__item-one;
@include dir.side(margin, var(--sp-loose), 0);
display: flex;
align-items: flex-end;
}
.profile-editor__info {
flex-direction: column;
& > div:first-child {
display: flex;
align-items: center;
}
.ic-btn {
margin: 0 var(--sp-extra-tight);
}
}
.profile-editor__form {
margin-top: 10px;
flex-wrap: wrap;
align-items: flex-end;
& > .input-container {
flex: 1;
@extend .cp-fx__item-one;
}
& > button {
height: 46px;
margin-top: var(--sp-normal);
}
& > * {
@include dir.side(margin, var(--sp-normal), 0);
}
}
@@ -4,7 +4,7 @@ import './PublicRooms.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { selectRoom } from '../../../client/action/navigation';
import { selectRoom, selectTab } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room';
import Text from '../../atoms/text/Text';
@@ -179,7 +179,9 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
}, [joiningRooms]);
function handleViewRoom(roomId) {
selectRoom(roomId);
const room = initMatrix.matrixClient.getRoom(roomId);
if (room.isSpaceRoom()) selectTab(roomId);
else selectRoom(roomId);
onRequestClose();
}
@@ -193,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.roomList.rooms.has(room.room_id);
const isJoined = initMatrix.matrixClient.getRoom(room.room_id) !== null;
return (
<RoomTile
key={room.room_id}
+4
View File
@@ -8,6 +8,8 @@ import Search from '../search/Search';
import ViewSource from '../view-source/ViewSource';
import CreateRoom from '../create-room/CreateRoom';
import ReusableDialog from '../../molecules/dialog/ReusableDialog';
function Dialogs() {
return (
<>
@@ -18,6 +20,8 @@ function Dialogs() {
<CreateRoom />
<SpaceAddExisting />
<Search />
<ReusableDialog />
</>
);
}
+1 -10
View File
@@ -18,7 +18,6 @@ function Windows() {
const [inviteUser, changeInviteUser] = useState({
isOpen: false, roomId: undefined, term: undefined,
});
const [settings, changeSettings] = useState(false);
function openInviteList() {
changeInviteList(true);
@@ -36,20 +35,15 @@ function Windows() {
searchTerm,
});
}
function openSettings() {
changeSettings(true);
}
useEffect(() => {
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
return () => {
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
};
}, []);
@@ -70,10 +64,7 @@ function Windows() {
searchTerm={inviteUser.searchTerm}
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
/>
<Settings
isOpen={settings}
onRequestClose={() => changeSettings(false)}
/>
<Settings />
<SpaceSettings />
<SpaceManage />
</>
@@ -64,9 +64,11 @@ function ReadReceipts() {
onRequestClose={() => setIsOpen(false)}
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
>
{
readers.map(renderPeople)
}
<div style={{ marginTop: 'var(--sp-tight)', marginBottom: 'var(--sp-extra-loose)' }}>
{
readers.map(renderPeople)
}
</div>
</Dialog>
);
}
+2 -21
View File
@@ -9,6 +9,7 @@ import { getPowerLabel, getUsernameOfRoomMember } from '../../../util/matrixUtil
import colorMXID from '../../../util/colorMXID';
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Header, { TitleWrapper } from '../../atoms/header/Header';
@@ -24,26 +25,6 @@ import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function AtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
function sortByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
function simplyfiMembers(members) {
const mx = initMatrix.matrixClient;
return members.map((member) => ({
@@ -111,7 +92,7 @@ function PeopleDrawer({ roomId }) {
setMemberList(
simplyfiMembers(
getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel),
.sort(memberByAtoZ).sort(memberByPowerLevel),
),
);
};
+1 -1
View File
@@ -26,7 +26,7 @@ function Room() {
roomInfo.roomTimeline?.removeInternalListeners();
if (mx.getRoom(rId)) {
setRoomInfo({
roomTimeline: new RoomTimeline(rId, initMatrix.notifications),
roomTimeline: new RoomTimeline(rId),
eventId: eId ?? null,
});
} else {
+2 -2
View File
@@ -86,7 +86,7 @@ function GeneralSettings({ roomId }) {
<MenuItem
variant="danger"
onClick={() => {
if (confirm('Are you really want to leave this room?')) {
if (confirm('Are you sure that you want to leave this room?')) {
roomActions.leave(roomId);
}
}}
@@ -123,7 +123,7 @@ function SecuritySettings({ roomId }) {
<RoomEncryption roomId={roomId} />
</div>
<div className="room-settings__card">
<MenuHeader>Message history visibility (Who can read history)</MenuHeader>
<MenuHeader>Message history visibility</MenuHeader>
<RoomHistoryVisibility roomId={roomId} />
</div>
</>
@@ -21,6 +21,7 @@ import AsyncSearch from '../../../util/AsyncSearch';
import Text from '../../atoms/text/Text';
import ScrollView from '../../atoms/scroll/ScrollView';
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
import { addRecentEmoji } from '../emoji-board/recent';
const commands = [{
name: 'markdown',
@@ -237,6 +238,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
viewEvent.emit('cmd_fired');
}
if (myCmd.prefix === ':') {
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
viewEvent.emit('cmd_fired', {
replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
});
+1 -1
View File
@@ -10,7 +10,7 @@
&__info {
display: flex;
width: 40px;
@include dir.side(margin, 10px, 14px);
@include dir.side(margin, 14px, 10px);
& > * {
margin: auto;
+8 -7
View File
@@ -14,6 +14,7 @@ import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { openProfileViewer } from '../../../client/action/navigation';
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
import { markAsRead } from '../../../client/action/notifications';
import Divider from '../../atoms/divider/Divider';
import ScrollView from '../../atoms/scroll/ScrollView';
@@ -62,7 +63,7 @@ function genRoomIntro(mEvent, roomTimeline) {
avatarSrc={avatarSrc}
name={roomTimeline.room.name}
heading={`Welcome to ${roomTimeline.room.name}`}
desc={`This is the beginning of ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
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}
/>
);
@@ -253,7 +254,7 @@ function useHandleScroll(
);
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
if (isAtBottom && readUptoEvtStore.getItem()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
});
autoPaginate();
@@ -263,7 +264,7 @@ function useHandleScroll(
const timelineScroll = timelineScrollRef.current;
const limit = eventLimitRef.current;
if (readUptoEvtStore.getItem()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
if (roomTimeline.isServingLiveTimeline()) {
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
@@ -286,7 +287,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
const limit = eventLimitRef.current;
const trySendReadReceipt = (event) => {
if (myUserId === event.getSender()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
return;
}
const readUpToEvent = readUptoEvtStore.getItem();
@@ -295,7 +296,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
if (isUnread === false) {
if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
} else {
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
}
@@ -305,7 +306,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
const { timeline } = roomTimeline;
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
if (unreadMsgIsLast) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
};
@@ -399,7 +400,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
const readUpToId = roomTimeline.getReadUpToEventId();
if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
requestAnimationFrame(() => roomTimeline.markAllAsRead());
requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
}
}
jumpToItemIndex = -1;
+15 -20
View File
@@ -5,12 +5,13 @@ import './RoomViewFloating.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { markAsRead } from '../../../client/action/notifications';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import MessageIC from '../../../../public/res/ic/outlined/message.svg';
import MessageUnreadIC from '../../../../public/res/ic/outlined/message-unread.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsersActionJsx } from './common';
@@ -23,7 +24,7 @@ function useJumpToEvent(roomTimeline) {
};
const cancelJumpToEvent = () => {
roomTimeline.markAllAsRead();
markAsRead(roomTimeline.roomId);
setEventId(null);
};
@@ -36,11 +37,12 @@ function useJumpToEvent(roomTimeline) {
setEventId(readEventId);
}
const { notifications } = initMatrix;
const handleMarkAsRead = () => setEventId(null);
roomTimeline.on(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead);
notifications.on(cons.events.notifications.FULL_READ, handleMarkAsRead);
return () => {
roomTimeline.removeListener(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead);
notifications.removeListener(cons.events.notifications.FULL_READ, handleMarkAsRead);
setEventId(null);
};
}, [roomTimeline]);
@@ -96,28 +98,21 @@ function RoomViewFloating({
return (
<>
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
<Button onClick={jumpToEvent} variant="primary">
<Text variant="b2">Jump to unread</Text>
<Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
<Text variant="b3" weight="medium">Jump to unread messages</Text>
</Button>
<Button iconSrc={TickMarkIC} onClick={cancelJumpToEvent} variant="primary">
<Text variant="b3" weight="bold">Mark as read</Text>
</Button>
<IconButton
onClick={cancelJumpToEvent}
variant="primary"
size="extra-small"
src={TickMarkIC}
tooltipPlacement="bottom"
tooltip="Mark as read"
/>
</div>
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
<div className="bouncing-loader"><div /></div>
<Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
</div>
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
<IconButton
onClick={handleScrollToBottom}
src={ChevronBottomIC}
tooltip="Scroll to Bottom"
/>
<Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
<Text variant="b3" weight="medium">Jump to latest</Text>
</Button>
</div>
</>
);
+27 -24
View File
@@ -72,51 +72,54 @@
}
}
&__STB,
&__unread {
overflow: hidden;
background-color: var(--bg-surface-low);
border-radius: var(--bo-radius);
& button {
justify-content: flex-start;
border-radius: 0;
box-shadow: none;
padding: 6px var(--sp-tight);
& .ic-raw {
width: 16px;
height: 16px;
}
}
}
&__STB {
position: absolute;
@include dir.prop(right, var(--sp-normal), unset);
@include dir.prop(left, unset, var(--sp-normal));
@include dir.prop(left, 50%, unset);
@include dir.prop(right, unset, 50%);
bottom: 0;
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border);
background-color: var(--bg-surface-low);
transition: transform 200ms ease-in-out;
transform: translateY(100%) scale(0);
transform: translate(-50%, 100%);
&--open {
transform: translateY(-28px) scale(1);
transform: translate(-50%, -28px);
}
}
&__unread {
position: absolute;
top: var(--sp-extra-tight);
@include dir.prop(right, var(--sp-extra-tight), unset);
@include dir.prop(left, unset, var(--sp-extra-tight));
@include dir.prop(left, var(--sp-normal), unset);
@include dir.prop(right, unset, var(--sp-normal));
z-index: 999;
display: none;
background-color: var(--bg-surface);
border-radius: var(--bo-radius);
box-shadow: var(--bs-primary-border);
overflow: hidden;
width: calc(100% - var(--sp-extra-loose));
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%);
&--open {
display: flex;
}
& .ic-btn {
padding: 6px var(--sp-extra-tight);
border-radius: 0;
}
& .btn-primary {
& button:first-child {
@extend .cp-fx__item-one;
@include dir.side(margin, 0, 1px);
border-radius: 0;
padding: 0 var(--sp-tight);
&:focus {
background-color: var(--bg-primary-hover);
}
}
}
}
+30 -33
View File
@@ -7,6 +7,7 @@ import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch';
import { selectRoom, selectTab } from '../../../client/action/navigation';
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
import { roomIdByActivity } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -47,27 +48,24 @@ function useVisiblityToggle(setResult) {
return [isOpen, requestClose];
}
function mapRoomIds(roomIds, type) {
function mapRoomIds(roomIds) {
const mx = initMatrix.matrixClient;
const { directs, roomIdToParents } = initMatrix.roomList;
return roomIds.map((roomId) => {
let roomType = type;
if (!roomType) {
roomType = directs.has(roomId) ? 'direct' : 'room';
}
const room = mx.getRoom(roomId);
const parentSet = roomIdToParents.get(roomId);
const parentNames = parentSet
? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
: undefined;
const parentNames = parentSet ? [] : undefined;
parentSet?.forEach((parentId) => parentNames.push(mx.getRoom(parentId).name));
const parents = parentNames ? parentNames.join(', ') : null;
let type = 'room';
if (room.isSpaceRoom()) type = 'space';
else if (directs.has(roomId)) type = 'direct';
return ({
type: roomType,
type,
name: room.name,
parents,
roomId,
@@ -93,34 +91,32 @@ function Search() {
const generateResults = (term) => {
const prefix = term.match(/^[#@*]/)?.[0];
if (term.length === 1) {
const { roomList } = initMatrix;
const spaces = mapRoomIds([...roomList.spaces], 'space').reverse();
const rooms = mapRoomIds([...roomList.rooms], 'room').reverse();
const directs = mapRoomIds([...roomList.directs], 'direct').reverse();
if (prefix === '*') {
asyncSearch.setup(spaces, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(spaces, '*');
} else if (prefix === '#') {
asyncSearch.setup(rooms, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(rooms, '#');
} else if (prefix === '@') {
asyncSearch.setup(directs, { keys: 'name', isContain: true, limit: 20 });
handleSearchResults(directs, '@');
} else {
const dataList = spaces.concat(rooms, directs);
asyncSearch.setup(dataList, { keys: 'name', isContain: true, limit: 20 });
asyncSearch.search(term);
}
} else {
if (term.length > 1) {
asyncSearch.search(prefix ? term.slice(1) : term);
return;
}
const { spaces, rooms, directs } = initMatrix.roomList;
let ids = null;
if (prefix) {
if (prefix === '#') ids = [...rooms];
else if (prefix === '@') ids = [...directs];
else ids = [...spaces];
} else {
ids = [...rooms].concat([...directs], [...spaces]);
}
ids.sort(roomIdByActivity);
const mappedIds = mapRoomIds(ids);
asyncSearch.setup(mappedIds, { keys: 'name', isContain: true, limit: 20 });
if (prefix) handleSearchResults(mappedIds, prefix);
else asyncSearch.search(term);
};
const loadRecentRooms = () => {
const { recentRooms } = navigation;
handleSearchResults(mapRoomIds(recentRooms).reverse(), '');
handleSearchResults(mapRoomIds(recentRooms).reverse());
};
const handleAfterOpen = () => {
@@ -154,6 +150,7 @@ function Search() {
else {
searchRef.current.value = '';
searchRef.current.focus();
loadRecentRooms();
}
};
+117
View File
@@ -0,0 +1,117 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './AuthRequest.scss';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import { useStore } from '../../hooks/useStore';
let lastUsedPassword;
const getAuthId = (password) => ({
type: 'm.login.password',
password,
identifier: {
type: 'm.id.user',
user: initMatrix.matrixClient.getUserId(),
},
});
function AuthRequest({ onComplete, makeRequest }) {
const [status, setStatus] = useState(false);
const mountStore = useStore();
const handleForm = async (e) => {
mountStore.setItem(true);
e.preventDefault();
const password = e.target.password.value;
if (password.trim() === '') return;
try {
setStatus({ ongoing: true });
await makeRequest(getAuthId(password));
lastUsedPassword = password;
if (!mountStore.getItem()) return;
onComplete(true);
} catch (err) {
lastUsedPassword = undefined;
if (!mountStore.getItem()) return;
if (err.errcode === 'M_FORBIDDEN') {
setStatus({ error: 'Wrong password. Please enter correct password.' });
return;
}
setStatus({ error: 'Request failed!' });
}
};
const handleChange = () => {
setStatus(false);
};
return (
<div className="auth-request">
<form onSubmit={handleForm}>
<Input
name="password"
label="Account password"
type="password"
onChange={handleChange}
required
/>
{status.ongoing && <Spinner size="small" />}
{status.error && <Text variant="b3">{status.error}</Text>}
{(status === false || status.error) && <Button variant="primary" type="submit" disabled={!!status.error}>Continue</Button>}
</form>
</div>
);
}
AuthRequest.propTypes = {
onComplete: PropTypes.func.isRequired,
makeRequest: PropTypes.func.isRequired,
};
/**
* @param {string} title Title of dialog
* @param {(auth) => void} makeRequest request to make
* @returns {Promise<boolean>} whether the request succeed or not.
*/
export const authRequest = async (title, makeRequest) => {
try {
const auth = lastUsedPassword ? getAuthId(lastUsedPassword) : undefined;
await makeRequest(auth);
return true;
} catch (e) {
lastUsedPassword = undefined;
if (e.httpStatus !== 401 || e.data?.flows === undefined) return false;
const { flows } = e.data;
const canUsePassword = flows.find((f) => f.stages.includes('m.login.password'));
if (!canUsePassword) return false;
return new Promise((resolve) => {
let isCompleted = false;
openReusableDialog(
<Text variant="s1" weight="medium">{title}</Text>,
(requestClose) => (
<AuthRequest
onComplete={(done) => {
isCompleted = true;
resolve(done);
requestClose();
}}
makeRequest={makeRequest}
/>
),
() => {
if (!isCompleted) resolve(false);
},
);
});
}
};
export default AuthRequest;
@@ -0,0 +1,12 @@
.auth-request {
padding: var(--sp-normal);
& form > *:not(:first-child) {
margin-top: var(--sp-normal);
}
& .text-b3 {
color: var(--tc-danger-high);
margin-top: var(--sp-ultra-tight) !important;
}
}
+223
View File
@@ -0,0 +1,223 @@
/* eslint-disable react/jsx-one-expression-per-line */
import React, { useState } from 'react';
import './CrossSigning.scss';
import FileSaver from 'file-saver';
import { Formik } from 'formik';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import { copyToClipboard } from '../../../util/common';
import { clearSecretStorageKeys } from '../../../client/state/secretStorageKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import { authRequest } from './AuthRequest';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
const failedDialog = () => {
const renderFailure = (requestClose) => (
<div className="cross-signing__failure">
<Text variant="h1">{twemojify('❌')}</Text>
<Text weight="medium">Failed to setup cross signing. Please try again.</Text>
<Button onClick={requestClose}>Close</Button>
</div>
);
openReusableDialog(
<Text variant="s1" weight="medium">Setup cross signing</Text>,
renderFailure,
);
};
const securityKeyDialog = (key) => {
const downloadKey = () => {
const blob = new Blob([key.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'security-key.txt');
};
const copyKey = () => {
copyToClipboard(key.encodedPrivateKey);
};
const renderSecurityKey = () => (
<div className="cross-signing__key">
<Text weight="medium">Please save this security key somewhere safe.</Text>
<Text className="cross-signing__key-text">
{key.encodedPrivateKey}
</Text>
<div className="cross-signing__key-btn">
<Button variant="primary" onClick={() => copyKey(key)}>Copy</Button>
<Button onClick={() => downloadKey(key)}>Download</Button>
</div>
</div>
);
// Download automatically.
downloadKey();
openReusableDialog(
<Text variant="s1" weight="medium">Security Key</Text>,
() => renderSecurityKey(),
);
};
function CrossSigningSetup() {
const initialValues = { phrase: '', confirmPhrase: '' };
const [genWithPhrase, setGenWithPhrase] = useState(undefined);
const setup = async (securityPhrase = undefined) => {
const mx = initMatrix.matrixClient;
setGenWithPhrase(typeof securityPhrase === 'string');
const recoveryKey = await mx.createRecoveryKeyFromPassphrase(securityPhrase);
clearSecretStorageKeys();
await mx.bootstrapSecretStorage({
createSecretStorageKey: async () => recoveryKey,
setupNewKeyBackup: true,
setupNewSecretStorage: true,
});
const authUploadDeviceSigningKeys = async (makeRequest) => {
const isDone = await authRequest('Setup cross signing', async (auth) => {
await makeRequest(auth);
});
setTimeout(() => {
if (isDone) securityKeyDialog(recoveryKey);
else failedDialog();
});
};
await mx.bootstrapCrossSigning({
authUploadDeviceSigningKeys,
setupNewCrossSigning: true,
});
};
const validator = (values) => {
const errors = {};
if (values.phrase === '12345678') {
errors.phrase = 'How about 87654321 ?';
}
if (values.phrase === '87654321') {
errors.phrase = 'Your are playing with 🔥';
}
const PHRASE_REGEX = /^([^\s]){8,127}$/;
if (values.phrase.length > 0 && !PHRASE_REGEX.test(values.phrase)) {
errors.phrase = 'Phrase must contain 8-127 characters with no space.';
}
if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
errors.confirmPhrase = 'Phrase don\'t match.';
}
return errors;
};
return (
<div className="cross-signing__setup">
<div className="cross-signing__setup-entry">
<Text>
We will generate a <b>Security Key</b>,
which you can use to manage messages backup and session verification.
</Text>
{genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>}
{genWithPhrase === false && <Spinner size="small" />}
</div>
<Text className="cross-signing__setup-divider">OR</Text>
<Formik
initialValues={initialValues}
onSubmit={(values) => setup(values.phrase)}
validate={validator}
>
{({
values, errors, handleChange, handleSubmit,
}) => (
<form
className="cross-signing__setup-entry"
onSubmit={handleSubmit}
disabled={genWithPhrase !== undefined}
>
<Text>
Alternatively you can also set a <b>Security Phrase </b>
so you don't have to remember long Security Key,
and optionally save the Key as backup.
</Text>
<Input
name="phrase"
value={values.phrase}
onChange={handleChange}
label="Security Phrase"
type="password"
required
disabled={genWithPhrase !== undefined}
/>
{errors.phrase && <Text variant="b3" className="cross-signing__error">{errors.phrase}</Text>}
<Input
name="confirmPhrase"
value={values.confirmPhrase}
onChange={handleChange}
label="Confirm Security Phrase"
type="password"
required
disabled={genWithPhrase !== undefined}
/>
{errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>}
{genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>Set Phrase & Generate Key</Button>}
{genWithPhrase === true && <Spinner size="small" />}
</form>
)}
</Formik>
</div>
);
}
const setupDialog = () => {
openReusableDialog(
<Text variant="s1" weight="medium">Setup cross signing</Text>,
() => <CrossSigningSetup />,
);
};
function CrossSigningReset() {
return (
<div className="cross-signing__reset">
<Text variant="h1">{twemojify('✋🧑‍🚒🤚')}</Text>
<Text weight="medium">Resetting cross-signing keys is permanent.</Text>
<Text>
Anyone you have verified with will see security alerts and your message backup will lost.
You almost certainly do not want to do this,
unless you have lost <b>Security Key</b> or <b>Phrase</b> and
every session you can cross-sign from.
</Text>
<Button variant="danger" onClick={setupDialog}>Reset</Button>
</div>
);
}
const resetDialog = () => {
openReusableDialog(
<Text variant="s1" weight="medium">Reset cross signing</Text>,
() => <CrossSigningReset />,
);
};
function CrossSignin() {
const isCSEnabled = useCrossSigningStatus();
return (
<SettingTile
title="Cross signing"
content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
options={(
isCSEnabled
? <Button variant="danger" onClick={resetDialog}>Reset</Button>
: <Button variant="primary" onClick={setupDialog}>Setup</Button>
)}
/>
);
}
export default CrossSignin;
@@ -0,0 +1,55 @@
.cross-signing {
&__setup {
padding: var(--sp-normal);
}
&__setup-entry {
& > *:not(:first-child) {
margin-top: var(--sp-normal);
}
}
&__error {
color: var(--tc-danger-high);
margin-top: var(--sp-ultra-tight) !important;
}
&__setup-divider {
margin: var(--sp-tight) 0;
display: flex;
align-items: center;
&::before,
&::after {
flex: 1;
content: '';
margin: var(--sp-tight) 0;
border-bottom: 1px solid var(--bg-surface-border);
}
}
}
.cross-signing__key {
padding: var(--sp-normal);
&-text {
margin: var(--sp-normal) 0;
padding: var(--sp-extra-tight);
background-color: var(--bg-surface-low);
border-radius: var(--bo-radius);
}
&-btn {
display: flex;
& > button:last-child {
margin: 0 var(--sp-normal);
}
}
}
.cross-signing__failure,
.cross-signing__reset {
padding: var(--sp-normal);
padding-top: var(--sp-extra-loose);
& > .text {
padding-bottom: var(--sp-normal);
}
}
+174
View File
@@ -0,0 +1,174 @@
import React, { useState, useEffect } from 'react';
import './DeviceManage.scss';
import dateFormat from 'dateformat';
import initMatrix from '../../../client/initMatrix';
import { isCrossVerified } from '../../../util/matrixUtil';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import InfoCard from '../../atoms/card/InfoCard';
import Spinner from '../../atoms/spinner/Spinner';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
import { authRequest } from './AuthRequest';
import { useStore } from '../../hooks/useStore';
import { useDeviceList } from '../../hooks/useDeviceList';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
function DeviceManage() {
const TRUNCATED_COUNT = 4;
const mx = initMatrix.matrixClient;
const isCSEnabled = useCrossSigningStatus();
const deviceList = useDeviceList();
const [processing, setProcessing] = useState([]);
const [truncated, setTruncated] = useState(true);
const mountStore = useStore();
mountStore.setItem(true);
useEffect(() => {
setProcessing([]);
}, [deviceList]);
const addToProcessing = (device) => {
const old = [...processing];
old.push(device.device_id);
setProcessing(old);
};
const removeFromProcessing = () => {
setProcessing([]);
};
if (deviceList === null) {
return (
<div className="device-manage">
<div className="device-manage__loading">
<Spinner size="small" />
<Text>Loading devices...</Text>
</div>
</div>
);
}
const handleRename = async (device) => {
const newName = window.prompt('Edit session name', device.display_name);
if (newName === null || newName.trim() === '') return;
if (newName.trim() === device.display_name) return;
addToProcessing(device);
try {
await mx.setDeviceDetails(device.device_id, {
display_name: newName,
});
} catch {
if (!mountStore.getItem()) return;
removeFromProcessing(device);
}
};
const handleRemove = async (device) => {
if (window.confirm(`You are about to logout "${device.display_name}" session.`)) {
addToProcessing(device);
await authRequest(`Logout "${device.display_name}"`, async (auth) => {
await mx.deleteDevice(device.device_id, auth);
});
if (!mountStore.getItem()) return;
removeFromProcessing(device);
}
};
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;
return (
<SettingTile
key={deviceId}
title={(
<Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}>
{displayName}
<Text variant="b3" span>{` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text>
</Text>
)}
options={
processing.includes(deviceId)
? <Spinner size="small" />
: (
<>
<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>
)}
/>
);
};
const unverified = [];
const verified = [];
deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
if (isCrossVerified(device.device_id)) verified.push(device);
else unverified.push(device);
});
return (
<div className="device-manage">
<div>
<MenuHeader>Unverified sessions</MenuHeader>
{!isCSEnabled && (
<div style={{ padding: 'var(--sp-extra-tight) var(--sp-normal)' }}>
<InfoCard
rounded
variant="caution"
iconSrc={InfoIC}
title="Setup cross signing in case you lose all your sessions."
/>
</div>
)}
{
unverified.length > 0
? unverified.map((device) => renderDevice(device, false))
: <Text className="device-manage__info">No unverified session</Text>
}
</div>
<div>
<MenuHeader>Verified sessions</MenuHeader>
{
verified.length > 0
? verified.map((device, index) => {
if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true);
})
: <Text className="device-manage__info">No verified session</Text>
}
{ verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
{truncated ? `View ${verified.length - 4} more` : 'View less'}
</Button>
)}
{ deviceList.length > 0 && (
<Text className="device-manage__info" variant="b3">Session names are visible to everyone, so do not put any private info here.</Text>
)}
</div>
</div>
);
}
export default DeviceManage;
@@ -0,0 +1,18 @@
@use '../../partials/flex';
.device-manage {
&__loading {
@extend .cp-fx__row--c-c;
padding: var(--sp-extra-loose) var(--sp-normal);
.text {
margin: 0 var(--sp-normal);
}
}
&__info {
margin: var(--sp-normal);
}
& .setting-tile:last-of-type {
border-bottom: none;
}
}
+288
View File
@@ -0,0 +1,288 @@
/* eslint-disable react/prop-types */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './KeyBackup.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import { deletePrivateKey } from '../../../client/state/secretStorageKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import Spinner from '../../atoms/spinner/Spinner';
import InfoCard from '../../atoms/card/InfoCard';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import { accessSecretStorage } from './SecretStorageAccess';
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import DownloadIC from '../../../../public/res/ic/outlined/download.svg';
import { useStore } from '../../hooks/useStore';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
function CreateKeyBackupDialog({ keyData }) {
const [done, setDone] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const doBackup = async () => {
setDone(false);
let info;
try {
info = await mx.prepareKeyBackupVersion(
null,
{ secureSecretStorage: true },
);
info = await mx.createKeyBackupVersion(info);
await mx.scheduleAllGroupSessionsForBackup();
if (!mountStore.getItem()) return;
setDone(true);
} catch (e) {
deletePrivateKey(keyData.keyId);
await mx.deleteKeyBackupVersion(info.version);
if (!mountStore.getItem()) return;
setDone(null);
}
};
useEffect(() => {
mountStore.setItem(true);
doBackup();
}, []);
return (
<div className="key-backup__create">
{done === false && (
<div>
<Spinner size="small" />
<Text>Creating backup...</Text>
</div>
)}
{done === true && (
<>
<Text variant="h1">{twemojify('✅')}</Text>
<Text>Successfully created backup</Text>
</>
)}
{done === null && (
<>
<Text>Failed to create backup</Text>
<Button onClick={doBackup}>Retry</Button>
</>
)}
</div>
);
}
CreateKeyBackupDialog.propTypes = {
keyData: PropTypes.shape({}).isRequired,
};
function RestoreKeyBackupDialog({ keyData }) {
const [status, setStatus] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const restoreBackup = async () => {
setStatus(false);
let meBreath = true;
const progressCallback = (progress) => {
if (!progress.successes) return;
if (meBreath === false) return;
meBreath = false;
setTimeout(() => {
meBreath = true;
}, 200);
setStatus({ message: `Restoring backup keys... (${progress.successes}/${progress.total})` });
};
try {
const backupInfo = await mx.getKeyBackupVersion();
const info = await mx.restoreKeyBackupWithSecretStorage(
backupInfo,
undefined,
undefined,
{ progressCallback },
);
if (!mountStore.getItem()) return;
setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
} catch (e) {
if (!mountStore.getItem()) return;
if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') {
deletePrivateKey(keyData.keyId);
setStatus({ error: 'Failed to restore backup. Key is invalid!', errorCode: 'BAD_KEY' });
} else {
setStatus({ error: 'Failed to restore backup.', errCode: 'UNKNOWN' });
}
}
};
useEffect(() => {
mountStore.setItem(true);
restoreBackup();
}, []);
return (
<div className="key-backup__restore">
{(status === false || status.message) && (
<div>
<Spinner size="small" />
<Text>{status.message ?? 'Restoring backup keys...'}</Text>
</div>
)}
{status.done && (
<>
<Text variant="h1">{twemojify('✅')}</Text>
<Text>{status.done}</Text>
</>
)}
{status.error && (
<>
<Text>{status.error}</Text>
<Button onClick={restoreBackup}>Retry</Button>
</>
)}
</div>
);
}
RestoreKeyBackupDialog.propTypes = {
keyData: PropTypes.shape({}).isRequired,
};
function DeleteKeyBackupDialog({ requestClose }) {
const [isDeleting, setIsDeleting] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
mountStore.setItem(true);
const deleteBackup = async () => {
setIsDeleting(true);
try {
const backupInfo = await mx.getKeyBackupVersion();
if (backupInfo) await mx.deleteKeyBackupVersion(backupInfo.version);
if (!mountStore.getItem()) return;
requestClose(true);
} catch {
if (!mountStore.getItem()) return;
setIsDeleting(false);
}
};
return (
<div className="key-backup__delete">
<Text variant="h1">{twemojify('🗑')}</Text>
<Text weight="medium">Deleting key backup is permanent.</Text>
<Text>All encrypted messages keys stored on server will be deleted.</Text>
{
isDeleting
? <Spinner size="small" />
: <Button variant="danger" onClick={deleteBackup}>Delete</Button>
}
</div>
);
}
DeleteKeyBackupDialog.propTypes = {
requestClose: PropTypes.func.isRequired,
};
function KeyBackup() {
const mx = initMatrix.matrixClient;
const isCSEnabled = useCrossSigningStatus();
const [keyBackup, setKeyBackup] = useState(undefined);
const mountStore = useStore();
const fetchKeyBackupVersion = async () => {
const info = await mx.getKeyBackupVersion();
if (!mountStore.getItem()) return;
setKeyBackup(info);
};
useEffect(() => {
mountStore.setItem(true);
fetchKeyBackupVersion();
const handleAccountData = (event) => {
if (event.getType() === 'm.megolm_backup.v1') {
fetchKeyBackupVersion();
}
};
mx.on('accountData', handleAccountData);
return () => {
mx.removeListener('accountData', handleAccountData);
};
}, [isCSEnabled]);
const openCreateKeyBackup = async () => {
const keyData = await accessSecretStorage('Create Key Backup');
if (keyData === null) return;
openReusableDialog(
<Text variant="s1" weight="medium">Create Key Backup</Text>,
() => <CreateKeyBackupDialog keyData={keyData} />,
() => fetchKeyBackupVersion(),
);
};
const openRestoreKeyBackup = async () => {
const keyData = await accessSecretStorage('Restore Key Backup');
if (keyData === null) return;
openReusableDialog(
<Text variant="s1" weight="medium">Restore Key Backup</Text>,
() => <RestoreKeyBackupDialog keyData={keyData} />,
);
};
const openDeleteKeyBackup = () => openReusableDialog(
<Text variant="s1" weight="medium">Delete Key Backup</Text>,
(requestClose) => (
<DeleteKeyBackupDialog
requestClose={(isDone) => {
if (isDone) setKeyBackup(null);
requestClose();
}}
/>
),
);
const renderOptions = () => {
if (keyBackup === undefined) return <Spinner size="small" />;
if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>Create Backup</Button>;
return (
<>
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
</>
);
};
return (
<SettingTile
title="Encrypted messages backup"
content={(
<>
<Text variant="b3">Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.</Text>
{!isCSEnabled && (
<InfoCard
style={{ marginTop: 'var(--sp-ultra-tight)' }}
rounded
variant="caution"
iconSrc={InfoIC}
title="Setup cross signing to backup your encrypted messages."
/>
)}
</>
)}
options={isCSEnabled ? renderOptions() : null}
/>
);
}
export default KeyBackup;
+27
View File
@@ -0,0 +1,27 @@
.key-backup__create,
.key-backup__restore {
padding: var(--sp-normal);
& > div {
padding: var(--sp-normal) 0;
display: flex;
align-items: center;
& > .text {
margin: 0 var(--sp-normal);
}
}
& > .text {
margin-bottom: var(--sp-normal);
}
}
.key-backup__delete {
padding: var(--sp-normal);
padding-top: var(--sp-extra-loose);
& > .text {
padding-bottom: var(--sp-normal);
}
}
@@ -0,0 +1,133 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './SecretStorageAccess.scss';
import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import { getDefaultSSKey, getSSKeyInfo } from '../../../util/matrixUtil';
import { storePrivateKey, hasPrivateKey, getPrivateKey } from '../../../client/state/secretStorageKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import { useStore } from '../../hooks/useStore';
function SecretStorageAccess({ onComplete }) {
const mx = initMatrix.matrixClient;
const sSKeyId = getDefaultSSKey();
const sSKeyInfo = getSSKeyInfo(sSKeyId);
const isPassphrase = !!sSKeyInfo.passphrase;
const [withPhrase, setWithPhrase] = useState(isPassphrase);
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 }) => {
setProcess(true);
try {
const { salt, iterations } = sSKeyInfo.passphrase;
const privateKey = key
? mx.keyBackupKeyFromRecoveryKey(key)
: await deriveKey(phrase, salt, iterations);
const isCorrect = await mx.checkSecretStorageKey(privateKey, sSKeyInfo);
if (!mountStore.getItem()) return;
if (!isCorrect) {
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
setProcess(false);
return;
}
onComplete({
keyId: sSKeyId,
key,
phrase,
privateKey,
});
} catch (e) {
if (!mountStore.getItem()) return;
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
setProcess(false);
}
};
const handleForm = async (e) => {
e.preventDefault();
const password = e.target.password.value;
if (password.trim() === '') return;
const data = {};
if (withPhrase) data.phrase = password;
else data.key = password;
processInput(data);
};
const handleChange = () => {
setError(null);
setProcess(false);
};
return (
<div className="secret-storage-access">
<form onSubmit={handleForm}>
<Input
name="password"
label={`Security ${withPhrase ? 'Phrase' : 'Key'}`}
type="password"
onChange={handleChange}
required
/>
{error && <Text variant="b3">{error}</Text>}
{!process && (
<div className="secret-storage-access__btn">
<Button variant="primary" type="submit">Continue</Button>
{isPassphrase && <Button onClick={toggleWithPhrase}>{`Use Security ${withPhrase ? 'Key' : 'Phrase'}`}</Button>}
</div>
)}
</form>
{process && <Spinner size="small" />}
</div>
);
}
SecretStorageAccess.propTypes = {
onComplete: PropTypes.func.isRequired,
};
/**
* @param {string} title Title of secret storage access dialog
* @returns {Promise<keyData | null>} resolve to keyData or null
*/
export const accessSecretStorage = (title) => new Promise((resolve) => {
let isCompleted = false;
const defaultSSKey = getDefaultSSKey();
if (hasPrivateKey(defaultSSKey)) {
resolve({ keyId: defaultSSKey, privateKey: getPrivateKey(defaultSSKey) });
return;
}
const handleComplete = (keyData) => {
isCompleted = true;
storePrivateKey(keyData.keyId, keyData.privateKey);
resolve(keyData);
};
openReusableDialog(
<Text variant="s1" weight="medium">{title}</Text>,
(requestClose) => (
<SecretStorageAccess
onComplete={(keyData) => {
handleComplete(keyData);
requestClose(requestClose);
}}
/>
),
() => {
if (!isCompleted) resolve(null);
},
);
});
export default SecretStorageAccess;
@@ -0,0 +1,20 @@
.secret-storage-access {
padding: var(--sp-normal);
& form > *:not(:first-child) {
margin-top: var(--sp-normal);
}
& .text-b3 {
color: var(--tc-danger-high);
margin-top: var(--sp-ultra-tight) !important;
}
&__btn {
display: flex;
justify-content: space-between;
}
& .donut-spinner {
margin-top: var(--sp-normal);
}
}
+218 -194
View File
@@ -1,13 +1,13 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import React, { useState, useEffect } from 'react';
import './Settings.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import settings from '../../../client/state/settings';
import navigation from '../../../client/state/navigation';
import {
toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents,
toggleNotifications,
toggleNotifications, toggleNotificationSounds,
} from '../../../client/action/settings';
import logout from '../../../client/action/logout';
import { usePermission } from '../../hooks/usePermission';
@@ -16,16 +16,20 @@ import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Toggle from '../../atoms/button/Toggle';
import Tabs from '../../atoms/tabs/Tabs';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
import PopupWindow, { PWContentSelector } from '../../molecules/popup-window/PopupWindow';
import PopupWindow from '../../molecules/popup-window/PopupWindow';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
import ProfileEditor from '../profile-editor/ProfileEditor';
import CrossSigning from './CrossSigning';
import KeyBackup from './KeyBackup';
import DeviceManage from './DeviceManage';
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
import BellIC from '../../../../public/res/ic/outlined/bell.svg';
@@ -35,85 +39,74 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import CinnySVG from '../../../../public/res/svg/cinny.svg';
function GeneralSection() {
return (
<div className="settings-content">
<SettingTile
title=""
content={(
<ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
)}
/>
</div>
);
}
function AppearanceSection() {
const [, updateState] = useState({});
return (
<div className="settings-content">
<SettingTile
title="Follow system theme"
options={(
<Toggle
isActive={settings.useSystemTheme}
onToggle={() => { toggleSystemTheme(); updateState({}); }}
/>
)}
content={<Text variant="b3">Use light or dark mode based on the system's settings.</Text>}
/>
{(() => {
if (!settings.useSystemTheme) {
return (
<SettingTile
title="Theme"
content={(
<SegmentedControls
selected={settings.getThemeIndex()}
segments={[
{ text: 'Light' },
{ text: 'Silver' },
{ text: 'Dark' },
{ text: 'Butter' },
]}
onSelect={(index) => settings.setTheme(index)}
/>
)}
<div className="settings-appearance">
<div className="settings-appearance__card">
<MenuHeader>Theme</MenuHeader>
<SettingTile
title="Follow system theme"
options={(
<Toggle
isActive={settings.useSystemTheme}
onToggle={() => { toggleSystemTheme(); updateState({}); }}
/>
);
}
})()}
<SettingTile
title="Markdown formatting"
options={(
<Toggle
isActive={settings.isMarkdown}
onToggle={() => { toggleMarkdown(); updateState({}); }}
)}
content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
/>
{!settings.useSystemTheme && (
<SettingTile
title="Theme"
content={(
<SegmentedControls
selected={settings.getThemeIndex()}
segments={[
{ text: 'Light' },
{ text: 'Silver' },
{ text: 'Dark' },
{ text: 'Butter' },
]}
onSelect={(index) => settings.setTheme(index)}
/>
)}
/>
)}
content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
/>
<SettingTile
title="Hide membership events"
options={(
<Toggle
isActive={settings.hideMembershipEvents}
onToggle={() => { toggleMembershipEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
/>
<SettingTile
title="Hide nick/avatar events"
options={(
<Toggle
isActive={settings.hideNickAvatarEvents}
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
/>
</div>
<div className="settings-appearance__card">
<MenuHeader>Room messages</MenuHeader>
<SettingTile
title="Markdown formatting"
options={(
<Toggle
isActive={settings.isMarkdown}
onToggle={() => { toggleMarkdown(); updateState({}); }}
/>
)}
content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
/>
<SettingTile
title="Hide membership events"
options={(
<Toggle
isActive={settings.hideMembershipEvents}
onToggle={() => { toggleMembershipEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
/>
<SettingTile
title="Hide nick/avatar events"
options={(
<Toggle
isActive={settings.hideNickAvatarEvents}
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
/>
</div>
</div>
);
}
@@ -125,7 +118,7 @@ function NotificationsSection() {
const renderOptions = () => {
if (window.Notification === undefined) {
return <Text className="set-notifications__not-supported">Not supported in this browser.</Text>;
return <Text className="settings-notifications__not-supported">Not supported in this browser.</Text>;
}
if (permission === 'granted') {
@@ -152,11 +145,22 @@ function NotificationsSection() {
};
return (
<div className="set-notifications settings-content">
<div className="settings-notifications">
<MenuHeader>Notification & Sound</MenuHeader>
<SettingTile
title="Show desktop notifications"
title="Desktop notification"
options={renderOptions()}
content={<Text variant="b3">Show notifications when new messages arrive.</Text>}
content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
/>
<SettingTile
title="Notification Sound"
options={(
<Toggle
isActive={settings.isNotificationSounds}
onToggle={() => { toggleNotificationSounds(); updateState({}); }}
/>
)}
content={<Text variant="b3">Play sound when new messages arrive.</Text>}
/>
</div>
);
@@ -164,149 +168,169 @@ function NotificationsSection() {
function SecuritySection() {
return (
<div className="set-security settings-content">
<SettingTile
title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
/>
<SettingTile
title={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
/>
<SettingTile
title="Export E2E room keys"
content={(
<>
<Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
<ExportE2ERoomKeys />
</>
)}
/>
<SettingTile
title="Import E2E room keys"
content={(
<>
<Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
<ImportE2ERoomKeys />
</>
)}
/>
<div className="settings-security">
<div className="settings-security__card">
<MenuHeader>Cross signing and backup</MenuHeader>
<CrossSigning />
<KeyBackup />
</div>
<DeviceManage />
<div className="settings-security__card">
<MenuHeader>Export/Import encryption keys</MenuHeader>
<SettingTile
title="Export E2E room keys"
content={(
<>
<Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
<ExportE2ERoomKeys />
</>
)}
/>
<SettingTile
title="Import E2E room keys"
content={(
<>
<Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
<ImportE2ERoomKeys />
</>
)}
/>
</div>
</div>
);
}
function AboutSection() {
return (
<div className="settings-content set__about">
<div className="set-about__branding">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
<div>
<Text variant="h2" weight='medium'>
Cinny
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
</Text>
<Text>Yet another matrix client</Text>
<div className="settings-about">
<div className="settings-about__card">
<MenuHeader>Application</MenuHeader>
<div className="settings-about__branding">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
<div>
<Text variant="h2" weight="medium">
Cinny
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
</Text>
<Text>Yet another matrix client</Text>
<div className="set-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
<div className="settings-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
</div>
</div>
</div>
</div>
<div className="set-about__credits">
<Text variant="s1" weight="medium">Credits</Text>
<ul>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://github.com/matrix-org/matrix-js-sdk" rel="noreferrer noopener" target="_blank">matrix-js-sdk</a> is © <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">The Matrix.org Foundation C.I.C</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twemoji</a> emoji art is © <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twitter, Inc and other contributors</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
</ul>
<div className="settings-about__card">
<MenuHeader>Credits</MenuHeader>
<div className="settings-about__credits">
<ul>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://github.com/matrix-org/matrix-js-sdk" rel="noreferrer noopener" target="_blank">matrix-js-sdk</a> is © <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">The Matrix.org Foundation C.I.C</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twemoji</a> emoji art is © <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twitter, Inc and other contributors</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
<li>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<Text>The <a href="https://material.io/design/sound/sound-resources.html" target="_blank" rel="noreferrer noopener">Material sound resources</a> are © <a href="https://google.com" target="_blank" rel="noreferrer noopener">Google</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
</li>
</ul>
</div>
</div>
</div>
);
}
function Settings({ isOpen, onRequestClose }) {
const settingSections = [{
name: 'General',
iconSrc: SettingsIC,
render() {
return <GeneralSection />;
},
}, {
name: 'Appearance',
iconSrc: SunIC,
render() {
return <AppearanceSection />;
},
}, {
name: 'Notifications',
iconSrc: BellIC,
render() {
return <NotificationsSection />;
},
}, {
name: 'Security & Privacy',
iconSrc: LockIC,
render() {
return <SecuritySection />;
},
}, {
name: 'Help & About',
iconSrc: InfoIC,
render() {
return <AboutSection />;
},
}];
const [selectedSection, setSelectedSection] = useState(settingSections[0]);
export const tabText = {
APPEARANCE: 'Appearance',
NOTIFICATIONS: 'Notifications',
SECURITY: 'Security',
ABOUT: 'About',
};
const tabItems = [{
text: tabText.APPEARANCE,
iconSrc: SunIC,
disabled: false,
render: () => <AppearanceSection />,
}, {
text: tabText.NOTIFICATIONS,
iconSrc: BellIC,
disabled: false,
render: () => <NotificationsSection />,
}, {
text: tabText.SECURITY,
iconSrc: LockIC,
disabled: false,
render: () => <SecuritySection />,
}, {
text: tabText.ABOUT,
iconSrc: InfoIC,
disabled: false,
render: () => <AboutSection />,
}];
function useWindowToggle(setSelectedTab) {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const openSettings = (tab) => {
const tabItem = tabItems.find((item) => item.text === tab);
if (tabItem) setSelectedTab(tabItem);
setIsOpen(true);
};
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
return () => {
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
};
}, []);
const requestClose = () => setIsOpen(false);
return [isOpen, requestClose];
}
function Settings() {
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
const handleTabChange = (tabItem) => setSelectedTab(tabItem);
const handleLogout = () => {
if (confirm('Confirm logout')) logout();
};
return (
<PopupWindow
className="settings-window"
isOpen={isOpen}
onRequestClose={onRequestClose}
title="Settings"
contentTitle={selectedSection.name}
drawer={(
className="settings-window"
title={<Text variant="s1" weight="medium" primary>Settings</Text>}
contentOptions={(
<>
{
settingSections.map((section) => (
<PWContentSelector
key={section.name}
selected={selectedSection.name === section.name}
onClick={() => setSelectedSection(section)}
iconSrc={section.iconSrc}
>
{section.name}
</PWContentSelector>
))
}
<PWContentSelector
variant="danger"
onClick={handleLogout}
iconSrc={PowerIC}
>
<Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
Logout
</PWContentSelector>
</Button>
<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />
</>
)}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{selectedSection.render()}
{isOpen && (
<div className="settings-window__content">
<ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
<Tabs
items={tabItems}
defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
onSelect={handleTabChange}
/>
<div className="settings-window__cards-wrapper">
{ selectedTab.render() }
</div>
</div>
)}
</PopupWindow>
);
}
Settings.propTypes = {
isOpen: PropTypes.bool.isRequired,
onRequestClose: PropTypes.func.isRequired,
};
export default Settings;
+60 -31
View File
@@ -2,58 +2,87 @@
@use '../../partials/dir';
.settings-window {
& .pw__drawer__content {
@extend .cp-fx__column;
min-height: 100%;
padding-bottom: var(--sp-extra-tight);
& .pw {
background-color: var(--bg-surface-low);
}
.header .btn-danger {
margin: 0 var(--sp-tight);
box-shadow: none;
}
& > .pw-content-selector:last-child {
margin-top: auto;
& .profile-editor {
padding: var(--sp-loose) var(--sp-extra-loose);
}
& .tabs__content {
padding: 0 var(--sp-normal);
}
&__cards-wrapper {
padding: 0 var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
}
}
.settings-window__card {
margin: var(--sp-normal) 0;
background-color: var(--bg-surface);
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border);
overflow: hidden;
& > .context-menu__header:first-child {
margin-top: 2px;
}
}
.settings-appearance__card,
.settings-notifications,
.settings-security__card,
.settings-security .device-manage,
.settings-about__card {
@extend .settings-window__card;
}
.settings-window__cards-wrapper{
& .setting-tile {
margin: 0 var(--sp-normal);
margin-top: var(--sp-normal);
padding-bottom: 16px;
border-bottom: 1px solid var(--bg-surface-border);
&:last-child {
border-bottom: none;
}
}
& .pw__content-container {
min-height: 100%;
}
}
.settings-content {
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
& .setting-tile {
margin-top: var(--sp-normal);
border-bottom: 1px solid var(--bg-surface-border);
padding-bottom: 16px;
}
}
.set-notifications {
.settings-notifications {
&__not-supported {
padding: 0 var(--sp-ultra-tight);
}
}
.set-about {
.settings-about {
&__branding {
margin-top: var(--sp-extra-tight);
margin-bottom: var(--sp-normal);
padding: var(--sp-normal);
display: flex;
& > div {
margin: 0 var(--sp-loose);
}
}
&__btns {
margin: 0;
margin-top: var(--sp-normal);
& button:last-child {
margin: 0 var(--sp-tight)
& button {
margin-top: var(--sp-tight);
@include dir.side(margin, 0, var(--sp-tight));
}
}
&__credits {
margin-top: var(--sp-loose);
padding: 0 var(--sp-normal);
& ul {
color: var(--tc-surface-low);
padding: var(--sp-normal);
margin: var(--sp-extra-tight) 0;
}
}
@@ -6,6 +6,7 @@ import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/accountData';
import { joinRuleToIconSrc } from '../../../util/matrixUtil';
import { roomIdByAtoZ } from '../../../util/sort';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
@@ -20,7 +21,6 @@ import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useSpaceShortcut } from '../../hooks/useSpaceShortcut';
import { AtoZ } from '../navigation/common';
function ShortcutSpacesContent() {
const mx = initMatrix.matrixClient;
@@ -29,7 +29,7 @@ function ShortcutSpacesContent() {
const [spaceShortcut] = useSpaceShortcut();
const spaceWithoutShortcut = [...spaces].filter(
(spaceId) => !spaceShortcut.includes(spaceId),
).sort(AtoZ);
).sort(roomIdByAtoZ);
const [process, setProcess] = useState(null);
const [selected, setSelected] = useState([]);
@@ -90,7 +90,7 @@ function GeneralSettings({ roomId }) {
<MenuItem
variant="danger"
onClick={() => {
if (confirm('Are you really want to leave this space?')) {
if (confirm('Are you sure that you want to leave this space?')) {
leave(roomId);
}
}}
@@ -7,14 +7,10 @@
& .room-profile {
padding: var(--sp-loose) var(--sp-extra-loose);
}
}
& .tabs {
box-shadow: inset 0 -1px 0 var(--bg-surface-border);
&__content {
padding: 0 var(--sp-normal);
}
& .tabs__content {
padding: 0 var(--sp-normal);
}
&__cards-wrapper {
+5
View File
@@ -1,6 +1,9 @@
import React, { useState, useEffect } from 'react';
import './Client.scss';
import { initHotkeys } from '../../../client/event/hotkeys';
import { initRoomListListener } from '../../../client/event/roomList';
import Text from '../../atoms/text/Text';
import Spinner from '../../atoms/spinner/Spinner';
import Navigation from '../../organisms/navigation/Navigation';
@@ -38,6 +41,8 @@ function Client() {
}, 15000);
initMatrix.once('init_loading_finished', () => {
clearInterval(iId);
initHotkeys();
initRoomListListener(initMatrix.roomList);
changeLoading(false);
});
initMatrix.init();
+11 -1
View File
@@ -95,9 +95,10 @@ export function openProfileViewer(userId, roomId) {
});
}
export function openSettings() {
export function openSettings(tabText) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_SETTINGS,
tabText,
});
}
@@ -149,3 +150,12 @@ export function openReusableContextMenu(placement, cords, render, afterClose) {
afterClose,
});
}
export function openReusableDialog(title, render, afterClose) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_REUSABLE_DIALOG,
title,
render,
afterClose,
});
}
+26
View File
@@ -0,0 +1,26 @@
import initMatrix from '../initMatrix';
// eslint-disable-next-line import/prefer-default-export
export async function markAsRead(roomId) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
if (!room) return;
initMatrix.notifications.deleteNoti(roomId);
const timeline = room.getLiveTimeline().getEvents();
const readEventId = room.getEventReadUpTo(mx.getUserId());
const getLatestValidEvent = () => {
for (let i = timeline.length - 1; i >= 0; i -= 1) {
const latestEvent = timeline[i];
if (latestEvent.getId() === readEventId) return null;
if (!latestEvent.isSending()) return latestEvent;
}
return null;
};
if (timeline.length === 0) return;
const latestEvent = getLatestValidEvent();
if (latestEvent === null) return;
await mx.sendReadReceipt(latestEvent);
}
+6
View File
@@ -36,3 +36,9 @@ export function toggleNotifications() {
type: cons.actions.settings.TOGGLE_NOTIFICATIONS,
});
}
export function toggleNotificationSounds() {
appDispatcher.dispatch({
type: cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS,
});
}
+12 -3
View File
@@ -1,5 +1,6 @@
import { openSearch, toggleRoomSettings } from '../action/navigation';
import navigation from '../state/navigation';
import { markAsRead } from '../action/notifications';
function listenKeyboard(event) {
// Ctrl/Cmd +
@@ -18,11 +19,19 @@ function listenKeyboard(event) {
return;
}
// esc - close room settings panel
if (event.keyCode === 27 && navigation.isRoomSettings) {
toggleRoomSettings();
// esc
if (event.keyCode === 27) {
if (navigation.isRoomSettings) {
toggleRoomSettings();
return;
}
if (navigation.selectedRoomId) {
markAsRead(navigation.selectedRoomId);
return;
}
}
// 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)) {
+2 -4
View File
@@ -7,8 +7,7 @@ import RoomList from './state/RoomList';
import AccountData from './state/AccountData';
import RoomsInput from './state/RoomsInput';
import Notifications from './state/Notifications';
import { initHotkeys } from './event/hotkeys';
import { initRoomListListener } from './event/roomList';
import { cryptoCallbacks } from './state/secretStorageKeys';
global.Olm = require('@matrix-org/olm');
@@ -38,6 +37,7 @@ class InitMatrix extends EventEmitter {
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
deviceId: secret.deviceId,
timelineSupport: true,
cryptoCallbacks,
});
await this.matrixClient.initCrypto();
@@ -66,8 +66,6 @@ class InitMatrix extends EventEmitter {
this.accountData = new AccountData(this.roomList);
this.roomsInput = new RoomsInput(this.matrixClient);
this.notifications = new Notifications(this.roomList);
initHotkeys();
initRoomListListener(this.roomList);
this.emit('init_loading_finished');
}
},
+54 -21
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;
@@ -80,7 +83,12 @@ class Notifications extends EventEmitter {
getNotiType(roomId) {
const mx = this.matrixClient;
const pushRule = mx.getRoomPushRule('global', roomId);
let pushRule;
try {
pushRule = mx.getRoomPushRule('global', roomId);
} catch {
pushRule = undefined;
}
if (pushRule === undefined) {
const overrideRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
@@ -185,7 +193,7 @@ class Notifications extends EventEmitter {
}
async _displayPopupNoti(mEvent, room) {
if (!settings.showNotifications) return;
if (!settings.showNotifications && !settings.isNotificationSounds) return;
const actions = this.matrixClient.getPushActionsForEvent(mEvent);
if (!actions?.notify) return;
@@ -196,28 +204,50 @@ class Notifications extends EventEmitter {
await mEvent.attemptDecryption(this.matrixClient.crypto);
}
let title;
if (!mEvent.sender || room.name === mEvent.sender.name) {
title = room.name;
} else if (mEvent.sender) {
title = `${mEvent.sender.name} (${room.name})`;
if (settings.showNotifications) {
let title;
if (!mEvent.sender || room.name === mEvent.sender.name) {
title = room.name;
} else if (mEvent.sender) {
title = `${mEvent.sender.name} (${room.name})`;
}
const iconSize = 36;
const icon = await renderAvatar({
text: mEvent.sender.name,
bgColor: cssColorMXID(mEvent.getSender()),
imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'),
size: iconSize,
borderRadius: 8,
scale: 8,
});
const noti = new window.Notification(title, {
body: mEvent.getContent().body,
icon,
silent: settings.isNotificationSounds,
});
if (settings.isNotificationSounds) {
noti.onshow = () => this._playNotiSound();
}
noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
} else {
this._playNotiSound();
}
}
const iconSize = 36;
const icon = await renderAvatar({
text: mEvent.sender.name,
bgColor: cssColorMXID(mEvent.getSender()),
imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'),
size: iconSize,
borderRadius: 8,
scale: 8,
});
_playNotiSound() {
if (!this._notiAudio) {
this._notiAudio = new Audio(NotificationSound);
}
this._notiAudio.play();
}
const noti = new window.Notification(title, {
body: mEvent.getContent().body,
icon,
});
noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
_playInviteSound() {
if (!this._inviteAudio) {
this._inviteAudio = new Audio(InviteSound);
}
this._inviteAudio.play();
}
_listenEvents() {
@@ -299,6 +329,9 @@ class Notifications extends EventEmitter {
if (membership === 'leave' && this.hasNoti(room.roomId)) {
this.deleteNoti(room.roomId);
}
if (membership === 'invite') {
this._playInviteSound();
}
});
}
}
+1 -1
View File
@@ -206,7 +206,7 @@ class RoomList extends EventEmitter {
this.matrixClient.getRooms().forEach((room) => {
const { roomId } = room;
const tombstone = room.currentState.events.get('m.room.tombstone');
if (typeof tombstone !== 'undefined') {
if (tombstone?.get('') !== undefined) {
const repRoomId = tombstone.get('').getContent().replacement_room;
const repRoomMembership = this.matrixClient.getRoom(repRoomId)?.getMyMembership();
if (repRoomMembership === 'join') return;
+1 -21
View File
@@ -77,7 +77,7 @@ function isTimelineLinked(tm1, tm2) {
}
class RoomTimeline extends EventEmitter {
constructor(roomId, notifications) {
constructor(roomId) {
super();
// These are local timelines
this.timeline = [];
@@ -88,7 +88,6 @@ class RoomTimeline extends EventEmitter {
this.matrixClient = initMatrix.matrixClient;
this.roomId = roomId;
this.room = this.matrixClient.getRoom(roomId);
this.notifications = notifications;
this.liveTimeline = this.room.getLiveTimeline();
this.activeTimeline = this.liveTimeline;
@@ -228,25 +227,6 @@ class RoomTimeline extends EventEmitter {
return Promise.allSettled(decryptionPromises);
}
markAllAsRead() {
const readEventId = this.getReadUpToEventId();
const getLatestValidEvent = () => {
for (let i = this.timeline.length - 1; i >= 0; i -= 1) {
const latestEvent = this.timeline[i];
if (latestEvent.getId() === readEventId) return null;
if (!latestEvent.isSending()) return latestEvent;
}
return null;
};
this.notifications.deleteNoti(this.roomId);
if (this.timeline.length === 0) return;
const latestEvent = getLatestValidEvent();
if (latestEvent === null) return;
if (readEventId === latestEvent.getId()) return;
this.matrixClient.sendReadReceipt(latestEvent);
this.emit(cons.events.roomTimeline.MARKED_AS_READ, latestEvent);
}
hasEventInTimeline(eventId, timeline = this.activeTimeline) {
const timelineSet = this.getUnfilteredTimelineSet();
const eventTimeline = timelineSet.getTimelineForEvent(eventId);
+2 -2
View File
@@ -97,13 +97,13 @@ function getFormattedBody(markdown) {
function getReplyFormattedBody(roomId, reply) {
const replyToLink = `<a href="https://matrix.to/#/${roomId}/${reply.eventId}">In reply to</a>`;
const userLink = `<a href="https://matrix.to/#/${reply.userId}">${reply.userId}</a>`;
const formattedReply = getFormattedBody(reply.body.replaceAll('\n', '\n> '));
const formattedReply = getFormattedBody(reply.body.replace(/\n/g, '\n> '));
return `<mx-reply><blockquote>${replyToLink}${userLink}<br />${formattedReply}</blockquote></mx-reply>`;
}
function bindReplyToContent(roomId, reply, content) {
const newContent = { ...content };
newContent.body = `> <${reply.userId}> ${reply.body.replaceAll('\n', '\n> ')}`;
newContent.body = `> <${reply.userId}> ${reply.body.replace(/\n/g, '\n> ')}`;
newContent.body += `\n\n${content.body}`;
newContent.format = 'org.matrix.custom.html';
newContent['m.relates_to'] = content['m.relates_to'] || {};
+5 -2
View File
@@ -1,5 +1,5 @@
const cons = {
version: '1.8.1',
version: '1.8.2',
secretKey: {
ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id',
@@ -47,6 +47,7 @@ const cons = {
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
OPEN_SEARCH: 'OPEN_SEARCH',
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
},
room: {
JOIN: 'JOIN',
@@ -67,6 +68,7 @@ const cons = {
TOGGLE_MEMBERSHIP_EVENT: 'TOGGLE_MEMBERSHIP_EVENT',
TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT',
TOGGLE_NOTIFICATIONS: 'TOGGLE_NOTIFICATIONS',
TOGGLE_NOTIFICATION_SOUNDS: 'TOGGLE_NOTIFICATION_SOUNDS',
},
},
events: {
@@ -91,6 +93,7 @@ const cons = {
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
SEARCH_OPENED: 'SEARCH_OPENED',
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
@@ -115,7 +118,6 @@ const cons = {
PAGINATED: 'PAGINATED',
TYPING_MEMBERS_UPDATED: 'TYPING_MEMBERS_UPDATED',
LIVE_RECEIPT: 'LIVE_RECEIPT',
MARKED_AS_READ: 'MARKED_AS_READ',
EVENT_REDACTED: 'EVENT_REDACTED',
AT_BOTTOM: 'AT_BOTTOM',
SCROLL_TO_LIVE: 'SCROLL_TO_LIVE',
@@ -135,6 +137,7 @@ const cons = {
MEMBERSHIP_EVENTS_TOGGLED: 'MEMBERSHIP_EVENTS_TOGGLED',
NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED',
NOTIFICATIONS_TOGGLED: 'NOTIFICATIONS_TOGGLED',
NOTIFICATION_SOUNDS_TOGGLED: 'NOTIFICATION_SOUNDS_TOGGLED',
},
},
};
+11 -2
View File
@@ -129,12 +129,13 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId);
},
[cons.actions.navigation.OPEN_SETTINGS]: () => {
this.emit(cons.events.navigation.SETTINGS_OPENED);
this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText);
},
[cons.actions.navigation.OPEN_EMOJIBOARD]: () => {
this.emit(
cons.events.navigation.EMOJIBOARD_OPENED,
action.cords, action.requestEmojiCallback,
action.cords,
action.requestEmojiCallback,
);
},
[cons.actions.navigation.OPEN_READRECEIPTS]: () => {
@@ -173,6 +174,14 @@ class Navigation extends EventEmitter {
action.afterClose,
);
},
[cons.actions.navigation.OPEN_REUSABLE_DIALOG]: () => {
this.emit(
cons.events.navigation.REUSABLE_DIALOG_OPENED,
action.title,
action.render,
action.afterClose,
);
},
};
actions[action.type]?.();
}
+41
View File
@@ -0,0 +1,41 @@
const secretStorageKeys = new Map();
export function storePrivateKey(keyId, privateKey) {
if (privateKey instanceof Uint8Array === false) {
throw new Error('Unable to store, privateKey is invalid.');
}
secretStorageKeys.set(keyId, privateKey);
}
export function hasPrivateKey(keyId) {
return secretStorageKeys.get(keyId) instanceof Uint8Array;
}
export function getPrivateKey(keyId) {
return secretStorageKeys.get(keyId);
}
export function deletePrivateKey(keyId) {
delete secretStorageKeys.delete(keyId);
}
export function clearSecretStorageKeys() {
secretStorageKeys.clear();
}
async function getSecretStorageKey({ keys }) {
const keyIds = Object.keys(keys);
const keyId = keyIds.find(hasPrivateKey);
if (!keyId) return undefined;
const privateKey = getPrivateKey(keyId);
return [keyId, privateKey];
}
function cacheSecretStorageKey(keyId, keyInfo, privateKey) {
secretStorageKeys.set(keyId, privateKey);
}
export const cryptoCallbacks = {
getSecretStorageKey,
cacheSecretStorageKey,
};
+15
View File
@@ -29,6 +29,7 @@ class Settings extends EventEmitter {
this.hideMembershipEvents = this.getHideMembershipEvents();
this.hideNickAvatarEvents = this.getHideNickAvatarEvents();
this._showNotifications = this.getShowNotifications();
this.isNotificationSounds = this.getIsNotificationSounds();
this.isTouchScreenDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
}
@@ -125,6 +126,15 @@ class Settings extends EventEmitter {
return settings.showNotifications;
}
getIsNotificationSounds() {
if (typeof this.isNotificationSounds === 'boolean') return this.isNotificationSounds;
const settings = getSettings();
if (settings === null) return true;
if (typeof settings.isNotificationSounds === 'undefined') return true;
return settings.isNotificationSounds;
}
setter(action) {
const actions = {
[cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => {
@@ -164,6 +174,11 @@ class Settings extends EventEmitter {
setSettings('showNotifications', this._showNotifications);
this.emit(cons.events.settings.NOTIFICATIONS_TOGGLED, this._showNotifications);
},
[cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS]: () => {
this.isNotificationSounds = !this.isNotificationSounds;
setSettings('isNotificationSounds', this.isNotificationSounds);
this.emit(cons.events.settings.NOTIFICATION_SOUNDS_TOGGLED, this.isNotificationSounds);
},
};
actions[action.type]?.();
+4
View File
@@ -69,9 +69,13 @@
--ic-surface-high: #272727;
--ic-surface-normal: #626262;
--ic-surface-low: #7c7c7c;
--ic-primary-high: #ffffff;
--ic-primary-normal: #ffffff;
--ic-positive-high: rgba(69, 184, 59);
--ic-positive-normal: rgba(69, 184, 59, 80%);
--ic-caution-high: rgba(255, 179, 0);
--ic-caution-normal: rgba(255, 179, 0, 80%);
--ic-danger-high: rgba(240, 71, 71);
--ic-danger-normal: rgba(240, 71, 71, 0.7);
/* user mxid colors */
+1 -1
View File
@@ -123,7 +123,7 @@ class AsyncSearch extends EventEmitter {
_normalize(item) {
let myItem = item.normalize(this.normalizeUnicode ? 'NFKC' : 'NFC');
if (!this.isCaseSensitive) myItem = myItem.toLocaleLowerCase();
if (this.ignoreWhitespace) myItem = myItem.replaceAll(' ', '');
if (this.ignoreWhitespace) myItem = myItem.replace(/\s/g, '');
return myItem;
}
+18
View File
@@ -114,3 +114,21 @@ export function getScrollInfo(target) {
export function avatarInitials(text) {
return [...text][0];
}
export function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
const host = document.body;
const copyInput = document.createElement('input');
copyInput.style.position = 'fixed';
copyInput.style.opacity = '0';
copyInput.value = text;
host.append(copyInput);
copyInput.select();
copyInput.setSelectionRange(0, 99999);
document.execCommand('Copy');
copyInput.remove();
}
}
+36
View File
@@ -162,3 +162,39 @@ export function genRoomVia(room) {
}
return via.concat(mostPop3.slice(0, 2));
}
export function isCrossVerified(deviceId) {
try {
const mx = initMatrix.matrixClient;
const crossSignInfo = mx.getStoredCrossSigningForUser(mx.getUserId());
const deviceInfo = mx.getStoredDevice(mx.getUserId(), deviceId);
const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true);
return deviceTrust.isCrossSigningVerified();
} catch {
return false;
}
}
export function hasCrossSigningAccountData() {
const mx = initMatrix.matrixClient;
const masterKeyData = mx.getAccountData('m.cross_signing.master');
return !!masterKeyData;
}
export function getDefaultSSKey() {
const mx = initMatrix.matrixClient;
try {
return mx.getAccountData('m.secret_storage.default_key').getContent().key;
} catch {
return undefined;
}
}
export function getSSKeyInfo(key) {
const mx = initMatrix.matrixClient;
try {
return mx.getAccountData(`m.secret_storage.key.${key}`).getContent();
} catch {
return undefined;
}
}
+47
View File
@@ -0,0 +1,47 @@
import initMatrix from '../client/initMatrix';
export function roomIdByActivity(id1, id2) {
const room1 = initMatrix.matrixClient.getRoom(id1);
const room2 = initMatrix.matrixClient.getRoom(id2);
return room2.getLastActiveTimestamp() - room1.getLastActiveTimestamp();
}
export function roomIdByAtoZ(aId, bId) {
let aName = initMatrix.matrixClient.getRoom(aId).name;
let bName = initMatrix.matrixClient.getRoom(bId).name;
// remove "#" from the room name
// To ignore it in sorting
aName = aName.replace(/#/g, '');
bName = bName.replace(/#/g, '');
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
export function memberByAtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
export function memberByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more