diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 9d5a27d9..dca01a3d 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -31,12 +31,15 @@ module.exports = {
'linebreak-style': 0,
'no-underscore-dangle': 0,
"no-shadow": "off",
+ "max-classes-per-file": "off",
+ "no-useless-constructor": "off",
+ "no-empty-function": "off",
"import/prefer-default-export": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"import/no-extraneous-dependencies": [
- "error",
+ "off",
{
devDependencies: true,
},
@@ -58,8 +61,10 @@ module.exports = {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
+ "@typescript-eslint/no-useless-constructor": "error",
+ "@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-unused-vars": "error",
- "@typescript-eslint/no-shadow": "error"
+ "@typescript-eslint/no-shadow": "error",
},
overrides: [
{
diff --git a/.zed/settings.json b/.zed/settings.json
new file mode 100644
index 00000000..83b47ade
--- /dev/null
+++ b/.zed/settings.json
@@ -0,0 +1,7 @@
+{
+ "format_on_save": "on",
+ "prettier": {
+ "parser": "",
+ "allowed": true
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 158a6112..5c36b489 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
"@fontsource/inter": "4.5.14",
+ "@matrix-org/matrix-sdk-crypto-wasm": "18.3.1",
"@tanstack/react-query": "5.24.1",
"@tanstack/react-query-devtools": "5.24.1",
"@tanstack/react-virtual": "3.2.0",
@@ -32,12 +33,13 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.4.0",
+ "folds": "2.7.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
"i18next-browser-languagedetector": "8.0.0",
"i18next-http-backend": "2.5.2",
+ "idb": "8.0.3",
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "2.6.0",
@@ -2256,9 +2258,9 @@
}
},
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
- "version": "15.3.0",
- "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz",
- "integrity": "sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.1.tgz",
+ "integrity": "sha512-VRjWhE1UgHnPpJ3b9B5+8z71ZC/HICFngPPFIN6ktzmUBKI5RusPujzbAQUoB3CgZ0yU58L99AfSQS4YTztSWw==",
"license": "Apache-2.0",
"engines": {
"node": ">= 18"
@@ -7157,9 +7159,9 @@
}
},
"node_modules/folds": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/folds/-/folds-2.4.0.tgz",
- "integrity": "sha512-Q5xCmvU3SIM8etQ9qLF6Y5Jtv01c9JpG3QcnF+Z3nlbMvtktfE13Pj7p0XgSPBcA3OuoU0zXiRwiTlMcbU7KhA==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/folds/-/folds-2.7.0.tgz",
+ "integrity": "sha512-YOWx0raqLignz7Jgw7G5f90fczKSVEUiNuLwgZo9MvZcHOlRvf6+77Oa/1B6Baq1Qj5mO/cNTLBNiA5yF9pMdQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@vanilla-extract/css": "1.9.2",
@@ -7713,10 +7715,10 @@
}
},
"node_modules/idb": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
- "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
- "dev": true
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
+ "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==",
+ "license": "ISC"
},
"node_modules/ieee754": {
"version": "1.2.1",
@@ -8649,6 +8651,15 @@
"node": ">=22.0.0"
}
},
+ "node_modules/matrix-js-sdk/node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
+ "version": "15.3.0",
+ "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz",
+ "integrity": "sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/matrix-js-sdk/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
@@ -11789,6 +11800,13 @@
"workbox-core": "7.3.0"
}
},
+ "node_modules/workbox-background-sync/node_modules/idb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/workbox-broadcast-update": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz",
@@ -11935,6 +11953,13 @@
"workbox-core": "7.3.0"
}
},
+ "node_modules/workbox-expiration/node_modules/idb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/workbox-google-analytics": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz",
diff --git a/package.json b/package.json
index 7316dcce..619ad78e 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
"@fontsource/inter": "4.5.14",
+ "@matrix-org/matrix-sdk-crypto-wasm": "18.3.1",
"@tanstack/react-query": "5.24.1",
"@tanstack/react-query-devtools": "5.24.1",
"@tanstack/react-virtual": "3.2.0",
@@ -43,12 +44,13 @@
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
"focus-trap-react": "10.0.2",
- "folds": "2.4.0",
+ "folds": "2.7.0",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
"i18next-browser-languagedetector": "8.0.0",
"i18next-http-backend": "2.5.2",
+ "idb": "8.0.3",
"immer": "9.0.16",
"is-hotkey": "0.2.0",
"jotai": "2.6.0",
diff --git a/src/app/components/AuthFlowsLoader.tsx b/src/app/components/AuthFlowsLoader.tsx
index f21bad04..14ba56b2 100644
--- a/src/app/components/AuthFlowsLoader.tsx
+++ b/src/app/components/AuthFlowsLoader.tsx
@@ -1,14 +1,18 @@
import { ReactNode, useCallback, useEffect, useMemo } from 'react';
-import { MatrixError, createClient } from 'matrix-js-sdk';
+import { getLogin, GetLoginOK } from 'neurone/lib/api/matrix/client/v3/login';
+import { postRegister } from 'neurone/lib/api/matrix/client/v3/register';
+import { MatrixErrorData, parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
-import { promiseFulfilledResult, promiseRejectedResult } from '../utils/common';
+import { promiseFulfilledResult } from '../utils/common';
import {
AuthFlows,
RegisterFlowStatus,
RegisterFlowsResponse,
parseRegisterErrResp,
} from '../hooks/useAuthFlows';
+import { getJSONRequest } from '../../client/request';
+import { AuthResponse } from '../utils/matrix-uia';
type AuthFlowsLoaderProps = {
fallback?: () => ReactNode;
@@ -19,24 +23,33 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;
- const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
+ const requestJSON = useMemo(() => getJSONRequest(baseUrl), [baseUrl]);
const [state, load] = useAsyncCallback(
useCallback(async () => {
- const result = await Promise.allSettled([mx.loginFlows(), mx.registerRequest({})]);
- const loginFlows = promiseFulfilledResult(result[0]);
- const registerResp = promiseRejectedResult(result[1]) as MatrixError | undefined;
- let registerFlows: RegisterFlowsResponse = { status: RegisterFlowStatus.InvalidRequest };
+ const loginEndpoint = getLogin({});
+ const registerEndpoint = postRegister({});
- if (typeof registerResp === 'object' && registerResp.httpStatus) {
- registerFlows = parseRegisterErrResp(registerResp);
+ const [loginResponse, registerResponse] = (
+ await Promise.allSettled([requestJSON(loginEndpoint), requestJSON(registerEndpoint)])
+ ).map(promiseFulfilledResult);
+
+ let registerFlows: RegisterFlowsResponse = { status: RegisterFlowStatus.InvalidRequest };
+ const [registerError] =
+ typeof registerResponse === 'object'
+ ? await parseMatrixResp(registerResponse)
+ : [];
+
+ if (registerError) {
+ registerFlows = parseRegisterErrResp(registerError);
}
- if (!loginFlows) {
+ if (!loginResponse) {
throw new Error('Missing auth flow!');
}
- if ('errcode' in loginFlows) {
- throw new Error('Failed to load auth flow!');
+ const [loginError, loginFlows] = await parseMatrixResp(loginResponse);
+ if (loginError) {
+ throw new Error(`Failed to load auth flow! ${printMatrixError(loginError)}`);
}
const authFlows: AuthFlows = {
@@ -45,7 +58,7 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr
};
return authFlows;
- }, [mx])
+ }, [requestJSON])
);
useEffect(() => {
diff --git a/src/app/components/CapabilitiesLoader.tsx b/src/app/components/CapabilitiesLoader.tsx
index 667191e9..bfdae4e5 100644
--- a/src/app/components/CapabilitiesLoader.tsx
+++ b/src/app/components/CapabilitiesLoader.tsx
@@ -1,15 +1,23 @@
import { ReactNode, useCallback, useEffect } from 'react';
-import { Capabilities } from 'matrix-js-sdk';
+import { getCapabilities, GetCapabilitiesOK } from 'neurone/lib/api/matrix/client/v3/capabilities';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
-import { useMatrixClient } from '../hooks/useMatrixClient';
+import { useClient } from '../hooks/useClient';
+import { Capabilities } from '../hooks/useCapabilities';
type CapabilitiesLoaderProps = {
children: (capabilities: Capabilities | undefined) => ReactNode;
};
export function CapabilitiesLoader({ children }: CapabilitiesLoaderProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
- const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(), [mx]));
+ const [state, load] = useAsyncCallback(
+ useCallback(async () => {
+ const resp = await ct.requestAuthJSON(getCapabilities({}));
+ const [, result] = await parseMatrixResp(resp);
+ return result?.capabilities;
+ }, [ct])
+ );
useEffect(() => {
load();
diff --git a/src/app/components/LogoutDialog.tsx b/src/app/components/LogoutDialog.tsx
index cc9bf067..9fd9cdef 100644
--- a/src/app/components/LogoutDialog.tsx
+++ b/src/app/components/LogoutDialog.tsx
@@ -1,33 +1,19 @@
import React, { forwardRef, useCallback } from 'react';
import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
-import { logoutClient } from '../../client/initMatrix';
-import { useMatrixClient } from '../hooks/useMatrixClient';
-import { useCrossSigningActive } from '../hooks/useCrossSigning';
-import { InfoCard } from './info-card';
-import {
- useDeviceVerificationStatus,
- VerificationStatus,
-} from '../hooks/useDeviceVerificationStatus';
+import { useClient } from '../hooks/useClient';
type LogoutDialogProps = {
handleClose: () => void;
};
export const LogoutDialog = forwardRef(
({ handleClose }, ref) => {
- const mx = useMatrixClient();
- const hasEncryptedRoom = !!mx.getRooms().find((room) => room.hasEncryptionStateEvent());
- const crossSigningActive = useCrossSigningActive();
- const verificationStatus = useDeviceVerificationStatus(
- mx.getCrypto(),
- mx.getSafeUserId(),
- mx.getDeviceId() ?? undefined
- );
+ const ct = useClient();
const [logoutState, logout] = useAsyncCallback(
useCallback(async () => {
- await logoutClient(mx);
- }, [mx])
+ await ct.logout();
+ }, [ct])
);
const ongoingLogout = logoutState.status === AsyncStatus.Loading;
@@ -47,7 +33,7 @@ export const LogoutDialog = forwardRef(
- {hasEncryptedRoom &&
+ {/* {hasEncryptedRoom &&
(crossSigningActive ? (
verificationStatus === VerificationStatus.Unverified && (
(
title="Alert"
description="Enable device verification or export your encrypted data from settings to avoid losing access to your messages."
/>
- ))}
+ ))} */}
You’re about to log out. Are you sure?
{logoutState.status === AsyncStatus.Error && (
diff --git a/src/app/components/MediaConfigLoader.tsx b/src/app/components/MediaConfigLoader.tsx
index 9fd99330..634973dd 100644
--- a/src/app/components/MediaConfigLoader.tsx
+++ b/src/app/components/MediaConfigLoader.tsx
@@ -1,15 +1,25 @@
import { ReactNode, useCallback, useEffect } from 'react';
+import { getConfig, GetConfigOK } from 'neurone/lib/api/matrix/client/v1/media';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
-import { useMatrixClient } from '../hooks/useMatrixClient';
import { MediaConfig } from '../hooks/useMediaConfig';
+import { useClient } from '../hooks/useClient';
type MediaConfigLoaderProps = {
children: (mediaConfig: MediaConfig | undefined) => ReactNode;
};
export function MediaConfigLoader({ children }: MediaConfigLoaderProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
- const [state, load] = useAsyncCallback(useCallback(() => mx.getMediaConfig(), [mx]));
+ const [state, load] = useAsyncCallback(
+ useCallback(async () => {
+ const resp = await ct.requestAuthJSON(getConfig({}));
+
+ const [, config] = await parseMatrixResp(resp);
+
+ return config;
+ }, [ct])
+ );
useEffect(() => {
load();
diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx
index 4cfcb7dc..423fb1ff 100644
--- a/src/app/components/RenderMessageContent.tsx
+++ b/src/app/components/RenderMessageContent.tsx
@@ -1,147 +1,101 @@
import React from 'react';
-import { MsgType } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts } from 'linkifyjs';
import { config } from 'folds';
import {
- AudioContent,
- DownloadFile,
- FileContent,
- ImageContent,
MAudio,
- MBadEncrypted,
MEmote,
MFile,
MImage,
MLocation,
MNotice,
+ MSticker,
MText,
MVideo,
- ReadPdfFile,
- ReadTextFile,
- RenderBody,
- ThumbnailContent,
+ RedactedContent,
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';
-import { testMatrixTo } from '../plugins/matrix-to';
-import { IImageContent } from '../../types/matrix/common';
+import { MsgType } from '../../types/matrix/common';
+import { rContent } from '../../client/reader';
+import { MessageEvent } from '../../types/matrix/room';
type RenderMessageContentProps = {
displayName: string;
- msgType: string;
+ eventType: string;
ts: number;
edited?: boolean;
- getContent: () => T;
+ content: object;
mediaAutoLoad?: boolean;
urlPreview?: boolean;
highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions;
linkifyOpts: Opts;
outlineAttachment?: boolean;
+ redacted?: boolean;
};
export function RenderMessageContent({
displayName,
- msgType,
+ eventType,
ts,
edited,
- getContent,
+ content,
mediaAutoLoad,
urlPreview,
highlightRegex,
htmlReactParserOptions,
linkifyOpts,
outlineAttachment,
+ redacted,
}: RenderMessageContentProps) {
- const renderUrlsPreview = (urls: string[]) => {
- const filteredUrls = urls.filter((url) => !testMatrixTo(url));
- if (filteredUrls.length === 0) return undefined;
+ if (redacted) return ;
+
+ if (eventType === MessageEvent.Sticker) {
return (
-
- {filteredUrls.map((url) => (
-
- ))}
-
+
);
- };
+ }
+
+ if (eventType !== MessageEvent.RoomMessage) {
+ return ;
+ }
+
const renderCaption = () => {
- const content: IImageContent = getContent();
- if (content.filename && content.filename !== content.body) {
+ const filename = rContent.filename(content);
+ const body = rContent.body(content);
+
+ if (filename && filename !== body && body) {
return (
(
-
- )}
- renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
+ urlPreview={urlPreview}
+ ts={ts}
+ highlightRegex={highlightRegex}
+ htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
);
}
return null;
};
- const renderFile = () => (
- <>
- (
- (
- }
- />
- )}
- renderAsTextFile={() => (
- }
- />
- )}
- >
-
-
- )}
- outlined={outlineAttachment}
- />
- {renderCaption()}
- >
- );
+ const msgType = rContent.msgtype(content);
if (msgType === MsgType.Text) {
return (
(
-
- )}
- renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
+ content={content}
+ urlPreview={urlPreview}
+ ts={ts}
+ highlightRegex={highlightRegex}
+ htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
);
}
@@ -151,16 +105,12 @@ export function RenderMessageContent({
(
-
- )}
- renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
+ content={content}
+ urlPreview={urlPreview}
+ ts={ts}
+ highlightRegex={highlightRegex}
+ htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
);
}
@@ -169,16 +119,12 @@ export function RenderMessageContent({
return (
(
-
- )}
- renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
+ content={content}
+ urlPreview={urlPreview}
+ ts={ts}
+ highlightRegex={highlightRegex}
+ htmlReactParserOptions={htmlReactParserOptions}
+ linkifyOpts={linkifyOpts}
/>
);
}
@@ -186,18 +132,7 @@ export function RenderMessageContent({
if (msgType === MsgType.Image) {
return (
<>
- (
- }
- renderViewer={(p) => }
- />
- )}
- outlined={outlineAttachment}
- />
+
{renderCaption()}
>
);
@@ -206,31 +141,7 @@ export function RenderMessageContent({
if (msgType === MsgType.Video) {
return (
<>
- (
- (
- (
-
- )}
- />
- )
- : undefined
- }
- renderVideo={(p) => }
- />
- )}
- outlined={outlineAttachment}
- />
+
{renderCaption()}
>
);
@@ -239,30 +150,34 @@ export function RenderMessageContent({
if (msgType === MsgType.Audio) {
return (
<>
- (
- } />
- )}
- outlined={outlineAttachment}
- />
+
{renderCaption()}
>
);
}
if (msgType === MsgType.File) {
- return renderFile();
+ return (
+ <>
+
+ {renderCaption()}
+ >
+ );
}
if (msgType === MsgType.Location) {
- return ;
+ return ;
}
- if (msgType === 'm.bad.encrypted') {
- return ;
- }
-
- return ;
+ return (
+
+ );
}
diff --git a/src/app/components/RoomSummaryLoader.tsx b/src/app/components/RoomSummaryLoader.tsx
index efdce930..88b10446 100644
--- a/src/app/components/RoomSummaryLoader.tsx
+++ b/src/app/components/RoomSummaryLoader.tsx
@@ -1,22 +1,36 @@
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 { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
+import { getRoomSummary, GetRoomSummaryOK } from 'neurone/lib/api/matrix/client/v1/roomSummary';
+import { getHierarchy, GetHierarchyOK } from 'neurone/lib/api/matrix/client/v1/rooms';
+import { SpaceHierarchyRoomChunk } from 'neurone/lib/api/types';
import { AsyncState, AsyncStatus } from '../hooks/useAsyncCallback';
-
-export type IRoomSummary = Awaited>;
+import { useClient } from '../hooks/useClient';
+import { useSpecVersionSupport } from '../hooks/useSpecVersions';
type RoomSummaryLoaderProps = {
roomIdOrAlias: string;
- children: (roomSummary?: IRoomSummary) => ReactNode;
+ children: (roomSummary?: GetRoomSummaryOK) => ReactNode;
};
export function RoomSummaryLoader({ roomIdOrAlias, children }: RoomSummaryLoaderProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
+ const roomSummarySupported = useSpecVersionSupport('v1.15');
- const fetchSummary = useCallback(() => mx.getRoomSummary(roomIdOrAlias), [mx, roomIdOrAlias]);
+ const fetchSummary = useCallback(async (): Promise => {
+ const endpoint = getRoomSummary({ pathParams: { roomIdOrAlias } });
+ if (!roomSummarySupported) {
+ endpoint.path = `/_matrix/client/unstable/im.nheko.summary/summary/${encodeURIComponent(
+ roomIdOrAlias
+ )}`;
+ }
+ const response = await ct.requestAuthJSON(endpoint);
+ const [err, result] = await parseMatrixResp(response);
+ if (err) {
+ throw new Error(printMatrixError(err));
+ }
+ return result;
+ }, [ct, roomIdOrAlias, roomSummarySupported]);
const { data } = useQuery({
queryKey: [roomIdOrAlias, `summary`],
@@ -26,28 +40,35 @@ export function RoomSummaryLoader({ roomIdOrAlias, children }: RoomSummaryLoader
return children(data);
}
-export function LocalRoomSummaryLoader({
- room,
- children,
-}: {
- room: Room;
- children: (roomSummary: LocalRoomSummary) => ReactNode;
-}) {
- const summary = useLocalRoomSummary(room);
-
- return children(summary);
+export function LocalRoomSummaryLoader({ children }: { children: () => ReactNode }) {
+ return children();
}
-
export function HierarchyRoomSummaryLoader({
roomId,
children,
}: {
roomId: string;
- children: (state: AsyncState) => ReactNode;
+ children: (state: AsyncState) => ReactNode;
}) {
- const mx = useMatrixClient();
+ const ct = useClient();
- const fetchSummary = useCallback(() => mx.getRoomHierarchy(roomId, 1, 1), [mx, roomId]);
+ const fetchSummary = useCallback(async (): Promise => {
+ const response = await ct.requestAuthJSON(
+ getHierarchy({
+ pathParams: { roomId },
+ searchParams: {
+ limit: 1,
+ max_depth: 1,
+ },
+ })
+ );
+
+ const [err, result] = await parseMatrixResp(response);
+ if (err) {
+ throw new Error(printMatrixError(err));
+ }
+ return result;
+ }, [ct, roomId]);
const [errorMemo, setError] = useState();
const { data, error } = useQuery({
@@ -62,7 +83,7 @@ export function HierarchyRoomSummaryLoader({
},
});
- let state: AsyncState = {
+ let state: AsyncState = {
status: AsyncStatus.Loading,
};
if (error) {
@@ -78,7 +99,7 @@ export function HierarchyRoomSummaryLoader({
};
}
- const summary = data?.rooms[0] ?? undefined;
+ const summary = data?.rooms[0];
if (summary) {
state = {
status: AsyncStatus.Success,
diff --git a/src/app/components/RoomUnreadProvider.tsx b/src/app/components/RoomUnreadProvider.tsx
deleted file mode 100644
index 43dfd48b..00000000
--- a/src/app/components/RoomUnreadProvider.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-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);
-}
diff --git a/src/app/components/ServerConfigsLoader.tsx b/src/app/components/ServerConfigsLoader.tsx
index 3c8ce8eb..7fc0971b 100644
--- a/src/app/components/ServerConfigsLoader.tsx
+++ b/src/app/components/ServerConfigsLoader.tsx
@@ -1,48 +1,48 @@
import { ReactNode, useCallback, useMemo } from 'react';
-import { Capabilities, validateAuthMetadata, ValidatedAuthMetadata } from 'matrix-js-sdk';
+import { getCapabilities, GetCapabilitiesOK } from 'neurone/lib/api/matrix/client/v3/capabilities';
+import { getConfig, GetConfigOK } from 'neurone/lib/api/matrix/client/v1/media';
+import { getAuthMetadata, GetAuthMetadataOK } from 'neurone/lib/api/matrix/client/v1/authMetadata';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { AsyncStatus, useAsyncCallbackValue } from '../hooks/useAsyncCallback';
-import { useMatrixClient } from '../hooks/useMatrixClient';
-import { MediaConfig } from '../hooks/useMediaConfig';
import { promiseFulfilledResult } from '../utils/common';
+import { useClient } from '../hooks/useClient';
+import { MediaConfig } from '../hooks/useMediaConfig';
+import { Capabilities } from '../hooks/useCapabilities';
+import { getJSONRequest } from '../../client/request';
export type ServerConfigs = {
capabilities?: Capabilities;
mediaConfig?: MediaConfig;
- authMetadata?: ValidatedAuthMetadata;
+ authMetadata?: GetAuthMetadataOK;
};
type ServerConfigsLoaderProps = {
children: (configs: ServerConfigs) => ReactNode;
};
export function ServerConfigsLoader({ children }: ServerConfigsLoaderProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const fallbackConfigs = useMemo(() => ({}), []);
const [configsState] = useAsyncCallbackValue(
useCallback(async () => {
const result = await Promise.allSettled([
- mx.getCapabilities(),
- mx.getMediaConfig(),
- mx.getAuthMetadata(),
+ ct.requestAuthJSON(getCapabilities({})).then(parseMatrixResp),
+ ct.requestAuthJSON(getConfig({})).then(parseMatrixResp),
+ getJSONRequest(ct.session.baseUrl)(getAuthMetadata({})).then(
+ parseMatrixResp
+ ),
]);
- const capabilities = promiseFulfilledResult(result[0]);
- const mediaConfig = promiseFulfilledResult(result[1]);
- const authMetadata = promiseFulfilledResult(result[2]);
- let validatedAuthMetadata: ValidatedAuthMetadata | undefined;
-
- try {
- validatedAuthMetadata = validateAuthMetadata(authMetadata);
- } catch (e) {
- console.error(e);
- }
+ const [, capResult] = promiseFulfilledResult(result[0]) ?? [];
+ const [, mediaConfig] = promiseFulfilledResult(result[1]) ?? [];
+ const [, authMetadata] = promiseFulfilledResult(result[2]) ?? [];
return {
- capabilities,
+ capabilities: capResult?.capabilities,
mediaConfig,
- authMetadata: validatedAuthMetadata,
+ authMetadata,
};
- }, [mx])
+ }, [ct])
);
const configs: ServerConfigs =
diff --git a/src/app/components/SpecVersionsLoader.tsx b/src/app/components/SpecVersionsLoader.tsx
index 5ee47bdf..5b459c1d 100644
--- a/src/app/components/SpecVersionsLoader.tsx
+++ b/src/app/components/SpecVersionsLoader.tsx
@@ -2,6 +2,29 @@ import { ReactNode, useCallback, useEffect, useState } from 'react';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { SpecVersions, specVersions } from '../cs-api';
+const getSpecVersionKey = (baseUrl: string) => `specVersions:${baseUrl}`;
+const setSpecVersions = (baseUrl: string, data: SpecVersions) => {
+ localStorage.setItem(getSpecVersionKey(baseUrl), JSON.stringify(data));
+};
+
+const getSpecVersions = (baseUrl: string): SpecVersions | undefined => {
+ try {
+ const dataStr = localStorage.getItem(getSpecVersionKey(baseUrl));
+ if (dataStr === null) return undefined;
+ const data = JSON.parse(dataStr);
+ if (typeof data === 'object' && 'versions' in data && Array.isArray(data.versions)) {
+ return data;
+ }
+ return undefined;
+ } catch {
+ return undefined;
+ }
+};
+
+const FALLBACK_DATA: SpecVersions = {
+ versions: [],
+};
+
type SpecVersionsLoaderProps = {
baseUrl: string;
fallback?: () => ReactNode;
@@ -15,8 +38,15 @@ export function SpecVersionsLoader({
children,
}: SpecVersionsLoaderProps) {
const [state, load] = useAsyncCallback(
- useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
+ useCallback(async () => {
+ const data = await specVersions(fetch, baseUrl);
+ setSpecVersions(baseUrl, data);
+ return data;
+ }, [baseUrl])
);
+
+ const data = state.status === AsyncStatus.Success ? state.data : getSpecVersions(baseUrl);
+
const [ignoreError, setIgnoreError] = useState(false);
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
@@ -25,19 +55,13 @@ export function SpecVersionsLoader({
load();
}, [load]);
- if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
+ if (!data && (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading)) {
return fallback?.();
}
- if (!ignoreError && state.status === AsyncStatus.Error) {
+ if (!data && !ignoreError && state.status === AsyncStatus.Error) {
return error?.(state.error, load, ignoreCallback);
}
- return children(
- state.status === AsyncStatus.Success
- ? state.data
- : {
- versions: [],
- }
- );
+ return children(state.status === AsyncStatus.Success ? state.data : data ?? FALLBACK_DATA);
}
diff --git a/src/app/components/SupportedUIAFlowsLoader.tsx b/src/app/components/SupportedUIAFlowsLoader.tsx
index 442eb572..e3b2096a 100644
--- a/src/app/components/SupportedUIAFlowsLoader.tsx
+++ b/src/app/components/SupportedUIAFlowsLoader.tsx
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
-import { UIAFlow } from 'matrix-js-sdk';
import { useSupportedUIAFlows } from '../hooks/useUIAFlows';
+import { UIAFlow } from '../utils/matrix-uia';
export function SupportedUIAFlowsLoader({
flows,
diff --git a/src/app/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx
index 84b92231..80b655d4 100644
--- a/src/app/components/create-room/AdditionalCreatorInput.tsx
+++ b/src/app/components/create-room/AdditionalCreatorInput.tsx
@@ -27,19 +27,19 @@ import React, {
import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix';
import { useDirectUsers } from '../../hooks/useDirectUsers';
import { SettingTile } from '../setting-tile';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { stopPropagation } from '../../utils/keyboard';
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser';
+import { useClient } from '../../hooks/useClient';
export const useAdditionalCreators = (defaultCreators?: string[]) => {
- const mx = useMatrixClient();
+ const ct = useClient();
const [additionalCreators, setAdditionalCreators] = useState(
- () => defaultCreators?.filter((id) => id !== mx.getSafeUserId()) ?? []
+ () => defaultCreators?.filter((id) => id !== ct.session.userId) ?? []
);
const addAdditionalCreator = (userId: string) => {
- if (userId === mx.getSafeUserId()) return;
+ if (userId === ct.session.userId) return;
setAdditionalCreators((creators) => {
const creatorsSet = new Set(creators);
@@ -83,7 +83,7 @@ export function AdditionalCreatorInput({
onRemove,
disabled,
}: AdditionalCreatorInputProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const [menuCords, setMenuCords] = useState();
const directUsers = useDirectUsers();
@@ -156,7 +156,7 @@ export function AdditionalCreatorInput({
- {mx.getSafeUserId()}
+ {ct.session.userId}
{additionalCreators.map((creator) => (
(null);
const [aliasAvail, setAliasAvail] = useState>({
status: AsyncStatus.Idle,
@@ -31,18 +32,20 @@ export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) {
const checkAliasAvail = useAsync(
useCallback(
async (aliasLocalPart: string) => {
- const roomAlias = `#${aliasLocalPart}:${getMxIdServer(mx.getSafeUserId())}`;
- try {
- const result = await mx.getRoomIdForAlias(roomAlias);
- return typeof result.room_id !== 'string';
- } catch (e) {
- if (e instanceof MatrixError && e.httpStatus === 404) {
- return true;
- }
- throw e;
+ const roomAlias = `#${aliasLocalPart}:${getMxIdServer(ct.session.userId)}`;
+ const response = await ct.requestAuthJSON(
+ getRoom({
+ pathParams: { roomAlias },
+ })
+ );
+ const [err, result] = await parseMatrixResp(response);
+ if (err) {
+ if (err.status === 404) return true;
+ throw new Error(printMatrixError(err));
}
+ return typeof result.room_id !== 'string';
},
- [mx]
+ [ct]
),
setAliasAvail
);
@@ -94,7 +97,7 @@ export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) {
}
after={
- :{getMxIdServer(mx.getSafeUserId())}
+ :{getMxIdServer(ct.session.userId)}
}
onKeyDown={handleAliasKeyDown}
diff --git a/src/app/components/create-room/utils.ts b/src/app/components/create-room/utils.ts
index a0ca7488..b0de4d21 100644
--- a/src/app/components/create-room/utils.ts
+++ b/src/app/components/create-room/utils.ts
@@ -1,18 +1,19 @@
import {
- ICreateRoomOpts,
- ICreateRoomStateEvent,
- JoinRule,
- MatrixClient,
- RestrictedAllowType,
- Room,
-} from 'matrix-js-sdk';
-import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
+ CreateRoomStateEvent,
+ postCreateRoom,
+ PostCreateRoomBody,
+ PostCreateRoomOK,
+} from 'neurone/lib/api/matrix/client/v3/createRoom';
+import { MatrixResponseResult, parseMatrixResp } from 'neurone/lib/matrix-request';
+import { putStateEvent } from 'neurone/lib/api/matrix/client/v3/rooms';
import { CreateRoomKind } from './CreateRoomKindSelector';
-import { RoomType, StateEvent } from '../../../types/matrix/room';
-import { getViaServers } from '../../plugins/via-servers';
+import { JoinRulesContent, RoomType, StateEvent, StateEventType } from '../../../types/matrix/room';
import { getMxIdServer } from '../../utils/matrix';
+import { ClientContext } from '../../../client/context';
+import { getViaServers } from '../../plugins/via-servers';
+import { getJoinRoomStateId } from '../../../client/storage/collections';
-export const createRoomCreationContent = (
+const createRoomCreationContent = (
type: RoomType | undefined,
allowFederation: boolean,
additionalCreators: string[] | undefined
@@ -31,28 +32,28 @@ export const createRoomCreationContent = (
return content;
};
-export const createRoomJoinRulesState = (
+const createRoomJoinRulesState = (
kind: CreateRoomKind,
- parent: Room | undefined,
+ parentId: string | undefined,
knock: boolean
) => {
- let content: RoomJoinRulesEventContent = {
- join_rule: knock ? JoinRule.Knock : JoinRule.Invite,
+ let content: JoinRulesContent = {
+ join_rule: knock ? 'knock' : 'invite',
};
if (kind === CreateRoomKind.Public) {
content = {
- join_rule: JoinRule.Public,
+ join_rule: 'public',
};
}
- if (kind === CreateRoomKind.Restricted && parent) {
+ if (kind === CreateRoomKind.Restricted && parentId) {
content = {
- join_rule: knock ? ('knock_restricted' as JoinRule) : JoinRule.Restricted,
+ join_rule: knock ? 'knock_restricted' : 'restricted',
allow: [
{
- type: RestrictedAllowType.RoomMembership,
- room_id: parent.roomId,
+ type: [StateEventType.RoomMember],
+ room_id: parentId,
},
],
};
@@ -65,12 +66,12 @@ export const createRoomJoinRulesState = (
};
};
-export const createRoomParentState = (parent: Room) => ({
+const createRoomParentState = (parentId: string, via: string[]) => ({
type: StateEvent.SpaceParent,
- state_key: parent.roomId,
+ state_key: parentId,
content: {
canonical: true,
- via: getViaServers(parent),
+ via,
},
});
@@ -85,7 +86,7 @@ export const createRoomEncryptionState = () => ({
export type CreateRoomData = {
version: string;
type?: RoomType;
- parent?: Room;
+ parentId?: string;
kind: CreateRoomKind;
name: string;
topic?: string;
@@ -95,20 +96,29 @@ export type CreateRoomData = {
allowFederation: boolean;
additionalCreators?: string[];
};
-export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise => {
- const initialState: ICreateRoomStateEvent[] = [];
+export const createRoom = async (
+ ct: ClientContext,
+ data: CreateRoomData
+): Promise> => {
+ const initialState: CreateRoomStateEvent[] = [];
if (data.encryption) {
initialState.push(createRoomEncryptionState());
}
- if (data.parent) {
- initialState.push(createRoomParentState(data.parent));
+ if (data.parentId) {
+ const creators = ct.roomToCreators.read.get(data.parentId);
+ const powerLevelsObj = await ct.storage.joinRoomState.read.get(
+ getJoinRoomStateId(data.parentId, StateEventType.RoomPowerLevels, '')
+ );
+ const powerLevelsContent = powerLevelsObj?.event.content;
+ const via = getViaServers(creators, powerLevelsContent);
+ initialState.push(createRoomParentState(data.parentId, via));
}
- initialState.push(createRoomJoinRulesState(data.kind, data.parent, data.knock));
+ initialState.push(createRoomJoinRulesState(data.kind, data.parentId, data.knock));
- const options: ICreateRoomOpts = {
+ const body: PostCreateRoomBody = {
room_version: data.version,
name: data.name,
topic: data.topic,
@@ -121,20 +131,31 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
initial_state: initialState,
};
- const result = await mx.createRoom(options);
+ const response = await ct.requestAuthJSON(postCreateRoom({ body }));
- if (data.parent) {
- await mx.sendStateEvent(
- data.parent.roomId,
- StateEvent.SpaceChild as any,
- {
- auto_join: false,
- suggested: false,
- via: [getMxIdServer(mx.getUserId() ?? '') ?? ''],
- },
- result.room_id
- );
+ const [err, result] = await parseMatrixResp(response);
+
+ if (err) {
+ return [err, undefined];
}
- return result.room_id;
+ if (data.parentId) {
+ const resp = await ct.requestAuthJSON(
+ putStateEvent({
+ pathParams: {
+ roomId: data.parentId,
+ eventType: StateEventType.SpaceChild,
+ stateKey: result.room_id,
+ },
+ body: {
+ auto_join: false,
+ suggested: false,
+ via: [getMxIdServer(ct.session.userId) ?? ''],
+ },
+ })
+ );
+ await parseMatrixResp(resp);
+ }
+
+ return [undefined, result.room_id];
};
diff --git a/src/app/components/editor/Elements.tsx b/src/app/components/editor/Elements.tsx
index 675c4542..52371d5b 100644
--- a/src/app/components/editor/Elements.tsx
+++ b/src/app/components/editor/Elements.tsx
@@ -10,11 +10,11 @@ import {
import * as css from '../../styles/CustomHtml.css';
import { CommandElement, EmoticonElement, LinkElement, MentionElement } from './slate';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getBeginCommand } from './utils';
import { BlockType } from './types';
-import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useClient } from '../../hooks/useClient';
+import { mxcToHttp } from '../../../client/mxcToHttp';
// Put this at the start and end of an inline component to work around this Chromium bug:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
@@ -77,7 +77,7 @@ function RenderEmoticonElement({
element,
children,
}: { element: EmoticonElement } & RenderElementProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const selected = useSelected();
const focused = useFocused();
@@ -93,7 +93,7 @@ function RenderEmoticonElement({
{element.key.startsWith('mxc://') ? (
) : (
diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
index cc0dff19..9f687ef8 100644
--- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
@@ -1,11 +1,8 @@
import React, { KeyboardEvent as ReactKeyboardEvent, useEffect, useMemo } from 'react';
import { Editor } from 'slate';
import { Box, MenuItem, Text, toRem } from 'folds';
-import { Room } from 'matrix-js-sdk';
-
import { AutocompleteQuery } from './autocompleteQuery';
import { AutocompleteMenu } from './AutocompleteMenu';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch';
import { onTabPress } from '../../../utils/keyboard';
import { createEmoticonElement, moveCursor, replaceWithElement } from '../utils';
@@ -13,17 +10,18 @@ import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
import { useRelevantImagePacks } from '../../../hooks/useImagePacks';
import { IEmoji, emojis } from '../../../plugins/emoji';
import { useKeyDown } from '../../../hooks/useKeyDown';
-import { mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { ImageUsage, PackImageReader } from '../../../plugins/custom-emoji';
import { getEmoticonSearchStr } from '../../../plugins/utils';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
+import { useClient } from '../../../hooks/useClient';
type EmoticonCompleteHandler = (key: string, shortcode: string) => void;
type EmoticonSearchItem = PackImageReader | IEmoji;
type EmoticonAutocompleteProps = {
- imagePackRooms: Room[];
+ imagePackRooms: string[];
editor: Editor;
query: AutocompleteQuery;
requestClose: () => void;
@@ -41,11 +39,11 @@ export function EmoticonAutocomplete({
query,
requestClose,
}: EmoticonAutocompleteProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
- const imagePacks = useRelevantImagePacks(ImageUsage.Emoticon, imagePackRooms);
- const recentEmoji = useRecentEmoji(mx, 20);
+ const [imagePacks] = useRelevantImagePacks(ImageUsage.Emoticon, imagePackRooms);
+ const recentEmojiResult = useRecentEmoji(20);
const searchList = useMemo(() => {
const list: Array = [];
@@ -60,7 +58,9 @@ export function EmoticonAutocomplete({
getEmoticonSearchStr,
SEARCH_OPTIONS
);
- const autoCompleteEmoticon = result ? result.items.slice(0, 20) : recentEmoji;
+ const autoCompleteEmoticon: EmoticonSearchItem[] = result
+ ? result.items.slice(0, 20)
+ : recentEmojiResult.value ?? [];
useEffect(() => {
if (query.text) search(query.text);
@@ -102,7 +102,7 @@ export function EmoticonAutocomplete({
diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
index b0c64f60..70abcaa9 100644
--- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
@@ -1,30 +1,34 @@
-import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect } from 'react';
+import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo } from 'react';
import { Editor } from 'slate';
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 { getDirectRoomAvatarUrl } from '../../../utils/room';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AutocompleteQuery } from './autocompleteQuery';
import { AutocompleteMenu } from './AutocompleteMenu';
-import { getMxIdServer, isRoomAlias } from '../../../utils/matrix';
+import { getMxIdServer, isRoomAlias, isRoomId } 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 { factoryRoomIdByAtoZ } from '../../../utils/sort';
import { RoomAvatar, RoomIcon } from '../../room-avatar';
import { getViaServers } from '../../../plugins/via-servers';
+import { useClient } from '../../../hooks/useClient';
+import { useRoomHeroes } from '../../../hooks/storage/useRoomHeros';
+import { useGetRoomName } from '../../../hooks/storage/useGetRoomName';
+import { useGetRoomAvatar } from '../../../hooks/storage/useGetRoomAvatar';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { nameInitials } from '../../../utils/common';
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
-const roomAliasFromQueryText = (mx: MatrixClient, text: string) =>
+const roomAliasFromQueryText = (myUserId: string, text: string) =>
isRoomAlias(`#${text}`)
? `#${text}`
- : `#${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`;
+ : `#${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(myUserId)}`;
function UnknownRoomMentionItem({
query,
@@ -33,8 +37,8 @@ function UnknownRoomMentionItem({
query: AutocompleteQuery;
handleAutocomplete: MentionAutoCompleteHandler;
}) {
- const mx = useMatrixClient();
- const roomAlias: string = roomAliasFromQueryText(mx, query.text);
+ const ct = useClient();
+ const roomAlias: string = roomAliasFromQueryText(ct.session.userId, query.text);
const handleSelect = () => handleAutocomplete(roomAlias, roomAlias);
@@ -76,22 +80,30 @@ export function RoomMentionAutocomplete({
query,
requestClose,
}: RoomMentionAutocompleteProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
+ const useAuthentication = useMediaAuthentication();
const mDirects = useAtomValue(mDirectAtom);
+ const rooms = useAtomValue(allRoomsAtom);
- const allRooms = useAtomValue(allRoomsAtom).sort(factoryRoomIdByActivity(mx));
+ const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(rooms);
+ const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
+ const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
+ const allRooms = useMemo(
+ () => Array.from(rooms).sort(factoryRoomIdByAtoZ(getRoomName)),
+ [rooms, getRoomName]
+ );
const [result, search, resetSearch] = useAsyncSearch(
allRooms,
useCallback(
(rId) => {
- const r = mx.getRoom(rId);
- if (!r) return 'Unknown Room';
- const alias = r.getCanonicalAlias();
- if (alias) return [r.name, alias];
- return r.name;
+ const name = getRoomName(rId);
+ if (!name) return 'Unknown Room';
+ const alias = ct.roomToAlias.read.get(rId);
+ if (alias) return [name, alias];
+ return name;
},
- [mx]
+ [ct, getRoomName]
),
SEARCH_OPTIONS
);
@@ -104,12 +116,14 @@ export function RoomMentionAutocomplete({
}, [query.text, search, resetSearch]);
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
- const mentionRoom = mx.getRoom(roomAliasOrId);
- const viaServers = mentionRoom ? getViaServers(mentionRoom) : undefined;
+ const creators = isRoomId(roomAliasOrId)
+ ? ct.roomToCreators.read.get(roomAliasOrId)
+ : undefined;
+ const viaServers = creators ? getViaServers(creators) : undefined;
const mentionEl = createMentionElement(
roomAliasOrId,
name.startsWith('#') ? name : `#${name}`,
- roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId,
+ roomId === roomAliasOrId || ct.roomToAlias.read.get(roomId) === roomAliasOrId,
undefined,
viaServers
);
@@ -121,14 +135,13 @@ export function RoomMentionAutocomplete({
useKeyDown(window, (evt: KeyboardEvent) => {
onTabPress(evt, () => {
if (autoCompleteRoomIds.length === 0) {
- const alias = roomAliasFromQueryText(mx, query.text);
+ const alias = roomAliasFromQueryText(ct.session.userId, query.text);
handleAutocomplete(alias, alias);
return;
}
const rId = autoCompleteRoomIds[0];
- const r = mx.getRoom(rId);
- const name = r?.name ?? rId;
- handleAutocomplete(r?.getCanonicalAlias() ?? rId, name);
+ const name = getRoomName(rId) ?? 'Unknown';
+ handleAutocomplete(ct.roomToAlias.read.get(rId) ?? rId, name);
});
});
@@ -138,11 +151,19 @@ export function RoomMentionAutocomplete({
) : (
autoCompleteRoomIds.map((rId) => {
- const room = mx.getRoom(rId);
- if (!room) return null;
- const dm = mDirects.has(room.roomId);
+ const dm = mDirects.has(rId);
+ const name = getRoomName(rId) ?? 'Unknown';
+ const alias = ct.roomToAlias.read.get(rId);
+ const avatarMxc = getRoomAvatar(rId);
+ const avatar = avatarMxc
+ ? mxcToHttp(ct.session.baseUrl, avatarMxc, useAuthentication).thumbnail({
+ width: 32,
+ height: 32,
+ method: 'crop',
+ })
+ : undefined;
- const handleSelect = () => handleAutocomplete(room.getCanonicalAlias() ?? rId, room.name);
+ const handleSelect = () => handleAutocomplete(alias ?? rId, name);
return (
}
before={
{dm ? (
(
-
+
+ {nameInitials(name)}
+
)}
/>
) : (
-
+
)}
}
>
- {room.name}
+ {name}
);
diff --git a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx
index 7a8012eb..1d56325d 100644
--- a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx
@@ -1,32 +1,29 @@
-import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react';
+import React, { useEffect, KeyboardEvent as ReactKeyboardEvent, useCallback } from 'react';
import { Editor } from 'slate';
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
-import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import { AutocompleteQuery } from './autocompleteQuery';
import { AutocompleteMenu } from './AutocompleteMenu';
-import { useRoomMembers } from '../../../hooks/useRoomMembers';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
-import {
- SearchItemStrGetter,
- UseAsyncSearchOptions,
- useAsyncSearch,
-} from '../../../hooks/useAsyncSearch';
+import { GetRoomMemberMembershipCallback, useRoomMembersAPI } from '../../../hooks/useRoomMembers';
+import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch';
import { onTabPress } from '../../../utils/keyboard';
import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
import { useKeyDown } from '../../../hooks/useKeyDown';
-import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix';
-import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
+import { getMxIdServer, isUserId } from '../../../utils/matrix';
+import { getMemberSearchStr } from '../../../utils/room';
import { UserAvatar } from '../../user-avatar';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
-import { Membership } from '../../../../types/matrix/room';
+import { useClient } from '../../../hooks/useClient';
+import { JoinRoomMemberObject } from '../../../../client/storage/collections';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
+import { nameInitials } from '../../../utils/common';
type MentionAutoCompleteHandler = (userId: string, name: string) => void;
-const userIdFromQueryText = (mx: MatrixClient, text: string) =>
+const userIdFromQueryText = (myUserId: string, text: string) =>
isUserId(`@${text}`)
? `@${text}`
- : `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`;
+ : `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(myUserId)}`;
function UnknownMentionItem({
userId,
@@ -62,16 +59,18 @@ function UnknownMentionItem({
}
type UserMentionAutocompleteProps = {
- room: Room;
+ roomId: string;
editor: Editor;
query: AutocompleteQuery;
requestClose: () => void;
};
-const withAllowedMembership = (member: RoomMember): boolean =>
- member.membership === Membership.Join ||
- member.membership === Membership.Invite ||
- member.membership === Membership.Knock;
+const withAllowedMembership =
+ (getMembership: GetRoomMemberMembershipCallback) =>
+ (member: JoinRoomMemberObject): boolean =>
+ getMembership(member.userId) === 'join' ||
+ getMembership(member.userId) === 'invite' ||
+ getMembership(member.userId) === 'knock';
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
@@ -80,25 +79,28 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = {
},
};
-const mxIdToName = (mxId: string) => getMxIdLocalPart(mxId) ?? mxId;
-const getRoomMemberStr: SearchItemStrGetter = (m, query) =>
- getMemberSearchStr(m, query, mxIdToName);
+const EMPTY_MEMBER_LIST: JoinRoomMemberObject[] = [];
export function UserMentionAutocomplete({
- room,
+ roomId,
editor,
query,
requestClose,
}: UserMentionAutocompleteProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
- const roomId: string = room.roomId!;
- const roomAliasOrId = room.getCanonicalAlias() || roomId;
- const members = useRoomMembers(mx, roomId);
+ const roomAliasOrId = ct.roomToAlias.read.get(roomId) || roomId;
+ const membersAPI = useRoomMembersAPI();
+ const members = membersAPI.memberEventsResult.value ?? EMPTY_MEMBER_LIST;
+
+ const [result, search, resetSearch] = useAsyncSearch(
+ members,
+ useCallback((m, q) => getMemberSearchStr(m, q, membersAPI.getName), [membersAPI.getName]),
+ SEARCH_OPTIONS
+ );
- const [result, search, resetSearch] = useAsyncSearch(members, getRoomMemberStr, SEARCH_OPTIONS);
const autoCompleteMembers = (result ? result.items.slice(0, 20) : members.slice(0, 20)).filter(
- withAllowedMembership
+ withAllowedMembership(membersAPI.getMembership)
);
useEffect(() => {
@@ -110,7 +112,7 @@ export function UserMentionAutocomplete({
const mentionEl = createMentionElement(
uId,
name.startsWith('@') ? name : `@${name}`,
- mx.getUserId() === uId || roomAliasOrId === uId
+ uId === ct.session.userId || roomAliasOrId === uId
);
replaceWithElement(editor, query.range, mentionEl);
moveCursor(editor, true);
@@ -124,18 +126,15 @@ export function UserMentionAutocomplete({
return;
}
if (autoCompleteMembers.length === 0) {
- const userId = userIdFromQueryText(mx, query.text);
+ const userId = userIdFromQueryText(ct.session.userId, query.text);
handleAutocomplete(userId, userId);
return;
}
const roomMember = autoCompleteMembers[0];
- handleAutocomplete(roomMember.userId, roomMember.name);
+ handleAutocomplete(roomMember.userId, membersAPI.getName(roomMember.userId));
});
});
- const getName = (member: RoomMember) =>
- getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
-
return (
Mentions} requestClose={requestClose}>
{query.text === 'room' && (
@@ -147,15 +146,20 @@ export function UserMentionAutocomplete({
)}
{autoCompleteMembers.length === 0 ? (
) : (
autoCompleteMembers.map((roomMember) => {
- const avatarMxcUrl = roomMember.getMxcAvatarUrl();
+ const name = membersAPI.getName(roomMember.userId);
+ const avatarMxcUrl = membersAPI.getAvatar(roomMember.userId);
const avatarUrl = avatarMxcUrl
- ? mx.mxcUrlToHttp(avatarMxcUrl, 32, 32, 'crop', undefined, false, useAuthentication)
+ ? mxcToHttp(ct.session.baseUrl, avatarMxcUrl, useAuthentication).thumbnail({
+ width: 32,
+ height: 32,
+ method: 'crop',
+ })
: undefined;
return (
);
diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts
index dbdd51f3..76454885 100644
--- a/src/app/components/editor/output.ts
+++ b/src/app/components/editor/output.ts
@@ -1,5 +1,4 @@
import { Descendant, Editor, Text } from 'slate';
-import { MatrixClient } from 'matrix-js-sdk';
import { sanitizeText } from '../../utils/sanitize';
import { BlockType } from './types';
import { CustomElement } from './slate';
@@ -11,7 +10,8 @@ import {
} from '../../plugins/markdown';
import { findAndReplace } from '../../utils/findAndReplace';
import { sanitizeForRegex } from '../../utils/regex';
-import { getCanonicalAliasOrRoomId, isUserId } from '../../utils/matrix';
+import { isUserId } from '../../utils/matrix';
+import { ClientContext } from '../../../client/context';
export type OutputOptions = {
allowTextFormatting?: boolean;
@@ -201,7 +201,7 @@ export type MentionsData = {
room: boolean;
users: Set;
};
-export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): MentionsData => {
+export const getMentions = (ct: ClientContext, editor: Editor): MentionsData => {
const mentionData: MentionsData = {
room: false,
users: new Set(),
@@ -212,10 +212,10 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
if (node.type === BlockType.CodeBlock) return;
if (node.type === BlockType.Mention) {
- if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) {
+ if (node.name === '@room') {
mentionData.room = true;
}
- if (isUserId(node.id) && node.id !== mx.getUserId()) {
+ if (isUserId(node.id) && node.id !== ct.session.userId) {
mentionData.users.add(node.id);
}
diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx
index 3db27e2a..eab41f9f 100644
--- a/src/app/components/emoji-board/EmojiBoard.tsx
+++ b/src/app/components/emoji-board/EmojiBoard.tsx
@@ -12,7 +12,6 @@ import React, {
import { Box, config, Icons, Scroll } from 'folds';
import FocusTrap from 'focus-trap-react';
import { isKeyHotkey } from 'is-hotkey';
-import { Room } from 'matrix-js-sdk';
import { atom, PrimitiveAtom, useAtom, useSetAtom } from 'jotai';
import { useVirtualizer } from '@tanstack/react-virtual';
import { IEmoji, emojiGroups, emojis } from '../../plugins/emoji';
@@ -20,9 +19,8 @@ import { useEmojiGroupLabels } from './useEmojiGroupLabels';
import { useEmojiGroupIcons } from './useEmojiGroupIcons';
import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard';
import { useRelevantImagePacks } from '../../hooks/useImagePacks';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../hooks/useRecentEmoji';
-import { isUserId, mxcUrlToHttp } from '../../utils/matrix';
+import { isUserId } from '../../utils/matrix';
import { editableActiveElement, targetFromEvent } from '../../utils/dom';
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
import { useDebounce } from '../../hooks/useDebounce';
@@ -52,6 +50,10 @@ import {
} from './components';
import { EmojiBoardTab, EmojiType } from './types';
import { VirtualTile } from '../virtualizer';
+import { useClient } from '../../hooks/useClient';
+import { useRoomHeroes } from '../../hooks/storage/useRoomHeros';
+import { GetRoomNameCallback, useGetRoomName } from '../../hooks/storage/useGetRoomName';
+import { mxcToHttp } from '../../../client/mxcToHttp';
const RECENT_GROUP_ID = 'recent_group';
const SEARCH_GROUP_ID = 'search_group';
@@ -69,11 +71,10 @@ type StickerGroupItem = {
const useGroups = (
tab: EmojiBoardTab,
- imagePacks: ImagePack[]
+ imagePacks: ImagePack[],
+ getRoomName: GetRoomNameCallback
): [EmojiGroupItem[], StickerGroupItem[]] => {
- const mx = useMatrixClient();
-
- const recentEmojis = useRecentEmoji(mx, 21);
+ const recentEmojisResult = useRecentEmoji(21);
const labels = useEmojiGroupLabels();
const emojiGroupItems = useMemo(() => {
@@ -83,12 +84,12 @@ const useGroups = (
g.push({
id: RECENT_GROUP_ID,
name: 'Recent',
- items: recentEmojis,
+ items: recentEmojisResult.value ?? [],
});
imagePacks.forEach((pack) => {
let label = pack.meta.name;
- if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
+ if (!label) label = isUserId(pack.id) ? 'Personal Pack' : getRoomName(pack.id);
g.push({
id: pack.id,
@@ -108,7 +109,7 @@ const useGroups = (
});
return g;
- }, [mx, recentEmojis, labels, imagePacks, tab]);
+ }, [getRoomName, recentEmojisResult, labels, imagePacks, tab]);
const stickerGroupItems = useMemo(() => {
const g: StickerGroupItem[] = [];
@@ -116,7 +117,7 @@ const useGroups = (
imagePacks.forEach((pack) => {
let label = pack.meta.name;
- if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
+ if (!label) label = isUserId(pack.id) ? 'Personal Pack' : getRoomName(pack.id);
g.push({
id: pack.id,
@@ -128,37 +129,24 @@ const useGroups = (
});
return g;
- }, [mx, imagePacks, tab]);
+ }, [getRoomName, imagePacks, tab]);
return [emojiGroupItems, stickerGroupItems];
};
const useItemRenderer = (tab: EmojiBoardTab) => {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const renderItem = (emoji: IEmoji | PackImageReader, index: number) => {
if ('unicode' in emoji) {
return ;
}
+ const url = mxcToHttp(ct.session.baseUrl, emoji.url, useAuthentication).url();
if (tab === EmojiBoardTab.Sticker) {
- return (
-
- );
+ return ;
}
- return (
-
- );
+ return ;
};
return renderItem;
@@ -168,9 +156,10 @@ type EmojiSidebarProps = {
activeGroupAtom: PrimitiveAtom;
packs: ImagePack[];
onScrollToGroup: (groupId: string) => void;
+ getRoomName: GetRoomNameCallback;
};
-function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarProps) {
- const mx = useMatrixClient();
+function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup, getRoomName }: EmojiSidebarProps) {
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const [activeGroupId, setActiveGroupId] = useAtom(activeGroupAtom);
@@ -199,11 +188,12 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
{packs.map((pack) => {
let label = pack.meta.name;
- if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
+ if (!label) label = isUserId(pack.id) ? 'Personal Pack' : getRoomName(pack.id);
- const url =
- mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ||
- pack.meta.avatar;
+ const avatarMxc = pack.getAvatarUrl(usage);
+ const url = avatarMxc
+ ? mxcToHttp(ct.session.baseUrl, avatarMxc, useAuthentication).url()
+ : undefined;
return (
;
packs: ImagePack[];
onScrollToGroup: (groupId: string) => void;
+ getRoomName: GetRoomNameCallback;
};
-function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSidebarProps) {
- const mx = useMatrixClient();
+function StickerSidebar({
+ activeGroupAtom,
+ packs,
+ onScrollToGroup,
+ getRoomName,
+}: StickerSidebarProps) {
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const [activeGroupId, setActiveGroupId] = useAtom(activeGroupAtom);
@@ -263,10 +259,12 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide
{packs.map((pack) => {
let label = pack.meta.name;
- if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
+ if (!label) label = isUserId(pack.id) ? 'Personal Pack' : getRoomName(pack.id);
- const url =
- mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar;
+ const avatarMxc = pack.getAvatarUrl(usage);
+ const url = avatarMxc
+ ? mxcToHttp(ct.session.baseUrl, avatarMxc, useAuthentication).url()
+ : undefined;
return (
void;
- imagePackRooms: Room[];
+ imagePackRooms: string[];
requestClose: () => void;
returnFocusOnDeactivate?: boolean;
onEmojiSelect?: (unicode: string, shortcode: string) => void;
@@ -377,7 +375,9 @@ export function EmojiBoard({
allowTextCustomEmoji,
addToRecentEmoji = true,
}: EmojiBoardProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
+ const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(imagePackRooms);
+ const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
const emojiTab = tab === EmojiBoardTab.Emoji;
const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker;
@@ -388,8 +388,8 @@ export function EmojiBoard({
);
const activeGroupIdAtom = useMemo(() => atom(undefined), []);
const setActiveGroupId = useSetAtom(activeGroupIdAtom);
- const imagePacks = useRelevantImagePacks(usage, imagePackRooms);
- const [emojiGroupItems, stickerGroupItems] = useGroups(tab, imagePacks);
+ const [imagePacks, loading] = useRelevantImagePacks(usage, imagePackRooms);
+ const [emojiGroupItems, stickerGroupItems] = useGroups(tab, imagePacks, getRoomName);
const groups = emojiTab ? emojiGroupItems : stickerGroupItems;
const renderItem = useItemRenderer(tab);
@@ -438,7 +438,7 @@ export function EmojiBoard({
if (emojiInfo.type === EmojiType.Emoji) {
onEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode);
if (!evt.altKey && !evt.shiftKey && addToRecentEmoji) {
- addRecentEmoji(mx, emojiInfo.data);
+ addRecentEmoji(ct, emojiInfo.data);
}
}
if (emojiInfo.type === EmojiType.CustomEmoji) {
@@ -522,57 +522,61 @@ export function EmojiBoard({
activeGroupAtom={activeGroupIdAtom}
packs={imagePacks}
onScrollToGroup={handleScrollToGroup}
+ getRoomName={getRoomName}
/>
) : (
)
}
>
-
- {searchedItems && (
-
- {searchedItems.map(renderItem)}
-
- )}
-
- {vItems.map((vItem) => {
- const group = groups[vItem.index];
+ {searchedItems && (
+
+ {searchedItems.map(renderItem)}
+
+ )}
+
+ {vItems.map((vItem) => {
+ const group = groups[vItem.index];
- return (
-
-
- {group.items.map(renderItem)}
-
-
- );
- })}
-
- {tab === EmojiBoardTab.Sticker && groups.length === 0 &&
}
-
+ return (
+
+
+ {group.items.map(renderItem)}
+
+
+ );
+ })}
+
+ {tab === EmojiBoardTab.Sticker && groups.length === 0 && }
+
+ )}
diff --git a/src/app/components/emoji-board/components/Item.tsx b/src/app/components/emoji-board/components/Item.tsx
index c3fd3c3a..021ce1b2 100644
--- a/src/app/components/emoji-board/components/Item.tsx
+++ b/src/app/components/emoji-board/components/Item.tsx
@@ -1,11 +1,9 @@
import React from 'react';
import { Box } from 'folds';
-import { MatrixClient } from 'matrix-js-sdk';
import { EmojiItemInfo, EmojiType } from '../types';
import * as css from './styles.css';
import { PackImageReader } from '../../../plugins/custom-emoji';
import { IEmoji } from '../../../plugins/emoji';
-import { mxcUrlToHttp } from '../../../utils/matrix';
export const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => {
const label = element.getAttribute('title');
@@ -46,11 +44,10 @@ export function EmojiItem({ emoji }: EmojiItemProps) {
}
type CustomEmojiItemProps = {
- mx: MatrixClient;
- useAuthentication?: boolean;
+ url?: string;
image: PackImageReader;
};
-export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiItemProps) {
+export function CustomEmojiItem({ url, image }: CustomEmojiItemProps) {
return (
);
}
type StickerItemProps = {
- mx: MatrixClient;
- useAuthentication?: boolean;
+ url?: string;
image: PackImageReader;
};
-export function StickerItem({ mx, useAuthentication, image }: StickerItemProps) {
+export function StickerItem({ url, image }: StickerItemProps) {
return (
);
diff --git a/src/app/components/emoji-board/components/Preview.tsx b/src/app/components/emoji-board/components/Preview.tsx
index 3f5f8d3a..093aa455 100644
--- a/src/app/components/emoji-board/components/Preview.tsx
+++ b/src/app/components/emoji-board/components/Preview.tsx
@@ -2,9 +2,9 @@ import { Box, Text } from 'folds';
import React from 'react';
import { Atom, atom, useAtomValue } from 'jotai';
import * as css from './styles.css';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
-import { mxcUrlToHttp } from '../../../utils/matrix';
+import { useClient } from '../../../hooks/useClient';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
export type PreviewData = {
key: string;
@@ -18,7 +18,7 @@ type PreviewProps = {
previewAtom: Atom;
};
export function Preview({ previewAtom }: PreviewProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const { key, shortcode } = useAtomValue(previewAtom) ?? {};
@@ -37,7 +37,7 @@ export function Preview({ previewAtom }: PreviewProps) {
{key.startsWith('mxc://') ? (
) : (
diff --git a/src/app/components/encrypted-event/EncryptedEvent.tsx b/src/app/components/encrypted-event/EncryptedEvent.tsx
new file mode 100644
index 00000000..692aa725
--- /dev/null
+++ b/src/app/components/encrypted-event/EncryptedEvent.tsx
@@ -0,0 +1,109 @@
+import React, { ReactNode } from 'react';
+import { as, Box, color, config, Icon, Icons, Text } from 'folds';
+import { MessageEventWithoutRoomId } from 'neurone/lib/api/types';
+import {
+ DecryptionManager,
+ EventDecryptionIdle,
+ EventDecryptionPending,
+ EventDecryptionSuccess,
+} from '../../../client/decryption';
+import { useObservableValue } from '../../hooks/useObservableValue';
+
+export const MessageDecryptionIdle = as<'div', { children?: never }>(({ ...props }, ref) => (
+
+
+ Decrypt message event to view.
+
+));
+
+export const MessageDecryptionPending = as<'div', { children?: never }>(({ ...props }, ref) => (
+
+
+ Waiting for message to be decrypted...
+
+));
+
+export const MessageDecryptionError = as<'div', { message: string; children?: never }>(
+ ({ message, ...props }, ref) => (
+
+
+ {message}
+
+ )
+);
+
+type EncryptedMessageEventProps = {
+ decryptionManager: DecryptionManager;
+ roomId: string;
+ event: MessageEventWithoutRoomId;
+ children: (decryptedEvent: MessageEventWithoutRoomId) => ReactNode;
+};
+export function EncryptedMessageEvent({
+ decryptionManager,
+ roomId,
+ event,
+ children,
+}: EncryptedMessageEventProps) {
+ const encryptedEvent = decryptionManager.decryptableMessageEvent(roomId, event);
+ if (encryptedEvent.decryption.read instanceof EventDecryptionIdle) {
+ encryptedEvent.decrypt();
+ }
+ const decryption = useObservableValue(encryptedEvent.decryption);
+
+ if (decryption instanceof EventDecryptionIdle) {
+ return (
+
+
+
+ );
+ }
+
+ if (decryption instanceof EventDecryptionPending) {
+ return (
+
+
+
+ );
+ }
+
+ if (decryption instanceof EventDecryptionSuccess) {
+ return children(decryption.event);
+ }
+
+ const { cause, error } = decryption;
+
+ if (cause) {
+ return (
+
+
+
+ );
+ }
+
+ if (!error) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/components/encrypted-event/index.ts b/src/app/components/encrypted-event/index.ts
new file mode 100644
index 00000000..0a307315
--- /dev/null
+++ b/src/app/components/encrypted-event/index.ts
@@ -0,0 +1 @@
+export * from './EncryptedEvent';
diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx
index c7900237..e4291e02 100644
--- a/src/app/components/event-readers/EventReaders.tsx
+++ b/src/app/components/event-readers/EventReaders.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import classNames from 'classnames';
import {
Avatar,
@@ -13,33 +13,46 @@ import {
as,
config,
} from 'folds';
-import { Room } from 'matrix-js-sdk';
-import { useRoomEventReaders } from '../../hooks/useRoomEventReaders';
-import { getMemberDisplayName } from '../../utils/room';
-import { getMxIdLocalPart } from '../../utils/matrix';
import * as css from './EventReaders.css';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { UserAvatar } from '../user-avatar';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { getMouseEventCords } from '../../utils/dom';
+import { useClient } from '../../hooks/useClient';
+import { useObservableValuePromise } from '../../hooks/useObservableValue';
+import { useRoomMembersAPI } from '../../hooks/useRoomMembers';
+import { mxcToHttp } from '../../../client/mxcToHttp';
+import { useRoom } from '../../hooks/useRoom';
export type EventReadersProps = {
- room: Room;
eventId: string;
requestClose: () => void;
};
export const EventReaders = as<'div', EventReadersProps>(
- ({ className, room, eventId, requestClose, ...props }, ref) => {
- const mx = useMatrixClient();
- const useAuthentication = useMediaAuthentication();
- const latestEventReaders = useRoomEventReaders(room, eventId);
- const openProfile = useOpenUserRoomProfile();
- const space = useSpaceOptionally();
+ ({ className, eventId, requestClose, ...props }, ref) => {
+ const ct = useClient();
+ const roomId = useRoom();
+ const membersAPI = useRoomMembersAPI();
- const getName = (userId: string) =>
- getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
+ const eventReceipt = useObservableValuePromise(
+ useMemo(
+ () => ct.storage.readReceipt.observableRead.queryIndex('eventId', eventId),
+ [ct, eventId]
+ )
+ );
+
+ const users: string[] = useMemo(() => {
+ const u = new Set();
+ eventReceipt.value?.forEach((r) => {
+ u.add(r.userId);
+ });
+ return Array.from(u);
+ }, [eventReceipt]);
+
+ const useAuthentication = useMediaAuthentication();
+ const openProfile = useOpenUserRoomProfile();
+ const spaceId = useSpaceOptionally();
return (
(
- {latestEventReaders.map((readerId) => {
- const name = getName(readerId);
- const avatarMxcUrl = room.getMember(readerId)?.getMxcAvatarUrl();
+ {users.map((readerId) => {
+ const name = membersAPI.getName(readerId);
+ const avatarMxcUrl = membersAPI.getAvatar(readerId);
const avatarUrl = avatarMxcUrl
- ? mx.mxcUrlToHttp(
- avatarMxcUrl,
- 100,
- 100,
- 'crop',
- undefined,
- false,
- useAuthentication
- )
+ ? mxcToHttp(ct.session.baseUrl, avatarMxcUrl, useAuthentication).thumbnail({
+ width: 96,
+ height: 96,
+ method: 'crop',
+ })
: undefined;
return (
@@ -81,8 +90,8 @@ export const EventReaders = as<'div', EventReadersProps>(
radii="400"
onClick={(event) => {
openProfile(
- room.roomId,
- space?.roomId,
+ roomId,
+ spaceId,
readerId,
getMouseEventCords(event.nativeEvent),
'Bottom'
diff --git a/src/app/components/image-pack-view/ImagePackContent.tsx b/src/app/components/image-pack-view/ImagePackContent.tsx
index a696ebe8..ff4c7dff 100644
--- a/src/app/components/image-pack-view/ImagePackContent.tsx
+++ b/src/app/components/image-pack-view/ImagePackContent.tsx
@@ -19,7 +19,7 @@ import * as css from './style.css';
import { useFilePicker } from '../../hooks/useFilePicker';
import { CompactUploadCardRenderer } from '../upload-card';
import { UploadSuccess } from '../../state/upload';
-import { getImageInfo, TUploadContent } from '../../utils/matrix';
+import { getImageInfo, UploadContent } from '../../utils/matrix';
import { getImageFileUrl, loadImageElement, renameFile } from '../../utils/dom';
import { replaceSpaceWithDash, suffixRename } from '../../utils/common';
import { getFileNameWithoutExt } from '../../utils/mimeTypes';
@@ -110,7 +110,7 @@ export const ImagePackContent = as<'div', ImagePackContentProps>(
[imagePack.meta]
);
- const handleUploadRemove = useCallback((file: TUploadContent) => {
+ const handleUploadRemove = useCallback((file: UploadContent) => {
setFiles((fs) => fs.filter((f) => f !== file));
}, []);
diff --git a/src/app/components/image-pack-view/ImageTile.tsx b/src/app/components/image-pack-view/ImageTile.tsx
index 37337ff7..959426f4 100644
--- a/src/app/components/image-pack-view/ImageTile.tsx
+++ b/src/app/components/image-pack-view/ImageTile.tsx
@@ -7,7 +7,7 @@ import { ImageUsage, imageUsageEqual, PackImageReader } from '../../plugins/cust
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { SettingTile } from '../setting-tile';
import { useObjectURL } from '../../hooks/useObjectURL';
-import { createUploadAtom, TUploadAtom } from '../../state/upload';
+import { createUploadAtom, UploadAtom } from '../../state/upload';
import { replaceSpaceWithDash } from '../../utils/common';
type ImageTileProps = {
@@ -91,7 +91,7 @@ export function ImageTile({
type ImageTileUploadProps = {
file: File;
- children: (uploadAtom: TUploadAtom) => ReactNode;
+ children: (uploadAtom: UploadAtom) => ReactNode;
};
export function ImageTileUpload({ file, children }: ImageTileUploadProps) {
const url = useObjectURL(file);
diff --git a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx
index ddac0576..57f54c0d 100644
--- a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx
+++ b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx
@@ -3,7 +3,6 @@ import React, {
FormEventHandler,
KeyboardEventHandler,
useCallback,
- useMemo,
useRef,
useState,
} from 'react';
@@ -29,19 +28,18 @@ import {
Scroll,
MenuItem,
} from 'folds';
-import { Room } from 'matrix-js-sdk';
import { isKeyHotkey } from 'is-hotkey';
import FocusTrap from 'focus-trap-react';
+import { postInvite } from 'neurone/lib/api/matrix/client/v3/rooms';
import { stopPropagation } from '../../utils/keyboard';
import { useDirectUsers } from '../../hooks/useDirectUsers';
import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix';
-import { Membership } from '../../../types/matrix/room';
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { BreakWord } from '../../styles/Text.css';
import { useAlive } from '../../hooks/useAlive';
+import { useClient } from '../../hooks/useClient';
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
@@ -52,27 +50,19 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = {
const getUserIdString = (userId: string) => getMxIdLocalPart(userId) ?? userId;
type InviteUserProps = {
- room: Room;
+ roomId: string;
requestClose: () => void;
};
-export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
- const mx = useMatrixClient();
+export function InviteUserPrompt({ roomId, requestClose }: InviteUserProps) {
+ const ct = useClient();
const alive = useAlive();
const inputRef = useRef(null);
const directUsers = useDirectUsers();
const [validUserId, setValidUserId] = useState();
- const filteredUsers = useMemo(
- () =>
- directUsers.filter((userId) => {
- const membership = room.getMember(userId)?.membership;
- return membership !== Membership.Join;
- }),
- [directUsers, room]
- );
const [result, search, resetSearch] = useAsyncSearch(
- filteredUsers,
+ directUsers,
getUserIdString,
SEARCH_OPTIONS
);
@@ -83,9 +73,17 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
const [inviteState, invite] = useAsyncCallback(
useCallback(
async (userId, reason) => {
- await mx.invite(room.roomId, userId, reason);
+ await ct.requestAuthJSON(
+ postInvite({
+ pathParams: { roomId },
+ body: {
+ user_id: userId,
+ reason,
+ },
+ })
+ );
},
- [mx, room]
+ [ct, roomId]
)
);
diff --git a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
index 217491e6..eba75721 100644
--- a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
+++ b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
@@ -16,10 +16,16 @@ import {
Button,
Spinner,
} from 'folds';
-import { MatrixError } from 'matrix-js-sdk';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { postLeave } from 'neurone/lib/api/matrix/client/v3/rooms';
+import {
+ MatrixResponseResult,
+ parseMatrixResp,
+ printMatrixError,
+} from 'neurone/lib/matrix-request';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { stopPropagation } from '../../utils/keyboard';
+import { useClient } from '../../hooks/useClient';
+import { deleteJoinRooms } from '../../../client/syncWriter/utils';
type LeaveRoomPromptProps = {
roomId: string;
@@ -27,23 +33,31 @@ type LeaveRoomPromptProps = {
onCancel: () => void;
};
export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
- const [leaveState, leaveRoom] = useAsyncCallback(
+ const [leaveState, leaveRoom] = useAsyncCallback, Error, []>(
useCallback(async () => {
- mx.leave(roomId);
- }, [mx, roomId])
+ const response = await ct.requestAuthJSON(postLeave({ pathParams: { roomId } }));
+ deleteJoinRooms(ct, [roomId])
+ .then(() => console.log('=====> deleted'))
+ .catch((e) => {
+ console.error(e);
+ });
+ return parseMatrixResp(response);
+ }, [ct, roomId])
);
+ const success = leaveState.status === AsyncStatus.Success;
+ const [requestError] = success ? leaveState.data : [];
const handleLeave = () => {
leaveRoom();
};
useEffect(() => {
- if (leaveState.status === AsyncStatus.Success) {
+ if (success) {
onDone();
}
- }, [leaveState, onDone]);
+ }, [success, onDone]);
return (
}>
@@ -80,6 +94,11 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
Failed to leave room! {leaveState.error.message}
)}
+ {requestError && (
+
+ Failed to leave room! {printMatrixError(requestError)}
+
+ )}
);
diff --git a/src/app/components/message/content/FileContent.tsx b/src/app/components/message/content/FileContent.tsx
index ad54c531..a4e55290 100644
--- a/src/app/components/message/content/FileContent.tsx
+++ b/src/app/components/message/content/FileContent.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode, useCallback, useState } from 'react';
+import React, { useCallback, useState } from 'react';
import {
Box,
Button,
@@ -12,14 +12,12 @@ import {
Text,
Tooltip,
TooltipProvider,
- as,
} from 'folds';
import FileSaver from 'file-saver';
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import FocusTrap from 'focus-trap-react';
import { IFileInfo } from '../../../../types/matrix/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { bytesToSize } from '../../../utils/common';
import {
READABLE_EXT_TO_MIME_TYPE,
@@ -28,14 +26,13 @@ import {
mimeTypeToExt,
} from '../../../utils/mimeTypes';
import { stopPropagation } from '../../../utils/keyboard';
-import {
- decryptFile,
- downloadEncryptedMedia,
- downloadMedia,
- mxcUrlToHttp,
-} from '../../../utils/matrix';
+import { decryptFile, downloadEncryptedMedia, downloadMedia } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { ModalWide } from '../../../styles/Modal.css';
+import { useClient } from '../../../hooks/useClient';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
+import { TextViewer } from '../../text-viewer';
+import { PdfViewer } from '../../Pdf-viewer';
const renderErrorButton = (retry: () => void, text: string) => (
void, text: string) => (
);
-type RenderTextViewerProps = {
- name: string;
- text: string;
- langName: string;
- requestClose: () => void;
-};
type ReadTextFileProps = {
body: string;
mimeType: string;
url: string;
- encInfo?: EncryptedAttachmentInfo;
- renderViewer: (props: RenderTextViewerProps) => ReactNode;
+ encInfo?: object;
};
-export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: ReadTextFileProps) {
- const mx = useMatrixClient();
+export function ReadTextFile({ body, mimeType, url, encInfo }: ReadTextFileProps) {
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const [textViewer, setTextViewer] = useState(false);
const [textState, loadText] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
+ const mediaUrl = mxcToHttp(ct.session.baseUrl, url, useAuthentication).url() ?? url;
const fileContent = encInfo
- ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
+ ? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
+ decryptFile(encBuf, mimeType, encInfo as EncryptedAttachmentInfo)
+ )
: await downloadMedia(mediaUrl);
const text = fileContent.text();
setTextViewer(true);
return text;
- }, [mx, useAuthentication, mimeType, encInfo, url])
+ }, [ct, useAuthentication, mimeType, encInfo, url])
);
return (
@@ -115,14 +107,16 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
size="500"
onContextMenu={(evt: any) => evt.stopPropagation()}
>
- {renderViewer({
- name: body,
- text: textState.data,
- langName: READABLE_TEXT_MIME_TYPES.includes(mimeType)
- ? mimeTypeToExt(mimeType)
- : mimeTypeToExt(READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)] ?? mimeType),
- requestClose: () => setTextViewer(false),
- })}
+ setTextViewer(false)}
+ />
@@ -157,32 +151,28 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
);
}
-type RenderPdfViewerProps = {
- name: string;
- src: string;
- requestClose: () => void;
-};
export type ReadPdfFileProps = {
body: string;
mimeType: string;
url: string;
- encInfo?: EncryptedAttachmentInfo;
- renderViewer: (props: RenderPdfViewerProps) => ReactNode;
+ encInfo?: object;
};
-export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: ReadPdfFileProps) {
- const mx = useMatrixClient();
+export function ReadPdfFile({ body, mimeType, url, encInfo }: ReadPdfFileProps) {
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const [pdfViewer, setPdfViewer] = useState(false);
const [pdfState, loadPdf] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
+ const mediaUrl = mxcToHttp(ct.session.baseUrl, url, useAuthentication).url() ?? url;
const fileContent = encInfo
- ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
+ ? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
+ decryptFile(encBuf, mimeType, encInfo as EncryptedAttachmentInfo)
+ )
: await downloadMedia(mediaUrl);
setPdfViewer(true);
return URL.createObjectURL(fileContent);
- }, [mx, url, useAuthentication, mimeType, encInfo])
+ }, [ct, url, useAuthentication, mimeType, encInfo])
);
return (
@@ -203,11 +193,11 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
size="500"
onContextMenu={(evt: any) => evt.stopPropagation()}
>
- {renderViewer({
- name: body,
- src: pdfState.data,
- requestClose: () => setPdfViewer(false),
- })}
+ setPdfViewer(false)}
+ />
@@ -245,23 +235,25 @@ export type DownloadFileProps = {
mimeType: string;
url: string;
info: IFileInfo;
- encInfo?: EncryptedAttachmentInfo;
+ encInfo?: object;
};
export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFileProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const [downloadState, download] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
+ const mediaUrl = mxcToHttp(ct.session.baseUrl, url, useAuthentication).url() ?? url;
const fileContent = encInfo
- ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
+ ? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
+ decryptFile(encBuf, mimeType, encInfo as EncryptedAttachmentInfo)
+ )
: await downloadMedia(mediaUrl);
const fileURL = URL.createObjectURL(fileContent);
FileSaver.saveAs(fileURL, body);
return fileURL;
- }, [mx, url, useAuthentication, mimeType, encInfo, body])
+ }, [ct, url, useAuthentication, mimeType, encInfo, body])
);
return downloadState.status === AsyncStatus.Error ? (
@@ -294,17 +286,21 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
type FileContentProps = {
body: string;
mimeType: string;
- renderAsTextFile: () => ReactNode;
- renderAsPdfFile: () => ReactNode;
+ info: object;
+ url: string;
+ encInfo?: object;
};
-export const FileContent = as<'div', FileContentProps>(
- ({ body, mimeType, renderAsTextFile, renderAsPdfFile, children, ...props }, ref) => (
-
+export function FileContent({ body, mimeType, info, url, encInfo }: FileContentProps) {
+ return (
+
{(READABLE_TEXT_MIME_TYPES.includes(mimeType) ||
- READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)]) &&
- renderAsTextFile()}
- {mimeType === 'application/pdf' && renderAsPdfFile()}
- {children}
+ READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)]) && (
+
+ )}
+ {mimeType === 'application/pdf' && (
+
+ )}
+
- )
-);
+ );
+}
diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx
index 84e3709e..919fb7f8 100644
--- a/src/app/components/message/content/ImageContent.tsx
+++ b/src/app/components/message/content/ImageContent.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode, useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import {
Badge,
Box,
@@ -22,41 +22,28 @@ import FocusTrap from 'focus-trap-react';
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { IImageInfo, MATRIX_BLUR_HASH_PROPERTY_NAME } from '../../../../types/matrix/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import * as css from './style.css';
import { bytesToSize } from '../../../utils/common';
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
import { stopPropagation } from '../../../utils/keyboard';
-import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
+import { decryptFile, downloadEncryptedMedia } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { ModalWide } from '../../../styles/Modal.css';
import { validBlurHash } from '../../../utils/blurHash';
+import { useClient } from '../../../hooks/useClient';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
+import { Image } from '../../media';
+import { ImageViewer } from '../../image-viewer';
-type RenderViewerProps = {
- src: string;
- alt: string;
- requestClose: () => void;
-};
-type RenderImageProps = {
- alt: string;
- title: string;
- src: string;
- onLoad: () => void;
- onError: () => void;
- onClick: () => void;
- tabIndex: number;
-};
export type ImageContentProps = {
body: string;
mimeType?: string;
url: string;
info?: IImageInfo;
- encInfo?: EncryptedAttachmentInfo;
+ encInfo?: object;
autoPlay?: boolean;
markedAsSpoiler?: boolean;
spoilerReason?: string;
- renderViewer: (props: RenderViewerProps) => ReactNode;
- renderImage: (props: RenderImageProps) => ReactNode;
};
export const ImageContent = as<'div', ImageContentProps>(
(
@@ -70,13 +57,11 @@ export const ImageContent = as<'div', ImageContentProps>(
autoPlay,
markedAsSpoiler,
spoilerReason,
- renderViewer,
- renderImage,
...props
},
ref
) => {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const blurHash = validBlurHash(info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]);
@@ -87,15 +72,15 @@ export const ImageContent = as<'div', ImageContentProps>(
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
+ const mediaUrl = mxcToHttp(ct.session.baseUrl, url, useAuthentication).url() ?? url;
if (encInfo) {
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
- decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
+ decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo as EncryptedAttachmentInfo)
);
return URL.createObjectURL(fileContent);
}
return mediaUrl;
- }, [mx, url, useAuthentication, mimeType, encInfo])
+ }, [ct, url, useAuthentication, mimeType, encInfo])
);
const handleLoad = () => {
@@ -133,11 +118,11 @@ export const ImageContent = as<'div', ImageContentProps>(
size="500"
onContextMenu={(evt: any) => evt.stopPropagation()}
>
- {renderViewer({
- src: srcState.data,
- alt: body,
- requestClose: () => setViewer(false),
- })}
+ setViewer(false)}
+ />
@@ -168,15 +153,16 @@ export const ImageContent = as<'div', ImageContentProps>(
)}
{srcState.status === AsyncStatus.Success && (
- {renderImage({
- alt: body,
- title: body,
- src: srcState.data,
- onLoad: handleLoad,
- onError: handleError,
- onClick: () => setViewer(true),
- tabIndex: 0,
- })}
+ setViewer(true)}
+ tabIndex={0}
+ loading="lazy"
+ />
)}
{blurred && !error && srcState.status !== AsyncStatus.Error && (
diff --git a/src/app/components/message/content/ThumbnailContent.tsx b/src/app/components/message/content/ThumbnailContent.tsx
index 0746137c..25d86b3d 100644
--- a/src/app/components/message/content/ThumbnailContent.tsx
+++ b/src/app/components/message/content/ThumbnailContent.tsx
@@ -1,43 +1,50 @@
-import { ReactNode, useCallback, useEffect } from 'react';
-import { IThumbnailContent } from '../../../../types/matrix/common';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import React, { useCallback, useEffect } from 'react';
+import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
-import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../../utils/matrix';
+import { decryptFile, downloadEncryptedMedia } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
+import { rEncryptedFile, rInfo, rThumbnailInfo } from '../../../../client/reader';
+import { Image } from '../../media';
+import { useClient } from '../../../hooks/useClient';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
export type ThumbnailContentProps = {
- info: IThumbnailContent;
- renderImage: (src: string) => ReactNode;
+ info: object;
};
-export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
- const mx = useMatrixClient();
+export function ThumbnailContent({ info }: ThumbnailContentProps) {
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const [thumbSrcState, loadThumbSrc] = useAsyncCallback(
useCallback(async () => {
- const thumbInfo = info.thumbnail_info;
- const thumbMxcUrl = info.thumbnail_file?.url ?? info.thumbnail_url;
- const encInfo = info.thumbnail_file;
- if (typeof thumbMxcUrl !== 'string' || typeof thumbInfo?.mimetype !== 'string') {
+ const thumbInfo = rInfo.thumbnailInfo(info);
+ const encInfo = rInfo.thumbnailFile(info);
+ const thumbMxcUrl = rEncryptedFile.url(encInfo) ?? rInfo.thumbnailUrl(info);
+ const mimeType = rThumbnailInfo.mimetype(thumbInfo);
+
+ if (typeof thumbMxcUrl !== 'string' || typeof mimeType !== 'string') {
throw new Error('Failed to load thumbnail');
}
- const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? thumbMxcUrl;
+ const mediaUrl =
+ mxcToHttp(ct.session.baseUrl, thumbMxcUrl, useAuthentication).url() ?? thumbMxcUrl;
if (encInfo) {
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
- decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
+ decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo as EncryptedAttachmentInfo)
);
return URL.createObjectURL(fileContent);
}
return mediaUrl;
- }, [mx, info, useAuthentication])
+ }, [ct, info, useAuthentication])
);
useEffect(() => {
loadThumbSrc();
}, [loadThumbSrc]);
- return thumbSrcState.status === AsyncStatus.Success ? renderImage(thumbSrcState.data) : null;
+ return thumbSrcState.status === AsyncStatus.Success ? (
+
+ ) : null;
}
diff --git a/src/app/components/message/content/VideoContent.tsx b/src/app/components/message/content/VideoContent.tsx
index 52073ac1..62047a0c 100644
--- a/src/app/components/message/content/VideoContent.tsx
+++ b/src/app/components/message/content/VideoContent.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode, useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import {
Badge,
Box,
@@ -15,43 +15,28 @@ import {
import classNames from 'classnames';
import { BlurhashCanvas } from 'react-blurhash';
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
-import {
- IThumbnailContent,
- IVideoInfo,
- MATRIX_BLUR_HASH_PROPERTY_NAME,
-} from '../../../../types/matrix/common';
import * as css from './style.css';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { bytesToSize, millisecondsToMinutesAndSeconds } from '../../../utils/common';
-import {
- decryptFile,
- downloadEncryptedMedia,
- downloadMedia,
- mxcUrlToHttp,
-} from '../../../utils/matrix';
+import { decryptFile, downloadEncryptedMedia, downloadMedia } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { validBlurHash } from '../../../utils/blurHash';
+import { useClient } from '../../../hooks/useClient';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
+import { Video } from '../../media';
+import { rInfo, rThumbnailInfo } from '../../../../client/reader';
+import { ThumbnailContent } from './ThumbnailContent';
-type RenderVideoProps = {
- title: string;
- src: string;
- onLoadedMetadata: () => void;
- onError: () => void;
- autoPlay: boolean;
- controls: boolean;
-};
type VideoContentProps = {
body: string;
mimeType: string;
url: string;
- info: IVideoInfo & IThumbnailContent;
- encInfo?: EncryptedAttachmentInfo;
+ info: object;
+ encInfo?: object;
autoPlay?: boolean;
+ mediaAutoLoad?: boolean;
markedAsSpoiler?: boolean;
spoilerReason?: string;
- renderThumbnail?: () => ReactNode;
- renderVideo: (props: RenderVideoProps) => ReactNode;
};
export const VideoContent = as<'div', VideoContentProps>(
(
@@ -63,17 +48,20 @@ export const VideoContent = as<'div', VideoContentProps>(
info,
encInfo,
autoPlay,
+ mediaAutoLoad,
markedAsSpoiler,
spoilerReason,
- renderThumbnail,
- renderVideo,
...props
},
ref
) => {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
- const blurHash = validBlurHash(info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]);
+ const thumbnailInfo = rInfo.thumbnailInfo(info);
+ const blurHash = validBlurHash(rThumbnailInfo.blurhash(thumbnailInfo));
+
+ const size = rInfo.size(info);
+ const duration = rInfo.duration(info);
const [load, setLoad] = useState(false);
const [error, setError] = useState(false);
@@ -81,14 +69,14 @@ export const VideoContent = as<'div', VideoContentProps>(
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
- const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
+ const mediaUrl = mxcToHttp(ct.session.baseUrl, url, useAuthentication).url() ?? url;
const fileContent = encInfo
? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
- decryptFile(encBuf, mimeType, encInfo)
+ decryptFile(encBuf, mimeType, encInfo as EncryptedAttachmentInfo)
)
: await downloadMedia(mediaUrl);
return URL.createObjectURL(fileContent);
- }, [mx, url, useAuthentication, mimeType, encInfo])
+ }, [ct, url, useAuthentication, mimeType, encInfo])
);
const handleLoad = () => {
@@ -119,13 +107,13 @@ export const VideoContent = as<'div', VideoContentProps>(
punch={1}
/>
)}
- {renderThumbnail && !load && (
+ {mediaAutoLoad && !load && (
- {renderThumbnail()}
+
)}
{!autoPlay && !blurred && srcState.status === AsyncStatus.Idle && (
@@ -144,14 +132,14 @@ export const VideoContent = as<'div', VideoContentProps>(
)}
{srcState.status === AsyncStatus.Success && (
- {renderVideo({
- title: body,
- src: srcState.data,
- onLoadedMetadata: handleLoad,
- onError: handleError,
- autoPlay: true,
- controls: true,
- })}
+
)}
{blurred && !error && srcState.status !== AsyncStatus.Error && (
@@ -219,7 +207,7 @@ export const VideoContent = as<'div', VideoContentProps>(
)}
- {!load && typeof info.size === 'number' && (
+ {!load && typeof size === 'number' && (
(
gap="200"
>
- {millisecondsToMinutesAndSeconds(info.duration ?? 0)}
+ {millisecondsToMinutesAndSeconds(duration ?? 0)}
- {bytesToSize(info.size)}
+ {bytesToSize(size)}
)}
diff --git a/src/app/components/message/layout/Base.tsx b/src/app/components/message/layout/Base.tsx
index ac196a5b..b07ae257 100644
--- a/src/app/components/message/layout/Base.tsx
+++ b/src/app/components/message/layout/Base.tsx
@@ -29,12 +29,18 @@ export const UsernameBold = as<'b'>(({ as: AsUsernameBold = 'b', className, ...p
));
export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?: boolean }>(
- ({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => (
+ (
+ { as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, truncate, ...props },
+ ref
+ ) => (
diff --git a/src/app/components/message/layout/layout.css.ts b/src/app/components/message/layout/layout.css.ts
index cc2cd0c6..0a61b361 100644
--- a/src/app/components/message/layout/layout.css.ts
+++ b/src/app/components/message/layout/layout.css.ts
@@ -112,8 +112,7 @@ export const CompactHeader = style([
DefaultReset,
StickySection,
{
- maxWidth: toRem(170),
- width: '100%',
+ width: toRem(170),
},
]);
@@ -182,6 +181,7 @@ export const UsernameBold = style({
export const MessageTextBody = recipe({
base: {
wordBreak: 'break-word',
+ position: 'relative',
},
variants: {
preWrap: {
@@ -201,6 +201,27 @@ export const MessageTextBody = recipe({
fontStyle: 'italic',
},
},
+ truncate: {
+ true: {
+ maxHeight: '15em',
+ overflow: 'hidden',
+
+ selectors: {
+ '&::before': {
+ content: '',
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ pointerEvents: 'none',
+
+ height: config.space.S400,
+ background: `linear-gradient(to top, ${color.Primary.Container}, #00000000)`,
+ borderBottom: `${config.borderWidth.B400} solid ${color.Primary.ContainerLine}`,
+ },
+ },
+ },
+ },
},
});
diff --git a/src/app/components/room-avatar/RoomAvatar.css.ts b/src/app/components/room-avatar/RoomAvatar.css.ts
index c9745a9a..26c2a7d5 100644
--- a/src/app/components/room-avatar/RoomAvatar.css.ts
+++ b/src/app/components/room-avatar/RoomAvatar.css.ts
@@ -12,3 +12,7 @@ export const RoomAvatar = style({
},
},
});
+
+export const RoomAvatarImg = style({
+ fontSize: 1,
+});
diff --git a/src/app/components/room-avatar/RoomAvatar.tsx b/src/app/components/room-avatar/RoomAvatar.tsx
index 23f3998d..86866d82 100644
--- a/src/app/components/room-avatar/RoomAvatar.tsx
+++ b/src/app/components/room-avatar/RoomAvatar.tsx
@@ -1,9 +1,10 @@
-import { JoinRule } from 'matrix-js-sdk';
import { AvatarFallback, AvatarImage, Icon, Icons, color } from 'folds';
import React, { ComponentProps, ReactEventHandler, ReactNode, forwardRef, useState } from 'react';
+import classNames from 'classnames';
import * as css from './RoomAvatar.css';
import { joinRuleToIconSrc } from '../../utils/room';
import colorMXID from '../../../util/colorMXID';
+import { JoinRule } from '../../../types/matrix/room';
type RoomAvatarProps = {
roomId: string;
@@ -31,7 +32,7 @@ export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps
return (
setError(true)}
@@ -44,12 +45,15 @@ export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps
export const RoomIcon = forwardRef<
SVGSVGElement,
Omit, 'src'> & {
- joinRule: JoinRule;
+ joinRule?: JoinRule;
space?: boolean;
}
>(({ joinRule, space, ...props }, ref) => (
diff --git a/src/app/components/room-card/RoomCard.tsx b/src/app/components/room-card/RoomCard.tsx
index 34a7e24b..9fb283c2 100644
--- a/src/app/components/room-card/RoomCard.tsx
+++ b/src/app/components/room-card/RoomCard.tsx
@@ -1,5 +1,4 @@
import React, { ReactNode, useCallback, useRef, useState } from 'react';
-import { MatrixError, Room } from 'matrix-js-sdk';
import {
Avatar,
Badge,
@@ -19,20 +18,22 @@ import {
} from 'folds';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
+import { postJoin, PostJoinOK } from 'neurone/lib/api/matrix/client/v3/join';
+import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
import * as css from './style.css';
import { RoomAvatar } from '../room-avatar';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
+import { getMxIdLocalPart } from '../../utils/matrix';
import { nameInitials } from '../../utils/common';
import { millify } from '../../plugins/millify';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
-import { RoomType, StateEvent } from '../../../types/matrix/room';
+import { RoomType } from '../../../types/matrix/room';
import { useJoinedRoomId } from '../../hooks/useJoinedRoomId';
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
-import { getRoomAvatarUrl, getStateEvent } from '../../utils/room';
-import { useStateEventCallback } from '../../hooks/useStateEventCallback';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useLocalRoomSummary } from '../../hooks/useLocalRoomSummary';
+import { mxcToHttp } from '../../../client/mxcToHttp';
+import { useClient } from '../../hooks/useClient';
type GridColumnCount = '1' | '2' | '3';
const getGridColumnCount = (gridWidth: number): GridColumnCount => {
@@ -161,44 +162,40 @@ export const RoomCard = as<'div', RoomCardProps>(
},
ref
) => {
- const mx = useMatrixClient();
+ const ct = useClient();
+ const { baseUrl } = ct.session;
const useAuthentication = useMediaAuthentication();
const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias);
- const joinedRoom = mx.getRoom(joinedRoomId);
- const [topicEvent, setTopicEvent] = useState(() =>
- joinedRoom ? getStateEvent(joinedRoom, StateEvent.RoomTopic) : undefined
- );
+ const localSummary = useLocalRoomSummary(joinedRoomId ?? '');
const fallbackName = getMxIdLocalPart(roomIdOrAlias) ?? roomIdOrAlias;
const fallbackTopic = roomIdOrAlias;
- const avatar = joinedRoom
- ? getRoomAvatarUrl(mx, joinedRoom, 96, useAuthentication)
- : avatarUrl && mxcUrlToHttp(mx, avatarUrl, useAuthentication, 96, 96, 'crop');
+ const avatarMxc = localSummary.avatarUrl ?? avatarUrl;
+ const avatarHttpUrl =
+ avatarMxc &&
+ mxcToHttp(baseUrl, avatarMxc, useAuthentication).thumbnail({
+ width: 96,
+ height: 96,
+ method: 'crop',
+ });
- const roomName = joinedRoom?.name || name || fallbackName;
- const roomTopic =
- (topicEvent?.getContent().topic as string) || undefined || topic || fallbackTopic;
- const joinedMemberCount = joinedRoom?.getJoinedMemberCount() ?? memberCount;
+ const roomName = localSummary?.name || name || fallbackName;
+ const roomTopic = localSummary.topic || topic || fallbackTopic;
+ const joinedMemberCount = localSummary.memberCount ?? memberCount;
- useStateEventCallback(
- mx,
- useCallback(
- (event) => {
- if (
- joinedRoom &&
- event.getRoomId() === joinedRoom.roomId &&
- event.getType() === StateEvent.RoomTopic
- ) {
- setTopicEvent(getStateEvent(joinedRoom, StateEvent.RoomTopic));
- }
- },
- [joinedRoom]
- )
- );
-
- const [joinState, join] = useAsyncCallback(
- useCallback(() => mx.joinRoom(roomIdOrAlias, { viaServers }), [mx, roomIdOrAlias, viaServers])
+ const [joinState, join] = useAsyncCallback(
+ useCallback(async () => {
+ const response = await ct.requestAuthJSON(
+ postJoin({
+ pathParams: { roomIdOrAlias },
+ searchParams: { server_name: viaServers, via: viaServers },
+ })
+ );
+ const [err, result] = await parseMatrixResp(response);
+ if (err) throw new Error(printMatrixError(err));
+ return result;
+ }, [ct, roomIdOrAlias, viaServers])
);
const joining =
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
@@ -213,7 +210,7 @@ export const RoomCard = as<'div', RoomCardProps>(
(
@@ -222,7 +219,7 @@ export const RoomCard = as<'div', RoomCardProps>(
)}
/>
- {(roomType === RoomType.Space || joinedRoom?.isSpaceRoom()) && (
+ {(roomType === RoomType.Space || localSummary.space) && (
Space
diff --git a/src/app/components/room-intro/RoomIntro.tsx b/src/app/components/room-intro/RoomIntro.tsx
index ce550992..ff17c964 100644
--- a/src/app/components/room-intro/RoomIntro.tsx
+++ b/src/app/components/room-intro/RoomIntro.tsx
@@ -1,50 +1,55 @@
-import React, { useCallback, useState } from 'react';
-import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
-import { Room } from 'matrix-js-sdk';
-import { useAtomValue } from 'jotai';
-import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
-import { getMemberDisplayName, getStateEvent } from '../../utils/room';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
-import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
+import React, { useState } from 'react';
+import { Avatar, Box, Button, Text, as } from 'folds';
import { timeDayMonthYear, timeHourMinute } from '../../utils/time';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { RoomAvatar } from '../room-avatar';
import { nameInitials } from '../../utils/common';
-import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
-import { mDirectAtom } from '../../state/mDirectList';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { InviteUserPrompt } from '../invite-user-prompt';
+import { useClient } from '../../hooks/useClient';
+import { useRoom } from '../../hooks/useRoom';
+import { mxcToHttp } from '../../../client/mxcToHttp';
+import { useRoomTopic } from '../../hooks/storage/useRoomTopic';
+import { useRoomStateEvent } from '../../hooks/storage/useRoomStateEvent';
+import { StateEventType } from '../../../types/matrix/room';
+import { useRoomMembersAPI } from '../../hooks/useRoomMembers';
-export type RoomIntroProps = {
- room: Room;
-};
-
-export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => {
- const mx = useMatrixClient();
+export const RoomIntro = as<
+ 'div',
+ {
+ name: string;
+ avatar?: string;
+ }
+>(({ name, avatar, ...props }, ref) => {
+ const ct = useClient();
+ const roomId = useRoom();
const useAuthentication = useMediaAuthentication();
const { navigateRoom } = useRoomNavigate();
- const mDirects = useAtomValue(mDirectAtom);
const [invitePrompt, setInvitePrompt] = useState(false);
+ const { getName } = useRoomMembersAPI();
- const createEvent = getStateEvent(room, StateEvent.RoomCreate);
- const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
- const name = useRoomName(room);
- const topic = useRoomTopic(room);
- const avatarHttpUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication) : undefined;
+ const topic = useRoomTopic(roomId);
+ const avatarHttpUrl = avatar
+ ? mxcToHttp(ct.session.baseUrl, avatar, useAuthentication).url()
+ : undefined;
- const createContent = createEvent?.getContent();
- const ts = createEvent?.getTs();
- const creatorId = createEvent?.getSender();
- const creatorName =
- creatorId && (getMemberDisplayName(room, creatorId) ?? getMxIdLocalPart(creatorId));
- const prevRoomId = createContent?.predecessor?.room_id;
+ const createResult = useRoomStateEvent(roomId, StateEventType.RoomCreate, '');
+ const event = createResult.value?.event;
+ const content = event?.content;
- const [prevRoomState, joinPrevRoom] = useAsyncCallback(
- useCallback(async (roomId: string) => mx.joinRoom(roomId), [mx])
- );
+ const ts = event?.origin_server_ts;
+ const creatorId = event?.sender;
+ const creatorName = creatorId && getName(creatorId);
+
+ const prevRoomId =
+ content &&
+ 'predecessor' in event.content &&
+ typeof event.content.predecessor === 'object' &&
+ event.content.predecessor &&
+ 'room_id' in event.content.predecessor &&
+ event.content.predecessor.room_id;
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
@@ -53,7 +58,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
{nameInitials(name)}}
@@ -82,36 +87,19 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
{invitePrompt && (
- setInvitePrompt(false)} />
+ setInvitePrompt(false)} />
+ )}
+ {typeof prevRoomId === 'string' && (
+
)}
- {typeof prevRoomId === 'string' &&
- (mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (
-
- ) : (
-
- ))}
diff --git a/src/app/components/splash-screen/SplashScreen.css.ts b/src/app/components/splash-screen/SplashScreen.css.ts
index bd3c300a..3cb6a483 100644
--- a/src/app/components/splash-screen/SplashScreen.css.ts
+++ b/src/app/components/splash-screen/SplashScreen.css.ts
@@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css';
-import { color, config } from 'folds';
+import { color, config, toRem } from 'folds';
export const SplashScreen = style({
minHeight: '100%',
@@ -7,6 +7,11 @@ export const SplashScreen = style({
color: color.Background.OnContainer,
});
+export const SplashLogo = style({
+ width: toRem(42),
+ height: toRem(42),
+});
+
export const SplashScreenFooter = style({
padding: config.space.S400,
});
diff --git a/src/app/components/splash-screen/SplashScreen.tsx b/src/app/components/splash-screen/SplashScreen.tsx
index 27adadba..7df70c4a 100644
--- a/src/app/components/splash-screen/SplashScreen.tsx
+++ b/src/app/components/splash-screen/SplashScreen.tsx
@@ -3,26 +3,33 @@ import React, { ReactNode } from 'react';
import classNames from 'classnames';
import * as patternsCSS from '../../styles/Patterns.css';
import * as css from './SplashScreen.css';
+import CinnySVG from '../../../../public/res/svg/cinny.svg';
type SplashScreenProps = {
- children: ReactNode;
+ footer?: ReactNode;
+ children?: ReactNode;
};
-export function SplashScreen({ children }: SplashScreenProps) {
+export function SplashScreen({ footer, children }: SplashScreenProps) {
return (
- {children}
+ {children || (
+
+
+
+ Cinny
+
+
+ )}
-
- Cinny
-
+ {footer}
);
diff --git a/src/app/components/uia-stages/DummyStage.tsx b/src/app/components/uia-stages/DummyStage.tsx
index 7e0f8586..ce19ab26 100644
--- a/src/app/components/uia-stages/DummyStage.tsx
+++ b/src/app/components/uia-stages/DummyStage.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useCallback } from 'react';
import { Dialog, Text, Box, Button, config } from 'folds';
-import { AuthType } from 'matrix-js-sdk';
import { StageComponentProps } from './types';
+import { AuthType } from '../../utils/matrix-uia';
function DummyErrorDialog({
title,
diff --git a/src/app/components/uia-stages/EmailStage.tsx b/src/app/components/uia-stages/EmailStage.tsx
index fdc2b61a..fd2cc3d0 100644
--- a/src/app/components/uia-stages/EmailStage.tsx
+++ b/src/app/components/uia-stages/EmailStage.tsx
@@ -1,9 +1,10 @@
import React, { useEffect, useCallback, FormEventHandler } from 'react';
import { Dialog, Text, Box, Button, config, Input, color, Spinner } from 'folds';
-import { AuthType, MatrixError } from 'matrix-js-sdk';
+import { printMatrixError } from 'neurone/lib/matrix-request';
import { StageComponentProps } from './types';
import { AsyncState, AsyncStatus } from '../../hooks/useAsyncCallback';
-import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../hooks/types';
+import { RequestEmailTokenCallback, RequestEmailTokenResult } from '../../hooks/types';
+import { AuthType } from '../../utils/matrix-uia';
function EmailErrorDialog({
title,
@@ -77,10 +78,14 @@ export function EmailStageDialog({
}: StageComponentProps & {
email?: string;
clientSecret: string;
- emailTokenState: AsyncState;
+ emailTokenState: AsyncState;
requestEmailToken: RequestEmailTokenCallback;
}) {
const { errorCode, error, session } = stageData;
+ const [matrixError, result] =
+ emailTokenState.status === AsyncStatus.Success ? emailTokenState.data : [];
+ const requestError =
+ emailTokenState.status === AsyncStatus.Error ? emailTokenState.error : undefined;
const handleSubmit = useCallback(
(sessionId: string) => {
@@ -120,14 +125,23 @@ export function EmailStageDialog({
);
}
- if (emailTokenState.status === AsyncStatus.Error) {
+ if (requestError) {
return (
+ );
+ }
+
+ if (matrixError) {
+ return (
+
Verification Request Sent
- {`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}
+ {`Please check your email "${result.email}" and validate before continuing further.`}
{errorCode && (
{`${errorCode}: ${error}`}
)}
-
- {error && (
+ {(error || respError) && (
-
- {error instanceof MatrixError && error.name === ErrorCode.M_LIMIT_EXCEEDED
- ? `Server rate-limited your request for ${millisecondsToMinutes(
- (error.data.retry_after_ms as number | undefined) ?? 0
- )} minutes!`
- : error.message}
-
+ {error && error.message}
+ {respError &&
+ (respError.data.errcode === ErrorCode.M_LIMIT_EXCEEDED ? (
+
+ Server rate-limited your request for{' '}
+ {millisecondsToMinutes(rError.retryAfterMs(respError.data) ?? 0)} minutes!
+
+ ) : (
+ {printMatrixError(respError)}
+ ))}
)}
diff --git a/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx b/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx
index 028cd560..1a5ca147 100644
--- a/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx
+++ b/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx
@@ -6,10 +6,10 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer';
import { Page, PageHeader } from '../../components/page';
import { RoomSummaryLoader } from '../../components/RoomSummaryLoader';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { BackRouteHandler } from '../../components/BackRouteHandler';
+import { useClient } from '../../hooks/useClient';
type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
export function JoinBeforeNavigate({
@@ -17,13 +17,13 @@ export function JoinBeforeNavigate({
eventId,
viaServers,
}: JoinBeforeNavigateProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const allRooms = useAtomValue(allRoomsAtom);
const { navigateRoom, navigateSpace } = useRoomNavigate();
const screenSize = useScreenSizeContext();
const handleView = (roomId: string) => {
- if (mx.getRoom(roomId)?.isSpaceRoom()) {
+ if (ct.spaceRooms.read.has(roomId)) {
navigateSpace(roomId);
return;
}
diff --git a/src/app/features/lobby/DnD.css.ts b/src/app/features/lobby/DnD.css.ts
index 76a0e2bf..77259c70 100644
--- a/src/app/features/lobby/DnD.css.ts
+++ b/src/app/features/lobby/DnD.css.ts
@@ -36,7 +36,7 @@ const DropTargetLine = style({
borderBottom: `${toRem(LineHeight)} solid currentColor`,
},
- '&[data-hover=true]:after': {
+ '&[data-hover=true]::after': {
content: '',
display: 'block',
width: toRem(LineHeight * 3),
diff --git a/src/app/features/lobby/HierarchyItemMenu.tsx b/src/app/features/lobby/HierarchyItemMenu.tsx
index c6aff741..58ed2109 100644
--- a/src/app/features/lobby/HierarchyItemMenu.tsx
+++ b/src/app/features/lobby/HierarchyItemMenu.tsx
@@ -17,7 +17,7 @@ import {
} from 'folds';
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
+import { SpaceChildContent, StateEvent } from '../../../types/matrix/room';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
@@ -47,7 +47,7 @@ function SuggestMenuItem({
const [toggleState, handleToggleSuggested] = useAsyncCallback(
useCallback(() => {
- const newContent: MSpaceChildContent = { ...content, suggested: !content.suggested };
+ const newContent: SpaceChildContent = { ...content, suggested: !content.suggested };
return mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, newContent, roomId);
}, [mx, parentId, roomId, content])
);
diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx
index 4b19e516..c182223d 100644
--- a/src/app/features/lobby/Lobby.tsx
+++ b/src/app/features/lobby/Lobby.tsx
@@ -3,9 +3,6 @@ import { Box, Chip, Icon, IconButton, Icons, Line, Scroll, Spinner, Text, config
import { useVirtualizer } from '@tanstack/react-virtual';
import { useAtom, useAtomValue } from 'jotai';
import { useNavigate } from 'react-router-dom';
-import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
-import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
-import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import produce from 'immer';
import { useSpace } from '../../hooks/useSpace';
import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page';
@@ -33,7 +30,6 @@ import {
import { mDirectAtom } from '../../state/mDirectList';
import { makeLobbyCategoryId } from '../../state/closedLobbyCategories';
import { useCategoryHandler } from '../../hooks/useCategoryHandler';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList';
import { getCanonicalAliasOrRoomId, rateLimitedActions } from '../../utils/matrix';
import { getSpaceRoomPath } from '../../pages/pathUtils';
diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx
index 33b21bff..dd014cd1 100644
--- a/src/app/features/room-nav/RoomNavItem.tsx
+++ b/src/app/features/room-nav/RoomNavItem.tsx
@@ -1,5 +1,4 @@
import React, { MouseEventHandler, forwardRef, useState } from 'react';
-import { Room } from 'matrix-js-sdk';
import {
Avatar,
Box,
@@ -22,21 +21,15 @@ import FocusTrap from 'focus-trap-react';
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
-import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
import { nameInitials } from '../../utils/common';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { useRoomUnread } from '../../state/hooks/unread';
-import { roomToUnreadAtom } from '../../state/room/roomToUnread';
-import { usePowerLevels } from '../../hooks/usePowerLevels';
+import { usePowerLevels, usePowerLevelsState } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom';
-import { markAsRead } from '../../utils/notifications';
import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { TypingIndicator } from '../../components/typing-indicator';
import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to';
-import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
import { getViaServers } from '../../plugins/via-servers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useSetting } from '../../state/hooks/settings';
@@ -51,29 +44,35 @@ import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationS
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt';
+import { JoinRule } from '../../../types/matrix/room';
+import { useClient } from '../../hooks/useClient';
+import { mxcToHttp } from '../../../client/mxcToHttp';
+import { TotalNotifications } from '../../state/hooks/roomNotifications';
type RoomNavItemMenuProps = {
- room: Room;
+ roomId: string;
requestClose: () => void;
notificationMode?: RoomNotificationMode;
+ notifications?: TotalNotifications;
};
const RoomNavItemMenu = forwardRef(
- ({ room, requestClose, notificationMode }, ref) => {
- const mx = useMatrixClient();
+ ({ roomId, requestClose, notificationMode, notifications }, ref) => {
+ const ct = useClient();
+ const { userId } = ct.session;
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
- const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
- const powerLevels = usePowerLevels(room);
- const creators = useRoomCreators(room);
+ const powerLevelState = usePowerLevelsState(roomId);
+ const powerLevels = usePowerLevels(powerLevelState.value);
+ const creators = useRoomCreators(roomId);
const permissions = useRoomPermissions(creators, powerLevels);
- const canInvite = permissions.action('invite', mx.getSafeUserId());
+ const canInvite = permissions.action('invite', userId);
const openRoomSettings = useOpenRoomSettings();
const space = useSpaceOptionally();
const [invitePrompt, setInvitePrompt] = useState(false);
const handleMarkAsRead = () => {
- markAsRead(mx, room.roomId, hideActivity);
+ // markAsRead(mx, roomId, hideActivity);
requestClose();
};
@@ -82,22 +81,22 @@ const RoomNavItemMenu = forwardRef(
};
const handleCopyLink = () => {
- const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
- const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
- copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
+ const alias = ct.roomToAlias.read.get(roomId);
+ const viaServers = alias ? undefined : getViaServers(creators, powerLevels);
+ copyToClipboard(getMatrixToRoom(alias ?? roomId, viaServers));
requestClose();
};
const handleRoomSettings = () => {
- openRoomSettings(room.roomId, space?.roomId);
+ openRoomSettings(roomId, space?.roomId);
requestClose();
};
return (
)}
- {messageLayout === MessageLayout.Compact && (
-
- {msgContentJSX}
-
- )}
- {messageLayout === MessageLayout.Bubble && (
-
- {msgContentJSX}
-
- )}
- {messageLayout !== MessageLayout.Compact && messageLayout !== MessageLayout.Bubble && (
-
- {headerJSX}
- {msgContentJSX}
-
- )}
+
+ {reply}
+
+
+ )
+ }
+ username={
+ !collapse && (
+
+ )
+ }
+ time={
+
+ }
+ collapse={collapse}
+ messageLayout={messageLayout}
+ onContextMenu={handleContextMenu}
+ >
+ {msgContentJSX}
+
);
}
);
export type EventProps = {
- room: Room;
- mEvent: MatrixEvent;
+ event: MessageEventWithoutRoomId;
highlight: boolean;
canDelete?: boolean;
+ redacted?: boolean;
messageSpacing: MessageSpacing;
hideReadReceipts?: boolean;
showDeveloperTools?: boolean;
@@ -1165,10 +1262,10 @@ export const Event = as<'div', EventProps>(
(
{
className,
- room,
- mEvent,
+ event,
highlight,
canDelete,
+ redacted,
messageSpacing,
hideReadReceipts,
showDeveloperTools,
@@ -1177,12 +1274,11 @@ export const Event = as<'div', EventProps>(
},
ref
) => {
- const mx = useMatrixClient();
+ const ct = useClient();
const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [menuAnchor, setMenuAnchor] = useState();
- const stateEvent = typeof mEvent.getStateKey() === 'string';
const handleContextMenu: MouseEventHandler = (evt) => {
if (evt.altKey || !window.getSelection()?.isCollapsed) return;
@@ -1242,39 +1338,22 @@ export const Event = as<'div', EventProps>(
{!hideReadReceipts && (
-
+
)}
{showDeveloperTools && (
-
+
)}
-
+
- {((!mEvent.isRedacted() && canDelete && !stateEvent) ||
- (mEvent.getSender() !== mx.getUserId() && !stateEvent)) && (
+ {((!redacted && canDelete) || event.sender !== ct.session.userId) && (
<>
- {!mEvent.isRedacted() && canDelete && (
-
+ {!redacted && canDelete && (
+
)}
- {mEvent.getSender() !== mx.getUserId() && (
-
+ {event.sender !== ct.session.userId && (
+
)}
>
@@ -1302,3 +1381,67 @@ export const Event = as<'div', EventProps>(
);
}
);
+
+export function ThreadEvents({
+ users,
+ ts,
+ messageLayout,
+ hour24Clock,
+ dateFormatString,
+ messageSpacing,
+ count,
+ onClick,
+}: {
+ users: string[];
+ ts: number;
+ messageLayout: MessageLayout;
+ hour24Clock: boolean;
+ dateFormatString: string;
+ messageSpacing: MessageSpacing;
+ count: number;
+ onClick?: () => void;
+}) {
+ return (
+
+
+ }
+ iconSrc={Icons.Thread}
+ content={
+
+ {users.length === 1 && {users[0]}}
+ {users.length === 2 && (
+ <>
+ {users[0]}, {users[1]}
+ >
+ )}
+ {users.length > 2 && (
+ <>
+ {users[0]}, {users[1]}, +{users.length - 2} others
+ >
+ )}{' '}
+ replied in thread.{' '}
+ onClick?.()}
+ style={{ flexShrink: 0 }}
+ radii="Pill"
+ variant="Primary"
+ fill="None"
+ >
+
+ {count} {count === 1 ? 'Reply' : 'Replies'}
+
+
+
+ }
+ />
+
+ );
+}
diff --git a/src/app/features/room/message/MessageEditor.tsx b/src/app/features/room/message/MessageEditor.tsx
index 9a7567aa..339931c6 100644
--- a/src/app/features/room/message/MessageEditor.tsx
+++ b/src/app/features/room/message/MessageEditor.tsx
@@ -21,7 +21,7 @@ import {
} from 'folds';
import { Editor, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
-import { IContent, IMentions, MatrixEvent, RelationType, Room } from 'matrix-js-sdk';
+import { MessageEventWithoutRoomId } from 'neurone/lib/api/types';
import { isKeyHotkey } from 'is-hotkey';
import {
AUTOCOMPLETE_PREFIXES,
@@ -50,21 +50,21 @@ import { settingsAtom } from '../../../state/settings';
import { UseStateProvider } from '../../../components/UseStateProvider';
import { EmojiBoard } from '../../../components/emoji-board';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getEditedEvent, getMentionContent, trimReplyFromFormattedBody } from '../../../utils/room';
import { mobileOrTablet } from '../../../utils/user-agent';
import { useComposingCheck } from '../../../hooks/useComposingCheck';
+import { useClient } from '../../../hooks/useClient';
+import { MentionsContent, MessageContent, RelationType } from '../../../../types/matrix/common';
type MessageEditorProps = {
roomId: string;
- room: Room;
- mEvent: MatrixEvent;
- imagePackRooms?: Room[];
+ event: MessageEventWithoutRoomId;
+ imagePackRooms?: string[];
onCancel: () => void;
};
export const MessageEditor = as<'div', MessageEditorProps>(
- ({ room, roomId, mEvent, imagePackRooms, onCancel, ...props }, ref) => {
- const mx = useMatrixClient();
+ ({ roomId, event, imagePackRooms, onCancel, ...props }, ref) => {
+ const ct = useClient();
const editor = useEditor();
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [globalToolbar] = useSetting(settingsAtom, 'editorToolbar');
@@ -78,9 +78,9 @@ export const MessageEditor = as<'div', MessageEditorProps>(
const getPrevBodyAndFormattedBody = useCallback((): [
string | undefined,
string | undefined,
- IMentions | undefined
+ MentionsContent | undefined
] => {
- const evtId = mEvent.getId()!;
+ const evtId = event.event_id;
const evtTimeline = room.getTimelineForEvent(evtId);
const editedEvent =
evtTimeline && getEditedEvent(evtId, mEvent, evtTimeline.getTimelineSet());
@@ -95,7 +95,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
typeof customHtml === 'string' ? customHtml : undefined,
mMentions,
];
- }, [room, mEvent]);
+ }, [roomId, event]);
const [saveState, save] = useAsyncCallback(
useCallback(async () => {
@@ -124,12 +124,13 @@ export const MessageEditor = as<'div', MessageEditorProps>(
}
}
- const newContent: IContent = {
- msgtype: mEvent.getContent().msgtype,
+ const prevContent = event.content as MessageContent;
+ const newContent: MessageContent = {
+ msgtype: prevContent.msgtype,
body: plainText,
};
- const mentionData = getMentions(mx, roomId, editor);
+ const mentionData = getMentions(ct, editor);
prevMentions?.user_ids?.forEach((prevMentionId) => {
mentionData.users.add(prevMentionId);
@@ -143,18 +144,20 @@ export const MessageEditor = as<'div', MessageEditorProps>(
newContent.formatted_body = customHtml;
}
- const content: IContent = {
+ const content: MessageContent = {
...newContent,
body: `* ${plainText}`,
'm.new_content': newContent,
'm.relates_to': {
- event_id: mEvent.getId(),
+ event_id: event.event_id,
rel_type: RelationType.Replace,
},
};
- return mx.sendMessage(roomId, content);
- }, [mx, editor, roomId, mEvent, isMarkdown, getPrevBodyAndFormattedBody])
+ // TODO: send message
+ // return mx.sendMessage(roomId, content);
+ return Promise.resolve();
+ }, [ct, editor, event, isMarkdown, getPrevBodyAndFormattedBody])
);
const handleSave = useCallback(() => {
@@ -241,7 +244,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
)}
{autocompleteQuery?.prefix === AutocompletePrefix.UserMention && (
[];
onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
};
export const Reactions = as<'div', ReactionsProps>(
- ({ className, room, relations, mEventId, canSendReaction, onReactionToggle, ...props }, ref) => {
- const mx = useMatrixClient();
+ ({ className, reactions, eventId, canSendReaction, onReactionToggle, ...props }, ref) => {
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const [viewer, setViewer] = useState(false);
- const myUserId = mx.getUserId();
- const reactions = useRelations(
- relations,
- useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], [])
- );
const handleViewReaction: MouseEventHandler = (evt) => {
evt.stopPropagation();
@@ -50,6 +43,8 @@ export const Reactions = as<'div', ReactionsProps>(
else setViewer(key);
};
+ const reactionGroups = useMemo(() => getReactionGroups(reactions), [reactions]);
+
return (
(
{...props}
ref={ref}
>
- {reactions.map(([key, events]) => {
- const rEvents = Array.from(events);
- if (rEvents.length === 0 || typeof key !== 'string') return null;
- const myREvent = myUserId ? rEvents.find(factoryEventSentBy(myUserId)) : undefined;
- const isPressed = !!myREvent?.getRelation();
+ {reactionGroups.map((reaction) => {
+ const isPressed = reaction.users.has(ct.session.userId);
return (
-
+
}
@@ -79,13 +75,15 @@ export const Reactions = as<'div', ReactionsProps>(
{(targetRef) => (
onReactionToggle(mEventId, key) : undefined}
+ key={reaction.key}
+ ct={ct}
+ reaction={reaction.key}
+ count={reaction.users.size}
+ onClick={
+ canSendReaction ? () => onReactionToggle(eventId, reaction.key) : undefined
+ }
onContextMenu={handleViewReaction}
aria-disabled={!canSendReaction}
useAuthentication={useAuthentication}
@@ -114,9 +112,8 @@ export const Reactions = as<'div', ReactionsProps>(
>
setViewer(false)}
/>
diff --git a/src/app/features/room/message/thread-selector/ThreadSelector.tsx b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
new file mode 100644
index 00000000..41bbc9b0
--- /dev/null
+++ b/src/app/features/room/message/thread-selector/ThreadSelector.tsx
@@ -0,0 +1,75 @@
+import { Box, Icon, Icons, Line, Text } from 'folds';
+import React, { ReactNode } from 'react';
+import classNames from 'classnames';
+import * as css from './styles.css';
+import { Time } from '../../../../components/message';
+import { ThreadTimeline } from '../../../../../client/room/ThreadTimeline';
+import { useObservableValue } from '../../../../hooks/useObservableValue';
+import { GetRoomMemberNameCallback } from '../../../../hooks/useRoomMembers';
+
+export function ThreadSelectorContainer({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+type ThreadSelectorProps = {
+ thread: ThreadTimeline;
+ outlined?: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
+ getMemberName: GetRoomMemberNameCallback;
+ onClick?: (threadId: string) => void;
+};
+
+export function ThreadSelector({
+ thread,
+ outlined,
+ hour24Clock,
+ dateFormatString,
+ getMemberName,
+ onClick,
+}: ThreadSelectorProps) {
+ const meta = useObservableValue(thread.meta);
+ const latestEvent = meta?.latestEvent;
+
+ return (
+ onClick?.(thread.threadId)}
+ >
+
+
+
+ {meta?.count ?? `+${thread.liveMessages.read.size || 1}`}{' '}
+ {meta?.count === 1 ? 'Reply' : 'Replies'}
+
+
+ {latestEvent && (
+ <>
+
+
+
+ Last reply by
+ {getMemberName(latestEvent.sender)}
+ —
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/app/features/room/message/thread-selector/index.ts b/src/app/features/room/message/thread-selector/index.ts
new file mode 100644
index 00000000..2dfa5d9e
--- /dev/null
+++ b/src/app/features/room/message/thread-selector/index.ts
@@ -0,0 +1 @@
+export * from './ThreadSelector';
diff --git a/src/app/features/room/message/thread-selector/styles.css.ts b/src/app/features/room/message/thread-selector/styles.css.ts
new file mode 100644
index 00000000..e0e3d68c
--- /dev/null
+++ b/src/app/features/room/message/thread-selector/styles.css.ts
@@ -0,0 +1,38 @@
+import { style } from '@vanilla-extract/css';
+import { color, config, FocusOutline, toRem } from 'folds';
+import { ContainerColor } from '../../../../styles/ContainerColor.css';
+
+export const ThreadSelectorContainer = style({
+ marginTop: config.space.S200,
+});
+
+export const ThreadSelector = style([
+ ContainerColor({ variant: 'SurfaceVariant' }),
+ {
+ padding: config.space.S200,
+ borderRadius: config.radii.R400,
+ cursor: 'pointer',
+
+ selectors: {
+ '&:hover, &:focus-visible': {
+ backgroundColor: color.SurfaceVariant.ContainerHover,
+ },
+ '&:active': {
+ backgroundColor: color.SurfaceVariant.ContainerActive,
+ },
+ },
+ },
+ FocusOutline,
+]);
+
+export const ThreadSectorOutlined = style({
+ borderWidth: config.borderWidth.B300,
+});
+
+export const ThreadSelectorDivider = style({
+ height: toRem(16),
+});
+
+export const ThreadRepliesCount = style({
+ color: color.Primary.Main,
+});
diff --git a/src/app/features/room/msgContent.ts b/src/app/features/room/msgContent.ts
index 5b7cd145..48a10113 100644
--- a/src/app/features/room/msgContent.ts
+++ b/src/app/features/room/msgContent.ts
@@ -1,9 +1,14 @@
-import { IContent, MatrixClient, MsgType } from 'matrix-js-sdk';
import to from 'await-to-js';
+import { postUpload, PostUploadOK } from 'neurone/lib/api/matrix/media/v3/upload';
import {
+ IAudioContent,
+ IFileContent,
+ IImageContent,
IThumbnailContent,
+ IVideoContent,
MATRIX_BLUR_HASH_PROPERTY_NAME,
MATRIX_SPOILER_PROPERTY_NAME,
+ MsgType,
} from '../../../types/matrix/common';
import {
getImageFileUrl,
@@ -14,12 +19,14 @@ import {
loadVideoElement,
} from '../../utils/dom';
import { encryptFile, getImageInfo, getThumbnailContent, getVideoInfo } from '../../utils/matrix';
-import { TUploadItem } from '../../state/room/roomInputDrafts';
+import { UploadItem } from '../../state/room/roomInputDrafts';
import { encodeBlurHash } from '../../utils/blurHash';
import { scaleYDimension } from '../../utils/common';
+import { ClientContext } from '../../../client/context';
+import { getAuthUploadRequest } from '../../../client/request';
const generateThumbnailContent = async (
- mx: MatrixClient,
+ ct: ClientContext,
img: HTMLImageElement | HTMLVideoElement,
dimensions: [number, number],
encrypt: boolean
@@ -30,7 +37,8 @@ const generateThumbnailContent = async (
const thumbnailFile = encThumbData?.file ?? thumbnail;
if (!thumbnailFile) throw new Error('Can not create thumbnail!');
- const data = await mx.uploadContent(thumbnailFile);
+ const request = getAuthUploadRequest(ct.session.baseUrl, ct.session.accessToken);
+ const [, data] = await request(postUpload({ body: thumbnailFile }));
const thumbMxc = data?.content_uri;
if (!thumbMxc) throw new Error('Failed when uploading thumbnail!');
const thumbnailContent = getThumbnailContent({
@@ -44,15 +52,15 @@ const generateThumbnailContent = async (
};
export const getImageMsgContent = async (
- mx: MatrixClient,
- item: TUploadItem,
+ ct: ClientContext,
+ item: UploadItem,
mxc: string
-): Promise => {
+): Promise => {
const { file, originalFile, encInfo, metadata } = item;
const [imgError, imgEl] = await to(loadImageElement(getImageFileUrl(originalFile)));
if (imgError) console.warn(imgError);
- const content: IContent = {
+ const content: IImageContent = {
msgtype: MsgType.Image,
filename: file.name,
body: file.name,
@@ -78,16 +86,16 @@ export const getImageMsgContent = async (
};
export const getVideoMsgContent = async (
- mx: MatrixClient,
- item: TUploadItem,
+ ct: ClientContext,
+ item: UploadItem,
mxc: string
-): Promise => {
+): Promise => {
const { file, originalFile, encInfo, metadata } = item;
const [videoError, videoEl] = await to(loadVideoElement(getVideoFileUrl(originalFile)));
if (videoError) console.warn(videoError);
- const content: IContent = {
+ const content: IVideoContent = {
msgtype: MsgType.Video,
filename: file.name,
body: file.name,
@@ -96,7 +104,7 @@ export const getVideoMsgContent = async (
if (videoEl) {
const [thumbError, thumbContent] = await to(
generateThumbnailContent(
- mx,
+ ct,
videoEl,
getThumbnailDimensions(videoEl.videoWidth, videoEl.videoHeight),
!!encInfo
@@ -126,9 +134,9 @@ export const getVideoMsgContent = async (
return content;
};
-export const getAudioMsgContent = (item: TUploadItem, mxc: string): IContent => {
+export const getAudioMsgContent = (item: UploadItem, mxc: string): IAudioContent => {
const { file, encInfo } = item;
- const content: IContent = {
+ const content: IAudioContent = {
msgtype: MsgType.Audio,
filename: file.name,
body: file.name,
@@ -148,9 +156,9 @@ export const getAudioMsgContent = (item: TUploadItem, mxc: string): IContent =>
return content;
};
-export const getFileMsgContent = (item: TUploadItem, mxc: string): IContent => {
+export const getFileMsgContent = (item: UploadItem, mxc: string): IFileContent => {
const { file, encInfo } = item;
- const content: IContent = {
+ const content: IFileContent = {
msgtype: MsgType.File,
body: file.name,
filename: file.name,
diff --git a/src/app/features/room/reaction-viewer/ReactionViewer.tsx b/src/app/features/room/reaction-viewer/ReactionViewer.tsx
index 6c686bc5..8af2186d 100644
--- a/src/app/features/room/reaction-viewer/ReactionViewer.tsx
+++ b/src/app/features/room/reaction-viewer/ReactionViewer.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from 'react';
+import React, { useState } from 'react';
import classNames from 'classnames';
import {
Avatar,
@@ -14,13 +14,8 @@ import {
as,
config,
} from 'folds';
-import { MatrixEvent, Room, RoomMember } from 'matrix-js-sdk';
-import { Relations } from 'matrix-js-sdk/lib/models/relations';
-import { getMemberDisplayName } from '../../../utils/room';
-import { eventWithShortcode, getMxIdLocalPart } from '../../../utils/matrix';
+import { ReactionGroup } from '../../../utils/room';
import * as css from './ReactionViewer.css';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
-import { useRelations } from '../../../hooks/useRelations';
import { Reaction } from '../../../components/message';
import { getHexcodeForEmoji, getShortcodeFor } from '../../../plugins/emoji';
import { UserAvatar } from '../../../components/user-avatar';
@@ -28,44 +23,28 @@ import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { useOpenUserRoomProfile } from '../../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../../hooks/useSpace';
import { getMouseEventCords } from '../../../utils/dom';
+import { useClient } from '../../../hooks/useClient';
+import { useRoomMembersAPI } from '../../../hooks/useRoomMembers';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
+import { useRoom } from '../../../hooks/useRoom';
export type ReactionViewerProps = {
- room: Room;
initialKey?: string;
- relations: Relations;
+ reactions: ReactionGroup[];
requestClose: () => void;
};
export const ReactionViewer = as<'div', ReactionViewerProps>(
- ({ className, room, initialKey, relations, requestClose, ...props }, ref) => {
- const mx = useMatrixClient();
+ ({ className, initialKey, reactions, requestClose, ...props }, ref) => {
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
- const reactions = useRelations(
- relations,
- useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], [])
- );
- const space = useSpaceOptionally();
+ const roomId = useRoom();
+ const spaceId = useSpaceOptionally();
const openProfile = useOpenUserRoomProfile();
+ const membersAPI = useRoomMembersAPI();
- const [selectedKey, setSelectedKey] = useState(() => {
- if (initialKey) return initialKey;
- const defaultReaction = reactions.find((reaction) => typeof reaction[0] === 'string');
- return defaultReaction ? defaultReaction[0] : '';
- });
-
- const getName = (member: RoomMember) =>
- getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
-
- const getReactionsForKey = (key: string): MatrixEvent[] => {
- const reactSet = reactions.find(([k]) => k === key)?.[1];
- if (!reactSet) return [];
- return Array.from(reactSet);
- };
-
- const selectedReactions = getReactionsForKey(selectedKey);
- const selectedShortcode =
- selectedReactions.find(eventWithShortcode)?.getContent().shortcode ??
- getShortcodeFor(getHexcodeForEmoji(selectedKey)) ??
- selectedKey;
+ const [selectedReaction, setSelectedReaction] = useState(() =>
+ reactions.find((r) => (initialKey ? r.key === initialKey : r))
+ );
return (
(
- {reactions.map(([key, evts]) => {
- if (typeof key !== 'string') return null;
- return (
- setSelectedKey(key)}
- useAuthentication={useAuthentication}
- />
- );
- })}
+ {reactions.map((reaction) => (
+ setSelectedReaction(reaction)}
+ useAuthentication={useAuthentication}
+ />
+ ))}
@@ -98,7 +74,17 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
- {`Reacted with :${selectedShortcode}:`}
+ {selectedReaction ? (
+ {`Reacted with :${
+ selectedReaction.shortcode ??
+ getShortcodeFor(getHexcodeForEmoji(selectedReaction.key)) ??
+ selectedReaction.key
+ }:`}
+ ) : (
+
+ Reactions
+
+ )}
@@ -108,58 +94,51 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
- {selectedReactions.map((mEvent) => {
- const senderId = mEvent.getSender();
- if (!senderId) return null;
- const member = room.getMember(senderId);
- const name = (member ? getName(member) : getMxIdLocalPart(senderId)) ?? senderId;
+ {selectedReaction &&
+ Array.from(selectedReaction.users).map((userId) => {
+ const name = membersAPI.getName(userId);
+ const avatarMxcUrl = membersAPI.getAvatar(userId);
+ const avatarUrl = avatarMxcUrl
+ ? mxcToHttp(ct.session.baseUrl, avatarMxcUrl, useAuthentication).thumbnail({
+ width: 96,
+ height: 96,
+ method: 'crop',
+ })
+ : undefined;
- const avatarMxcUrl = member?.getMxcAvatarUrl();
- const avatarUrl = avatarMxcUrl
- ? mx.mxcUrlToHttp(
- avatarMxcUrl,
- 100,
- 100,
- 'crop',
- undefined,
- false,
- useAuthentication
- )
- : undefined;
-
- return (
-
- );
- })}
+ return (
+
+ );
+ })}
diff --git a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx
index 9986849f..0939f0d5 100644
--- a/src/app/features/room/room-pin-menu/RoomPinMenu.tsx
+++ b/src/app/features/room/room-pin-menu/RoomPinMenu.tsx
@@ -49,7 +49,12 @@ import {
getMemberDisplayName,
getStateEvent,
} from '../../../utils/room';
-import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/matrix/room';
+import {
+ GetContentCallback,
+ MessageEvent,
+ StateEvent,
+ StateEventType,
+} from '../../../../types/matrix/room';
import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
import {
@@ -76,7 +81,7 @@ import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { useTheme } from '../../../hooks/useTheme';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
-import { useIsDirectRoom } from '../../../hooks/useRoom';
+import { useIsDirectRoom, useRoom } from '../../../hooks/useRoom';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
import {
@@ -86,6 +91,7 @@ import {
useGetMemberPowerTag,
} from '../../../hooks/useMemberPowerTag';
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
+import { useClient } from '../../../hooks/useClient';
type PinnedMessageProps = {
room: Room;
@@ -244,302 +250,292 @@ function PinnedMessage({
}
type RoomPinMenuProps = {
- room: Room;
requestClose: () => void;
};
-export const RoomPinMenu = forwardRef(
- ({ room, requestClose }, ref) => {
- const mx = useMatrixClient();
- const userId = mx.getUserId()!;
- const powerLevels = usePowerLevelsContext();
- const creators = useRoomCreators(room);
+export const RoomPinMenu = forwardRef(({ requestClose }, ref) => {
+ const ct = useClient();
+ const roomId = useRoom();
+ const { userId } = ct.session;
+ const powerLevels = usePowerLevelsContext();
+ const creators = useRoomCreators(roomId);
- const permissions = useRoomPermissions(creators, powerLevels);
- const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, userId);
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canPinEvent = permissions.stateEvent(StateEventType.RoomPinnedEvents, userId);
- const creatorsTag = useRoomCreatorsTag();
- const powerLevelTags = usePowerLevelTags(room, powerLevels);
- const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
- const theme = useTheme();
- const accessibleTagColors = useAccessiblePowerTagColors(
- theme.kind,
- creatorsTag,
- powerLevelTags
- );
+ const theme = useTheme();
+ const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
- const pinnedEvents = useRoomPinnedEvents(room);
- const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]);
- const useAuthentication = useMediaAuthentication();
- const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
- const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
+ const pinnedEvents = useRoomPinnedEvents(roomId);
+ const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]);
+ const useAuthentication = useMediaAuthentication();
+ const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
+ const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
- const direct = useIsDirectRoom();
- const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+ const direct = useIsDirectRoom();
+ const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
- const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
- const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
- const { navigateRoom } = useRoomNavigate();
- const scrollRef = useRef(null);
+ const { navigateRoom } = useRoomNavigate();
+ const scrollRef = useRef(null);
- const virtualizer = useVirtualizer({
- count: sortedPinnedEvent.length,
- getScrollElement: () => scrollRef.current,
- estimateSize: () => 75,
- overscan: 4,
- });
+ const virtualizer = useVirtualizer({
+ count: sortedPinnedEvent.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => 75,
+ overscan: 4,
+ });
- const mentionClickHandler = useMentionClickHandler(room.roomId);
- const spoilerClickHandler = useSpoilerClickHandler();
+ const mentionClickHandler = useMentionClickHandler(roomId);
+ const spoilerClickHandler = useSpoilerClickHandler();
- const linkifyOpts = useMemo(
- () => ({
- ...LINKIFY_OPTS,
- render: factoryRenderLinkifyWithMention((href) =>
- renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
- ),
+ const linkifyOpts = useMemo(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
+ ),
+ }),
+ [mx, room, mentionClickHandler]
+ );
+ const htmlReactParserOptions = useMemo(
+ () =>
+ getReactCustomHtmlParser(mx, room.roomId, {
+ linkifyOpts,
+ useAuthentication,
+ handleSpoilerClick: spoilerClickHandler,
+ handleMentionClick: mentionClickHandler,
}),
- [mx, room, mentionClickHandler]
- );
- const htmlReactParserOptions = useMemo(
- () =>
- getReactCustomHtmlParser(mx, room.roomId, {
- linkifyOpts,
- useAuthentication,
- handleSpoilerClick: spoilerClickHandler,
- handleMentionClick: mentionClickHandler,
- }),
- [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
- );
+ [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
+ );
- const renderMatrixEvent = useMatrixEventRenderer<[MatrixEvent, string, GetContentCallback]>(
- {
- [MessageEvent.RoomMessage]: (event, displayName, getContent) => {
- if (event.isRedacted()) {
- return (
-
- );
- }
+ const renderMatrixEvent = useMatrixEventRenderer<[MatrixEvent, string, GetContentCallback]>(
+ {
+ [MessageEvent.RoomMessage]: (event, displayName, getContent) => {
+ if (event.isRedacted()) {
+ return ;
+ }
+ return (
+
+ );
+ },
+ [MessageEvent.RoomMessageEncrypted]: (event, displayName) => {
+ const eventId = event.getId()!;
+ const evtTimeline = room.getTimelineForEvent(eventId);
+
+ const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === eventId);
+
+ if (!mEvent || !evtTimeline) {
return (
-
+
+
+ {event.getType()}
+ {' event'}
+
+
);
- },
- [MessageEvent.RoomMessageEncrypted]: (event, displayName) => {
- const eventId = event.getId()!;
- const evtTimeline = room.getTimelineForEvent(eventId);
+ }
- const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === eventId);
+ return (
+
+ {() => {
+ if (mEvent.isRedacted()) return ;
+ if (mEvent.getType() === MessageEvent.Sticker)
+ return (
+ (
+ }
+ renderViewer={(p) => }
+ />
+ )}
+ />
+ );
+ if (mEvent.getType() === MessageEvent.RoomMessage) {
+ const editedEvent = getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet());
+ const getContent = (() =>
+ editedEvent?.getContent()['m.new_content'] ??
+ mEvent.getContent()) as GetContentCallback;
- if (!mEvent || !evtTimeline) {
- return (
-
-
- {event.getType()}
- {' event'}
-
-
- );
- }
-
- return (
-
- {() => {
- if (mEvent.isRedacted()) return ;
- if (mEvent.getType() === MessageEvent.Sticker)
- return (
- (
- }
- renderViewer={(p) => }
- />
- )}
- />
- );
- if (mEvent.getType() === MessageEvent.RoomMessage) {
- const editedEvent = getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet());
- const getContent = (() =>
- editedEvent?.getContent()['m.new_content'] ??
- mEvent.getContent()) as GetContentCallback;
-
- return (
-
- );
- }
- if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
- return (
-
-
-
- );
+ return (
+
+ );
+ }
+ if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
return (
-
+
);
- }}
-
- );
- },
- [MessageEvent.Sticker]: (event, displayName, getContent) => {
- if (event.isRedacted()) {
- return (
-
- );
- }
- return (
- (
- }
- renderViewer={(p) => }
- />
- )}
- />
- );
- },
+ return (
+
+
+
+ );
+ }}
+
+ );
},
- undefined,
- (event) => {
+ [MessageEvent.Sticker]: (event, displayName, getContent) => {
if (event.isRedacted()) {
return ;
}
return (
-
-
- {event.getType()}
- {' event'}
-
-
+ (
+ }
+ renderViewer={(p) => }
+ />
+ )}
+ />
);
+ },
+ },
+ undefined,
+ (event) => {
+ if (event.isRedacted()) {
+ return ;
}
- );
-
- const handleOpen = (roomId: string, eventId: string) => {
- navigateRoom(roomId, eventId);
- requestClose();
- };
-
- return (
-
+ return (
-
-
- Pinned Messages
-
-
-
-
-
-
-
-
-
-
- {sortedPinnedEvent.length > 0 ? (
-
- {virtualizer.getVirtualItems().map((vItem) => {
- const eventId = sortedPinnedEvent[vItem.index];
- if (!eventId) return null;
+
+ {event.getType()}
+ {' event'}
+
+
+ );
+ }
+ );
- return (
-
{
+ navigateRoom(roomId, eventId);
+ requestClose();
+ };
+
+ return (
+
+
+
+
+ Pinned Messages
+
+
+
+
+
+
+
+
+
+
+ {sortedPinnedEvent.length > 0 ? (
+
+ {virtualizer.getVirtualItems().map((vItem) => {
+ const eventId = sortedPinnedEvent[vItem.index];
+ if (!eventId) return null;
+
+ return (
+
+
-
-
-
-
- );
- })}
-
- ) : (
+
+
+
+ );
+ })}
+
+ ) : (
+
+
-
-
-
- No Pinned Messages
-
-
- Users with sufficient power level can pin a messages from its context menu.
-
-
+
+ No Pinned Messages
+
+
+ Users with sufficient power level can pin a messages from its context menu.
+
- )}
-
-
-
+
+ )}
+
+
-
- );
- }
-);
+
+
+ );
+});
diff --git a/src/app/features/room/thread-view/ThreadView.tsx b/src/app/features/room/thread-view/ThreadView.tsx
new file mode 100644
index 00000000..69d5f2ef
--- /dev/null
+++ b/src/app/features/room/thread-view/ThreadView.tsx
@@ -0,0 +1,626 @@
+import React, {
+ forwardRef,
+ MouseEventHandler,
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { HTMLReactParserOptions } from 'html-react-parser';
+import classNames from 'classnames';
+import { ReactEditor } from 'slate-react';
+import {
+ Badge,
+ Box,
+ Icon,
+ IconButton,
+ Icons,
+ Scroll,
+ Text,
+ Tooltip,
+ TooltipProvider,
+ config,
+} from 'folds';
+import { Opts as LinkifyOpts } from 'linkifyjs';
+import { MessageEventWithoutRoomId } from 'neurone/lib/api/types';
+import FocusTrap from 'focus-trap-react';
+import { useVirtualPaginator } from '../../../hooks/useVirtualPaginator';
+import { useAlive } from '../../../hooks/useAlive';
+import { scrollToBottom } from '../../../utils/dom';
+import {
+ DefaultPlaceholder,
+ CompactPlaceholder,
+ MessageBase,
+ Reply,
+} from '../../../components/message';
+import {
+ factoryRenderLinkifyWithMention,
+ getReactCustomHtmlParser,
+ LINKIFY_OPTS,
+ makeMentionCustomProps,
+ renderMatrixMention,
+} from '../../../plugins/react-custom-html-parser';
+import { fallbackReply, getLatestEditEvent, getReplyId } from '../../../utils/room';
+import { useSetting } from '../../../state/hooks/settings';
+import { MessageLayout, settingsAtom } from '../../../state/settings';
+import { Reactions, Message } from '../message';
+import * as css from './styles.css';
+import { inSameDay, minuteDifference } from '../../../utils/time';
+import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
+import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
+import { RenderMessageContent } from '../../../components/RenderMessageContent';
+import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
+import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
+import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
+import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
+import { useImagePackRooms } from '../../../hooks/useImagePackRooms';
+import { useIsDirectRoom, useRoom } from '../../../hooks/useRoom';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
+import {
+ useAccessiblePowerTagColors,
+ useGetMemberPowerTag,
+} from '../../../hooks/useMemberPowerTag';
+import { useTheme } from '../../../hooks/useTheme';
+import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
+import { usePowerLevelTags, usePowerLevelTagsState } from '../../../hooks/usePowerLevelTags';
+import { ThreadTimeline } from '../../../../client/room/ThreadTimeline';
+import { useObservableValue } from '../../../hooks/useObservableValue';
+import { useClient } from '../../../hooks/useClient';
+import { useRoomMembersAPI } from '../../../hooks/useRoomMembers';
+import { useGetRoomName } from '../../../hooks/storage/useGetRoomName';
+import { useRoomHeroes } from '../../../hooks/storage/useRoomHeros';
+import {
+ useAtBottomToggle,
+ useEditorResizeStickBottom,
+ useReplyClickHandler,
+ useRestoreScrollToBottom,
+ useTimelinePaginate,
+ useTimelinePrefetch,
+ useTimelineScrollRestore,
+ useUserClickHandler,
+ useUsernameClickHandler,
+} from '../hooks/useTimeline';
+import { rContent } from '../../../../client/reader';
+import { Page, PageHeader } from '../../../components/page';
+import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
+import { useEditor } from '../../../components/editor';
+import { TimelineDivider, TimelineFloat, JumpToLatestButton, DayDivider } from '../components';
+import { RoomInput } from '../RoomInput';
+import { RoomViewFollowingPlaceholder } from '../RoomViewFollowing';
+
+type PlaceholderListProps = {
+ count: number;
+ Placeholder: React.ComponentType;
+};
+
+const PlaceholderList = forwardRef(
+ ({ count, Placeholder }, ref) => (
+
+ {Array.from({ length: count }).map((_, i) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+
+
+ ))}
+
+ )
+);
+
+type ThreadViewProps = {
+ threadTimeline: ThreadTimeline;
+ requestClose: () => void;
+};
+
+const PAGINATION_LIMIT = 80;
+
+export function ThreadView({ threadTimeline, requestClose }: ThreadViewProps) {
+ const ct = useClient();
+ const roomId = useRoom();
+ const useAuthentication = useMediaAuthentication();
+ const encryptedRooms = useObservableValue(ct.encryptedRooms);
+ const screenSize = useScreenSizeContext();
+ const floating = screenSize !== ScreenSize.Desktop;
+ const editor = useEditor();
+ const roomInputRef = useRef(null);
+ const roomViewRef = useRef(null);
+
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
+ const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
+ const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
+ const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+ const direct = useIsDirectRoom();
+ const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
+ const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
+ const [encUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
+ const showUrlPreview = encryptedRooms.has(roomId) ? encUrlPreview : urlPreview;
+ const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
+ const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools');
+
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
+ const ignoredUsersList = useIgnoredUsers();
+ const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]);
+
+ const powerLevels = usePowerLevelsContext();
+ const creators = useRoomCreators(roomId);
+ const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(roomId);
+ const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
+
+ const membersAPI = useRoomMembersAPI();
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTagsState = usePowerLevelTagsState(roomId);
+ const powerLevelTags = usePowerLevelTags(powerLevels, powerLevelTagsState.value);
+ const getMemberPowerTag = useGetMemberPowerTag(creators, powerLevels);
+
+ const theme = useTheme();
+ const accessiblePowerTagColors = useAccessiblePowerTagColors(
+ theme.kind,
+ creatorsTag,
+ powerLevelTags
+ );
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+
+ const canRedact = permissions.action('redact', ct.session.userId);
+ const canSendReaction = permissions.event(MessageEvent.Reaction, ct.session.userId);
+ const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, ct.session.userId);
+ const [editId, setEditId] = useState();
+
+ const mentionClickHandler = useMentionClickHandler(roomId);
+ const spoilerClickHandler = useSpoilerClickHandler();
+ const imagePackRooms = useImagePackRooms(roomId);
+
+ const scrollRef = useRef(null);
+ const scrollToBottomRef = useRef({
+ count: 0,
+ smooth: true,
+ });
+
+ const [focusItem, setFocusItem] = useState<
+ | {
+ index: number;
+ scrollTo: boolean;
+ highlight: boolean;
+ }
+ | undefined
+ >();
+ const alive = useAlive();
+
+ const linkifyOpts = useMemo(
+ () => ({
+ ...LINKIFY_OPTS,
+ render: factoryRenderLinkifyWithMention((href) =>
+ renderMatrixMention(
+ ct,
+ roomId,
+ href,
+ makeMentionCustomProps(mentionClickHandler),
+ membersAPI.getName,
+ getRoomName
+ )
+ ),
+ }),
+ [ct, roomId, mentionClickHandler, membersAPI.getName, getRoomName]
+ );
+
+ const htmlReactParserOptions = useMemo(
+ () =>
+ getReactCustomHtmlParser(ct, roomId, {
+ linkifyOpts,
+ useAuthentication,
+ handleSpoilerClick: spoilerClickHandler,
+ handleMentionClick: mentionClickHandler,
+ getMemberName: membersAPI.getName,
+ getRoomName,
+ }),
+ [
+ ct,
+ roomId,
+ linkifyOpts,
+ spoilerClickHandler,
+ mentionClickHandler,
+ useAuthentication,
+ membersAPI.getName,
+ getRoomName,
+ ]
+ );
+
+ const redacted = useObservableValue(threadTimeline.relations.redacted);
+ const reactions = useObservableValue(threadTimeline.relations.reactions.eventToRelations);
+ const edits = useObservableValue(threadTimeline.relations.edits.eventToRelations);
+ const messageBatch = useObservableValue(threadTimeline.batch);
+ const { events } = messageBatch.messages;
+
+ window.tt = threadTimeline;
+
+ const range = useObservableValue(threadTimeline.range.info);
+ const rangeAtStart = range.start === 0;
+ const rangeAtEnd = range.end === messageBatch.size;
+
+ const getScrollElement = useCallback(() => scrollRef.current, []);
+
+ useTimelinePrefetch(threadTimeline, PAGINATION_LIMIT);
+ const handlePaginate = useTimelinePaginate(threadTimeline, PAGINATION_LIMIT);
+ useTimelineScrollRestore(threadTimeline, scrollRef);
+ useRestoreScrollToBottom(scrollRef, messageBatch);
+
+ const { getItems, scrollToItem, scrollToElement, observeBackAnchor, observeFrontAnchor } =
+ useVirtualPaginator({
+ count: messageBatch.size,
+ limit: PAGINATION_LIMIT,
+ range,
+ onRangeChange: useCallback((r) => threadTimeline.range.setRange(r), [threadTimeline]),
+ getScrollElement,
+ getItemElement: useCallback(
+ (index: number) =>
+ (scrollRef.current?.querySelector(`[data-message-item="${index}"]`) as HTMLElement) ??
+ undefined,
+ []
+ ),
+ onEnd: handlePaginate,
+ });
+
+ const tryAutoMarkAsRead = useCallback(() => {
+ console.log('mark thread as read.');
+ }, []);
+
+ const [atBottom, atBottomAnchorRef] = useAtBottomToggle(
+ getScrollElement,
+ messageBatch.next.end ?? false,
+ useCallback(
+ (bottom: boolean) => {
+ if (bottom && document.hasFocus()) {
+ tryAutoMarkAsRead();
+ }
+ },
+ [tryAutoMarkAsRead]
+ )
+ );
+
+ useEditorResizeStickBottom(
+ getScrollElement,
+ useCallback(() => roomInputRef?.current ?? null, [roomInputRef]),
+ atBottom
+ );
+
+ // scroll to focused message
+ useLayoutEffect(() => {
+ if (focusItem && focusItem.scrollTo) {
+ scrollToItem(focusItem.index, {
+ behavior: 'instant',
+ align: 'center',
+ stopInView: true,
+ });
+ }
+
+ setTimeout(() => {
+ if (!alive()) return;
+ setFocusItem((currentItem) => {
+ if (currentItem === focusItem) return undefined;
+ return currentItem;
+ });
+ }, 2000);
+ }, [alive, focusItem, scrollToItem]);
+
+ // scroll to bottom of timeline
+ const scrollToBottomCount = scrollToBottomRef.current.count;
+ useLayoutEffect(() => {
+ if (scrollToBottomCount > 0) {
+ const scrollEl = scrollRef.current;
+ if (scrollEl)
+ scrollToBottom(scrollEl, scrollToBottomRef.current.smooth ? 'smooth' : 'instant');
+ }
+ }, [scrollToBottomCount]);
+
+ // scroll out of view msg editor in view.
+ useEffect(() => {
+ if (editId) {
+ const editMsgElement =
+ (scrollRef.current?.querySelector(`[data-message-id="${editId}"]`) as HTMLElement) ??
+ undefined;
+ if (editMsgElement) {
+ scrollToElement(editMsgElement, {
+ align: 'center',
+ behavior: 'smooth',
+ stopInView: true,
+ });
+ }
+ }
+ }, [scrollToElement, editId]);
+
+ const handleOpenReply: MouseEventHandler = useCallback(async (evt) => {
+ const targetId = evt.currentTarget.getAttribute('data-event-id');
+ if (!targetId) return;
+ console.log(targetId);
+ }, []);
+
+ const handleUserClick = useUserClickHandler();
+ const handleUsernameClick = useUsernameClickHandler(editor);
+ const [replyDraft, handleReplyClick] = useReplyClickHandler(
+ threadTimeline,
+ editor,
+ threadTimeline.threadId
+ );
+
+ const handleReactionToggle = useCallback((targetEventId: string) => {
+ console.log(targetEventId);
+ }, []);
+
+ const handleEdit = useCallback(
+ (editEvtId?: string) => {
+ if (editEvtId) {
+ setEditId(editEvtId);
+ return;
+ }
+ setEditId(undefined);
+ if (editor) {
+ ReactEditor.focus(editor);
+ }
+ },
+ [editor]
+ );
+
+ const renderMatrixEvent = (event: MessageEventWithoutRoomId, item: number, collapse: boolean) => {
+ const reactionEvents = reactions.get(event.event_id)?.filter((r) => !redacted.has(r.event_id));
+ const editEvents = edits.get(event.event_id);
+ const editedEvent = editEvents && getLatestEditEvent(event.sender, editEvents, redacted);
+ const highlighted = focusItem?.index === item && focusItem.highlight;
+
+ const reactionJSX = reactionEvents && reactionEvents?.length > 0 && (
+
+ );
+
+ const content = editedEvent
+ ? rContent.newContent(editedEvent.content) ?? event.content
+ : event.content;
+
+ return (
+ 0 ? reactionEvents : undefined}
+ onUserClick={handleUserClick}
+ onUsernameClick={handleUsernameClick}
+ onReplyClick={handleReplyClick}
+ onReactionToggle={handleReactionToggle}
+ onEditId={handleEdit}
+ redacted={redacted.has(event.event_id)}
+ hideReadReceipts={hideActivity}
+ showDeveloperTools={showDeveloperTools}
+ memberPowerTag={getMemberPowerTag(event.sender)}
+ accessibleTagColors={accessiblePowerTagColors}
+ legacyUsernameColor={legacyUsernameColor || direct}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
+ >
+
+ {reactionJSX}
+
+ );
+ };
+
+ let prevEvent: MessageEventWithoutRoomId | undefined;
+ let isPrevRendered = false;
+ let dayDivider = false;
+
+ const eventRenderer = (item: number) => {
+ const event = events[item];
+
+ if (!event) return null;
+
+ if (event.sender && ignoredUsersSet.has(event.sender)) {
+ return null;
+ }
+ if (redacted.has(event.event_id) && !showHiddenEvents) {
+ return null;
+ }
+
+ if (!dayDivider) {
+ dayDivider = prevEvent
+ ? !inSameDay(prevEvent.origin_server_ts, event.origin_server_ts)
+ : false;
+ }
+
+ const collapsed =
+ isPrevRendered &&
+ !dayDivider &&
+ prevEvent !== undefined &&
+ prevEvent.sender === event.sender &&
+ prevEvent.type === event.type &&
+ minuteDifference(prevEvent.origin_server_ts, event.origin_server_ts) < 2 &&
+ !getReplyId(event.content);
+
+ const eventJSX = renderMatrixEvent(event, item, collapsed);
+ prevEvent = event;
+ isPrevRendered = !!eventJSX;
+
+ const dayDividerJSX =
+ dayDivider && eventJSX ? (
+
+ ) : null;
+
+ const replyEventId = getReplyId(event.content);
+
+ const replyJSX =
+ !fallbackReply(event.content) && replyEventId ? (
+
+ ) : null;
+
+ if (eventJSX && dayDividerJSX) {
+ dayDivider = false;
+
+ return (
+
+ {dayDividerJSX}
+ {replyJSX}
+ {eventJSX}
+
+ );
+ }
+
+ if (replyJSX) {
+ return (
+
+ {replyJSX}
+ {eventJSX}
+
+ );
+ }
+
+ return eventJSX;
+ };
+
+ return (
+
+
+
+
+
+
+ Thread
+
+
+
+
+ Close
+
+ }
+ >
+ {(triggerRef) => (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {!messageBatch.prev.end || !rangeAtStart ? (
+
+ ) : (
+
+
+
+ Replies
+
+
+
+ )}
+ {getItems().map(eventRenderer)}
+ {(!messageBatch.next.end || !rangeAtEnd) && (
+
+ )}
+
+
+
+
+
+
+
+ {!atBottom && (
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/features/room/thread-view/index.ts b/src/app/features/room/thread-view/index.ts
new file mode 100644
index 00000000..6ba05aab
--- /dev/null
+++ b/src/app/features/room/thread-view/index.ts
@@ -0,0 +1 @@
+export * from './ThreadView';
diff --git a/src/app/features/room/thread-view/styles.css.ts b/src/app/features/room/thread-view/styles.css.ts
new file mode 100644
index 00000000..5709c58f
--- /dev/null
+++ b/src/app/features/room/thread-view/styles.css.ts
@@ -0,0 +1,22 @@
+import { style } from '@vanilla-extract/css';
+import { config, toRem } from 'folds';
+
+export const ThreadView = style({
+ // width: toRem(456),
+ width: toRem(496),
+ flexShrink: 0,
+ flexGrow: 0,
+});
+
+export const ThreadViewFloating = style({
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ bottom: 0,
+ zIndex: 1,
+
+ maxWidth: toRem(496),
+ flexShrink: 1,
+ width: '100vw',
+ boxShadow: config.shadow.E400,
+});
diff --git a/src/app/features/settings/Settings.tsx b/src/app/features/settings/Settings.tsx
index 5e1a20f4..a4d879e8 100644
--- a/src/app/features/settings/Settings.tsx
+++ b/src/app/features/settings/Settings.tsx
@@ -20,8 +20,7 @@ import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../componen
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { Account } from './account';
import { useUserProfile } from '../../hooks/useUserProfile';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
+import { getMxIdLocalPart } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { UserAvatar } from '../../components/user-avatar';
import { nameInitials } from '../../utils/common';
@@ -33,6 +32,8 @@ import { About } from './about';
import { UseStateProvider } from '../../components/UseStateProvider';
import { stopPropagation } from '../../utils/keyboard';
import { LogoutDialog } from '../../components/LogoutDialog';
+import { useClient } from '../../hooks/useClient';
+import { mxcToHttp } from '../../../client/mxcToHttp';
export enum SettingsPages {
GeneralPage,
@@ -97,13 +98,17 @@ type SettingsProps = {
requestClose: () => void;
};
export function Settings({ initialPage, requestClose }: SettingsProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
- const userId = mx.getUserId()!;
+ const { userId } = ct.session;
const profile = useUserProfile(userId);
- const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
- const avatarUrl = profile.avatarUrl
- ? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
+ const displayName = profile?.displayName ?? getMxIdLocalPart(userId) ?? userId;
+ const avatarUrl = profile?.avatarUrl
+ ? mxcToHttp(ct.session.baseUrl, profile.avatarUrl, useAuthentication).thumbnail({
+ width: 96,
+ height: 96,
+ method: 'crop',
+ })
: undefined;
const screenSize = useScreenSizeContext();
diff --git a/src/app/features/settings/account/ContactInfo.tsx b/src/app/features/settings/account/ContactInfo.tsx
index cfde7e29..6449ddd7 100644
--- a/src/app/features/settings/account/ContactInfo.tsx
+++ b/src/app/features/settings/account/ContactInfo.tsx
@@ -1,15 +1,22 @@
import React, { useCallback, useEffect } from 'react';
import { Box, Text, Chip } from 'folds';
+import { get3pid, Get3pidOK } from 'neurone/lib/api/matrix/client/v3/account';
+import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { useClient } from '../../../hooks/useClient';
export function ContactInformation() {
- const mx = useMatrixClient();
+ const ct = useClient();
const [threePIdsState, loadThreePIds] = useAsyncCallback(
- useCallback(() => mx.getThreePids(), [mx])
+ useCallback(async () => {
+ const response = await ct.requestAuthJSON(get3pid({}));
+ const [err, result] = await parseMatrixResp(response);
+ if (err) throw new Error(printMatrixError(err));
+ return result;
+ }, [ct])
);
const threePIds =
threePIdsState.status === AsyncStatus.Success ? threePIdsState.data.threepids : undefined;
diff --git a/src/app/features/settings/account/IgnoredUserList.tsx b/src/app/features/settings/account/IgnoredUserList.tsx
index 98db9459..89f0c10c 100644
--- a/src/app/features/settings/account/IgnoredUserList.tsx
+++ b/src/app/features/settings/account/IgnoredUserList.tsx
@@ -1,25 +1,31 @@
import React, { ChangeEventHandler, FormEventHandler, useCallback, useState } from 'react';
import { Box, Button, Chip, Icon, IconButton, Icons, Input, Spinner, Text, config } from 'folds';
+import { putAccountData } from 'neurone/lib/api/matrix/client/v3/user';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { isUserId } from '../../../utils/matrix';
-import { useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
+import { getIgnoredUsersContent, useIgnoredUsers } from '../../../hooks/useIgnoredUsers';
import { useAlive } from '../../../hooks/useAlive';
+import { useClient } from '../../../hooks/useClient';
+import { AccountDataType } from '../../../../types/matrix/accountData';
function IgnoreUserInput({ userList }: { userList: string[] }) {
- const mx = useMatrixClient();
+ const ct = useClient();
const [userId, setUserId] = useState('');
const alive = useAlive();
const [ignoreState, ignore] = useAsyncCallback(
useCallback(
- async (uId: string) => {
- await mx.setIgnoredUsers([...userList, uId]);
- },
- [mx, userList]
+ (uId: string) =>
+ ct.requestAuthJSON(
+ putAccountData({
+ pathParams: { userId: ct.session.userId, type: AccountDataType.IgnoredUserList },
+ body: getIgnoredUsersContent(userList.filter((id) => id !== uId).concat([uId])),
+ })
+ ),
+ [ct, userList]
)
);
const ignoring = ignoreState.status === AsyncStatus.Loading;
@@ -96,11 +102,17 @@ function IgnoreUserInput({ userList }: { userList: string[] }) {
}
function IgnoredUserChip({ userId, userList }: { userId: string; userList: string[] }) {
- const mx = useMatrixClient();
+ const ct = useClient();
const [unignoreState, unignore] = useAsyncCallback(
useCallback(
- () => mx.setIgnoredUsers(userList.filter((uId) => uId !== userId)),
- [mx, userId, userList]
+ async () =>
+ ct.requestAuthJSON(
+ putAccountData({
+ pathParams: { userId: ct.session.userId, type: AccountDataType.IgnoredUserList },
+ body: getIgnoredUsersContent(userList.filter((u) => u !== userId)),
+ })
+ ),
+ [ct, userList, userId]
)
);
diff --git a/src/app/features/settings/account/MatrixId.tsx b/src/app/features/settings/account/MatrixId.tsx
index 0c06a75d..bb02510e 100644
--- a/src/app/features/settings/account/MatrixId.tsx
+++ b/src/app/features/settings/account/MatrixId.tsx
@@ -1,14 +1,14 @@
import React from 'react';
import { Box, Text, Chip } from 'folds';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { copyToClipboard } from '../../../utils/dom';
+import { useClient } from '../../../hooks/useClient';
export function MatrixId() {
- const mx = useMatrixClient();
- const userId = mx.getUserId()!;
+ const ct = useClient();
+ const { userId } = ct.session;
return (
diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx
index e982a799..a29c239d 100644
--- a/src/app/features/settings/account/Profile.tsx
+++ b/src/app/features/settings/account/Profile.tsx
@@ -25,12 +25,12 @@ import {
Spinner,
} from 'folds';
import FocusTrap from 'focus-trap-react';
+import { putAvatarUrl, putDisplayname } from 'neurone/lib/api/matrix/client/v3/profile';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
+import { getMxIdLocalPart } from '../../../utils/matrix';
import { UserAvatar } from '../../../components/user-avatar';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { nameInitials } from '../../../utils/common';
@@ -43,21 +43,27 @@ import { ModalWide } from '../../../styles/Modal.css';
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { useCapabilities } from '../../../hooks/useCapabilities';
+import { useClient } from '../../../hooks/useClient';
+import { mxcToHttp } from '../../../../client/mxcToHttp';
type ProfileProps = {
- profile: UserProfile;
+ profile?: UserProfile;
userId: string;
};
function ProfileAvatar({ profile, userId }: ProfileProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const useAuthentication = useMediaAuthentication();
const capabilities = useCapabilities();
const [alertRemove, setAlertRemove] = useState(false);
const disableSetAvatar = capabilities['m.set_avatar_url']?.enabled === false;
- const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
- const avatarUrl = profile.avatarUrl
- ? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
+ const defaultDisplayName = profile?.displayName ?? getMxIdLocalPart(userId) ?? userId;
+ const avatarUrl = profile?.avatarUrl
+ ? mxcToHttp(ct.session.baseUrl, profile.avatarUrl, useAuthentication).thumbnail({
+ width: 96,
+ height: 96,
+ method: 'crop',
+ })
: undefined;
const [imageFile, setImageFile] = useState();
@@ -76,14 +82,14 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
const handleUploaded = useCallback(
(upload: UploadSuccess) => {
const { mxc } = upload;
- mx.setAvatarUrl(mxc);
+ ct.requestAuthJSON(putAvatarUrl({ pathParams: { userId }, body: { avatar_url: mxc } }));
handleRemoveUpload();
},
- [mx, handleRemoveUpload]
+ [ct, userId, handleRemoveUpload]
);
const handleRemoveAvatar = () => {
- mx.setAvatarUrl('');
+ ct.requestAuthJSON(putAvatarUrl({ pathParams: { userId }, body: { avatar_url: '' } }));
setAlertRemove(false);
};
@@ -206,15 +212,19 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
}
function ProfileDisplayName({ profile, userId }: ProfileProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const capabilities = useCapabilities();
const disableSetDisplayname = capabilities['m.set_displayname']?.enabled === false;
- const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
+ const defaultDisplayName = profile?.displayName ?? getMxIdLocalPart(userId) ?? userId;
const [displayName, setDisplayName] = useState(defaultDisplayName);
const [changeState, changeDisplayName] = useAsyncCallback(
- useCallback((name: string) => mx.setDisplayName(name), [mx])
+ useCallback(
+ (name: string) =>
+ ct.requestAuthJSON(putDisplayname({ pathParams: { userId }, body: { displayname: name } })),
+ [ct, userId]
+ )
);
const changingDisplayName = changeState.status === AsyncStatus.Loading;
@@ -304,8 +314,8 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
}
export function Profile() {
- const mx = useMatrixClient();
- const userId = mx.getUserId()!;
+ const ct = useClient();
+ const { userId } = ct.session;
const profile = useUserProfile(userId);
return (
diff --git a/src/app/features/settings/notifications/AllMessages.tsx b/src/app/features/settings/notifications/AllMessages.tsx
index 301cb9a7..bcf3589e 100644
--- a/src/app/features/settings/notifications/AllMessages.tsx
+++ b/src/app/features/settings/notifications/AllMessages.tsx
@@ -1,8 +1,8 @@
import React, { useCallback, useMemo } from 'react';
import { Badge, Box, Text } from 'folds';
-import { ConditionKind, IPushRules, PushRuleCondition, PushRuleKind, RuleId } from 'matrix-js-sdk';
-import { useAccountData } from '../../../hooks/useAccountData';
-import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import { PushRuleCondition, PushRules } from 'neurone/lib/api/types';
+import { putPushRuleActions } from 'neurone/lib/api/matrix/client/v3/pushrules';
+import { AccountDataType, ConditionKind, PushRuleId } from '../../../../types/matrix/accountData';
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
@@ -13,10 +13,11 @@ import {
NotificationMode,
useNotificationModeActions,
} from '../../../hooks/useNotificationMode';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { useClient } from '../../../hooks/useClient';
+import { useAccountData } from '../../../hooks/storage/useAccountData';
const getAllMessageDefaultRule = (
- ruleId: RuleId,
+ ruleId: PushRuleId,
encrypted: boolean,
oneToOne: boolean
): PushRuleData => {
@@ -33,7 +34,7 @@ const getAllMessageDefaultRule = (
});
return {
- kind: PushRuleKind.Underride,
+ kind: 'underride',
pushRule: {
rule_id: ruleId,
default: true,
@@ -45,8 +46,8 @@ const getAllMessageDefaultRule = (
};
type PushRulesProps = {
- ruleId: RuleId.DM | RuleId.EncryptedDM | RuleId.Message | RuleId.EncryptedMessage;
- pushRules: IPushRules;
+ ruleId: PushRuleId.DM | PushRuleId.EncryptedDM | PushRuleId.Message | PushRuleId.EncryptedMessage;
+ pushRules: PushRules;
encrypted?: boolean;
oneToOne?: boolean;
};
@@ -56,7 +57,7 @@ function AllMessagesModeSwitcher({
encrypted = false,
oneToOne = false,
}: PushRulesProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const defaultPushRuleData = getAllMessageDefaultRule(ruleId, encrypted, oneToOne);
const { kind, pushRule } = usePushRule(pushRules, ruleId) ?? defaultPushRuleData;
const getModeActions = useNotificationModeActions();
@@ -64,20 +65,26 @@ function AllMessagesModeSwitcher({
const handleChange = useCallback(
async (mode: NotificationMode) => {
const actions = getModeActions(mode);
- await mx.setPushRuleActions('global', kind, ruleId, actions);
+ await ct.requestAuthJSON(
+ putPushRuleActions({
+ pathParams: { scope: 'global', kind, ruleId },
+ body: { actions },
+ })
+ );
},
- [mx, getModeActions, kind, ruleId]
+ [ct, getModeActions, kind, ruleId]
);
return ;
}
export function AllMessagesNotifications() {
- const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
- const pushRules = useMemo(
- () => pushRulesEvt?.getContent() ?? { global: {} },
- [pushRulesEvt]
- );
+ const pushRuleEventResult = useAccountData(AccountDataType.PushRules);
+ const pushRules: PushRules | undefined = useMemo(() => {
+ if (pushRuleEventResult.pending) return undefined;
+ const content = pushRuleEventResult.value?.content as PushRules;
+ return content ?? { global: {} };
+ }, [pushRuleEventResult]);
return (
@@ -98,7 +105,11 @@ export function AllMessagesNotifications() {
>
}
+ after={
+ pushRules && (
+
+ )
+ }
/>
+ pushRules && (
+
+ )
}
/>
@@ -127,7 +140,11 @@ export function AllMessagesNotifications() {
>
}
+ after={
+ pushRules && (
+
+ )
+ }
/>
+ pushRules && (
+
+ )
}
/>
diff --git a/src/app/features/settings/notifications/KeywordMessages.tsx b/src/app/features/settings/notifications/KeywordMessages.tsx
index 7f84d607..259736c8 100644
--- a/src/app/features/settings/notifications/KeywordMessages.tsx
+++ b/src/app/features/settings/notifications/KeywordMessages.tsx
@@ -1,12 +1,16 @@
import React, { ChangeEventHandler, FormEventHandler, useCallback, useMemo, useState } from 'react';
-import { IPushRule, IPushRules, PushRuleKind } from 'matrix-js-sdk';
import { Box, Text, Badge, Button, Input, config, IconButton, Icons, Icon, Spinner } from 'folds';
-import { useAccountData } from '../../../hooks/useAccountData';
-import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import {
+ deletePushRule,
+ putPushRule,
+ putPushRuleActions,
+} from 'neurone/lib/api/matrix/client/v3/pushrules';
+import { PushRule, PushRules } from 'neurone/lib/api/types';
+import { useAccountData } from '../../../hooks/storage/useAccountData';
+import { AccountDataType } from '../../../../types/matrix/accountData';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import {
getNotificationModeActions,
NotificationMode,
@@ -15,25 +19,31 @@ import {
} from '../../../hooks/useNotificationMode';
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { useClient } from '../../../hooks/useClient';
const NOTIFY_MODE_OPS: NotificationModeOptions = {
highlight: true,
};
function KeywordInput() {
- const mx = useMatrixClient();
+ const ct = useClient();
const [keyword, setKeyword] = useState('');
const [keywordState, addKeyword] = useAsyncCallback(
useCallback(
async (k: string) => {
- mx.addPushRule('global', PushRuleKind.ContentSpecific, k, {
- actions: getNotificationModeActions(NotificationMode.Notify, NOTIFY_MODE_OPS),
- pattern: k,
- });
+ await ct.requestAuthJSON(
+ putPushRule({
+ pathParams: { scope: 'global', kind: 'content', ruleId: k },
+ body: {
+ actions: getNotificationModeActions(NotificationMode.Notify, NOTIFY_MODE_OPS),
+ pattern: k,
+ },
+ })
+ );
setKeyword('');
},
- [mx]
+ [ct]
)
);
const addingKeyword = keywordState.status === AsyncStatus.Loading;
@@ -104,15 +114,20 @@ function KeywordInput() {
}
type PushRulesProps = {
- pushRule: IPushRule;
+ pushRule: PushRule;
};
function KeywordCross({ pushRule }: PushRulesProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const [removeState, remove] = useAsyncCallback(
useCallback(
- () => mx.deletePushRule('global', PushRuleKind.ContentSpecific, pushRule.rule_id),
- [mx, pushRule]
+ () =>
+ ct.requestAuthJSON(
+ deletePushRule({
+ pathParams: { scope: 'global', kind: 'content', ruleId: pushRule.rule_id },
+ })
+ ),
+ [ct, pushRule]
)
);
@@ -125,35 +140,38 @@ function KeywordCross({ pushRule }: PushRulesProps) {
}
function KeywordModeSwitcher({ pushRule }: PushRulesProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const getModeActions = useNotificationModeActions(NOTIFY_MODE_OPS);
const handleChange = useCallback(
async (mode: NotificationMode) => {
const actions = getModeActions(mode);
- await mx.setPushRuleActions(
- 'global',
- PushRuleKind.ContentSpecific,
- pushRule.rule_id,
- actions
+ await ct.requestAuthJSON(
+ putPushRuleActions({
+ pathParams: { scope: 'global', kind: 'content', ruleId: pushRule.rule_id },
+ body: {
+ actions,
+ },
+ })
);
},
- [mx, getModeActions, pushRule]
+ [ct, getModeActions, pushRule]
);
return ;
}
export function KeywordMessagesNotifications() {
- const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
- const pushRules = useMemo(
- () => pushRulesEvt?.getContent() ?? { global: {} },
- [pushRulesEvt]
- );
+ const pushRuleEventResult = useAccountData(AccountDataType.PushRules);
+ const pushRules: PushRules | undefined = useMemo(() => {
+ if (pushRuleEventResult.pending) return undefined;
+ const content = pushRuleEventResult.value?.content as PushRules;
+ return content ?? { global: {} };
+ }, [pushRuleEventResult]);
const keywordPushRules = useMemo(() => {
- const content = pushRules.global.content ?? [];
+ const content = pushRules?.global.content ?? [];
return content.filter(
(pushRule) => pushRule.default === false && typeof pushRule.pattern === 'string'
);
diff --git a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
index fe008390..9e353d6f 100644
--- a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
+++ b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx
@@ -11,9 +11,9 @@ import {
Spinner,
Text,
} from 'folds';
-import { IPushRule } from 'matrix-js-sdk';
import React, { MouseEventHandler, useMemo, useState } from 'react';
import FocusTrap from 'focus-trap-react';
+import { PushRule } from 'neurone/lib/api/types';
import { NotificationMode, useNotificationActionsMode } from '../../../hooks/useNotificationMode';
import { stopPropagation } from '../../../utils/keyboard';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
@@ -32,7 +32,7 @@ const useNotificationModeStr = (): Record =>
);
type NotificationModeSwitcherProps = {
- pushRule: IPushRule;
+ pushRule: PushRule;
onChange: (mode: NotificationMode) => Promise;
};
export function NotificationModeSwitcher({ pushRule, onChange }: NotificationModeSwitcherProps) {
diff --git a/src/app/features/settings/notifications/SpecialMessages.tsx b/src/app/features/settings/notifications/SpecialMessages.tsx
index cbebf648..edea496b 100644
--- a/src/app/features/settings/notifications/SpecialMessages.tsx
+++ b/src/app/features/settings/notifications/SpecialMessages.tsx
@@ -1,12 +1,12 @@
import React, { useCallback, useMemo } from 'react';
-import { ConditionKind, IPushRules, PushRuleKind, RuleId } from 'matrix-js-sdk';
import { Box, Text, Badge } from 'folds';
-import { useAccountData } from '../../../hooks/useAccountData';
-import { AccountDataEvent } from '../../../../types/matrix/accountData';
+import { PushRules } from 'neurone/lib/api/types';
+import { putPushRuleActions } from 'neurone/lib/api/matrix/client/v3/pushrules';
+import { useAccountData } from '../../../hooks/storage/useAccountData';
+import { AccountDataType, ConditionKind, PushRuleId } from '../../../../types/matrix/accountData';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useUserProfile } from '../../../hooks/useUserProfile';
import { getMxIdLocalPart } from '../../../utils/matrix';
import { makePushRuleData, PushRuleData, usePushRule } from '../../../hooks/usePushRule';
@@ -17,14 +17,15 @@ import {
useNotificationModeActions,
} from '../../../hooks/useNotificationMode';
import { NotificationModeSwitcher } from './NotificationModeSwitcher';
+import { useClient } from '../../../hooks/useClient';
const NOTIFY_MODE_OPS: NotificationModeOptions = {
highlight: true,
};
const getDefaultIsUserMention = (userId: string): PushRuleData =>
makePushRuleData(
- PushRuleKind.Override,
- RuleId.IsUserMention,
+ 'override',
+ PushRuleId.IsUserMention,
getNotificationModeActions(NotificationMode.NotifyLoud, { highlight: true }),
[
{
@@ -36,8 +37,8 @@ const getDefaultIsUserMention = (userId: string): PushRuleData =>
);
const DefaultContainsDisplayName = makePushRuleData(
- PushRuleKind.Override,
- RuleId.ContainsDisplayName,
+ 'override',
+ PushRuleId.ContainsDisplayName,
getNotificationModeActions(NotificationMode.NotifyLoud, { highlight: true }),
[
{
@@ -48,16 +49,16 @@ const DefaultContainsDisplayName = makePushRuleData(
const getDefaultContainsUsername = (username: string) =>
makePushRuleData(
- PushRuleKind.ContentSpecific,
- RuleId.ContainsUserName,
+ 'content',
+ PushRuleId.ContainsUserName,
getNotificationModeActions(NotificationMode.NotifyLoud, { highlight: true }),
undefined,
username
);
const DefaultIsRoomMention = makePushRuleData(
- PushRuleKind.Override,
- RuleId.IsRoomMention,
+ 'override',
+ PushRuleId.IsRoomMention,
getNotificationModeActions(NotificationMode.Notify, { highlight: true }),
[
{
@@ -73,8 +74,8 @@ const DefaultIsRoomMention = makePushRuleData(
);
const DefaultAtRoomNotification = makePushRuleData(
- PushRuleKind.Override,
- RuleId.AtRoomNotification,
+ 'override',
+ PushRuleId.AtRoomNotification,
getNotificationModeActions(NotificationMode.Notify, { highlight: true }),
[
{
@@ -90,12 +91,12 @@ const DefaultAtRoomNotification = makePushRuleData(
);
type PushRulesProps = {
- ruleId: RuleId;
- pushRules: IPushRules;
+ ruleId: PushRuleId;
+ pushRules: PushRules;
defaultPushRuleData: PushRuleData;
};
function MentionModeSwitcher({ ruleId, pushRules, defaultPushRuleData }: PushRulesProps) {
- const mx = useMatrixClient();
+ const ct = useClient();
const { kind, pushRule } = usePushRule(pushRules, ruleId) ?? defaultPushRuleData;
const getModeActions = useNotificationModeActions(NOTIFY_MODE_OPS);
@@ -103,23 +104,30 @@ function MentionModeSwitcher({ ruleId, pushRules, defaultPushRuleData }: PushRul
const handleChange = useCallback(
async (mode: NotificationMode) => {
const actions = getModeActions(mode);
- await mx.setPushRuleActions('global', kind, ruleId, actions);
+ await ct.requestAuthJSON(
+ putPushRuleActions({
+ pathParams: { scope: 'global', kind, ruleId },
+ body: { actions },
+ })
+ );
},
- [mx, getModeActions, kind, ruleId]
+ [ct, getModeActions, kind, ruleId]
);
return ;
}
export function SpecialMessagesNotifications() {
- const mx = useMatrixClient();
- const userId = mx.getUserId()!;
- const { displayName } = useUserProfile(userId);
- const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
- const pushRules = useMemo(
- () => pushRulesEvt?.getContent() ?? { global: {} },
- [pushRulesEvt]
- );
+ const ct = useClient();
+ const { userId } = ct.session;
+ const { displayName } = useUserProfile(userId) ?? {};
+
+ const pushRuleEventResult = useAccountData(AccountDataType.PushRules);
+ const pushRules: PushRules | undefined = useMemo(() => {
+ if (pushRuleEventResult.pending) return undefined;
+ const content = pushRuleEventResult.value?.content as PushRules;
+ return content ?? { global: {} };
+ }, [pushRuleEventResult]);
return (
@@ -141,11 +149,13 @@ export function SpecialMessagesNotifications() {
+ pushRules && (
+
+ )
}
/>
@@ -158,11 +168,13 @@ export function SpecialMessagesNotifications() {
+ pushRules && (
+
+ )
}
/>
@@ -175,11 +187,13 @@ export function SpecialMessagesNotifications() {
+ pushRules && (
+
+ )
}
/>
@@ -192,11 +206,13 @@ export function SpecialMessagesNotifications() {
+ pushRules && (
+
+ )
}
/>
@@ -209,11 +225,13 @@ export function SpecialMessagesNotifications() {
+ pushRules && (
+
+ )
}
/>
diff --git a/src/app/features/settings/notifications/SystemNotification.tsx b/src/app/features/settings/notifications/SystemNotification.tsx
index e0df06df..11c077ff 100644
--- a/src/app/features/settings/notifications/SystemNotification.tsx
+++ b/src/app/features/settings/notifications/SystemNotification.tsx
@@ -1,6 +1,7 @@
import React, { useCallback } from 'react';
import { Box, Text, Switch, Button, color, Spinner } from 'folds';
-import { IPusherRequest } from 'matrix-js-sdk';
+import { postPushersSet } from 'neurone/lib/api/matrix/client/v3/pushers';
+import { PusherData } from 'neurone/lib/api/types';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
@@ -9,37 +10,45 @@ import { settingsAtom } from '../../../state/settings';
import { getNotificationState, usePermissionState } from '../../../hooks/usePermission';
import { useEmailNotifications } from '../../../hooks/useEmailNotifications';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
-import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { useClient } from '../../../hooks/useClient';
function EmailNotification() {
- const mx = useMatrixClient();
+ const ct = useClient();
const [result, refreshResult] = useEmailNotifications();
const [setState, setEnable] = useAsyncCallback(
useCallback(
async (email: string, enable: boolean) => {
if (enable) {
- await mx.setPusher({
- kind: 'email',
- app_id: 'm.email',
- pushkey: email,
- app_display_name: 'Email Notifications',
- device_display_name: email,
- lang: 'en',
- data: {
- brand: 'Cinny',
- },
- append: true,
- });
+ await ct.requestAuthJSON(
+ postPushersSet({
+ body: {
+ kind: 'email',
+ app_id: 'm.email',
+ pushkey: email,
+ app_display_name: 'Email Notifications',
+ device_display_name: email,
+ lang: 'en',
+ data: {
+ brand: 'Cinny', // brand field is not in spec.
+ } as PusherData,
+ append: true,
+ },
+ })
+ );
return;
}
- await mx.setPusher({
- pushkey: email,
- app_id: 'm.email',
- kind: null,
- } as unknown as IPusherRequest);
+ await ct.requestAuthJSON(
+ postPushersSet({
+ body: {
+ pushkey: email,
+ app_id: 'm.email',
+ kind: null,
+ },
+ })
+ );
},
- [mx]
+ [ct]
)
);
diff --git a/src/app/hooks/router/useSelectedRoom.ts b/src/app/hooks/router/useSelectedRoom.ts
index 6d0ee4f1..8358aeb0 100644
--- a/src/app/hooks/router/useSelectedRoom.ts
+++ b/src/app/hooks/router/useSelectedRoom.ts
@@ -1,15 +1,15 @@
import { useParams } from 'react-router-dom';
-import { getCanonicalAliasRoomId, isRoomAlias } from '../../utils/matrix';
-import { useMatrixClient } from '../useMatrixClient';
+import { isRoomAlias } from '../../utils/matrix';
+import { useClient } from '../useClient';
export const useSelectedRoom = (): string | undefined => {
- const mx = useMatrixClient();
+ const ct = useClient();
const { roomIdOrAlias } = useParams();
- const roomId =
- roomIdOrAlias && isRoomAlias(roomIdOrAlias)
- ? getCanonicalAliasRoomId(mx, roomIdOrAlias)
- : roomIdOrAlias;
- return roomId;
+ if (roomIdOrAlias && isRoomAlias(roomIdOrAlias)) {
+ return ct.rooms.join.read.find((roomId) => ct.roomToAlias.read.get(roomId) === roomIdOrAlias);
+ }
+
+ return roomIdOrAlias;
};
diff --git a/src/app/hooks/router/useSelectedSpace.ts b/src/app/hooks/router/useSelectedSpace.ts
index 2e8e73a0..dce85948 100644
--- a/src/app/hooks/router/useSelectedSpace.ts
+++ b/src/app/hooks/router/useSelectedSpace.ts
@@ -1,17 +1,23 @@
import { useMatch, useParams } from 'react-router-dom';
-import { getCanonicalAliasRoomId, isRoomAlias } from '../../utils/matrix';
-import { useMatrixClient } from '../useMatrixClient';
+import { isRoomAlias } from '../../utils/matrix';
import { getSpaceLobbyPath, getSpaceSearchPath } from '../../pages/pathUtils';
+import { useClient } from '../useClient';
+import { useObservableValue } from '../useObservableValue';
export const useSelectedSpace = (): string | undefined => {
- const mx = useMatrixClient();
-
+ const ct = useClient();
const { spaceIdOrAlias } = useParams();
- const spaceId =
- spaceIdOrAlias && isRoomAlias(spaceIdOrAlias)
- ? getCanonicalAliasRoomId(mx, spaceIdOrAlias)
- : spaceIdOrAlias;
+ const roomToAlias = useObservableValue(ct.roomToAlias);
+
+ if (!spaceIdOrAlias) return undefined;
+
+ const spaceId = isRoomAlias(spaceIdOrAlias)
+ ? ct.rooms.join.read.find(
+ (roomId) =>
+ roomToAlias.get(roomId) === spaceIdOrAlias && !ct.roomToReplacement.read.has(roomId)
+ )
+ : spaceIdOrAlias;
return spaceId;
};
diff --git a/src/app/hooks/storage/useAccountData.ts b/src/app/hooks/storage/useAccountData.ts
new file mode 100644
index 00000000..83b5c4d7
--- /dev/null
+++ b/src/app/hooks/storage/useAccountData.ts
@@ -0,0 +1,25 @@
+import { useCallback, useMemo } from 'react';
+import { AccountDataEvent } from 'neurone/lib/api/types';
+import { PendingObservableResult } from 'neurone/lib/utils/observable';
+import { useClient } from '../useClient';
+import {
+ useComputedPendingObservableResult,
+ useObservableValuePromise,
+} from '../useObservableValue';
+
+export function useAccountData(
+ eventType: string
+): PendingObservableResult {
+ const ct = useClient();
+ const observablePromise = useMemo(
+ () => ct.storage.accountData.observableRead.get(eventType),
+ [ct, eventType]
+ );
+
+ const result = useObservableValuePromise(observablePromise);
+
+ return useComputedPendingObservableResult(
+ result,
+ useCallback((v) => v?.event as T | undefined, [])
+ );
+}
diff --git a/src/app/hooks/storage/useGetRoomAvatar.ts b/src/app/hooks/storage/useGetRoomAvatar.ts
new file mode 100644
index 00000000..b2fe8716
--- /dev/null
+++ b/src/app/hooks/storage/useGetRoomAvatar.ts
@@ -0,0 +1,36 @@
+import { useCallback } from 'react';
+import { useClient } from '../useClient';
+import { useObservableValue } from '../useObservableValue';
+import { GetRoomHeroesCallback, RoomToHeroMembers } from './useRoomHeros';
+import { roomMemberContentAvatarUrl } from '../../utils/room';
+
+export type GetRoomAvatarCallback = (roomId: string) => string | undefined;
+
+export const useGetRoomAvatar = (
+ getRoomHeroes: GetRoomHeroesCallback,
+ roomToHeroMembers: RoomToHeroMembers | undefined
+): GetRoomAvatarCallback => {
+ const ct = useClient();
+ const roomToAvatar = useObservableValue(ct.roomToAvatar);
+
+ const getRoomAvatar: GetRoomAvatarCallback = useCallback(
+ (roomId: string) => {
+ const avatar = roomToAvatar.get(roomId);
+ if (avatar) return avatar;
+
+ const members = roomToHeroMembers?.get(roomId);
+
+ const heroesAvatar: string[] = [];
+ getRoomHeroes(roomId).forEach((userId) => {
+ const content = members?.get(userId)?.content;
+ const avatarUrl = content ? roomMemberContentAvatarUrl(content) : undefined;
+ if (avatarUrl) heroesAvatar.push(avatarUrl);
+ });
+
+ return heroesAvatar[0];
+ },
+ [roomToAvatar, getRoomHeroes, roomToHeroMembers]
+ );
+
+ return getRoomAvatar;
+};
diff --git a/src/app/hooks/storage/useGetRoomName.ts b/src/app/hooks/storage/useGetRoomName.ts
new file mode 100644
index 00000000..ca083daa
--- /dev/null
+++ b/src/app/hooks/storage/useGetRoomName.ts
@@ -0,0 +1,45 @@
+import { useCallback } from 'react';
+import { useClient } from '../useClient';
+import { useObservableValue } from '../useObservableValue';
+import { GetRoomHeroesCallback, RoomToHeroMembers } from './useRoomHeros';
+import { getMxIdLocalPart } from '../../utils/matrix';
+import { roomMemberContentDisplayname } from '../../utils/room';
+
+export type GetRoomNameCallback = (roomId: string) => string | undefined;
+
+export const useGetRoomName = (
+ getRoomHeroes: GetRoomHeroesCallback,
+ roomToHeroMembers: RoomToHeroMembers | undefined
+): GetRoomNameCallback => {
+ const ct = useClient();
+ const roomToName = useObservableValue(ct.roomToName);
+ const roomToAlias = useObservableValue(ct.roomToAlias);
+
+ const getRoomName: GetRoomNameCallback = useCallback(
+ (roomId: string) => {
+ const name = roomToName.get(roomId);
+ if (name) return name;
+
+ const alias = roomToAlias.get(roomId);
+ if (alias) return alias;
+
+ const members = roomToHeroMembers?.get(roomId);
+
+ const heroesName: string[] = getRoomHeroes(roomId).map((userId) => {
+ const member = members?.get(userId);
+ if (!member) return getMxIdLocalPart(userId) ?? userId;
+ const { content } = member;
+ const username =
+ roomMemberContentDisplayname(content)?.trim() || getMxIdLocalPart(userId) || userId;
+ return username;
+ });
+
+ if (heroesName) return heroesName.join(', ');
+
+ return undefined;
+ },
+ [roomToName, roomToAlias, getRoomHeroes, roomToHeroMembers]
+ );
+
+ return getRoomName;
+};
diff --git a/src/app/hooks/storage/useRoomHeros.ts b/src/app/hooks/storage/useRoomHeros.ts
new file mode 100644
index 00000000..c1a214e0
--- /dev/null
+++ b/src/app/hooks/storage/useRoomHeros.ts
@@ -0,0 +1,57 @@
+import { useCallback, useMemo } from 'react';
+import { PendingObservableResult } from 'neurone/lib/utils/observable';
+import { StateEventWithoutRoomId } from 'neurone/lib/api/types';
+import { useClient } from '../useClient';
+import {
+ useComputedPendingObservableResult,
+ useObservableValue,
+ useObservableValuePromise,
+} from '../useObservableValue';
+import { getJoinRoomMemberId } from '../../../client/storage/collections';
+
+export type UserIdToRoomMember = Map;
+export type RoomToHeroMembers = Map;
+
+export type GetRoomHeroesCallback = (roomId: string) => string[];
+
+export const useRoomHeroes = (
+ roomOrRooms: string | string[]
+): [GetRoomHeroesCallback, PendingObservableResult] => {
+ const ct = useClient();
+ const roomToSummary = useObservableValue(ct.roomToSummary);
+
+ const getHeroes: GetRoomHeroesCallback = useCallback(
+ (roomId) => roomToSummary.get(roomId)?.['m.heroes'] ?? [],
+ [roomToSummary]
+ );
+
+ const membersResult = useObservableValuePromise(
+ useMemo(() => {
+ const ids = Array.isArray(roomOrRooms)
+ ? roomOrRooms.flatMap((roomId) =>
+ getHeroes(roomId).map((userId) => getJoinRoomMemberId(roomId, userId))
+ )
+ : getHeroes(roomOrRooms).map((userId) => getJoinRoomMemberId(roomOrRooms, userId));
+
+ return ct.storage.joinRoomMember.observableRead.getMultiple(ids);
+ }, [roomOrRooms, getHeroes, ct])
+ );
+
+ const roomToHeroesResult: PendingObservableResult =
+ useComputedPendingObservableResult(
+ membersResult,
+ useCallback((members) => {
+ const roomToHeroes = new Map();
+ members.forEach((member) => {
+ if (!member) return;
+ const { roomId, userId, event } = member;
+ const userToEvent: UserIdToRoomMember = roomToHeroes.get(roomId) ?? new Map();
+ userToEvent.set(userId, event);
+ roomToHeroes.set(roomId, userToEvent);
+ });
+ return roomToHeroes;
+ }, [])
+ );
+
+ return [getHeroes, roomToHeroesResult];
+};
diff --git a/src/app/hooks/storage/useRoomStateEvent.ts b/src/app/hooks/storage/useRoomStateEvent.ts
new file mode 100644
index 00000000..6c26559f
--- /dev/null
+++ b/src/app/hooks/storage/useRoomStateEvent.ts
@@ -0,0 +1,45 @@
+import { useMemo } from 'react';
+import { PendingObservableResult } from 'neurone/lib/utils/observable';
+import { QueryRange } from 'neurone/lib/storage/collection';
+import { getJoinRoomStateId, JoinRoomStateObject } from '../../../client/storage/collections';
+import { useClient } from '../useClient';
+import { useObservableValuePromise } from '../useObservableValue';
+import { MAX_UNICODE } from '../../../client/storage';
+
+export const useRoomStateEvent = (
+ roomId: string,
+ eventType: string,
+ stateKey: string
+): PendingObservableResult => {
+ const ct = useClient();
+ const stateEventPromise = useMemo(
+ () =>
+ ct.storage.joinRoomState.observableRead.get(getJoinRoomStateId(roomId, eventType, stateKey)),
+ [ct, roomId, eventType, stateKey]
+ );
+
+ const stateEventResult = useObservableValuePromise(stateEventPromise);
+
+ return stateEventResult;
+};
+
+export const useRoomStateEvents = (
+ roomId: string,
+ eventType: string
+): PendingObservableResult => {
+ const ct = useClient();
+ const stateEventPromise = useMemo(
+ () =>
+ ct.storage.joinRoomState.observableRead.query(
+ new QueryRange(
+ getJoinRoomStateId(roomId, eventType, ''),
+ getJoinRoomStateId(roomId, eventType, MAX_UNICODE)
+ )
+ ),
+ [ct, roomId, eventType]
+ );
+
+ const stateEventResult = useObservableValuePromise(stateEventPromise);
+
+ return stateEventResult;
+};
diff --git a/src/app/hooks/storage/useRoomTopic.ts b/src/app/hooks/storage/useRoomTopic.ts
new file mode 100644
index 00000000..14e1cebb
--- /dev/null
+++ b/src/app/hooks/storage/useRoomTopic.ts
@@ -0,0 +1,11 @@
+import { StateEventType } from '../../../types/matrix/room';
+import { useRoomStateEvent } from './useRoomStateEvent';
+
+export const useRoomTopic = (roomId: string): string | undefined => {
+ const topicEventResult = useRoomStateEvent(roomId, StateEventType.RoomTopic, '');
+
+ const content = topicEventResult.value?.event.content;
+ if (content && 'topic' in content && typeof content.topic === 'string') return content.topic;
+
+ return undefined;
+};
diff --git a/src/app/hooks/types.ts b/src/app/hooks/types.ts
index 9aac2b31..1ec9dfea 100644
--- a/src/app/hooks/types.ts
+++ b/src/app/hooks/types.ts
@@ -1,12 +1,17 @@
-import { IRequestTokenResponse } from 'matrix-js-sdk';
+import { PostEmailRegisterTokenOK } from 'neurone/lib/api/matrix/client/v3/register';
+import { MatrixError } from 'neurone/lib/matrix-request';
export type RequestEmailTokenResponse = {
email: string;
clientSecret: string;
- result: IRequestTokenResponse;
+ result: PostEmailRegisterTokenOK;
};
+export type RequestEmailTokenResult =
+ | [undefined, RequestEmailTokenResponse]
+ | [MatrixError, undefined];
+
export type RequestEmailTokenCallback = (
email: string,
clientSecret: string,
nextLink?: string
-) => Promise;
+) => Promise;
diff --git a/src/app/hooks/useAuthFlows.ts b/src/app/hooks/useAuthFlows.ts
index 7bb7ddc5..43faa6bb 100644
--- a/src/app/hooks/useAuthFlows.ts
+++ b/src/app/hooks/useAuthFlows.ts
@@ -1,6 +1,7 @@
import { createContext, useContext } from 'react';
-import { IAuthData, MatrixError } from 'matrix-js-sdk';
-import { ILoginFlowsResponse } from 'matrix-js-sdk/lib/@types/auth';
+import { MatrixError, MatrixErrorData } from 'neurone/lib/matrix-request';
+import { GetLoginOK } from 'neurone/lib/api/matrix/client/v3/login';
+import { AuthResponse } from '../utils/matrix-uia';
export enum RegisterFlowStatus {
FlowRequired = 401,
@@ -12,14 +13,16 @@ export enum RegisterFlowStatus {
export type RegisterFlowsResponse =
| {
status: RegisterFlowStatus.FlowRequired;
- data: IAuthData;
+ data: AuthResponse;
}
| {
status: Exclude;
};
-export const parseRegisterErrResp = (matrixError: MatrixError): RegisterFlowsResponse => {
- switch (matrixError.httpStatus) {
+export const parseRegisterErrResp = (
+ matrixError: MatrixError
+): RegisterFlowsResponse => {
+ switch (matrixError.status) {
case RegisterFlowStatus.InvalidRequest: {
return { status: RegisterFlowStatus.InvalidRequest };
}
@@ -32,7 +35,7 @@ export const parseRegisterErrResp = (matrixError: MatrixError): RegisterFlowsRes
case RegisterFlowStatus.FlowRequired: {
return {
status: RegisterFlowStatus.FlowRequired,
- data: matrixError.data as IAuthData,
+ data: matrixError.data as AuthResponse,
};
}
default: {
@@ -42,7 +45,7 @@ export const parseRegisterErrResp = (matrixError: MatrixError): RegisterFlowsRes
};
export type AuthFlows = {
- loginFlows: ILoginFlowsResponse;
+ loginFlows: GetLoginOK;
registerFlows: RegisterFlowsResponse;
};
diff --git a/src/app/hooks/useAuthMetadata.ts b/src/app/hooks/useAuthMetadata.ts
index db967463..bc991930 100644
--- a/src/app/hooks/useAuthMetadata.ts
+++ b/src/app/hooks/useAuthMetadata.ts
@@ -1,11 +1,11 @@
-import { ValidatedAuthMetadata } from 'matrix-js-sdk';
+import { GetAuthMetadataOK } from 'neurone/lib/api/matrix/client/v1/authMetadata';
import { createContext, useContext } from 'react';
-const AuthMetadataContext = createContext(undefined);
+const AuthMetadataContext = createContext(undefined);
export const AuthMetadataProvider = AuthMetadataContext.Provider;
-export const useAuthMetadata = (): ValidatedAuthMetadata | undefined => {
+export const useAuthMetadata = (): GetAuthMetadataOK | undefined => {
const metadata = useContext(AuthMetadataContext);
return metadata;
diff --git a/src/app/hooks/useCapabilities.ts b/src/app/hooks/useCapabilities.ts
index b2fbd640..72aeecac 100644
--- a/src/app/hooks/useCapabilities.ts
+++ b/src/app/hooks/useCapabilities.ts
@@ -1,6 +1,31 @@
-import { Capabilities } from 'matrix-js-sdk';
import { createContext, useContext } from 'react';
+export interface ICapability {
+ enabled: boolean;
+}
+
+export enum RoomVersionStability {
+ Stable = 'stable',
+ Unstable = 'unstable',
+}
+
+export interface IRoomVersionsCapability {
+ default: string;
+ available: Record;
+}
+
+export interface Capabilities {
+ [key: string]: any;
+ 'm.change_password'?: ICapability;
+ 'm.room_versions'?: IRoomVersionsCapability;
+ 'io.element.thread'?: ICapability;
+ 'm.get_login_token'?: ICapability;
+ 'org.matrix.msc3882.get_login_token'?: ICapability;
+ 'm.set_displayname'?: ICapability;
+ 'm.set_avatar_url'?: ICapability;
+ 'uk.tcpip.msc4133.profile_fields'?: ICapability;
+}
+
const CapabilitiesContext = createContext(null);
export const CapabilitiesProvider = CapabilitiesContext.Provider;
diff --git a/src/app/hooks/useClient.ts b/src/app/hooks/useClient.ts
new file mode 100644
index 00000000..aafd8287
--- /dev/null
+++ b/src/app/hooks/useClient.ts
@@ -0,0 +1,16 @@
+import { createContext, useContext } from 'react';
+import { ClientContext } from '../../client/context';
+
+const ClientCtx = createContext(null);
+
+export const ClientContextProvider = ClientCtx.Provider;
+
+export const useClient = (): ClientContext => {
+ const ct = useContext(ClientCtx);
+
+ if (ct === null) {
+ throw new Error('Client Context is not provided!');
+ }
+
+ return ct;
+};
diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts
index c02b0e94..9eb27a95 100644
--- a/src/app/hooks/useCommands.ts
+++ b/src/app/hooks/useCommands.ts
@@ -1,19 +1,25 @@
-import {
- Direction,
- EventTimeline,
- IContextResponse,
- MatrixClient,
- Method,
- Preset,
- Room,
- RoomMember,
- Visibility,
-} from 'matrix-js-sdk';
-import { RoomServerAclEventContent } from 'matrix-js-sdk/lib/types';
import { useMemo } from 'react';
+import { postCreateRoom, PostCreateRoomOK } from 'neurone/lib/api/matrix/client/v3/createRoom';
+import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
+import { postJoin } from 'neurone/lib/api/matrix/client/v3/join';
+import {
+ getContext,
+ GetContextOK,
+ getMessages,
+ GetMessagesOK,
+ postBan,
+ postInvite,
+ postKick,
+ postLeave,
+ postUnban,
+ putRedact,
+ putStateEvent,
+} from 'neurone/lib/api/matrix/client/v3/rooms';
+import { putAccountData } from 'neurone/lib/api/matrix/client/v3/user';
+import { getTimestampToEvent, GetTimestampToEventOK } from 'neurone/lib/api/matrix/client/v1/rooms';
import {
addRoomIdToMDirect,
- getDMRoomFor,
+ getUserDirectRooms,
guessDmRoomUserId,
isRoomAlias,
isRoomId,
@@ -23,10 +29,19 @@ import {
removeRoomIdFromMDirect,
} from '../utils/matrix';
import { useRoomNavigate } from './useRoomNavigate';
-import { Membership, StateEvent } from '../../types/matrix/room';
-import { getStateEvent } from '../utils/room';
+import { RoomServerAclContent, StateEventType } from '../../types/matrix/room';
import { splitWithSpace } from '../utils/common';
import { createRoomEncryptionState } from '../components/create-room';
+import { useClient } from './useClient';
+import { useRoom } from './useRoom';
+import { useRoomMembersAPI } from './useRoomMembers';
+import {
+ getJoinRoomMemberId,
+ getJoinRoomStateId,
+ JoinRoomMemberObject,
+} from '../../client/storage/collections';
+import { AccountDataType } from '../../types/matrix/accountData';
+import { getIgnoredUsersContent, parseIgnoreUsersContent } from './useIgnoredUsers';
export const SHRUG = '¯\\_(ツ)_/¯';
export const TABLEFLIP = '(╯°□°)╯︵ ┻━┻';
@@ -36,7 +51,7 @@ const FLAG_PAT = '(?:^|\\s)-(\\w+)\\b';
const FLAG_REG = new RegExp(FLAG_PAT);
const FLAG_REG_G = new RegExp(FLAG_PAT, 'g');
-export const splitPayloadContentAndFlags = (payload: string): [string, string | undefined] => {
+const splitPayloadContentAndFlags = (payload: string): [string, string | undefined] => {
const flagMatch = payload.match(FLAG_REG);
if (!flagMatch) {
@@ -48,7 +63,7 @@ export const splitPayloadContentAndFlags = (payload: string): [string, string |
return [content, flags];
};
-export const parseFlags = (flags: string | undefined): Record => {
+const parseFlags = (flags: string | undefined): Record => {
const result: Record = {};
if (!flags) return result;
@@ -69,7 +84,7 @@ export const parseFlags = (flags: string | undefined): Record {
+const parseUsers = (payload: string): string[] => {
const users: string[] = [];
splitWithSpace(payload).forEach((item) => {
@@ -81,7 +96,7 @@ export const parseUsers = (payload: string): string[] => {
return users;
};
-export const parseServers = (payload: string): string[] => {
+const parseServers = (payload: string): string[] => {
const servers: string[] = [];
splitWithSpace(payload).forEach((item) => {
@@ -93,13 +108,10 @@ export const parseServers = (payload: string): string[] => {
return servers;
};
-const getServerMembers = (room: Room, server: string): RoomMember[] => {
- const members: RoomMember[] = room
- .getMembers()
- .filter((member) => member.userId.endsWith(`:${server}`));
-
- return members;
-};
+const getServerMembers = (
+ members: JoinRoomMemberObject[],
+ server: string
+): JoinRoomMemberObject[] => members.filter((member) => member.userId.endsWith(`:${server}`));
export const parseTimestampFlag = (input: string): number | undefined => {
const match = input.match(/^(\d+(?:\.\d+)?)([dhms])$/); // supports floats like 1.5d
@@ -169,7 +181,10 @@ export type CommandContent = {
export type CommandRecord = Record;
-export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
+export const useCommands = (): CommandRecord => {
+ const ct = useClient();
+ const roomId = useRoom();
+ const membersAPI = useRoomMembersAPI();
const { navigateRoom } = useRoomNavigate();
const commands: CommandRecord = useMemo(
@@ -204,23 +219,33 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
description: 'Start direct message with user. Example: /startdm userId1',
exe: async (payload) => {
const rawIds = splitWithSpace(payload);
- const userIds = rawIds.filter((id) => isUserId(id) && id !== mx.getSafeUserId());
+ const userIds = rawIds.filter((id) => isUserId(id) && id !== ct.session.userId);
if (userIds.length === 0) return;
if (userIds.length === 1) {
- const dmRoomId = getDMRoomFor(mx, userIds[0])?.roomId;
+ const dmRoomId = getUserDirectRooms(ct, userIds[0])[0];
if (dmRoomId) {
navigateRoom(dmRoomId);
return;
}
}
- const result = await mx.createRoom({
- is_direct: true,
- invite: userIds,
- visibility: Visibility.Private,
- preset: Preset.TrustedPrivateChat,
- initial_state: [createRoomEncryptionState()],
- });
- addRoomIdToMDirect(mx, result.room_id, userIds[0]);
+ const response = await ct.requestAuthJSON(
+ postCreateRoom({
+ body: {
+ is_direct: true,
+ invite: userIds,
+ visibility: 'private',
+ preset: 'trusted_private_chat',
+ initial_state: [createRoomEncryptionState()],
+ },
+ })
+ );
+ const [err, result] = await parseMatrixResp(response);
+ if (err) {
+ console.error(printMatrixError(err));
+ return;
+ }
+
+ addRoomIdToMDirect(ct, result.room_id, userIds[0]);
navigateRoom(result.room_id);
},
},
@@ -232,8 +257,8 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const roomIdOrAliases = rawIds.filter(
(idOrAlias) => isRoomId(idOrAlias) || isRoomAlias(idOrAlias)
);
- roomIdOrAliases.forEach(async (idOrAlias) => {
- await mx.joinRoom(idOrAlias);
+ roomIdOrAliases.forEach(async (roomIdOrAlias) => {
+ await ct.requestAuthJSON(postJoin({ pathParams: { roomIdOrAlias } }));
});
},
},
@@ -242,12 +267,12 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
description: 'Leave current room.',
exe: async (payload) => {
if (payload.trim() === '') {
- mx.leave(room.roomId);
+ ct.requestAuthJSON(postLeave({ pathParams: { roomId } }));
return;
}
const rawIds = splitWithSpace(payload);
const roomIds = rawIds.filter((id) => isRoomId(id));
- roomIds.map((id) => mx.leave(id));
+ roomIds.map((id) => ct.requestAuthJSON(postLeave({ pathParams: { roomId: id } })));
},
},
[Command.Invite]: {
@@ -258,7 +283,14 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const users = parseUsers(content);
const flagToContent = parseFlags(flags);
const reason = flagToContent.r;
- users.map((id) => mx.invite(room.roomId, id, reason));
+ users.map((id) =>
+ ct.requestAuthJSON(
+ postInvite({
+ pathParams: { roomId },
+ body: { user_id: id, reason },
+ })
+ )
+ );
},
},
[Command.DisInvite]: {
@@ -269,7 +301,14 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const users = parseUsers(content);
const flagToContent = parseFlags(flags);
const reason = flagToContent.r;
- users.map((id) => mx.kick(room.roomId, id, reason));
+ users.map((id) =>
+ ct.requestAuthJSON(
+ postKick({
+ pathParams: { roomId },
+ body: { user_id: id, reason },
+ })
+ )
+ );
},
},
[Command.Kick]: {
@@ -282,9 +321,11 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const flagToContent = parseFlags(flags);
const reason = flagToContent.r;
- const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
+ const serverMembers = servers?.flatMap((server) =>
+ getServerMembers(membersAPI.memberEventsResult.value ?? [], server)
+ );
const serverUsers = serverMembers
- ?.filter((m) => m.membership !== Membership.Ban)
+ ?.filter((m) => membersAPI.getMembership(m.userId) !== 'ban')
.map((m) => m.userId);
if (Array.isArray(serverUsers)) {
@@ -293,7 +334,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
});
}
- rateLimitedActions(users, (id) => mx.kick(room.roomId, id, reason));
+ rateLimitedActions(users, async (id) => {
+ const response = await ct.requestAuthJSON(
+ postKick({
+ pathParams: { roomId },
+ body: { user_id: id, reason },
+ })
+ );
+ const [err] = await parseMatrixResp(response);
+ return err;
+ });
},
},
[Command.Ban]: {
@@ -306,7 +356,9 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const flagToContent = parseFlags(flags);
const reason = flagToContent.r;
- const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
+ const serverMembers = servers?.flatMap((server) =>
+ getServerMembers(membersAPI.memberEventsResult.value ?? [], server)
+ );
const serverUsers = serverMembers?.map((m) => m.userId);
if (Array.isArray(serverUsers)) {
@@ -315,7 +367,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
});
}
- rateLimitedActions(users, (id) => mx.ban(room.roomId, id, reason));
+ rateLimitedActions(users, async (id) => {
+ const response = await ct.requestAuthJSON(
+ postBan({
+ pathParams: { roomId },
+ body: { user_id: id, reason },
+ })
+ );
+ const [err] = await parseMatrixResp(response);
+ return err;
+ });
},
},
[Command.UnBan]: {
@@ -324,7 +385,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
exe: async (payload) => {
const rawIds = splitWithSpace(payload);
const users = rawIds.filter((id) => isUserId(id));
- users.map((id) => mx.unban(room.roomId, id));
+ rateLimitedActions(users, async (id) => {
+ const response = await ct.requestAuthJSON(
+ postUnban({
+ pathParams: { roomId },
+ body: { user_id: id },
+ })
+ );
+ const [err] = await parseMatrixResp(response);
+ return err;
+ });
},
},
[Command.Ignore]: {
@@ -334,9 +404,17 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const rawIds = splitWithSpace(payload);
const userIds = rawIds.filter((id) => isUserId(id));
if (userIds.length > 0) {
- let ignoredUsers = mx.getIgnoredUsers().concat(userIds);
- ignoredUsers = [...new Set(ignoredUsers)];
- await mx.setIgnoredUsers(ignoredUsers);
+ const ignoreUserListObj = await ct.storage.accountData.read.get(
+ AccountDataType.IgnoredUserList
+ );
+ const { content } = ignoreUserListObj?.event ?? {};
+ const ignoredUsers = parseIgnoreUsersContent(content).concat(userIds);
+ await ct.requestAuthJSON(
+ putAccountData({
+ pathParams: { userId: ct.session.userId, type: AccountDataType.IgnoredUserList },
+ body: getIgnoredUsersContent(ignoredUsers),
+ })
+ );
}
},
},
@@ -347,8 +425,19 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const rawIds = splitWithSpace(payload);
const userIds = rawIds.filter((id) => isUserId(id));
if (userIds.length > 0) {
- const ignoredUsers = mx.getIgnoredUsers();
- await mx.setIgnoredUsers(ignoredUsers.filter((id) => !userIds.includes(id)));
+ const ignoreUserListObj = await ct.storage.accountData.read.get(
+ AccountDataType.IgnoredUserList
+ );
+ const { content } = ignoreUserListObj?.event ?? {};
+ const ignoredUsers = parseIgnoreUsersContent(content).filter(
+ (id) => !userIds.includes(id)
+ );
+ await ct.requestAuthJSON(
+ putAccountData({
+ pathParams: { userId: ct.session.userId, type: AccountDataType.IgnoredUserList },
+ body: getIgnoredUsersContent(ignoredUsers),
+ })
+ );
}
},
},
@@ -358,20 +447,24 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
exe: async (payload) => {
const nick = payload.trim();
if (nick === '') return;
- const mEvent = room
- .getLiveTimeline()
- .getState(EventTimeline.FORWARDS)
- ?.getStateEvents(StateEvent.RoomMember, mx.getSafeUserId());
- const content = mEvent?.getContent();
+ const memberObj = await ct.storage.joinRoomMember.read.get(
+ getJoinRoomMemberId(roomId, ct.session.userId)
+ );
+ const { content } = memberObj?.event ?? {};
if (!content) return;
- await mx.sendStateEvent(
- room.roomId,
- StateEvent.RoomMember as any,
- {
- ...content,
- displayname: nick,
- },
- mx.getSafeUserId()
+
+ ct.requestAuthJSON(
+ putStateEvent({
+ pathParams: {
+ roomId,
+ eventType: StateEventType.RoomMember,
+ stateKey: ct.session.userId,
+ },
+ body: {
+ ...content,
+ displayname: nick,
+ },
+ })
);
},
},
@@ -380,20 +473,23 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
description: 'Change profile picture in current room. Example /myroomavatar mxc://xyzabc',
exe: async (payload) => {
if (payload.match(/^mxc:\/\/\S+$/)) {
- const mEvent = room
- .getLiveTimeline()
- .getState(EventTimeline.FORWARDS)
- ?.getStateEvents(StateEvent.RoomMember, mx.getSafeUserId());
- const content = mEvent?.getContent();
+ const memberObj = await ct.storage.joinRoomMember.read.get(
+ getJoinRoomMemberId(roomId, ct.session.userId)
+ );
+ const { content } = memberObj?.event ?? {};
if (!content) return;
- await mx.sendStateEvent(
- room.roomId,
- StateEvent.RoomMember as any,
- {
- ...content,
- avatar_url: payload,
- },
- mx.getSafeUserId()
+ ct.requestAuthJSON(
+ putStateEvent({
+ pathParams: {
+ roomId,
+ eventType: StateEventType.RoomMember,
+ stateKey: ct.session.userId,
+ },
+ body: {
+ ...content,
+ avatar_url: payload,
+ },
+ })
);
}
},
@@ -402,15 +498,18 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name: Command.ConvertToDm,
description: 'Convert room to direct message',
exe: async () => {
- const dmUserId = guessDmRoomUserId(room, mx.getSafeUserId());
- await addRoomIdToMDirect(mx, room.roomId, dmUserId);
+ const dmUserId = guessDmRoomUserId(
+ membersAPI.memberEventsResult.value ?? [],
+ ct.session.userId
+ );
+ await addRoomIdToMDirect(ct, roomId, dmUserId);
},
},
[Command.ConvertToRoom]: {
name: Command.ConvertToRoom,
description: 'Convert direct message to room',
exe: async () => {
- await removeRoomIdFromMDirect(mx, room.roomId);
+ await removeRoomIdFromMDirect(ct, roomId);
},
},
[Command.Delete]: {
@@ -431,7 +530,9 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const ts = parseTimestampFlag(pastContent);
if (!ts) return;
- const serverMembers = servers?.flatMap((server) => getServerMembers(room, server));
+ const serverMembers = servers?.flatMap((server) =>
+ getServerMembers(membersAPI.memberEventsResult.value ?? [], server)
+ );
const serverUsers = serverMembers?.map((m) => m.userId);
if (Array.isArray(serverUsers)) {
@@ -440,26 +541,50 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
});
}
- const result = await mx.timestampToEvent(room.roomId, ts, Direction.Forward);
+ const tsResp = await ct.requestAuthJSON(
+ getTimestampToEvent({
+ pathParams: { roomId },
+ searchParams: { ts, dir: 'f' },
+ })
+ );
+ const [err, result] = await parseMatrixResp(tsResp);
+ if (err) {
+ console.error(printMatrixError(err));
+ return;
+ }
const startEventId = result.event_id;
- const path = `/rooms/${encodeURIComponent(room.roomId)}/context/${encodeURIComponent(
- startEventId
- )}`;
- const eventContext = await mx.http.authedRequest(Method.Get, path, {
- limit: 0,
- });
+ const ctxResp = await ct.requestAuthJSON(
+ getContext({
+ pathParams: { roomId, eventId: startEventId },
+ searchParams: { limit: 0 },
+ })
+ );
+ const [ctxErr, eventContext] = await parseMatrixResp(ctxResp);
+ if (ctxErr) {
+ console.error(printMatrixError(ctxErr));
+ return;
+ }
let token: string | undefined = eventContext.start;
while (token) {
// eslint-disable-next-line no-await-in-loop
- const response = await mx.createMessagesRequest(
- room.roomId,
- token,
- 20,
- Direction.Forward,
- undefined
+ const msgResp = await ct.requestAuthJSON(
+ getMessages({
+ pathParams: { roomId },
+ searchParams: {
+ from: token,
+ limit: 20,
+ dir: 'f',
+ },
+ })
);
+ // eslint-disable-next-line no-await-in-loop
+ const [msgErr, response] = await parseMatrixResp(msgResp);
+ if (msgErr) {
+ console.error(printMatrixError(msgErr));
+ break;
+ }
const { end, chunk } = response;
// remove until the latest event;
token = end;
@@ -468,15 +593,22 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
(roomEvent) =>
(messageTypes.length > 0 ? messageTypes.includes(roomEvent.type) : true) &&
users.includes(roomEvent.sender) &&
- roomEvent.unsigned?.redacted_because === undefined
+ (!roomEvent.unsigned || !('redacted_because' in roomEvent.unsigned))
);
const eventIds = eventsToDelete.map((roomEvent) => roomEvent.event_id);
// eslint-disable-next-line no-await-in-loop
- await rateLimitedActions(eventIds, (eventId) =>
- mx.redactEvent(room.roomId, eventId, undefined, { reason })
- );
+ await rateLimitedActions(eventIds, async (eventId) => {
+ const redactResp = await ct.requestAuthJSON(
+ putRedact({
+ pathParams: { roomId, eventId, txnId: ct.getTxnId() },
+ body: { reason },
+ })
+ );
+ const [redactErr] = await parseMatrixResp(redactResp);
+ return redactErr;
+ });
}
},
},
@@ -498,12 +630,12 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
const removeAllowList = removeAllowFlag ? splitWithSpace(removeAllowFlag) : [];
const removeDenyList = removeDenyFlag ? splitWithSpace(removeDenyFlag) : [];
- const serverAcl = getStateEvent(
- room,
- StateEvent.RoomServerAcl
- )?.getContent();
+ const serverAclObj = await ct.storage.joinRoomState.read.get(
+ getJoinRoomStateId(roomId, StateEventType.RoomServerAcl, '')
+ );
+ const serverAcl: RoomServerAclContent = serverAclObj?.event.content ?? {};
- const aclContent: RoomServerAclEventContent = {
+ const aclContent: RoomServerAclContent = {
allow: serverAcl?.allow ? [...serverAcl.allow] : [],
allow_ip_literals: serverAcl?.allow_ip_literals,
deny: serverAcl?.deny ? [...serverAcl.deny] : [],
@@ -528,11 +660,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
aclContent.allow?.sort();
aclContent.deny?.sort();
- await mx.sendStateEvent(room.roomId, StateEvent.RoomServerAcl as any, aclContent);
+ await ct.requestAuthJSON(
+ putStateEvent({
+ pathParams: { roomId, eventType: StateEventType.RoomServerAcl, stateKey: '' },
+ body: aclContent,
+ })
+ );
},
},
}),
- [mx, room, navigateRoom]
+ [ct, membersAPI, roomId, navigateRoom]
);
return commands;
diff --git a/src/app/hooks/useDirectUsers.ts b/src/app/hooks/useDirectUsers.ts
index 3aa18928..4691bac9 100644
--- a/src/app/hooks/useDirectUsers.ts
+++ b/src/app/hooks/useDirectUsers.ts
@@ -1,27 +1,18 @@
import { useMemo } from 'react';
-import { AccountDataEvent, MDirectContent } from '../../types/matrix/accountData';
-import { useAccountData } from './useAccountData';
-import { useAllJoinedRoomsSet, useGetRoom } from './useGetRoom';
+import { useClient } from './useClient';
+import { useObservableValue } from './useObservableValue';
export const useDirectUsers = (): string[] => {
- const directEvent = useAccountData(AccountDataEvent.Direct);
- const content = directEvent?.getContent();
-
- const allJoinedRooms = useAllJoinedRoomsSet();
- const getRoom = useGetRoom(allJoinedRooms);
+ const ct = useClient();
+ const roomToSummary = useObservableValue(ct.roomToSummary);
const users = useMemo(() => {
- if (typeof content !== 'object') return [];
+ const heroes = Array.from(roomToSummary.values()).flatMap(
+ (summary) => summary['m.heroes'] ?? []
+ );
- const u = Object.keys(content).filter((userId) => {
- const rooms = content[userId];
- if (!Array.isArray(rooms)) return false;
- const hasDM = rooms.some((roomId) => typeof roomId === 'string' && !!getRoom(roomId));
- return hasDM;
- });
-
- return u;
- }, [content, getRoom]);
+ return Array.from(new Set(heroes));
+ }, [roomToSummary]);
return users;
};
diff --git a/src/app/hooks/useEmailNotifications.ts b/src/app/hooks/useEmailNotifications.ts
index 58639394..566fb4e7 100644
--- a/src/app/hooks/useEmailNotifications.ts
+++ b/src/app/hooks/useEmailNotifications.ts
@@ -1,6 +1,9 @@
import { useCallback } from 'react';
+import { get3pid, Get3pidOK } from 'neurone/lib/api/matrix/client/v3/account';
+import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
+import { getPushers, GetPushersOK } from 'neurone/lib/api/matrix/client/v3/pushers';
import { AsyncStatus, useAsyncCallbackValue } from './useAsyncCallback';
-import { useMatrixClient } from './useMatrixClient';
+import { useClient } from './useClient';
type RefreshHandler = () => void;
@@ -13,19 +16,27 @@ export const useEmailNotifications = (): [
EmailNotificationResult | undefined | null,
RefreshHandler
] => {
- const mx = useMatrixClient();
+ const ct = useClient();
const [emailState, refresh] = useAsyncCallbackValue(
useCallback(async () => {
- const tpIDs = (await mx.getThreePids())?.threepids;
- const emailAddresses = tpIDs.filter((id) => id.medium === 'email').map((id) => id.address);
+ const response = await ct.requestAuthJSON(get3pid({}));
+ const [err, result] = await parseMatrixResp(response);
+ if (err) throw new Error(printMatrixError(err));
+
+ const emailAddresses = result.threepids
+ .filter((id) => id.medium === 'email')
+ .map((id) => id.address);
if (emailAddresses.length === 0)
return {
enabled: false,
};
- const pushers = (await mx.getPushers())?.pushers;
- const emailPusher = pushers.find(
+ const resp = await ct.requestAuthJSON(getPushers({}));
+ const [e, r] = await parseMatrixResp(resp);
+ if (e) throw new Error(printMatrixError(e));
+
+ const emailPusher = r.pushers.find(
(pusher) => pusher.app_id === 'm.email' && emailAddresses.includes(pusher.pushkey)
);
@@ -40,7 +51,7 @@ export const useEmailNotifications = (): [
enabled: false,
email: emailAddresses[0],
};
- }, [mx])
+ }, [ct])
);
if (emailState.status === AsyncStatus.Success) {
diff --git a/src/app/hooks/useIgnoredUsers.ts b/src/app/hooks/useIgnoredUsers.ts
index baf2327a..0ac634cb 100644
--- a/src/app/hooks/useIgnoredUsers.ts
+++ b/src/app/hooks/useIgnoredUsers.ts
@@ -1,18 +1,39 @@
import { useMemo } from 'react';
-import { useAccountData } from './useAccountData';
-import { AccountDataEvent } from '../../types/matrix/accountData';
+import { useAccountData } from './storage/useAccountData';
+import { AccountDataType } from '../../types/matrix/accountData';
export type IgnoredUserListContent = {
ignored_users?: Record;
};
+export const getIgnoredUsersContent = (userList: string[]): IgnoredUserListContent => {
+ const users: Record = {};
+ userList.forEach((userId) => {
+ users[userId] = {};
+ });
+
+ return {
+ ignored_users: users,
+ };
+};
+
+export const parseIgnoreUsersContent = (content?: object): string[] => {
+ if (
+ content &&
+ 'ignored_users' in content &&
+ typeof content.ignored_users === 'object' &&
+ content.ignored_users
+ ) {
+ return Object.keys(content.ignored_users);
+ }
+ return [];
+};
+
export const useIgnoredUsers = (): string[] => {
- const ignoredUserListEvt = useAccountData(AccountDataEvent.IgnoredUserList);
- const ignoredUsers = useMemo(() => {
- const ignoredUsersRecord =
- ignoredUserListEvt?.getContent().ignored_users ?? {};
- return Object.keys(ignoredUsersRecord);
- }, [ignoredUserListEvt]);
+ const ignoreEventObjResult = useAccountData(AccountDataType.IgnoredUserList);
+ const content = ignoreEventObjResult.value?.content;
+
+ const ignoredUsers = useMemo(() => parseIgnoreUsersContent(content), [content]);
return ignoredUsers;
};
diff --git a/src/app/hooks/useImagePackRooms.ts b/src/app/hooks/useImagePackRooms.ts
index f04d2f22..25636589 100644
--- a/src/app/hooks/useImagePackRooms.ts
+++ b/src/app/hooks/useImagePackRooms.ts
@@ -1,22 +1,20 @@
-import { Room } from 'matrix-js-sdk';
+import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { getAllParents } from '../utils/room';
-import { useMatrixClient } from './useMatrixClient';
+import { useAllJoinedRoomsSet } from './useGetRoom';
+import { roomToParentsAtom } from '../state/room/roomToParents';
-export const useImagePackRooms = (
- roomId: string,
- roomToParents: Map>
-): Room[] => {
- const mx = useMatrixClient();
+export const useImagePackRooms = (roomId: string): string[] => {
+ const roomToParents = useAtomValue(roomToParentsAtom);
+ const allJoinedRooms = useAllJoinedRoomsSet();
- const imagePackRooms: Room[] = useMemo(() => {
+ const imagePackRooms: string[] = useMemo(() => {
const allParentSpaces = [roomId].concat(Array.from(getAllParents(roomToParents, roomId)));
- return allParentSpaces.reduce((list, rId) => {
- const r = mx.getRoom(rId);
- if (r) list.push(r);
+ return allParentSpaces.reduce((list, rId) => {
+ if (allJoinedRooms.has(rId)) list.push(rId);
return list;
}, []);
- }, [mx, roomId, roomToParents]);
+ }, [roomId, roomToParents, allJoinedRooms]);
return imagePackRooms;
};
diff --git a/src/app/hooks/useImagePacks.ts b/src/app/hooks/useImagePacks.ts
index 318d6cba..6f3d52fc 100644
--- a/src/app/hooks/useImagePacks.ts
+++ b/src/app/hooks/useImagePacks.ts
@@ -1,161 +1,188 @@
-import { Room } from 'matrix-js-sdk';
-import { useCallback, useMemo, useState } from 'react';
-import { AccountDataEvent } from '../../types/matrix/accountData';
-import { StateEvent } from '../../types/matrix/room';
+import { useCallback, useMemo } from 'react';
+import { ObservableValue, PendingObservableResult } from 'neurone/lib/utils/observable';
+import { AccountDataType } from '../../types/matrix/accountData';
+import { StateEventType } from '../../types/matrix/room';
+import { getGlobalPackRoomToStateKey, ImagePack, ImageUsage } from '../plugins/custom-emoji';
+import { useClient } from './useClient';
import {
- getGlobalImagePacks,
- getRoomImagePack,
- getRoomImagePacks,
- getUserImagePack,
- ImagePack,
- ImageUsage,
-} from '../plugins/custom-emoji';
-import { useMatrixClient } from './useMatrixClient';
-import { useAccountDataCallback } from './useAccountDataCallback';
-import { useStateEventCallback } from './useStateEventCallback';
+ useComputedPendingObservableResult,
+ useObservableValuePromise,
+} from './useObservableValue';
+import { getJoinRoomStateId, JoinRoomStateObject } from '../../client/storage/collections';
+import { useRoomStateEvent, useRoomStateEvents } from './storage/useRoomStateEvent';
-export const useUserImagePack = (): ImagePack | undefined => {
- const mx = useMatrixClient();
- const [userPack, setUserPack] = useState(() => getUserImagePack(mx));
-
- useAccountDataCallback(
- mx,
- useCallback(
- (mEvent) => {
- if (mEvent.getType() === AccountDataEvent.PoniesUserEmotes) {
- setUserPack(getUserImagePack(mx));
- }
- },
- [mx]
- )
+export const useUserImagePack = (): PendingObservableResult => {
+ const ct = useClient();
+ const userPackObjPromise = useMemo(
+ () => ct.storage.accountData.observableRead.get(AccountDataType.PoniesUserEmotes),
+ [ct]
);
- return userPack;
+ const userPackObjResult = useObservableValuePromise(userPackObjPromise);
+
+ return useComputedPendingObservableResult(
+ userPackObjResult,
+ useCallback(
+ (userPackObj) => {
+ if (!userPackObj) return undefined;
+ return ImagePack.fromAccountData(ct.session.userId, userPackObj.event);
+ },
+ [ct]
+ )
+ );
};
-export const useGlobalImagePacks = (): ImagePack[] => {
- const mx = useMatrixClient();
- const [globalPacks, setGlobalPacks] = useState(() => getGlobalImagePacks(mx));
+export const useGlobalImagePacks = (): PendingObservableResult => {
+ const ct = useClient();
+ const emoteRoomsObjPromise = useMemo(
+ () => ct.storage.accountData.observableRead.get(AccountDataType.PoniesEmoteRooms),
+ [ct]
+ );
+ const emoteRoomsObjResult = useObservableValuePromise(emoteRoomsObjPromise);
- useAccountDataCallback(
- mx,
+ const roomToStateKeysResult = useComputedPendingObservableResult(
+ emoteRoomsObjResult,
+ useCallback((emoteRoomsObj) => getGlobalPackRoomToStateKey(emoteRoomsObj?.event.content), [])
+ );
+ const roomToStateKeys = roomToStateKeysResult.value;
+
+ const roomPackEventsPromise = useMemo(() => {
+ const keys = roomToStateKeys
+ ? Object.keys(roomToStateKeys).flatMap((roomId) =>
+ roomToStateKeys[roomId].map((stateKey) =>
+ getJoinRoomStateId(roomId, StateEventType.PoniesRoomEmotes, stateKey)
+ )
+ )
+ : [];
+
+ return ct.storage.joinRoomState.observableRead.getMultiple(keys);
+ }, [ct, roomToStateKeys]);
+
+ const roomPackEventsResult = useObservableValuePromise(roomPackEventsPromise);
+
+ return useComputedPendingObservableResult(
+ roomPackEventsResult,
useCallback(
- (mEvent) => {
- if (mEvent.getType() === AccountDataEvent.PoniesEmoteRooms) {
- setGlobalPacks(getGlobalImagePacks(mx));
- }
- },
- [mx]
+ (roomPackEvents) =>
+ roomPackEvents
+ .filter((p) => !!p)
+ .map((packEvent) => ImagePack.fromStateEvent(packEvent.roomId, packEvent.event)),
+ []
)
);
-
- useStateEventCallback(
- mx,
- useCallback(
- (mEvent) => {
- const eventType = mEvent.getType();
- const roomId = mEvent.getRoomId();
- const stateKey = mEvent.getStateKey();
- if (eventType === StateEvent.PoniesRoomEmotes && roomId && typeof stateKey === 'string') {
- const global = !!globalPacks.find(
- (pack) =>
- pack.address && pack.address.roomId === roomId && pack.address.stateKey === stateKey
- );
- if (global) {
- setGlobalPacks(getGlobalImagePacks(mx));
- }
- }
- },
- [mx, globalPacks]
- )
- );
-
- return globalPacks;
};
-export const useRoomImagePack = (room: Room, stateKey: string): ImagePack | undefined => {
- const mx = useMatrixClient();
- const [roomPack, setRoomPack] = useState(() => getRoomImagePack(room, stateKey));
+export const useRoomImagePack = (
+ roomId: string,
+ stateKey: string
+): PendingObservableResult => {
+ const roomPackEventResult = useRoomStateEvent(roomId, StateEventType.PoniesRoomEmotes, stateKey);
- useStateEventCallback(
- mx,
+ return useComputedPendingObservableResult(
+ roomPackEventResult,
useCallback(
- (mEvent) => {
- if (
- mEvent.getRoomId() === room.roomId &&
- mEvent.getType() === StateEvent.PoniesRoomEmotes &&
- mEvent.getStateKey() === stateKey
- ) {
- setRoomPack(getRoomImagePack(room, stateKey));
- }
- },
- [room, stateKey]
+ (roomPackEvent) =>
+ roomPackEvent
+ ? ImagePack.fromStateEvent(roomPackEvent.roomId, roomPackEvent.event)
+ : undefined,
+ []
)
);
-
- return roomPack;
};
-export const useRoomImagePacks = (room: Room): ImagePack[] => {
- const mx = useMatrixClient();
- const [roomPacks, setRoomPacks] = useState(() => getRoomImagePacks(room));
+export const useRoomImagePacks = (roomId: string): PendingObservableResult => {
+ const roomPackEventsResult = useRoomStateEvents(roomId, StateEventType.PoniesRoomEmotes);
- useStateEventCallback(
- mx,
+ return useComputedPendingObservableResult(
+ roomPackEventsResult,
useCallback(
- (mEvent) => {
- if (
- mEvent.getRoomId() === room.roomId &&
- mEvent.getType() === StateEvent.PoniesRoomEmotes
- ) {
- setRoomPacks(getRoomImagePacks(room));
- }
- },
- [room]
+ (roomPackEvents) =>
+ roomPackEvents.map((roomPackEvent) =>
+ ImagePack.fromStateEvent(roomPackEvent.roomId, roomPackEvent.event)
+ ),
+ []
)
);
-
- return roomPacks;
};
-export const useRoomsImagePacks = (rooms: Room[]) => {
- const mx = useMatrixClient();
- const [roomPacks, setRoomPacks] = useState(() => rooms.flatMap(getRoomImagePacks));
+export const useRoomsImagePacks = (rooms: string[]): PendingObservableResult => {
+ const ct = useClient();
+ const idsPromise = useMemo(
+ () =>
+ ct.storage.joinRoomState.observableRead.queryIndexKeys(
+ 'type',
+ StateEventType.PoniesRoomEmotes
+ ),
+ [ct]
+ );
+ const idsResult = useObservableValuePromise(idsPromise);
- useStateEventCallback(
- mx,
+ const packKeysResult = useComputedPendingObservableResult(
+ idsResult,
useCallback(
- (mEvent) => {
- if (
- rooms.find((room) => room.roomId === mEvent.getRoomId()) &&
- mEvent.getType() === StateEvent.PoniesRoomEmotes
- ) {
- setRoomPacks(rooms.flatMap(getRoomImagePacks));
- }
- },
+ (ids) =>
+ ids.filter((id) =>
+ rooms.find((roomId) =>
+ id.startsWith(getJoinRoomStateId(roomId, StateEventType.PoniesRoomEmotes, ''))
+ )
+ ),
[rooms]
)
);
- return roomPacks;
+ const roomPackEventsResult = useObservableValuePromise(
+ useMemo(() => {
+ if (!packKeysResult.value) {
+ return new Promise>(
+ (resolve, reject) => {
+ if (packKeysResult.error) {
+ reject(packKeysResult.error);
+ }
+ }
+ );
+ }
+ return ct.storage.joinRoomState.observableRead.getMultiple(packKeysResult.value);
+ }, [ct, packKeysResult])
+ );
+
+ return useComputedPendingObservableResult(
+ roomPackEventsResult,
+ useCallback(
+ (roomPackEvents) =>
+ roomPackEvents
+ .filter((p) => !!p)
+ .map((roomPackEvent) =>
+ ImagePack.fromStateEvent(roomPackEvent.roomId, roomPackEvent.event)
+ ),
+ []
+ )
+ );
};
-export const useRelevantImagePacks = (usage: ImageUsage, rooms: Room[]): ImagePack[] => {
- const userPack = useUserImagePack();
- const globalPacks = useGlobalImagePacks();
- const roomsPacks = useRoomsImagePacks(rooms);
+export const useRelevantImagePacks = (
+ usage: ImageUsage,
+ rooms: string[]
+): [ImagePack[], boolean | undefined, Error | undefined] => {
+ const userPackResult = useUserImagePack();
+ const globalPacksResult = useGlobalImagePacks();
+
+ const roomsPacksResult = useRoomsImagePacks(rooms);
+ const loading: boolean | undefined =
+ userPackResult.pending || globalPacksResult.pending || roomsPacksResult.pending;
+ const error: Error | undefined =
+ userPackResult.error || globalPacksResult.error || roomsPacksResult.error;
const relevantPacks = useMemo(() => {
- const packs = userPack ? [userPack] : [];
- const globalPackIds = new Set(globalPacks.map((pack) => pack.id));
+ const packs = userPackResult.value ? [userPackResult.value] : [];
+ const globalPackIds = new Set(globalPacksResult.value?.map((pack) => pack.id));
const relPacks = packs.concat(
- globalPacks,
- roomsPacks.filter((pack) => !globalPackIds.has(pack.id))
+ globalPacksResult.value ?? [],
+ roomsPacksResult.value?.filter((pack) => !globalPackIds.has(pack.id)) ?? []
);
return relPacks.filter((pack) => pack.getImages(usage).length > 0);
- }, [userPack, globalPacks, roomsPacks, usage]);
+ }, [userPackResult, globalPacksResult, roomsPacksResult, usage]);
- return relevantPacks;
+ return [relevantPacks, loading, error];
};
diff --git a/src/app/hooks/useJoinedRoomId.ts b/src/app/hooks/useJoinedRoomId.ts
index 857defa8..ada42fea 100644
--- a/src/app/hooks/useJoinedRoomId.ts
+++ b/src/app/hooks/useJoinedRoomId.ts
@@ -1,19 +1,14 @@
-import { useMemo } from 'react';
-import { useMatrixClient } from './useMatrixClient';
-
-import { getCanonicalAliasRoomId, isRoomAlias } from '../utils/matrix';
+import { isRoomAlias } from '../utils/matrix';
+import { useClient } from './useClient';
export const useJoinedRoomId = (allRooms: string[], roomIdOrAlias: string): string | undefined => {
- const mx = useMatrixClient();
+ const ct = useClient();
- const joinedRoomId = useMemo(() => {
- const roomId = isRoomAlias(roomIdOrAlias)
- ? getCanonicalAliasRoomId(mx, roomIdOrAlias)
- : roomIdOrAlias;
+ const roomId = isRoomAlias(roomIdOrAlias)
+ ? ct.rooms.join.read.find((rId) => ct.roomToAlias.read.get(rId) === roomIdOrAlias)
+ : roomIdOrAlias;
- if (roomId && allRooms.includes(roomId)) return roomId;
- return undefined;
- }, [mx, allRooms, roomIdOrAlias]);
+ if (roomId && allRooms.includes(roomId)) return roomId;
- return joinedRoomId;
+ return undefined;
};
diff --git a/src/app/hooks/useLocalRoomSummary.ts b/src/app/hooks/useLocalRoomSummary.ts
index 46f81569..4802a3cf 100644
--- a/src/app/hooks/useLocalRoomSummary.ts
+++ b/src/app/hooks/useLocalRoomSummary.ts
@@ -1,44 +1,40 @@
-import { GuestAccess, HistoryVisibility, JoinRule, Room } from 'matrix-js-sdk';
-import { getStateEvent } from '../utils/room';
-import { StateEvent } from '../../types/matrix/room';
+import { JoinRule } from '../../types/matrix/room';
+import { useRoomHeroes } from './storage/useRoomHeros';
+import { useGetRoomName } from './storage/useGetRoomName';
+import { useGetRoomAvatar } from './storage/useGetRoomAvatar';
+import { useRoomTopic } from './storage/useRoomTopic';
+import { useObservableValue } from './useObservableValue';
+import { useClient } from './useClient';
export type LocalRoomSummary = {
roomId: string;
- name: string;
+ name?: string;
topic?: string;
avatarUrl?: string;
canonicalAlias?: string;
- worldReadable?: boolean;
- guestCanJoin?: boolean;
memberCount?: number;
- roomType?: string;
+ space?: boolean;
joinRule?: JoinRule;
};
-export const useLocalRoomSummary = (room: Room): LocalRoomSummary => {
- const topicEvent = getStateEvent(room, StateEvent.RoomTopic);
- const topicContent = topicEvent?.getContent();
- const topic =
- topicContent && typeof topicContent.topic === 'string' ? topicContent.topic : undefined;
-
- const historyEvent = getStateEvent(room, StateEvent.RoomHistoryVisibility);
- const historyContent = historyEvent?.getContent();
- const worldReadable =
- historyContent && typeof historyContent.history_visibility === 'string'
- ? historyContent.history_visibility === HistoryVisibility.WorldReadable
- : undefined;
-
- const guestCanJoin = room.getGuestAccess() === GuestAccess.CanJoin;
+export const useLocalRoomSummary = (roomId: string): LocalRoomSummary => {
+ const ct = useClient();
+ const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(roomId);
+ const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
+ const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
+ const roomTopic = useRoomTopic(roomId);
+ const roomToAlias = useObservableValue(ct.roomToAlias);
+ const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
+ const roomToSummary = useObservableValue(ct.roomToSummary);
+ const spaceRooms = useObservableValue(ct.spaceRooms);
return {
- roomId: room.roomId,
- name: room.name,
- topic,
- avatarUrl: room.getMxcAvatarUrl() ?? undefined,
- canonicalAlias: room.getCanonicalAlias() ?? undefined,
- worldReadable,
- guestCanJoin,
- memberCount: room.getJoinedMemberCount(),
- roomType: room.getType(),
- joinRule: room.getJoinRule(),
+ roomId,
+ name: getRoomName(roomId),
+ topic: roomTopic,
+ avatarUrl: getRoomAvatar(roomId),
+ canonicalAlias: roomToAlias.get(roomId),
+ memberCount: roomToSummary.get(roomId)?.['m.joined_member_count'],
+ space: spaceRooms.has(roomId),
+ joinRule: roomToJoinRule.get(roomId),
};
};
diff --git a/src/app/hooks/useMediaAuthentication.ts b/src/app/hooks/useMediaAuthentication.ts
index 6e4d06aa..7967cdce 100644
--- a/src/app/hooks/useMediaAuthentication.ts
+++ b/src/app/hooks/useMediaAuthentication.ts
@@ -1,11 +1,11 @@
-import { useSpecVersions } from './useSpecVersions';
+import { useSpecVersionSupport, useUnstableFeatureSupport } from './useSpecVersions';
export const useMediaAuthentication = (): boolean => {
- const { versions, unstable_features: unstableFeatures } = useSpecVersions();
+ const versionSupport = useSpecVersionSupport('v1.11');
+ const unstableSupport = useUnstableFeatureSupport()('org.matrix.msc3916.stable');
// Media authentication is introduced in spec version 1.11
- const authenticatedMedia =
- unstableFeatures?.['org.matrix.msc3916.stable'] || versions.includes('v1.11');
+ const authenticatedMedia = unstableSupport || versionSupport;
return authenticatedMedia;
};
diff --git a/src/app/hooks/useMemberFilter.ts b/src/app/hooks/useMemberFilter.ts
index 2c33c7c8..8d7a9b7e 100644
--- a/src/app/hooks/useMemberFilter.ts
+++ b/src/app/hooks/useMemberFilter.ts
@@ -1,52 +1,55 @@
import { useMemo } from 'react';
-import { RoomMember } from 'matrix-js-sdk';
-import { Membership } from '../../types/matrix/room';
+import { GetRoomMemberMembershipCallback } from './useRoomMembers';
+import { JoinRoomMemberObject } from '../../client/storage/collections';
-export const MembershipFilter = {
- filterJoined: (m: RoomMember) => m.membership === Membership.Join,
- filterInvited: (m: RoomMember) => m.membership === Membership.Invite,
- filterLeaved: (m: RoomMember) =>
- m.membership === Membership.Leave &&
- m.events.member?.getStateKey() === m.events.member?.getSender(),
- filterKicked: (m: RoomMember) =>
- m.membership === Membership.Leave &&
- m.events.member?.getStateKey() !== m.events.member?.getSender(),
- filterBanned: (m: RoomMember) => m.membership === Membership.Ban,
-};
+export const getMembershipFilter = (getMembership: GetRoomMemberMembershipCallback) => ({
+ filterJoined: (m: JoinRoomMemberObject) => getMembership(m.userId) === 'join',
+ filterInvited: (m: JoinRoomMemberObject) => getMembership(m.userId) === 'invite',
+ filterLeaved: (m: JoinRoomMemberObject) =>
+ getMembership(m.userId) === 'leave' && m.event.state_key === m.event.sender,
+ filterKicked: (m: JoinRoomMemberObject) =>
+ getMembership(m.userId) === 'leave' && m.event.state_key !== m.event.sender,
+ filterBanned: (m: JoinRoomMemberObject) => getMembership(m.userId) === 'ban',
+});
-export type MembershipFilterFn = (m: RoomMember) => boolean;
+export type MembershipFilterFn = (m: JoinRoomMemberObject) => boolean;
export type MembershipFilterItem = {
name: string;
filterFn: MembershipFilterFn;
};
-export const useMembershipFilterMenu = (): MembershipFilterItem[] =>
- useMemo(
+export const useMembershipFilterMenu = (
+ getMembership: GetRoomMemberMembershipCallback
+): MembershipFilterItem[] => {
+ const filterFn = useMemo(() => getMembershipFilter(getMembership), [getMembership]);
+
+ return useMemo(
() => [
{
name: 'Joined',
- filterFn: MembershipFilter.filterJoined,
+ filterFn: filterFn.filterJoined,
},
{
name: 'Invited',
- filterFn: MembershipFilter.filterInvited,
+ filterFn: filterFn.filterInvited,
},
{
name: 'Left',
- filterFn: MembershipFilter.filterLeaved,
+ filterFn: filterFn.filterLeaved,
},
{
name: 'Kicked',
- filterFn: MembershipFilter.filterKicked,
+ filterFn: filterFn.filterKicked,
},
{
name: 'Banned',
- filterFn: MembershipFilter.filterBanned,
+ filterFn: filterFn.filterBanned,
},
],
- []
+ [filterFn]
);
+};
export const useMembershipFilter = (
index: number,
diff --git a/src/app/hooks/useMemberPowerTag.ts b/src/app/hooks/useMemberPowerTag.ts
index 31e52aa0..2ccc4f20 100644
--- a/src/app/hooks/useMemberPowerTag.ts
+++ b/src/app/hooks/useMemberPowerTag.ts
@@ -1,25 +1,26 @@
import { useCallback, useMemo } from 'react';
-import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
-import { getPowerLevelTag, PowerLevelTags, usePowerLevelTags } from './usePowerLevelTags';
+import { getPowerLevelTag, PowerLevelTags, usePowerLevelTagsContext } from './usePowerLevelTags';
import { IPowerLevels, readPowerLevel } from './usePowerLevels';
import { MemberPowerTag, MemberPowerTagIcon } from '../../types/matrix/room';
import { useRoomCreatorsTag } from './useRoomCreatorsTag';
import { ThemeKind } from './useTheme';
import { accessibleColor } from '../plugins/color';
+import { ClientContext } from '../../client/context';
+import { mxcToHttp } from '../../client/mxcToHttp';
+import { JoinRoomMemberObject } from '../../client/storage/collections';
export type GetMemberPowerTag = (userId: string) => MemberPowerTag;
export const useGetMemberPowerTag = (
- room: Room,
- creators: Set,
+ creators: Set | undefined,
powerLevels: IPowerLevels
) => {
const creatorsTag = useRoomCreatorsTag();
- const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const powerLevelTags = usePowerLevelTagsContext();
const getMemberPowerTag: GetMemberPowerTag = useCallback(
(userId) => {
- if (creators.has(userId)) {
+ if (creators?.has(userId)) {
return creatorsTag;
}
@@ -33,12 +34,16 @@ export const useGetMemberPowerTag = (
};
export const getPowerTagIconSrc = (
- mx: MatrixClient,
+ ct: ClientContext,
useAuthentication: boolean,
icon: MemberPowerTagIcon
): string | undefined =>
icon?.key?.startsWith('mxc://')
- ? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
+ ? mxcToHttp(ct.session.baseUrl, icon.key, useAuthentication).thumbnail({
+ width: 96,
+ height: 96,
+ method: 'scale',
+ }) ?? '🌻'
: icon?.key;
export const useAccessiblePowerTagColors = (
@@ -66,12 +71,12 @@ export const useAccessiblePowerTagColors = (
};
export const useFlattenPowerTagMembers = (
- members: RoomMember[],
+ members: JoinRoomMemberObject[],
getTag: GetMemberPowerTag
-): Array => {
+): Array => {
const PLTagOrRoomMember = useMemo(() => {
let prevTag: MemberPowerTag | undefined;
- const tagOrMember: Array = [];
+ const tagOrMember: Array = [];
members.forEach((member) => {
const tag = getTag(member.userId);
if (tag !== prevTag) {
diff --git a/src/app/hooks/useMemberSort.ts b/src/app/hooks/useMemberSort.ts
index 48d2f605..a711f42c 100644
--- a/src/app/hooks/useMemberSort.ts
+++ b/src/app/hooks/useMemberSort.ts
@@ -1,46 +1,50 @@
-import { RoomMember } from 'matrix-js-sdk';
import { useCallback, useMemo } from 'react';
+import { JoinRoomMemberObject } from '../../client/storage/collections';
+import { GetRoomMemberNameCallback } from './useRoomMembers';
-export const MemberSort = {
- Ascending: (a: RoomMember, b: RoomMember) =>
- a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1,
- Descending: (a: RoomMember, b: RoomMember) =>
- a.name.toLowerCase() > b.name.toLowerCase() ? -1 : 1,
- NewestFirst: (a: RoomMember, b: RoomMember) =>
- (b.events.member?.getTs() ?? 0) - (a.events.member?.getTs() ?? 0),
- Oldest: (a: RoomMember, b: RoomMember) =>
- (a.events.member?.getTs() ?? 0) - (b.events.member?.getTs() ?? 0),
-};
+export const getMemberSort = (getName: GetRoomMemberNameCallback) => ({
+ Ascending: (a: JoinRoomMemberObject, b: JoinRoomMemberObject) =>
+ getName(a.userId).toLowerCase() < getName(b.userId).toLowerCase() ? -1 : 1,
+ Descending: (a: JoinRoomMemberObject, b: JoinRoomMemberObject) =>
+ getName(a.userId).toLowerCase() > getName(b.userId).toLowerCase() ? -1 : 1,
+ NewestFirst: (a: JoinRoomMemberObject, b: JoinRoomMemberObject) =>
+ b.event.origin_server_ts - a.event.origin_server_ts,
+ Oldest: (a: JoinRoomMemberObject, b: JoinRoomMemberObject) =>
+ a.event.origin_server_ts - b.event.origin_server_ts,
+});
-export type MemberSortFn = (a: RoomMember, b: RoomMember) => number;
+export type MemberSortFn = (a: JoinRoomMemberObject, b: JoinRoomMemberObject) => number;
export type MemberSortItem = {
name: string;
sortFn: MemberSortFn;
};
-export const useMemberSortMenu = (): MemberSortItem[] =>
- useMemo(
+export const useMemberSortMenu = (getName: GetRoomMemberNameCallback): MemberSortItem[] => {
+ const sortFun = useMemo(() => getMemberSort(getName), [getName]);
+
+ return useMemo(
() => [
{
name: 'A to Z',
- sortFn: MemberSort.Ascending,
+ sortFn: sortFun.Ascending,
},
{
name: 'Z to A',
- sortFn: MemberSort.Descending,
+ sortFn: sortFun.Descending,
},
{
name: 'Newest',
- sortFn: MemberSort.NewestFirst,
+ sortFn: sortFun.NewestFirst,
},
{
name: 'Oldest',
- sortFn: MemberSort.Oldest,
+ sortFn: sortFun.Oldest,
},
],
- []
+ [sortFun]
);
+};
export const useMemberSort = (index: number, memberSort: MemberSortItem[]): MemberSortItem => {
const item = memberSort[index] ?? memberSort[0];
@@ -48,20 +52,20 @@ export const useMemberSort = (index: number, memberSort: MemberSortItem[]): Memb
};
export const useMemberPowerSort = (
- creators: Set,
+ creators: Set | undefined,
getPowerLevel: (userId: string) => number
): MemberSortFn => {
const sort: MemberSortFn = useCallback(
(a, b) => {
- if (creators.has(a.userId) && creators.has(b.userId)) {
+ if (creators?.has(a.userId) && creators?.has(b.userId)) {
return 0;
}
- if (creators.has(a.userId)) return -1;
- if (creators.has(b.userId)) return 1;
+ if (creators?.has(a.userId)) return -1;
+ if (creators?.has(b.userId)) return 1;
return getPowerLevel(b.userId) - getPowerLevel(a.userId);
},
- [creators]
+ [creators, getPowerLevel]
);
return sort;
diff --git a/src/app/hooks/useMentionClickHandler.ts b/src/app/hooks/useMentionClickHandler.ts
index 9dc81d4e..d86c1b3d 100644
--- a/src/app/hooks/useMentionClickHandler.ts
+++ b/src/app/hooks/useMentionClickHandler.ts
@@ -1,19 +1,19 @@
import { ReactEventHandler, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRoomNavigate } from './useRoomNavigate';
-import { useMatrixClient } from './useMatrixClient';
import { isRoomId, isUserId } from '../utils/matrix';
import { getHomeRoomPath, withSearchParam } from '../pages/pathUtils';
import { _RoomSearchParams } from '../pages/paths';
import { useOpenUserRoomProfile } from '../state/hooks/userRoomProfile';
import { useSpaceOptionally } from './useSpace';
+import { useClient } from './useClient';
export const useMentionClickHandler = (roomId: string): ReactEventHandler => {
- const mx = useMatrixClient();
+ const ct = useClient();
const { navigateRoom, navigateSpace } = useRoomNavigate();
const navigate = useNavigate();
const openProfile = useOpenUserRoomProfile();
- const space = useSpaceOptionally();
+ const spaceId = useSpaceOptionally();
const handleClick: ReactEventHandler = useCallback(
(evt) => {
@@ -24,13 +24,13 @@ export const useMentionClickHandler = (roomId: string): ReactEventHandler(path, { viaServers }) : path);
},
- [mx, navigate, navigateRoom, navigateSpace, roomId, space, openProfile]
+ [ct, navigate, navigateRoom, navigateSpace, roomId, spaceId, openProfile]
);
return handleClick;
diff --git a/src/app/hooks/useMutualRooms.ts b/src/app/hooks/useMutualRooms.ts
index a7b38938..34606b4e 100644
--- a/src/app/hooks/useMutualRooms.ts
+++ b/src/app/hooks/useMutualRooms.ts
@@ -1,15 +1,15 @@
import { useCallback } from 'react';
import { useMatrixClient } from './useMatrixClient';
import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
-import { useSpecVersions } from './useSpecVersions';
+import { useUnstableFeatureSupport } from './useSpecVersions';
export const useMutualRoomsSupport = (): boolean => {
- const { unstable_features: unstableFeatures } = useSpecVersions();
+ const checkSupport = useUnstableFeatureSupport();
const supported =
- unstableFeatures?.['uk.half-shot.msc2666'] ||
- unstableFeatures?.['uk.half-shot.msc2666.mutual_rooms'] ||
- unstableFeatures?.['uk.half-shot.msc2666.query_mutual_rooms'];
+ checkSupport('uk.half-shot.msc2666') ||
+ checkSupport('uk.half-shot.msc2666.mutual_rooms') ||
+ checkSupport('uk.half-shot.msc2666.query_mutual_rooms');
return !!supported;
};
diff --git a/src/app/hooks/useNetwork.ts b/src/app/hooks/useNetwork.ts
new file mode 100644
index 00000000..bf3e6b15
--- /dev/null
+++ b/src/app/hooks/useNetwork.ts
@@ -0,0 +1,23 @@
+import { useEffect } from 'react';
+
+export const useNetworkOnlineCallback = (callback: () => void) => {
+ useEffect(() => {
+ const handleNetwork = () => callback();
+
+ window.addEventListener('online', handleNetwork);
+ return () => {
+ window.removeEventListener('online', handleNetwork);
+ };
+ }, [callback]);
+};
+
+export const useNetworkOfflineCallback = (callback: () => void) => {
+ useEffect(() => {
+ const handleNetwork = () => callback();
+
+ window.addEventListener('offline', handleNetwork);
+ return () => {
+ window.removeEventListener('offline', handleNetwork);
+ };
+ }, [callback]);
+};
diff --git a/src/app/hooks/useNotificationMode.ts b/src/app/hooks/useNotificationMode.ts
index df90f647..484bf1cf 100644
--- a/src/app/hooks/useNotificationMode.ts
+++ b/src/app/hooks/useNotificationMode.ts
@@ -1,4 +1,4 @@
-import { PushRuleAction, PushRuleActionName, TweakName } from 'matrix-js-sdk';
+import { PushRuleAction } from 'neurone/lib/api/types';
import { useCallback, useMemo } from 'react';
export enum NotificationMode {
@@ -7,9 +7,20 @@ export enum NotificationMode {
NotifyLoud = 'NotifyLoud',
}
+export enum PushRuleActionName {
+ DontNotify = 'dont_notify',
+ Notify = 'notify',
+}
+
+export enum TweakName {
+ Highlight = 'highlight',
+ Sound = 'sound',
+}
+
export const getNotificationMode = (actions: PushRuleAction[]): NotificationMode => {
const soundTweak = actions.find(
- (action) => typeof action === 'object' && action.set_tweak === TweakName.Sound
+ (action) =>
+ typeof action === 'object' && 'set_tweak' in action && action.set_tweak === TweakName.Sound
);
const notify = actions.find(
(action) => typeof action === 'string' && action === PushRuleActionName.Notify
diff --git a/src/app/hooks/useObservableValue.ts b/src/app/hooks/useObservableValue.ts
new file mode 100644
index 00000000..79f80cea
--- /dev/null
+++ b/src/app/hooks/useObservableValue.ts
@@ -0,0 +1,85 @@
+import {
+ ObservableValue,
+ PendingObservableValue,
+ PendingObservableResult,
+ ComputedObservableValue,
+} from 'neurone/lib/utils/observable';
+import { useMemo, useSyncExternalStore, useCallback, useEffect } from 'react';
+
+export function useObservableValue(observableValue: ObservableValue): T {
+ const value = useSyncExternalStore(
+ useCallback((c) => observableValue.subscribe(c), [observableValue]),
+ useCallback(() => observableValue.read, [observableValue])
+ );
+
+ return value;
+}
+
+export function useObservableValueOptionally(
+ observableValue?: ObservableValue
+): T | undefined {
+ const value = useSyncExternalStore(
+ useCallback(
+ (c) => {
+ const unsubscribe = observableValue?.subscribe(c);
+ return () => {
+ unsubscribe?.();
+ };
+ },
+ [observableValue]
+ ),
+ useCallback(() => observableValue?.read, [observableValue])
+ );
+
+ return value;
+}
+
+export function useObservableValueListener(
+ observableValue: ObservableValue,
+ listener: (value: T) => void
+): void {
+ useEffect(() => {
+ const unsub = observableValue.subscribe(listener);
+ return unsub;
+ }, [observableValue, listener]);
+}
+
+export function useObservableDispose(observableValue: ObservableValue) {
+ useEffect(() => () => observableValue.dispose(), [observableValue]);
+}
+
+export function useComputedObservable(observable: ComputedObservableValue): T {
+ const value = useObservableValue(observable);
+ useObservableDispose(observable);
+
+ return value;
+}
+
+export const useObservableValuePromise = (
+ observableValuePromise: Promise>
+): PendingObservableResult => {
+ const observable: ObservableValue> = useMemo(
+ () => new PendingObservableValue(observableValuePromise),
+ [observableValuePromise]
+ );
+
+ const value = useObservableValue(observable);
+
+ useObservableDispose(observable);
+
+ return value;
+};
+
+export const useComputedPendingObservableResult = (
+ result: PendingObservableResult,
+ compute: (value: T) => U
+): PendingObservableResult => {
+ const computedResult: PendingObservableResult = useMemo(() => {
+ const { value, error } = result;
+ if (value) return { value: compute(value) };
+ if (error) return { error };
+ return { pending: true };
+ }, [result, compute]);
+
+ return computedResult;
+};
diff --git a/src/app/hooks/useParsedLoginFlows.ts b/src/app/hooks/useParsedLoginFlows.ts
index 088a514e..17276574 100644
--- a/src/app/hooks/useParsedLoginFlows.ts
+++ b/src/app/hooks/useParsedLoginFlows.ts
@@ -1,22 +1,19 @@
import { useMemo } from 'react';
-import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth';
+import { LoginFlow } from 'neurone/lib/api/types';
-export const getSSOFlow = (loginFlows: LoginFlow[]): ISSOFlow | undefined =>
- loginFlows.find((flow) => flow.type === 'm.login.sso' || flow.type === 'm.login.cas') as
- | ISSOFlow
- | undefined;
+const getSSOFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined =>
+ loginFlows.find((flow) => flow.type === 'm.login.sso' || flow.type === 'm.login.cas');
-export const getPasswordFlow = (loginFlows: LoginFlow[]): IPasswordFlow | undefined =>
- loginFlows.find((flow) => flow.type === 'm.login.password') as IPasswordFlow;
-export const getTokenFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined =>
- loginFlows.find((flow) => flow.type === 'm.login.token') as ILoginFlow & {
- type: 'm.login.token';
- };
+const getPasswordFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined =>
+ loginFlows.find((flow) => flow.type === 'm.login.password');
+
+const getTokenFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined =>
+ loginFlows.find((flow) => flow.type === 'm.login.token');
export type ParsedLoginFlows = {
password?: LoginFlow;
token?: LoginFlow;
- sso?: ISSOFlow;
+ sso?: LoginFlow;
};
export const useParsedLoginFlows = (loginFlows: LoginFlow[]) => {
const parsedFlow: ParsedLoginFlows = useMemo(
diff --git a/src/app/hooks/usePasswordEmail.ts b/src/app/hooks/usePasswordEmail.ts
index 37e96433..cf79786e 100644
--- a/src/app/hooks/usePasswordEmail.ts
+++ b/src/app/hooks/usePasswordEmail.ts
@@ -1,30 +1,54 @@
-import { MatrixClient, MatrixError } from 'matrix-js-sdk';
-import { useCallback, useRef } from 'react';
+import { useCallback, useMemo, useRef } from 'react';
+import {
+ postPasswordEmailRequestToken,
+ PostPasswordEmailRequestTokenOK,
+} from 'neurone/lib/api/matrix/client/v3/account';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { AsyncState, useAsyncCallback } from './useAsyncCallback';
-import { RequestEmailTokenCallback, RequestEmailTokenResponse } from './types';
+import { RequestEmailTokenCallback, RequestEmailTokenResult } from './types';
+import { getJSONRequest } from '../../client/request';
export const usePasswordEmail = (
- mx: MatrixClient
-): [AsyncState, RequestEmailTokenCallback] => {
+ baseUrl: string
+): [AsyncState, RequestEmailTokenCallback] => {
const sendAttemptRef = useRef(1);
+ const requestJSON = useMemo(() => getJSONRequest(baseUrl), [baseUrl]);
const passwordEmailCallback: RequestEmailTokenCallback = useCallback(
async (email, clientSecret, nextLink) => {
const sendAttempt = sendAttemptRef.current;
sendAttemptRef.current += 1;
- const result = await mx.requestPasswordEmailToken(email, clientSecret, sendAttempt, nextLink);
- return {
- email,
- clientSecret,
- result,
- };
+
+ const response = await requestJSON(
+ postPasswordEmailRequestToken({
+ body: {
+ email,
+ client_secret: clientSecret,
+ send_attempt: sendAttempt,
+ next_link: nextLink,
+ },
+ })
+ );
+
+ const [error, result] = await parseMatrixResp(response);
+
+ if (error) return [error, undefined];
+
+ return [
+ undefined,
+ {
+ email,
+ clientSecret,
+ result,
+ },
+ ];
},
- [mx]
+ [requestJSON]
);
const [passwordEmailState, passwordEmail] = useAsyncCallback<
- RequestEmailTokenResponse,
- MatrixError,
+ RequestEmailTokenResult,
+ Error,
Parameters
>(passwordEmailCallback);
diff --git a/src/app/hooks/usePendingMembers.ts b/src/app/hooks/usePendingMembers.ts
new file mode 100644
index 00000000..d533c8c9
--- /dev/null
+++ b/src/app/hooks/usePendingMembers.ts
@@ -0,0 +1,74 @@
+import { useCallback, useEffect, useMemo } from 'react';
+import { getMembers, GetMembersOK } from 'neurone/lib/api/matrix/client/v3/rooms';
+import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
+import { StateEventWithoutRoomId } from 'neurone/lib/api/types';
+import { useClient } from './useClient';
+import { useObservableValue } from './useObservableValue';
+import {
+ getJoinRoomMemberId,
+ JoinRoomMemberObject,
+ sessionValue,
+} from '../../client/storage/collections';
+import { useAlive } from './useAlive';
+
+export const usePendingMembers = (roomId: string): boolean => {
+ const ct = useClient();
+ const pendingMemberRooms = useObservableValue(ct.pendingMemberRooms);
+ const hasPendingMembers = useMemo(
+ () => pendingMemberRooms.includes(roomId),
+ [pendingMemberRooms, roomId]
+ );
+
+ return hasPendingMembers;
+};
+
+export const usePendingMemberLoader = (roomId: string, pending: boolean) => {
+ const ct = useClient();
+ const alive = useAlive();
+
+ const loadMembers = useCallback(
+ async (signal?: AbortSignal) => {
+ const response = await ct.requestAuthJSON(getMembers({ pathParams: { roomId } }), signal);
+ const [err, result] = await parseMatrixResp(response);
+ if (err) {
+ throw new Error(printMatrixError(err));
+ }
+
+ const memberObjects: JoinRoomMemberObject[] =
+ result?.chunk.map((event) => {
+ const eventWithoutRoomId: StateEventWithoutRoomId = event;
+ if ('room_id' in eventWithoutRoomId) delete eventWithoutRoomId.room_id;
+
+ const obj: JoinRoomMemberObject = {
+ id: getJoinRoomMemberId(roomId, eventWithoutRoomId.state_key),
+ roomId,
+ userId: eventWithoutRoomId.state_key,
+ event: eventWithoutRoomId,
+ };
+
+ return obj;
+ }) ?? [];
+
+ if (alive()) {
+ await ct.storage.joinRoomMember.write.putMultiple(memberObjects);
+ await ct.storage.sessionValue.write.put({
+ id: sessionValue.pendingMemberRooms.id,
+ value: ct.pendingMemberRooms.read.filter((rId) => rId !== roomId),
+ });
+ }
+ },
+ [ct, roomId, alive]
+ );
+
+ useEffect(() => {
+ const abortController = new AbortController();
+
+ if (pending) {
+ loadMembers(abortController.signal);
+ }
+
+ return () => {
+ abortController.abort();
+ };
+ }, [pending, loadMembers]);
+};
diff --git a/src/app/hooks/usePowerLevelTags.ts b/src/app/hooks/usePowerLevelTags.ts
index 0a6cca50..c90ddaf4 100644
--- a/src/app/hooks/usePowerLevelTags.ts
+++ b/src/app/hooks/usePowerLevelTags.ts
@@ -1,8 +1,10 @@
-import { Room } from 'matrix-js-sdk';
-import { useMemo } from 'react';
+import { createContext, useContext, useMemo } from 'react';
+import { PendingObservableResult } from 'neurone/lib/utils/observable';
import { IPowerLevels } from './usePowerLevels';
-import { useStateEvent } from './useStateEvent';
-import { MemberPowerTag, StateEvent } from '../../types/matrix/room';
+import { MemberPowerTag, StateEventType } from '../../types/matrix/room';
+import { getJoinRoomStateId, JoinRoomStateObject } from '../../client/storage/collections';
+import { useClient } from './useClient';
+import { useObservableValuePromise } from './useObservableValue';
export type PowerLevelTags = Record;
@@ -87,11 +89,38 @@ const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): Mem
};
};
-export const usePowerLevelTags = (room: Room, powerLevels: IPowerLevels): PowerLevelTags => {
- const tagsEvent = useStateEvent(room, StateEvent.PowerLevelTags);
+export function usePowerLevelTagsState(
+ roomId: string
+): PendingObservableResult {
+ const ct = useClient();
+ const eventObservablePromise = useMemo(
+ () =>
+ ct.storage.joinRoomState.observableRead.get(
+ getJoinRoomStateId(roomId, StateEventType.PowerLevelTags, '')
+ ),
+ [ct, roomId]
+ );
+
+ return useObservableValuePromise(eventObservablePromise);
+}
+
+const PowerLevelTagsContext = createContext(null);
+
+export const PowerLevelTagsContextProvider = PowerLevelTagsContext.Provider;
+
+export const usePowerLevelTagsContext = (): PowerLevelTags => {
+ const tags = useContext(PowerLevelTagsContext);
+ if (!tags) throw new Error('PowerLevelTagsContext is not initialized!');
+ return tags;
+};
+
+export const usePowerLevelTags = (
+ powerLevels: IPowerLevels,
+ eventObj: JoinRoomStateObject | undefined
+): PowerLevelTags => {
const powerLevelTags: PowerLevelTags = useMemo(() => {
- const content = tagsEvent?.getContent();
+ const { content } = eventObj?.event ?? {};
const powerToTags: PowerLevelTags = { ...content };
const powers = getUsedPowers(powerLevels);
@@ -102,7 +131,7 @@ export const usePowerLevelTags = (room: Room, powerLevels: IPowerLevels): PowerL
});
return powerToTags;
- }, [powerLevels, tagsEvent]);
+ }, [powerLevels, eventObj]);
return powerLevelTags;
};
diff --git a/src/app/hooks/usePowerLevels.ts b/src/app/hooks/usePowerLevels.ts
index 0281b23c..80ef3fdb 100644
--- a/src/app/hooks/usePowerLevels.ts
+++ b/src/app/hooks/usePowerLevels.ts
@@ -1,11 +1,14 @@
import { MatrixEvent, Room } from 'matrix-js-sdk';
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import produce from 'immer';
-import { useStateEvent } from './useStateEvent';
-import { StateEvent } from '../../types/matrix/room';
+import { PendingObservableResult } from 'neurone/lib/utils/observable';
+import { StateEvent, StateEventType } from '../../types/matrix/room';
import { useStateEventCallback } from './useStateEventCallback';
import { useMatrixClient } from './useMatrixClient';
import { getStateEvent } from '../utils/room';
+import { getJoinRoomStateId, JoinRoomStateObject } from '../../client/storage/collections';
+import { useClient } from './useClient';
+import { useObservableValuePromise } from './useObservableValue';
export type PowerLevelActions = 'invite' | 'redact' | 'kick' | 'ban' | 'historical';
export type PowerLevelNotificationsAction = 'room';
@@ -65,17 +68,33 @@ const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
return powerLevels;
};
-export function usePowerLevels(room: Room): IPowerLevels {
- const powerLevelsEvent = useStateEvent(room, StateEvent.RoomPowerLevels);
- const powerLevels: IPowerLevels = useMemo(
- () => getPowersLevelFromMatrixEvent(powerLevelsEvent),
- [powerLevelsEvent]
+export function usePowerLevelsState(
+ roomId: string
+): PendingObservableResult {
+ const ct = useClient();
+
+ const eventObservablePromise = useMemo(
+ () =>
+ ct.storage.joinRoomState.observableRead.get(
+ getJoinRoomStateId(roomId, StateEventType.RoomPowerLevels, '')
+ ),
+ [ct, roomId]
);
+ return useObservableValuePromise(eventObservablePromise);
+}
+
+export function usePowerLevels(eventObj: JoinRoomStateObject | undefined): IPowerLevels {
+ const powerLevels: IPowerLevels = useMemo(() => {
+ const content: IPowerLevels | undefined = eventObj?.event.content;
+
+ return !content ? DEFAULT_POWER_LEVELS : fillMissingPowers(content);
+ }, [eventObj]);
+
return powerLevels;
}
-export const PowerLevelsContext = createContext(null);
+const PowerLevelsContext = createContext(null);
export const PowerLevelsContextProvider = PowerLevelsContext.Provider;
diff --git a/src/app/hooks/usePushRule.ts b/src/app/hooks/usePushRule.ts
index ec87c217..48ee655d 100644
--- a/src/app/hooks/usePushRule.ts
+++ b/src/app/hooks/usePushRule.ts
@@ -1,21 +1,21 @@
import {
- IPushRule,
- IPushRules,
+ PushRule,
PushRuleAction,
PushRuleCondition,
PushRuleKind,
- RuleId,
-} from 'matrix-js-sdk';
+ PushRules,
+} from 'neurone/lib/api/types';
import { useMemo } from 'react';
+import { PushRuleId } from '../../types/matrix/accountData';
export type PushRuleData = {
kind: PushRuleKind;
- pushRule: IPushRule;
+ pushRule: PushRule;
};
export const makePushRuleData = (
kind: PushRuleKind,
- ruleId: RuleId,
+ ruleId: PushRuleId,
actions: PushRuleAction[],
conditions?: PushRuleCondition[],
pattern?: string,
@@ -34,16 +34,16 @@ export const makePushRuleData = (
});
export const orderedPushRuleKinds: PushRuleKind[] = [
- PushRuleKind.Override,
- PushRuleKind.ContentSpecific,
- PushRuleKind.RoomSpecific,
- PushRuleKind.SenderSpecific,
- PushRuleKind.Underride,
+ 'override',
+ 'content',
+ 'room',
+ 'sender',
+ 'underride',
];
export const getPushRule = (
- pushRules: IPushRules,
- ruleId: RuleId | string
+ pushRules: PushRules,
+ ruleId: PushRuleId | string
): PushRuleData | undefined => {
const { global } = pushRules;
@@ -66,6 +66,6 @@ export const getPushRule = (
};
export const usePushRule = (
- pushRules: IPushRules,
- ruleId: RuleId | string
+ pushRules: PushRules,
+ ruleId: PushRuleId | string
): PushRuleData | undefined => useMemo(() => getPushRule(pushRules, ruleId), [pushRules, ruleId]);
diff --git a/src/app/hooks/useRecentEmoji.ts b/src/app/hooks/useRecentEmoji.ts
index 926075f5..abc40a6f 100644
--- a/src/app/hooks/useRecentEmoji.ts
+++ b/src/app/hooks/useRecentEmoji.ts
@@ -1,23 +1,28 @@
-import { useEffect, useState } from 'react';
-import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
+import { useCallback, useMemo } from 'react';
+import { PendingObservableResult } from 'neurone/lib/utils/observable';
import { getRecentEmojis } from '../plugins/recent-emoji';
-import { AccountDataEvent } from '../../types/matrix/accountData';
+import { AccountDataType } from '../../types/matrix/accountData';
import { IEmoji } from '../plugins/emoji';
+import {
+ useComputedPendingObservableResult,
+ useObservableValuePromise,
+} from './useObservableValue';
+import { useClient } from './useClient';
-export const useRecentEmoji = (mx: MatrixClient, limit?: number): IEmoji[] => {
- const [recentEmoji, setRecentEmoji] = useState(() => getRecentEmojis(mx, limit));
+export const useRecentEmoji = (limit?: number): PendingObservableResult => {
+ const ct = useClient();
+ const recentEmojiObjResult = useObservableValuePromise(
+ useMemo(
+ () => ct.storage.accountData.observableRead.get(AccountDataType.ElementRecentEmoji),
+ [ct]
+ )
+ );
- useEffect(() => {
- const handleAccountData = (event: MatrixEvent) => {
- if (event.getType() !== AccountDataEvent.ElementRecentEmoji) return;
- setRecentEmoji(getRecentEmojis(mx, limit));
- };
-
- mx.on(ClientEvent.AccountData, handleAccountData);
- return () => {
- mx.removeListener(ClientEvent.AccountData, handleAccountData);
- };
- }, [mx, limit]);
-
- return recentEmoji;
+ return useComputedPendingObservableResult(
+ recentEmojiObjResult,
+ useCallback(
+ (recentEmojiObj) => (recentEmojiObj ? getRecentEmojis(recentEmojiObj.event, limit) : []),
+ [limit]
+ )
+ );
};
diff --git a/src/app/hooks/useRecoveryAttempt.ts b/src/app/hooks/useRecoveryAttempt.ts
new file mode 100644
index 00000000..f8505c92
--- /dev/null
+++ b/src/app/hooks/useRecoveryAttempt.ts
@@ -0,0 +1,26 @@
+import { useRef } from 'react';
+
+export const getAttemptDelaySec = (attempt: number): number => {
+ if (attempt > 5) return 60;
+ if (attempt === 5) return 45;
+ if (attempt === 4) return 30;
+ if (attempt === 3) return 15;
+ if (attempt === 2) return 10;
+ if (attempt === 1) return 5;
+
+ return 0;
+};
+
+export const useRecoveryAttempt = (hasError: boolean, hasRecovered: boolean): number => {
+ const recoverAttemptRef = useRef(0);
+
+ if (hasError) {
+ recoverAttemptRef.current += 1;
+ }
+
+ if (hasRecovered) {
+ recoverAttemptRef.current = 0;
+ }
+
+ return recoverAttemptRef.current;
+};
diff --git a/src/app/hooks/useRegisterEmail.ts b/src/app/hooks/useRegisterEmail.ts
index d29c9e6d..e504ce7c 100644
--- a/src/app/hooks/useRegisterEmail.ts
+++ b/src/app/hooks/useRegisterEmail.ts
@@ -1,30 +1,54 @@
-import { MatrixClient, MatrixError } from 'matrix-js-sdk';
-import { useCallback, useRef } from 'react';
+import { useCallback, useRef, useMemo } from 'react';
+import {
+ PostEmailRegisterTokenOK,
+ postEmailRequestToken,
+} from 'neurone/lib/api/matrix/client/v3/register';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { AsyncState, useAsyncCallback } from './useAsyncCallback';
-import { RequestEmailTokenCallback, RequestEmailTokenResponse } from './types';
+import { RequestEmailTokenCallback, RequestEmailTokenResult } from './types';
+import { getJSONRequest } from '../../client/request';
export const useRegisterEmail = (
- mx: MatrixClient
-): [AsyncState, RequestEmailTokenCallback] => {
+ baseUrl: string
+): [AsyncState, RequestEmailTokenCallback] => {
+ const requestJSON = useMemo(() => getJSONRequest(baseUrl), [baseUrl]);
const sendAttemptRef = useRef(1);
const registerEmailCallback: RequestEmailTokenCallback = useCallback(
async (email, clientSecret, nextLink) => {
const sendAttempt = sendAttemptRef.current;
sendAttemptRef.current += 1;
- const result = await mx.requestRegisterEmailToken(email, clientSecret, sendAttempt, nextLink);
- return {
- email,
- clientSecret,
- result,
- };
+
+ const response = await requestJSON(
+ postEmailRequestToken({
+ body: {
+ email,
+ client_secret: clientSecret,
+ next_link: nextLink,
+ send_attempt: sendAttempt,
+ },
+ })
+ );
+
+ const [error, result] = await parseMatrixResp(response);
+
+ if (error) return [error, undefined];
+
+ return [
+ undefined,
+ {
+ email,
+ clientSecret,
+ result,
+ },
+ ];
},
- [mx]
+ [requestJSON]
);
const [registerEmailState, registerEmail] = useAsyncCallback<
- RequestEmailTokenResponse,
- MatrixError,
+ RequestEmailTokenResult,
+ Error,
Parameters
>(registerEmailCallback);
diff --git a/src/app/hooks/useReportRoomSupported.ts b/src/app/hooks/useReportRoomSupported.ts
index 198172cd..3d5baa55 100644
--- a/src/app/hooks/useReportRoomSupported.ts
+++ b/src/app/hooks/useReportRoomSupported.ts
@@ -1,10 +1,11 @@
-import { useSpecVersions } from './useSpecVersions';
+import { useSpecVersionSupport, useUnstableFeatureSupport } from './useSpecVersions';
export const useReportRoomSupported = (): boolean => {
- const { versions, unstable_features: unstableFeatures } = useSpecVersions();
+ const versionSupport = useSpecVersionSupport('v1.13');
+ const unstableSupport = useUnstableFeatureSupport()('org.matrix.msc4151');
// report room is introduced in spec version 1.13
- const supported = unstableFeatures?.['org.matrix.msc4151'] || versions.includes('v1.13');
+ const supported = unstableSupport || versionSupport;
return supported;
};
diff --git a/src/app/hooks/useRequest.ts b/src/app/hooks/useRequest.ts
new file mode 100644
index 00000000..c08a1d34
--- /dev/null
+++ b/src/app/hooks/useRequest.ts
@@ -0,0 +1,22 @@
+import { MatrixRequest } from 'neurone/lib/matrix-request';
+import { createContext, useContext } from 'react';
+
+const JSONRequestContext = createContext(null);
+export const JSONRequestProvider = JSONRequestContext.Provider;
+export const useJSONRequest = (): MatrixRequest => {
+ const request = useContext(JSONRequestContext);
+
+ if (!request) throw new Error('JSON request is not provided!');
+
+ return request;
+};
+
+const AuthJSONRequestContext = createContext(null);
+export const AuthJSONRequestProvider = AuthJSONRequestContext.Provider;
+export const useAuthJSONRequest = (): MatrixRequest => {
+ const request = useContext(AuthJSONRequestContext);
+
+ if (!request) throw new Error('Auth JSON request is not provided!');
+
+ return request;
+};
diff --git a/src/app/hooks/useRoom.ts b/src/app/hooks/useRoom.ts
index 4041887d..398c962f 100644
--- a/src/app/hooks/useRoom.ts
+++ b/src/app/hooks/useRoom.ts
@@ -1,11 +1,10 @@
-import { Room } from 'matrix-js-sdk';
import { createContext, useContext } from 'react';
-const RoomContext = createContext(null);
+const RoomContext = createContext(null);
export const RoomProvider = RoomContext.Provider;
-export function useRoom(): Room {
+export function useRoom(): string {
const room = useContext(RoomContext);
if (!room) throw new Error('Room not provided!');
return room;
diff --git a/src/app/hooks/useRoomCreators.ts b/src/app/hooks/useRoomCreators.ts
index 269d11af..911f9b2e 100644
--- a/src/app/hooks/useRoomCreators.ts
+++ b/src/app/hooks/useRoomCreators.ts
@@ -1,9 +1,9 @@
-import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
-import { useMemo } from 'react';
-import { useStateEvent } from './useStateEvent';
+import { MatrixClient, MatrixEvent } from 'matrix-js-sdk';
import { IRoomCreateContent, StateEvent } from '../../types/matrix/room';
import { creatorsSupported } from '../utils/matrix';
import { getStateEvent } from '../utils/room';
+import { useObservableValue } from './useObservableValue';
+import { useClient } from './useClient';
export const getRoomCreators = (createEvent: MatrixEvent): Set => {
const createContent = createEvent.getContent();
@@ -27,15 +27,15 @@ export const getRoomCreators = (createEvent: MatrixEvent): Set => {
return creators;
};
-export const useRoomCreators = (room: Room): Set => {
- const createEvent = useStateEvent(room, StateEvent.RoomCreate);
+export const useRoomCreators = (roomId: string): Set | undefined => {
+ const ct = useClient();
+ const roomToCreators = useObservableValue(ct.roomToCreators);
- const creators = useMemo(
- () => (createEvent ? getRoomCreators(createEvent) : new Set()),
- [createEvent]
- );
+ if (!creatorsSupported(ct.roomToVersion.read.get(roomId) ?? '1')) {
+ return undefined;
+ }
- return creators;
+ return roomToCreators.get(roomId);
};
export const getRoomCreatorsForRoomId = (mx: MatrixClient, roomId: string): Set => {
diff --git a/src/app/hooks/useRoomEvent.ts b/src/app/hooks/useRoomEvent.ts
index c2715f1f..93fdf02e 100644
--- a/src/app/hooks/useRoomEvent.ts
+++ b/src/app/hooks/useRoomEvent.ts
@@ -1,29 +1,23 @@
-import { IEvent, MatrixEvent, Room } from 'matrix-js-sdk';
import { useCallback, useMemo } from 'react';
-import to from 'await-to-js';
-import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
+import { getEvent, GetEventOK } from 'neurone/lib/api/matrix/client/v3/rooms';
import { useQuery } from '@tanstack/react-query';
-import { useMatrixClient } from './useMatrixClient';
+import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
+import { ClientEvent, MessageEventWithoutRoomId } from 'neurone/lib/api/types';
+import { useClient } from './useClient';
-const useFetchEvent = (room: Room, eventId: string) => {
- const mx = useMatrixClient();
+const useFetchEvent = (roomId: string, eventId: string) => {
+ const ct = useClient();
- const fetchEventCallback = useCallback(async () => {
- const evt = await mx.fetchRoomEvent(room.roomId, eventId);
- const mEvent = new MatrixEvent(evt);
+ const fetchEventCallback = useCallback(async (): Promise => {
+ const resp = await ct.requestAuthJSON(getEvent({ pathParams: { roomId, eventId } }));
- if (evt.unsigned?.['m.relations'] && evt.unsigned?.['m.relations']['m.replace']) {
- const replaceEvt = evt.unsigned?.['m.relations']['m.replace'] as IEvent;
- const replaceEvent = new MatrixEvent(replaceEvt);
- mEvent.makeReplaced(replaceEvent);
+ const [err, result] = await parseMatrixResp(resp);
+ if (err) {
+ throw new Error(printMatrixError(err));
}
- if (mEvent.isEncrypted() && mx.getCrypto()) {
- await to(mEvent.attemptDecryption(mx.getCrypto() as CryptoBackend));
- }
-
- return mEvent;
- }, [mx, room.roomId, eventId]);
+ return result;
+ }, [ct, roomId, eventId]);
return fetchEventCallback;
};
@@ -35,20 +29,20 @@ const useFetchEvent = (room: Room, eventId: string) => {
* @returns `MatrixEvent`, `undefined` means loading, `null` means failure
*/
export const useRoomEvent = (
- room: Room,
+ roomId: string,
eventId: string,
- getLocally?: () => MatrixEvent | undefined
+ getLocally?: () => MessageEventWithoutRoomId | undefined
) => {
const event = useMemo(() => {
if (getLocally) return getLocally();
- return room.findEventById(eventId);
- }, [room, eventId, getLocally]);
+ return undefined;
+ }, [getLocally]);
- const fetchEvent = useFetchEvent(room, eventId);
+ const fetchEvent = useFetchEvent(roomId, eventId);
const { data, error } = useQuery({
enabled: event === undefined,
- queryKey: [room.roomId, eventId],
+ queryKey: [roomId, eventId],
queryFn: fetchEvent,
staleTime: Infinity,
gcTime: 60 * 60 * 1000, // 1hour
diff --git a/src/app/hooks/useRoomMembers.ts b/src/app/hooks/useRoomMembers.ts
index df369011..e30d83e5 100644
--- a/src/app/hooks/useRoomMembers.ts
+++ b/src/app/hooks/useRoomMembers.ts
@@ -1,37 +1,108 @@
-import { MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from 'matrix-js-sdk';
-import { useEffect, useState } from 'react';
+import { createContext, useCallback, useContext, useMemo } from 'react';
+import { PendingObservableResult } from 'neurone/lib/utils/observable';
+import { RoomMembership } from 'neurone/lib/api/types';
+import { useClient } from './useClient';
+import { useObservableValuePromise } from './useObservableValue';
+import { JoinRoomMemberObject } from '../../client/storage/collections';
+import { getMxIdLocalPart } from '../utils/matrix';
+import {
+ roomMemberContentAvatarUrl,
+ roomMemberContentDisplayname,
+ roomMemberContentMembership,
+} from '../utils/room';
-export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] => {
- const [members, setMembers] = useState([]);
-
- useEffect(() => {
- const room = mx.getRoom(roomId);
- let loadingMembers = true;
- let disposed = false;
-
- const updateMemberList = (event?: MatrixEvent) => {
- if (!room || disposed || (event && event.getRoomId() !== roomId)) return;
- if (loadingMembers) return;
- setMembers(room.getMembers());
- };
-
- if (room) {
- setMembers(room.getMembers());
- room.loadMembersIfNeeded().then(() => {
- loadingMembers = false;
- if (disposed) return;
- updateMemberList();
- });
- }
-
- mx.on(RoomMemberEvent.Membership, updateMemberList);
- mx.on(RoomMemberEvent.PowerLevel, updateMemberList);
- return () => {
- disposed = true;
- mx.removeListener(RoomMemberEvent.Membership, updateMemberList);
- mx.removeListener(RoomMemberEvent.PowerLevel, updateMemberList);
- };
- }, [mx, roomId]);
-
- return members;
+export type GetRoomMemberCallback = (userId: string) => JoinRoomMemberObject | undefined;
+export type GetRoomMemberNameCallback = (userId: string) => string;
+export type GetRoomMemberAvatarCallback = (userId: string) => string | undefined;
+export type GetRoomMemberMembershipCallback = (userId: string) => RoomMembership | undefined;
+export type RoomMembersAPI = {
+ getMember: GetRoomMemberCallback;
+ getName: GetRoomMemberNameCallback;
+ getAvatar: GetRoomMemberAvatarCallback;
+ getMembership: GetRoomMemberMembershipCallback;
+ memberEventsResult: PendingObservableResult;
+};
+
+// FIXME: remove after usage over
+export const useRoomMembers = () => undefined;
+
+export const useCreateRoomMembersAPI = (roomId: string): RoomMembersAPI => {
+ const ct = useClient();
+ const memberEventsPromise = useMemo(
+ () => ct.storage.joinRoomMember.observableRead.queryIndex('roomId', roomId),
+ [ct, roomId]
+ );
+
+ const memberEventsResult = useObservableValuePromise(memberEventsPromise);
+ const memberEvents = memberEventsResult.value;
+
+ const userIdToMemberEvent = useMemo(() => {
+ const uToM: Record = {};
+ memberEvents?.forEach((member) => {
+ uToM[member.userId] = member;
+ });
+ return uToM;
+ }, [memberEvents]);
+
+ const getMember: GetRoomMemberCallback = useCallback(
+ (userId) => {
+ const member = userIdToMemberEvent[userId];
+ return member;
+ },
+ [userIdToMemberEvent]
+ );
+
+ const getName: GetRoomMemberNameCallback = useCallback(
+ (userId) => {
+ const member = getMember(userId);
+
+ const content = member?.event.content;
+ const displayName = content ? roomMemberContentDisplayname(content) : undefined;
+
+ return displayName || getMxIdLocalPart(userId) || userId;
+ },
+ [getMember]
+ );
+
+ const getAvatar: GetRoomMemberAvatarCallback = useCallback(
+ (userId) => {
+ const member = getMember(userId);
+
+ const content = member?.event.content;
+ if (!content) return undefined;
+
+ return roomMemberContentAvatarUrl(content);
+ },
+ [getMember]
+ );
+
+ const getMembership: GetRoomMemberMembershipCallback = useCallback(
+ (userId) => {
+ const member = getMember(userId);
+
+ const content = member?.event.content;
+ if (!content) return undefined;
+
+ return roomMemberContentMembership(content);
+ },
+ [getMember]
+ );
+
+ const api: RoomMembersAPI = useMemo(
+ () => ({ memberEventsResult, getMember, getName, getAvatar, getMembership }),
+ [memberEventsResult, getMember, getName, getAvatar, getMembership]
+ );
+ return api;
+};
+
+const RoomMembersAPIContext = createContext(null);
+export const RoomMembersAPIProvider = RoomMembersAPIContext.Provider;
+
+export const useRoomMembersAPI = (): RoomMembersAPI => {
+ const api = useContext(RoomMembersAPIContext);
+ if (!api) {
+ throw new Error('Room Members context is not provided!');
+ }
+
+ return api;
};
diff --git a/src/app/hooks/useRoomNavigate.ts b/src/app/hooks/useRoomNavigate.ts
index b2d7a91a..67912b4c 100644
--- a/src/app/hooks/useRoomNavigate.ts
+++ b/src/app/hooks/useRoomNavigate.ts
@@ -1,24 +1,25 @@
import { useCallback } from 'react';
import { NavigateOptions, useNavigate } from 'react-router-dom';
import { useAtomValue } from 'jotai';
-import { getCanonicalAliasOrRoomId } from '../utils/matrix';
import {
getDirectRoomPath,
getHomeRoomPath,
getSpacePath,
getSpaceRoomPath,
} from '../pages/pathUtils';
-import { useMatrixClient } from './useMatrixClient';
import { getOrphanParents, guessPerfectParent } from '../utils/room';
import { roomToParentsAtom } from '../state/room/roomToParents';
import { mDirectAtom } from '../state/mDirectList';
import { useSelectedSpace } from './router/useSelectedSpace';
import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings';
+import { useClient } from './useClient';
+import { getJoinRoomStateId } from '../../client/storage/collections';
+import { StateEventType } from '../../types/matrix/room';
export const useRoomNavigate = () => {
+ const ct = useClient();
const navigate = useNavigate();
- const mx = useMatrixClient();
const roomToParents = useAtomValue(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom);
const spaceSelectedId = useSelectedSpace();
@@ -26,16 +27,17 @@ export const useRoomNavigate = () => {
const navigateSpace = useCallback(
(roomId: string) => {
- const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
+ const roomIdOrAlias = ct.roomToAlias.read.get(roomId) ?? roomId;
navigate(getSpacePath(roomIdOrAlias));
},
- [mx, navigate]
+ [ct, navigate]
);
const navigateRoom = useCallback(
- (roomId: string, eventId?: string, opts?: NavigateOptions) => {
- const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
- const openSpaceTimeline = developerTools && spaceSelectedId === roomId;
+ async (roomId: string, eventId?: string, opts?: NavigateOptions) => {
+ const roomIdOrAlias = ct.roomToAlias.read.get(roomId) ?? roomId;
+ const openSpaceTimeline =
+ developerTools && ct.spaceRooms.read.has(roomId) && spaceSelectedId === roomId;
const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
if (orphanParents.length > 0) {
@@ -43,10 +45,22 @@ export const useRoomNavigate = () => {
if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) {
parentSpace = spaceSelectedId;
} else {
- parentSpace = guessPerfectParent(mx, roomId, orphanParents) ?? orphanParents[0];
+ const powerEventIds = [roomId]
+ .concat(orphanParents)
+ .map((rId) => getJoinRoomStateId(rId, StateEventType.RoomPowerLevels, ''));
+ const powerLevelEventObjs = await ct.storage.joinRoomState.read.getMultiple(
+ powerEventIds
+ );
+ parentSpace =
+ guessPerfectParent(
+ roomId,
+ orphanParents,
+ (rId) => ct.roomToCreators.read.get(rId),
+ (rId) => powerLevelEventObjs.find((obj) => obj?.roomId === rId)?.event.content
+ ) ?? orphanParents[0];
}
- const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
+ const pSpaceIdOrAlias = ct.roomToAlias.read.get(parentSpace) ?? parentSpace;
navigate(
getSpaceRoomPath(pSpaceIdOrAlias, openSpaceTimeline ? roomId : roomIdOrAlias, eventId),
@@ -62,7 +76,7 @@ export const useRoomNavigate = () => {
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
},
- [mx, navigate, spaceSelectedId, roomToParents, mDirects, developerTools]
+ [ct, navigate, spaceSelectedId, roomToParents, mDirects, developerTools]
);
return {
diff --git a/src/app/hooks/useRoomPermissions.ts b/src/app/hooks/useRoomPermissions.ts
index cb6f69a2..0ad17989 100644
--- a/src/app/hooks/useRoomPermissions.ts
+++ b/src/app/hooks/useRoomPermissions.ts
@@ -14,30 +14,30 @@ export type RoomPermissionsAPI = {
};
export const getRoomPermissionsAPI = (
- creators: Set,
+ creators: Set | undefined,
powerLevels: IPowerLevels
): RoomPermissionsAPI => {
const api: RoomPermissionsAPI = {
event: (type, userId) => {
- if (creators.has(userId)) return true;
+ if (creators?.has(userId)) return true;
const userPower = readPowerLevel.user(powerLevels, userId);
const requiredPL = readPowerLevel.event(powerLevels, type);
return userPower >= requiredPL;
},
stateEvent: (type, userId) => {
- if (creators.has(userId)) return true;
+ if (creators?.has(userId)) return true;
const userPower = readPowerLevel.user(powerLevels, userId);
const requiredPL = readPowerLevel.state(powerLevels, type);
return userPower >= requiredPL;
},
action: (action, userId) => {
- if (creators.has(userId)) return true;
+ if (creators?.has(userId)) return true;
const userPower = readPowerLevel.user(powerLevels, userId);
const requiredPL = readPowerLevel.action(powerLevels, action);
return userPower >= requiredPL;
},
notificationAction: (action, userId) => {
- if (creators.has(userId)) return true;
+ if (creators?.has(userId)) return true;
const userPower = readPowerLevel.user(powerLevels, userId);
const requiredPL = readPowerLevel.notification(powerLevels, action);
return userPower >= requiredPL;
@@ -48,7 +48,7 @@ export const getRoomPermissionsAPI = (
};
export const useRoomPermissions = (
- creators: Set,
+ creators: Set | undefined,
powerLevels: IPowerLevels
): RoomPermissionsAPI => {
const api: RoomPermissionsAPI = useMemo(
diff --git a/src/app/hooks/useRoomPinnedEvents.ts b/src/app/hooks/useRoomPinnedEvents.ts
index 9ab1d6bd..42d88fa7 100644
--- a/src/app/hooks/useRoomPinnedEvents.ts
+++ b/src/app/hooks/useRoomPinnedEvents.ts
@@ -1,14 +1,16 @@
import { useMemo } from 'react';
-import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
-import { Room } from 'matrix-js-sdk';
-import { StateEvent } from '../../types/matrix/room';
-import { useStateEvent } from './useStateEvent';
+import { StateEventType } from '../../types/matrix/room';
+import { useRoomStateEvent } from './storage/useRoomStateEvent';
-export const useRoomPinnedEvents = (room: Room): string[] => {
- const pinEvent = useStateEvent(room, StateEvent.RoomPinnedEvents);
+export const useRoomPinnedEvents = (roomId: string): string[] => {
+ const pinEventResult = useRoomStateEvent(roomId, StateEventType.RoomPinnedEvents, '');
+ const pinEvent = pinEventResult.value;
const events = useMemo(() => {
- const content = pinEvent?.getContent();
- return content?.pinned ?? [];
+ const { content } = pinEvent?.event ?? {};
+ if (content && 'pinned' in content && Array.isArray(content.pinned)) {
+ return content.pinned;
+ }
+ return [];
}, [pinEvent]);
return events;
diff --git a/src/app/hooks/useRoomTypingMembers.ts b/src/app/hooks/useRoomTypingMembers.ts
index f526cbf1..28570fd5 100644
--- a/src/app/hooks/useRoomTypingMembers.ts
+++ b/src/app/hooks/useRoomTypingMembers.ts
@@ -1,18 +1,11 @@
import { useAtomValue } from 'jotai';
import { selectAtom } from 'jotai/utils';
import { useCallback } from 'react';
-import {
- IRoomIdToTypingMembers,
- TypingReceipt,
- roomIdToTypingMembersAtom,
-} from '../state/typingMembers';
+import { IRoomIdToTypingMembers, roomIdToTypingMembersAtom } from '../state/typingMembers';
-const typingReceiptEqual = (a: TypingReceipt, b: TypingReceipt): boolean =>
- a.userId === b.userId && a.ts === b.ts;
-
-const equalTypingMembers = (x: TypingReceipt[], y: TypingReceipt[]): boolean => {
+const equalTypingMembers = (x: string[], y: string[]): boolean => {
if (x.length !== y.length) return false;
- return x.every((a, i) => typingReceiptEqual(a, y[i]));
+ return x.every((a, i) => a[1] === y[i]);
};
export const useRoomTypingMember = (roomId: string) => {
diff --git a/src/app/hooks/useRoomsNotificationPreferences.ts b/src/app/hooks/useRoomsNotificationPreferences.ts
index ceea95f7..4897806a 100644
--- a/src/app/hooks/useRoomsNotificationPreferences.ts
+++ b/src/app/hooks/useRoomsNotificationPreferences.ts
@@ -1,8 +1,7 @@
import { createContext, useCallback, useContext, useMemo } from 'react';
-import { ConditionKind, IPushRules, MatrixClient, PushRuleKind } from 'matrix-js-sdk';
+import { AccountDataEvent, PushRules } from 'neurone/lib/api/types';
+import { deletePushRule, putPushRule } from 'neurone/lib/api/matrix/client/v3/pushrules';
import { Icons, IconSrc } from 'folds';
-import { AccountDataEvent } from '../../types/matrix/accountData';
-import { useAccountData } from './useAccountData';
import { isRoomId } from '../utils/matrix';
import {
getNotificationMode,
@@ -10,7 +9,10 @@ import {
NotificationMode,
} from './useNotificationMode';
import { useAsyncCallback } from './useAsyncCallback';
-import { useMatrixClient } from './useMatrixClient';
+import { useAccountData } from './storage/useAccountData';
+import { ClientContext } from '../../client/context';
+import { useClient } from './useClient';
+import { AccountDataType, ConditionKind } from '../../types/matrix/accountData';
export type RoomsNotificationPreferences = {
mute: Set;
@@ -34,7 +36,10 @@ export const useRoomsNotificationPreferencesContext = (): RoomsNotificationPrefe
};
export const useRoomsNotificationPreferences = (): RoomsNotificationPreferences => {
- const pushRules = useAccountData(AccountDataEvent.PushRules)?.getContent();
+ const { value: pushRulesEvent } = useAccountData>(
+ AccountDataType.PushRules
+ );
+ const pushRules = pushRulesEvent?.content;
const preferences: RoomsNotificationPreferences = useMemo(() => {
const global = pushRules?.global;
@@ -108,7 +113,7 @@ export const getRoomNotificationModeIcon = (mode?: RoomNotificationMode): IconSr
};
export const setRoomNotificationPreference = async (
- mx: MatrixClient,
+ ct: ClientContext,
roomId: string,
mode: RoomNotificationMode,
previousMode: RoomNotificationMode
@@ -118,10 +123,26 @@ export const setRoomNotificationPreference = async (
previousMode === RoomNotificationMode.AllMessages ||
previousMode === RoomNotificationMode.SpecialMessages
) {
- await mx.deletePushRule('global', PushRuleKind.RoomSpecific, roomId);
+ await ct.requestAuthJSON(
+ deletePushRule({
+ pathParams: {
+ scope: 'global',
+ kind: 'room',
+ ruleId: roomId,
+ },
+ })
+ );
}
if (previousMode === RoomNotificationMode.Mute) {
- await mx.deletePushRule('global', PushRuleKind.Override, roomId);
+ await ct.requestAuthJSON(
+ deletePushRule({
+ pathParams: {
+ scope: 'global',
+ kind: 'override',
+ ruleId: roomId,
+ },
+ })
+ );
}
// set new preference
@@ -130,35 +151,53 @@ export const setRoomNotificationPreference = async (
}
if (mode === RoomNotificationMode.Mute) {
- await mx.addPushRule('global', PushRuleKind.Override, roomId, {
- conditions: [
- {
- kind: ConditionKind.EventMatch,
- key: 'room_id',
- pattern: roomId,
+ await ct.requestAuthJSON(
+ putPushRule({
+ pathParams: {
+ scope: 'global',
+ kind: 'override',
+ ruleId: roomId,
},
- ],
- actions: getNotificationModeActions(NotificationMode.OFF),
- });
+ body: {
+ conditions: [
+ {
+ kind: ConditionKind.EventMatch,
+ key: 'room_id',
+ pattern: roomId,
+ },
+ ],
+ actions: getNotificationModeActions(NotificationMode.OFF),
+ },
+ })
+ );
return;
}
- await mx.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
- actions:
- mode === RoomNotificationMode.AllMessages
- ? getNotificationModeActions(NotificationMode.NotifyLoud)
- : getNotificationModeActions(NotificationMode.OFF),
- });
+ await ct.requestAuthJSON(
+ putPushRule({
+ pathParams: {
+ scope: 'global',
+ kind: 'room',
+ ruleId: roomId,
+ },
+ body: {
+ actions:
+ mode === RoomNotificationMode.AllMessages
+ ? getNotificationModeActions(NotificationMode.NotifyLoud)
+ : getNotificationModeActions(NotificationMode.OFF),
+ },
+ })
+ );
};
export const useSetRoomNotificationPreference = (roomId: string) => {
- const mx = useMatrixClient();
+ const ct = useClient();
const [modeState, setMode] = useAsyncCallback(
useCallback(
(mode: RoomNotificationMode, previousMode: RoomNotificationMode) =>
- setRoomNotificationPreference(mx, roomId, mode, previousMode),
- [mx, roomId]
+ setRoomNotificationPreference(ct, roomId, mode, previousMode),
+ [ct, roomId]
)
);
diff --git a/src/app/hooks/useSecondsCountdown.ts b/src/app/hooks/useSecondsCountdown.ts
new file mode 100644
index 00000000..a101e1f7
--- /dev/null
+++ b/src/app/hooks/useSecondsCountdown.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from 'react';
+
+export const useSecondsCountdown = (seconds: number, onEnd?: () => void): number => {
+ const [countdown, setCountdown] = useState(seconds);
+
+ useEffect(() => {
+ const timeoutId = window.setTimeout(() => {
+ if (countdown <= 1) {
+ onEnd?.();
+ } else {
+ setCountdown(countdown - 1);
+ }
+ }, 1000);
+
+ return () => {
+ window.clearTimeout(timeoutId);
+ };
+ }, [countdown, onEnd]);
+
+ return countdown;
+};
diff --git a/src/app/hooks/useSidebarItems.ts b/src/app/hooks/useSidebarItems.ts
index 16aba225..375c40e7 100644
--- a/src/app/hooks/useSidebarItems.ts
+++ b/src/app/hooks/useSidebarItems.ts
@@ -1,10 +1,7 @@
-import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
-import { MatrixClient } from 'matrix-js-sdk';
-import { AccountDataEvent } from '../../types/matrix/accountData';
-import { useMatrixClient } from './useMatrixClient';
-import { getAccountData, isSpace } from '../utils/room';
-import { Membership } from '../../types/matrix/room';
-import { useAccountDataCallback } from './useAccountDataCallback';
+import { useMemo } from 'react';
+import { AccountDataEvent } from 'neurone/lib/api/types';
+import { useClient } from './useClient';
+import { ClientContext } from '../../client/context';
export type ISidebarFolder = {
name?: string;
@@ -20,7 +17,7 @@ export type InCinnySpacesContent = {
};
export const parseSidebar = (
- mx: MatrixClient,
+ ct: ClientContext,
orphanSpaces: string[],
content?: InCinnySpacesContent
) => {
@@ -31,9 +28,8 @@ export const parseSidebar = (
const safeToAdd = (spaceId: string): boolean => {
if (typeof spaceId !== 'string') return false;
- const space = mx.getRoom(spaceId);
- if (space?.getMyMembership() !== Membership.Join) return false;
- return isSpace(space);
+ if (!ct.rooms.join.read.includes(spaceId)) return false;
+ return ct.spaceRooms.read.has(spaceId);
};
sidebar.forEach((item) => {
@@ -64,40 +60,17 @@ export const parseSidebar = (
};
export const useSidebarItems = (
- orphanSpaces: string[]
-): [SidebarItems, Dispatch>] => {
- const mx = useMatrixClient();
+ orphanSpaces: string[],
+ cinnySpacesEvent?: AccountDataEvent
+): SidebarItems => {
+ const ct = useClient();
- const [sidebarItems, setSidebarItems] = useState(() => {
- const inCinnySpacesContent = getAccountData(
- mx,
- AccountDataEvent.CinnySpaces
- )?.getContent();
- return parseSidebar(mx, orphanSpaces, inCinnySpacesContent);
- });
+ const sidebarItems: SidebarItems = useMemo(() => {
+ const content = cinnySpacesEvent?.content;
+ return parseSidebar(ct, orphanSpaces, content);
+ }, [ct, cinnySpacesEvent, orphanSpaces]);
- useEffect(() => {
- const inCinnySpacesContent = getAccountData(
- mx,
- AccountDataEvent.CinnySpaces
- )?.getContent();
- setSidebarItems(parseSidebar(mx, orphanSpaces, inCinnySpacesContent));
- }, [mx, orphanSpaces]);
-
- useAccountDataCallback(
- mx,
- useCallback(
- (mEvent) => {
- if (mEvent.getType() === AccountDataEvent.CinnySpaces) {
- const newContent = mEvent.getContent();
- setSidebarItems(parseSidebar(mx, orphanSpaces, newContent));
- }
- },
- [mx, orphanSpaces]
- )
- );
-
- return [sidebarItems, setSidebarItems];
+ return sidebarItems;
};
export const sidebarItemWithout = (items: SidebarItems, roomId: string) => {
@@ -123,16 +96,24 @@ export const sidebarItemWithout = (items: SidebarItems, roomId: string) => {
};
export const makeCinnySpacesContent = (
- mx: MatrixClient,
- items: SidebarItems
+ items: SidebarItems,
+ cinnySpacesEvent?: AccountDataEvent
): InCinnySpacesContent => {
- const currentInSpaces =
- getAccountData(mx, AccountDataEvent.CinnySpaces)?.getContent() ?? {};
+ const content = cinnySpacesEvent?.content ?? {};
const newSpacesContent: InCinnySpacesContent = {
- ...currentInSpaces,
+ ...content,
sidebar: items,
};
return newSpacesContent;
};
+
+export const useSidebarRooms = (sidebarItems: SidebarItems): string[] => {
+ const rooms = useMemo(() => {
+ const ids = sidebarItems.flatMap((item) => (typeof item === 'string' ? [item] : item.content));
+ return Array.from(new Set(ids));
+ }, [sidebarItems]);
+
+ return rooms;
+};
diff --git a/src/app/hooks/useSpace.ts b/src/app/hooks/useSpace.ts
index 8bec49e4..595c6d8d 100644
--- a/src/app/hooks/useSpace.ts
+++ b/src/app/hooks/useSpace.ts
@@ -1,17 +1,16 @@
-import { Room } from 'matrix-js-sdk';
import { createContext, useContext } from 'react';
-const SpaceContext = createContext(null);
+const SpaceContext = createContext(null);
export const SpaceProvider = SpaceContext.Provider;
-export function useSpace(): Room {
+export function useSpace(): string {
const space = useContext(SpaceContext);
if (!space) throw new Error('Space not provided!');
return space;
}
-export function useSpaceOptionally(): Room | null {
+export function useSpaceOptionally(): string | undefined {
const space = useContext(SpaceContext);
- return space;
+ return space ?? undefined;
}
diff --git a/src/app/hooks/useSpaceHierarchy.ts b/src/app/hooks/useSpaceHierarchy.ts
index ad34e3f4..8379fd86 100644
--- a/src/app/hooks/useSpaceHierarchy.ts
+++ b/src/app/hooks/useSpaceHierarchy.ts
@@ -1,20 +1,22 @@
-import { atom, useAtom, useAtomValue } from 'jotai';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { MatrixError, Room } from 'matrix-js-sdk';
-import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
+import { useAtomValue } from 'jotai';
+import { useCallback, useEffect, useMemo, useRef } from 'react';
+import { SpaceHierarchyRoomChunk, StateEventWithoutRoomId } from 'neurone/lib/api/types';
import { QueryFunction, useInfiniteQuery } from '@tanstack/react-query';
-import { useMatrixClient } from './useMatrixClient';
-import { roomToParentsAtom } from '../state/room/roomToParents';
-import { MSpaceChildContent, StateEvent } from '../../types/matrix/room';
-import { getAllParents, getStateEvents, isValidChild } from '../utils/room';
+import { getHierarchy, GetHierarchyOK } from 'neurone/lib/api/matrix/client/v1/rooms';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
+import { SpaceChildContent } from '../../types/matrix/room';
import { isRoomId } from '../utils/matrix';
-import { SortFunc, byOrderKey, byTsOldToNew, factoryRoomIdByActivity } from '../utils/sort';
-import { useStateEventCallback } from './useStateEventCallback';
+import { SortFunc, byOrderKey, byTsOldToNew } from '../utils/sort';
import { ErrorCode } from '../cs-errorcode';
+import { useObservableValue } from './useObservableValue';
+import { useClient } from './useClient';
+import { useAllJoinedRoomsSet } from './useGetRoom';
+import { spaceRoomsAtom } from '../state/spaceRooms';
+import { rError } from '../../client/reader';
export type HierarchyItemSpace = {
roomId: string;
- content: MSpaceChildContent;
+ content: SpaceChildContent;
ts: number;
space: true;
parentId?: string;
@@ -22,23 +24,22 @@ export type HierarchyItemSpace = {
export type HierarchyItemRoom = {
roomId: string;
- content: MSpaceChildContent;
+ content: SpaceChildContent;
ts: number;
parentId: string;
};
export type HierarchyItem = HierarchyItemSpace | HierarchyItemRoom;
-type GetRoomCallback = (roomId: string) => Room | undefined;
-
const hierarchyItemTs: SortFunc = (a, b) => byTsOldToNew(a.ts, b.ts);
const hierarchyItemByOrder: SortFunc = (a, b) =>
byOrderKey(a.content.order, b.content.order);
const getHierarchySpaces = (
rootSpaceId: string,
- getRoom: GetRoomCallback,
- spaceRooms: Set
+ roomToChildEvents: Map[]>,
+ spaceRooms: Set,
+ spaceRoomsCache: Set
): HierarchyItemSpace[] => {
const rootSpaceItem: HierarchyItemSpace = {
roomId: rootSpaceId,
@@ -50,25 +51,22 @@ const getHierarchySpaces = (
const findAndCollectHierarchySpaces = (spaceItem: HierarchyItemSpace) => {
if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return;
- const space = getRoom(spaceItem.roomId);
spaceItems.push(spaceItem);
- if (!space) return;
- const childEvents = getStateEvents(space, StateEvent.SpaceChild);
+ const childEvents = roomToChildEvents.get(spaceItem.roomId);
- childEvents.forEach((childEvent) => {
- if (!isValidChild(childEvent)) return;
- const childId = childEvent.getStateKey();
- if (!childId || !isRoomId(childId)) return;
+ childEvents?.forEach((childEvent) => {
+ const childId = childEvent.state_key;
+ if (!isRoomId(childId)) return;
// because we can not find if a childId is space without joining
// or requesting room summary, we will look it into spaceRooms local
// cache which we maintain as we load summary in UI.
- if (getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId)) {
+ if (spaceRooms.has(childId) || spaceRoomsCache.has(childId)) {
const childItem: HierarchyItemSpace = {
roomId: childId,
- content: childEvent.getContent(),
- ts: childEvent.getTs(),
+ content: childEvent.content,
+ ts: childEvent.origin_server_ts,
space: true,
parentId: spaceItem.roomId,
};
@@ -95,31 +93,30 @@ export type SpaceHierarchy = {
};
const getSpaceHierarchy = (
rootSpaceId: string,
+ roomToChildEvents: Map[]>,
spaceRooms: Set,
- getRoom: (roomId: string) => Room | undefined,
- closedCategory: (spaceId: string) => boolean
+ spaceRoomsCache: Set
): SpaceHierarchy[] => {
- const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, spaceRooms);
+ const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(
+ rootSpaceId,
+ roomToChildEvents,
+ spaceRooms,
+ spaceRoomsCache
+ );
const hierarchy: SpaceHierarchy[] = spaceItems.map((spaceItem) => {
- const space = getRoom(spaceItem.roomId);
- if (!space || closedCategory(spaceItem.roomId)) {
- return {
- space: spaceItem,
- };
- }
- const childEvents = getStateEvents(space, StateEvent.SpaceChild);
+ const childEvents = roomToChildEvents.get(spaceItem.roomId);
+
const childItems: HierarchyItemRoom[] = [];
- childEvents.forEach((childEvent) => {
- if (!isValidChild(childEvent)) return;
- const childId = childEvent.getStateKey();
+ childEvents?.forEach((childEvent) => {
+ const childId = childEvent.state_key;
if (!childId || !isRoomId(childId)) return;
- if (getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId)) return;
+ if (spaceRooms.has(childId) || spaceRoomsCache.has(childId)) return;
const childItem: HierarchyItemRoom = {
roomId: childId,
- content: childEvent.getContent(),
- ts: childEvent.getTs(),
+ content: childEvent.content,
+ ts: childEvent.origin_server_ts,
parentId: spaceItem.roomId,
};
childItems.push(childItem);
@@ -134,38 +131,15 @@ const getSpaceHierarchy = (
return hierarchy;
};
-export const useSpaceHierarchy = (
- spaceId: string,
- spaceRooms: Set,
- getRoom: (roomId: string) => Room | undefined,
- closedCategory: (spaceId: string) => boolean
-): SpaceHierarchy[] => {
- const mx = useMatrixClient();
- const roomToParents = useAtomValue(roomToParentsAtom);
+export const useSpaceHierarchy = (spaceId: string): SpaceHierarchy[] => {
+ const ct = useClient();
+ const roomToChildEvents = useObservableValue(ct.roomToChildEvents);
+ const spaceRooms = useObservableValue(ct.spaceRooms);
+ const spaceRoomsCache = useAtomValue(spaceRoomsAtom);
- const [hierarchyAtom] = useState(() =>
- atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory))
- );
- const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
-
- useEffect(() => {
- setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
- }, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory]);
-
- useStateEventCallback(
- mx,
- useCallback(
- (mEvent) => {
- if (mEvent.getType() !== StateEvent.SpaceChild) return;
- const eventRoomId = mEvent.getRoomId();
- if (!eventRoomId) return;
-
- if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) {
- setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
- }
- },
- [spaceId, roomToParents, setHierarchy, spaceRooms, getRoom, closedCategory]
- )
+ const hierarchy = useMemo(
+ () => getSpaceHierarchy(spaceId, roomToChildEvents, spaceRooms, spaceRoomsCache),
+ [spaceId, roomToChildEvents, spaceRooms, spaceRoomsCache]
);
return hierarchy;
@@ -173,94 +147,65 @@ export const useSpaceHierarchy = (
const getSpaceJoinedHierarchy = (
rootSpaceId: string,
- getRoom: GetRoomCallback,
- excludeRoom: (parentId: string, roomId: string) => boolean,
+ joinedRooms: Set,
+ roomToChildEvents: Map[]>,
+ spaceRooms: Set,
sortRoomItems: (parentId: string, items: HierarchyItem[]) => HierarchyItem[]
): HierarchyItem[] => {
- const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(rootSpaceId, getRoom, new Set());
+ const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(
+ rootSpaceId,
+ roomToChildEvents,
+ spaceRooms,
+ new Set()
+ );
const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
- const space = getRoom(spaceItem.roomId);
- if (!space) {
- return [];
- }
- const joinedRoomEvents = getStateEvents(space, StateEvent.SpaceChild).filter((childEvent) => {
- if (!isValidChild(childEvent)) return false;
- const childId = childEvent.getStateKey();
- if (!childId || !isRoomId(childId)) return false;
- const room = getRoom(childId);
- if (!room || room.isSpaceRoom()) return false;
-
- return true;
- });
-
- if (joinedRoomEvents.length === 0) return [];
+ const childEvents = roomToChildEvents.get(spaceItem.roomId);
const childItems: HierarchyItemRoom[] = [];
- joinedRoomEvents.forEach((childEvent) => {
- const childId = childEvent.getStateKey();
- if (!childId) return;
- if (excludeRoom(space.roomId, childId)) return;
+ childEvents?.forEach((childEvent) => {
+ const childId = childEvent.state_key;
+ if (!childId || !isRoomId(childId)) return;
+ if (!joinedRooms.has(childId) || spaceRooms.has(childId)) return;
const childItem: HierarchyItemRoom = {
roomId: childId,
- content: childEvent.getContent(),
- ts: childEvent.getTs(),
+ content: childEvent.content,
+ ts: childEvent.origin_server_ts,
parentId: spaceItem.roomId,
};
childItems.push(childItem);
});
+ if (childItems.length === 0) return [];
+
return [spaceItem, ...sortRoomItems(spaceItem.roomId, childItems)];
});
return hierarchy;
};
-export const useSpaceJoinedHierarchy = (
- spaceId: string,
- getRoom: GetRoomCallback,
- excludeRoom: (parentId: string, roomId: string) => boolean,
- sortByActivity: (spaceId: string) => boolean
-): HierarchyItem[] => {
- const mx = useMatrixClient();
- const roomToParents = useAtomValue(roomToParentsAtom);
+export const useSpaceJoinedHierarchy = (spaceId: string): HierarchyItem[] => {
+ const ct = useClient();
+ const roomToChildEvents = useObservableValue(ct.roomToChildEvents);
+ const allJoinedRooms = useAllJoinedRoomsSet();
+ const spaceRooms = useObservableValue(ct.spaceRooms);
- const sortRoomItems = useCallback(
- (sId: string, items: HierarchyItem[]) => {
- if (sortByActivity(sId)) {
- items.sort((a, b) => factoryRoomIdByActivity(mx)(a.roomId, b.roomId));
- return items;
- }
- items.sort(hierarchyItemTs).sort(hierarchyItemByOrder);
- return items;
- },
- [mx, sortByActivity]
- );
+ const sortRoomItems = useCallback((sId: string, items: HierarchyItem[]) => {
+ items.sort(hierarchyItemTs).sort(hierarchyItemByOrder);
+ return items;
+ }, []);
- const [hierarchyAtom] = useState(() =>
- atom(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems))
- );
- const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
-
- useEffect(() => {
- setHierarchy(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems));
- }, [mx, spaceId, setHierarchy, getRoom, excludeRoom, sortRoomItems]);
-
- useStateEventCallback(
- mx,
- useCallback(
- (mEvent) => {
- if (mEvent.getType() !== StateEvent.SpaceChild) return;
- const eventRoomId = mEvent.getRoomId();
- if (!eventRoomId) return;
-
- if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) {
- setHierarchy(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems));
- }
- },
- [spaceId, roomToParents, setHierarchy, getRoom, excludeRoom, sortRoomItems]
- )
+ const hierarchy = useMemo(
+ () =>
+ getSpaceJoinedHierarchy(
+ spaceId,
+ allJoinedRooms,
+ roomToChildEvents,
+ spaceRooms,
+ sortRoomItems
+ ),
+ [spaceId, allJoinedRooms, roomToChildEvents, spaceRooms, sortRoomItems]
);
return hierarchy;
@@ -272,22 +217,36 @@ const MAX_AUTO_PAGE_COUNT = 50;
export type FetchSpaceHierarchyLevelData = {
fetching: boolean;
error: Error | null;
- rooms: Map;
+ rooms: Map;
};
export const useFetchSpaceHierarchyLevel = (
roomId: string,
enable: boolean
): FetchSpaceHierarchyLevelData => {
- const mx = useMatrixClient();
+ const ct = useClient();
const pageNoRef = useRef(0);
- const fetchLevel: QueryFunction<
- Awaited>,
- string[],
- string | undefined
- > = useCallback(
- ({ pageParam }) => mx.getRoomHierarchy(roomId, PER_PAGE_COUNT, 1, false, pageParam),
- [roomId, mx]
+ const fetchLevel: QueryFunction = useCallback(
+ async ({ pageParam }) => {
+ const response = await ct.requestAuthJSON(
+ getHierarchy({
+ pathParams: { roomId },
+ searchParams: {
+ from: pageParam,
+ max_depth: 1,
+ limit: PER_PAGE_COUNT,
+ suggested_only: false,
+ },
+ })
+ );
+ const [err, result] = await parseMatrixResp(response);
+ if (err) {
+ throw err.data;
+ }
+
+ return result;
+ },
+ [roomId, ct]
);
const queryResponse = useInfiniteQuery({
@@ -301,11 +260,8 @@ export const useFetchSpaceHierarchyLevel = (
},
retry: 5,
retryDelay: (failureCount, error) => {
- if (error instanceof MatrixError && error.errcode === ErrorCode.M_LIMIT_EXCEEDED) {
- const { retry_after_ms: delay } = error.data;
- if (typeof delay === 'number') {
- return delay;
- }
+ if (rError.errcode(error) === ErrorCode.M_LIMIT_EXCEEDED && rError.retryAfterMs(error)) {
+ return rError.retryAfterMs(error) ?? 500 * failureCount;
}
return 500 * failureCount;
@@ -327,8 +283,8 @@ export const useFetchSpaceHierarchyLevel = (
}
}, [fetchNextPage, hasNextPage, data, error]);
- const rooms: Map = useMemo(() => {
- const roomsMap: Map = new Map();
+ const rooms: Map = useMemo(() => {
+ const roomsMap: Map = new Map();
if (!data) return roomsMap;
const rms = data.pages.flatMap((result) => result.rooms);
diff --git a/src/app/hooks/useSpecVersions.ts b/src/app/hooks/useSpecVersions.ts
index 42403c61..7f6318d0 100644
--- a/src/app/hooks/useSpecVersions.ts
+++ b/src/app/hooks/useSpecVersions.ts
@@ -1,4 +1,4 @@
-import { createContext, useContext } from 'react';
+import { createContext, useCallback, useContext } from 'react';
import { SpecVersions } from '../cs-api';
const SpecVersionsContext = createContext(null);
@@ -10,3 +10,20 @@ export function useSpecVersions(): SpecVersions {
if (!versions) throw new Error('Server versions are not provided!');
return versions;
}
+
+export function useSpecVersionSupport(specVersion: string): boolean {
+ const { versions } = useSpecVersions();
+
+ return versions.includes(specVersion);
+}
+
+export function useUnstableFeatureSupport(): (unstablePrefix: string) => boolean | undefined {
+ const { unstable_features: unstableFeatures } = useSpecVersions();
+
+ const checkSupport = useCallback(
+ (unstablePrefix: string): boolean | undefined => unstableFeatures?.[unstablePrefix],
+ [unstableFeatures]
+ );
+
+ return checkSupport;
+}
diff --git a/src/app/hooks/useTypingStatusUpdater.ts b/src/app/hooks/useTypingStatusUpdater.ts
index db8ceff1..b06684de 100644
--- a/src/app/hooks/useTypingStatusUpdater.ts
+++ b/src/app/hooks/useTypingStatusUpdater.ts
@@ -1,10 +1,14 @@
-import { MatrixClient } from 'matrix-js-sdk';
import { useMemo, useRef } from 'react';
-import { TYPING_TIMEOUT_MS } from '../state/typingMembers';
+import { putTyping } from 'neurone/lib/api/matrix/client/v3/rooms';
+import { useClient } from './useClient';
+
+const TYPING_TIMEOUT_MS = 5000; // 5 seconds
type TypingStatusUpdater = (typing: boolean) => void;
-export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): TypingStatusUpdater => {
+export const useTypingStatusUpdater = (roomId: string): TypingStatusUpdater => {
+ const ct = useClient();
+ const { userId } = ct.session;
const statusSentTsRef = useRef(0);
const sendTypingStatus: TypingStatusUpdater = useMemo(() => {
@@ -15,7 +19,16 @@ export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): Typing
return;
}
- mx.sendTyping(roomId, true, TYPING_TIMEOUT_MS);
+ ct.requestAuthJSON(
+ putTyping({
+ pathParams: { roomId, userId },
+ body: {
+ typing: true,
+ timeout: TYPING_TIMEOUT_MS,
+ },
+ })
+ );
+
const sentTs = Date.now();
statusSentTsRef.current = sentTs;
@@ -23,7 +36,12 @@ export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): Typing
// Clear typing status after timeout if already not;
setTimeout(() => {
if (statusSentTsRef.current === sentTs) {
- mx.sendTyping(roomId, false, TYPING_TIMEOUT_MS);
+ ct.requestAuthJSON(
+ putTyping({
+ pathParams: { roomId, userId },
+ body: { typing: false },
+ })
+ );
statusSentTsRef.current = 0;
}
}, TYPING_TIMEOUT_MS);
@@ -31,11 +49,16 @@ export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): Typing
}
if (Date.now() - statusSentTsRef.current < TYPING_TIMEOUT_MS) {
- mx.sendTyping(roomId, false, TYPING_TIMEOUT_MS);
+ ct.requestAuthJSON(
+ putTyping({
+ pathParams: { roomId, userId },
+ body: { typing: false },
+ })
+ );
}
statusSentTsRef.current = 0;
};
- }, [mx, roomId]);
+ }, [ct, roomId, userId]);
return sendTypingStatus;
};
diff --git a/src/app/hooks/useUIAFlows.ts b/src/app/hooks/useUIAFlows.ts
index 0e6c0ddc..012298d4 100644
--- a/src/app/hooks/useUIAFlows.ts
+++ b/src/app/hooks/useUIAFlows.ts
@@ -1,12 +1,15 @@
-import { AuthType, IAuthData, MatrixError, UIAFlow } from 'matrix-js-sdk';
import { useCallback, useMemo } from 'react';
+import { MatrixError } from 'matrix-js-sdk';
import {
+ AuthResponse,
+ AuthType,
getSupportedUIAFlows,
getUIACompleted,
getUIAError,
getUIAErrorCode,
getUIAParams,
getUIASession,
+ UIAFlow,
} from '../utils/matrix-uia';
export const SUPPORTED_FLOW_TYPES = [
@@ -21,19 +24,19 @@ export const SUPPORTED_FLOW_TYPES = [
export const useSupportedUIAFlows = (uiaFlows: UIAFlow[], supportedStages: string[]): UIAFlow[] =>
useMemo(() => getSupportedUIAFlows(uiaFlows, supportedStages), [uiaFlows, supportedStages]);
-export const useUIACompleted = (authData: IAuthData): string[] =>
+export const useUIACompleted = (authData: AuthResponse): string[] =>
useMemo(() => getUIACompleted(authData), [authData]);
-export const useUIAParams = (authData: IAuthData) =>
+export const useUIAParams = (authData: AuthResponse) =>
useMemo(() => getUIAParams(authData), [authData]);
-export const useUIASession = (authData: IAuthData) =>
+export const useUIASession = (authData: AuthResponse) =>
useMemo(() => getUIASession(authData), [authData]);
-export const useUIAErrorCode = (authData: IAuthData) =>
+export const useUIAErrorCode = (authData: AuthResponse) =>
useMemo(() => getUIAErrorCode(authData), [authData]);
-export const useUIAError = (authData: IAuthData) =>
+export const useUIAError = (authData: AuthResponse) =>
useMemo(() => getUIAError(authData), [authData]);
export type StageInfo = Record;
@@ -51,7 +54,7 @@ export type UIAFlowInterface = {
hasStage: (stageType: string) => boolean;
getStageInfo: (stageType: string) => StageInfo | undefined;
};
-export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterface => {
+export const useUIAFlow = (authData: AuthResponse, uiaFlow: UIAFlow): UIAFlowInterface => {
const completed = useUIACompleted(authData);
const params = useUIAParams(authData);
const session = useUIASession(authData);
@@ -97,9 +100,9 @@ export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterf
export const useUIAMatrixError = (
error?: MatrixError
-): [undefined, undefined] | [IAuthData, undefined] | [undefined, MatrixError] => {
+): [undefined, undefined] | [AuthResponse, undefined] | [undefined, MatrixError] => {
if (!error) return [undefined, undefined];
- if (error.httpStatus === 401) return [error.data as IAuthData, undefined];
+ if (error.httpStatus === 401) return [error.data as AuthResponse, undefined];
return [undefined, error];
};
diff --git a/src/app/hooks/useUserProfile.ts b/src/app/hooks/useUserProfile.ts
index c7cb7487..0f8c46dd 100644
--- a/src/app/hooks/useUserProfile.ts
+++ b/src/app/hooks/useUserProfile.ts
@@ -1,51 +1,37 @@
-import { useEffect, useState } from 'react';
-import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
-import { useMatrixClient } from './useMatrixClient';
+import { useCallback } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { getProfile, GetProfileOK } from 'neurone/lib/api/matrix/client/v3/profile';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
+import { useClient } from './useClient';
export type UserProfile = {
avatarUrl?: string;
displayName?: string;
};
-export const useUserProfile = (userId: string): UserProfile => {
- const mx = useMatrixClient();
+export const useUserProfile = (userId: string): UserProfile | undefined => {
+ const ct = useClient();
- const [profile, setProfile] = useState(() => {
- const user = mx.getUser(userId);
- return {
- avatarUrl: user?.avatarUrl,
- displayName: user?.displayName,
- };
- });
-
- useEffect(() => {
- const user = mx.getUser(userId);
- const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => {
- setProfile((cp) => ({
- ...cp,
- avatarUrl: myUser.avatarUrl,
- }));
- };
- const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => {
- setProfile((cp) => ({
- ...cp,
- displayName: myUser.displayName,
- }));
- };
-
- mx.getProfileInfo(userId).then((info) =>
- setProfile({
- avatarUrl: info.avatar_url,
- displayName: info.displayname,
+ const fetchProfile = useCallback(async (): Promise => {
+ const response = await ct.requestAuthJSON(
+ getProfile({
+ pathParams: { userId },
})
);
- user?.on(UserEvent.AvatarUrl, onAvatarChange);
- user?.on(UserEvent.DisplayName, onDisplayNameChange);
- return () => {
- user?.removeListener(UserEvent.AvatarUrl, onAvatarChange);
- user?.removeListener(UserEvent.DisplayName, onDisplayNameChange);
+ const [, profile] = await parseMatrixResp(response);
+ return {
+ avatarUrl: profile?.avatar_url || undefined,
+ displayName: profile?.displayname || undefined,
};
- }, [mx, userId]);
+ }, [ct, userId]);
- return profile;
+ const { data } = useQuery({
+ queryKey: ['profile', userId],
+ queryFn: fetchProfile,
+ staleTime: 0,
+ gcTime: Infinity,
+ refetchOnMount: 'always',
+ });
+
+ return data;
};
diff --git a/src/app/hooks/useVirtualPaginator.ts b/src/app/hooks/useVirtualPaginator.ts
index 5ad056a6..1200e153 100644
--- a/src/app/hooks/useVirtualPaginator.ts
+++ b/src/app/hooks/useVirtualPaginator.ts
@@ -209,13 +209,10 @@ export const useVirtualPaginator = (
let scrollTo = element.offsetTop;
if (opts?.align === 'center' && canFitInScrollView(scrollElement, element)) {
const scrollInfo = getScrollInfo(scrollElement);
- scrollTo =
- element.offsetTop -
- Math.round(scrollInfo.viewHeight / 2) +
- Math.round(element.clientHeight / 2);
+ scrollTo = element.offsetTop - scrollInfo.viewHeight / 2 + element.clientHeight / 2;
} else if (opts?.align === 'end' && canFitInScrollView(scrollElement, element)) {
const scrollInfo = getScrollInfo(scrollElement);
- scrollTo = element.offsetTop - Math.round(scrollInfo.viewHeight) + element.clientHeight;
+ scrollTo = element.offsetTop - scrollInfo.viewHeight + element.clientHeight;
}
scrollElement.scrollTo({
@@ -366,7 +363,7 @@ export const useVirtualPaginator = (
scrollEl.scrollTo({
top: restoreTop,
- behavior: 'instant',
+ behavior: 'auto',
});
restoreScrollRef.current = undefined;
}, [range, getScrollElement, getItemElement]);
@@ -378,7 +375,7 @@ export const useVirtualPaginator = (
const { index, opts } = scrollToItemRef.current;
scrollToItem(index, {
...opts,
- behavior: 'instant',
+ behavior: 'auto',
});
scrollToItemRef.current = undefined;
}, [range, scrollToItem]);
diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx
index 52ec7f20..a6b6437a 100644
--- a/src/app/pages/App.tsx
+++ b/src/app/pages/App.tsx
@@ -7,7 +7,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ClientConfigLoader } from '../components/ClientConfigLoader';
import { ClientConfigProvider } from '../hooks/useClientConfig';
-import { ConfigConfigError, ConfigConfigLoading } from './ConfigConfig';
+import { ClientConfigError, ClientConfigLoading } from './ClientConfig';
import { FeatureCheck } from './FeatureCheck';
import { createRouter } from './Router';
import { ScreenSizeProvider, useScreenSize } from '../hooks/useScreenSize';
@@ -28,9 +28,9 @@ function App() {
}
+ fallback={() => }
error={(err, retry, ignore) => (
-
+
)}
>
{(clientConfig) => (
diff --git a/src/app/pages/ConfigConfig.tsx b/src/app/pages/ClientConfig.tsx
similarity index 72%
rename from src/app/pages/ConfigConfig.tsx
rename to src/app/pages/ClientConfig.tsx
index dbcdca73..d06673ae 100644
--- a/src/app/pages/ConfigConfig.tsx
+++ b/src/app/pages/ClientConfig.tsx
@@ -1,24 +1,17 @@
-import { Box, Button, Dialog, Spinner, Text, color, config } from 'folds';
+import { Box, Button, Dialog, Text, color, config } from 'folds';
import React from 'react';
import { SplashScreen } from '../components/splash-screen';
-export function ConfigConfigLoading() {
- return (
-
-
-
- Heating up
-
-
- );
+export function ClientConfigLoading() {
+ return Loading Configurations} />;
}
-type ConfigConfigErrorProps = {
+type ClientConfigErrorProps = {
error: unknown;
retry: () => void;
ignore: () => void;
};
-export function ConfigConfigError({ error, retry, ignore }: ConfigConfigErrorProps) {
+export function ClientConfigError({ error, retry, ignore }: ClientConfigErrorProps) {
return (
diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx
index 18591f29..346838c0 100644
--- a/src/app/pages/Router.tsx
+++ b/src/app/pages/Router.tsx
@@ -121,27 +121,25 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
-
-
-
-
-
- }
- >
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ }
+ >
+
+
+ {/*
+
+
+
+
+
+
+ */}
@@ -165,7 +163,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
{mobile ? null : } />}
} />
join
} />
- } />
+ {/* } /> */}
}
/>
)}
- } />
- } />
+ {/* } />
+ } /> */}
}
>
-
+ hello
+ {/* */}
}
>
@@ -275,7 +274,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
}
>
-
+ hello
+ {/* */}
}
>
diff --git a/src/app/pages/auth/SSOLogin.tsx b/src/app/pages/auth/SSOLogin.tsx
index 3ff1a229..45c72c14 100644
--- a/src/app/pages/auth/SSOLogin.tsx
+++ b/src/app/pages/auth/SSOLogin.tsx
@@ -1,10 +1,34 @@
import { Avatar, AvatarImage, Box, Button, Text } from 'folds';
-import { IIdentityProvider, SSOAction, createClient } from 'matrix-js-sdk';
-import React, { useMemo } from 'react';
+import { IdentityProvider } from 'neurone/lib/api/types';
+import React from 'react';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
+import { mxcToHttp } from '../../../client/mxcToHttp';
+
+const UNSTABLE_ACTION_PARAM = 'org.matrix.msc3824.action';
+type SSOAction = 'login' | 'register';
+
+function getSsoLoginUrl(
+ baseUrl: string,
+ redirectUrl: string,
+ idpId?: string,
+ action?: SSOAction
+): string {
+ let path = '_matrix/client/v3/login/sso/redirect';
+ if (idpId) {
+ path += `/${idpId}`;
+ }
+ const url = new URL(path, baseUrl);
+
+ url.searchParams.append('redirectUrl', redirectUrl);
+ if (action) {
+ url.searchParams.append(UNSTABLE_ACTION_PARAM, action);
+ }
+
+ return url.href;
+}
type SSOLoginProps = {
- providers?: IIdentityProvider[];
+ providers?: IdentityProvider[];
redirectUrl: string;
action?: SSOAction;
saveScreenSpace?: boolean;
@@ -12,14 +36,15 @@ type SSOLoginProps = {
export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SSOLoginProps) {
const discovery = useAutoDiscoveryInfo();
const baseUrl = discovery['m.homeserver'].base_url;
- const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
const getSSOIdUrl = (ssoId?: string): string =>
- mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId, action);
+ getSsoLoginUrl(baseUrl, redirectUrl, ssoId, action);
const withoutIcon = providers
? providers.find(
- (provider) => !provider.icon || !mx.mxcUrlToHttp(provider.icon, 96, 96, 'crop', false)
+ ({ icon }) =>
+ !icon ||
+ !mxcToHttp(baseUrl, icon, false).thumbnail({ width: 96, height: 96, method: 'crop' })
)
: true;
@@ -30,7 +55,13 @@ export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SS
{providers ? (
providers.map((provider) => {
const { id, name, icon } = provider;
- const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false);
+ const iconUrl =
+ icon &&
+ mxcToHttp(baseUrl, icon, false).thumbnail({
+ width: 96,
+ height: 96,
+ method: 'crop',
+ });
const buttonTitle = `Continue with ${name}`;
@@ -45,7 +76,7 @@ export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SS
size="300"
radii="300"
>
-
+
);
}
diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx
index 2f04a733..e6fad6a0 100644
--- a/src/app/pages/auth/login/Login.tsx
+++ b/src/app/pages/auth/login/Login.tsx
@@ -1,7 +1,6 @@
import React, { useMemo } from 'react';
import { Box, Text, color } from 'folds';
import { Link, useSearchParams } from 'react-router-dom';
-import { SSOAction } from 'matrix-js-sdk';
import { useAuthFlows } from '../../../hooks/useAuthFlows';
import { useAuthServer } from '../../../hooks/useAuthServer';
import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
@@ -52,7 +51,7 @@ export function Login() {
);
}
- const parsedFlows = useParsedLoginFlows(loginFlows.flows);
+ const parsedFlows = useParsedLoginFlows(loginFlows.flows ?? []);
return (
@@ -77,7 +76,7 @@ export function Login() {
diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx
index 62f46dd2..89204a89 100644
--- a/src/app/pages/auth/login/PasswordLoginForm.tsx
+++ b/src/app/pages/auth/login/PasswordLoginForm.tsx
@@ -19,24 +19,18 @@ import {
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { Link } from 'react-router-dom';
-import { MatrixError } from 'matrix-js-sdk';
import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix';
import { EMAIL_REGEX } from '../../../utils/regex';
import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useAuthServer } from '../../../hooks/useAuthServer';
import { useClientConfig } from '../../../hooks/useClientConfig';
-import {
- CustomLoginResponse,
- LoginError,
- factoryGetBaseUrl,
- login,
- useLoginComplete,
-} from './loginUtil';
+import { LoginError, factoryGetBaseUrl, login, useLoginComplete } from './loginUtil';
import { PasswordInput } from '../../../components/password-input';
import { FieldError } from '../FiledError';
import { getResetPasswordPath } from '../../pathUtils';
import { stopPropagation } from '../../../utils/keyboard';
+import { getInitialDeviceDisplayName } from '../../../utils/user-agent';
function UsernameHint({ server }: { server: string }) {
const [anchor, setAnchor] = useState();
@@ -117,13 +111,11 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
const serverDiscovery = useAutoDiscoveryInfo();
const baseUrl = serverDiscovery['m.homeserver'].base_url;
- const [loginState, startLogin] = useAsyncCallback<
- CustomLoginResponse,
- MatrixError,
- Parameters
- >(useCallback(login, []));
+ const [loginState, startLogin] = useAsyncCallback(useCallback(login, []));
+ const [loginError, customLoginResponse] =
+ loginState.status === AsyncStatus.Success ? loginState.data : [];
- useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined);
+ useLoginComplete(customLoginResponse);
const handleUsernameLogin = (username: string, password: string) => {
startLogin(baseUrl, {
@@ -133,7 +125,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
user: username,
},
password,
- initial_device_display_name: 'Cinny Web',
+ initial_device_display_name: getInitialDeviceDisplayName(),
});
};
@@ -151,7 +143,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
user: mxIdUsername,
},
password,
- initial_device_display_name: 'Cinny Web',
+ initial_device_display_name: getInitialDeviceDisplayName(),
});
};
const handleEmailLogin = (email: string, password: string) => {
@@ -163,7 +155,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
address: email,
},
password,
- initial_device_display_name: 'Cinny Web',
+ initial_device_display_name: getInitialDeviceDisplayName(),
});
};
@@ -212,12 +204,12 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
outlined
after={}
/>
- {loginState.status === AsyncStatus.Error && (
+ {loginError && (
<>
- {loginState.error.errcode === LoginError.ServerNotAllowed && (
+ {loginError === LoginError.ServerNotAllowed && (
)}
- {loginState.error.errcode === LoginError.InvalidServer && (
+ {loginError === LoginError.InvalidServer && (
)}
>
@@ -229,25 +221,34 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
- {loginState.status === AsyncStatus.Error && (
+ {loginError && (
<>
- {loginState.error.errcode === LoginError.Forbidden && (
+ {loginError === LoginError.Forbidden && (
)}
- {loginState.error.errcode === LoginError.UserDeactivated && (
+ {loginError === LoginError.UserDeactivated && (
)}
- {loginState.error.errcode === LoginError.InvalidRequest && (
+ {loginError === LoginError.InvalidRequest && (
)}
- {loginState.error.errcode === LoginError.RateLimited && (
+ {loginError === LoginError.RateLimited && (
)}
- {loginState.error.errcode === LoginError.Unknown && (
+ {loginError === LoginError.Unknown && (
)}
>
)}
+ {loginState.status === AsyncStatus.Error && (
+
+ )}
Forget Password?
@@ -262,9 +263,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
}
>
diff --git a/src/app/pages/auth/login/TokenLogin.tsx b/src/app/pages/auth/login/TokenLogin.tsx
index 761d5dc5..33f73e62 100644
--- a/src/app/pages/auth/login/TokenLogin.tsx
+++ b/src/app/pages/auth/login/TokenLogin.tsx
@@ -11,10 +11,10 @@ import {
config,
} from 'folds';
import React, { useCallback, useEffect } from 'react';
-import { MatrixError } from 'matrix-js-sdk';
import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
-import { CustomLoginResponse, LoginError, login, useLoginComplete } from './loginUtil';
+import { LoginError, login, useLoginComplete } from './loginUtil';
+import { getInitialDeviceDisplayName } from '../../../utils/user-agent';
function LoginTokenError({ message }: { message: string }) {
return (
@@ -47,44 +47,55 @@ export function TokenLogin({ token }: TokenLoginProps) {
const discovery = useAutoDiscoveryInfo();
const baseUrl = discovery['m.homeserver'].base_url;
- const [loginState, startLogin] = useAsyncCallback<
- CustomLoginResponse,
- MatrixError,
- Parameters
- >(useCallback(login, []));
+ const [loginState, startLogin] = useAsyncCallback(useCallback(login, []));
useEffect(() => {
startLogin(baseUrl, {
type: 'm.login.token',
token,
- initial_device_display_name: 'Cinny Web',
+ initial_device_display_name: getInitialDeviceDisplayName(),
});
}, [baseUrl, token, startLogin]);
- useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined);
+ const [loginError, customLoginResponse] =
+ loginState.status === AsyncStatus.Success ? loginState.data : [];
+
+ useLoginComplete(customLoginResponse);
return (
<>
- {loginState.status === AsyncStatus.Error && (
+ {loginError && (
<>
- {loginState.error.errcode === LoginError.Forbidden && (
+ {loginError === LoginError.Forbidden && (
)}
- {loginState.error.errcode === LoginError.UserDeactivated && (
+ {loginError === LoginError.UserDeactivated && (
)}
- {loginState.error.errcode === LoginError.InvalidRequest && (
+ {loginError === LoginError.InvalidRequest && (
)}
- {loginState.error.errcode === LoginError.RateLimited && (
+ {loginError === LoginError.RateLimited && (
)}
- {loginState.error.errcode === LoginError.Unknown && (
+ {loginError === LoginError.Unknown && (
)}
>
)}
- }>
+ {loginState.status === AsyncStatus.Error && (
+
+ )}
+ }
+ >
diff --git a/src/app/pages/auth/login/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts
index ba5b0bc9..0922572a 100644
--- a/src/app/pages/auth/login/loginUtil.ts
+++ b/src/app/pages/auth/login/loginUtil.ts
@@ -1,7 +1,8 @@
import to from 'await-to-js';
-import { LoginRequest, LoginResponse, MatrixError, createClient } from 'matrix-js-sdk';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
+import { postLogin, PostLoginBody, PostLoginOK } from 'neurone/lib/api/matrix/client/v3/login';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { ClientConfig, clientAllowedServer } from '../../../hooks/useClientConfig';
import { autoDiscovery, specVersions } from '../../../cs-api';
import { ErrorCode } from '../../../cs-errorcode';
@@ -11,6 +12,7 @@ import {
} from '../../afterLoginRedirectPath';
import { getHomePath } from '../../pathUtils';
import { setFallbackSession } from '../../../state/sessions';
+import { getJSONRequest } from '../../../../client/request';
export enum GetBaseUrlError {
NotAllow = 'NotAllow',
@@ -55,57 +57,53 @@ export enum LoginError {
export type CustomLoginResponse = {
baseUrl: string;
- response: LoginResponse;
+ response: PostLoginOK;
};
export const login = async (
serverBaseUrl: string | (() => Promise),
- data: LoginRequest
-): Promise => {
+ body: PostLoginBody
+): Promise<[LoginError, undefined] | [undefined, CustomLoginResponse]> => {
const [urlError, url] =
typeof serverBaseUrl === 'function' ? await to(serverBaseUrl()) : [undefined, serverBaseUrl];
+
if (urlError) {
- throw new MatrixError({
- errcode:
- urlError.message === GetBaseUrlError.NotAllow
- ? LoginError.ServerNotAllowed
- : LoginError.InvalidServer,
- });
+ return [
+ urlError.message === GetBaseUrlError.NotAllow
+ ? LoginError.ServerNotAllowed
+ : LoginError.InvalidServer,
+ undefined,
+ ];
}
- const mx = createClient({ baseUrl: url });
- const [err, res] = await to(mx.loginRequest(data));
+ const requestJSON = getJSONRequest(url);
+ const postLoginEndpoint = postLogin({ body });
+ const response = await requestJSON(postLoginEndpoint);
+ const [loginError, loginResult] = await parseMatrixResp(response);
- if (err) {
- if (err.httpStatus === 400) {
- throw new MatrixError({
- errcode: LoginError.InvalidRequest,
- });
+ if (loginError) {
+ if (loginError.status === 400) {
+ return [LoginError.InvalidRequest, undefined];
}
- if (err.httpStatus === 429) {
- throw new MatrixError({
- errcode: LoginError.RateLimited,
- });
+ if (loginError.status === 429) {
+ return [LoginError.RateLimited, undefined];
}
- if (err.errcode === ErrorCode.M_USER_DEACTIVATED) {
- throw new MatrixError({
- errcode: LoginError.UserDeactivated,
- });
+ if (loginError.data.errcode === ErrorCode.M_USER_DEACTIVATED) {
+ return [LoginError.UserDeactivated, undefined];
}
- if (err.httpStatus === 403) {
- throw new MatrixError({
- errcode: LoginError.Forbidden,
- });
+ if (loginError.status === 403) {
+ return [LoginError.Forbidden, undefined];
}
- throw new MatrixError({
- errcode: LoginError.Unknown,
- });
+ return [LoginError.Unknown, undefined];
}
- return {
- baseUrl: url,
- response: res,
- };
+ return [
+ undefined,
+ {
+ baseUrl: url,
+ response: loginResult,
+ },
+ ];
};
export const useLoginComplete = (data?: CustomLoginResponse) => {
diff --git a/src/app/pages/auth/register/PasswordRegisterForm.tsx b/src/app/pages/auth/register/PasswordRegisterForm.tsx
index 9f173422..30667963 100644
--- a/src/app/pages/auth/register/PasswordRegisterForm.tsx
+++ b/src/app/pages/auth/register/PasswordRegisterForm.tsx
@@ -10,22 +10,17 @@ import {
Text,
color,
} from 'folds';
-import React, { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
-import {
- AuthDict,
- AuthType,
- IAuthData,
- MatrixError,
- RegisterRequest,
- UIAFlow,
- createClient,
-} from 'matrix-js-sdk';
+import React, { ChangeEventHandler, useCallback, useState } from 'react';
+import { PostRegisterBody } from 'neurone/lib/api/matrix/client/v3/register';
import { PasswordInput } from '../../../components/password-input';
import {
+ AuthResponse,
+ AuthType,
getLoginTermUrl,
getUIAFlowForStages,
hasStageInFlows,
requiredStageInFlows,
+ UIAFlow,
} from '../../../utils/matrix-uia';
import { useUIACompleted, useUIAFlow, useUIAParams } from '../../../hooks/useUIAFlows';
import { AsyncState, AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
@@ -42,7 +37,9 @@ import {
import { useRegisterEmail } from '../../../hooks/useRegisterEmail';
import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
import { UIAFlowOverlay } from '../../../components/UIAFlowOverlay';
-import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../../hooks/types';
+import { RequestEmailTokenCallback, RequestEmailTokenResult } from '../../../hooks/types';
+import { generateClientSecret } from '../../../utils/randomString';
+import { getInitialDeviceDisplayName } from '../../../utils/user-agent';
export const SUPPORTED_REGISTER_STAGES = [
AuthType.RegistrationToken,
@@ -84,10 +81,10 @@ const pickStages = (uiaFlows: UIAFlow[], formData: FormData): string[] => {
type RegisterUIAFlowProps = {
formData: FormData;
flow: UIAFlow;
- authData: IAuthData;
- registerEmailState: AsyncState;
+ authData: AuthResponse;
+ registerEmailState: AsyncState;
registerEmail: RequestEmailTokenCallback;
- onRegister: (registerReqData: RegisterRequest) => void;
+ onRegister: (registerReqData: PostRegisterBody) => void;
};
function RegisterUIAFlow({
formData,
@@ -103,13 +100,13 @@ function RegisterUIAFlow({
const stageToComplete = getStageToComplete();
const handleAuthDict = useCallback(
- (authDict: AuthDict) => {
+ (authDict: object) => {
const { password, username } = formData;
onRegister({
auth: authDict,
password,
username,
- initial_device_display_name: 'Cinny Web',
+ initial_device_display_name: getInitialDeviceDisplayName(),
});
},
[onRegister, formData]
@@ -171,7 +168,7 @@ function RegisterUIAFlow({
}
type PasswordRegisterFormProps = {
- authData: IAuthData;
+ authData: AuthResponse;
uiaFlows: UIAFlow[];
defaultUsername?: string;
defaultEmail?: string;
@@ -186,23 +183,24 @@ export function PasswordRegisterForm({
}: PasswordRegisterFormProps) {
const serverDiscovery = useAutoDiscoveryInfo();
const baseUrl = serverDiscovery['m.homeserver'].base_url;
- const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
+
const params = useUIAParams(authData);
const termUrl = getLoginTermUrl(params);
const [formData, setFormData] = useState();
const [ongoingFlow, setOngoingFlow] = useState();
- const [registerEmailState, registerEmail] = useRegisterEmail(mx);
+ const [registerEmailState, registerEmail] = useRegisterEmail(baseUrl);
const [registerState, handleRegister] = useAsyncCallback<
RegisterResult,
- MatrixError,
- [RegisterRequest]
- >(useCallback(async (registerReqData) => register(mx, registerReqData), [mx]));
- const [ongoingAuthData, customRegisterResp] =
+ Error,
+ [PostRegisterBody]
+ >(useCallback(async (registerReqData) => register(baseUrl, registerReqData), [baseUrl]));
+ const [ongoingAuthData, registerError, customRegisterResp] =
registerState.status === AsyncStatus.Success ? registerState.data : [];
- const registerError =
+
+ const registerRequestError =
registerState.status === AsyncStatus.Error ? registerState.error : undefined;
useRegisterComplete(customRegisterResp);
@@ -238,8 +236,9 @@ export function PasswordRegisterForm({
token,
email,
terms,
- clientSecret: mx.generateClientSecret(),
+ clientSecret: generateClientSecret(),
};
+
const pickedStages = pickStages(uiaFlows, fData);
const pickedFlow = getUIAFlowForStages(uiaFlows, pickedStages);
setOngoingFlow(pickedFlow);
@@ -250,7 +249,7 @@ export function PasswordRegisterForm({
auth: {
session: authData.session,
},
- initial_device_display_name: 'Cinny Web',
+ initial_device_display_name: getInitialDeviceDisplayName(),
});
};
@@ -269,13 +268,13 @@ export function PasswordRegisterForm({
outlined
required
/>
- {registerError?.errcode === RegisterError.UserTaken && (
+ {registerError?.error === RegisterError.UserTaken && (
)}
- {registerError?.errcode === RegisterError.UserInvalid && (
+ {registerError?.error === RegisterError.UserInvalid && (
)}
- {registerError?.errcode === RegisterError.UserExclusive && (
+ {registerError?.error === RegisterError.UserExclusive && (
)}
@@ -295,18 +294,18 @@ export function PasswordRegisterForm({
outlined
required
/>
- {registerError?.errcode === RegisterError.PasswordWeak && (
+ {registerError?.error === RegisterError.PasswordWeak && (
)}
- {registerError?.errcode === RegisterError.PasswordShort && (
+ {registerError?.error === RegisterError.PasswordShort && (
@@ -376,17 +375,22 @@ export function PasswordRegisterForm({
)}
- {registerError?.errcode === RegisterError.RateLimited && (
+ {registerError?.error === RegisterError.RateLimited && (
)}
- {registerError?.errcode === RegisterError.Forbidden && (
+ {registerError?.error === RegisterError.Forbidden && (
)}
- {registerError?.errcode === RegisterError.InvalidRequest && (
+ {registerError?.error === RegisterError.InvalidRequest && (
)}
- {registerError?.errcode === RegisterError.Unknown && (
-
+ {registerError?.error === RegisterError.Unknown && (
+
+ )}
+ {registerRequestError && (
+
)}
diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx
index 7176489b..f937dca0 100644
--- a/src/app/pages/auth/register/Register.tsx
+++ b/src/app/pages/auth/register/Register.tsx
@@ -1,7 +1,6 @@
import React, { useMemo } from 'react';
import { Box, Text, color } from 'folds';
import { Link, useSearchParams } from 'react-router-dom';
-import { SSOAction } from 'matrix-js-sdk';
import { useAuthServer } from '../../../hooks/useAuthServer';
import { RegisterFlowStatus, useAuthFlows } from '../../../hooks/useAuthFlows';
import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
@@ -28,7 +27,7 @@ export function Register() {
const { loginFlows, registerFlows } = useAuthFlows();
const [searchParams] = useSearchParams();
const registerSearchParams = useRegisterSearchParams(searchParams);
- const { sso } = useParsedLoginFlows(loginFlows.flows);
+ const { sso } = useParsedLoginFlows(loginFlows.flows ?? []);
// redirect to /login because only that path handle m.login.token
const ssoRedirectUrl = usePathWithOrigin(getLoginPath(server));
@@ -84,7 +83,7 @@ export function Register() {
diff --git a/src/app/pages/auth/register/registerUtil.ts b/src/app/pages/auth/register/registerUtil.ts
index 86a38cb5..24e7a8c0 100644
--- a/src/app/pages/auth/register/registerUtil.ts
+++ b/src/app/pages/auth/register/registerUtil.ts
@@ -1,11 +1,9 @@
-import to from 'await-to-js';
import {
- IAuthData,
- MatrixClient,
- MatrixError,
- RegisterRequest,
- RegisterResponse,
-} from 'matrix-js-sdk';
+ postRegister,
+ PostRegisterBody,
+ PostRegisterOK,
+} from 'neurone/lib/api/matrix/client/v3/register';
+import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { LoginPathSearchParams } from '../../paths';
@@ -17,6 +15,8 @@ import {
import { getHomePath, getLoginPath, withSearchParam } from '../../pathUtils';
import { getMxIdLocalPart, getMxIdServer } from '../../../utils/matrix';
import { setFallbackSession } from '../../../state/sessions';
+import { AuthResponse } from '../../../utils/matrix-uia';
+import { getJSONRequest } from '../../../../client/request';
export enum RegisterError {
UserTaken = 'UserTaken',
@@ -32,77 +32,106 @@ export enum RegisterError {
export type CustomRegisterResponse = {
baseUrl: string;
- response: RegisterResponse;
+ response: PostRegisterOK;
};
-export type RegisterResult = [IAuthData, undefined] | [undefined, CustomRegisterResponse];
+
+export type RegisterErrorData = {
+ error: RegisterError;
+ message?: string;
+};
+
+export type RegisterResult =
+ | [AuthResponse, undefined, undefined]
+ | [undefined, RegisterErrorData, undefined]
+ | [undefined, undefined, CustomRegisterResponse];
+
export const register = async (
- mx: MatrixClient,
- requestData: RegisterRequest
+ baseUrl: string,
+ requestData: PostRegisterBody
): Promise => {
- const [err, res] = await to(mx.registerRequest(requestData));
+ const requestJSON = getJSONRequest(baseUrl);
+ const response = await requestJSON(postRegister({ body: requestData }));
+ const [registerError, registerResult] = await parseMatrixResp(response);
- if (err) {
- if (err.httpStatus === 401) {
- const authData = err.data as IAuthData;
- return [authData, undefined];
+ if (registerError) {
+ if (registerError.status === 401) {
+ const authData = registerError.data as AuthResponse;
+ return [authData, undefined, undefined];
}
- if (err.errcode === ErrorCode.M_USER_IN_USE) {
- throw new MatrixError({
- errcode: RegisterError.UserTaken,
- });
+ if (registerError.data.errcode === ErrorCode.M_USER_IN_USE) {
+ return [
+ undefined,
+ { error: RegisterError.UserTaken, message: registerError.data.error },
+ undefined,
+ ];
}
- if (err.errcode === ErrorCode.M_INVALID_USERNAME) {
- throw new MatrixError({
- errcode: RegisterError.UserInvalid,
- });
+ if (registerError.data.errcode === ErrorCode.M_INVALID_USERNAME) {
+ return [
+ undefined,
+ { error: RegisterError.UserInvalid, message: registerError.data.error },
+ undefined,
+ ];
}
- if (err.errcode === ErrorCode.M_EXCLUSIVE) {
- throw new MatrixError({
- errcode: RegisterError.UserExclusive,
- });
+ if (registerError.data.errcode === ErrorCode.M_EXCLUSIVE) {
+ return [
+ undefined,
+ { error: RegisterError.UserExclusive, message: registerError.data.error },
+ undefined,
+ ];
}
- if (err.errcode === ErrorCode.M_WEAK_PASSWORD) {
- throw new MatrixError({
- errcode: RegisterError.PasswordWeak,
- error: err.data.error,
- });
+ if (registerError.data.errcode === ErrorCode.M_WEAK_PASSWORD) {
+ return [
+ undefined,
+ { error: RegisterError.PasswordWeak, message: registerError.data.error },
+ undefined,
+ ];
}
- if (err.errcode === ErrorCode.M_PASSWORD_TOO_SHORT) {
- throw new MatrixError({
- errcode: RegisterError.PasswordShort,
- error: err.data.error,
- });
+ if (registerError.data.errcode === ErrorCode.M_PASSWORD_TOO_SHORT) {
+ return [
+ undefined,
+ { error: RegisterError.PasswordShort, message: registerError.data.error },
+ undefined,
+ ];
}
- if (err.httpStatus === 429) {
- throw new MatrixError({
- errcode: RegisterError.RateLimited,
- });
+ if (registerError.status === 429) {
+ return [
+ undefined,
+ { error: RegisterError.RateLimited, message: registerError.data.error },
+ undefined,
+ ];
}
- if (err.httpStatus === 400) {
- throw new MatrixError({
- errcode: RegisterError.InvalidRequest,
- });
+ if (registerError.status === 400) {
+ return [
+ undefined,
+ { error: RegisterError.InvalidRequest, message: registerError.data.error },
+ undefined,
+ ];
}
- if (err.httpStatus === 403) {
- throw new MatrixError({
- errcode: RegisterError.Forbidden,
- });
+ if (registerError.status === 403) {
+ return [
+ undefined,
+ { error: RegisterError.Forbidden, message: registerError.data.error },
+ undefined,
+ ];
}
- throw new MatrixError({
- errcode: RegisterError.Unknown,
- error: err.data.error,
- });
+ return [
+ undefined,
+ { error: RegisterError.Unknown, message: registerError.data.error },
+ undefined,
+ ];
}
+
return [
+ undefined,
undefined,
{
- baseUrl: mx.baseUrl,
- response: res,
+ baseUrl,
+ response: registerResult,
},
];
};
diff --git a/src/app/pages/auth/reset-password/PasswordResetForm.tsx b/src/app/pages/auth/reset-password/PasswordResetForm.tsx
index 392f45c0..b06dcf13 100644
--- a/src/app/pages/auth/reset-password/PasswordResetForm.tsx
+++ b/src/app/pages/auth/reset-password/PasswordResetForm.tsx
@@ -1,4 +1,4 @@
-import React, { FormEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
+import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
import {
Box,
Button,
@@ -12,9 +12,10 @@ import {
color,
config,
} from 'folds';
+import { printMatrixError } from 'neurone/lib/matrix-request';
+import { PostPasswordBody } from 'neurone/lib/api/matrix/client/v3/account';
import { useNavigate } from 'react-router-dom';
import FocusTrap from 'focus-trap-react';
-import { AuthDict, AuthType, MatrixError, createClient } from 'matrix-js-sdk';
import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useAuthServer } from '../../../hooks/useAuthServer';
@@ -27,7 +28,8 @@ import { EmailStageDialog } from '../../../components/uia-stages';
import { ResetPasswordResult, resetPassword } from './resetPasswordUtil';
import { getLoginPath, withSearchParam } from '../../pathUtils';
import { LoginPathSearchParams } from '../../paths';
-import { getUIAError, getUIAErrorCode } from '../../../utils/matrix-uia';
+import { AuthType, getUIAError, getUIAErrorCode } from '../../../utils/matrix-uia';
+import { generateClientSecret } from '../../../utils/randomString';
type FormData = {
email: string;
@@ -79,21 +81,24 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
const serverDiscovery = useAutoDiscoveryInfo();
const baseUrl = serverDiscovery['m.homeserver'].base_url;
- const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
const [formData, setFormData] = useState();
- const [passwordEmailState, passwordEmail] = usePasswordEmail(mx);
+ const [passwordEmailState, passwordEmail] = usePasswordEmail(baseUrl);
+ const [passwordEmailError] =
+ passwordEmailState.status === AsyncStatus.Success ? passwordEmailState.data : [];
+ const passwordEmailRequestError =
+ passwordEmailState.status === AsyncStatus.Error ? passwordEmailState.error : undefined;
const [resetPasswordState, handleResetPassword] = useAsyncCallback<
ResetPasswordResult,
- MatrixError,
- [AuthDict, string]
- >(useCallback(async (authDict, newPassword) => resetPassword(mx, authDict, newPassword), [mx]));
+ Error,
+ [PostPasswordBody]
+ >(useCallback(async (requestData) => resetPassword(baseUrl, requestData), [baseUrl]));
- const [ongoingAuthData, resetPasswordResult] =
+ const [ongoingAuthData, resetPasswordError, resetPasswordResult] =
resetPasswordState.status === AsyncStatus.Success ? resetPasswordState.data : [];
- const resetPasswordError =
+ const resetPasswordRequestError =
resetPasswordState.status === AsyncStatus.Error ? resetPasswordState.error : undefined;
const flowErrorCode = ongoingAuthData && getUIAErrorCode(ongoingAuthData);
@@ -102,7 +107,7 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
let waitingToVerifyEmail = true;
if (resetPasswordResult) waitingToVerifyEmail = false;
if (ongoingAuthData && flowErrorCode === undefined) waitingToVerifyEmail = false;
- if (resetPasswordError) waitingToVerifyEmail = false;
+ if (resetPasswordError || resetPasswordRequestError) waitingToVerifyEmail = false;
if (resetPasswordState.status === AsyncStatus.Loading) waitingToVerifyEmail = false;
// We only support UIA m.login.password stage for reset password
@@ -110,18 +115,18 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
// we have 401 with no error on initial request.
useEffect(() => {
if (formData && ongoingAuthData && !flowErrorCode) {
- handleResetPassword(
- {
+ handleResetPassword({
+ auth: {
type: AuthType.Password,
identifier: {
type: 'm.id.thirdparty',
medium: 'email',
address: formData.email,
},
- password: formData.password,
},
- formData.password
- );
+ new_password: formData.password,
+ logout_devices: false,
+ });
}
}, [ongoingAuthData, flowErrorCode, formData, handleResetPassword]);
@@ -142,7 +147,7 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
}
if (password !== confirmPassword) return;
- const clientSecret = mx.generateClientSecret();
+ const clientSecret = generateClientSecret();
passwordEmail(email, clientSecret);
setFormData({
email,
@@ -156,10 +161,14 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
};
const handleSubmitRequest = useCallback(
- (authDict: AuthDict) => {
+ (authDict: object) => {
if (!formData) return;
const { password } = formData;
- handleResetPassword(authDict, password);
+ handleResetPassword({
+ auth: authDict,
+ new_password: password,
+ logout_devices: false,
+ });
},
[formData, handleResetPassword]
);
@@ -182,11 +191,8 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
required
outlined
/>
- {passwordEmailState.status === AsyncStatus.Error && (
-
- )}
+ {passwordEmailRequestError && }
+ {passwordEmailError && }
{(match, doMatch, passRef, confPassRef) => (
@@ -223,12 +229,9 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
>
)}
- {resetPasswordError && (
-
+ {resetPasswordError && }
+ {resetPasswordRequestError && (
+
)}
diff --git a/src/app/pages/auth/reset-password/resetPasswordUtil.ts b/src/app/pages/auth/reset-password/resetPasswordUtil.ts
index 5eb436fa..2d8b0467 100644
--- a/src/app/pages/auth/reset-password/resetPasswordUtil.ts
+++ b/src/app/pages/auth/reset-password/resetPasswordUtil.ts
@@ -1,23 +1,31 @@
-import to from 'await-to-js';
-import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk';
+import { MatrixError, parseMatrixResp } from 'neurone/lib/matrix-request';
+import {
+ postPassword,
+ PostPasswordBody,
+ PostPasswordOK,
+} from 'neurone/lib/api/matrix/client/v3/account';
+import { AuthResponse } from '../../../utils/matrix-uia';
+import { getJSONRequest } from '../../../../client/request';
export type ResetPasswordResponse = Record;
-export type ResetPasswordResult = [IAuthData, undefined] | [undefined, ResetPasswordResponse];
+export type ResetPasswordResult =
+ | [AuthResponse, undefined, undefined]
+ | [undefined, MatrixError, undefined]
+ | [undefined, undefined, ResetPasswordResponse];
export const resetPassword = async (
- mx: MatrixClient,
- authDict: AuthDict,
- newPassword: string
+ baseUrl: string,
+ requestData: PostPasswordBody
): Promise => {
- const [err, res] = await to(
- mx.setPassword(authDict, newPassword, false)
- );
+ const requestJSON = getJSONRequest(baseUrl);
+ const response = await requestJSON(postPassword({ body: requestData }));
+ const [passwordError, passwordResult] = await parseMatrixResp(response);
- if (err) {
- if (err.httpStatus === 401) {
- const authData = err.data as IAuthData;
- return [authData, undefined];
+ if (passwordError) {
+ if (passwordError.status === 401) {
+ const authData = passwordError.data as AuthResponse;
+ return [authData, undefined, undefined];
}
- throw err;
+ return [undefined, passwordError, undefined];
}
- return [undefined, res];
+ return [undefined, undefined, passwordResult];
};
diff --git a/src/app/pages/client/ClientBindAtoms.ts b/src/app/pages/client/ClientBindAtoms.ts
index 5d57321e..4abfdbc4 100644
--- a/src/app/pages/client/ClientBindAtoms.ts
+++ b/src/app/pages/client/ClientBindAtoms.ts
@@ -1,14 +1,9 @@
-import { ReactNode } from 'react';
-
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useBindAtoms } from '../../state/hooks/useBindAtoms';
+import { useClient } from '../../hooks/useClient';
-type ClientBindAtomsProps = {
- children: ReactNode;
-};
-export function ClientBindAtoms({ children }: ClientBindAtomsProps) {
- const mx = useMatrixClient();
- useBindAtoms(mx);
+export function ClientBindAtoms() {
+ const ct = useClient();
+ useBindAtoms(ct);
- return children;
+ return null;
}
diff --git a/src/app/pages/client/ClientInitStorageAtom.tsx b/src/app/pages/client/ClientInitStorageAtom.tsx
index 1abee707..aeabb3e9 100644
--- a/src/app/pages/client/ClientInitStorageAtom.tsx
+++ b/src/app/pages/client/ClientInitStorageAtom.tsx
@@ -1,5 +1,4 @@
import React, { ReactNode, useMemo } from 'react';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
import { makeClosedNavCategoriesAtom } from '../../state/closedNavCategories';
import { ClosedNavCategoriesProvider } from '../../state/hooks/closedNavCategories';
import { makeClosedLobbyCategoriesAtom } from '../../state/closedLobbyCategories';
@@ -8,13 +7,14 @@ import { makeNavToActivePathAtom } from '../../state/navToActivePath';
import { NavToActivePathProvider } from '../../state/hooks/navToActivePath';
import { makeOpenedSidebarFolderAtom } from '../../state/openedSidebarFolder';
import { OpenedSidebarFolderProvider } from '../../state/hooks/openedSidebarFolder';
+import { useClient } from '../../hooks/useClient';
type ClientInitStorageAtomProps = {
children: ReactNode;
};
export function ClientInitStorageAtom({ children }: ClientInitStorageAtomProps) {
- const mx = useMatrixClient();
- const userId = mx.getUserId()!;
+ const ct = useClient();
+ const { userId } = ct.session;
const closedNavCategoriesAtom = useMemo(() => makeClosedNavCategoriesAtom(userId), [userId]);
diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx
index ce952bfc..0df6f9f0 100644
--- a/src/app/pages/client/ClientNonUIFeatures.tsx
+++ b/src/app/pages/client/ClientNonUIFeatures.tsx
@@ -1,31 +1,6 @@
-import { useAtomValue } from 'jotai';
-import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
-import { roomToUnreadAtom, unreadEqual, unreadInfoToUnread } from '../../state/room/roomToUnread';
-import LogoSVG from '../../../../public/res/svg/cinny.svg';
-import LogoUnreadSVG from '../../../../public/res/svg/cinny-unread.svg';
-import LogoHighlightSVG from '../../../../public/res/svg/cinny-highlight.svg';
-import NotificationSound from '../../../../public/sound/notification.ogg';
-import InviteSound from '../../../../public/sound/invite.ogg';
-import { notificationPermission, setFavicon } from '../../utils/dom';
+import React from 'react';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
-import { allInvitesAtom } from '../../state/room-list/inviteList';
-import { usePreviousValue } from '../../hooks/usePreviousValue';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { getInboxInvitesPath, getInboxNotificationsPath } from '../pathUtils';
-import {
- getMemberDisplayName,
- getNotificationType,
- getUnreadInfo,
- isNotificationEvent,
-} from '../../utils/room';
-import { NotificationType, UnreadInfo } from '../../../types/matrix/room';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
-import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
-import { useInboxNotificationsSelected } from '../../hooks/router/useInbox';
-import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
function SystemEmojiFeature() {
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
@@ -51,221 +26,11 @@ function PageZoomFeature() {
return null;
}
-function FaviconUpdater() {
- const roomToUnread = useAtomValue(roomToUnreadAtom);
-
- useEffect(() => {
- let notification = false;
- let highlight = false;
- roomToUnread.forEach((unread) => {
- if (unread.total > 0) {
- notification = true;
- }
- if (unread.highlight > 0) {
- highlight = true;
- }
- });
-
- if (notification) {
- setFavicon(highlight ? LogoHighlightSVG : LogoUnreadSVG);
- } else {
- setFavicon(LogoSVG);
- }
- }, [roomToUnread]);
-
- return null;
-}
-
-function InviteNotifications() {
- const audioRef = useRef(null);
- const invites = useAtomValue(allInvitesAtom);
- const perviousInviteLen = usePreviousValue(invites.length, 0);
- const mx = useMatrixClient();
-
- const navigate = useNavigate();
- const [showNotifications] = useSetting(settingsAtom, 'showNotifications');
- const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds');
-
- const notify = useCallback(
- (count: number) => {
- const noti = new window.Notification('Invitation', {
- icon: LogoSVG,
- badge: LogoSVG,
- body: `You have ${count} new invitation request.`,
- silent: true,
- });
-
- noti.onclick = () => {
- if (!window.closed) navigate(getInboxInvitesPath());
- noti.close();
- };
- },
- [navigate]
- );
-
- const playSound = useCallback(() => {
- const audioElement = audioRef.current;
- audioElement?.play();
- }, []);
-
- useEffect(() => {
- if (invites.length > perviousInviteLen && mx.getSyncState() === 'SYNCING') {
- if (showNotifications && notificationPermission('granted')) {
- notify(invites.length - perviousInviteLen);
- }
-
- if (notificationSound) {
- playSound();
- }
- }
- }, [mx, invites, perviousInviteLen, showNotifications, notificationSound, notify, playSound]);
-
- return (
- // eslint-disable-next-line jsx-a11y/media-has-caption
-
- );
-}
-
-function MessageNotifications() {
- const audioRef = useRef(null);
- const notifRef = useRef();
- const unreadCacheRef = useRef