Compare commits
13 Commits
v4.0.3
...
e68c56b334
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e68c56b334 | ||
|
|
cabfdd47b5 | ||
|
|
cfe893f358 | ||
|
|
581211f13e | ||
|
|
8ed78d48fb | ||
|
|
96222de5bc | ||
|
|
681287c46a | ||
|
|
9cb5c70d51 | ||
|
|
c62050445b | ||
|
|
a8f5a6c2f4 | ||
|
|
e54bb2e423 | ||
|
|
5058136737 | ||
|
|
74dc76e22e |
2
.github/workflows/prod-deploy.yml
vendored
2
.github/workflows/prod-deploy.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.5.0
|
||||
uses: docker/setup-buildx-action@v3.6.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
|
||||
26
README.md
26
README.md
@@ -19,22 +19,24 @@ A Matrix client focusing primarily on simple, elegant and secure interface. The
|
||||
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
|
||||
|
||||
## Getting started
|
||||
Web app is available at https://app.cinny.in and gets updated on each new release. The `dev` branch is continuously deployed at https://dev.cinny.in but keep in mind that it could have things broken.
|
||||
* Web app is available at https://app.cinny.in and gets updated on each new release. The `dev` branch is continuously deployed at https://dev.cinny.in but keep in mind that it could have things broken.
|
||||
|
||||
You can also download our desktop app from [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
||||
* You can also download our desktop app from [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
||||
|
||||
To host Cinny on your own, download tarball of the app from [GitHub release](https://github.com/cinnyapp/cinny/releases/latest).
|
||||
* To host Cinny on your own, download tarball of the app from [GitHub release](https://github.com/cinnyapp/cinny/releases/latest).
|
||||
You can serve the application with a webserver of your choice by simply copying `dist/` directory to the webroot.
|
||||
To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
|
||||
To set default Homeserver on login, register and Explore Community page, place a customized [`config.json`](config.json) in webroot of your choice.
|
||||
You will also need to setup redirects to serve the assests. An example setting of redirects for netlify is done in [`netlify.toml`](netlify.toml). You can also set `hashRouter.enabled = true` in [`config.json`](config.json) if you have trouble setting redirects.
|
||||
To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts). For example, if you want to deploy on `https://cinny.in/app`, then change `base: '/app'`.
|
||||
|
||||
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by:
|
||||
```
|
||||
docker pull ajbura/cinny
|
||||
```
|
||||
or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by:
|
||||
```
|
||||
docker pull ghcr.io/cinnyapp/cinny:latest
|
||||
```
|
||||
* Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by:
|
||||
```
|
||||
docker pull ajbura/cinny
|
||||
```
|
||||
or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by:
|
||||
```
|
||||
docker pull ghcr.io/cinnyapp/cinny:latest
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>PGP Public Key to verify tarball</summary>
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "4.0.3",
|
||||
"version": "4.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cinny",
|
||||
"version": "4.0.3",
|
||||
"version": "4.1.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "4.0.3",
|
||||
"version": "4.1.0",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
86
src/app/components/BackRouteHandler.tsx
Normal file
86
src/app/components/BackRouteHandler.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
getDirectPath,
|
||||
getExplorePath,
|
||||
getHomePath,
|
||||
getInboxPath,
|
||||
getSpacePath,
|
||||
} from '../pages/pathUtils';
|
||||
import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '../pages/paths';
|
||||
|
||||
type BackRouteHandlerProps = {
|
||||
children: (onBack: () => void) => ReactNode;
|
||||
};
|
||||
export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
if (
|
||||
matchPath(
|
||||
{
|
||||
path: HOME_PATH,
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
navigate(getHomePath());
|
||||
return;
|
||||
}
|
||||
if (
|
||||
matchPath(
|
||||
{
|
||||
path: DIRECT_PATH,
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
navigate(getDirectPath());
|
||||
return;
|
||||
}
|
||||
const spaceMatch = matchPath(
|
||||
{
|
||||
path: SPACE_PATH,
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname
|
||||
);
|
||||
if (spaceMatch?.params.spaceIdOrAlias) {
|
||||
navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
matchPath(
|
||||
{
|
||||
path: EXPLORE_PATH,
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
navigate(getExplorePath());
|
||||
return;
|
||||
}
|
||||
if (
|
||||
matchPath(
|
||||
{
|
||||
path: INBOX_PATH,
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
navigate(getInboxPath());
|
||||
}
|
||||
}, [navigate, location]);
|
||||
|
||||
return children(goBack);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { MsgType } from 'matrix-js-sdk';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Opts } from 'linkifyjs';
|
||||
import {
|
||||
AudioContent,
|
||||
DownloadFile,
|
||||
@@ -27,6 +28,7 @@ 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';
|
||||
|
||||
type RenderMessageContentProps = {
|
||||
displayName: string;
|
||||
@@ -38,6 +40,7 @@ type RenderMessageContentProps = {
|
||||
urlPreview?: boolean;
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
outlineAttachment?: boolean;
|
||||
};
|
||||
export function RenderMessageContent({
|
||||
@@ -50,8 +53,21 @@ export function RenderMessageContent({
|
||||
urlPreview,
|
||||
highlightRegex,
|
||||
htmlReactParserOptions,
|
||||
linkifyOpts,
|
||||
outlineAttachment,
|
||||
}: RenderMessageContentProps) {
|
||||
const renderUrlsPreview = (urls: string[]) => {
|
||||
const filteredUrls = urls.filter((url) => !testMatrixTo(url));
|
||||
if (filteredUrls.length === 0) return undefined;
|
||||
return (
|
||||
<UrlPreviewHolder>
|
||||
{filteredUrls.map((url) => (
|
||||
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||
))}
|
||||
</UrlPreviewHolder>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFile = () => (
|
||||
<MFile
|
||||
content={getContent()}
|
||||
@@ -95,19 +111,10 @@ export function RenderMessageContent({
|
||||
{...props}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
)}
|
||||
renderUrlsPreview={
|
||||
urlPreview
|
||||
? (urls) => (
|
||||
<UrlPreviewHolder>
|
||||
{urls.map((url) => (
|
||||
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||
))}
|
||||
</UrlPreviewHolder>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -123,19 +130,10 @@ export function RenderMessageContent({
|
||||
{...props}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
)}
|
||||
renderUrlsPreview={
|
||||
urlPreview
|
||||
? (urls) => (
|
||||
<UrlPreviewHolder>
|
||||
{urls.map((url) => (
|
||||
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||
))}
|
||||
</UrlPreviewHolder>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -150,19 +148,10 @@ export function RenderMessageContent({
|
||||
{...props}
|
||||
highlightRegex={highlightRegex}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
)}
|
||||
renderUrlsPreview={
|
||||
urlPreview
|
||||
? (urls) => (
|
||||
<UrlPreviewHolder>
|
||||
{urls.map((url) => (
|
||||
<UrlPreviewCard key={url} url={url} ts={ts} />
|
||||
))}
|
||||
</UrlPreviewHolder>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { mDirectAtom } from '../../../state/mDirectList';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { factoryRoomIdByActivity } from '../../../utils/sort';
|
||||
import { RoomAvatar, RoomIcon } from '../../room-avatar';
|
||||
import { getViaServers } from '../../../plugins/via-servers';
|
||||
|
||||
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
|
||||
|
||||
@@ -104,10 +105,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 mentionEl = createMentionElement(
|
||||
roomAliasOrId,
|
||||
name.startsWith('#') ? name : `#${name}`,
|
||||
roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId
|
||||
roomId === roomAliasOrId || mx.getRoom(roomId)?.getCanonicalAlias() === roomAliasOrId,
|
||||
undefined,
|
||||
viaServers
|
||||
);
|
||||
replaceWithElement(editor, query.range, mentionEl);
|
||||
moveCursor(editor, true);
|
||||
|
||||
@@ -18,8 +18,14 @@ import {
|
||||
ParagraphElement,
|
||||
UnorderedListElement,
|
||||
} from './slate';
|
||||
import { parseMatrixToUrl } from '../../utils/matrix';
|
||||
import { createEmoticonElement, createMentionElement } from './utils';
|
||||
import {
|
||||
parseMatrixToRoom,
|
||||
parseMatrixToRoomEvent,
|
||||
parseMatrixToUser,
|
||||
testMatrixTo,
|
||||
} from '../../plugins/matrix-to';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
|
||||
const markNodeToType: Record<string, MarkType> = {
|
||||
b: MarkType.Bold,
|
||||
@@ -68,11 +74,33 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
|
||||
return createEmoticonElement(src, alt || 'Unknown Emoji');
|
||||
}
|
||||
if (node.name === 'a') {
|
||||
const { href } = node.attribs;
|
||||
const href = tryDecodeURIComponent(node.attribs.href);
|
||||
if (typeof href !== 'string') return undefined;
|
||||
const [mxId] = parseMatrixToUrl(href);
|
||||
if (mxId) {
|
||||
return createMentionElement(mxId, parseNodeText(node) || mxId, false);
|
||||
if (testMatrixTo(href)) {
|
||||
const userMention = parseMatrixToUser(href);
|
||||
if (userMention) {
|
||||
return createMentionElement(userMention, parseNodeText(node) || userMention, false);
|
||||
}
|
||||
const roomMention = parseMatrixToRoom(href);
|
||||
if (roomMention) {
|
||||
return createMentionElement(
|
||||
roomMention.roomIdOrAlias,
|
||||
parseNodeText(node) || roomMention.roomIdOrAlias,
|
||||
false,
|
||||
undefined,
|
||||
roomMention.viaServers
|
||||
);
|
||||
}
|
||||
const eventMention = parseMatrixToRoomEvent(href);
|
||||
if (eventMention) {
|
||||
return createMentionElement(
|
||||
eventMention.roomIdOrAlias,
|
||||
parseNodeText(node) || eventMention.roomIdOrAlias,
|
||||
false,
|
||||
eventMention.eventId,
|
||||
eventMention.viaServers
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -51,10 +51,19 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
|
||||
case BlockType.UnorderedList:
|
||||
return `<ul>${children}</ul>`;
|
||||
|
||||
case BlockType.Mention:
|
||||
return `<a href="https://matrix.to/#/${encodeURIComponent(node.id)}">${sanitizeText(
|
||||
node.name
|
||||
)}</a>`;
|
||||
case BlockType.Mention: {
|
||||
let fragment = node.id;
|
||||
|
||||
if (node.eventId) {
|
||||
fragment += `/${node.eventId}`;
|
||||
}
|
||||
if (node.viaServers && node.viaServers.length > 0) {
|
||||
fragment += `?${node.viaServers.map((server) => `via=${server}`).join('&')}`;
|
||||
}
|
||||
|
||||
const matrixTo = `https://matrix.to/#/${fragment}`;
|
||||
return `<a href="${encodeURIComponent(matrixTo)}">${sanitizeText(node.name)}</a>`;
|
||||
}
|
||||
case BlockType.Emoticon:
|
||||
return node.key.startsWith('mxc://')
|
||||
? `<img data-mx-emoticon src="${node.key}" alt="${sanitizeText(
|
||||
|
||||
2
src/app/components/editor/slate.d.ts
vendored
2
src/app/components/editor/slate.d.ts
vendored
@@ -29,6 +29,8 @@ export type LinkElement = {
|
||||
export type MentionElement = {
|
||||
type: BlockType.Mention;
|
||||
id: string;
|
||||
eventId?: string;
|
||||
viaServers?: string[];
|
||||
highlight: boolean;
|
||||
name: string;
|
||||
children: Text[];
|
||||
|
||||
@@ -158,10 +158,14 @@ export const resetEditorHistory = (editor: Editor) => {
|
||||
export const createMentionElement = (
|
||||
id: string,
|
||||
name: string,
|
||||
highlight: boolean
|
||||
highlight: boolean,
|
||||
eventId?: string,
|
||||
viaServers?: string[]
|
||||
): MentionElement => ({
|
||||
type: BlockType.Mention,
|
||||
id,
|
||||
eventId,
|
||||
viaServers,
|
||||
highlight,
|
||||
name,
|
||||
children: [{ text: '' }],
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import React from 'react';
|
||||
import parse, { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import Linkify from 'linkify-react';
|
||||
import { Opts } from 'linkifyjs';
|
||||
import { MessageEmptyContent } from './content';
|
||||
import { sanitizeCustomHtml } from '../../utils/sanitize';
|
||||
import {
|
||||
LINKIFY_OPTS,
|
||||
highlightText,
|
||||
scaleSystemEmoji,
|
||||
} from '../../plugins/react-custom-html-parser';
|
||||
import { highlightText, scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
|
||||
|
||||
type RenderBodyProps = {
|
||||
body: string;
|
||||
@@ -15,12 +12,14 @@ type RenderBodyProps = {
|
||||
|
||||
highlightRegex?: RegExp;
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
};
|
||||
export function RenderBody({
|
||||
body,
|
||||
customBody,
|
||||
highlightRegex,
|
||||
htmlReactParserOptions,
|
||||
linkifyOpts,
|
||||
}: RenderBodyProps) {
|
||||
if (body === '') <MessageEmptyContent />;
|
||||
if (customBody) {
|
||||
@@ -28,7 +27,7 @@ export function RenderBody({
|
||||
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
|
||||
}
|
||||
return (
|
||||
<Linkify options={LINKIFY_OPTS}>
|
||||
<Linkify options={linkifyOpts}>
|
||||
{highlightRegex
|
||||
? highlightText(highlightRegex, scaleSystemEmoji(body))
|
||||
: scaleSystemEmoji(body)}
|
||||
|
||||
@@ -87,15 +87,17 @@ export const Page = as<'div'>(({ className, ...props }, ref) => (
|
||||
/>
|
||||
));
|
||||
|
||||
export const PageHeader = as<'div'>(({ className, ...props }, ref) => (
|
||||
<Header
|
||||
as="header"
|
||||
size="600"
|
||||
className={classNames(css.PageHeader, className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
export const PageHeader = as<'div', css.PageHeaderVariants>(
|
||||
({ className, balance, ...props }, ref) => (
|
||||
<Header
|
||||
as="header"
|
||||
size="600"
|
||||
className={classNames(css.PageHeader({ balance }), className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const PageContent = as<'div'>(({ className, ...props }, ref) => (
|
||||
<div className={classNames(css.PageContent, className)} {...props} ref={ref} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
export const PageNav = style({
|
||||
@@ -33,11 +34,21 @@ export const PageNavContent = style({
|
||||
paddingBottom: config.space.S700,
|
||||
});
|
||||
|
||||
export const PageHeader = style({
|
||||
paddingLeft: config.space.S400,
|
||||
paddingRight: config.space.S200,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
export const PageHeader = recipe({
|
||||
base: {
|
||||
paddingLeft: config.space.S400,
|
||||
paddingRight: config.space.S200,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
},
|
||||
variants: {
|
||||
balance: {
|
||||
true: {
|
||||
paddingLeft: config.space.S200,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export type PageHeaderVariants = RecipeVariants<typeof PageHeader>;
|
||||
|
||||
export const PageContent = style([
|
||||
DefaultReset,
|
||||
|
||||
@@ -138,6 +138,7 @@ type RoomCardProps = {
|
||||
topic?: string;
|
||||
memberCount?: number;
|
||||
roomType?: string;
|
||||
viaServers?: string[];
|
||||
onView?: (roomId: string) => void;
|
||||
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
|
||||
};
|
||||
@@ -152,6 +153,7 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||
topic,
|
||||
memberCount,
|
||||
roomType,
|
||||
viaServers,
|
||||
onView,
|
||||
renderTopicViewer,
|
||||
...props
|
||||
@@ -194,7 +196,7 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||
);
|
||||
|
||||
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
|
||||
useCallback(() => mx.joinRoom(roomIdOrAlias), [mx, roomIdOrAlias])
|
||||
useCallback(() => mx.joinRoom(roomIdOrAlias, { viaServers }), [mx, roomIdOrAlias, viaServers])
|
||||
);
|
||||
const joining =
|
||||
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useIntersectionObserver,
|
||||
} from '../../hooks/useIntersectionObserver';
|
||||
import * as css from './UrlPreviewCard.css';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
|
||||
const linkStyles = { color: color.Success.Main };
|
||||
|
||||
@@ -43,7 +44,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
||||
priority="300"
|
||||
>
|
||||
{typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
|
||||
{decodeURIComponent(url)}
|
||||
{tryDecodeURIComponent(url)}
|
||||
</Text>
|
||||
<Text truncate priority="400">
|
||||
<b>{prev['og:title']}</b>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Scroll, Text, toRem } from 'folds';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text, toRem } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { RoomCard } from '../../components/room-card';
|
||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||
@@ -8,28 +8,48 @@ 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';
|
||||
|
||||
type JoinBeforeNavigateProps = { roomIdOrAlias: string };
|
||||
export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
|
||||
type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
|
||||
export function JoinBeforeNavigate({
|
||||
roomIdOrAlias,
|
||||
eventId,
|
||||
viaServers,
|
||||
}: JoinBeforeNavigateProps) {
|
||||
const mx = useMatrixClient();
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const handleView = (roomId: string) => {
|
||||
if (mx.getRoom(roomId)?.isSpaceRoom()) {
|
||||
navigateSpace(roomId);
|
||||
return;
|
||||
}
|
||||
navigateRoom(roomId);
|
||||
navigateRoom(roomId, eventId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{roomIdOrAlias}
|
||||
</Text>
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box shrink="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{roomIdOrAlias}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
@@ -46,6 +66,7 @@ export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
|
||||
topic={summary?.topic}
|
||||
memberCount={summary?.num_joined_members}
|
||||
roomType={summary?.room_type}
|
||||
viaServers={viaServers}
|
||||
renderTopicViewer={(name, topic, requestClose) => (
|
||||
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
|
||||
)}
|
||||
|
||||
@@ -31,6 +31,8 @@ import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
||||
|
||||
type LobbyMenuProps = {
|
||||
roomId: string;
|
||||
@@ -123,6 +125,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
||||
const space = useSpace();
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const name = useRoomName(space);
|
||||
const avatarMxc = useRoomAvatar(space);
|
||||
@@ -133,42 +136,72 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageHeader className={showProfile ? undefined : css.Header}>
|
||||
<PageHeader className={showProfile ? undefined : css.Header} balance>
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Box grow="Yes" basis="No" />
|
||||
<Box justifyContent="Center" alignItems="Center" gap="300">
|
||||
{showProfile && (
|
||||
<>
|
||||
<Avatar size="300">
|
||||
<RoomAvatar
|
||||
roomId={space.roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
<Text size="H3" truncate>
|
||||
{name}
|
||||
</Text>
|
||||
</>
|
||||
{screenSize === ScreenSize.Mobile ? (
|
||||
<>
|
||||
<Box shrink="No">
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
</Box>
|
||||
<Box grow="Yes" justifyContent="Center">
|
||||
{showProfile && (
|
||||
<Text size="H3" truncate>
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box grow="Yes" basis="No" />
|
||||
<Box justifyContent="Center" alignItems="Center" gap="300">
|
||||
{showProfile && (
|
||||
<>
|
||||
<Avatar size="300">
|
||||
<RoomAvatar
|
||||
roomId={space.roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
<Text size="H3" truncate>
|
||||
{name}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<Box
|
||||
shrink="No"
|
||||
grow={screenSize === ScreenSize.Mobile ? 'No' : 'Yes'}
|
||||
basis={screenSize === ScreenSize.Mobile ? 'Yes' : 'No'}
|
||||
justifyContent="End"
|
||||
>
|
||||
{screenSize !== ScreenSize.Mobile && (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Members</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
|
||||
<Icon size="400" src={Icons.User} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</Box>
|
||||
<Box shrink="No" grow="Yes" basis="No" justifyContent="End">
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Members</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
|
||||
<Icon size="400" src={Icons.User} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
|
||||
@@ -3,13 +3,17 @@ import React, { MouseEventHandler, useMemo } from 'react';
|
||||
import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import {
|
||||
factoryRenderLinkifyWithMention,
|
||||
getReactCustomHtmlParser,
|
||||
LINKIFY_OPTS,
|
||||
makeHighlightRegex,
|
||||
makeMentionCustomProps,
|
||||
renderMatrixMention,
|
||||
} from '../../plugins/react-custom-html-parser';
|
||||
import { getMxIdLocalPart, isRoomId, isUserId } from '../../utils/matrix';
|
||||
import { openJoinAlias, openProfileViewer } from '../../../client/action/navigation';
|
||||
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
|
||||
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
|
||||
import {
|
||||
@@ -31,8 +35,9 @@ import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../.
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { ResultItem } from './useMessageSearch';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { UserAvatar } from '../../components/user-avatar';
|
||||
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
|
||||
|
||||
type SearchResultGroupProps = {
|
||||
room: Room;
|
||||
@@ -51,38 +56,29 @@ export function SearchResultGroup({
|
||||
onOpen,
|
||||
}: SearchResultGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
|
||||
|
||||
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||
() => ({
|
||||
...LINKIFY_OPTS,
|
||||
render: factoryRenderLinkifyWithMention((href) =>
|
||||
renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
|
||||
),
|
||||
}),
|
||||
[mx, room, mentionClickHandler]
|
||||
);
|
||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(mx, room, {
|
||||
getReactCustomHtmlParser(mx, room.roomId, {
|
||||
linkifyOpts,
|
||||
highlightRegex,
|
||||
handleSpoilerClick: (evt) => {
|
||||
const target = evt.currentTarget;
|
||||
if (target.getAttribute('aria-pressed') === 'true') {
|
||||
evt.stopPropagation();
|
||||
target.setAttribute('aria-pressed', 'false');
|
||||
target.style.cursor = 'initial';
|
||||
}
|
||||
},
|
||||
handleMentionClick: (evt) => {
|
||||
const target = evt.currentTarget;
|
||||
const mentionId = target.getAttribute('data-mention-id');
|
||||
if (typeof mentionId !== 'string') return;
|
||||
if (isUserId(mentionId)) {
|
||||
openProfileViewer(mentionId, room.roomId);
|
||||
return;
|
||||
}
|
||||
if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
|
||||
if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
|
||||
else navigateRoom(mentionId);
|
||||
return;
|
||||
}
|
||||
openJoinAlias(mentionId);
|
||||
},
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
}),
|
||||
[mx, room, highlightRegex, navigateRoom, navigateSpace]
|
||||
[mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler]
|
||||
);
|
||||
|
||||
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
|
||||
@@ -101,6 +97,7 @@ export function SearchResultGroup({
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
highlightRegex={highlightRegex}
|
||||
outlineAttachment
|
||||
/>
|
||||
|
||||
@@ -28,25 +28,24 @@ import { useRoomUnread } from '../../state/hooks/unread';
|
||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import { copyToClipboard } from '../../utils/dom';
|
||||
import { getOriginBaseUrl, withOriginBaseUrl } from '../../pages/pathUtils';
|
||||
import { markAsRead } from '../../../client/action/notifications';
|
||||
import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
||||
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';
|
||||
|
||||
type RoomNavItemMenuProps = {
|
||||
room: Room;
|
||||
linkPath: string;
|
||||
requestClose: () => void;
|
||||
};
|
||||
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||
({ room, linkPath, requestClose }, ref) => {
|
||||
({ room, requestClose }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const { hashRouter } = useClientConfig();
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
@@ -63,7 +62,9 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), linkPath));
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
@@ -273,11 +274,7 @@ export function RoomNavItem({
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<RoomNavItemMenu
|
||||
room={room}
|
||||
linkPath={linkPath}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
/>
|
||||
<RoomNavItemMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
import { markAsRead } from '../../../client/action/notifications';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||
import { editableActiveElement } from '../../utils/dom';
|
||||
|
||||
export function Room() {
|
||||
const { eventId } = useParams();
|
||||
@@ -28,7 +29,7 @@ export function Room() {
|
||||
window,
|
||||
useCallback(
|
||||
(evt) => {
|
||||
if (isKeyHotkey('escape', evt)) {
|
||||
if (isKeyHotkey('escape', evt) && !editableActiveElement()) {
|
||||
markAsRead(mx, room.roomId);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -186,9 +186,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
Transforms.insertFragment(editor, msgDraft);
|
||||
}, [editor, msgDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||
return () => {
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (!isEmptyEditor(editor)) {
|
||||
const parsedDraft = JSON.parse(JSON.stringify(editor.children));
|
||||
setMsgDraft(parsedDraft);
|
||||
@@ -197,8 +196,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
}
|
||||
resetEditor(editor);
|
||||
resetEditorHistory(editor);
|
||||
};
|
||||
}, [roomId, editor, setMsgDraft]);
|
||||
},
|
||||
[roomId, editor, setMsgDraft]
|
||||
);
|
||||
|
||||
const handleRemoveUpload = useCallback(
|
||||
(upload: TUploadContent | TUploadContent[]) => {
|
||||
|
||||
@@ -45,13 +45,12 @@ import {
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import {
|
||||
decryptFile,
|
||||
eventWithShortcode,
|
||||
factoryEventSentBy,
|
||||
getMxIdLocalPart,
|
||||
isRoomId,
|
||||
isUserId,
|
||||
} from '../../utils/matrix';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
|
||||
@@ -70,7 +69,13 @@ import {
|
||||
ImageContent,
|
||||
EventContent,
|
||||
} from '../../components/message';
|
||||
import { getReactCustomHtmlParser } from '../../plugins/react-custom-html-parser';
|
||||
import {
|
||||
factoryRenderLinkifyWithMention,
|
||||
getReactCustomHtmlParser,
|
||||
LINKIFY_OPTS,
|
||||
makeMentionCustomProps,
|
||||
renderMatrixMention,
|
||||
} from '../../plugins/react-custom-html-parser';
|
||||
import {
|
||||
canEditEvent,
|
||||
decryptAllTimelineEvent,
|
||||
@@ -85,7 +90,7 @@ import {
|
||||
} from '../../utils/room';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { openJoinAlias, openProfileViewer } from '../../../client/action/navigation';
|
||||
import { openProfileViewer } from '../../../client/action/navigation';
|
||||
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
|
||||
import { Reactions, Message, Event, EncryptedContent } from './message';
|
||||
import { useMemberEventParser } from '../../hooks/useMemberEventParser';
|
||||
@@ -109,10 +114,12 @@ import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
|
||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
||||
import { Image } from '../../components/media';
|
||||
import { ImageViewer } from '../../components/image-viewer';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { useRoomUnread } from '../../state/hooks/unread';
|
||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
||||
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
|
||||
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
|
||||
({ position, className, ...props }, ref) => (
|
||||
@@ -445,9 +452,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const canRedact = canDoAction('redact', myPowerLevel);
|
||||
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
|
||||
const [editId, setEditId] = useState<string>();
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
const imagePackRooms: Room[] = useMemo(() => {
|
||||
const allParentSpaces = [room.roomId].concat(
|
||||
@@ -487,34 +496,23 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
>();
|
||||
const alive = useAlive();
|
||||
|
||||
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||
() => ({
|
||||
...LINKIFY_OPTS,
|
||||
render: factoryRenderLinkifyWithMention((href) =>
|
||||
renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
|
||||
),
|
||||
}),
|
||||
[mx, room, mentionClickHandler]
|
||||
);
|
||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(mx, room, {
|
||||
handleSpoilerClick: (evt) => {
|
||||
const target = evt.currentTarget;
|
||||
if (target.getAttribute('aria-pressed') === 'true') {
|
||||
evt.stopPropagation();
|
||||
target.setAttribute('aria-pressed', 'false');
|
||||
target.style.cursor = 'initial';
|
||||
}
|
||||
},
|
||||
handleMentionClick: (evt) => {
|
||||
const target = evt.currentTarget;
|
||||
const mentionId = target.getAttribute('data-mention-id');
|
||||
if (typeof mentionId !== 'string') return;
|
||||
if (isUserId(mentionId)) {
|
||||
openProfileViewer(mentionId, room.roomId);
|
||||
return;
|
||||
}
|
||||
if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
|
||||
if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
|
||||
else navigateRoom(mentionId);
|
||||
return;
|
||||
}
|
||||
openJoinAlias(mentionId);
|
||||
},
|
||||
getReactCustomHtmlParser(mx, room.roomId, {
|
||||
linkifyOpts,
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
}),
|
||||
[mx, room, navigateRoom, navigateSpace]
|
||||
[mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler]
|
||||
);
|
||||
const parseMemberEvent = useMemberEventParser();
|
||||
|
||||
@@ -597,7 +595,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
// so timeline can be updated with evt like: edits, reactions etc
|
||||
if (atBottomRef.current) {
|
||||
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
|
||||
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()));
|
||||
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!));
|
||||
}
|
||||
|
||||
if (document.hasFocus()) {
|
||||
@@ -819,6 +817,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
}, [scrollToElement, editId]);
|
||||
|
||||
const handleJumpToLatest = () => {
|
||||
if (eventId) {
|
||||
navigateRoom(room.roomId, undefined, { replace: true });
|
||||
}
|
||||
setTimeline(getInitialTimeline(room));
|
||||
scrollToBottomRef.current.count += 1;
|
||||
scrollToBottomRef.current.smooth = false;
|
||||
@@ -1036,6 +1037,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === 2}
|
||||
/>
|
||||
)}
|
||||
@@ -1132,6 +1134,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={showUrlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment={messageLayout === 2}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Box, Button, Spinner, Text, color } from 'folds';
|
||||
|
||||
import * as css from './RoomTombstone.css';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { genRoomVia } from '../../../util/matrixUtil';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { Membership } from '../../../types/matrix/room';
|
||||
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { getViaServers } from '../../plugins/via-servers';
|
||||
|
||||
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string };
|
||||
export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) {
|
||||
@@ -17,7 +17,7 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
|
||||
const [joinState, handleJoin] = useAsyncCallback(
|
||||
useCallback(() => {
|
||||
const currentRoom = mx.getRoom(roomId);
|
||||
const via = currentRoom ? genRoomVia(currentRoom) : [];
|
||||
const via = currentRoom ? getViaServers(currentRoom) : [];
|
||||
return mx.joinRoom(replacementRoomId, {
|
||||
viaServers: via,
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||
if (evt.metaKey || evt.altKey || evt.ctrlKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// do not focus on F keys
|
||||
if (/^F\d+$/.test(code)) return false;
|
||||
|
||||
@@ -36,6 +37,9 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||
code.startsWith('Alt') ||
|
||||
code.startsWith('Control') ||
|
||||
code.startsWith('Arrow') ||
|
||||
code.startsWith('Page') ||
|
||||
code.startsWith('End') ||
|
||||
code.startsWith('Home') ||
|
||||
code === 'Tab' ||
|
||||
code === 'Space' ||
|
||||
code === 'Enter' ||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
PopOut,
|
||||
RectCords,
|
||||
} from 'folds';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { JoinRule, Room } from 'matrix-js-sdk';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
@@ -35,15 +35,8 @@ import { useRoom } from '../../hooks/useRoom';
|
||||
import { useSetSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||
import {
|
||||
getHomeSearchPath,
|
||||
getOriginBaseUrl,
|
||||
getSpaceSearchPath,
|
||||
joinPathComponent,
|
||||
withOriginBaseUrl,
|
||||
withSearchParam,
|
||||
} from '../../pages/pathUtils';
|
||||
import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
|
||||
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
|
||||
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
|
||||
import { _SearchPathSearchParams } from '../../pages/paths';
|
||||
import * as css from './RoomViewHeader.css';
|
||||
import { useRoomUnread } from '../../state/hooks/unread';
|
||||
@@ -55,128 +48,128 @@ import { copyToClipboard } from '../../utils/dom';
|
||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
||||
import { getViaServers } from '../../plugins/via-servers';
|
||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
||||
|
||||
type RoomMenuProps = {
|
||||
room: Room;
|
||||
linkPath: string;
|
||||
requestClose: () => void;
|
||||
};
|
||||
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
|
||||
({ room, linkPath, requestClose }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const { hashRouter } = useClientConfig();
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
markAsRead(mx, room.roomId);
|
||||
requestClose();
|
||||
};
|
||||
const handleMarkAsRead = () => {
|
||||
markAsRead(mx, room.roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
const handleInvite = () => {
|
||||
openInviteUser(room.roomId);
|
||||
requestClose();
|
||||
};
|
||||
const handleInvite = () => {
|
||||
openInviteUser(room.roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), linkPath));
|
||||
requestClose();
|
||||
};
|
||||
const handleCopyLink = () => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
const handleRoomSettings = () => {
|
||||
toggleRoomSettings(room.roomId);
|
||||
requestClose();
|
||||
};
|
||||
const handleRoomSettings = () => {
|
||||
toggleRoomSettings(room.roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.CheckTwice} />}
|
||||
radii="300"
|
||||
disabled={!unread}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Mark as Read
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||
radii="300"
|
||||
disabled={!canInvite}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Invite
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleCopyLink}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Link} />}
|
||||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Copy Link
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleRoomSettings}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Setting} />}
|
||||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Room Settings
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<UseStateProvider initial={false}>
|
||||
{(promptLeave, setPromptLeave) => (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => setPromptLeave(true)}
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.ArrowGoLeft} />}
|
||||
radii="300"
|
||||
aria-pressed={promptLeave}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Leave Room
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptLeave && (
|
||||
<LeaveRoomPrompt
|
||||
roomId={room.roomId}
|
||||
onDone={requestClose}
|
||||
onCancel={() => setPromptLeave(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.CheckTwice} />}
|
||||
radii="300"
|
||||
disabled={!unread}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Mark as Read
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||
radii="300"
|
||||
disabled={!canInvite}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Invite
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleCopyLink}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Link} />}
|
||||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Copy Link
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleRoomSettings}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Setting} />}
|
||||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Room Settings
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<UseStateProvider initial={false}>
|
||||
{(promptLeave, setPromptLeave) => (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => setPromptLeave(true)}
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.ArrowGoLeft} />}
|
||||
radii="300"
|
||||
aria-pressed={promptLeave}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Leave Room
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptLeave && (
|
||||
<LeaveRoomPrompt
|
||||
roomId={room.roomId}
|
||||
onDone={requestClose}
|
||||
onCancel={() => setPromptLeave(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
export function RoomViewHeader() {
|
||||
const navigate = useNavigate();
|
||||
@@ -195,8 +188,6 @@ export function RoomViewHeader() {
|
||||
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
|
||||
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const location = useLocation();
|
||||
const currentPath = joinPathComponent(location);
|
||||
|
||||
const handleSearchClick = () => {
|
||||
const searchParams: _SearchPathSearchParams = {
|
||||
@@ -213,19 +204,36 @@ export function RoomViewHeader() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageHeader>
|
||||
<PageHeader balance={screenSize === ScreenSize.Mobile}>
|
||||
<Box grow="Yes" gap="300">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<Box shrink="No" alignItems="Center">
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
<Box grow="Yes" alignItems="Center" gap="300">
|
||||
<Avatar size="300">
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<RoomIcon size="200" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
{screenSize !== ScreenSize.Mobile && (
|
||||
<Avatar size="300">
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<RoomIcon
|
||||
size="200"
|
||||
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
<Box direction="Column">
|
||||
<Text size={topic ? 'H5' : 'H3'} truncate>
|
||||
{name}
|
||||
@@ -336,11 +344,7 @@ export function RoomViewHeader() {
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<RoomMenu
|
||||
room={room}
|
||||
linkPath={currentPath}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
/>
|
||||
<RoomMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
getMemberAvatarMxc,
|
||||
getMemberDisplayName,
|
||||
} from '../../../utils/room';
|
||||
import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../../utils/matrix';
|
||||
import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias } from '../../../utils/matrix';
|
||||
import { MessageLayout, MessageSpacing } from '../../../state/settings';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
@@ -63,18 +63,10 @@ import { EmojiBoard } from '../../../components/emoji-board';
|
||||
import { ReactionViewer } from '../reaction-viewer';
|
||||
import { MessageEditor } from './MessageEditor';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { useSpaceOptionally } from '../../../hooks/useSpace';
|
||||
import { useDirectSelected } from '../../../hooks/router/useDirectSelected';
|
||||
import {
|
||||
getDirectRoomPath,
|
||||
getHomeRoomPath,
|
||||
getOriginBaseUrl,
|
||||
getSpaceRoomPath,
|
||||
withOriginBaseUrl,
|
||||
} from '../../../pages/pathUtils';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { getMatrixToRoomEvent } from '../../../plugins/matrix-to';
|
||||
import { getViaServers } from '../../../plugins/via-servers';
|
||||
|
||||
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
|
||||
|
||||
@@ -321,23 +313,13 @@ export const MessageCopyLinkItem = as<
|
||||
}
|
||||
>(({ room, mEvent, onClose, ...props }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const { hashRouter } = useClientConfig();
|
||||
const space = useSpaceOptionally();
|
||||
const directSelected = useDirectSelected();
|
||||
|
||||
const handleCopy = () => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||
let eventPath = getHomeRoomPath(roomIdOrAlias, mEvent.getId());
|
||||
if (space) {
|
||||
eventPath = getSpaceRoomPath(
|
||||
getCanonicalAliasOrRoomId(mx, space.roomId),
|
||||
roomIdOrAlias,
|
||||
mEvent.getId()
|
||||
);
|
||||
} else if (directSelected) {
|
||||
eventPath = getDirectRoomPath(roomIdOrAlias, mEvent.getId());
|
||||
}
|
||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), eventPath));
|
||||
const eventId = mEvent.getId();
|
||||
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||
if (!eventId) return;
|
||||
copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
|
||||
14
src/app/hooks/router/useSearchParamsViaServers.ts
Normal file
14
src/app/hooks/router/useSearchParamsViaServers.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { getRoomSearchParams } from '../../pages/pathSearchParam';
|
||||
import { decodeSearchParamValueArray } from '../../pages/pathUtils';
|
||||
|
||||
export const useSearchParamsViaServers = (): string[] | undefined => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const roomSearchParams = useMemo(() => getRoomSearchParams(searchParams), [searchParams]);
|
||||
const viaServers = roomSearchParams.viaServers
|
||||
? decodeSearchParamValueArray(roomSearchParams.viaServers)
|
||||
: undefined;
|
||||
|
||||
return viaServers;
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export function useDeviceList() {
|
||||
const mx = useMatrixClient();
|
||||
const [deviceList, setDeviceList] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const updateDevices = () => mx.getDevices().then((data) => {
|
||||
if (!isMounted) return;
|
||||
setDeviceList(data.devices || []);
|
||||
});
|
||||
updateDevices();
|
||||
|
||||
const handleDevicesUpdate = (users) => {
|
||||
if (users.includes(mx.getUserId())) {
|
||||
updateDevices();
|
||||
}
|
||||
};
|
||||
|
||||
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
|
||||
return () => {
|
||||
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
|
||||
isMounted = false;
|
||||
};
|
||||
}, [mx]);
|
||||
return deviceList;
|
||||
}
|
||||
35
src/app/hooks/useDeviceList.ts
Normal file
35
src/app/hooks/useDeviceList.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CryptoEvent, IMyDevice } from 'matrix-js-sdk';
|
||||
import { CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export function useDeviceList() {
|
||||
const mx = useMatrixClient();
|
||||
const [deviceList, setDeviceList] = useState<IMyDevice[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const updateDevices = () =>
|
||||
mx.getDevices().then((data) => {
|
||||
if (!isMounted) return;
|
||||
setDeviceList(data.devices || []);
|
||||
});
|
||||
updateDevices();
|
||||
|
||||
const handleDevicesUpdate: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] = (users) => {
|
||||
const userId = mx.getUserId();
|
||||
if (userId && users.includes(userId)) {
|
||||
updateDevices();
|
||||
}
|
||||
};
|
||||
|
||||
mx.on(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||
isMounted = false;
|
||||
};
|
||||
}, [mx]);
|
||||
return deviceList;
|
||||
}
|
||||
43
src/app/hooks/useMentionClickHandler.ts
Normal file
43
src/app/hooks/useMentionClickHandler.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
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 { openProfileViewer } from '../../client/action/navigation';
|
||||
import { getHomeRoomPath, withSearchParam } from '../pages/pathUtils';
|
||||
import { _RoomSearchParams } from '../pages/paths';
|
||||
|
||||
export const useMentionClickHandler = (roomId: string): ReactEventHandler<HTMLElement> => {
|
||||
const mx = useMatrixClient();
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick: ReactEventHandler<HTMLElement> = useCallback(
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
const target = evt.currentTarget;
|
||||
const mentionId = target.getAttribute('data-mention-id');
|
||||
if (typeof mentionId !== 'string') return;
|
||||
|
||||
if (isUserId(mentionId)) {
|
||||
openProfileViewer(mentionId, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventId = target.getAttribute('data-mention-event-id') || undefined;
|
||||
if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
|
||||
if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
|
||||
else navigateRoom(mentionId, eventId);
|
||||
return;
|
||||
}
|
||||
|
||||
const viaServers = target.getAttribute('data-mention-via') || undefined;
|
||||
const path = getHomeRoomPath(mentionId, eventId);
|
||||
|
||||
navigate(viaServers ? withSearchParam<_RoomSearchParams>(path, { viaServers }) : path);
|
||||
},
|
||||
[mx, navigate, navigateRoom, navigateSpace, roomId]
|
||||
);
|
||||
|
||||
return handleClick;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { NavigateOptions, useNavigate } from 'react-router-dom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
||||
import {
|
||||
@@ -12,12 +12,14 @@ import { useMatrixClient } from './useMatrixClient';
|
||||
import { getOrphanParents } from '../utils/room';
|
||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||
import { mDirectAtom } from '../state/mDirectList';
|
||||
import { useSelectedSpace } from './router/useSelectedSpace';
|
||||
|
||||
export const useRoomNavigate = () => {
|
||||
const navigate = useNavigate();
|
||||
const mx = useMatrixClient();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const spaceSelectedId = useSelectedSpace();
|
||||
|
||||
const navigateSpace = useCallback(
|
||||
(roomId: string) => {
|
||||
@@ -28,24 +30,29 @@ export const useRoomNavigate = () => {
|
||||
);
|
||||
|
||||
const navigateRoom = useCallback(
|
||||
(roomId: string, eventId?: string) => {
|
||||
(roomId: string, eventId?: string, opts?: NavigateOptions) => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
||||
|
||||
const orphanParents = getOrphanParents(roomToParents, roomId);
|
||||
if (orphanParents.length > 0) {
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, orphanParents[0]);
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId));
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
|
||||
mx,
|
||||
spaceSelectedId && orphanParents.includes(spaceSelectedId)
|
||||
? spaceSelectedId
|
||||
: orphanParents[0]
|
||||
);
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDirects.has(roomId)) {
|
||||
navigate(getDirectRoomPath(roomIdOrAlias, eventId));
|
||||
navigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(getHomeRoomPath(roomIdOrAlias, eventId));
|
||||
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
|
||||
},
|
||||
[mx, navigate, roomToParents, mDirects]
|
||||
[mx, navigate, spaceSelectedId, roomToParents, mDirects]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
14
src/app/hooks/useSpoilerClickHandler.ts
Normal file
14
src/app/hooks/useSpoilerClickHandler.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ReactEventHandler, useCallback } from 'react';
|
||||
|
||||
export const useSpoilerClickHandler = (): ReactEventHandler<HTMLElement> => {
|
||||
const handleClick: ReactEventHandler<HTMLElement> = useCallback((evt) => {
|
||||
const target = evt.currentTarget;
|
||||
if (target.getAttribute('aria-pressed') === 'true') {
|
||||
evt.stopPropagation();
|
||||
target.setAttribute('aria-pressed', 'false');
|
||||
target.style.cursor = 'initial';
|
||||
}
|
||||
}, []);
|
||||
|
||||
return handleClick;
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import './SpaceAddExisting.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { joinRuleToIconSrc, getIdServer, genRoomVia } from '../../../util/matrixUtil';
|
||||
import { joinRuleToIconSrc, getIdServer } from '../../../util/matrixUtil';
|
||||
import { Debounce } from '../../../util/common';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
@@ -27,6 +27,7 @@ import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
|
||||
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { getViaServers } from '../../plugins/via-servers';
|
||||
|
||||
function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
|
||||
const mountStore = useStore(roomId);
|
||||
@@ -69,7 +70,7 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
|
||||
|
||||
const promises = selected.map((rId) => {
|
||||
const room = mx.getRoom(rId);
|
||||
const via = genRoomVia(room);
|
||||
const via = getViaServers(room);
|
||||
if (via.length === 0) {
|
||||
via.push(getIdServer(rId));
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
} from './pathUtils';
|
||||
import { ClientBindAtoms, ClientLayout, ClientRoot } from './client';
|
||||
import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home';
|
||||
import { Direct, DirectRouteRoomProvider } from './client/direct';
|
||||
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
|
||||
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
|
||||
import { Explore, FeaturedRooms, PublicRooms } from './client/explore';
|
||||
import { Notifications, Inbox, Invites } from './client/inbox';
|
||||
@@ -160,7 +160,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
}
|
||||
>
|
||||
{mobile ? null : <Route index element={<WelcomePage />} />}
|
||||
<Route path={_CREATE_PATH} element={<p>create</p>} />
|
||||
<Route path={_CREATE_PATH} element={<DirectCreate />} />
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
|
||||
@@ -15,7 +15,7 @@ export function AuthFooter() {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
v4.0.3
|
||||
v4.1.0
|
||||
</Text>
|
||||
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
|
||||
Twitter
|
||||
|
||||
@@ -29,6 +29,7 @@ import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo';
|
||||
import { AuthFlowsLoader } from '../../components/AuthFlowsLoader';
|
||||
import { AuthFlowsProvider } from '../../hooks/useAuthFlows';
|
||||
import { AuthServerProvider } from '../../hooks/useAuthServer';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
|
||||
const currentAuthPath = (pathname: string): string => {
|
||||
if (matchPath(LOGIN_PATH, pathname)) {
|
||||
@@ -72,7 +73,7 @@ export function AuthLayout() {
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
const defaultServer = clientDefaultServer(clientConfig);
|
||||
let server: string = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer;
|
||||
let server: string = urlEncodedServer ? tryDecodeURIComponent(urlEncodedServer) : defaultServer;
|
||||
|
||||
if (!clientAllowedServer(clientConfig, server)) {
|
||||
server = defaultServer;
|
||||
@@ -94,7 +95,7 @@ export function AuthLayout() {
|
||||
|
||||
// if server is mismatches with path server, update path
|
||||
useEffect(() => {
|
||||
if (!urlEncodedServer || decodeURIComponent(urlEncodedServer) !== server) {
|
||||
if (!urlEncodedServer || tryDecodeURIComponent(urlEncodedServer) !== server) {
|
||||
navigate(
|
||||
generatePath(currentAuthPath(location.pathname), {
|
||||
server: encodeURIComponent(server),
|
||||
|
||||
@@ -183,17 +183,17 @@ function MessageNotifications() {
|
||||
removed,
|
||||
data
|
||||
) => {
|
||||
if (mx.getSyncState() !== 'SYNCING') return;
|
||||
if (document.hasFocus() && (selectedRoomId === room?.roomId || notificationSelected)) return;
|
||||
if (
|
||||
mx.getSyncState() !== 'SYNCING' ||
|
||||
selectedRoomId === room?.roomId ||
|
||||
notificationSelected ||
|
||||
!room ||
|
||||
!data.liveEvent ||
|
||||
room.isSpaceRoom() ||
|
||||
!isNotificationEvent(mEvent) ||
|
||||
getNotificationType(mx, room.roomId) === NotificationType.Mute
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sender = mEvent.getSender();
|
||||
const eventId = mEvent.getId();
|
||||
|
||||
@@ -10,7 +10,15 @@ import {
|
||||
SidebarItemTooltip,
|
||||
SidebarItem,
|
||||
} from '../../components/sidebar';
|
||||
import { DirectTab, HomeTab, SpaceTabs, InboxTab, ExploreTab, UserTab } from './sidebar';
|
||||
import {
|
||||
DirectTab,
|
||||
HomeTab,
|
||||
SpaceTabs,
|
||||
InboxTab,
|
||||
ExploreTab,
|
||||
UserTab,
|
||||
UnverifiedTab,
|
||||
} from './sidebar';
|
||||
import { openCreateRoom, openSearch } from '../../../client/action/navigation';
|
||||
|
||||
export function SidebarNav() {
|
||||
@@ -65,6 +73,8 @@ export function SidebarNav() {
|
||||
</SidebarItemTooltip>
|
||||
</SidebarItem>
|
||||
|
||||
<UnverifiedTab />
|
||||
|
||||
<InboxTab />
|
||||
<UserTab />
|
||||
</SidebarStack>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function WelcomePage() {
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
v4.0.3
|
||||
v4.1.0
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
|
||||
33
src/app/pages/client/direct/DirectCreate.tsx
Normal file
33
src/app/pages/client/direct/DirectCreate.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { WelcomePage } from '../WelcomePage';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { getDirectCreateSearchParams } from '../../pathSearchParam';
|
||||
import { getDirectPath, getDirectRoomPath } from '../../pathUtils';
|
||||
import { getDMRoomFor } from '../../../utils/matrix';
|
||||
import { openInviteUser } from '../../../../client/action/navigation';
|
||||
import { useDirectRooms } from './useDirectRooms';
|
||||
|
||||
export function DirectCreate() {
|
||||
const mx = useMatrixClient();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { userId } = getDirectCreateSearchParams(searchParams);
|
||||
const directs = useDirectRooms();
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
const room = getDMRoomFor(mx, userId);
|
||||
const { roomId } = room ?? {};
|
||||
if (roomId && directs.includes(roomId)) {
|
||||
navigate(getDirectRoomPath(roomId), { replace: true });
|
||||
} else {
|
||||
openInviteUser(undefined, userId);
|
||||
}
|
||||
} else {
|
||||
navigate(getDirectPath(), { replace: true });
|
||||
}
|
||||
}, [mx, navigate, directs, userId]);
|
||||
|
||||
return <WelcomePage />;
|
||||
}
|
||||
@@ -10,12 +10,12 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||
const mx = useMatrixClient();
|
||||
const rooms = useDirectRooms();
|
||||
|
||||
const { roomIdOrAlias } = useParams();
|
||||
const { roomIdOrAlias, eventId } = useParams();
|
||||
const roomId = useSelectedRoom();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
if (!room || !rooms.includes(room.roomId)) {
|
||||
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />;
|
||||
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} eventId={eventId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './Direct';
|
||||
export * from './RoomProvider';
|
||||
export * from './DirectCreate';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Icon, Icons, Scroll, Text } from 'folds';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
import { RoomCard, RoomCardGrid } from '../../../components/room-card';
|
||||
@@ -9,21 +9,38 @@ import {
|
||||
Page,
|
||||
PageContent,
|
||||
PageContentCenter,
|
||||
PageHeader,
|
||||
PageHero,
|
||||
PageHeroSection,
|
||||
} from '../../../components/page';
|
||||
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
|
||||
import * as css from './style.css';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
export function FeaturedRooms() {
|
||||
const { featuredCommunities } = useClientConfig();
|
||||
const { rooms, spaces } = featuredCommunities ?? {};
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const screenSize = useScreenSizeContext();
|
||||
const { navigateSpace, navigateRoom } = useRoomNavigate();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<PageHeader>
|
||||
<Box shrink="No">
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
)}
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Button,
|
||||
Chip,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Line,
|
||||
@@ -42,6 +43,8 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { getMxIdServer } from '../../../utils/matrix';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
|
||||
useMemo(
|
||||
@@ -344,6 +347,7 @@ export function PublicRooms() {
|
||||
const userServer = userId && getMxIdServer(userId);
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const { navigateSpace, navigateRoom } = useRoomNavigate();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const serverSearchParams = useServerSearchParams(searchParams);
|
||||
@@ -466,7 +470,7 @@ export function PublicRooms() {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageHeader balance>
|
||||
{isSearch ? (
|
||||
<>
|
||||
<Box grow="Yes" basis="No">
|
||||
@@ -482,20 +486,34 @@ export function PublicRooms() {
|
||||
</Box>
|
||||
|
||||
<Box grow="No" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Search} />
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
|
||||
<Text size="H3" truncate>
|
||||
Search
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" />
|
||||
<Box grow="Yes" basis="No" />
|
||||
</>
|
||||
) : (
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Category} />
|
||||
<Text size="H3" truncate>
|
||||
{server}
|
||||
</Text>
|
||||
</Box>
|
||||
<>
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Category} />}
|
||||
<Text size="H3" truncate>
|
||||
{server}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</>
|
||||
)}
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
|
||||
@@ -5,17 +5,25 @@ import { RoomProvider } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useHomeRooms } from './useHomeRooms';
|
||||
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
||||
|
||||
export function HomeRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||
const mx = useMatrixClient();
|
||||
const rooms = useHomeRooms();
|
||||
|
||||
const { roomIdOrAlias } = useParams();
|
||||
const { roomIdOrAlias, eventId } = useParams();
|
||||
const viaServers = useSearchParamsViaServers();
|
||||
const roomId = useSelectedRoom();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
if (!room || !rooms.includes(room.roomId)) {
|
||||
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />;
|
||||
return (
|
||||
<JoinBeforeNavigate
|
||||
roomIdOrAlias={roomIdOrAlias!}
|
||||
eventId={eventId}
|
||||
viaServers={viaServers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Box, Icon, Icons, Text, Scroll } from 'folds';
|
||||
import { Box, Icon, Icons, Text, Scroll, IconButton } from 'folds';
|
||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
|
||||
import { MessageSearch } from '../../../features/message-search';
|
||||
import { useHomeRooms } from './useHomeRooms';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
export function HomeSearch() {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const rooms = useHomeRooms();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Search} />
|
||||
<Text size="H3" truncate>
|
||||
Message Search
|
||||
</Text>
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
|
||||
<Text size="H3" truncate>
|
||||
Message Search
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box style={{ position: 'relative' }} grow="Yes">
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
@@ -39,6 +40,8 @@ import { RoomTopicViewer } from '../../../components/room-topic-viewer';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { useRoomTopic } from '../../../hooks/useRoomMeta';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
const COMPACT_CARD_WIDTH = 548;
|
||||
|
||||
@@ -205,6 +208,7 @@ export function Invites() {
|
||||
useCallback(() => containerRef.current, []),
|
||||
useCallback((width) => setCompact(width <= COMPACT_CARD_WIDTH), [])
|
||||
);
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
|
||||
@@ -225,12 +229,26 @@ export function Invites() {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Mail} />
|
||||
<Text size="H3" truncate>
|
||||
Invitations
|
||||
</Text>
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Mail} />}
|
||||
<Text size="H3" truncate>
|
||||
Invitations
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
|
||||
@@ -24,9 +24,10 @@ import {
|
||||
} from 'matrix-js-sdk';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { HTMLReactParserOptions } from 'html-react-parser';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { getMxIdLocalPart, isRoomId, isUserId } from '../../../utils/matrix';
|
||||
import { getMxIdLocalPart } from '../../../utils/matrix';
|
||||
import { InboxNotificationsPathSearchParams } from '../../paths';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
@@ -52,8 +53,13 @@ import {
|
||||
Username,
|
||||
} from '../../../components/message';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import { getReactCustomHtmlParser } from '../../../plugins/react-custom-html-parser';
|
||||
import { openJoinAlias, openProfileViewer } from '../../../../client/action/navigation';
|
||||
import {
|
||||
factoryRenderLinkifyWithMention,
|
||||
getReactCustomHtmlParser,
|
||||
LINKIFY_OPTS,
|
||||
makeMentionCustomProps,
|
||||
renderMatrixMention,
|
||||
} from '../../../plugins/react-custom-html-parser';
|
||||
import { RenderMessageContent } from '../../../components/RenderMessageContent';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
@@ -70,6 +76,10 @@ import { ContainerColor } from '../../../styles/ContainerColor.css';
|
||||
import { VirtualTile } from '../../../components/virtualizer';
|
||||
import { UserAvatar } from '../../../components/user-avatar';
|
||||
import { EncryptedContent } from '../../../features/room/message';
|
||||
import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
type RoomNotificationsGroup = {
|
||||
roomId: string;
|
||||
@@ -181,36 +191,26 @@ function RoomNotificationsGroupComp({
|
||||
}: RoomNotificationsGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const mentionClickHandler = useMentionClickHandler(room.roomId);
|
||||
const spoilerClickHandler = useSpoilerClickHandler();
|
||||
|
||||
const linkifyOpts = useMemo<LinkifyOpts>(
|
||||
() => ({
|
||||
...LINKIFY_OPTS,
|
||||
render: factoryRenderLinkifyWithMention((href) =>
|
||||
renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler))
|
||||
),
|
||||
}),
|
||||
[mx, room, mentionClickHandler]
|
||||
);
|
||||
const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
|
||||
() =>
|
||||
getReactCustomHtmlParser(mx, room, {
|
||||
handleSpoilerClick: (evt) => {
|
||||
const target = evt.currentTarget;
|
||||
if (target.getAttribute('aria-pressed') === 'true') {
|
||||
evt.stopPropagation();
|
||||
target.setAttribute('aria-pressed', 'false');
|
||||
target.style.cursor = 'initial';
|
||||
}
|
||||
},
|
||||
handleMentionClick: (evt) => {
|
||||
const target = evt.currentTarget;
|
||||
const mentionId = target.getAttribute('data-mention-id');
|
||||
if (typeof mentionId !== 'string') return;
|
||||
if (isUserId(mentionId)) {
|
||||
openProfileViewer(mentionId, room.roomId);
|
||||
return;
|
||||
}
|
||||
if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
|
||||
if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
|
||||
else navigateRoom(mentionId);
|
||||
return;
|
||||
}
|
||||
openJoinAlias(mentionId);
|
||||
},
|
||||
getReactCustomHtmlParser(mx, room.roomId, {
|
||||
linkifyOpts,
|
||||
handleSpoilerClick: spoilerClickHandler,
|
||||
handleMentionClick: mentionClickHandler,
|
||||
}),
|
||||
[mx, room, navigateRoom, navigateSpace]
|
||||
[mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler]
|
||||
);
|
||||
|
||||
const renderMatrixEvent = useMatrixEventRenderer<[IRoomEvent, string, GetContentCallback]>(
|
||||
@@ -229,6 +229,7 @@ function RoomNotificationsGroupComp({
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
outlineAttachment
|
||||
/>
|
||||
);
|
||||
@@ -287,6 +288,7 @@ function RoomNotificationsGroupComp({
|
||||
mediaAutoLoad={mediaAutoLoad}
|
||||
urlPreview={urlPreview}
|
||||
htmlReactParserOptions={htmlReactParserOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -484,6 +486,7 @@ export function Notifications() {
|
||||
const mx = useMatrixClient();
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -549,12 +552,26 @@ export function Notifications() {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Message} />
|
||||
<Text size="H3" truncate>
|
||||
Notification Messages
|
||||
</Text>
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Message} />}
|
||||
<Text size="H3" truncate>
|
||||
Notification Messages
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
|
||||
|
||||
@@ -47,13 +47,7 @@ import {
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import {
|
||||
getOriginBaseUrl,
|
||||
getSpaceLobbyPath,
|
||||
getSpacePath,
|
||||
joinPathComponent,
|
||||
withOriginBaseUrl,
|
||||
} from '../../pathUtils';
|
||||
import { getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils';
|
||||
import {
|
||||
SidebarAvatar,
|
||||
SidebarItem,
|
||||
@@ -67,7 +61,7 @@ import {
|
||||
import { RoomUnreadProvider, RoomsUnreadProvider } from '../../../components/RoomUnreadProvider';
|
||||
import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
|
||||
import { UnreadBadge } from '../../../components/unread-badge';
|
||||
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
||||
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
|
||||
import { RoomAvatar } from '../../../components/room-avatar';
|
||||
import { nameInitials, randomStr } from '../../../utils/common';
|
||||
import {
|
||||
@@ -83,7 +77,6 @@ import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
|
||||
import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||
@@ -91,6 +84,8 @@ import { markAsRead } from '../../../../client/action/notifications';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { openInviteUser, openSpaceSettings } from '../../../../client/action/navigation';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
||||
import { getViaServers } from '../../../plugins/via-servers';
|
||||
|
||||
type SpaceMenuProps = {
|
||||
room: Room;
|
||||
@@ -100,7 +95,6 @@ type SpaceMenuProps = {
|
||||
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||
({ room, requestClose, onUnpin }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const { hashRouter } = useClientConfig();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
@@ -124,8 +118,9 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, room.roomId));
|
||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), spacePath));
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
|
||||
24
src/app/pages/client/sidebar/UnverifiedTab.css.ts
Normal file
24
src/app/pages/client/sidebar/UnverifiedTab.css.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
import { color, toRem } from 'folds';
|
||||
|
||||
const pushRight = keyframes({
|
||||
from: {
|
||||
transform: `translateX(${toRem(2)}) scale(1)`,
|
||||
},
|
||||
to: {
|
||||
transform: 'translateX(0) scale(1)',
|
||||
},
|
||||
});
|
||||
|
||||
export const UnverifiedTab = style({
|
||||
animationName: pushRight,
|
||||
animationDuration: '400ms',
|
||||
animationIterationCount: 30,
|
||||
animationDirection: 'alternate',
|
||||
});
|
||||
|
||||
export const UnverifiedAvatar = style({
|
||||
backgroundColor: color.Critical.Container,
|
||||
color: color.Critical.OnContainer,
|
||||
borderColor: color.Critical.ContainerLine,
|
||||
});
|
||||
49
src/app/pages/client/sidebar/UnverifiedTab.tsx
Normal file
49
src/app/pages/client/sidebar/UnverifiedTab.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Badge, color, Icon, Icons, Text } from 'folds';
|
||||
import { openSettings } from '../../../../client/action/navigation';
|
||||
import { isCrossVerified } from '../../../../util/matrixUtil';
|
||||
import {
|
||||
SidebarAvatar,
|
||||
SidebarItem,
|
||||
SidebarItemBadge,
|
||||
SidebarItemTooltip,
|
||||
} from '../../../components/sidebar';
|
||||
import { useDeviceList } from '../../../hooks/useDeviceList';
|
||||
import { tabText } from '../../../organisms/settings/Settings';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import * as css from './UnverifiedTab.css';
|
||||
|
||||
export function UnverifiedTab() {
|
||||
const mx = useMatrixClient();
|
||||
const deviceList = useDeviceList();
|
||||
const unverified = deviceList?.filter(
|
||||
(device) => isCrossVerified(mx, device.device_id) === false
|
||||
);
|
||||
|
||||
if (!unverified?.length) return null;
|
||||
|
||||
return (
|
||||
<SidebarItem className={css.UnverifiedTab}>
|
||||
<SidebarItemTooltip tooltip="Unverified Sessions">
|
||||
{(triggerRef) => (
|
||||
<SidebarAvatar
|
||||
className={css.UnverifiedAvatar}
|
||||
as="button"
|
||||
ref={triggerRef}
|
||||
outlined
|
||||
onClick={() => openSettings(tabText.SECURITY)}
|
||||
>
|
||||
<Icon style={{ color: color.Critical.Main }} src={Icons.ShieldUser} />
|
||||
</SidebarAvatar>
|
||||
)}
|
||||
</SidebarItemTooltip>
|
||||
<SidebarItemBadge hasCount>
|
||||
<Badge variant="Critical" size="400" fill="Solid" radii="Pill" outlined={false}>
|
||||
<Text as="span" size="L400">
|
||||
{unverified.length}
|
||||
</Text>
|
||||
</Badge>
|
||||
</SidebarItemBadge>
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './SpaceTabs';
|
||||
export * from './InboxTab';
|
||||
export * from './ExploreTab';
|
||||
export * from './UserTab';
|
||||
export * from './UnverifiedTab';
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useSpace } from '../../../hooks/useSpace';
|
||||
import { getAllParents } from '../../../utils/room';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
||||
|
||||
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||
const mx = useMatrixClient();
|
||||
@@ -16,7 +17,8 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
|
||||
const { roomIdOrAlias } = useParams();
|
||||
const { roomIdOrAlias, eventId } = useParams();
|
||||
const viaServers = useSearchParamsViaServers();
|
||||
const roomId = useSelectedRoom();
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
@@ -26,7 +28,13 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||
!allRooms.includes(room.roomId) ||
|
||||
!getAllParents(roomToParents, room.roomId).has(space.roomId)
|
||||
) {
|
||||
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />;
|
||||
return (
|
||||
<JoinBeforeNavigate
|
||||
roomIdOrAlias={roomIdOrAlias!}
|
||||
eventId={eventId}
|
||||
viaServers={viaServers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Box, Icon, Icons, Text, Scroll } from 'folds';
|
||||
import { Box, Icon, Icons, Text, Scroll, IconButton } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
|
||||
import { MessageSearch } from '../../../features/message-search';
|
||||
@@ -9,11 +9,14 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { mDirectAtom } from '../../../state/mDirectList';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
export function SpaceSearch() {
|
||||
const mx = useMatrixClient();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const space = useSpace();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
@@ -25,12 +28,26 @@ export function SpaceSearch() {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Search} />
|
||||
<Text size="H3" truncate>
|
||||
Message Search
|
||||
</Text>
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
|
||||
<Text size="H3" truncate>
|
||||
Message Search
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box style={{ position: 'relative' }} grow="Yes">
|
||||
|
||||
@@ -34,15 +34,8 @@ import {
|
||||
NavItemContent,
|
||||
NavLink,
|
||||
} from '../../../components/nav';
|
||||
import {
|
||||
getOriginBaseUrl,
|
||||
getSpaceLobbyPath,
|
||||
getSpacePath,
|
||||
getSpaceRoomPath,
|
||||
getSpaceSearchPath,
|
||||
withOriginBaseUrl,
|
||||
} from '../../pathUtils';
|
||||
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
|
||||
import { getSpaceLobbyPath, getSpaceRoomPath, getSpaceSearchPath } from '../../pathUtils';
|
||||
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
|
||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||
import {
|
||||
useSpaceLobbySelected,
|
||||
@@ -69,11 +62,12 @@ import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
import { LeaveSpacePrompt } from '../../../components/leave-space-prompt';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
|
||||
import { useStateEvent } from '../../../hooks/useStateEvent';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
||||
import { getViaServers } from '../../../plugins/via-servers';
|
||||
|
||||
type SpaceMenuProps = {
|
||||
room: Room;
|
||||
@@ -81,7 +75,6 @@ type SpaceMenuProps = {
|
||||
};
|
||||
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const { hashRouter } = useClientConfig();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
@@ -100,8 +93,9 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, room.roomId));
|
||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), spacePath));
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
|
||||
import { SpaceProvider } from '../../../hooks/useSpace';
|
||||
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
|
||||
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
|
||||
|
||||
type RouteSpaceProviderProps = {
|
||||
children: ReactNode;
|
||||
@@ -13,13 +14,15 @@ type RouteSpaceProviderProps = {
|
||||
export function RouteSpaceProvider({ children }: RouteSpaceProviderProps) {
|
||||
const mx = useMatrixClient();
|
||||
const joinedSpaces = useSpaces(mx, allRoomsAtom);
|
||||
|
||||
const { spaceIdOrAlias } = useParams();
|
||||
const viaServers = useSearchParamsViaServers();
|
||||
|
||||
const selectedSpaceId = useSelectedSpace();
|
||||
const space = mx.getRoom(selectedSpaceId);
|
||||
|
||||
if (!space || !joinedSpaces.includes(space.roomId)) {
|
||||
return <JoinBeforeNavigate roomIdOrAlias={spaceIdOrAlias ?? ''} />;
|
||||
return <JoinBeforeNavigate roomIdOrAlias={spaceIdOrAlias ?? ''} viaServers={viaServers} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
13
src/app/pages/pathSearchParam.ts
Normal file
13
src/app/pages/pathSearchParam.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { _RoomSearchParams, DirectCreateSearchParams } from './paths';
|
||||
|
||||
type SearchParamsGetter<T> = (searchParams: URLSearchParams) => T;
|
||||
|
||||
export const getRoomSearchParams: SearchParamsGetter<_RoomSearchParams> = (searchParams) => ({
|
||||
viaServers: searchParams.get('viaServers') ?? undefined,
|
||||
});
|
||||
|
||||
export const getDirectCreateSearchParams: SearchParamsGetter<DirectCreateSearchParams> = (
|
||||
searchParams
|
||||
) => ({
|
||||
userId: searchParams.get('userId') ?? undefined,
|
||||
});
|
||||
@@ -35,6 +35,11 @@ export type _SearchPathSearchParams = {
|
||||
senders?: string;
|
||||
};
|
||||
export const _SEARCH_PATH = 'search/';
|
||||
|
||||
export type _RoomSearchParams = {
|
||||
/* comma separated string of servers */
|
||||
viaServers?: string;
|
||||
};
|
||||
export const _ROOM_PATH = ':roomIdOrAlias/:eventId?/';
|
||||
|
||||
export const HOME_PATH = '/home/';
|
||||
@@ -44,6 +49,9 @@ export const HOME_SEARCH_PATH = `/home/${_SEARCH_PATH}`;
|
||||
export const HOME_ROOM_PATH = `/home/${_ROOM_PATH}`;
|
||||
|
||||
export const DIRECT_PATH = '/direct/';
|
||||
export type DirectCreateSearchParams = {
|
||||
userId?: string;
|
||||
};
|
||||
export const DIRECT_CREATE_PATH = `/direct/${_CREATE_PATH}`;
|
||||
export const DIRECT_ROOM_PATH = `/direct/${_ROOM_PATH}`;
|
||||
|
||||
|
||||
84
src/app/plugins/matrix-to.ts
Normal file
84
src/app/plugins/matrix-to.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
const MATRIX_TO_BASE = 'https://matrix.to';
|
||||
|
||||
export const getMatrixToUser = (userId: string): string => `${MATRIX_TO_BASE}/#/${userId}`;
|
||||
|
||||
const withViaServers = (fragment: string, viaServers: string[]): string =>
|
||||
`${fragment}?${viaServers.map((server) => `via=${server}`).join('&')}`;
|
||||
|
||||
export const getMatrixToRoom = (roomIdOrAlias: string, viaServers?: string[]): string => {
|
||||
let fragment = roomIdOrAlias;
|
||||
|
||||
if (Array.isArray(viaServers) && viaServers.length > 0) {
|
||||
fragment = withViaServers(fragment, viaServers);
|
||||
}
|
||||
|
||||
return `${MATRIX_TO_BASE}/#/${fragment}`;
|
||||
};
|
||||
|
||||
export const getMatrixToRoomEvent = (
|
||||
roomIdOrAlias: string,
|
||||
eventId: string,
|
||||
viaServers?: string[]
|
||||
): string => {
|
||||
let fragment = `${roomIdOrAlias}/${eventId}`;
|
||||
|
||||
if (Array.isArray(viaServers) && viaServers.length > 0) {
|
||||
fragment = withViaServers(fragment, viaServers);
|
||||
}
|
||||
|
||||
return `${MATRIX_TO_BASE}/#/${fragment}`;
|
||||
};
|
||||
|
||||
export type MatrixToRoom = {
|
||||
roomIdOrAlias: string;
|
||||
viaServers?: string[];
|
||||
};
|
||||
|
||||
export type MatrixToRoomEvent = MatrixToRoom & {
|
||||
eventId: string;
|
||||
};
|
||||
|
||||
const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/;
|
||||
export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href);
|
||||
|
||||
const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/;
|
||||
const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/?(\?[\S]*)?$/;
|
||||
const MATRIX_TO_ROOM_EVENT =
|
||||
/^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/;
|
||||
|
||||
export const parseMatrixToUser = (href: string): string | undefined => {
|
||||
const match = href.match(MATRIX_TO_USER);
|
||||
if (!match) return undefined;
|
||||
const userId = match[1];
|
||||
return userId;
|
||||
};
|
||||
|
||||
export const parseMatrixToRoom = (href: string): MatrixToRoom | undefined => {
|
||||
const match = href.match(MATRIX_TO_ROOM);
|
||||
if (!match) return undefined;
|
||||
|
||||
const roomIdOrAlias = match[1];
|
||||
const viaSearchStr = match[2];
|
||||
const viaServers = new URLSearchParams(viaSearchStr).getAll('via');
|
||||
|
||||
return {
|
||||
roomIdOrAlias,
|
||||
viaServers: viaServers.length === 0 ? undefined : viaServers,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseMatrixToRoomEvent = (href: string): MatrixToRoomEvent | undefined => {
|
||||
const match = href.match(MATRIX_TO_ROOM_EVENT);
|
||||
if (!match) return undefined;
|
||||
|
||||
const roomIdOrAlias = match[1];
|
||||
const eventId = match[2];
|
||||
const viaSearchStr = match[3];
|
||||
const viaServers = new URLSearchParams(viaSearchStr).getAll('via');
|
||||
|
||||
return {
|
||||
roomIdOrAlias,
|
||||
eventId,
|
||||
viaServers: viaServers.length === 0 ? undefined : viaServers,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import React, { ReactEventHandler, Suspense, lazy } from 'react';
|
||||
import React, { ComponentPropsWithoutRef, ReactEventHandler, Suspense, lazy } from 'react';
|
||||
import {
|
||||
Element,
|
||||
Text as DOMText,
|
||||
@@ -7,18 +7,26 @@ import {
|
||||
attributesToProps,
|
||||
domToReact,
|
||||
} from 'html-react-parser';
|
||||
import { MatrixClient, Room } from 'matrix-js-sdk';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import { Scroll, Text } from 'folds';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs';
|
||||
import Linkify from 'linkify-react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import * as css from '../styles/CustomHtml.css';
|
||||
import { getMxIdLocalPart, getCanonicalAliasRoomId } from '../utils/matrix';
|
||||
import { getMxIdLocalPart, getCanonicalAliasRoomId, isRoomAlias } from '../utils/matrix';
|
||||
import { getMemberDisplayName } from '../utils/room';
|
||||
import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex';
|
||||
import { getHexcodeForEmoji, getShortcodeFor } from './emoji';
|
||||
import { findAndReplace } from '../utils/findAndReplace';
|
||||
import {
|
||||
parseMatrixToRoom,
|
||||
parseMatrixToRoomEvent,
|
||||
parseMatrixToUser,
|
||||
testMatrixTo,
|
||||
} from './matrix-to';
|
||||
import { onEnterOrSpace } from '../utils/keyboard';
|
||||
import { tryDecodeURIComponent } from '../utils/dom';
|
||||
|
||||
const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
|
||||
|
||||
@@ -35,6 +43,108 @@ export const LINKIFY_OPTS: LinkifyOpts = {
|
||||
ignoreTags: ['span'],
|
||||
};
|
||||
|
||||
export const makeMentionCustomProps = (
|
||||
handleMentionClick?: ReactEventHandler<HTMLElement>
|
||||
): ComponentPropsWithoutRef<'a'> => ({
|
||||
style: { cursor: 'pointer' },
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
role: 'link',
|
||||
tabIndex: handleMentionClick ? 0 : -1,
|
||||
onKeyDown: handleMentionClick ? onEnterOrSpace(handleMentionClick) : undefined,
|
||||
onClick: handleMentionClick,
|
||||
});
|
||||
|
||||
export const renderMatrixMention = (
|
||||
mx: MatrixClient,
|
||||
currentRoomId: string | undefined,
|
||||
href: string,
|
||||
customProps: ComponentPropsWithoutRef<'a'>
|
||||
) => {
|
||||
const userId = parseMatrixToUser(href);
|
||||
if (userId) {
|
||||
const currentRoom = mx.getRoom(currentRoomId);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...customProps}
|
||||
className={css.Mention({ highlight: mx.getUserId() === userId })}
|
||||
data-mention-id={userId}
|
||||
>
|
||||
{`@${
|
||||
(currentRoom && getMemberDisplayName(currentRoom, userId)) ?? getMxIdLocalPart(userId)
|
||||
}`}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const matrixToRoom = parseMatrixToRoom(href);
|
||||
if (matrixToRoom) {
|
||||
const { roomIdOrAlias, viaServers } = matrixToRoom;
|
||||
const mentionRoom = mx.getRoom(
|
||||
isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
|
||||
);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...customProps}
|
||||
className={css.Mention({
|
||||
highlight: currentRoomId === (mentionRoom?.roomId ?? roomIdOrAlias),
|
||||
})}
|
||||
data-mention-id={mentionRoom?.roomId ?? roomIdOrAlias}
|
||||
data-mention-via={viaServers?.join(',')}
|
||||
>
|
||||
{mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const matrixToRoomEvent = parseMatrixToRoomEvent(href);
|
||||
if (matrixToRoomEvent) {
|
||||
const { roomIdOrAlias, eventId, viaServers } = matrixToRoomEvent;
|
||||
const mentionRoom = mx.getRoom(
|
||||
isRoomAlias(roomIdOrAlias) ? getCanonicalAliasRoomId(mx, roomIdOrAlias) : roomIdOrAlias
|
||||
);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...customProps}
|
||||
className={css.Mention({
|
||||
highlight: currentRoomId === (mentionRoom?.roomId ?? roomIdOrAlias),
|
||||
})}
|
||||
data-mention-id={mentionRoom?.roomId ?? roomIdOrAlias}
|
||||
data-mention-event-id={eventId}
|
||||
data-mention-via={viaServers?.join(',')}
|
||||
>
|
||||
Message: {mentionRoom ? `#${mentionRoom.name}` : roomIdOrAlias}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const factoryRenderLinkifyWithMention = (
|
||||
mentionRender: (href: string) => JSX.Element | undefined
|
||||
): OptFn<(ir: IntermediateRepresentation) => any> => {
|
||||
const render: OptFn<(ir: IntermediateRepresentation) => any> = ({
|
||||
tagName,
|
||||
attributes,
|
||||
content,
|
||||
}) => {
|
||||
if (tagName === 'a' && testMatrixTo(tryDecodeURIComponent(attributes.href))) {
|
||||
const mention = mentionRender(tryDecodeURIComponent(attributes.href));
|
||||
if (mention) return mention;
|
||||
}
|
||||
|
||||
return <a {...attributes}>{content}</a>;
|
||||
};
|
||||
return render;
|
||||
};
|
||||
|
||||
export const scaleSystemEmoji = (text: string): (string | JSX.Element)[] =>
|
||||
findAndReplace(
|
||||
text,
|
||||
@@ -76,8 +186,9 @@ export const highlightText = (
|
||||
|
||||
export const getReactCustomHtmlParser = (
|
||||
mx: MatrixClient,
|
||||
room: Room,
|
||||
roomId: string | undefined,
|
||||
params: {
|
||||
linkifyOpts: LinkifyOpts;
|
||||
highlightRegex?: RegExp;
|
||||
handleSpoilerClick?: ReactEventHandler<HTMLElement>;
|
||||
handleMentionClick?: ReactEventHandler<HTMLElement>;
|
||||
@@ -215,54 +326,14 @@ export const getReactCustomHtmlParser = (
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'a') {
|
||||
const mention = decodeURIComponent(props.href).match(
|
||||
/^https?:\/\/matrix.to\/#\/((@|#|!).+:[^?/]+)/
|
||||
if (name === 'a' && testMatrixTo(tryDecodeURIComponent(props.href))) {
|
||||
const mention = renderMatrixMention(
|
||||
mx,
|
||||
roomId,
|
||||
tryDecodeURIComponent(props.href),
|
||||
makeMentionCustomProps(params.handleMentionClick)
|
||||
);
|
||||
if (mention) {
|
||||
// convert mention link to pill
|
||||
const mentionId = mention[1];
|
||||
const mentionPrefix = mention[2];
|
||||
if (mentionPrefix === '#' || mentionPrefix === '!') {
|
||||
const mentionRoom = mx.getRoom(
|
||||
mentionPrefix === '#' ? getCanonicalAliasRoomId(mx, mentionId) : mentionId
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
{...props}
|
||||
className={css.Mention({
|
||||
highlight: room.roomId === (mentionRoom?.roomId ?? mentionId),
|
||||
})}
|
||||
data-mention-id={mentionRoom?.roomId ?? mentionId}
|
||||
data-mention-href={props.href}
|
||||
role="button"
|
||||
tabIndex={params.handleMentionClick ? 0 : -1}
|
||||
onKeyDown={params.handleMentionClick}
|
||||
onClick={params.handleMentionClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{domToReact(children, opts)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (mentionPrefix === '@')
|
||||
return (
|
||||
<span
|
||||
{...props}
|
||||
className={css.Mention({ highlight: mx.getUserId() === mentionId })}
|
||||
data-mention-id={mentionId}
|
||||
data-mention-href={props.href}
|
||||
role="button"
|
||||
tabIndex={params.handleMentionClick ? 0 : -1}
|
||||
onKeyDown={params.handleMentionClick}
|
||||
onClick={params.handleMentionClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{`@${getMemberDisplayName(room, mentionId) ?? getMxIdLocalPart(mentionId)}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (mention) return mention;
|
||||
}
|
||||
|
||||
if (name === 'span' && 'data-mx-spoiler' in props) {
|
||||
@@ -316,7 +387,7 @@ export const getReactCustomHtmlParser = (
|
||||
}
|
||||
|
||||
if (linkify) {
|
||||
return <Linkify options={LINKIFY_OPTS}>{jsx}</Linkify>;
|
||||
return <Linkify options={params.linkifyOpts}>{jsx}</Linkify>;
|
||||
}
|
||||
return jsx;
|
||||
}
|
||||
|
||||
65
src/app/plugins/via-servers.ts
Normal file
65
src/app/plugins/via-servers.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { IPowerLevels } from '../hooks/usePowerLevels';
|
||||
import { getMxIdServer } from '../utils/matrix';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { getStateEvent } from '../utils/room';
|
||||
|
||||
export const getViaServers = (room: Room): string[] => {
|
||||
const getHighestPowerUserId = (): string | undefined => {
|
||||
const powerLevels = getStateEvent(room, StateEvent.RoomPowerLevels)?.getContent<IPowerLevels>();
|
||||
|
||||
if (!powerLevels) return undefined;
|
||||
const userIdToPower = powerLevels.users;
|
||||
if (!userIdToPower) return undefined;
|
||||
let powerUserId: string | undefined;
|
||||
|
||||
Object.keys(userIdToPower).forEach((userId) => {
|
||||
if (userIdToPower[userId] <= (powerLevels.users_default ?? 0)) return;
|
||||
|
||||
if (!powerUserId) {
|
||||
powerUserId = userId;
|
||||
return;
|
||||
}
|
||||
if (userIdToPower[userId] > userIdToPower[powerUserId]) {
|
||||
powerUserId = userId;
|
||||
}
|
||||
});
|
||||
return powerUserId;
|
||||
};
|
||||
|
||||
const getServerToPopulation = (): Record<string, number> => {
|
||||
const members = room.getMembers();
|
||||
const serverToPop: Record<string, number> = {};
|
||||
|
||||
members?.forEach((member) => {
|
||||
const { userId } = member;
|
||||
const server = getMxIdServer(userId);
|
||||
if (!server) return;
|
||||
const serverPop = serverToPop[server];
|
||||
if (serverPop === undefined) {
|
||||
serverToPop[server] = 1;
|
||||
return;
|
||||
}
|
||||
serverToPop[server] = serverPop + 1;
|
||||
});
|
||||
|
||||
return serverToPop;
|
||||
};
|
||||
|
||||
const via: string[] = [];
|
||||
const userId = getHighestPowerUserId();
|
||||
if (userId) {
|
||||
const server = getMxIdServer(userId);
|
||||
if (server) via.push(server);
|
||||
}
|
||||
const serverToPop = getServerToPopulation();
|
||||
const sortedServers = Object.keys(serverToPop).sort(
|
||||
(svrA, svrB) => serverToPop[svrB] - serverToPop[svrA]
|
||||
);
|
||||
const mostPop3 = sortedServers.slice(0, 3);
|
||||
if (via.length === 0) return mostPop3;
|
||||
if (mostPop3.includes(via[0])) {
|
||||
mostPop3.splice(mostPop3.indexOf(via[0]), 1);
|
||||
}
|
||||
return via.concat(mostPop3.slice(0, 2));
|
||||
};
|
||||
@@ -83,7 +83,7 @@ export const useBindRoomToParentsAtom = (
|
||||
};
|
||||
|
||||
const handleMembershipChange = (room: Room, membership: string) => {
|
||||
if (room.getMyMembership() === Membership.Leave) {
|
||||
if (isSpace(room) && room.getMyMembership() === Membership.Leave) {
|
||||
setRoomToParents({ type: 'DELETE', roomId: room.roomId });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -196,3 +196,11 @@ export const setFavicon = (url: string): void => {
|
||||
if (!favicon) return;
|
||||
favicon.setAttribute('href', url);
|
||||
};
|
||||
|
||||
export const tryDecodeURIComponent = (encodedURIComponent: string): string => {
|
||||
try {
|
||||
return decodeURIComponent(encodedURIComponent);
|
||||
} catch {
|
||||
return encodedURIComponent;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,12 +24,14 @@ export const preventScrollWithArrowKey: KeyboardEventHandler = (evt) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const onEnterOrSpace = (callback: () => void) => (evt: KeyboardEventLike) => {
|
||||
if (isKeyHotkey('enter', evt) || isKeyHotkey('space', evt)) {
|
||||
evt.preventDefault();
|
||||
callback();
|
||||
}
|
||||
};
|
||||
export const onEnterOrSpace =
|
||||
<T>(callback: (evt: T) => void) =>
|
||||
(evt: KeyboardEventLike) => {
|
||||
if (isKeyHotkey('enter', evt) || isKeyHotkey('space', evt)) {
|
||||
evt.preventDefault();
|
||||
callback(evt as T);
|
||||
}
|
||||
};
|
||||
|
||||
export const stopPropagation = (evt: KeyboardEvent): boolean => {
|
||||
evt.stopPropagation();
|
||||
|
||||
@@ -32,17 +32,14 @@ export const isRoomId = (id: string): boolean => validMxId(id) && id.startsWith(
|
||||
|
||||
export const isRoomAlias = (id: string): boolean => validMxId(id) && id.startsWith('#');
|
||||
|
||||
export const parseMatrixToUrl = (url: string): [string | undefined, string | undefined] => {
|
||||
const href = decodeURIComponent(url);
|
||||
|
||||
const match = href.match(/^https?:\/\/matrix.to\/#\/([@!$+#]\S+:[^\\?|^\s|^\\/]+)(\?(via=\S+))?/);
|
||||
if (!match) return [undefined, undefined];
|
||||
const [, g1AsMxId, , g3AsVia] = match;
|
||||
return [g1AsMxId, g3AsVia];
|
||||
};
|
||||
|
||||
export const getCanonicalAliasRoomId = (mx: MatrixClient, alias: string): string | undefined =>
|
||||
mx.getRooms()?.find((room) => room.getCanonicalAlias() === alias)?.roomId;
|
||||
mx
|
||||
.getRooms()
|
||||
?.find(
|
||||
(room) =>
|
||||
room.getCanonicalAlias() === alias &&
|
||||
getStateEvent(room, StateEvent.RoomTombstone) === undefined
|
||||
)?.roomId;
|
||||
|
||||
export const getCanonicalAliasOrRoomId = (mx: MatrixClient, roomId: string): string => {
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const cons = {
|
||||
version: '4.0.3',
|
||||
version: '4.1.0',
|
||||
secretKey: {
|
||||
ACCESS_TOKEN: 'cinny_access_token',
|
||||
DEVICE_ID: 'cinny_device_id',
|
||||
|
||||
@@ -95,74 +95,18 @@ export function joinRuleToIconSrc(joinRule, isSpace) {
|
||||
}[joinRule]?.() || null);
|
||||
}
|
||||
|
||||
// NOTE: it gives userId with minimum power level 50;
|
||||
function getHighestPowerUserId(room) {
|
||||
const userIdToPower = room.currentState.getStateEvents('m.room.power_levels', '')?.getContent().users;
|
||||
let powerUserId = null;
|
||||
if (!userIdToPower) return powerUserId;
|
||||
|
||||
Object.keys(userIdToPower).forEach((userId) => {
|
||||
if (userIdToPower[userId] < 50) return;
|
||||
if (powerUserId === null) {
|
||||
powerUserId = userId;
|
||||
return;
|
||||
}
|
||||
if (userIdToPower[userId] > userIdToPower[powerUserId]) {
|
||||
powerUserId = userId;
|
||||
}
|
||||
});
|
||||
return powerUserId;
|
||||
}
|
||||
|
||||
export function getIdServer(userId) {
|
||||
const idParts = userId.split(':');
|
||||
return idParts[1];
|
||||
}
|
||||
|
||||
export function getServerToPopulation(room) {
|
||||
const members = room.getMembers();
|
||||
const serverToPop = {};
|
||||
|
||||
members?.forEach((member) => {
|
||||
const { userId } = member;
|
||||
const server = getIdServer(userId);
|
||||
const serverPop = serverToPop[server];
|
||||
if (serverPop === undefined) {
|
||||
serverToPop[server] = 1;
|
||||
return;
|
||||
}
|
||||
serverToPop[server] = serverPop + 1;
|
||||
});
|
||||
|
||||
return serverToPop;
|
||||
}
|
||||
|
||||
export function genRoomVia(room) {
|
||||
const via = [];
|
||||
const userId = getHighestPowerUserId(room);
|
||||
if (userId) {
|
||||
const server = getIdServer(userId);
|
||||
if (server) via.push(server);
|
||||
}
|
||||
const serverToPop = getServerToPopulation(room);
|
||||
const sortedServers = Object.keys(serverToPop).sort(
|
||||
(svrA, svrB) => serverToPop[svrB] - serverToPop[svrA],
|
||||
);
|
||||
const mostPop3 = sortedServers.slice(0, 3);
|
||||
if (via.length === 0) return mostPop3;
|
||||
if (mostPop3.includes(via[0])) {
|
||||
mostPop3.splice(mostPop3.indexOf(via[0]), 1);
|
||||
}
|
||||
return via.concat(mostPop3.slice(0, 2));
|
||||
}
|
||||
|
||||
export function isCrossVerified(mx, deviceId) {
|
||||
try {
|
||||
const crossSignInfo = mx.getStoredCrossSigningForUser(mx.getUserId());
|
||||
const deviceInfo = mx.getStoredDevice(mx.getUserId(), deviceId);
|
||||
const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true);
|
||||
return deviceTrust.isCrossSigningVerified();
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// device does not support encryption
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user