imnyang/cinny
Watch
1
0
Fork
You've already forked cinny
0

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:
Ajay Bura 2024-05-31 19:49:46 +05:30 • committed by GitHub
commit 4c76a7fd18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
290 changed files with 17436 additions and 3213 deletions

64
.eslintrc.cjs Normal file
View 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"
},
};

View file

@ -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": "/"

View file

@ -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"
@ -31,4 +32,5 @@
[[redirects]] [[redirects]]
from = "/*" from = "/*"
to = "/index.html" to = "/index.html"
status = 200 status = 200
force = true

121
package-lock.json generated
View file

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

View file

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

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

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

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

View file

@ -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={handleOpenJump}
onClick={() => setOpenJump(!openJump)} variant="SurfaceVariant"
ref={anchorRef} radii="300"
variant="SurfaceVariant" aria-pressed={jumpAnchor !== undefined}
radii="300" >
aria-pressed={openJump} <Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text>
> </Chip>
<Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text>
</Chip>
)}
</PopOut> </PopOut>
</Box> </Box>
<Chip <Chip

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

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

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

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

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

View file

@ -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: [],
}
);
} }

View file

@ -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,20 +205,17 @@ export function HeadingBlockButton() {
</FocusTrap> </FocusTrap>
} }
> >
{(ref) => ( <IconButton
<IconButton style={{ width: 'unset' }}
style={{ width: 'unset' }} variant="SurfaceVariant"
ref={ref} onClick={handleMenuOpen}
variant="SurfaceVariant" aria-pressed={isActive}
onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))} size="400"
aria-pressed={isActive} radii="300"
size="400" >
radii="300" <Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} />
> <Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
<Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} /> </IconButton>
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
</IconButton>
)}
</PopOut> </PopOut>
); );
} }

View file

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

View file

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

View file

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

View 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