Compare commits

..

125 Commits

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

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

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

fixes ajbura/cinny#544

* fix lint

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

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

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

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

* remove comments

* Change function name

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Manually merge #311 by @ginnyTheCat
2022-05-01 13:22:55 +05:30
dependabot[bot]
416fd02069 Bump actions/checkout from 3.0.1 to 3.0.2 (#508)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.1 to 3.0.2.
- [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.1...v3.0.2)

---
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-26 17:23:22 +05:30
dependabot[bot]
581963cfb4 Bump html-react-parser from 1.4.11 to 1.4.12 (#507)
Bumps [html-react-parser](https://github.com/remarkablemark/html-react-parser) from 1.4.11 to 1.4.12.
- [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.11...v1.4.12)

---
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-26 17:22:35 +05:30
dependabot[bot]
3b14543e66 Bump sass from 1.50.0 to 1.50.1 (#506)
Bumps [sass](https://github.com/sass/dart-sass) from 1.50.0 to 1.50.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.50.0...1.50.1)

---
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-26 17:20:49 +05:30
dependabot[bot]
5dd2194eba Bump babel-loader from 8.2.4 to 8.2.5 (#505)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 8.2.4 to 8.2.5.
- [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.4...v8.2.5)

---
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-04-26 17:20:12 +05:30
dependabot[bot]
5b7880f817 Bump eslint from 8.13.0 to 8.14.0 (#504)
Bumps [eslint](https://github.com/eslint/eslint) from 8.13.0 to 8.14.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.13.0...v8.14.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-26 17:19:12 +05:30
dependabot[bot]
bafe1c5781 Bump katex from 0.15.2 to 0.15.3 (#501)
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.15.2 to 0.15.3.
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.15.2...v0.15.3)

---
updated-dependencies:
- dependency-name: katex
  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-26 17:17:05 +05:30
Ajay Bura
d760be58c3 Replace confirm and prompt with custom dialogs (#500) 2022-04-25 20:21:21 +05:30
ginnyTheCat
3da9b70632 Show devices without encryption support separately from unverified session (#499)
* Show devices without encryption support separately

* Fix typo

* Don't show sessions without encryption support in red
2022-04-24 21:59:50 +05:30
ginnyTheCat
b7c5902f67 Add LaTeX / math input and rendering support (#345)
* Initial display support

* Use better colors for error in math parsing

* Parse math markdown

* Use proper jsx

* Better copy support

* use css var directly

* Remove console.debug call

* Lazy load math module

* Show fallback while katex is loading
2022-04-24 21:18:35 +05:30
Ajay Bura
9a22b25564 Show popup window in full screen at mobile devices 2022-04-24 17:29:49 +05:30
Ajay Bura
44c3dec9dc Fix crash on invite from dendrite hs (#453) 2022-04-24 17:10:18 +05:30
Ajay Bura
87f3afd8fd Fix members btn not visible in small devices 2022-04-24 16:47:24 +05:30
Ajay Bura
53f1129242 Fix app stuck on welcome screen in small device 2022-04-24 16:35:42 +05:30
Ajay Bura
74216f75e2 Fix padding in room header 2022-04-24 16:17:26 +05:30
Ajay Bura
ed8eca0c1d Remove unwanted css 2022-04-24 15:55:05 +05:30
Laurenz
dc8e6e53c7 Adapt to different device widths (#401)
* Now adapting to small screen sizes, needs improvements

* Fix that site only gets into mobile mode when resized

* - Added navigation event triggered if user requests to return to navigation on compact screens
- People drawer wont be shown on compact screens
  - Still accessible using settings
  - would be duplicated UI
- mobileSize is now compactSize

* Put threshold for collapsing the base UI in a shared file

* Switch to a more simple solution using CSS media queries over JS
- Move back button to the left a bit so it doesnt get in touch with room icon

* switch from component-individual-thresholds to device-type thresholds
- <750px: Mobile
- <900px: Tablet
- >900px: Desktop

* Make Settings drawer component collapse on mobile

* Fix EmojiBoard not showing up and messing up UI when screen is smaller than 360px

* Improve code quality; allow passing classNames to IconButton
- remove unnessesary div wrappers
- use dir.side where appropriate
- rename threshold and its mixins to more descriptive names
- Rename "OPEN_NAVIGATION" to "NAVIGATION_OPENED"

* - follow BEM methology
- remove ROOM_SELECTED listener
- rename NAVIGATION_OPENED to OPEN_NAVIGATION where appropriate
- this does NOT changes that ref should be used for changing visability

* Use ref to change visability to avoid re-rendering

* Use ref to change visability to avoid re-rendering

* Fix that room component is not hidden by default.
This resulted in a broken view when application is viewed in mobile size without having selected a room since loading.

* fix: leaving a room should bring one back to navigation

Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
2022-04-24 15:53:10 +05:30
Ajay Bura
989ab5a432 Add support to manage cross-signing and key backup (#461)
* Add useDeviceList hook

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

* Add isCrossVerified func to matrixUtil

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

* Add className prop in sidebar avatar comp

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

* Add unverified session indicator in sidebar

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

* Add info card component

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

* Add css variables

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

* Add cross signin status hook

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

* Add hasCrossSigninAccountData function

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

* Add cross signin info card in device manage component

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

* Add cross signing and key backup component

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

* Fix typo

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

* WIP

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

* Add cross singing dialogs

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

* Add cross signing set/reset

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

* Add SecretStorageAccess component

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

* Add key backup

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

* WIP

* WIP

* WIP

* WIP

* Show progress when restoring key backup

* Add SSSS and key backup
2022-04-24 15:42:24 +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
ba0de8800a Fix crash on unable to getContent of tombstoned room 2022-04-16 20:53:24 +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
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
3f6e3074f2 Add reusable dialog (#459)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-31 20:39:05 +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
Krishan
44ab6f181c Fix docker check to only run on Dockerfile change (#452) 2022-03-30 18:38:52 +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
ginnyTheCat
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
Ajay Bura
f5720bde14 Fix new message not appearing (#391)
* Fix new message no appearing

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

* Fix room not marking as read

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

* Fix room automatically gets mark as read

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

* Fix sending wrong read recipt

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

* Fix sending message not mark as read

Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-17 08:16:49 +05:30
137 changed files with 4852 additions and 2225 deletions

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
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
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
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
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

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.

View File

@@ -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.

View File

@@ -1,15 +1,13 @@
<!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request --> <!-- Please read https://github.com/ajbura/cinny/CONTRIBUTING.md before submitting your pull request -->
### Description ### 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 #### Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

View File

@@ -1,8 +1,16 @@
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates> # Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
version: 2 version: 2
updates: updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 15
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
@@ -10,6 +18,7 @@ updates:
day: "tuesday" day: "tuesday"
time: "01:00" time: "01:00"
timezone: "Asia/Kolkata" timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
- package-ecosystem: docker - package-ecosystem: docker
directory: / directory: /
@@ -18,11 +27,4 @@ updates:
day: "tuesday" day: "tuesday"
time: "01:00" time: "01:00"
timezone: "Asia/Kolkata" timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"

View File

@@ -10,8 +10,8 @@ jobs:
env: env:
PR_NUMBER: ${{github.event.number}} PR_NUMBER: ${{github.event.number}}
steps: steps:
- name: Check out the repo - name: Checkout repository
uses: actions/checkout@v3.0.0 uses: actions/checkout@v3.0.2
- name: Build app - name: Build app
run: npm ci && npm run build run: npm ci && npm run build
- name: Upload artifact - name: Upload artifact
@@ -32,8 +32,3 @@ jobs:
name: pr.json name: pr.json
path: pr.json path: pr.json
retention-days: 1 retention-days: 1
- name: Build Docker image
uses: docker/build-push-action@v2.10.0
with:
context: .
push: false

View File

@@ -13,7 +13,7 @@ jobs:
# There's a 'download artifact' action but it hasn't been updated for the # 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) # workflow_run action (https://github.com/actions/download-artifact/issues/60)
# so instead we get this mess: # so instead we get this mess:
- name: 'Download artifact' - name: Download artifact
uses: actions/github-script@v6.0.0 uses: actions/github-script@v6.0.0
with: with:
script: | script: |
@@ -46,7 +46,7 @@ jobs:
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data)); fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
- name: Extract Artifacts - name: Extract Artifacts
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip 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 id: readctx
uses: actions/github-script@v6.0.0 uses: actions/github-script@v6.0.0
with: with:
@@ -56,7 +56,7 @@ jobs:
console.log(`::set-output name=prnumber::${pr.number}`); console.log(`::set-output name=prnumber::${pr.number}`);
- name: Deploy to Netlify - name: Deploy to Netlify
id: netlify id: netlify
uses: nwtgck/actions-netlify@v1.2.3 uses: nwtgck/actions-netlify@b7c1504e00c6b8a249d1848cc1b522a4865eed99
with: with:
publish-dir: dist publish-dir: dist
deploy-message: "Deploy from GitHub Actions" deploy-message: "Deploy from GitHub Actions"
@@ -68,7 +68,7 @@ jobs:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
timeout-minutes: 1 timeout-minutes: 1
- name: Edit PR Description - name: Edit PR Description
uses: velas/pr-description@v1.0.1 uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:

21
.github/workflows/docker-pr.yml vendored Normal file
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.2
- name: Build Docker image
uses: docker/build-push-action@v2.10.0
with:
context: .
push: false

View File

@@ -6,13 +6,15 @@ on:
- dev - dev
jobs: jobs:
deploy: deploy-to-netlify:
name: 'Deploy' name: 'Deploy'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3.0.0 - name: Checkout repository
- uses: jsmrcaga/action-netlify-deploy@v1.7.2 uses: actions/checkout@v3.0.2
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@fb6a5f936a4b06a8f7793e69fc5a022ffe39807a
with: with:
install_command: "npm ci" install_command: "npm ci"
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

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

View File

@@ -1,5 +1,5 @@
## Builder ## Builder
FROM node:17.7.1-alpine3.15 as builder FROM node:17.9.0-alpine3.15 as builder
WORKDIR /src WORKDIR /src

View File

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

View File

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

View File

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

2275
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "1.8.1", "version": "2.0.3",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
@@ -15,8 +15,8 @@
"author": "Ajay Bura", "author": "Ajay Bura",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource/inter": "^4.5.5", "@fontsource/inter": "^4.5.10",
"@fontsource/roboto": "^4.5.3", "@fontsource/roboto": "^4.5.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
@@ -26,32 +26,34 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"flux": "^4.0.3", "flux": "^4.0.3",
"formik": "^2.2.9", "formik": "^2.2.9",
"html-react-parser": "^1.4.8", "html-react-parser": "^1.4.12",
"katex": "^0.15.3",
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"matrix-js-sdk": "^15.6.0", "matrix-js-sdk": "^17.1.0",
"micromark": "^3.0.10", "micromark": "^3.0.10",
"micromark-extension-gfm": "^2.0.1", "micromark-extension-gfm": "^2.0.1",
"micromark-extension-math": "^2.0.2",
"micromark-util-chunked": "^1.0.0", "micromark-util-chunked": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0", "micromark-util-resolve-all": "^1.0.0",
"micromark-util-symbol": "^1.0.1", "micromark-util-symbol": "^1.0.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-autosize-textarea": "^7.1.0", "react-autosize-textarea": "^7.1.0",
"react-dnd": "^15.1.1", "react-dnd": "^15.1.2",
"react-dnd-html5-backend": "^15.1.2", "react-dnd-html5-backend": "^15.1.3",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-google-recaptcha": "^2.1.0", "react-google-recaptcha": "^2.1.0",
"react-modal": "^3.14.4", "react-modal": "^3.15.1",
"sanitize-html": "^2.7.0", "sanitize-html": "^2.7.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"twemoji": "^14.0.1" "twemoji": "^14.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.17.7", "@babel/core": "^7.17.10",
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.17.10",
"@babel/preset-react": "^7.16.7", "@babel/preset-react": "^7.16.7",
"assert": "^2.0.0", "assert": "^2.0.0",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.5",
"browserify-fs": "^1.0.0", "browserify-fs": "^1.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
@@ -59,27 +61,27 @@
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"css-loader": "^6.7.1", "css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1", "css-minimizer-webpack-plugin": "^3.4.1",
"eslint": "^8.11.0", "eslint": "^8.14.0",
"eslint-config-airbnb": "^19.0.4", "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-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.29.4", "eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.5.0",
"favicons": "^6.2.2", "favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2", "favicons-webpack-plugin": "^5.0.2",
"html-loader": "^3.1.0", "html-loader": "^3.1.0",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"sass": "^1.49.9", "sass": "^1.51.0",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"url": "^0.11.0", "url": "^0.11.0",
"util": "^0.12.4", "util": "^0.12.4",
"webpack": "^5.70.0", "webpack": "^5.72.0",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4", "webpack-dev-server": "^4.9.0",
"webpack-merge": "^5.7.3" "webpack-merge": "^5.7.3"
} }
} }

View File

@@ -18,5 +18,11 @@
</head> </head>
<body id="appBody"> <body id="appBody">
<div id="root"></div> <div id="root"></div>
<audio id="notificationSound">
<source src="./sound/notification.ogg" type="audio/ogg" />
</audio>
<audio id="inviteSound">
<source src="./sound/invite.ogg" type="audio/ogg" />
</audio>
</body> </body>
</html> </html>

View File

@@ -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

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

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" <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"> viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<g> <g>
<polygon fill="#010101" points="11,6 11,12 15.2,16.2 16.7,14.8 13,11.2 13,6 "/> <polygon points="11,7 11,13 14.5,16.5 15.9,15.1 13,12.2 13,7 "/>
<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 <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
l-1.2,1.6C7.4,21.2,9.6,22,12,22c5.5,0,10-4.5,10-10S17.5,2,12,2z"/> c0,5.5,4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z"/>
</g> </g>
<g> <g>
<polygon fill="#010101" points="49,44 49,50 53.2,54.2 54.7,52.8 51,49.2 51,44 "/> <polygon 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 <polygon points="45.5,47 40.5,47 40.5,42 42,42 42,45.5 45.5,45.5 "/>
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"/> <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> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 985 B

BIN
public/sound/invite.ogg Executable file

Binary file not shown.

BIN
public/sound/notification.ogg Executable file

Binary file not shown.

View File

@@ -11,11 +11,12 @@ const IconButton = React.forwardRef(({
variant, size, type, variant, size, type,
tooltip, tooltipPlacement, src, tooltip, tooltipPlacement, src,
onClick, tabIndex, disabled, isImage, onClick, tabIndex, disabled, isImage,
className,
}, ref) => { }, ref) => {
const btn = ( const btn = (
<button <button
ref={ref} ref={ref}
className={`ic-btn ic-btn-${variant}`} className={`ic-btn ic-btn-${variant} ${className}`}
onMouseUp={(e) => blurOnBubbling(e, `.ic-btn-${variant}`)} onMouseUp={(e) => blurOnBubbling(e, `.ic-btn-${variant}`)}
onClick={onClick} onClick={onClick}
// eslint-disable-next-line react/button-has-type // eslint-disable-next-line react/button-has-type
@@ -47,6 +48,7 @@ IconButton.defaultProps = {
tabIndex: 0, tabIndex: 0,
disabled: false, disabled: false,
isImage: false, isImage: false,
className: '',
}; };
IconButton.propTypes = { IconButton.propTypes = {
@@ -60,6 +62,7 @@ IconButton.propTypes = {
tabIndex: PropTypes.number, tabIndex: PropTypes.number,
disabled: PropTypes.bool, disabled: PropTypes.bool,
isImage: PropTypes.bool, isImage: PropTypes.bool,
className: PropTypes.string,
}; };
export default IconButton; export default IconButton;

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;

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);
}
}
}
}

View File

@@ -0,0 +1,33 @@
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import katex from 'katex';
import 'katex/dist/katex.min.css';
import 'katex/dist/contrib/copy-tex';
import 'katex/dist/contrib/copy-tex.css';
const Math = React.memo(({
content, throwOnError, errorColor, displayMode,
}) => {
const ref = useRef(null);
useEffect(() => {
katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
}, [content, throwOnError, errorColor, displayMode]);
return <span ref={ref} />;
});
Math.defaultProps = {
throwOnError: null,
errorColor: null,
displayMode: null,
};
Math.propTypes = {
content: PropTypes.string.isRequired,
throwOnError: PropTypes.bool,
errorColor: PropTypes.string,
displayMode: PropTypes.bool,
};
export default Math;

View File

@@ -74,7 +74,7 @@ Tabs.defaultProps = {
Tabs.propTypes = { Tabs.propTypes = {
items: PropTypes.arrayOf( items: PropTypes.arrayOf(
PropTypes.exact({ PropTypes.shape({
iconSrc: PropTypes.string, iconSrc: PropTypes.string,
text: PropTypes.string, text: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
@@ -84,4 +84,4 @@ Tabs.propTypes = {
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
}; };
export { Tabs as default }; export default Tabs;

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;
}

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;
}

View File

@@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import './ConfirmDialog.scss';
import { openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
function ConfirmDialog({
desc, actionTitle, actionType, onComplete,
}) {
return (
<div className="confirm-dialog">
<Text>{desc}</Text>
<div className="confirm-dialog__btn">
<Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
<Button onClick={() => onComplete(false)}>Cancel</Button>
</div>
</div>
);
}
ConfirmDialog.propTypes = {
desc: PropTypes.string.isRequired,
actionTitle: PropTypes.string.isRequired,
actionType: PropTypes.oneOf(['primary', 'positive', 'danger', 'caution']).isRequired,
onComplete: PropTypes.func.isRequired,
};
/**
* @param {string} title title of confirm dialog
* @param {string} desc description of confirm dialog
* @param {string} actionTitle title of main action to take
* @param {'primary' | 'positive' | 'danger' | 'caution'} actionType type of action. default=primary
* @return {Promise<boolean>} does it get's confirmed or not
*/
// eslint-disable-next-line import/prefer-default-export
export const confirmDialog = (title, desc, actionTitle, actionType = 'primary') => new Promise((resolve) => {
let isCompleted = false;
openReusableDialog(
<Text variant="s1" weight="medium">{title}</Text>,
(requestClose) => (
<ConfirmDialog
desc={desc}
actionTitle={actionTitle}
actionType={actionType}
onComplete={(isConfirmed) => {
isCompleted = true;
resolve(isConfirmed);
requestClose();
}}
/>
),
() => {
if (!isCompleted) resolve(false);
},
);
});

View File

@@ -0,0 +1,11 @@
.confirm-dialog {
padding: var(--sp-normal);
& > .text {
padding-bottom: var(--sp-normal);
}
&__btn {
display: flex;
gap: var(--sp-normal);
}
}

View File

@@ -12,10 +12,11 @@ import RawModal from '../../atoms/modal/RawModal';
function Dialog({ function Dialog({
className, isOpen, title, onAfterOpen, onAfterClose, className, isOpen, title, onAfterOpen, onAfterClose,
contentOptions, onRequestClose, closeFromOutside, children, contentOptions, onRequestClose, closeFromOutside, children,
invisibleScroll,
}) { }) {
return ( return (
<RawModal <RawModal
className={`${className === null ? '' : `${className} `}dialog-model`} className={`${className === null ? '' : `${className} `}dialog-modal`}
isOpen={isOpen} isOpen={isOpen}
onAfterOpen={onAfterOpen} onAfterOpen={onAfterOpen}
onAfterClose={onAfterClose} onAfterClose={onAfterClose}
@@ -36,7 +37,7 @@ function Dialog({
{contentOptions} {contentOptions}
</Header> </Header>
<div className="dialog__content__wrapper"> <div className="dialog__content__wrapper">
<ScrollView autoHide> <ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
<div className="dialog__content-container"> <div className="dialog__content-container">
{children} {children}
</div> </div>
@@ -55,6 +56,7 @@ Dialog.defaultProps = {
onAfterClose: null, onAfterClose: null,
onRequestClose: null, onRequestClose: null,
closeFromOutside: true, closeFromOutside: true,
invisibleScroll: false,
}; };
Dialog.propTypes = { Dialog.propTypes = {
@@ -67,6 +69,7 @@ Dialog.propTypes = {
onRequestClose: PropTypes.func, onRequestClose: PropTypes.func,
closeFromOutside: PropTypes.bool, closeFromOutside: PropTypes.bool,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
invisibleScroll: PropTypes.bool,
}; };
export default Dialog; export default Dialog;

View File

@@ -1,4 +1,4 @@
.dialog-model { .dialog-modal {
--modal-height: 656px; --modal-height: 656px;
max-height: min(100%, var(--modal-height)); max-height: min(100%, var(--modal-height));
display: flex; display: flex;
@@ -21,8 +21,3 @@
flex-direction: column; flex-direction: column;
} }
} }
.dialog__content-container {
padding-top: var(--sp-extra-tight);
padding-bottom: var(--sp-extra-loose);
}

View File

@@ -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;

View File

@@ -36,6 +36,8 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg'; import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg'; import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function PlaceholderMessage() { function PlaceholderMessage() {
return ( return (
<div className="ph-msg"> <div className="ph-msg">
@@ -121,17 +123,26 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
const eTimeline = await mx.getEventTimeline(timelineSet, eventId); const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
await roomTimeline.decryptAllEventsOfTimeline(eTimeline); await roomTimeline.decryptAllEventsOfTimeline(eTimeline);
const mEvent = eTimeline.getTimelineSet().findEventById(eventId); let mEvent = eTimeline.getTimelineSet().findEventById(eventId);
const editedList = roomTimeline.editedTimeline.get(mEvent.getId());
if (editedList) {
mEvent = editedList[editedList.length - 1];
}
const rawBody = mEvent.getContent().body; const rawBody = mEvent.getContent().body;
const username = getUsernameOfRoomMember(mEvent.sender); const username = getUsernameOfRoomMember(mEvent.sender);
if (isMountedRef.current === false) return; if (isMountedRef.current === false) return;
const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***'; const fallbackBody = mEvent.isRedacted() ? '*** This message has been deleted ***' : '*** Unable to load reply ***';
let parsedBody = parseReply(rawBody)?.body ?? rawBody ?? fallbackBody;
if (editedList && parsedBody.startsWith(' * ')) {
parsedBody = parsedBody.slice(3);
}
setReply({ setReply({
to: username, to: username,
color: colorMXID(mEvent.getSender()), color: colorMXID(mEvent.getSender()),
body: parseReply(rawBody)?.body ?? rawBody ?? fallbackBody, body: parsedBody,
event: mEvent, event: mEvent,
}); });
} catch { } catch {
@@ -186,9 +197,17 @@ const MessageBody = React.memo(({
// if body is not string it is a React element. // if body is not string it is a React element.
if (typeof body !== 'string') return <div className="message__body">{body}</div>; if (typeof body !== 'string') return <div className="message__body">{body}</div>;
let content = isCustomHTML let content = null;
? twemojify(sanitizeCustomHtml(body), undefined, true, false) if (isCustomHTML) {
: twemojify(body, undefined, true); try {
content = twemojify(sanitizeCustomHtml(body), undefined, true, false, true);
} 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 // Determine if this message should render with large emojis
// Criteria: // Criteria:
@@ -538,10 +557,15 @@ const MessageOptions = React.memo(({
<MenuItem <MenuItem
variant="danger" variant="danger"
iconSrc={BinIC} iconSrc={BinIC}
onClick={() => { onClick={async () => {
if (window.confirm('Are you sure you want to delete this event')) { const isConfirmed = await confirmDialog(
redactEvent(roomId, mEvent.getId()); 'Delete message',
} 'Are you sure that you want to delete this message?',
'Delete',
'danger',
);
if (!isConfirmed) return;
redactEvent(roomId, mEvent.getId());
}} }}
> >
Delete Delete

View File

@@ -1,6 +1,7 @@
@use '../../atoms/scroll/scrollbar'; @use '../../atoms/scroll/scrollbar';
@use '../../partials/text'; @use '../../partials/text';
@use '../../partials/dir'; @use '../../partials/dir';
@use '../../partials/screen';
.message, .message,
.ph-msg { .ph-msg {
@@ -95,7 +96,7 @@
.message__reactions { .message__reactions {
max-width: calc(100% - 88px); max-width: calc(100% - 88px);
min-width: 0; min-width: 0;
@media (max-width: 1124px) { @include screen.smallerThan(tabletBreakpoint) {
max-width: 100%; max-width: 100%;
} }
} }

View File

@@ -58,7 +58,8 @@ function PopupWindow({
return ( return (
<RawModal <RawModal
className={`${className === null ? '' : `${className} `}pw-model`} className={`${className === null ? '' : `${className} `}pw-modal`}
overlayClassName="pw-modal__overlay"
isOpen={isOpen} isOpen={isOpen}
onAfterClose={onAfterClose} onAfterClose={onAfterClose}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}

View File

@@ -1,9 +1,18 @@
@use '../../partials/dir'; @use '../../partials/dir';
@use '../../partials/screen';
.pw-model { .pw-modal {
--modal-height: 656px; --modal-height: 774px;
max-height: var(--modal-height) !important; max-height: var(--modal-height) !important;
height: 100%; height: 100%;
@include screen.smallerThan(mobileBreakpoint) {
--modal-height: 100%;
border-radius: 0 !important;
&__overlay {
padding: 0 !important;
}
}
} }
.pw { .pw {

View File

@@ -8,6 +8,8 @@ import Text from '../../atoms/text/Text';
import Toggle from '../../atoms/button/Toggle'; import Toggle from '../../atoms/button/Toggle';
import SettingTile from '../setting-tile/SettingTile'; import SettingTile from '../setting-tile/SettingTile';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function RoomEncryption({ roomId }) { function RoomEncryption({ roomId }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
@@ -15,17 +17,20 @@ function RoomEncryption({ roomId }) {
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0); const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId()); const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId());
const handleEncryptionEnable = () => { const handleEncryptionEnable = async () => {
const joinRule = room.getJoinRule(); const joinRule = room.getJoinRule();
const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.'; const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.';
const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly'; const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly';
if (joinRule === 'public' ? confirm(confirmMsg1) : true) {
if (confirm(confirmMsg2)) { const isConfirmed1 = (joinRule === 'public')
setIsEncrypted(true); ? await confirmDialog('Enable encryption', confirmMsg1, 'Continue', 'caution')
mx.sendStateEvent(roomId, 'm.room.encryption', { : true;
algorithm: 'm.megolm.v1.aes-sha2', if (!isConfirmed1) return;
}); if (await confirmDialog('Enable encryption', confirmMsg2, 'Enable', 'caution')) {
} setIsEncrypted(true);
mx.sendStateEvent(roomId, 'm.room.encryption', {
algorithm: 'm.megolm.v1.aes-sha2',
});
} }
}; };

View File

@@ -17,19 +17,19 @@ const visibility = {
const items = [{ const items = [{
iconSrc: null, iconSrc: null,
text: 'World readable (anyone can read)', text: 'Anyone (including guests)',
type: visibility.WORLD_READABLE, type: visibility.WORLD_READABLE,
}, { }, {
iconSrc: null, iconSrc: null,
text: 'Member shared (since the point in time of selecting this option)', text: 'Members (all messages)',
type: visibility.SHARED, type: visibility.SHARED,
}, { }, {
iconSrc: null, iconSrc: null,
text: 'Member invited (since they were invited)', text: 'Members (messages after invite)',
type: visibility.INVITED, type: visibility.INVITED,
}, { }, {
iconSrc: null, iconSrc: null,
text: 'Member joined (since they joined)', text: 'Members (messages after join)',
type: visibility.JOINED, type: visibility.JOINED,
}]; }];
@@ -87,7 +87,7 @@ function RoomHistoryVisibility({ roomId }) {
</MenuItem> </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> </div>
); );
} }

View File

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

View File

@@ -9,6 +9,7 @@ import colorMXID from '../../../util/colorMXID';
import { openProfileViewer } from '../../../client/action/navigation'; import { openProfileViewer } from '../../../client/action/navigation';
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil'; import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
import AsyncSearch from '../../../util/AsyncSearch'; import AsyncSearch from '../../../util/AsyncSearch';
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
@@ -19,26 +20,6 @@ import PeopleSelector from '../people-selector/PeopleSelector';
const PER_PAGE_MEMBER = 50; 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) { function normalizeMembers(members) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
return members.map((member) => ({ return members.map((member) => ({
@@ -65,7 +46,7 @@ function useMemberOfMembership(roomId, membership) {
if (event && event?.getRoomId() !== roomId) return; if (event && event?.getRoomId() !== roomId) return;
const memberOfMembership = normalizeMembers( const memberOfMembership = normalizeMembers(
room.getMembersWithMembership(membership) room.getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel), .sort(memberByAtoZ).sort(memberByPowerLevel),
); );
setMembers(memberOfMembership); setMembers(memberOfMembership);
}; };

View File

@@ -35,7 +35,12 @@ const items = [{
function setRoomNotifType(roomId, newType) { function setRoomNotifType(roomId, newType) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const { notifications } = initMatrix; const { notifications } = initMatrix;
const roomPushRule = mx.getRoomPushRule('global', roomId); let roomPushRule;
try {
roomPushRule = mx.getRoomPushRule('global', roomId);
} catch {
roomPushRule = undefined;
}
const promises = []; const promises = [];
if (newType === cons.notifs.MUTE) { if (newType === cons.notifs.MUTE) {

View File

@@ -6,6 +6,7 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import { openInviteUser } from '../../../client/action/navigation'; import { openInviteUser } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room'; import * as roomActions from '../../../client/action/room';
import { markAsRead } from '../../../client/action/notifications';
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu'; import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import RoomNotification from '../room-notification/RoomNotification'; import RoomNotification from '../room-notification/RoomNotification';
@@ -14,27 +15,32 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg'; import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg'; import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function RoomOptions({ roomId, afterOptionSelect }) { function RoomOptions({ roomId, afterOptionSelect }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
const canInvite = room?.canInvite(mx.getUserId()); const canInvite = room?.canInvite(mx.getUserId());
const handleMarkAsRead = () => { const handleMarkAsRead = () => {
markAsRead(roomId);
afterOptionSelect(); afterOptionSelect();
if (!room) return;
const events = room.getLiveTimeline().getEvents();
mx.sendReadReceipt(events[events.length - 1]);
}; };
const handleInviteClick = () => { const handleInviteClick = () => {
openInviteUser(roomId); openInviteUser(roomId);
afterOptionSelect(); afterOptionSelect();
}; };
const handleLeaveClick = () => { const handleLeaveClick = async () => {
if (confirm('Are you really want to leave this room?')) { afterOptionSelect();
roomActions.leave(roomId); const isConfirmed = await confirmDialog(
afterOptionSelect(); 'Leave room',
} `Are you sure that you want to leave "${room.name}" room?`,
'Leave',
'danger',
);
if (!isConfirmed) return;
roomActions.leave(roomId);
}; };
return ( return (

View File

@@ -179,6 +179,7 @@ function RoomPermissions({ roomId }) {
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0]; const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
const permissions = pLEvent.getContent(); const permissions = pLEvent.getContent();
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId()); 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 handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
const handlePowerLevelChange = (newPowerLevel) => { const handlePowerLevelChange = (newPowerLevel) => {
@@ -208,7 +209,7 @@ function RoomPermissions({ roomId }) {
(closeMenu) => ( (closeMenu) => (
<PowerLevelSelector <PowerLevelSelector
value={powerLevel} value={powerLevel}
max={100} max={myPowerLevel}
onSelect={(pl) => { onSelect={(pl) => {
closeMenu(); closeMenu();
handlePowerLevelChange(pl); handlePowerLevelChange(pl);

View File

@@ -19,6 +19,7 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function RoomProfile({ roomId }) { function RoomProfile({ roomId }) {
const isMountStore = useStore(); const isMountStore = useStore();
@@ -117,7 +118,13 @@ function RoomProfile({ roomId }) {
const handleAvatarUpload = async (url) => { const handleAvatarUpload = async (url) => {
if (url === null) { if (url === null) {
if (confirm('Are you sure you want to remove avatar?')) { const isConfirmed = await confirmDialog(
'Remove avatar',
'Are you sure that you want to remove room avatar?',
'Remove',
'caution',
);
if (isConfirmed) {
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, ''); await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
} }
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, ''); } else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');

View File

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import './RoomTile.scss'; import './RoomTile.scss';
import { twemojify } from '../../../util/twemojify'; import { twemojify } from '../../../util/twemojify';
import { sanitizeText } from '../../../util/sanitize';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';

View File

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

View File

@@ -9,7 +9,11 @@ function SettingTile({ title, options, content }) {
<div className="setting-tile"> <div className="setting-tile">
<div className="setting-tile__content"> <div className="setting-tile__content">
<div className="setting-tile__title"> <div className="setting-tile__title">
<Text variant="b1">{title}</Text> {
typeof title === 'string'
? <Text variant="b1">{title}</Text>
: title
}
</div> </div>
{content} {content}
</div> </div>
@@ -24,7 +28,7 @@ SettingTile.defaultProps = {
}; };
SettingTile.propTypes = { SettingTile.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.node.isRequired,
options: PropTypes.node, options: PropTypes.node,
content: PropTypes.node, content: PropTypes.node,
}; };

View File

@@ -9,11 +9,12 @@ import Tooltip from '../../atoms/tooltip/Tooltip';
import { blurOnBubbling } from '../../atoms/button/script'; import { blurOnBubbling } from '../../atoms/button/script';
const SidebarAvatar = React.forwardRef(({ const SidebarAvatar = React.forwardRef(({
tooltip, active, onClick, onContextMenu, className, tooltip, active, onClick,
avatar, notificationBadge, onContextMenu, avatar, notificationBadge,
}, ref) => { }, ref) => {
let activeClass = ''; const classes = ['sidebar-avatar'];
if (active) activeClass = ' sidebar-avatar--active'; if (active) classes.push('sidebar-avatar--active');
if (className) classes.push(className);
return ( return (
<Tooltip <Tooltip
content={<Text variant="b1">{twemojify(tooltip)}</Text>} content={<Text variant="b1">{twemojify(tooltip)}</Text>}
@@ -21,7 +22,7 @@ const SidebarAvatar = React.forwardRef(({
> >
<button <button
ref={ref} ref={ref}
className={`sidebar-avatar${activeClass}`} className={classes.join(' ')}
type="button" type="button"
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')} onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
onClick={onClick} onClick={onClick}
@@ -34,6 +35,7 @@ const SidebarAvatar = React.forwardRef(({
); );
}); });
SidebarAvatar.defaultProps = { SidebarAvatar.defaultProps = {
className: null,
active: false, active: false,
onClick: null, onClick: null,
onContextMenu: null, onContextMenu: null,
@@ -41,6 +43,7 @@ SidebarAvatar.defaultProps = {
}; };
SidebarAvatar.propTypes = { SidebarAvatar.propTypes = {
className: PropTypes.string,
tooltip: PropTypes.string.isRequired, tooltip: PropTypes.string.isRequired,
active: PropTypes.bool, active: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func,

View File

@@ -88,7 +88,7 @@ function SpaceAddExistingContent({ roomId }) {
}; };
const handleSearch = (ev) => { const handleSearch = (ev) => {
const term = ev.target.value.toLocaleLowerCase().replaceAll(' ', ''); const term = ev.target.value.toLocaleLowerCase().replace(/\s/g, '');
if (term === '') { if (term === '') {
setSearchIds(null); setSearchIds(null);
return; return;
@@ -100,7 +100,7 @@ function SpaceAddExistingContent({ roomId }) {
if (!name) return false; if (!name) return false;
name = name.normalize('NFKC') name = name.normalize('NFKC')
.toLocaleLowerCase() .toLocaleLowerCase()
.replaceAll(' ', ''); .replace(/\s/g, '');
return name.includes(term); return name.includes(term);
}); });
setSearchIds(searchedIds); setSearchIds(searchedIds);

View File

@@ -24,6 +24,8 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import PinIC from '../../../../public/res/ic/outlined/pin.svg'; import PinIC from '../../../../public/res/ic/outlined/pin.svg';
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg'; import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function SpaceOptions({ roomId, afterOptionSelect }) { function SpaceOptions({ roomId, afterOptionSelect }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
@@ -54,11 +56,16 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
afterOptionSelect(); afterOptionSelect();
}; };
const handleLeaveClick = () => { const handleLeaveClick = async () => {
if (confirm('Are you really want to leave this space?')) { afterOptionSelect();
leave(roomId); const isConfirmed = await confirmDialog(
afterOptionSelect(); 'Leave space',
} `Are you sure that you want to leave "${room.name}" space?`,
'Leave',
'danger',
);
if (!isConfirmed) return;
leave(roomId);
}; };
return ( return (

View File

@@ -210,7 +210,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
/> />
)} )}
content={( 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)" /> <Input name="topic" minHeight={174} resizable label="Topic (optional)" />

View File

@@ -2,6 +2,7 @@
@use '../../partials/dir'; @use '../../partials/dir';
.create-room { .create-room {
margin: var(--sp-normal);
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight)); @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
&__form > * { &__form > * {

View File

@@ -8,7 +8,7 @@ import Text from '../../atoms/text/Text';
function DragDrop({ isOpen }) { function DragDrop({ isOpen }) {
return ( return (
<RawModal <RawModal
className="drag-drop__model" className="drag-drop__modal"
overlayClassName="drag-drop__overlay" overlayClassName="drag-drop__overlay"
isOpen={isOpen} isOpen={isOpen}
> >

View File

@@ -1,4 +1,4 @@
.drag-drop__model { .drag-drop__modal {
box-shadow: none; box-shadow: none;
text-align: center; text-align: center;

View File

@@ -12,6 +12,7 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch'; import AsyncSearch from '../../../util/AsyncSearch';
import { addRecentEmoji, getRecentEmojis } from './recent';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon'; import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -20,6 +21,7 @@ import Input from '../../atoms/input/Input';
import ScrollView from '../../atoms/scroll/ScrollView'; import ScrollView from '../../atoms/scroll/ScrollView';
import SearchIC from '../../../../public/res/ic/outlined/search.svg'; 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 EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
import DogIC from '../../../../public/res/ic/outlined/dog.svg'; import DogIC from '../../../../public/res/ic/outlined/dog.svg';
import CupIC from '../../../../public/res/ic/outlined/cup.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 PeaceIC from '../../../../public/res/ic/outlined/peace.svg';
import FlagIC from '../../../../public/res/ic/outlined/flag.svg'; import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
const ROW_EMOJIS_COUNT = 7;
const EmojiGroup = React.memo(({ name, groupEmojis }) => { const EmojiGroup = React.memo(({ name, groupEmojis }) => {
function getEmojiBoard() { function getEmojiBoard() {
const emojiBoard = []; const emojiBoard = [];
const ROW_EMOJIS_COUNT = 7;
const totalEmojis = groupEmojis.length; const totalEmojis = groupEmojis.length;
for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) { for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
@@ -147,8 +150,9 @@ function EmojiBoard({ onSelect, searchRef }) {
function selectEmoji(e) { function selectEmoji(e) {
if (isTargetNotEmoji(e.target)) return; if (isTargetNotEmoji(e.target)) return;
const emoji = e.target; const emoji = getEmojiDataFromTarget(e.target);
onSelect(getEmojiDataFromTarget(emoji)); onSelect(emoji);
if (emoji.hexcode) addRecentEmoji(emoji.unicode);
} }
function setEmojiInfo(emoji) { function setEmojiInfo(emoji) {
@@ -188,6 +192,9 @@ function EmojiBoard({ onSelect, searchRef }) {
} }
const [availableEmojis, setAvailableEmojis] = useState([]); const [availableEmojis, setAvailableEmojis] = useState([]);
const [recentEmojis, setRecentEmojis] = useState([]);
const recentOffset = recentEmojis.length > 0 ? 1 : 0;
useEffect(() => { useEffect(() => {
const updateAvailableEmoji = (selectedRoomId) => { const updateAvailableEmoji = (selectedRoomId) => {
@@ -215,6 +222,9 @@ function EmojiBoard({ onSelect, searchRef }) {
const onOpen = () => { const onOpen = () => {
searchRef.current.value = ''; searchRef.current.value = '';
handleSearchChange(); 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); navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
@@ -230,7 +240,7 @@ function EmojiBoard({ onSelect, searchRef }) {
const $emojiContent = scrollEmojisRef.current.firstElementChild; const $emojiContent = scrollEmojisRef.current.firstElementChild;
const groupCount = $emojiContent.childElementCount; const groupCount = $emojiContent.childElementCount;
if (groupCount > emojiGroups.length) { if (groupCount > emojiGroups.length) {
tabIndex += groupCount - emojiGroups.length - availableEmojis.length; tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset;
} }
$emojiContent.children[tabIndex].scrollIntoView(); $emojiContent.children[tabIndex].scrollIntoView();
} }
@@ -246,6 +256,7 @@ function EmojiBoard({ onSelect, searchRef }) {
<ScrollView ref={scrollEmojisRef} autoHide> <ScrollView ref={scrollEmojisRef} autoHide>
<div onMouseMove={hoverEmoji} onClick={selectEmoji}> <div onMouseMove={hoverEmoji} onClick={selectEmoji}>
<SearchedEmoji /> <SearchedEmoji />
{recentEmojis.length > 0 && <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />}
{ {
availableEmojis.map((pack) => ( availableEmojis.map((pack) => (
<EmojiGroup <EmojiGroup
@@ -271,13 +282,21 @@ function EmojiBoard({ onSelect, searchRef }) {
</div> </div>
<ScrollView invisible> <ScrollView invisible>
<div className="emoji-board__nav"> <div className="emoji-board__nav">
{recentEmojis.length > 0 && (
<IconButton
onClick={() => openGroup(0)}
src={RecentClockIC}
tooltip="Recent"
tooltipPlacement="right"
/>
)}
<div className="emoji-board__nav-custom"> <div className="emoji-board__nav-custom">
{ {
availableEmojis.map((pack) => { availableEmojis.map((pack) => {
const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc); const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
return ( return (
<IconButton <IconButton
onClick={() => openGroup(pack.packIndex)} onClick={() => openGroup(recentOffset + pack.packIndex)}
src={src} src={src}
key={pack.packIndex} key={pack.packIndex}
tooltip={pack.displayName} tooltip={pack.displayName}
@@ -301,7 +320,7 @@ function EmojiBoard({ onSelect, searchRef }) {
[7, FlagIC, 'Flags'], [7, FlagIC, 'Flags'],
].map(([indx, ico, name]) => ( ].map(([indx, ico, name]) => (
<IconButton <IconButton
onClick={() => openGroup(availableEmojis.length + indx)} onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
key={indx} key={indx}
src={ico} src={ico}
tooltip={name} tooltip={name}

View File

@@ -6,6 +6,8 @@
--emoji-board-height: 390px; --emoji-board-height: 390px;
--emoji-board-width: 286px; --emoji-board-width: 286px;
display: flex; display: flex;
max-width: 90vw;
max-height: 90vh;
&__content { &__content {
@extend .cp-fx__item-one; @extend .cp-fx__item-one;
@@ -91,6 +93,10 @@
} }
} }
.emoji-row {
display: flex;
}
.emoji-group { .emoji-group {
--emoji-padding: 6px; --emoji-padding: 6px;
position: relative; position: relative;

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),
});
}

View File

@@ -0,0 +1,200 @@
/* eslint-disable react/prop-types */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './EmojiVerification.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { hasPrivateKey } from '../../../client/state/secretStorageKeys';
import { getDefaultSSKey, isCrossVerified } from '../../../util/matrixUtil';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Spinner from '../../atoms/spinner/Spinner';
import Dialog from '../../molecules/dialog/Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useStore } from '../../hooks/useStore';
import { accessSecretStorage } from '../settings/SecretStorageAccess';
function EmojiVerificationContent({ data, requestClose }) {
const [sas, setSas] = useState(null);
const [process, setProcess] = useState(false);
const { request, targetDevice } = data;
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const beginStore = useStore();
const beginVerification = async () => {
if (
isCrossVerified(mx.deviceId)
&& (mx.getCrossSigningId() === null || await mx.crypto.crossSigningInfo.isStoredInKeyCache('self_signing') === false)
) {
if (!hasPrivateKey(getDefaultSSKey())) {
const keyData = await accessSecretStorage('Emoji verification');
if (!keyData) {
request.cancel();
return;
}
}
await mx.checkOwnCrossSigningTrust();
}
setProcess(true);
await request.accept();
const verifier = request.beginKeyVerification('m.sas.v1', targetDevice);
const handleVerifier = (sasData) => {
verifier.off('show_sas', handleVerifier);
if (!mountStore.getItem()) return;
setSas(sasData);
setProcess(false);
};
verifier.on('show_sas', handleVerifier);
await verifier.verify();
};
const sasMismatch = () => {
sas.mismatch();
setProcess(true);
};
const sasConfirm = () => {
sas.confirm();
setProcess(true);
};
useEffect(() => {
mountStore.setItem(true);
const handleChange = () => {
if (request.done || request.cancelled) {
requestClose();
return;
}
if (targetDevice && !beginStore.getItem()) {
beginStore.setItem(true);
beginVerification();
}
};
if (request === null) return null;
const req = request;
req.on('change', handleChange);
return () => {
req.off('change', handleChange);
if (req.cancelled === false && req.done === false) {
req.cancel();
}
};
}, [request]);
const renderWait = () => (
<>
<Spinner size="small" />
<Text>Waiting for response from other device...</Text>
</>
);
if (sas !== null) {
return (
<div className="emoji-verification__content">
<Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
<div className="emoji-verification__emojis">
{sas.sas.emoji.map((emoji, i) => (
// eslint-disable-next-line react/no-array-index-key
<div className="emoji-verification__emoji-block" key={`${emoji[1]}-${i}`}>
<Text variant="h1">{twemojify(emoji[0])}</Text>
<Text>{emoji[1]}</Text>
</div>
))}
</div>
<div className="emoji-verification__buttons">
{process ? renderWait() : (
<>
<Button variant="primary" onClick={sasConfirm}>They match</Button>
<Button onClick={sasMismatch}>{'They don\'t match'}</Button>
</>
)}
</div>
</div>
);
}
if (targetDevice) {
return (
<div className="emoji-verification__content">
<Text>Please accept the request from other device.</Text>
<div className="emoji-verification__buttons">
{renderWait()}
</div>
</div>
);
}
return (
<div className="emoji-verification__content">
<Text>Click accept to start the verification process.</Text>
<div className="emoji-verification__buttons">
{
process
? renderWait()
: <Button variant="primary" onClick={beginVerification}>Accept</Button>
}
</div>
</div>
);
}
EmojiVerificationContent.propTypes = {
data: PropTypes.shape({}).isRequired,
requestClose: PropTypes.func.isRequired,
};
function useVisibilityToggle() {
const [data, setData] = useState(null);
const mx = initMatrix.matrixClient;
useEffect(() => {
const handleOpen = (request, targetDevice) => {
setData({ request, targetDevice });
};
navigation.on(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
mx.on('crypto.verification.request', handleOpen);
return () => {
navigation.removeListener(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
mx.removeListener('crypto.verification.request', handleOpen);
};
}, []);
const requestClose = () => setData(null);
return [data, requestClose];
}
function EmojiVerification() {
const [data, requestClose] = useVisibilityToggle();
return (
<Dialog
isOpen={data !== null}
className="emoji-verification"
title={(
<Text variant="s1" weight="medium" primary>
Emoji verification
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{
data !== null
? <EmojiVerificationContent data={data} requestClose={requestClose} />
: <div />
}
</Dialog>
);
}
export default EmojiVerification;

View File

@@ -0,0 +1,35 @@
@use '../../partials/flex';
@use '../../partials/dir';
.emoji-verification {
&__content {
padding: var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
display: flex;
flex-direction: column;
gap: var(--sp-normal);
}
&__emojis {
margin: var(--sp-loose) 0;
display: flex;
align-items: center;
justify-content: space-around;
gap: var(--sp-extra-tight);
flex-wrap: wrap;
}
&__emoji-block {
@extend .cp-fx__column;
flex: 1;
align-items: center;
gap: var(--sp-extra-tight);
white-space: nowrap;
text-transform: capitalize;
}
&__buttons {
display: flex;
gap: var(--sp-normal);
}
}

View File

@@ -54,17 +54,20 @@ function InviteList({ isOpen, onRequestClose }) {
}, [procInvite]); }, [procInvite]);
function renderRoomTile(roomId) { function renderRoomTile(roomId) {
const myRoom = initMatrix.matrixClient.getRoom(roomId); const mx = initMatrix.matrixClient;
const myRoom = mx.getRoom(roomId);
if (!myRoom) return null;
const roomName = myRoom.name; const roomName = myRoom.name;
let roomAlias = myRoom.getCanonicalAlias(); let roomAlias = myRoom.getCanonicalAlias();
if (roomAlias === null) roomAlias = myRoom.roomId; if (!roomAlias) roomAlias = myRoom.roomId;
const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? '';
return ( return (
<RoomTile <RoomTile
key={myRoom.roomId} key={myRoom.roomId}
name={roomName} name={roomName}
avatarSrc={initMatrix.matrixClient.getRoom(roomId).getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop')} avatarSrc={initMatrix.matrixClient.getRoom(roomId).getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop')}
id={roomAlias} id={roomAlias}
inviterName={myRoom.getJoinedMembers()[0].userId} inviterName={inviterName}
options={ options={
procInvite.has(myRoom.roomId) procInvite.has(myRoom.roomId)
? (<Spinner size="small" />) ? (<Spinner size="small" />)
@@ -95,12 +98,13 @@ function InviteList({ isOpen, onRequestClose }) {
{ {
Array.from(initMatrix.roomList.inviteDirects).map((roomId) => { Array.from(initMatrix.roomList.inviteDirects).map((roomId) => {
const myRoom = initMatrix.matrixClient.getRoom(roomId); const myRoom = initMatrix.matrixClient.getRoom(roomId);
if (myRoom === null) return null;
const roomName = myRoom.name; const roomName = myRoom.name;
return ( return (
<RoomTile <RoomTile
key={myRoom.roomId} key={myRoom.roomId}
name={roomName} name={roomName}
id={myRoom.getDMInviter()} id={myRoom.getDMInviter() || roomId}
options={ options={
procInvite.has(myRoom.roomId) procInvite.has(myRoom.roomId)
? (<Spinner size="small" />) ? (<Spinner size="small" />)

View File

@@ -103,6 +103,18 @@ function InviteUser({
updateIsSearching(false); updateIsSearching(false);
} }
async function hasDevices(userId) {
try {
const usersDeviceMap = await mx.downloadKeys([userId, mx.getUserId()]);
return Object.values(usersDeviceMap).every((userDevices) =>
Object.keys(userDevices).length > 0,
);
} catch (e) {
console.error("Error determining if it's possible to encrypt to all users: ", e);
return false;
}
}
async function createDM(userId) { async function createDM(userId) {
if (mx.getUserId() === userId) return; if (mx.getUserId() === userId) return;
const dmRoomId = hasDMWith(userId); const dmRoomId = hasDMWith(userId);
@@ -117,7 +129,7 @@ function InviteUser({
procUserError.delete(userId); procUserError.delete(userId);
updateUserProcError(getMapCopy(procUserError)); updateUserProcError(getMapCopy(procUserError));
const result = await roomActions.createDM(userId); const result = await roomActions.createDM(userId, await hasDevices(userId));
roomIdToUserId.set(result.room_id, userId); roomIdToUserId.set(result.room_id, userId);
updateRoomIdToUserId(getMapCopy(roomIdToUserId)); updateRoomIdToUserId(getMapCopy(roomIdToUserId));
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,155 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './JoinAlias.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { join } from '../../../client/action/room';
import { selectRoom, selectSpace } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import Dialog from '../../molecules/dialog/Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useStore } from '../../hooks/useStore';
const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/;
function JoinAliasContent({ term, requestClose }) {
const [process, setProcess] = useState(false);
const [error, setError] = useState(undefined);
const [lastJoinId, setLastJoinId] = useState(undefined);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const openRoom = (roomId) => {
const room = mx.getRoom(roomId);
if (!room) return;
if (room.isSpaceRoom()) selectSpace(roomId);
else selectRoom(roomId);
requestClose();
};
useEffect(() => {
const handleJoin = (roomId) => {
if (lastJoinId !== roomId) return;
openRoom(roomId);
};
initMatrix.roomList.on(cons.events.roomList.ROOM_JOINED, handleJoin);
return () => {
initMatrix.roomList.removeListener(cons.events.roomList.ROOM_JOINED, handleJoin);
};
}, [lastJoinId]);
const handleSubmit = async (e) => {
e.preventDefault();
mountStore.setItem(true);
const alias = e.target.alias.value;
if (alias?.trim() === '') return;
if (alias.match(ALIAS_OR_ID_REG) === null) {
setError('Invalid address.');
return;
}
setProcess('Looking for address...');
setError(undefined);
let via;
if (alias.startsWith('#')) {
try {
const aliasData = await mx.resolveRoomAlias(alias);
via = aliasData?.servers.slice(0, 3) || [];
if (mountStore.getItem()) {
setProcess(`Joining ${alias}...`);
}
} catch (err) {
if (!mountStore.getItem()) return;
setProcess(false);
setError(`Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`);
}
}
try {
const roomId = await join(alias, false, via);
if (!mountStore.getItem()) return;
setLastJoinId(roomId);
openRoom(roomId);
} catch {
if (!mountStore.getItem()) return;
setProcess(false);
setError(`Unable to join ${alias}. Either room/space is private or doesn't exist.`);
}
};
return (
<form className="join-alias" onSubmit={handleSubmit}>
<Input
label="Address"
value={term}
name="alias"
required
/>
{error && <Text className="join-alias__error" variant="b3">{error}</Text>}
<div className="join-alias__btn">
{
process
? (
<>
<Spinner size="small" />
<Text>{process}</Text>
</>
)
: <Button variant="primary" type="submit">Join</Button>
}
</div>
</form>
);
}
JoinAliasContent.defaultProps = {
term: undefined,
};
JoinAliasContent.propTypes = {
term: PropTypes.string,
requestClose: PropTypes.func.isRequired,
};
function useWindowToggle() {
const [data, setData] = useState(null);
useEffect(() => {
const handleOpen = (term) => {
setData({ term });
};
navigation.on(cons.events.navigation.JOIN_ALIAS_OPENED, handleOpen);
return () => {
navigation.removeListener(cons.events.navigation.JOIN_ALIAS_OPENED, handleOpen);
};
}, []);
const onRequestClose = () => setData(null);
return [data, onRequestClose];
}
function JoinAlias() {
const [data, requestClose] = useWindowToggle();
return (
<Dialog
isOpen={data !== null}
title={(
<Text variant="s1" weight="medium" primary>Join with address</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
onRequestClose={requestClose}
>
{ data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div /> }
</Dialog>
);
}
export default JoinAlias;

View File

@@ -0,0 +1,20 @@
@use '../../partials/dir';
.join-alias {
padding: var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
& > *:not(:first-child) {
margin-top: var(--sp-normal);
}
&__error {
color: var(--tc-danger-high);
margin-top: var(--sp-extra-tight) !important;
}
&__btn {
display: flex;
gap: var(--sp-normal);
}
}

View File

@@ -1,18 +1,39 @@
import React, { useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import Postie from '../../../util/Postie'; import Postie from '../../../util/Postie';
import { roomIdByActivity } from '../../../util/sort';
import RoomsCategory from './RoomsCategory'; import RoomsCategory from './RoomsCategory';
import { AtoZ } from './common';
const drawerPostie = new Postie(); const drawerPostie = new Postie();
function Directs() { function Directs({ size }) {
const mx = initMatrix.matrixClient;
const { roomList, notifications } = initMatrix; const { roomList, notifications } = initMatrix;
const directIds = [...roomList.directs].sort(AtoZ); const [directIds, setDirectIds] = useState([]);
useEffect(() => setDirectIds([...roomList.directs].sort(roomIdByActivity)), [size]);
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(() => { useEffect(() => {
const selectorChanged = (selectedRoomId, prevSelectedRoomId) => { const selectorChanged = (selectedRoomId, prevSelectedRoomId) => {
@@ -43,5 +64,8 @@ function Directs() {
return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />; return <RoomsCategory name="People" hideHeader roomIds={directIds} drawerPostie={drawerPostie} />;
} }
Directs.propTypes = {
size: PropTypes.number.isRequired,
};
export default Directs; export default Directs;

View File

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

View File

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

View File

@@ -5,11 +5,11 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import Postie from '../../../util/Postie'; import Postie from '../../../util/Postie';
import { roomIdByActivity, roomIdByAtoZ } from '../../../util/sort';
import RoomsCategory from './RoomsCategory'; import RoomsCategory from './RoomsCategory';
import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces'; import { useCategorizedSpaces } from '../../hooks/useCategorizedSpaces';
import { AtoZ, RoomToDM } from './common';
const drawerPostie = new Postie(); const drawerPostie = new Postie();
function Home({ spaceId }) { function Home({ spaceId }) {
@@ -34,10 +34,6 @@ function Home({ spaceId }) {
roomIds = roomList.getOrphanRooms(); roomIds = roomList.getOrphanRooms();
} }
spaceIds.sort(AtoZ);
roomIds.sort(AtoZ);
directIds.sort(AtoZ);
if (isCategorized) { if (isCategorized) {
categories = roomList.getCategorizedSpaces(spaceIds); categories = roomList.getCategorizedSpaces(spaceIds);
categories.delete(spaceId); categories.delete(spaceId);
@@ -73,26 +69,36 @@ function Home({ spaceId }) {
return ( return (
<> <>
{ !isCategorized && spaceIds.length !== 0 && ( { !isCategorized && spaceIds.length !== 0 && (
<RoomsCategory name="Spaces" roomIds={spaceIds} drawerPostie={drawerPostie} /> <RoomsCategory name="Spaces" roomIds={spaceIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
)} )}
{ roomIds.length !== 0 && ( { roomIds.length !== 0 && (
<RoomsCategory name="Rooms" roomIds={roomIds} drawerPostie={drawerPostie} /> <RoomsCategory name="Rooms" roomIds={roomIds.sort(roomIdByAtoZ)} drawerPostie={drawerPostie} />
)} )}
{ directIds.length !== 0 && ( { directIds.length !== 0 && (
<RoomsCategory name="People" roomIds={directIds} drawerPostie={drawerPostie} /> <RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
)} )}
{ isCategorized && [...categories].map(([catId, childIds]) => ( { isCategorized && [...categories].map(([catId, childIds]) => {
<RoomsCategory const rms = [];
key={catId} const dms = [];
spaceId={catId} childIds.forEach((id) => {
name={mx.getRoom(catId).name} if (directs.has(id)) dms.push(id);
roomIds={[...childIds].sort(AtoZ).sort(RoomToDM)} else rms.push(id);
drawerPostie={drawerPostie} });
/> rms.sort(roomIdByAtoZ);
))} dms.sort(roomIdByActivity);
return (
<RoomsCategory
key={catId}
spaceId={catId}
name={mx.getRoom(catId).name}
roomIds={rms.concat(dms)}
drawerPostie={drawerPostie}
/>
);
})}
</> </>
); );
} }

View File

@@ -14,6 +14,7 @@ import {
} from '../../../client/action/navigation'; } from '../../../client/action/navigation';
import { moveSpaceShortcut } from '../../../client/action/accountData'; import { moveSpaceShortcut } from '../../../client/action/accountData';
import { abbreviateNumber, getEventCords } from '../../../util/common'; import { abbreviateNumber, getEventCords } from '../../../util/common';
import { isCrossVerified } from '../../../util/matrixUtil';
import Avatar from '../../atoms/avatar/Avatar'; import Avatar from '../../atoms/avatar/Avatar';
import NotificationBadge from '../../atoms/badge/NotificationBadge'; 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 AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg'; import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import InviteIC from '../../../../public/res/ic/outlined/invite.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 { useSelectedTab } from '../../hooks/useSelectedTab';
import { useDeviceList } from '../../hooks/useDeviceList';
import { tabText as settingTabText } from '../settings/Settings';
function useNotificationUpdate() { function useNotificationUpdate() {
const { notifications } = initMatrix; const { notifications } = initMatrix;
@@ -72,7 +77,7 @@ function ProfileAvatarMenu() {
return ( return (
<SidebarAvatar <SidebarAvatar
onClick={openSettings} onClick={openSettings}
tooltip={profile.displayName} tooltip="Settings"
avatar={( avatar={(
<Avatar <Avatar
text={profile.displayName} text={profile.displayName}
@@ -85,6 +90,22 @@ function ProfileAvatarMenu() {
); );
} }
function CrossSigninAlert() {
const deviceList = useDeviceList();
const unverified = deviceList?.filter((device) => isCrossVerified(device.device_id) === false);
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() { function FeaturedTab() {
const { roomList, accountData, notifications } = initMatrix; const { roomList, accountData, notifications } = initMatrix;
const [selectedTab] = useSelectedTab(); const [selectedTab] = useSelectedTab();
@@ -358,6 +379,7 @@ function SideBar() {
notificationBadge={<NotificationBadge alert content={totalInvites} />} notificationBadge={<NotificationBadge alert content={totalInvites} />}
/> />
)} )}
<CrossSigninAlert />
<ProfileAvatarMenu /> <ProfileAvatarMenu />
</div> </div>
</div> </div>

View File

@@ -58,3 +58,20 @@
height: 1px; height: 1px;
background-color: var(--bg-surface-border); 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);
}
}

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 };

View File

@@ -1,37 +1,53 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
import ImageUpload from '../../molecules/image-upload/ImageUpload'; import ImageUpload from '../../molecules/image-upload/ImageUpload';
import Input from '../../atoms/input/Input'; import Input from '../../atoms/input/Input';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import './ProfileEditor.scss'; import './ProfileEditor.scss';
// TODO Fix bug that prevents 'Save' button from enabling up until second changed. // TODO Fix bug that prevents 'Save' button from enabling up until second changed.
function ProfileEditor({ function ProfileEditor({ userId }) {
userId, const [isEditing, setIsEditing] = useState(false);
}) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const user = mx.getUser(mx.getUserId());
const displayNameRef = useRef(null); const displayNameRef = useRef(null);
const bgColor = colorMXID(userId); const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
const [avatarSrc, setAvatarSrc] = useState(null); const [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true); const [disabled, setDisabled] = useState(true);
let username = mx.getUser(mx.getUserId()).displayName;
useEffect(() => { useEffect(() => {
let isMounted = true;
mx.getProfileInfo(mx.getUserId()).then((info) => { mx.getProfileInfo(mx.getUserId()).then((info) => {
if (!isMounted) return;
setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null); setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null);
setUsername(info.displayname);
}); });
return () => {
isMounted = false;
};
}, [userId]); }, [userId]);
// Sets avatar URL and updates the avatar component in profile editor to reflect new upload const handleAvatarUpload = async (url) => {
function handleAvatarUpload(url) {
if (url === null) { if (url === null) {
if (confirm('Are you sure you want to remove avatar?')) { const isConfirmed = await confirmDialog(
'Remove avatar',
'Are you sure that you want to remove avatar?',
'Remove',
'caution',
);
if (isConfirmed) {
mx.setAvatarUrl(''); mx.setAvatarUrl('');
setAvatarSrc(null); setAvatarSrc(null);
} }
@@ -39,48 +55,72 @@ function ProfileEditor({
} }
mx.setAvatarUrl(url); mx.setAvatarUrl(url);
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop')); setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
} };
function saveDisplayName() { const saveDisplayName = () => {
const newDisplayName = displayNameRef.current.value; const newDisplayName = displayNameRef.current.value;
if (newDisplayName !== null && newDisplayName !== username) { if (newDisplayName !== null && newDisplayName !== username) {
mx.setDisplayName(newDisplayName); mx.setDisplayName(newDisplayName);
username = newDisplayName; setUsername(newDisplayName);
setDisabled(true); setDisabled(true);
setIsEditing(false);
} }
} };
function onDisplayNameInputChange() { const onDisplayNameInputChange = () => {
setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null); setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
} };
function cancelDisplayNameChanges() { const cancelDisplayNameChanges = () => {
displayNameRef.current.value = username; displayNameRef.current.value = username;
onDisplayNameInputChange(); onDisplayNameInputChange();
} setIsEditing(false);
};
return ( const renderForm = () => (
<form <form
className="profile-editor" className="profile-editor__form"
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }} 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 <ImageUpload
text={username} text={username}
bgColor={bgColor} bgColor={colorMXID(userId)}
imageSrc={avatarSrc} imageSrc={avatarSrc}
onUpload={handleAvatarUpload} onUpload={handleAvatarUpload}
onRequestRemove={() => handleAvatarUpload(null)} onRequestRemove={() => handleAvatarUpload(null)}
/> />
<div className="profile-editor__input-wrapper"> {
<Input isEditing ? renderForm() : renderInfo()
label={`Display name of ${mx.getUserId()}`} }
onChange={onDisplayNameInputChange} </div>
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</div>
</form>
); );
} }

View File

@@ -1,28 +1,41 @@
@use '../../partials/dir'; @use '../../partials/dir';
@use '../../partials/flex';
.profile-editor { .profile-editor {
display: flex; display: flex;
align-items: flex-start; align-items: flex-end;
} }
.profile-editor__input-wrapper { .profile-editor__info,
flex: 1; .profile-editor__form {
min-width: 0; @extend .cp-fx__item-one;
margin-top: 10px; @include dir.side(margin, var(--sp-loose), 0);
display: flex; 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; flex-wrap: wrap;
align-items: flex-end;
& > .input-container { & > .input-container {
flex: 1; @extend .cp-fx__item-one;
} }
& > button { & > button {
height: 46px; height: 46px;
margin-top: var(--sp-normal); margin-top: var(--sp-normal);
}
& > * {
@include dir.side(margin, var(--sp-normal), 0); @include dir.side(margin, var(--sp-normal), 0);
} }
} }

View File

@@ -32,6 +32,7 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
function ModerationTools({ function ModerationTools({
roomId, userId, roomId, userId,
@@ -362,7 +363,7 @@ function ProfileViewer() {
&& (powerLevel < myPowerLevel || userId === mx.getUserId()) && (powerLevel < myPowerLevel || userId === mx.getUserId())
); );
const handleChangePowerLevel = (newPowerLevel) => { const handleChangePowerLevel = async (newPowerLevel) => {
if (newPowerLevel === powerLevel) return; if (newPowerLevel === powerLevel) return;
const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?'; const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?'; const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
@@ -370,9 +371,14 @@ function ProfileViewer() {
const isSharedPower = newPowerLevel === myPowerLevel; const isSharedPower = newPowerLevel === myPowerLevel;
const isDemotingMyself = userId === mx.getUserId(); const isDemotingMyself = userId === mx.getUserId();
if (isSharedPower || isDemotingMyself) { if (isSharedPower || isDemotingMyself) {
if (confirm(isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG)) { const isConfirmed = await confirmDialog(
roomActions.setPowerLevel(roomId, userId, newPowerLevel); 'Change power level',
} isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
'Change',
'caution',
);
if (!isConfirmed) return;
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
} else { } else {
roomActions.setPowerLevel(roomId, userId, newPowerLevel); roomActions.setPowerLevel(roomId, userId, newPowerLevel);
} }

View File

@@ -4,7 +4,7 @@ import './PublicRooms.scss';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; 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 * as roomActions from '../../../client/action/room';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
@@ -179,7 +179,9 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
}, [joiningRooms]); }, [joiningRooms]);
function handleViewRoom(roomId) { function handleViewRoom(roomId) {
selectRoom(roomId); const room = initMatrix.matrixClient.getRoom(roomId);
if (room.isSpaceRoom()) selectTab(roomId);
else selectRoom(roomId);
onRequestClose(); onRequestClose();
} }
@@ -193,7 +195,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
return rooms.map((room) => { return rooms.map((room) => {
const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id; const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id;
const name = typeof room.name === 'string' ? room.name : alias; const name = typeof room.name === 'string' ? room.name : alias;
const isJoined = initMatrix.roomList.rooms.has(room.room_id); const isJoined = initMatrix.matrixClient.getRoom(room.room_id)?.getMyMembership() === 'join';
return ( return (
<RoomTile <RoomTile
key={room.room_id} key={room.room_id}

View File

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

View File

@@ -18,7 +18,6 @@ function Windows() {
const [inviteUser, changeInviteUser] = useState({ const [inviteUser, changeInviteUser] = useState({
isOpen: false, roomId: undefined, term: undefined, isOpen: false, roomId: undefined, term: undefined,
}); });
const [settings, changeSettings] = useState(false);
function openInviteList() { function openInviteList() {
changeInviteList(true); changeInviteList(true);
@@ -36,20 +35,15 @@ function Windows() {
searchTerm, searchTerm,
}); });
} }
function openSettings() {
changeSettings(true);
}
useEffect(() => { useEffect(() => {
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
return () => { return () => {
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); 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} searchTerm={inviteUser.searchTerm}
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })} onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
/> />
<Settings <Settings />
isOpen={settings}
onRequestClose={() => changeSettings(false)}
/>
<SpaceSettings /> <SpaceSettings />
<SpaceManage /> <SpaceManage />
</> </>

View File

@@ -64,9 +64,11 @@ function ReadReceipts() {
onRequestClose={() => setIsOpen(false)} onRequestClose={() => setIsOpen(false)}
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />} contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
> >
{ <div style={{ marginTop: 'var(--sp-tight)', marginBottom: 'var(--sp-extra-loose)' }}>
readers.map(renderPeople) {
} readers.map(renderPeople)
}
</div>
</Dialog> </Dialog>
); );
} }

View File

@@ -9,6 +9,7 @@ import { getPowerLabel, getUsernameOfRoomMember } from '../../../util/matrixUtil
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import { openInviteUser, openProfileViewer } from '../../../client/action/navigation'; import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
import AsyncSearch from '../../../util/AsyncSearch'; import AsyncSearch from '../../../util/AsyncSearch';
import { memberByAtoZ, memberByPowerLevel } from '../../../util/sort';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import Header, { TitleWrapper } from '../../atoms/header/Header'; 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 SearchIC from '../../../../public/res/ic/outlined/search.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.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) { function simplyfiMembers(members) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
return members.map((member) => ({ return members.map((member) => ({
@@ -111,7 +92,7 @@ function PeopleDrawer({ roomId }) {
setMemberList( setMemberList(
simplyfiMembers( simplyfiMembers(
getMembersWithMembership(membership) getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel), .sort(memberByAtoZ).sort(memberByPowerLevel),
), ),
); );
}; };

View File

@@ -3,9 +3,10 @@ import './Room.scss';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import settings from '../../../client/state/settings'; import settings from '../../../client/state/settings';
import RoomTimeline from '../../../client/state/RoomTimeline'; import RoomTimeline from '../../../client/state/RoomTimeline';
import navigation from '../../../client/state/navigation';
import { openNavigation } from '../../../client/action/navigation';
import Welcome from '../welcome/Welcome'; import Welcome from '../welcome/Welcome';
import RoomView from './RoomView'; import RoomView from './RoomView';
@@ -13,34 +14,50 @@ import RoomSettings from './RoomSettings';
import PeopleDrawer from './PeopleDrawer'; import PeopleDrawer from './PeopleDrawer';
function Room() { function Room() {
const [roomTimeline, setRoomTimeline] = useState(null); const [roomInfo, setRoomInfo] = useState({
const [eventId, setEventId] = useState(null); roomTimeline: null,
eventId: null,
});
const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer); const [isDrawer, setIsDrawer] = useState(settings.isPeopleDrawer);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const handleRoomSelected = (rId, pRoomId, eId) => {
if (mx.getRoom(rId)) {
setRoomTimeline(new RoomTimeline(rId, initMatrix.notifications));
setEventId(eId);
} else {
// TODO: add ability to join room if roomId is invalid
setRoomTimeline(null);
setEventId(null);
}
};
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
useEffect(() => { useEffect(() => {
const handleRoomSelected = (rId, pRoomId, eId) => {
roomInfo.roomTimeline?.removeInternalListeners();
if (mx.getRoom(rId)) {
setRoomInfo({
roomTimeline: new RoomTimeline(rId),
eventId: eId ?? null,
});
} else {
// TODO: add ability to join room if roomId is invalid
setRoomInfo({
roomTimeline: null,
eventId: null,
});
}
};
navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected); navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
return () => { return () => {
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected);
};
}, [roomInfo]);
useEffect(() => {
const handleDrawerToggling = (visiblity) => setIsDrawer(visiblity);
settings.on(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
return () => {
settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling); settings.removeListener(cons.events.settings.PEOPLE_DRAWER_TOGGLED, handleDrawerToggling);
roomTimeline?.removeInternalListeners();
}; };
}, []); }, []);
if (roomTimeline === null) return <Welcome />; const { roomTimeline, eventId } = roomInfo;
if (roomTimeline === null) {
setTimeout(() => openNavigation());
return <Welcome />;
}
return ( return (
<div className="room"> <div className="room">
@@ -48,7 +65,7 @@ function Room() {
<RoomSettings roomId={roomTimeline.roomId} /> <RoomSettings roomId={roomTimeline.roomId} />
<RoomView roomTimeline={roomTimeline} eventId={eventId} /> <RoomView roomTimeline={roomTimeline} eventId={eventId} />
</div> </div>
{ isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />} {isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
</div> </div>
); );
} }

View File

@@ -1,4 +1,5 @@
@use '../../partials/flex'; @use '../../partials/flex';
@use '../../partials/screen';
.room { .room {
@extend .cp-fx__row; @extend .cp-fx__row;
@@ -10,3 +11,9 @@
overflow: hidden; overflow: hidden;
} }
} }
.room .people-drawer {
@include screen.smallerThan(tabletBreakpoint) {
display: none;
}
}

View File

@@ -36,6 +36,7 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg'; import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
const tabText = { const tabText = {
GENERAL: 'General', GENERAL: 'General',
@@ -85,10 +86,15 @@ function GeneralSettings({ roomId }) {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
variant="danger" variant="danger"
onClick={() => { onClick={async () => {
if (confirm('Are you really want to leave this room?')) { const isConfirmed = await confirmDialog(
roomActions.leave(roomId); 'Leave room',
} `Are you sure that you want to leave "${room.name}" room?`,
'Leave',
'danger',
);
if (!isConfirmed) return;
roomActions.leave(roomId);
}} }}
iconSrc={LeaveArrowIC} iconSrc={LeaveArrowIC}
> >
@@ -123,7 +129,7 @@ function SecuritySettings({ roomId }) {
<RoomEncryption roomId={roomId} /> <RoomEncryption roomId={roomId} />
</div> </div>
<div className="room-settings__card"> <div className="room-settings__card">
<MenuHeader>Message history visibility (Who can read history)</MenuHeader> <MenuHeader>Message history visibility</MenuHeader>
<RoomHistoryVisibility roomId={roomId} /> <RoomHistoryVisibility roomId={roomId} />
</div> </div>
</> </>

View File

@@ -1,4 +1,6 @@
@use '../../partials/flex'; @use '../../partials/flex';
@use '../../partials/screen';
@use '../../partials/dir';
.room-view { .room-view {
@extend .cp-fx__column; @extend .cp-fx__column;
@@ -18,6 +20,12 @@
box-shadow: var(--bs-popup); box-shadow: var(--bs-popup);
} }
& .header {
@include screen.smallerThan(mobileBreakpoint) {
padding: 0 var(--sp-tight);
}
}
&__content-wrapper { &__content-wrapper {
@extend .cp-fx__item-one; @extend .cp-fx__item-one;
@extend .cp-fx__column; @extend .cp-fx__column;

View File

@@ -21,6 +21,7 @@ import AsyncSearch from '../../../util/AsyncSearch';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import ScrollView from '../../atoms/scroll/ScrollView'; import ScrollView from '../../atoms/scroll/ScrollView';
import FollowingMembers from '../../molecules/following-members/FollowingMembers'; import FollowingMembers from '../../molecules/following-members/FollowingMembers';
import { addRecentEmoji } from '../emoji-board/recent';
const commands = [{ const commands = [{
name: 'markdown', name: 'markdown',
@@ -43,7 +44,9 @@ const commands = [{
}, { }, {
name: 'leave', name: 'leave',
description: 'Leave current room', description: 'Leave current room',
exe: (roomId) => roomActions.leave(roomId), exe: (roomId) => {
roomActions.leave(roomId);
},
}, { }, {
name: 'invite', name: 'invite',
isOptions: true, isOptions: true,
@@ -237,6 +240,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
viewEvent.emit('cmd_fired'); viewEvent.emit('cmd_fired');
} }
if (myCmd.prefix === ':') { if (myCmd.prefix === ':') {
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
viewEvent.emit('cmd_fired', { viewEvent.emit('cmd_fired', {
replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode, replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
}); });

View File

@@ -10,7 +10,7 @@
&__info { &__info {
display: flex; display: flex;
width: 40px; width: 40px;
@include dir.side(margin, 10px, 14px); @include dir.side(margin, 14px, 10px);
& > * { & > * {
margin: auto; margin: auto;

View File

@@ -8,12 +8,14 @@ import PropTypes from 'prop-types';
import './RoomViewContent.scss'; import './RoomViewContent.scss';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import { openProfileViewer } from '../../../client/action/navigation'; import { openProfileViewer } from '../../../client/action/navigation';
import { diffMinutes, isInSameDay, Throttle } from '../../../util/common'; import { diffMinutes, isInSameDay, Throttle } from '../../../util/common';
import { markAsRead } from '../../../client/action/notifications';
import Divider from '../../atoms/divider/Divider'; import Divider from '../../atoms/divider/Divider';
import ScrollView from '../../atoms/scroll/ScrollView'; import ScrollView from '../../atoms/scroll/ScrollView';
@@ -49,21 +51,54 @@ function loadingMsgPlaceholders(key, count = 2) {
); );
} }
function genRoomIntro(mEvent, roomTimeline) { function RoomIntroContainer({ event, timeline }) {
const [, nameForceUpdate] = useForceUpdate();
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic; const { roomList } = initMatrix;
const isDM = initMatrix.roomList.directs.has(roomTimeline.roomId); const { room } = timeline;
let avatarSrc = roomTimeline.room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop'); const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
avatarSrc = isDM ? roomTimeline.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc; const isDM = roomList.directs.has(timeline.roomId);
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
const heading = isDM ? room.name : `Welcome to ${room.name}`;
const topic = twemojify(roomTopic || '', undefined, true);
const nameJsx = twemojify(room.name);
const desc = isDM
? (
<>
This is the beginning of your direct message history with @
<b>{nameJsx}</b>
{'. '}
{topic}
</>
)
: (
<>
{'This is the beginning of the '}
<b>{nameJsx}</b>
{' room. '}
{topic}
</>
);
useEffect(() => {
const handleUpdate = () => nameForceUpdate();
roomList.on(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
return () => {
roomList.removeListener(cons.events.roomList.ROOM_PROFILE_UPDATED, handleUpdate);
};
}, []);
return ( return (
<RoomIntro <RoomIntro
key={mEvent ? mEvent.getId() : 'room-intro'} roomId={timeline.roomId}
roomId={roomTimeline.roomId}
avatarSrc={avatarSrc} avatarSrc={avatarSrc}
name={roomTimeline.room.name} name={room.name}
heading={`Welcome to ${roomTimeline.room.name}`} heading={twemojify(heading)}
desc={`This is the beginning of ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`} desc={desc}
time={mEvent ? `Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null} time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
/> />
); );
} }
@@ -145,13 +180,12 @@ function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
if (isSpecificEvent) { if (isSpecificEvent) {
focusEventIndex = roomTimeline.getEventIndex(eId); focusEventIndex = roomTimeline.getEventIndex(eId);
} else if (!readUptoEvtStore.getItem()) { }
if (!readUptoEvtStore.getItem() && roomTimeline.hasEventInTimeline(readUpToId)) {
// either opening live timeline or jump to unread. // either opening live timeline or jump to unread.
focusEventIndex = roomTimeline.getUnreadEventIndex(readUpToId); readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
if (roomTimeline.hasEventInTimeline(readUpToId)) { }
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId)); if (readUptoEvtStore.getItem() && !isSpecificEvent) {
}
} else {
focusEventIndex = roomTimeline.getUnreadEventIndex(readUptoEvtStore.getItem().getId()); focusEventIndex = roomTimeline.getUnreadEventIndex(readUptoEvtStore.getItem().getId());
} }
@@ -167,7 +201,6 @@ function useTimeline(roomTimeline, eventId, readUptoEvtStore, eventLimitRef) {
setEventTimeline(eventId); setEventTimeline(eventId);
return () => { return () => {
roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline); roomTimeline.removeListener(cons.events.roomTimeline.READY, initTimeline);
roomTimeline.removeInternalListeners();
limit.setFrom(0); limit.setFrom(0);
}; };
}, [roomTimeline, eventId]); }, [roomTimeline, eventId]);
@@ -200,7 +233,7 @@ function usePaginate(
}; };
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer); roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
return () => { return () => {
roomTimeline.on(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer); roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, handlePaginatedFromServer);
}; };
}, [roomTimeline]); }, [roomTimeline]);
@@ -255,7 +288,7 @@ function useHandleScroll(
); );
roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom); roomTimeline.emit(cons.events.roomTimeline.AT_BOTTOM, isAtBottom);
if (isAtBottom && readUptoEvtStore.getItem()) { if (isAtBottom && readUptoEvtStore.getItem()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead()); requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
} }
}); });
autoPaginate(); autoPaginate();
@@ -265,7 +298,7 @@ function useHandleScroll(
const timelineScroll = timelineScrollRef.current; const timelineScroll = timelineScrollRef.current;
const limit = eventLimitRef.current; const limit = eventLimitRef.current;
if (readUptoEvtStore.getItem()) { if (readUptoEvtStore.getItem()) {
requestAnimationFrame(() => roomTimeline.markAllAsRead()); requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
} }
if (roomTimeline.isServingLiveTimeline()) { if (roomTimeline.isServingLiveTimeline()) {
limit.setFrom(roomTimeline.timeline.length - limit.maxEvents); limit.setFrom(roomTimeline.timeline.length - limit.maxEvents);
@@ -286,44 +319,39 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
useEffect(() => { useEffect(() => {
const timelineScroll = timelineScrollRef.current; const timelineScroll = timelineScrollRef.current;
const limit = eventLimitRef.current; const limit = eventLimitRef.current;
const sendReadReceipt = (event) => { const trySendReadReceipt = (event) => {
if (event.isSending()) return;
if (myUserId === event.getSender()) { if (myUserId === event.getSender()) {
roomTimeline.markAllAsRead(); requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
return; return;
} }
const readUpToEvent = readUptoEvtStore.getItem(); const readUpToEvent = readUptoEvtStore.getItem();
const readUpToId = roomTimeline.getReadUpToEventId(); const readUpToId = roomTimeline.getReadUpToEventId();
const isUnread = readUpToEvent?.getId() === readUpToId; const isUnread = readUpToEvent ? readUpToEvent?.getId() === readUpToId : true;
// if user doesn't have focus on app don't mark messages as read. if (isUnread === false) {
if (document.visibilityState === 'hidden' || timelineScroll.bottom >= 16) { if (document.visibilityState === 'visible' && timelineScroll.bottom < 16) {
if (isUnread) return; requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId)); } else {
readUptoEvtStore.setItem(roomTimeline.findEventByIdInTimelineSet(readUpToId));
}
return; return;
} }
// user has not mark room as read
if (!isUnread) {
roomTimeline.markAllAsRead();
}
const { timeline } = roomTimeline; const { timeline } = roomTimeline;
const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToEvent?.getId(); const unreadMsgIsLast = timeline[timeline.length - 2].getId() === readUpToId;
if (unreadMsgIsLast) { if (unreadMsgIsLast) {
roomTimeline.markAllAsRead(); requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
} }
}; };
const handleEvent = (event) => { const handleEvent = (event) => {
const tLength = roomTimeline.timeline.length; const tLength = roomTimeline.timeline.length;
const isUserViewingLive = ( const isViewingLive = roomTimeline.isServingLiveTimeline() && limit.length >= tLength - 1;
roomTimeline.isServingLiveTimeline() const isAttached = timelineScroll.bottom < SCROLL_TRIGGER_POS;
&& limit.length >= tLength - 1
&& timelineScroll.bottom < SCROLL_TRIGGER_POS if (isViewingLive && isAttached) {
);
if (isUserViewingLive) {
limit.setFrom(tLength - limit.maxEvents); limit.setFrom(tLength - limit.maxEvents);
sendReadReceipt(event); trySendReadReceipt(event);
setEvent(event); setEvent(event);
return; return;
} }
@@ -332,11 +360,8 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
setEvent(event); setEvent(event);
return; return;
} }
const isUserDitchedLive = (
roomTimeline.isServingLiveTimeline() if (isViewingLive) {
&& limit.length >= tLength - 1
);
if (isUserDitchedLive) {
// This stateUpdate will help to put the // This stateUpdate will help to put the
// loading msg placeholder at bottom // loading msg placeholder at bottom
setEvent(event); setEvent(event);
@@ -353,17 +378,7 @@ function useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, event
}; };
}, [roomTimeline]); }, [roomTimeline]);
useEffect(() => { return newEvent;
const timelineScroll = timelineScrollRef.current;
if (!roomTimeline.initialized) return;
if (timelineScroll.bottom < 16
&& !roomTimeline.canPaginateForward()
&& document.visibilityState === 'visible') {
timelineScroll.scrollToBottom();
} else {
timelineScroll.tryRestoringScroll();
}
}, [newEvent, roomTimeline]);
} }
let jumpToItemIndex = -1; let jumpToItemIndex = -1;
@@ -394,7 +409,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
timelineScrollRef, timelineScrollRef,
eventLimitRef, eventLimitRef,
); );
useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef); const newEvent = useEventArrive(roomTimeline, readUptoEvtStore, timelineScrollRef, eventLimitRef);
const { timeline } = roomTimeline; const { timeline } = roomTimeline;
@@ -419,7 +434,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) { if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward()) {
const readUpToId = roomTimeline.getReadUpToEventId(); const readUpToId = roomTimeline.getReadUpToEventId();
if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) { if (readUptoEvtStore.getItem()?.getId() === readUpToId || readUpToId === null) {
requestAnimationFrame(() => roomTimeline.markAllAsRead()); requestAnimationFrame(() => markAsRead(roomTimeline.roomId));
} }
} }
jumpToItemIndex = -1; jumpToItemIndex = -1;
@@ -448,6 +463,16 @@ function RoomViewContent({ eventId, roomTimeline }) {
timelineScroll.tryRestoringScroll(); timelineScroll.tryRestoringScroll();
}, [onLimitUpdate]); }, [onLimitUpdate]);
useEffect(() => {
const timelineScroll = timelineScrollRef.current;
if (!roomTimeline.initialized) return;
if (timelineScroll.bottom < 16 && !roomTimeline.canPaginateForward() && document.visibilityState === 'visible') {
timelineScroll.scrollToBottom();
} else {
timelineScroll.tryRestoringScroll();
}
}, [newEvent]);
const handleTimelineScroll = (event) => { const handleTimelineScroll = (event) => {
const timelineScroll = timelineScrollRef.current; const timelineScroll = timelineScrollRef.current;
if (!event.target) return; if (!event.target) return;
@@ -479,12 +504,14 @@ function RoomViewContent({ eventId, roomTimeline }) {
if (i === 0 && !roomTimeline.canPaginateBackward()) { if (i === 0 && !roomTimeline.canPaginateBackward()) {
if (mEvent.getType() === 'm.room.create') { if (mEvent.getType() === 'm.room.create') {
tl.push(genRoomIntro(mEvent, roomTimeline)); tl.push(
<RoomIntroContainer key={mEvent.getId()} event={mEvent} timeline={roomTimeline} />,
);
itemCountIndex += 1; itemCountIndex += 1;
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
continue; continue;
} else { } else {
tl.push(genRoomIntro(undefined, roomTimeline)); tl.push(<RoomIntroContainer key="room-intro" event={null} timeline={roomTimeline} />);
itemCountIndex += 1; itemCountIndex += 1;
} }
} }

View File

@@ -5,12 +5,13 @@ import './RoomViewFloating.scss';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import { markAsRead } from '../../../client/action/notifications';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button'; 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 TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsersActionJsx } from './common'; import { getUsersActionJsx } from './common';
@@ -23,7 +24,7 @@ function useJumpToEvent(roomTimeline) {
}; };
const cancelJumpToEvent = () => { const cancelJumpToEvent = () => {
roomTimeline.markAllAsRead(); markAsRead(roomTimeline.roomId);
setEventId(null); setEventId(null);
}; };
@@ -36,11 +37,12 @@ function useJumpToEvent(roomTimeline) {
setEventId(readEventId); setEventId(readEventId);
} }
const { notifications } = initMatrix;
const handleMarkAsRead = () => setEventId(null); const handleMarkAsRead = () => setEventId(null);
roomTimeline.on(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead); notifications.on(cons.events.notifications.FULL_READ, handleMarkAsRead);
return () => { return () => {
roomTimeline.removeListener(cons.events.roomTimeline.MARKED_AS_READ, handleMarkAsRead); notifications.removeListener(cons.events.notifications.FULL_READ, handleMarkAsRead);
setEventId(null); setEventId(null);
}; };
}, [roomTimeline]); }, [roomTimeline]);
@@ -96,28 +98,21 @@ function RoomViewFloating({
return ( return (
<> <>
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}> <div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
<Button onClick={jumpToEvent} variant="primary"> <Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
<Text variant="b2">Jump to unread</Text> <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> </Button>
<IconButton
onClick={cancelJumpToEvent}
variant="primary"
size="extra-small"
src={TickMarkIC}
tooltipPlacement="bottom"
tooltip="Mark as read"
/>
</div> </div>
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}> <div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
<div className="bouncing-loader"><div /></div> <div className="bouncing-loader"><div /></div>
<Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text> <Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
</div> </div>
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}> <div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
<IconButton <Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
onClick={handleScrollToBottom} <Text variant="b3" weight="medium">Jump to latest</Text>
src={ChevronBottomIC} </Button>
tooltip="Scroll to Bottom"
/>
</div> </div>
</> </>
); );

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 { &__STB {
position: absolute; position: absolute;
@include dir.prop(right, var(--sp-normal), unset); @include dir.prop(left, 50%, unset);
@include dir.prop(left, unset, var(--sp-normal)); @include dir.prop(right, unset, 50%);
bottom: 0; bottom: 0;
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border); box-shadow: var(--bs-surface-border);
background-color: var(--bg-surface-low);
transition: transform 200ms ease-in-out; transition: transform 200ms ease-in-out;
transform: translateY(100%) scale(0); transform: translate(-50%, 100%);
&--open { &--open {
transform: translateY(-28px) scale(1); transform: translate(-50%, -28px);
} }
} }
&__unread { &__unread {
position: absolute; position: absolute;
top: var(--sp-extra-tight); top: var(--sp-extra-tight);
@include dir.prop(right, var(--sp-extra-tight), unset); @include dir.prop(left, var(--sp-normal), unset);
@include dir.prop(left, unset, var(--sp-extra-tight)); @include dir.prop(right, unset, var(--sp-normal));
z-index: 999; z-index: 999;
display: none; display: none;
background-color: var(--bg-surface); width: calc(100% - var(--sp-extra-loose));
border-radius: var(--bo-radius); box-shadow: 0 0 2px 0 rgba(0, 0, 0, 20%);
box-shadow: var(--bs-primary-border);
overflow: hidden;
&--open { &--open {
display: flex; display: flex;
} }
& button:first-child {
& .ic-btn {
padding: 6px var(--sp-extra-tight);
border-radius: 0;
}
& .btn-primary {
@extend .cp-fx__item-one; @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);
}
} }
} }
} }

View File

@@ -8,7 +8,7 @@ import { blurOnBubbling } from '../../atoms/button/script';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import { toggleRoomSettings, openReusableContextMenu } from '../../../client/action/navigation'; import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
import { togglePeopleDrawer } from '../../../client/action/settings'; import { togglePeopleDrawer } from '../../../client/action/settings';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import { getEventCords } from '../../../util/common'; import { getEventCords } from '../../../util/common';
@@ -25,6 +25,7 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
import SearchIC from '../../../../public/res/ic/outlined/search.svg'; import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import UserIC from '../../../../public/res/ic/outlined/user.svg'; import UserIC from '../../../../public/res/ic/outlined/user.svg';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useForceUpdate } from '../../hooks/useForceUpdate';
@@ -73,6 +74,12 @@ function RoomViewHeader({ roomId }) {
return ( return (
<Header> <Header>
<IconButton
src={BackArrowIC}
className="room-header__back-btn"
tooltip="Return to navigation"
onClick={() => openNavigation()}
/>
<button <button
ref={roomHeaderBtnRef} ref={roomHeaderBtnRef}
className="room-header__btn" className="room-header__btn"
@@ -87,7 +94,8 @@ function RoomViewHeader({ roomId }) {
<RawIcon src={ChevronBottomIC} /> <RawIcon src={ChevronBottomIC} />
</button> </button>
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} /> <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />
<IconButton onClick={togglePeopleDrawer} tooltip="People" src={UserIC} /> <IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
<IconButton <IconButton
onClick={openRoomOptions} onClick={openRoomOptions}
tooltip="Options" tooltip="Options"

View File

@@ -1,5 +1,6 @@
@use '../../partials/flex'; @use '../../partials/flex';
@use '../../partials/dir'; @use '../../partials/dir';
@use '../../partials/screen';
.room-header__btn { .room-header__btn {
min-width: 0; min-width: 0;
@@ -25,3 +26,22 @@
outline: none; outline: none;
} }
} }
.room-header__drawer-btn {
@include screen.smallerThan(tabletBreakpoint) {
display: none;
}
}
.room-header__members-btn {
@include screen.biggerThan(tabletBreakpoint) {
display: none;
}
}
.room-header__back-btn {
@include dir.side(margin, 0, var(--sp-tight));
@include screen.biggerThan(mobileBreakpoint) {
display: none;
}
}

View File

@@ -7,6 +7,7 @@ import navigation from '../../../client/state/navigation';
import AsyncSearch from '../../../util/AsyncSearch'; import AsyncSearch from '../../../util/AsyncSearch';
import { selectRoom, selectTab } from '../../../client/action/navigation'; import { selectRoom, selectTab } from '../../../client/action/navigation';
import { joinRuleToIconSrc } from '../../../util/matrixUtil'; import { joinRuleToIconSrc } from '../../../util/matrixUtil';
import { roomIdByActivity } from '../../../util/sort';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon'; import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -47,27 +48,24 @@ function useVisiblityToggle(setResult) {
return [isOpen, requestClose]; return [isOpen, requestClose];
} }
function mapRoomIds(roomIds, type) { function mapRoomIds(roomIds) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const { directs, roomIdToParents } = initMatrix.roomList; const { directs, roomIdToParents } = initMatrix.roomList;
return roomIds.map((roomId) => { return roomIds.map((roomId) => {
let roomType = type;
if (!roomType) {
roomType = directs.has(roomId) ? 'direct' : 'room';
}
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
const parentSet = roomIdToParents.get(roomId); const parentSet = roomIdToParents.get(roomId);
const parentNames = parentSet const parentNames = parentSet ? [] : undefined;
? [...parentSet].map((parentId) => mx.getRoom(parentId).name) parentSet?.forEach((parentId) => parentNames.push(mx.getRoom(parentId).name));
: undefined;
const parents = parentNames ? parentNames.join(', ') : null; const parents = parentNames ? parentNames.join(', ') : null;
let type = 'room';
if (room.isSpaceRoom()) type = 'space';
else if (directs.has(roomId)) type = 'direct';
return ({ return ({
type: roomType, type,
name: room.name, name: room.name,
parents, parents,
roomId, roomId,
@@ -93,34 +91,32 @@ function Search() {
const generateResults = (term) => { const generateResults = (term) => {
const prefix = term.match(/^[#@*]/)?.[0]; const prefix = term.match(/^[#@*]/)?.[0];
if (term.length === 1) { 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 {
asyncSearch.search(prefix ? term.slice(1) : term); 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 loadRecentRooms = () => {
const { recentRooms } = navigation; const { recentRooms } = navigation;
handleSearchResults(mapRoomIds(recentRooms).reverse(), ''); handleSearchResults(mapRoomIds(recentRooms).reverse());
}; };
const handleAfterOpen = () => { const handleAfterOpen = () => {
@@ -154,6 +150,7 @@ function Search() {
else { else {
searchRef.current.value = ''; searchRef.current.value = '';
searchRef.current.focus(); searchRef.current.focus();
loadRecentRooms();
} }
}; };
@@ -202,7 +199,7 @@ function Search() {
return ( return (
<RawModal <RawModal
className="search-dialog__model dialog-model" className="search-dialog__modal dialog-modal"
isOpen={isOpen} isOpen={isOpen}
onAfterOpen={handleAfterOpen} onAfterOpen={handleAfterOpen}
onAfterClose={handleAfterClose} onAfterClose={handleAfterClose}

View File

@@ -1,6 +1,6 @@
@use '../../partials/dir'; @use '../../partials/dir';
.search-dialog__model { .search-dialog__modal {
--modal-height: 380px; --modal-height: 380px;
height: 100%; height: 100%;
background-color: var(--bg-surface); background-color: var(--bg-surface);

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;

View File

@@ -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;
}
}

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;

View File

@@ -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);
}
}

View File

@@ -0,0 +1,262 @@
import React, { useState, useEffect } from 'react';
import './DeviceManage.scss';
import dateFormat from 'dateformat';
import initMatrix from '../../../client/initMatrix';
import { isCrossVerified } from '../../../util/matrixUtil';
import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import Input from '../../atoms/input/Input';
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 { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useStore } from '../../hooks/useStore';
import { useDeviceList } from '../../hooks/useDeviceList';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
import { accessSecretStorage } from './SecretStorageAccess';
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
let isCompleted = false;
const renderContent = (onComplete) => {
const handleSubmit = (e) => {
e.preventDefault();
const name = e.target.session.value;
if (typeof name !== 'string') onComplete(null);
onComplete(name);
};
return (
<form className="device-manage__rename" onSubmit={handleSubmit}>
<Input value={deviceName} label="Session name" name="session" />
<div className="device-manage__rename-btn">
<Button variant="primary" type="submit">Save</Button>
<Button onClick={() => onComplete(null)}>Cancel</Button>
</div>
</form>
);
};
openReusableDialog(
<Text variant="s1" weight="medium">Edit session name</Text>,
(requestClose) => renderContent((name) => {
isCompleted = true;
resolve(name);
requestClose();
}),
() => {
if (!isCompleted) resolve(null);
},
);
});
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);
const isMeVerified = isCrossVerified(mx.deviceId);
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 = await promptDeviceName(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) => {
const isConfirmed = await confirmDialog(
`Logout ${device.display_name}`,
`You are about to logout "${device.display_name}" session.`,
'Logout',
'danger',
);
if (!isConfirmed) return;
addToProcessing(device);
await authRequest(`Logout "${device.display_name}"`, async (auth) => {
await mx.deleteDevice(device.device_id, auth);
});
if (!mountStore.getItem()) return;
removeFromProcessing(device);
};
const verifyWithKey = async (device) => {
const keyData = await accessSecretStorage('Session verification');
if (!keyData) return;
addToProcessing(device);
await mx.checkOwnCrossSigningTrust();
};
const verifyWithEmojis = async (deviceId) => {
const req = await mx.requestVerification(mx.getUserId(), [deviceId]);
openEmojiVerification(req, { userId: mx.getUserId(), deviceId });
};
const verify = (deviceId, isCurrentDevice) => {
if (isCurrentDevice) {
verifyWithKey(deviceId);
return;
}
verifyWithEmojis(deviceId);
};
const renderDevice = (device, isVerified) => {
const deviceId = device.device_id;
const displayName = device.display_name;
const lastIP = device.last_seen_ip;
const lastTS = device.last_seen_ts;
const isCurrentDevice = mx.deviceId === deviceId;
return (
<SettingTile
key={deviceId}
title={(
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
{displayName}
<Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
{isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
</Text>
)}
options={
processing.includes(deviceId)
? <Spinner size="small" />
: (
<>
{((isMeVerified && isVerified === false) || (isCurrentDevice && isVerified === false)) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">Verify</Button>}
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
</>
)
}
content={(
<>
<Text variant="b3">
Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}>
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
</span>
{lastIP ? ` at ${lastIP}` : ''}
</Text>
{isCurrentDevice && (
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
{`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
</Text>
)}
</>
)}
/>
);
};
const unverified = [];
const verified = [];
const noEncryption = [];
deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
const isVerified = isCrossVerified(device.device_id);
if (isVerified === true) {
verified.push(device);
} else if (isVerified === false) {
unverified.push(device);
} else {
noEncryption.push(device);
}
});
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 sessions</Text>
}
</div>
{noEncryption.length > 0 && (
<div>
<MenuHeader>Sessions without encryption support</MenuHeader>
{noEncryption.map((device) => renderDevice(device, null))}
</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 sessions</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;

Some files were not shown because too many files have changed in this diff Show More