URL navigation in interface and other improvements (#1633)
* load room on url change * add direct room list * render space room list * fix css syntax error * update scroll virtualizer * render subspaces room list * improve sidebar notification badge perf * add nav category components * add space recursive direct component * use nav category component in home, direct and space room list * add empty home and direct list layout * fix unread room menu ref * add more navigation items in room, direct and space tab * add more navigation * fix unread room menu to links * fix space lobby and search link * add explore navigation section * add notifications navigation menu * redirect to initial path after login * include unsupported room in rooms * move router hooks in hooks/router folder * add featured explore - WIP * load featured room with room summary * fix room card topic line clamp * add react query * load room summary using react query * add join button in room card * add content component * use content component in featured community content * fix content width * add responsive room card grid * fix async callback error status * add room card error button * fix client drawer shrink * add room topic viewer * open room card topic in viewer * fix room topic close btn * add get orphan parent util * add room card error dialog * add view featured room or space btn * refactor orphanParent to orphanParents * WIP - explore server * show space hint in room card * add room type filters * add per page item limit popout * reset scroll on public rooms load * refactor explore ui * refactor public rooms component * reset search on server change * fix typo * add empty featured section info * display user server on top * make server room card view btn clickable * add user server as default redirect for explore path * make home empty btn clickable * add thirdparty instance filter in server explore * remove since param on instance change * add server button in explore menu * rename notifications path to inbox * update react-virtual * Add notification messages inbox - WIP * add scroll top container component * add useInterval hook * add visibility change callback prop to scroll top container component * auto refresh notifications every 10 seconds * make message related component reusable * refactor matrix event renderer hoook * render notification message content * refactor matrix event renderer hook * update sequence card styles * move room navigate hook in global hooks * add open message button in notifications * add mark room as read button in notification group * show error in notification messages * add more featured spaces * render reply in notification messages * make notification message reply clickable * add outline prop for attachments * make old settings dialog viewable * add open featured communities as default config option * add invite count notification badge in sidebar and inbox menu * add element size observer hook * improve element size observer hook props * improve screen size hook * fix room avatar util function * allow Text props in Time component * fix dm room util function * add invitations * add no invites and notification cards * fix inbox tab unread badge visible without invite count * update folds and change inbox icon * memo search param construction * add message search in home * fix default message search order * fix display edited message new content * highlight search text in search messages * fix message search loading * disable log in production * add use space context * add useRoom context * fix space room list * fix inbox tab active state * add hook to get space child room recursive * add search for space * add virtual tile component * virtualize home and directs room list * update nav category component * use virtual tile component in more places * fix message highlight when click on reply twice * virtualize space room list * fix space room list lag issue * update folds * add room nav item component in space room list * use room nav item in home and direct room list * make space categories closable and save it in local storage * show unread room when category is collapsed * make home and direct room list category closable * rename room nav item show avatar prop * fix explore server category text alignment * rename closedRoomCategories to closedNavCategories * add nav category handler hook * save and restore last navigation path on space select * filter space rooms category by activity when it is closed * save and restore home and direct nav path state * save and restore inbox active path on open * save and restore explore tab active path * remove notification badge unread menu * add join room or space before navigate screen * move room component to features folder and add new room header * update folds * add room header menu * fix home room list activity sorting * do not hide selected room item on category closed in home and direct tab * replace old select room/tab call with navigate hook * improve state event hooks * show room card summary for joined rooms * prevent room from opening in wrong tab * only show message sender id on hover in modern layout * revert state event hooks changes * add key prop to room provider components * add welcome page * prevent excessive redirects * fix sidebar style with no spaces * move room settings in popup window * remove invite option from room settings * fix open room list search * add leave room prompt * standardize room and user avatar * fix avatar text size * add new reply layout * rename space hierarchy hook * add room topic hook * add room name hook * add room avatar hook and add direct room avatar util * space lobby - WIP * hide invalid space child event from space hierarchy in lobby * move lobby to features * fix element size observer hook width and height * add lobby header and hero section * add hierarchy room item error and loading state * add first and last child prop in sequence card * redirect to lobby from index path * memo and retry hierarchy room summary error * fix hierarchy room item styles * rename lobby hierarchy item card to room item card * show direct room avatar in space lobby * add hierarchy space item * add space item unknown room join button * fix space hierarchy hook refresh after new space join * change user avatar color and fallback render to user icon * change room avatar fallback to room icon * rename room/user avatar renderInitial prop to renderFallback * add room join and view button in space lobby * make power level api more reusable * fix space hierarchy not updating on child update * add menu to suggest or remove space children * show reply arrow in place of reply bend in message * fix typeerror in search because of wrong js-sdk t.ds * do not refetch hierarchy room summary on window focus * make room/user avatar un-draggable * change welcome page support button copy * drag-and-drop ordering of lobby spaces/rooms - WIP * add ASCIILexicalTable algorithms * fix wrong power level check in lobby items options * fix lobby can drop checks * fix join button error crash * fix reply spacing * fix m direct updated with other account data * add option to open room/space settings from lobby * add option in lobby to add new or existing room/spaces * fix room nav item selected styles * add space children reorder mechanism * fix space child reorder bug * fix hierarchy item sort function * Apply reorder of lobby into room list * add and improve space lobby menu items * add existing spaces menu in lobby * change restricted room allow params when dragging outside space * move featured servers config from homeserver list * removed unused features from space settings * add canonical alias as name fallback in lobby item * fix unreliable unread count update bug * fix after login redirect * fix room card topic hover style * Add dnd and folders in sidebar spaces * fix orphan space not visible in sidebar * fix sso login has mix of icon and button * fix space children not visible in home upon leaving space * recalculate notification on updating any space child * fix user color saturation/lightness * add user color to user avatar * add background colors to room avatar * show 2 length initial in sidebar space avatar * improve link color * add nav button component * open legacy create room and create direct * improve page route structure * handle hash router in path utils * mobile friendly router and navigation * make room header member drawer icon mobile friendly * setup index redirect for inbox and explore server route * add leave space prompt * improve member drawer filter menu * add space context menu * add context menu in home * add leave button in lobby items * render user tab avatar on sidebar * force overwrite netlify - test * netlify test * fix reset-password path without server redirected to login * add message link copy button in message menu * reset unread on sync prepared * fix stuck typing notifications * show typing indication in room nav item * refactor closedNavCategories atom to use userId in store key * refactor closedLobbyCategoriesAtom to include userId in store key * refactor navToActivePathAtom to use userId in storage key * remove unused file * refactor openedSidebarFolderAtom to include userId in storage key * add context menu for sidebar space tab * fix eslint not working * add option to pin/unpin child spaces * add context menu for directs tab * add context menu for direct and home tab * show lock icon for non-public space in header * increase matrix max listener count * wrap lobby add space room in callback hook
This commit is contained in:
parent
2b7d825694
commit
4c76a7fd18
290 changed files with 17436 additions and 3213 deletions
64
.eslintrc.cjs
Normal file
64
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
'airbnb',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
JSX: "readonly"
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'react',
|
||||||
|
'@typescript-eslint'
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'linebreak-style': 0,
|
||||||
|
'no-underscore-dangle': 0,
|
||||||
|
"no-shadow": "off",
|
||||||
|
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"import/extensions": "off",
|
||||||
|
"import/no-unresolved": "off",
|
||||||
|
"import/no-extraneous-dependencies": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
devDependencies: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'react/no-unstable-nested-components': [
|
||||||
|
'error',
|
||||||
|
{ allowAsProps: true },
|
||||||
|
],
|
||||||
|
"react/jsx-filename-extension": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
extensions: [".tsx", ".jsx"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
"react/require-default-props": "off",
|
||||||
|
"react/jsx-props-no-spreading": "off",
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "error",
|
||||||
|
|
||||||
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
|
"@typescript-eslint/no-shadow": "error"
|
||||||
|
},
|
||||||
|
};
|
||||||
20
config.json
20
config.json
|
|
@ -10,6 +10,26 @@
|
||||||
],
|
],
|
||||||
"allowCustomHomeservers": true,
|
"allowCustomHomeservers": true,
|
||||||
|
|
||||||
|
"featuredCommunities": {
|
||||||
|
"openAsDefault": false,
|
||||||
|
"spaces": [
|
||||||
|
"#cinny-space:matrix.org",
|
||||||
|
"#community:matrix.org",
|
||||||
|
"#space:envs.net",
|
||||||
|
"#science-space:matrix.org",
|
||||||
|
"#libregaming-games:tchncs.de",
|
||||||
|
"#mathematics-on:matrix.org"
|
||||||
|
],
|
||||||
|
"rooms": [
|
||||||
|
"#cinny:matrix.org",
|
||||||
|
"#foundation-office:matrix.org",
|
||||||
|
"#thisweekinmatrix:matrix.org",
|
||||||
|
"#matrix-dev:matrix.org",
|
||||||
|
"#matrix:matrix.org"
|
||||||
|
],
|
||||||
|
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
||||||
|
},
|
||||||
|
|
||||||
"hashRouter": {
|
"hashRouter": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"basename": "/"
|
"basename": "/"
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@
|
||||||
status = 200
|
status = 200
|
||||||
|
|
||||||
[[redirects]]
|
[[redirects]]
|
||||||
from = "/olm.wasm"
|
from = "*/olm.wasm"
|
||||||
to = "/olm.wasm"
|
to = "/olm.wasm"
|
||||||
status = 200
|
status = 200
|
||||||
|
force = true
|
||||||
|
|
||||||
[[redirects]]
|
[[redirects]]
|
||||||
from = "/pdf.worker.min.js"
|
from = "/pdf.worker.min.js"
|
||||||
|
|
@ -32,3 +33,4 @@
|
||||||
from = "/*"
|
from = "/*"
|
||||||
to = "/index.html"
|
to = "/index.html"
|
||||||
status = 200
|
status = 200
|
||||||
|
force = true
|
||||||
121
package-lock.json
generated
121
package-lock.json
generated
|
|
@ -9,10 +9,15 @@
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
||||||
"@fontsource/inter": "4.5.14",
|
"@fontsource/inter": "4.5.14",
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
"@khanacademy/simple-markdown": "0.8.6",
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
"@tanstack/react-query": "5.24.1",
|
||||||
|
"@tanstack/react-query-devtools": "5.24.1",
|
||||||
|
"@tanstack/react-virtual": "3.2.0",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@vanilla-extract/css": "1.9.3",
|
"@vanilla-extract/css": "1.9.3",
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
|
|
@ -29,7 +34,7 @@
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"flux": "4.0.3",
|
"flux": "4.0.3",
|
||||||
"focus-trap-react": "10.0.2",
|
"focus-trap-react": "10.0.2",
|
||||||
"folds": "1.5.1",
|
"folds": "2.0.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-dom-parser": "4.0.0",
|
"html-dom-parser": "4.0.0",
|
||||||
"html-react-parser": "4.2.0",
|
"html-react-parser": "4.2.0",
|
||||||
|
|
@ -111,6 +116,34 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@atlaskit/pragmatic-drag-and-drop": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-+jGspaRMyHWB6g9w+N1KImS5I+xt0ML89pwUyCueEhf2KGsl6zyH9ZxjTVKfrbY89FyZvuuXT9oFRHTUKGBi/w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.0.0",
|
||||||
|
"bind-event-listener": "^3.0.0",
|
||||||
|
"raf-schd": "^4.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@atlaskit/pragmatic-drag-and-drop-auto-scroll": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-8wjKAI5qSrLojt8ZJ2WhoS5P75oBu5g0yMpAnTDgfqFyQnkt5Uc1txCRWpG26SS1mv19nm8ak9XHF2DOugVfpw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "^1.1.0",
|
||||||
|
"@babel/runtime": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@atlaskit/pragmatic-drag-and-drop-hitbox": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop-hitbox/-/pragmatic-drag-and-drop-hitbox-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-/Sbu/HqN2VGLYBhnsG7SbRNg98XKkbF6L7XDdBi+izRybfaK1FeMfodPpm/xnBHPJzwYMdkE0qtLyv6afhgMUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "^1.1.0",
|
||||||
|
"@babel/runtime": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.23.4",
|
"version": "7.23.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
|
||||||
|
|
@ -3093,25 +3126,75 @@
|
||||||
"@swc/counter": "^0.1.3"
|
"@swc/counter": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-virtual": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "3.0.0-beta.54",
|
"version": "5.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.54.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.24.1.tgz",
|
||||||
"integrity": "sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==",
|
"integrity": "sha512-DZ6Nx9p7BhjkG50ayJ+MKPgff+lMeol7QYXkvuU5jr2ryW/4ok5eanaS9W5eooA4xN0A/GPHdLGOZGzArgf5Cg==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-devtools": {
|
||||||
|
"version": "5.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.24.0.tgz",
|
||||||
|
"integrity": "sha512-pThim455t69zrZaQKa7IRkEIK8UBTS+gHVAdNfhO72Xh4rWpMc63ovRje5/n6iw63+d6QiJzVadsJVdPoodSeQ==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.24.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.24.1.tgz",
|
||||||
|
"integrity": "sha512-4+09JEdO4d6+Gc8Y/g2M/MuxDK5IY0QV8+2wL2304wPKJgJ54cBbULd3nciJ5uvh/as8rrxx6s0mtIwpRuGd1g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/virtual-core": "3.0.0-beta.54"
|
"@tanstack/query-core": "5.24.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query-devtools": {
|
||||||
|
"version": "5.24.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.24.1.tgz",
|
||||||
|
"integrity": "sha512-qa4SEugN+EF8JJXcpsM9Lu05HfUv5cvHvLuB0uw/81eJZyNHFdtHFBi5RLCgpBrOyVMDfH8UQ3VBMqXzFKV68A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-devtools": "5.24.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tanstack/react-query": "^5.24.1",
|
||||||
|
"react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/virtual-core": {
|
"node_modules/@tanstack/virtual-core": {
|
||||||
"version": "3.0.0-beta.54",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.54.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz",
|
||||||
"integrity": "sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==",
|
"integrity": "sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
|
@ -4065,6 +4148,11 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bind-event-listener": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q=="
|
||||||
|
},
|
||||||
"node_modules/blurhash": {
|
"node_modules/blurhash": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.4.tgz",
|
||||||
|
|
@ -5665,9 +5753,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/folds": {
|
"node_modules/folds": {
|
||||||
"version": "1.5.1",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/folds/-/folds-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/folds/-/folds-2.0.0.tgz",
|
||||||
"integrity": "sha512-2QxyA+FRKjPKXDTMDoD7NmOUiReWrKYO0Msg44QqlzTkTrRVEzJgyPIfC/Ia4/u0ByQpk6dbq8UQxomKmneJ/g==",
|
"integrity": "sha512-lKv31vij4GEpEzGKWk5c3ar78fMZ9Di5n1XFR14Z2wnnpqhiiM5JTIzr127Gk5dOfy4mJkjnv/ZfMZvM2k+OQg==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@vanilla-extract/css": "^1.9.2",
|
"@vanilla-extract/css": "^1.9.2",
|
||||||
"@vanilla-extract/recipes": "^0.3.0",
|
"@vanilla-extract/recipes": "^0.3.0",
|
||||||
|
|
@ -7580,6 +7668,11 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,15 @@
|
||||||
"author": "Ajay Bura",
|
"author": "Ajay Bura",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
||||||
"@fontsource/inter": "4.5.14",
|
"@fontsource/inter": "4.5.14",
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
"@khanacademy/simple-markdown": "0.8.6",
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
"@tanstack/react-query": "5.24.1",
|
||||||
|
"@tanstack/react-query-devtools": "5.24.1",
|
||||||
|
"@tanstack/react-virtual": "3.2.0",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"@vanilla-extract/css": "1.9.3",
|
"@vanilla-extract/css": "1.9.3",
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
|
|
@ -40,7 +45,7 @@
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"flux": "4.0.3",
|
"flux": "4.0.3",
|
||||||
"focus-trap-react": "10.0.2",
|
"focus-trap-react": "10.0.2",
|
||||||
"folds": "1.5.1",
|
"folds": "2.0.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-dom-parser": "4.0.0",
|
"html-dom-parser": "4.0.0",
|
||||||
"html-react-parser": "4.2.0",
|
"html-react-parser": "4.2.0",
|
||||||
|
|
|
||||||
36
src/app/components/CapabilitiesAndMediaConfigLoader.tsx
Normal file
36
src/app/components/CapabilitiesAndMediaConfigLoader.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ReactNode, useCallback, useEffect } from 'react';
|
||||||
|
import { Capabilities } from 'matrix-js-sdk';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||||
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
|
import { MediaConfig } from '../hooks/useMediaConfig';
|
||||||
|
import { promiseFulfilledResult } from '../utils/common';
|
||||||
|
|
||||||
|
type CapabilitiesAndMediaConfigLoaderProps = {
|
||||||
|
children: (capabilities?: Capabilities, mediaConfig?: MediaConfig) => ReactNode;
|
||||||
|
};
|
||||||
|
export function CapabilitiesAndMediaConfigLoader({
|
||||||
|
children,
|
||||||
|
}: CapabilitiesAndMediaConfigLoaderProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const [state, load] = useAsyncCallback<
|
||||||
|
[Capabilities | undefined, MediaConfig | undefined],
|
||||||
|
unknown,
|
||||||
|
[]
|
||||||
|
>(
|
||||||
|
useCallback(async () => {
|
||||||
|
const result = await Promise.allSettled([mx.getCapabilities(true), mx.getMediaConfig()]);
|
||||||
|
const capabilities = promiseFulfilledResult(result[0]);
|
||||||
|
const mediaConfig = promiseFulfilledResult(result[1]);
|
||||||
|
return [capabilities, mediaConfig];
|
||||||
|
}, [mx])
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const [capabilities, mediaConfig] =
|
||||||
|
state.status === AsyncStatus.Success ? state.data : [undefined, undefined];
|
||||||
|
return children(capabilities, mediaConfig);
|
||||||
|
}
|
||||||
19
src/app/components/CapabilitiesLoader.tsx
Normal file
19
src/app/components/CapabilitiesLoader.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ReactNode, useCallback, useEffect } from 'react';
|
||||||
|
import { Capabilities } from 'matrix-js-sdk';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||||
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
|
|
||||||
|
type CapabilitiesLoaderProps = {
|
||||||
|
children: (capabilities: Capabilities | undefined) => ReactNode;
|
||||||
|
};
|
||||||
|
export function CapabilitiesLoader({ children }: CapabilitiesLoaderProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(true), [mx]));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
||||||
|
}
|
||||||
19
src/app/components/MediaConfigLoader.tsx
Normal file
19
src/app/components/MediaConfigLoader.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ReactNode, useCallback, useEffect } from 'react';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||||
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
|
import { MediaConfig } from '../hooks/useMediaConfig';
|
||||||
|
|
||||||
|
type MediaConfigLoaderProps = {
|
||||||
|
children: (mediaConfig: MediaConfig | undefined) => ReactNode;
|
||||||
|
};
|
||||||
|
export function MediaConfigLoader({ children }: MediaConfigLoaderProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const [state, load] = useAsyncCallback(useCallback(() => mx.getMediaConfig(), [mx]));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||||
import React, { FormEventHandler, useEffect, useRef, useState } from 'react';
|
import React, { FormEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Input,
|
Input,
|
||||||
Menu,
|
Menu,
|
||||||
PopOut,
|
PopOut,
|
||||||
|
RectCords,
|
||||||
Scroll,
|
Scroll,
|
||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -48,7 +49,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||||
const isError =
|
const isError =
|
||||||
pdfJSState.status === AsyncStatus.Error || docState.status === AsyncStatus.Error;
|
pdfJSState.status === AsyncStatus.Error || docState.status === AsyncStatus.Error;
|
||||||
const [pageNo, setPageNo] = useState(1);
|
const [pageNo, setPageNo] = useState(1);
|
||||||
const [openJump, setOpenJump] = useState(false);
|
const [jumpAnchor, setJumpAnchor] = useState<RectCords>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPdfJS();
|
loadPdfJS();
|
||||||
|
|
@ -86,7 +87,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||||
if (!jumpInput) return;
|
if (!jumpInput) return;
|
||||||
const jumpTo = parseInt(jumpInput.value, 10);
|
const jumpTo = parseInt(jumpInput.value, 10);
|
||||||
setPageNo(Math.max(1, Math.min(docState.data.numPages, jumpTo)));
|
setPageNo(Math.max(1, Math.min(docState.data.numPages, jumpTo)));
|
||||||
setOpenJump(false);
|
setJumpAnchor(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevPage = () => {
|
const handlePrevPage = () => {
|
||||||
|
|
@ -98,6 +99,10 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||||
setPageNo((n) => Math.min(n + 1, docState.data.numPages));
|
setPageNo((n) => Math.min(n + 1, docState.data.numPages));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenJump: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
setJumpAnchor(evt.currentTarget.getBoundingClientRect());
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
|
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
|
||||||
<Header className={css.PdfViewerHeader} size="400">
|
<Header className={css.PdfViewerHeader} size="400">
|
||||||
|
|
@ -187,14 +192,14 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||||
</Chip>
|
</Chip>
|
||||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||||
<PopOut
|
<PopOut
|
||||||
open={openJump}
|
anchor={jumpAnchor}
|
||||||
align="Center"
|
align="Center"
|
||||||
position="Top"
|
position="Top"
|
||||||
content={
|
content={
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setOpenJump(false),
|
onDeactivate: () => setJumpAnchor(undefined),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -227,17 +232,14 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(anchorRef) => (
|
|
||||||
<Chip
|
<Chip
|
||||||
onClick={() => setOpenJump(!openJump)}
|
onClick={handleOpenJump}
|
||||||
ref={anchorRef}
|
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-pressed={openJump}
|
aria-pressed={jumpAnchor !== undefined}
|
||||||
>
|
>
|
||||||
<Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text>
|
<Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text>
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
|
||||||
</PopOut>
|
</PopOut>
|
||||||
</Box>
|
</Box>
|
||||||
<Chip
|
<Chip
|
||||||
|
|
|
||||||
245
src/app/components/RenderMessageContent.tsx
Normal file
245
src/app/components/RenderMessageContent.tsx
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { MsgType } from 'matrix-js-sdk';
|
||||||
|
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||||
|
import {
|
||||||
|
AudioContent,
|
||||||
|
DownloadFile,
|
||||||
|
FileContent,
|
||||||
|
ImageContent,
|
||||||
|
MAudio,
|
||||||
|
MBadEncrypted,
|
||||||
|
MEmote,
|
||||||
|
MFile,
|
||||||
|
MImage,
|
||||||
|
MLocation,
|
||||||
|
MNotice,
|
||||||
|
MText,
|
||||||
|
MVideo,
|
||||||
|
ReadPdfFile,
|
||||||
|
ReadTextFile,
|
||||||
|
RenderBody,
|
||||||
|
ThumbnailContent,
|
||||||
|
UnsupportedContent,
|
||||||
|
VideoContent,
|
||||||
|
} from './message';
|
||||||
|
import { UrlPreviewCard, UrlPreviewHolder } from './url-preview';
|
||||||
|
import { Image, MediaControl, Video } from './media';
|
||||||
|
import { ImageViewer } from './image-viewer';
|
||||||
|
import { PdfViewer } from './Pdf-viewer';
|
||||||
|
import { TextViewer } from './text-viewer';
|
||||||
|
|
||||||
|
type RenderMessageContentProps = {
|
||||||
|
displayName: string;
|
||||||
|
msgType: string;
|
||||||
|
ts: number;
|
||||||
|
edited?: boolean;
|
||||||
|
getContent: <T>() => T;
|
||||||
|
mediaAutoLoad?: boolean;
|
||||||
|
urlPreview?: boolean;
|
||||||
|
highlightRegex?: RegExp;
|
||||||
|
htmlReactParserOptions: HTMLReactParserOptions;
|
||||||
|
outlineAttachment?: boolean;
|
||||||
|
};
|
||||||
|
export function RenderMessageContent({
|
||||||
|
displayName,
|
||||||
|
msgType,
|
||||||
|
ts,
|
||||||
|
edited,
|
||||||
|
getContent,
|
||||||
|
mediaAutoLoad,
|
||||||
|
urlPreview,
|
||||||
|
highlightRegex,
|
||||||
|
htmlReactParserOptions,
|
||||||
|
outlineAttachment,
|
||||||
|
}: RenderMessageContentProps) {
|
||||||
|
const renderFile = () => (
|
||||||
|
<MFile
|
||||||
|
content={getContent()}
|
||||||
|
renderFileContent={({ body, mimeType, info, encInfo, url }) => (
|
||||||
|
<FileContent
|
||||||
|
body={body}
|
||||||
|
mimeType={mimeType}
|
||||||
|
renderAsPdfFile={() => (
|
||||||
|
<ReadPdfFile
|
||||||
|
body={body}
|
||||||
|
mimeType={mimeType}
|
||||||
|
url={url}
|
||||||
|
encInfo={encInfo}
|
||||||
|
renderViewer={(p) => <PdfViewer {...p} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderAsTextFile={() => (
|
||||||
|
<ReadTextFile
|
||||||
|
body={body}
|
||||||
|
mimeType={mimeType}
|
||||||
|
url={url}
|
||||||
|
encInfo={encInfo}
|
||||||
|
renderViewer={(p) => <TextViewer {...p} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DownloadFile body={body} mimeType={mimeType} url={url} encInfo={encInfo} info={info} />
|
||||||
|
</FileContent>
|
||||||
|
)}
|
||||||
|
outlined={outlineAttachment}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (msgType === MsgType.Text) {
|
||||||
|
return (
|
||||||
|
<MText
|
||||||
|
edited={edited}
|
||||||
|
content={getContent()}
|
||||||
|
renderBody={(props) => (
|
||||||
|
<RenderBody
|
||||||
|
{...props}
|
||||||
|
highlightRegex={highlightRegex}
|
||||||
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderUrlsPreview={
|
||||||
|
urlPreview
|
||||||
|
? (urls) => (
|
||||||
|
<UrlPreviewHolder>
|
||||||
|
{urls.map((url) => (
|
||||||
|
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||||
|
))}
|
||||||
|
</UrlPreviewHolder>
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === MsgType.Emote) {
|
||||||
|
return (
|
||||||
|
<MEmote
|
||||||
|
displayName={displayName}
|
||||||
|
edited={edited}
|
||||||
|
content={getContent()}
|
||||||
|
renderBody={(props) => (
|
||||||
|
<RenderBody
|
||||||
|
{...props}
|
||||||
|
highlightRegex={highlightRegex}
|
||||||
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderUrlsPreview={
|
||||||
|
urlPreview
|
||||||
|
? (urls) => (
|
||||||
|
<UrlPreviewHolder>
|
||||||
|
{urls.map((url) => (
|
||||||
|
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||||
|
))}
|
||||||
|
</UrlPreviewHolder>
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === MsgType.Notice) {
|
||||||
|
return (
|
||||||
|
<MNotice
|
||||||
|
edited={edited}
|
||||||
|
content={getContent()}
|
||||||
|
renderBody={(props) => (
|
||||||
|
<RenderBody
|
||||||
|
{...props}
|
||||||
|
highlightRegex={highlightRegex}
|
||||||
|
htmlReactParserOptions={htmlReactParserOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderUrlsPreview={
|
||||||
|
urlPreview
|
||||||
|
? (urls) => (
|
||||||
|
<UrlPreviewHolder>
|
||||||
|
{urls.map((url) => (
|
||||||
|
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||||
|
))}
|
||||||
|
</UrlPreviewHolder>
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === MsgType.Image) {
|
||||||
|
return (
|
||||||
|
<MImage
|
||||||
|
content={getContent()}
|
||||||
|
renderImageContent={(props) => (
|
||||||
|
<ImageContent
|
||||||
|
{...props}
|
||||||
|
autoPlay={mediaAutoLoad}
|
||||||
|
renderImage={(p) => <Image {...p} loading="lazy" />}
|
||||||
|
renderViewer={(p) => <ImageViewer {...p} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
outlined={outlineAttachment}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === MsgType.Video) {
|
||||||
|
return (
|
||||||
|
<MVideo
|
||||||
|
content={getContent()}
|
||||||
|
renderAsFile={renderFile}
|
||||||
|
renderVideoContent={({ body, info, mimeType, url, encInfo }) => (
|
||||||
|
<VideoContent
|
||||||
|
body={body}
|
||||||
|
info={info}
|
||||||
|
mimeType={mimeType}
|
||||||
|
url={url}
|
||||||
|
encInfo={encInfo}
|
||||||
|
renderThumbnail={
|
||||||
|
mediaAutoLoad
|
||||||
|
? () => (
|
||||||
|
<ThumbnailContent
|
||||||
|
info={info}
|
||||||
|
renderImage={(src) => (
|
||||||
|
<Image alt={body} title={body} src={src} loading="lazy" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
renderVideo={(p) => <Video {...p} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
outlined={outlineAttachment}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === MsgType.Audio) {
|
||||||
|
return (
|
||||||
|
<MAudio
|
||||||
|
content={getContent()}
|
||||||
|
renderAsFile={renderFile}
|
||||||
|
renderAudioContent={(props) => (
|
||||||
|
<AudioContent {...props} renderMediaControl={(p) => <MediaControl {...p} />} />
|
||||||
|
)}
|
||||||
|
outlined={outlineAttachment}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === MsgType.File) {
|
||||||
|
return renderFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === MsgType.Location) {
|
||||||
|
return <MLocation content={getContent()} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === 'm.bad.encrypted') {
|
||||||
|
return <MBadEncrypted />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <UnsupportedContent />;
|
||||||
|
}
|
||||||
90
src/app/components/RoomSummaryLoader.tsx
Normal file
90
src/app/components/RoomSummaryLoader.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { ReactNode, useCallback, useState } from 'react';
|
||||||
|
import { MatrixClient, Room } from 'matrix-js-sdk';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
|
||||||
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
|
import { LocalRoomSummary, useLocalRoomSummary } from '../hooks/useLocalRoomSummary';
|
||||||
|
import { AsyncState, AsyncStatus } from '../hooks/useAsyncCallback';
|
||||||
|
|
||||||
|
export type IRoomSummary = Awaited<ReturnType<MatrixClient['getRoomSummary']>>;
|
||||||
|
|
||||||
|
type RoomSummaryLoaderProps = {
|
||||||
|
roomIdOrAlias: string;
|
||||||
|
children: (roomSummary?: IRoomSummary) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RoomSummaryLoader({ roomIdOrAlias, children }: RoomSummaryLoaderProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const fetchSummary = useCallback(() => mx.getRoomSummary(roomIdOrAlias), [mx, roomIdOrAlias]);
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: [roomIdOrAlias, `summary`],
|
||||||
|
queryFn: fetchSummary,
|
||||||
|
});
|
||||||
|
|
||||||
|
return children(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalRoomSummaryLoader({
|
||||||
|
room,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
room: Room;
|
||||||
|
children: (roomSummary: LocalRoomSummary) => ReactNode;
|
||||||
|
}) {
|
||||||
|
const summary = useLocalRoomSummary(room);
|
||||||
|
|
||||||
|
return children(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HierarchyRoomSummaryLoader({
|
||||||
|
roomId,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
roomId: string;
|
||||||
|
children: (state: AsyncState<IHierarchyRoom, Error>) => ReactNode;
|
||||||
|
}) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const fetchSummary = useCallback(() => mx.getRoomHierarchy(roomId, 1, 1), [mx, roomId]);
|
||||||
|
const [errorMemo, setError] = useState<Error>();
|
||||||
|
|
||||||
|
const { data, error } = useQuery({
|
||||||
|
queryKey: [roomId, `hierarchy`],
|
||||||
|
queryFn: fetchSummary,
|
||||||
|
retryOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: (failureCount, err) => {
|
||||||
|
setError(err);
|
||||||
|
if (failureCount > 3) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let state: AsyncState<IHierarchyRoom, Error> = {
|
||||||
|
status: AsyncStatus.Loading,
|
||||||
|
};
|
||||||
|
if (error) {
|
||||||
|
state = {
|
||||||
|
status: AsyncStatus.Error,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (errorMemo) {
|
||||||
|
state = {
|
||||||
|
status: AsyncStatus.Error,
|
||||||
|
error: errorMemo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = data?.rooms[0] ?? undefined;
|
||||||
|
if (summary) {
|
||||||
|
state = {
|
||||||
|
status: AsyncStatus.Success,
|
||||||
|
data: summary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return children(state);
|
||||||
|
}
|
||||||
24
src/app/components/RoomUnreadProvider.tsx
Normal file
24
src/app/components/RoomUnreadProvider.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
import { Unread } from '../../types/matrix/room';
|
||||||
|
import { useRoomUnread, useRoomsUnread } from '../state/hooks/unread';
|
||||||
|
import { roomToUnreadAtom } from '../state/room/roomToUnread';
|
||||||
|
|
||||||
|
type RoomUnreadProviderProps = {
|
||||||
|
roomId: string;
|
||||||
|
children: (unread?: Unread) => ReactElement;
|
||||||
|
};
|
||||||
|
export function RoomUnreadProvider({ roomId, children }: RoomUnreadProviderProps) {
|
||||||
|
const unread = useRoomUnread(roomId, roomToUnreadAtom);
|
||||||
|
|
||||||
|
return children(unread);
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomsUnreadProviderProps = {
|
||||||
|
rooms: string[];
|
||||||
|
children: (unread?: Unread) => ReactElement;
|
||||||
|
};
|
||||||
|
export function RoomsUnreadProvider({ rooms, children }: RoomsUnreadProviderProps) {
|
||||||
|
const unread = useRoomsUnread(rooms, roomToUnreadAtom);
|
||||||
|
|
||||||
|
return children(unread);
|
||||||
|
}
|
||||||
28
src/app/components/SpaceChildDirectsProvider.tsx
Normal file
28
src/app/components/SpaceChildDirectsProvider.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { RoomToParents } from '../../types/matrix/room';
|
||||||
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
|
import { allRoomsAtom } from '../state/room-list/roomList';
|
||||||
|
import { useChildDirectScopeFactory, useSpaceChildren } from '../state/hooks/roomList';
|
||||||
|
|
||||||
|
type SpaceChildDirectsProviderProps = {
|
||||||
|
spaceId: string;
|
||||||
|
mDirects: Set<string>;
|
||||||
|
roomToParents: RoomToParents;
|
||||||
|
children: (rooms: string[]) => ReactNode;
|
||||||
|
};
|
||||||
|
export function SpaceChildDirectsProvider({
|
||||||
|
spaceId,
|
||||||
|
roomToParents,
|
||||||
|
mDirects,
|
||||||
|
children,
|
||||||
|
}: SpaceChildDirectsProviderProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const childDirects = useSpaceChildren(
|
||||||
|
allRoomsAtom,
|
||||||
|
spaceId,
|
||||||
|
useChildDirectScopeFactory(mx, mDirects, roomToParents)
|
||||||
|
);
|
||||||
|
|
||||||
|
return children(childDirects);
|
||||||
|
}
|
||||||
28
src/app/components/SpaceChildRoomsProvider.tsx
Normal file
28
src/app/components/SpaceChildRoomsProvider.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { RoomToParents } from '../../types/matrix/room';
|
||||||
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
|
import { allRoomsAtom } from '../state/room-list/roomList';
|
||||||
|
import { useChildRoomScopeFactory, useSpaceChildren } from '../state/hooks/roomList';
|
||||||
|
|
||||||
|
type SpaceChildRoomsProviderProps = {
|
||||||
|
spaceId: string;
|
||||||
|
mDirects: Set<string>;
|
||||||
|
roomToParents: RoomToParents;
|
||||||
|
children: (rooms: string[]) => ReactNode;
|
||||||
|
};
|
||||||
|
export function SpaceChildRoomsProvider({
|
||||||
|
spaceId,
|
||||||
|
roomToParents,
|
||||||
|
mDirects,
|
||||||
|
children,
|
||||||
|
}: SpaceChildRoomsProviderProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const childRooms = useSpaceChildren(
|
||||||
|
allRoomsAtom,
|
||||||
|
spaceId,
|
||||||
|
useChildRoomScopeFactory(mx, mDirects, roomToParents)
|
||||||
|
);
|
||||||
|
|
||||||
|
return children(childRooms);
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,25 @@
|
||||||
import { ReactNode, useCallback, useEffect } from 'react';
|
import { ReactNode, useCallback, useEffect, useState } from 'react';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||||
import { SpecVersions, specVersions } from '../cs-api';
|
import { SpecVersions, specVersions } from '../cs-api';
|
||||||
import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
|
|
||||||
|
|
||||||
type SpecVersionsLoaderProps = {
|
type SpecVersionsLoaderProps = {
|
||||||
|
baseUrl: string;
|
||||||
fallback?: () => ReactNode;
|
fallback?: () => ReactNode;
|
||||||
error?: (err: unknown) => ReactNode;
|
error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode;
|
||||||
children: (versions: SpecVersions) => ReactNode;
|
children: (versions: SpecVersions) => ReactNode;
|
||||||
};
|
};
|
||||||
export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLoaderProps) {
|
export function SpecVersionsLoader({
|
||||||
const autoDiscoveryInfo = useAutoDiscoveryInfo();
|
baseUrl,
|
||||||
const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;
|
fallback,
|
||||||
|
error,
|
||||||
|
children,
|
||||||
|
}: SpecVersionsLoaderProps) {
|
||||||
const [state, load] = useAsyncCallback(
|
const [state, load] = useAsyncCallback(
|
||||||
useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
|
useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
|
||||||
);
|
);
|
||||||
|
const [ignoreError, setIgnoreError] = useState(false);
|
||||||
|
|
||||||
|
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
|
|
@ -24,9 +29,15 @@ export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLo
|
||||||
return fallback?.();
|
return fallback?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status === AsyncStatus.Error) {
|
if (!ignoreError && state.status === AsyncStatus.Error) {
|
||||||
return error?.(state.error);
|
return error?.(state.error, load, ignoreCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
return children(state.data);
|
return children(
|
||||||
|
state.status === AsyncStatus.Success
|
||||||
|
? state.data
|
||||||
|
: {
|
||||||
|
versions: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ import {
|
||||||
Line,
|
Line,
|
||||||
Menu,
|
Menu,
|
||||||
PopOut,
|
PopOut,
|
||||||
|
RectCords,
|
||||||
Scroll,
|
Scroll,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
toRem,
|
toRem,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import React, { ReactNode, useState } from 'react';
|
import React, { MouseEventHandler, ReactNode, useState } from 'react';
|
||||||
import { ReactEditor, useSlate } from 'slate-react';
|
import { ReactEditor, useSlate } from 'slate-react';
|
||||||
import {
|
import {
|
||||||
headingLevel,
|
headingLevel,
|
||||||
|
|
@ -119,26 +120,33 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
|
||||||
export function HeadingBlockButton() {
|
export function HeadingBlockButton() {
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
const level = headingLevel(editor);
|
const level = headingLevel(editor);
|
||||||
const [open, setOpen] = useState(false);
|
const [anchor, setAnchor] = useState<RectCords>();
|
||||||
const isActive = isBlockActive(editor, BlockType.Heading);
|
const isActive = isBlockActive(editor, BlockType.Heading);
|
||||||
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
||||||
|
|
||||||
const handleMenuSelect = (selectedLevel: HeadingLevel) => {
|
const handleMenuSelect = (selectedLevel: HeadingLevel) => {
|
||||||
setOpen(false);
|
setAnchor(undefined);
|
||||||
toggleBlock(editor, BlockType.Heading, { level: selectedLevel });
|
toggleBlock(editor, BlockType.Heading, { level: selectedLevel });
|
||||||
ReactEditor.focus(editor);
|
ReactEditor.focus(editor);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMenuOpen: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
if (isActive) {
|
||||||
|
toggleBlock(editor, BlockType.Heading);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAnchor(evt.currentTarget.getBoundingClientRect());
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<PopOut
|
<PopOut
|
||||||
open={open}
|
anchor={anchor}
|
||||||
offset={5}
|
offset={5}
|
||||||
position="Top"
|
position="Top"
|
||||||
content={
|
content={
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
initialFocus: false,
|
||||||
onDeactivate: () => setOpen(false),
|
onDeactivate: () => setAnchor(undefined),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
isKeyForward: (evt: KeyboardEvent) =>
|
isKeyForward: (evt: KeyboardEvent) =>
|
||||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||||
|
|
@ -197,12 +205,10 @@ export function HeadingBlockButton() {
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(ref) => (
|
|
||||||
<IconButton
|
<IconButton
|
||||||
style={{ width: 'unset' }}
|
style={{ width: 'unset' }}
|
||||||
ref={ref}
|
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))}
|
onClick={handleMenuOpen}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
|
|
@ -210,7 +216,6 @@ export function HeadingBlockButton() {
|
||||||
<Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} />
|
<Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} />
|
||||||
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
|
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
|
||||||
</PopOut>
|
</PopOut>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo } from 'react';
|
import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect } from 'react';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
import { Avatar, AvatarFallback, AvatarImage, Icon, Icons, MenuItem, Text, color } from 'folds';
|
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { JoinRule, MatrixClient } from 'matrix-js-sdk';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
|
||||||
import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
|
import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
|
||||||
import { getRoomAvatarUrl, joinRuleToIconSrc } from '../../../utils/room';
|
import { getDirectRoomAvatarUrl } from '../../../utils/room';
|
||||||
import { roomIdByActivity } from '../../../../util/sort';
|
|
||||||
import initMatrix from '../../../../client/initMatrix';
|
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { AutocompleteQuery } from './autocompleteQuery';
|
import { AutocompleteQuery } from './autocompleteQuery';
|
||||||
import { AutocompleteMenu } from './AutocompleteMenu';
|
import { AutocompleteMenu } from './AutocompleteMenu';
|
||||||
|
|
@ -14,6 +13,10 @@ import { getMxIdServer, validMxId } from '../../../utils/matrix';
|
||||||
import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch';
|
import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch';
|
||||||
import { onTabPress } from '../../../utils/keyboard';
|
import { onTabPress } from '../../../utils/keyboard';
|
||||||
import { useKeyDown } from '../../../hooks/useKeyDown';
|
import { useKeyDown } from '../../../hooks/useKeyDown';
|
||||||
|
import { mDirectAtom } from '../../../state/mDirectList';
|
||||||
|
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||||
|
import { factoryRoomIdByActivity } from '../../../utils/sort';
|
||||||
|
import { RoomAvatar, RoomIcon } from '../../room-avatar';
|
||||||
|
|
||||||
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
|
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
|
||||||
|
|
||||||
|
|
@ -74,15 +77,12 @@ export function RoomMentionAutocomplete({
|
||||||
requestClose,
|
requestClose,
|
||||||
}: RoomMentionAutocompleteProps) {
|
}: RoomMentionAutocompleteProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const dms: Set<string> = initMatrix.roomList?.directs ?? new Set();
|
const mDirects = useAtomValue(mDirectAtom);
|
||||||
|
|
||||||
const allRoomId: string[] = useMemo(() => {
|
const allRooms = useAtomValue(allRoomsAtom).sort(factoryRoomIdByActivity(mx));
|
||||||
const { spaces = [], rooms = [], directs = [] } = initMatrix.roomList ?? {};
|
|
||||||
return [...spaces, ...rooms, ...directs].sort(roomIdByActivity);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [result, search, resetSearch] = useAsyncSearch(
|
const [result, search, resetSearch] = useAsyncSearch(
|
||||||
allRoomId,
|
allRooms,
|
||||||
useCallback(
|
useCallback(
|
||||||
(rId) => {
|
(rId) => {
|
||||||
const r = mx.getRoom(rId);
|
const r = mx.getRoom(rId);
|
||||||
|
|
@ -96,7 +96,7 @@ export function RoomMentionAutocomplete({
|
||||||
SEARCH_OPTIONS
|
SEARCH_OPTIONS
|
||||||
);
|
);
|
||||||
|
|
||||||
const autoCompleteRoomIds = result ? result.items : allRoomId.slice(0, 20);
|
const autoCompleteRoomIds = result ? result.items : allRooms.slice(0, 20);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.text) search(query.text);
|
if (query.text) search(query.text);
|
||||||
|
|
@ -136,9 +136,7 @@ export function RoomMentionAutocomplete({
|
||||||
autoCompleteRoomIds.map((rId) => {
|
autoCompleteRoomIds.map((rId) => {
|
||||||
const room = mx.getRoom(rId);
|
const room = mx.getRoom(rId);
|
||||||
if (!room) return null;
|
if (!room) return null;
|
||||||
const dm = dms.has(room.roomId);
|
const dm = mDirects.has(room.roomId);
|
||||||
const avatarUrl = getRoomAvatarUrl(mx, room);
|
|
||||||
const iconSrc = !dm && joinRuleToIconSrc(Icons, room.getJoinRule(), room.isSpaceRoom());
|
|
||||||
|
|
||||||
const handleSelect = () => handleAutocomplete(room.getCanonicalAlias() ?? rId, room.name);
|
const handleSelect = () => handleAutocomplete(room.getCanonicalAlias() ?? rId, room.name);
|
||||||
|
|
||||||
|
|
@ -158,17 +156,21 @@ export function RoomMentionAutocomplete({
|
||||||
}
|
}
|
||||||
before={
|
before={
|
||||||
<Avatar size="200">
|
<Avatar size="200">
|
||||||
{iconSrc && <Icon src={iconSrc} size="100" />}
|
{dm ? (
|
||||||
{avatarUrl && !iconSrc && <AvatarImage src={avatarUrl} alt={room.name} />}
|
<RoomAvatar
|
||||||
{!avatarUrl && !iconSrc && (
|
roomId={room.roomId}
|
||||||
<AvatarFallback
|
src={getDirectRoomAvatarUrl(mx, room)}
|
||||||
style={{
|
alt={room.name}
|
||||||
backgroundColor: color.Secondary.Container,
|
renderFallback={() => (
|
||||||
color: color.Secondary.OnContainer,
|
<RoomIcon
|
||||||
}}
|
size="50"
|
||||||
>
|
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||||
<Text size="H6">{room.name[0]}</Text>
|
filled
|
||||||
</AvatarFallback>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RoomIcon size="100" joinRule={room.getJoinRule()} space={room.isSpaceRoom()} />
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react';
|
import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
import { Avatar, AvatarFallback, AvatarImage, MenuItem, Text, color } from 'folds';
|
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
|
||||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
||||||
|
|
||||||
import { AutocompleteQuery } from './autocompleteQuery';
|
import { AutocompleteQuery } from './autocompleteQuery';
|
||||||
|
|
@ -17,6 +17,7 @@ import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
|
||||||
import { useKeyDown } from '../../../hooks/useKeyDown';
|
import { useKeyDown } from '../../../hooks/useKeyDown';
|
||||||
import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
|
import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
|
||||||
import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
|
import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
|
||||||
|
import { UserAvatar } from '../../user-avatar';
|
||||||
|
|
||||||
type MentionAutoCompleteHandler = (userId: string, name: string) => void;
|
type MentionAutoCompleteHandler = (userId: string, name: string) => void;
|
||||||
|
|
||||||
|
|
@ -26,12 +27,10 @@ const userIdFromQueryText = (mx: MatrixClient, text: string) =>
|
||||||
: `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`;
|
: `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`;
|
||||||
|
|
||||||
function UnknownMentionItem({
|
function UnknownMentionItem({
|
||||||
query,
|
|
||||||
userId,
|
userId,
|
||||||
name,
|
name,
|
||||||
handleAutocomplete,
|
handleAutocomplete,
|
||||||
}: {
|
}: {
|
||||||
query: AutocompleteQuery<string>;
|
|
||||||
userId: string;
|
userId: string;
|
||||||
name: string;
|
name: string;
|
||||||
handleAutocomplete: MentionAutoCompleteHandler;
|
handleAutocomplete: MentionAutoCompleteHandler;
|
||||||
|
|
@ -46,14 +45,10 @@ function UnknownMentionItem({
|
||||||
onClick={() => handleAutocomplete(userId, name)}
|
onClick={() => handleAutocomplete(userId, name)}
|
||||||
before={
|
before={
|
||||||
<Avatar size="200">
|
<Avatar size="200">
|
||||||
<AvatarFallback
|
<UserAvatar
|
||||||
style={{
|
userId={userId}
|
||||||
backgroundColor: color.Secondary.Container,
|
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||||
color: color.Secondary.OnContainer,
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text size="H6">{query.text[0]}</Text>
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -135,7 +130,6 @@ export function UserMentionAutocomplete({
|
||||||
<AutocompleteMenu headerContent={<Text size="L400">Mentions</Text>} requestClose={requestClose}>
|
<AutocompleteMenu headerContent={<Text size="L400">Mentions</Text>} requestClose={requestClose}>
|
||||||
{query.text === 'room' && (
|
{query.text === 'room' && (
|
||||||
<UnknownMentionItem
|
<UnknownMentionItem
|
||||||
query={query}
|
|
||||||
userId={roomAliasOrId}
|
userId={roomAliasOrId}
|
||||||
name="@room"
|
name="@room"
|
||||||
handleAutocomplete={handleAutocomplete}
|
handleAutocomplete={handleAutocomplete}
|
||||||
|
|
@ -143,7 +137,6 @@ export function UserMentionAutocomplete({
|
||||||
)}
|
)}
|
||||||
{autoCompleteMembers.length === 0 ? (
|
{autoCompleteMembers.length === 0 ? (
|
||||||
<UnknownMentionItem
|
<UnknownMentionItem
|
||||||
query={query}
|
|
||||||
userId={userIdFromQueryText(mx, query.text)}
|
userId={userIdFromQueryText(mx, query.text)}
|
||||||
name={userIdFromQueryText(mx, query.text)}
|
name={userIdFromQueryText(mx, query.text)}
|
||||||
handleAutocomplete={handleAutocomplete}
|
handleAutocomplete={handleAutocomplete}
|
||||||
|
|
@ -167,18 +160,12 @@ export function UserMentionAutocomplete({
|
||||||
}
|
}
|
||||||
before={
|
before={
|
||||||
<Avatar size="200">
|
<Avatar size="200">
|
||||||
{avatarUrl ? (
|
<UserAvatar
|
||||||
<AvatarImage src={avatarUrl} alt={getName(roomMember)} />
|
userId={roomMember.userId}
|
||||||
) : (
|
src={avatarUrl ?? undefined}
|
||||||
<AvatarFallback
|
alt={getName(roomMember)}
|
||||||
style={{
|
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||||
backgroundColor: color.Secondary.Container,
|
/>
|
||||||
color: color.Secondary.OnContainer,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text size="H6">{getName(roomMember)[0]}</Text>
|
|
||||||
</AvatarFallback>
|
|
||||||
)}
|
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
Box,
|
Box,
|
||||||
Header,
|
Header,
|
||||||
Icon,
|
Icon,
|
||||||
|
|
@ -21,8 +19,8 @@ import { getMemberDisplayName } from '../../utils/room';
|
||||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||||
import * as css from './EventReaders.css';
|
import * as css from './EventReaders.css';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
|
||||||
import { openProfileViewer } from '../../../client/action/navigation';
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
|
import { UserAvatar } from '../user-avatar';
|
||||||
|
|
||||||
export type EventReadersProps = {
|
export type EventReadersProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
|
@ -72,18 +70,12 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||||
}}
|
}}
|
||||||
before={
|
before={
|
||||||
<Avatar size="200">
|
<Avatar size="200">
|
||||||
{avatarUrl ? (
|
<UserAvatar
|
||||||
<AvatarImage src={avatarUrl} />
|
userId={readerId}
|
||||||
) : (
|
src={avatarUrl ?? undefined}
|
||||||
<AvatarFallback
|
alt={name}
|
||||||
style={{
|
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||||
background: colorMXID(readerId),
|
/>
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text size="H6">{name[0]}</Text>
|
|
||||||
</AvatarFallback>
|
|
||||||
)}
|
|
||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
106
src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
Normal file
106
src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
Overlay,
|
||||||
|
OverlayCenter,
|
||||||
|
OverlayBackdrop,
|
||||||
|
Header,
|
||||||
|
config,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
IconButton,
|
||||||
|
Icon,
|
||||||
|
Icons,
|
||||||
|
color,
|
||||||
|
Button,
|
||||||
|
Spinner,
|
||||||
|
} from 'folds';
|
||||||
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
|
||||||
|
type LeaveRoomPromptProps = {
|
||||||
|
roomId: string;
|
||||||
|
onDone: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
const [leaveState, leaveRoom] = useAsyncCallback<undefined, MatrixError, []>(
|
||||||
|
useCallback(async () => {
|
||||||
|
mx.leave(roomId);
|
||||||
|
}, [mx, roomId])
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLeave = () => {
|
||||||
|
leaveRoom();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (leaveState.status === AsyncStatus.Success) {
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
}, [leaveState, onDone]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
|
<OverlayCenter>
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: onCancel,
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog variant="Surface">
|
||||||
|
<Header
|
||||||
|
style={{
|
||||||
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||||||
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
|
}}
|
||||||
|
variant="Surface"
|
||||||
|
size="500"
|
||||||
|
>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="H4">Leave Room</Text>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="300" onClick={onCancel} radii="300">
|
||||||
|
<Icon src={Icons.Cross} />
|
||||||
|
</IconButton>
|
||||||
|
</Header>
|
||||||
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
|
<Box direction="Column" gap="200">
|
||||||
|
<Text priority="400">Are you sure you want to leave this room?</Text>
|
||||||
|
{leaveState.status === AsyncStatus.Error && (
|
||||||
|
<Text style={{ color: color.Critical.Main }} size="T300">
|
||||||
|
Failed to leave room! {leaveState.error.message}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="Critical"
|
||||||
|
onClick={handleLeave}
|
||||||
|
before={
|
||||||
|
leaveState.status === AsyncStatus.Loading ? (
|
||||||
|
<Spinner fill="Solid" variant="Critical" size="200" />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
aria-disabled={
|
||||||
|
leaveState.status === AsyncStatus.Loading ||
|
||||||
|
leaveState.status === AsyncStatus.Success
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="B400">
|
||||||
|
{leaveState.status === AsyncStatus.Loading ? 'Leaving...' : 'Leave'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
</FocusTrap>
|
||||||
|
</OverlayCenter>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue