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,
|
||||
|
||||
"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": {
|
||||
"enabled": false,
|
||||
"basename": "/"
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@
|
|||
status = 200
|
||||
|
||||
[[redirects]]
|
||||
from = "/olm.wasm"
|
||||
from = "*/olm.wasm"
|
||||
to = "/olm.wasm"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/pdf.worker.min.js"
|
||||
|
|
@ -32,3 +33,4 @@
|
|||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
force = true
|
||||
121
package-lock.json
generated
121
package-lock.json
generated
|
|
@ -9,10 +9,15 @@
|
|||
"version": "3.2.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"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",
|
||||
"@khanacademy/simple-markdown": "0.8.6",
|
||||
"@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",
|
||||
"@vanilla-extract/css": "1.9.3",
|
||||
"@vanilla-extract/recipes": "0.3.0",
|
||||
|
|
@ -29,7 +34,7 @@
|
|||
"file-saver": "2.0.5",
|
||||
"flux": "4.0.3",
|
||||
"focus-trap-react": "10.0.2",
|
||||
"folds": "1.5.1",
|
||||
"folds": "2.0.0",
|
||||
"formik": "2.2.9",
|
||||
"html-dom-parser": "4.0.0",
|
||||
"html-react-parser": "4.2.0",
|
||||
|
|
@ -111,6 +116,34 @@
|
|||
"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": {
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
|
||||
|
|
@ -3093,25 +3126,75 @@
|
|||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.0.0-beta.54",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.54.tgz",
|
||||
"integrity": "sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==",
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.24.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.24.1.tgz",
|
||||
"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": {
|
||||
"@tanstack/virtual-core": "3.0.0-beta.54"
|
||||
"@tanstack/query-core": "5.24.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"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": {
|
||||
"version": "3.0.0-beta.54",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.54.tgz",
|
||||
"integrity": "sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.2.0.tgz",
|
||||
"integrity": "sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
|
|
@ -4065,6 +4148,11 @@
|
|||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.4.tgz",
|
||||
|
|
@ -5665,9 +5753,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/folds": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/folds/-/folds-1.5.1.tgz",
|
||||
"integrity": "sha512-2QxyA+FRKjPKXDTMDoD7NmOUiReWrKYO0Msg44QqlzTkTrRVEzJgyPIfC/Ia4/u0ByQpk6dbq8UQxomKmneJ/g==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/folds/-/folds-2.0.0.tgz",
|
||||
"integrity": "sha512-lKv31vij4GEpEzGKWk5c3ar78fMZ9Di5n1XFR14Z2wnnpqhiiM5JTIzr127Gk5dOfy4mJkjnv/ZfMZvM2k+OQg==",
|
||||
"peerDependencies": {
|
||||
"@vanilla-extract/css": "^1.9.2",
|
||||
"@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": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -20,10 +20,15 @@
|
|||
"author": "Ajay Bura",
|
||||
"license": "AGPL-3.0-only",
|
||||
"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",
|
||||
"@khanacademy/simple-markdown": "0.8.6",
|
||||
"@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",
|
||||
"@vanilla-extract/css": "1.9.3",
|
||||
"@vanilla-extract/recipes": "0.3.0",
|
||||
|
|
@ -40,7 +45,7 @@
|
|||
"file-saver": "2.0.5",
|
||||
"flux": "4.0.3",
|
||||
"focus-trap-react": "10.0.2",
|
||||
"folds": "1.5.1",
|
||||
"folds": "2.0.0",
|
||||
"formik": "2.2.9",
|
||||
"html-dom-parser": "4.0.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 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 {
|
||||
Box,
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Input,
|
||||
Menu,
|
||||
PopOut,
|
||||
RectCords,
|
||||
Scroll,
|
||||
Spinner,
|
||||
Text,
|
||||
|
|
@ -48,7 +49,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
|||
const isError =
|
||||
pdfJSState.status === AsyncStatus.Error || docState.status === AsyncStatus.Error;
|
||||
const [pageNo, setPageNo] = useState(1);
|
||||
const [openJump, setOpenJump] = useState(false);
|
||||
const [jumpAnchor, setJumpAnchor] = useState<RectCords>();
|
||||
|
||||
useEffect(() => {
|
||||
loadPdfJS();
|
||||
|
|
@ -86,7 +87,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
|||
if (!jumpInput) return;
|
||||
const jumpTo = parseInt(jumpInput.value, 10);
|
||||
setPageNo(Math.max(1, Math.min(docState.data.numPages, jumpTo)));
|
||||
setOpenJump(false);
|
||||
setJumpAnchor(undefined);
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
|
|
@ -98,6 +99,10 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
|||
setPageNo((n) => Math.min(n + 1, docState.data.numPages));
|
||||
};
|
||||
|
||||
const handleOpenJump: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setJumpAnchor(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
|
||||
<Header className={css.PdfViewerHeader} size="400">
|
||||
|
|
@ -187,14 +192,14 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
|||
</Chip>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<PopOut
|
||||
open={openJump}
|
||||
anchor={jumpAnchor}
|
||||
align="Center"
|
||||
position="Top"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setOpenJump(false),
|
||||
onDeactivate: () => setJumpAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
}}
|
||||
>
|
||||
|
|
@ -227,17 +232,14 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
|||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
{(anchorRef) => (
|
||||
<Chip
|
||||
onClick={() => setOpenJump(!openJump)}
|
||||
ref={anchorRef}
|
||||
onClick={handleOpenJump}
|
||||
variant="SurfaceVariant"
|
||||
radii="300"
|
||||
aria-pressed={openJump}
|
||||
aria-pressed={jumpAnchor !== undefined}
|
||||
>
|
||||
<Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text>
|
||||
</Chip>
|
||||
)}
|
||||
</PopOut>
|
||||
</Box>
|
||||
<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 { SpecVersions, specVersions } from '../cs-api';
|
||||
import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
|
||||
|
||||
type SpecVersionsLoaderProps = {
|
||||
baseUrl: string;
|
||||
fallback?: () => ReactNode;
|
||||
error?: (err: unknown) => ReactNode;
|
||||
error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode;
|
||||
children: (versions: SpecVersions) => ReactNode;
|
||||
};
|
||||
export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLoaderProps) {
|
||||
const autoDiscoveryInfo = useAutoDiscoveryInfo();
|
||||
const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;
|
||||
|
||||
export function SpecVersionsLoader({
|
||||
baseUrl,
|
||||
fallback,
|
||||
error,
|
||||
children,
|
||||
}: SpecVersionsLoaderProps) {
|
||||
const [state, load] = useAsyncCallback(
|
||||
useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
|
||||
);
|
||||
const [ignoreError, setIgnoreError] = useState(false);
|
||||
|
||||
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
|
|
@ -24,9 +29,15 @@ export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLo
|
|||
return fallback?.();
|
||||
}
|
||||
|
||||
if (state.status === AsyncStatus.Error) {
|
||||
return error?.(state.error);
|
||||
if (!ignoreError && state.status === AsyncStatus.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,
|
||||
Menu,
|
||||
PopOut,
|
||||
RectCords,
|
||||
Scroll,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import React, { MouseEventHandler, ReactNode, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import {
|
||||
headingLevel,
|
||||
|
|
@ -119,26 +120,33 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
|
|||
export function HeadingBlockButton() {
|
||||
const editor = useSlate();
|
||||
const level = headingLevel(editor);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [anchor, setAnchor] = useState<RectCords>();
|
||||
const isActive = isBlockActive(editor, BlockType.Heading);
|
||||
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
||||
|
||||
const handleMenuSelect = (selectedLevel: HeadingLevel) => {
|
||||
setOpen(false);
|
||||
setAnchor(undefined);
|
||||
toggleBlock(editor, BlockType.Heading, { level: selectedLevel });
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
const handleMenuOpen: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
if (isActive) {
|
||||
toggleBlock(editor, BlockType.Heading);
|
||||
return;
|
||||
}
|
||||
setAnchor(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
return (
|
||||
<PopOut
|
||||
open={open}
|
||||
anchor={anchor}
|
||||
offset={5}
|
||||
position="Top"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setOpen(false),
|
||||
onDeactivate: () => setAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) =>
|
||||
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
||||
|
|
@ -197,12 +205,10 @@ export function HeadingBlockButton() {
|
|||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
{(ref) => (
|
||||
<IconButton
|
||||
style={{ width: 'unset' }}
|
||||
ref={ref}
|
||||
variant="SurfaceVariant"
|
||||
onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))}
|
||||
onClick={handleMenuOpen}
|
||||
aria-pressed={isActive}
|
||||
size="400"
|
||||
radii="300"
|
||||
|
|
@ -210,7 +216,6 @@ export function HeadingBlockButton() {
|
|||
<Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} />
|
||||
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
|
||||
</IconButton>
|
||||
)}
|
||||
</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 { Avatar, AvatarFallback, AvatarImage, Icon, Icons, MenuItem, Text, color } from 'folds';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
|
||||
import { JoinRule, MatrixClient } from 'matrix-js-sdk';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
|
||||
import { getRoomAvatarUrl, joinRuleToIconSrc } from '../../../utils/room';
|
||||
import { roomIdByActivity } from '../../../../util/sort';
|
||||
import initMatrix from '../../../../client/initMatrix';
|
||||
import { getDirectRoomAvatarUrl } from '../../../utils/room';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { AutocompleteQuery } from './autocompleteQuery';
|
||||
import { AutocompleteMenu } from './AutocompleteMenu';
|
||||
|
|
@ -14,6 +13,10 @@ import { getMxIdServer, validMxId } from '../../../utils/matrix';
|
|||
import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch';
|
||||
import { onTabPress } from '../../../utils/keyboard';
|
||||
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;
|
||||
|
||||
|
|
@ -74,15 +77,12 @@ export function RoomMentionAutocomplete({
|
|||
requestClose,
|
||||
}: RoomMentionAutocompleteProps) {
|
||||
const mx = useMatrixClient();
|
||||
const dms: Set<string> = initMatrix.roomList?.directs ?? new Set();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
const allRoomId: string[] = useMemo(() => {
|
||||
const { spaces = [], rooms = [], directs = [] } = initMatrix.roomList ?? {};
|
||||
return [...spaces, ...rooms, ...directs].sort(roomIdByActivity);
|
||||
}, []);
|
||||
const allRooms = useAtomValue(allRoomsAtom).sort(factoryRoomIdByActivity(mx));
|
||||
|
||||
const [result, search, resetSearch] = useAsyncSearch(
|
||||
allRoomId,
|
||||
allRooms,
|
||||
useCallback(
|
||||
(rId) => {
|
||||
const r = mx.getRoom(rId);
|
||||
|
|
@ -96,7 +96,7 @@ export function RoomMentionAutocomplete({
|
|||
SEARCH_OPTIONS
|
||||
);
|
||||
|
||||
const autoCompleteRoomIds = result ? result.items : allRoomId.slice(0, 20);
|
||||
const autoCompleteRoomIds = result ? result.items : allRooms.slice(0, 20);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.text) search(query.text);
|
||||
|
|
@ -136,9 +136,7 @@ export function RoomMentionAutocomplete({
|
|||
autoCompleteRoomIds.map((rId) => {
|
||||
const room = mx.getRoom(rId);
|
||||
if (!room) return null;
|
||||
const dm = dms.has(room.roomId);
|
||||
const avatarUrl = getRoomAvatarUrl(mx, room);
|
||||
const iconSrc = !dm && joinRuleToIconSrc(Icons, room.getJoinRule(), room.isSpaceRoom());
|
||||
const dm = mDirects.has(room.roomId);
|
||||
|
||||
const handleSelect = () => handleAutocomplete(room.getCanonicalAlias() ?? rId, room.name);
|
||||
|
||||
|
|
@ -158,17 +156,21 @@ export function RoomMentionAutocomplete({
|
|||
}
|
||||
before={
|
||||
<Avatar size="200">
|
||||
{iconSrc && <Icon src={iconSrc} size="100" />}
|
||||
{avatarUrl && !iconSrc && <AvatarImage src={avatarUrl} alt={room.name} />}
|
||||
{!avatarUrl && !iconSrc && (
|
||||
<AvatarFallback
|
||||
style={{
|
||||
backgroundColor: color.Secondary.Container,
|
||||
color: color.Secondary.OnContainer,
|
||||
}}
|
||||
>
|
||||
<Text size="H6">{room.name[0]}</Text>
|
||||
</AvatarFallback>
|
||||
{dm ? (
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={getDirectRoomAvatarUrl(mx, room)}
|
||||
alt={room.name}
|
||||
renderFallback={() => (
|
||||
<RoomIcon
|
||||
size="50"
|
||||
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<RoomIcon size="100" joinRule={room.getJoinRule()} space={room.isSpaceRoom()} />
|
||||
)}
|
||||
</Avatar>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||
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 { AutocompleteQuery } from './autocompleteQuery';
|
||||
|
|
@ -17,6 +17,7 @@ import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
|
|||
import { useKeyDown } from '../../../hooks/useKeyDown';
|
||||
import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
|
||||
import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
|
||||
import { UserAvatar } from '../../user-avatar';
|
||||
|
||||
type MentionAutoCompleteHandler = (userId: string, name: string) => void;
|
||||
|
||||
|
|
@ -26,12 +27,10 @@ const userIdFromQueryText = (mx: MatrixClient, text: string) =>
|
|||
: `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`;
|
||||
|
||||
function UnknownMentionItem({
|
||||
query,
|
||||
userId,
|
||||
name,
|
||||
handleAutocomplete,
|
||||
}: {
|
||||
query: AutocompleteQuery<string>;
|
||||
userId: string;
|
||||
name: string;
|
||||
handleAutocomplete: MentionAutoCompleteHandler;
|
||||
|
|
@ -46,14 +45,10 @@ function UnknownMentionItem({
|
|||
onClick={() => handleAutocomplete(userId, name)}
|
||||
before={
|
||||
<Avatar size="200">
|
||||
<AvatarFallback
|
||||
style={{
|
||||
backgroundColor: color.Secondary.Container,
|
||||
color: color.Secondary.OnContainer,
|
||||
}}
|
||||
>
|
||||
<Text size="H6">{query.text[0]}</Text>
|
||||
</AvatarFallback>
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
|
|
@ -135,7 +130,6 @@ export function UserMentionAutocomplete({
|
|||
<AutocompleteMenu headerContent={<Text size="L400">Mentions</Text>} requestClose={requestClose}>
|
||||
{query.text === 'room' && (
|
||||
<UnknownMentionItem
|
||||
query={query}
|
||||
userId={roomAliasOrId}
|
||||
name="@room"
|
||||
handleAutocomplete={handleAutocomplete}
|
||||
|
|
@ -143,7 +137,6 @@ export function UserMentionAutocomplete({
|
|||
)}
|
||||
{autoCompleteMembers.length === 0 ? (
|
||||
<UnknownMentionItem
|
||||
query={query}
|
||||
userId={userIdFromQueryText(mx, query.text)}
|
||||
name={userIdFromQueryText(mx, query.text)}
|
||||
handleAutocomplete={handleAutocomplete}
|
||||
|
|
@ -167,18 +160,12 @@ export function UserMentionAutocomplete({
|
|||
}
|
||||
before={
|
||||
<Avatar size="200">
|
||||
{avatarUrl ? (
|
||||
<AvatarImage src={avatarUrl} alt={getName(roomMember)} />
|
||||
) : (
|
||||
<AvatarFallback
|
||||
style={{
|
||||
backgroundColor: color.Secondary.Container,
|
||||
color: color.Secondary.OnContainer,
|
||||
}}
|
||||
>
|
||||
<Text size="H6">{getName(roomMember)[0]}</Text>
|
||||
</AvatarFallback>
|
||||
)}
|
||||
<UserAvatar
|
||||
userId={roomMember.userId}
|
||||
src={avatarUrl ?? undefined}
|
||||
alt={getName(roomMember)}
|
||||
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import React from 'react';
|
|||
import classNames from 'classnames';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
Box,
|
||||
Header,
|
||||
Icon,
|
||||
|
|
@ -21,8 +19,8 @@ import { getMemberDisplayName } from '../../utils/room';
|
|||
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||
import * as css from './EventReaders.css';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { openProfileViewer } from '../../../client/action/navigation';
|
||||
import { UserAvatar } from '../user-avatar';
|
||||
|
||||
export type EventReadersProps = {
|
||||
room: Room;
|
||||
|
|
@ -72,18 +70,12 @@ export const EventReaders = as<'div', EventReadersProps>(
|
|||
}}
|
||||
before={
|
||||
<Avatar size="200">
|
||||
{avatarUrl ? (
|
||||
<AvatarImage src={avatarUrl} />
|
||||
) : (
|
||||
<AvatarFallback
|
||||
style={{
|
||||
background: colorMXID(readerId),
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<Text size="H6">{name[0]}</Text>
|
||||
</AvatarFallback>
|
||||
)}
|
||||
<UserAvatar
|
||||
userId={readerId}
|
||||
src={avatarUrl ?? undefined}
|
||||
alt={name}
|
||||
renderFallback={() => <Icon size="50" src={Icons.User} filled />}
|
||||
/>
|
||||
</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