1
0
Fork 0
forked from mirror/cinny

Merge remote-tracking branch 'upstream/dev' into custom

This commit is contained in:
tezlm 2024-09-08 20:58:15 -07:00
commit 4b71372ba8
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
110 changed files with 5420 additions and 1288 deletions

View file

@ -25,7 +25,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build run: npm run build
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.3.6
with: with:
name: preview name: preview
path: dist path: dist
@ -33,7 +33,7 @@ jobs:
- name: Save pr number - name: Save pr number
run: echo ${PR_NUMBER} > ./pr.txt run: echo ${PR_NUMBER} > ./pr.txt
- name: Upload pr number - name: Upload pr number
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.3.6
with: with:
name: pr name: pr
path: ./pr.txt path: ./pr.txt

View file

@ -12,7 +12,7 @@ jobs:
- name: 'CLA Assistant' - name: 'CLA Assistant'
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release # Beta Release
uses: cla-assistant/github-action@v2.4.0 uses: cla-assistant/github-action@v2.5.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret # the below token should have repo scope and must be manually added by you in the repository's secret

View file

@ -13,7 +13,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@v6.5.0 uses: docker/build-push-action@v6.7.0
with: with:
context: . context: .
push: false push: false

View file

@ -70,7 +70,7 @@ jobs:
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - 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 - name: Login to Docker Hub
uses: docker/login-action@v3.3.0 uses: docker/login-action@v3.3.0
with: with:
@ -90,7 +90,7 @@ jobs:
${{ secrets.DOCKER_USERNAME }}/cinny ${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6.5.0 uses: docker/build-push-action@v6.7.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

1
.npmrc
View file

@ -1,3 +1,2 @@
legacy-peer-deps=true legacy-peer-deps=true
save-exact=true save-exact=true
@matrix-org:registry=https://gitlab.matrix.org/api/v4/projects/27/packages/npm/

128
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
cinnyapp@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View file

@ -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"> <img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
## Getting started ## 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. 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: * Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by:
``` ```
docker pull ajbura/cinny docker pull ajbura/cinny
``` ```
or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by: or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by:
``` ```
docker pull ghcr.io/cinnyapp/cinny:latest docker pull ghcr.io/cinnyapp/cinny:latest
``` ```
<details> <details>
<summary>PGP Public Key to verify tarball</summary> <summary>PGP Public Key to verify tarball</summary>

View file

@ -24,6 +24,7 @@ server {
rewrite ^/manifest.json$ /manifest.json break; rewrite ^/manifest.json$ /manifest.json break;
rewrite ^.*/olm.wasm$ /olm.wasm break; rewrite ^.*/olm.wasm$ /olm.wasm break;
rewrite ^/sw.js$ /sw.js break;
rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break; rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break;
rewrite ^/public/(.*)$ /public/$1 break; rewrite ^/public/(.*)$ /public/$1 break;

View file

@ -1,4 +1,7 @@
server { server {
listen 80;
listen [::]:80;
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
@ -6,6 +9,7 @@ server {
rewrite ^/manifest.json$ /manifest.json break; rewrite ^/manifest.json$ /manifest.json break;
rewrite ^.*/olm.wasm$ /olm.wasm break; rewrite ^.*/olm.wasm$ /olm.wasm break;
rewrite ^/sw.js$ /sw.js break;
rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break; rewrite ^/pdf.worker.min.js$ /pdf.worker.min.js break;
rewrite ^/public/(.*)$ /public/$1 break; rewrite ^/public/(.*)$ /public/$1 break;

View file

@ -8,6 +8,11 @@
to = "/manifest.json" to = "/manifest.json"
status = 200 status = 200
[[redirects]]
from = "/sw.js"
to = "/sw.js"
status = 200
[[redirects]] [[redirects]]
from = "*/olm.wasm" from = "*/olm.wasm"
to = "/olm.wasm" to = "/olm.wasm"

3501
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.0.3", "version": "4.1.0",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@ -24,7 +24,7 @@
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0", "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
"@fontsource/inter": "4.5.14", "@fontsource/inter": "4.5.14",
"@matrix-org/olm": "3.2.14", "@matrix-org/olm": "3.2.15",
"@tanstack/react-query": "5.24.1", "@tanstack/react-query": "5.24.1",
"@tanstack/react-query-devtools": "5.24.1", "@tanstack/react-query-devtools": "5.24.1",
"@tanstack/react-virtual": "3.2.0", "@tanstack/react-virtual": "3.2.0",
@ -49,12 +49,15 @@
"formik": "2.4.6", "formik": "2.4.6",
"html-dom-parser": "4.0.0", "html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "4.2.0",
"i18next": "23.12.2",
"i18next-browser-languagedetector": "8.0.0",
"i18next-http-backend": "2.5.2",
"immer": "9.0.16", "immer": "9.0.16",
"is-hotkey": "0.2.0", "is-hotkey": "0.2.0",
"jotai": "2.6.0", "jotai": "2.6.0",
"linkify-react": "4.1.3", "linkify-react": "4.1.3",
"linkifyjs": "4.1.3", "linkifyjs": "4.1.3",
"matrix-js-sdk": "29.1.0", "matrix-js-sdk": "34.4.0",
"millify": "6.1.0", "millify": "6.1.0",
"pdfjs-dist": "4.2.67", "pdfjs-dist": "4.2.67",
"prismjs": "1.29.0", "prismjs": "1.29.0",
@ -66,6 +69,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-error-boundary": "4.0.13", "react-error-boundary": "4.0.13",
"react-google-recaptcha": "2.1.0", "react-google-recaptcha": "2.1.0",
"react-i18next": "15.0.0",
"react-modal": "3.16.1", "react-modal": "3.16.1",
"react-range": "1.8.14", "react-range": "1.8.14",
"react-router-dom": "6.20.0", "react-router-dom": "6.20.0",
@ -87,6 +91,7 @@
"@types/react-dom": "18.2.17", "@types/react-dom": "18.2.17",
"@types/react-google-recaptcha": "2.1.8", "@types/react-google-recaptcha": "2.1.8",
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.9.0",
"@types/serviceworker": "0.0.95",
"@types/ua-parser-js": "0.7.36", "@types/ua-parser-js": "0.7.36",
"@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1", "@typescript-eslint/parser": "5.46.1",
@ -103,6 +108,7 @@
"sass": "1.56.2", "sass": "1.56.2",
"typescript": "4.9.4", "typescript": "4.9.4",
"vite": "5.0.13", "vite": "5.0.13",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4", "vite-plugin-static-copy": "1.0.4",
"vite-plugin-top-level-await": "1.4.1" "vite-plugin-top-level-await": "1.4.1"
} }

7
public/locales/de.json Normal file
View file

@ -0,0 +1,7 @@
{
"Organisms": {
"RoomCommon": {
"changed_room_name": " hat den Raum Name geändert"
}
}
}

7
public/locales/en.json Normal file
View file

@ -0,0 +1,7 @@
{
"Organisms": {
"RoomCommon": {
"changed_room_name": " changed room name"
}
}
}

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

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { MsgType } from 'matrix-js-sdk'; import { MsgType } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser'; import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts } from 'linkifyjs';
import { import {
AudioContent, AudioContent,
DownloadFile, DownloadFile,
@ -27,6 +28,7 @@ import { Image, MediaControl, Video } from './media';
import { ImageViewer } from './image-viewer'; import { ImageViewer } from './image-viewer';
import { PdfViewer } from './Pdf-viewer'; import { PdfViewer } from './Pdf-viewer';
import { TextViewer } from './text-viewer'; import { TextViewer } from './text-viewer';
import { testMatrixTo } from '../plugins/matrix-to';
type RenderMessageContentProps = { type RenderMessageContentProps = {
displayName: string; displayName: string;
@ -38,6 +40,7 @@ type RenderMessageContentProps = {
urlPreview?: boolean; urlPreview?: boolean;
highlightRegex?: RegExp; highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions; htmlReactParserOptions: HTMLReactParserOptions;
linkifyOpts: Opts;
outlineAttachment?: boolean; outlineAttachment?: boolean;
}; };
export function RenderMessageContent({ export function RenderMessageContent({
@ -50,8 +53,21 @@ export function RenderMessageContent({
urlPreview, urlPreview,
highlightRegex, highlightRegex,
htmlReactParserOptions, htmlReactParserOptions,
linkifyOpts,
outlineAttachment, outlineAttachment,
}: RenderMessageContentProps) { }: 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 = () => ( const renderFile = () => (
<MFile <MFile
content={getContent()} content={getContent()}
@ -95,19 +111,10 @@ export function RenderMessageContent({
{...props} {...props}
highlightRegex={highlightRegex} highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/> />
)} )}
renderUrlsPreview={ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
urlPreview
? (urls) => (
<UrlPreviewHolder>
{urls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} />
))}
</UrlPreviewHolder>
)
: undefined
}
/> />
); );
} }
@ -123,19 +130,10 @@ export function RenderMessageContent({
{...props} {...props}
highlightRegex={highlightRegex} highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/> />
)} )}
renderUrlsPreview={ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
urlPreview
? (urls) => (
<UrlPreviewHolder>
{urls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} />
))}
</UrlPreviewHolder>
)
: undefined
}
/> />
); );
} }
@ -150,19 +148,10 @@ export function RenderMessageContent({
{...props} {...props}
highlightRegex={highlightRegex} highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/> />
)} )}
renderUrlsPreview={ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
urlPreview
? (urls) => (
<UrlPreviewHolder>
{urls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} />
))}
</UrlPreviewHolder>
)
: undefined
}
/> />
); );
} }

View file

@ -13,6 +13,8 @@ import { CommandElement, EmoticonElement, LinkElement, MentionElement } from './
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getBeginCommand } from './utils'; import { getBeginCommand } from './utils';
import { BlockType } from './types'; import { BlockType } from './types';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useSpecVersions } from '../../hooks/useSpecVersions';
// Put this at the start and end of an inline component to work around this Chromium bug: // Put this at the start and end of an inline component to work around this Chromium bug:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405 // https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
@ -76,6 +78,8 @@ function RenderEmoticonElement({
children, children,
}: { element: EmoticonElement } & RenderElementProps) { }: { element: EmoticonElement } & RenderElementProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const selected = useSelected(); const selected = useSelected();
const focused = useFocused(); const focused = useFocused();
@ -90,7 +94,7 @@ function RenderEmoticonElement({
{element.key.startsWith('mxc://') ? ( {element.key.startsWith('mxc://') ? (
<img <img
className={css.EmoticonImg} className={css.EmoticonImg}
src={mx.mxcUrlToHttp(element.key) ?? element.key} src={mxcUrlToHttp(mx, element.key, useAuthentication) ?? element.key}
alt={element.shortcode} alt={element.shortcode}
/> />
) : ( ) : (

View file

@ -18,6 +18,8 @@ import { useRelevantImagePacks } from '../../../hooks/useImagePacks';
import { IEmoji, emojis } from '../../../plugins/emoji'; import { IEmoji, emojis } from '../../../plugins/emoji';
import { ExtendedPackImage, PackUsage } from '../../../plugins/custom-emoji'; import { ExtendedPackImage, PackUsage } from '../../../plugins/custom-emoji';
import { useKeyDown } from '../../../hooks/useKeyDown'; import { useKeyDown } from '../../../hooks/useKeyDown';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
type EmoticonCompleteHandler = (key: string, shortcode: string) => void; type EmoticonCompleteHandler = (key: string, shortcode: string) => void;
@ -48,6 +50,8 @@ export function EmoticonAutocomplete({
requestClose, requestClose,
}: EmoticonAutocompleteProps) { }: EmoticonAutocompleteProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const imagePacks = useRelevantImagePacks(mx, PackUsage.Emoticon, imagePackRooms); const imagePacks = useRelevantImagePacks(mx, PackUsage.Emoticon, imagePackRooms);
const recentEmoji = useRecentEmoji(mx, 20); const recentEmoji = useRecentEmoji(mx, 20);
@ -103,7 +107,7 @@ export function EmoticonAutocomplete({
<Box <Box
shrink="No" shrink="No"
as="img" as="img"
src={mx.mxcUrlToHttp(key) || key} src={mxcUrlToHttp(mx, key, useAuthentication) || key}
alt={emoticon.shortcode} alt={emoticon.shortcode}
style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }} style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }}
/> />

View file

@ -17,6 +17,7 @@ import { mDirectAtom } from '../../../state/mDirectList';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { factoryRoomIdByActivity } from '../../../utils/sort'; import { factoryRoomIdByActivity } from '../../../utils/sort';
import { RoomAvatar, RoomIcon } from '../../room-avatar'; import { RoomAvatar, RoomIcon } from '../../room-avatar';
import { getViaServers } from '../../../plugins/via-servers';
type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void; type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
@ -104,10 +105,14 @@ export function RoomMentionAutocomplete({
}, [query.text, search, resetSearch]); }, [query.text, search, resetSearch]);
const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => { const handleAutocomplete: MentionAutoCompleteHandler = (roomAliasOrId, name) => {
const mentionRoom = mx.getRoom(roomAliasOrId);
const viaServers = mentionRoom ? getViaServers(mentionRoom) : undefined;
const mentionEl = createMentionElement( const mentionEl = createMentionElement(
roomAliasOrId, roomAliasOrId,
name.startsWith('#') ? name : `#${name}`, 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); replaceWithElement(editor, query.range, mentionEl);
moveCursor(editor, true); moveCursor(editor, true);

View file

@ -18,6 +18,7 @@ import { useKeyDown } from '../../../hooks/useKeyDown';
import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix'; import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room'; import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room';
import { UserAvatar } from '../../user-avatar'; import { UserAvatar } from '../../user-avatar';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
type MentionAutoCompleteHandler = (userId: string, name: string) => void; type MentionAutoCompleteHandler = (userId: string, name: string) => void;
@ -84,6 +85,8 @@ export function UserMentionAutocomplete({
requestClose, requestClose,
}: UserMentionAutocompleteProps) { }: UserMentionAutocompleteProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const roomId: string = room.roomId!; const roomId: string = room.roomId!;
const roomAliasOrId = room.getCanonicalAlias() || roomId; const roomAliasOrId = room.getCanonicalAlias() || roomId;
const members = useRoomMembers(mx, roomId); const members = useRoomMembers(mx, roomId);
@ -143,7 +146,8 @@ export function UserMentionAutocomplete({
/> />
) : ( ) : (
autoCompleteMembers.map((roomMember) => { autoCompleteMembers.map((roomMember) => {
const avatarUrl = roomMember.getAvatarUrl(mx.baseUrl, 32, 32, 'crop', undefined, false); const avatarMxcUrl = roomMember.getMxcAvatarUrl();
const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(avatarMxcUrl, 32, 32, 'crop', undefined, false, useAuthentication) : undefined;
return ( return (
<MenuItem <MenuItem
key={roomMember.userId} key={roomMember.userId}

View file

@ -18,8 +18,14 @@ import {
ParagraphElement, ParagraphElement,
UnorderedListElement, UnorderedListElement,
} from './slate'; } from './slate';
import { parseMatrixToUrl } from '../../utils/matrix';
import { createEmoticonElement, createMentionElement } from './utils'; import { createEmoticonElement, createMentionElement } from './utils';
import {
parseMatrixToRoom,
parseMatrixToRoomEvent,
parseMatrixToUser,
testMatrixTo,
} from '../../plugins/matrix-to';
import { tryDecodeURIComponent } from '../../utils/dom';
const markNodeToType: Record<string, MarkType> = { const markNodeToType: Record<string, MarkType> = {
b: MarkType.Bold, b: MarkType.Bold,
@ -68,11 +74,33 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
return createEmoticonElement(src, alt || 'Unknown Emoji'); return createEmoticonElement(src, alt || 'Unknown Emoji');
} }
if (node.name === 'a') { if (node.name === 'a') {
const { href } = node.attribs; const href = tryDecodeURIComponent(node.attribs.href);
if (typeof href !== 'string') return undefined; if (typeof href !== 'string') return undefined;
const [mxId] = parseMatrixToUrl(href); if (testMatrixTo(href)) {
if (mxId) { const userMention = parseMatrixToUser(href);
return createMentionElement(mxId, parseNodeText(node) || mxId, false); 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; return undefined;

View file

@ -51,10 +51,19 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
case BlockType.UnorderedList: case BlockType.UnorderedList:
return `<ul>${children}</ul>`; return `<ul>${children}</ul>`;
case BlockType.Mention: case BlockType.Mention: {
return `<a href="https://matrix.to/#/${encodeURIComponent(node.id)}">${sanitizeText( let fragment = node.id;
node.name
)}</a>`; 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="${encodeURI(matrixTo)}">${sanitizeText(node.name)}</a>`;
}
case BlockType.Emoticon: case BlockType.Emoticon:
return node.key.startsWith('mxc://') return node.key.startsWith('mxc://')
? `<img data-mx-emoticon src="${node.key}" alt="${sanitizeText( ? `<img data-mx-emoticon src="${node.key}" alt="${sanitizeText(
@ -62,7 +71,7 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
)}" title="${sanitizeText(node.shortcode)}" height="32" />` )}" title="${sanitizeText(node.shortcode)}" height="32" />`
: sanitizeText(node.key); : sanitizeText(node.key);
case BlockType.Link: case BlockType.Link:
return `<a href="${encodeURIComponent(node.href)}">${node.children}</a>`; return `<a href="${encodeURI(node.href)}">${node.children}</a>`;
case BlockType.Command: case BlockType.Command:
return `/${sanitizeText(node.command)}`; return `/${sanitizeText(node.command)}`;
default: default:

View file

@ -29,6 +29,8 @@ export type LinkElement = {
export type MentionElement = { export type MentionElement = {
type: BlockType.Mention; type: BlockType.Mention;
id: string; id: string;
eventId?: string;
viaServers?: string[];
highlight: boolean; highlight: boolean;
name: string; name: string;
children: Text[]; children: Text[];

View file

@ -158,10 +158,14 @@ export const resetEditorHistory = (editor: Editor) => {
export const createMentionElement = ( export const createMentionElement = (
id: string, id: string,
name: string, name: string,
highlight: boolean highlight: boolean,
eventId?: string,
viaServers?: string[]
): MentionElement => ({ ): MentionElement => ({
type: BlockType.Mention, type: BlockType.Mention,
id, id,
eventId,
viaServers,
highlight, highlight,
name, name,
children: [{ text: '' }], children: [{ text: '' }],

View file

@ -42,13 +42,14 @@ import { useRelevantImagePacks } from '../../hooks/useImagePacks';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../hooks/useRecentEmoji'; import { useRecentEmoji } from '../../hooks/useRecentEmoji';
import { ExtendedPackImage, ImagePack, PackUsage } from '../../plugins/custom-emoji'; import { ExtendedPackImage, ImagePack, PackUsage } from '../../plugins/custom-emoji';
import { isUserId } from '../../utils/matrix'; import { isUserId, mxcUrlToHttp } from '../../utils/matrix';
import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from '../../utils/dom'; import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from '../../utils/dom';
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
import { useDebounce } from '../../hooks/useDebounce'; import { useDebounce } from '../../hooks/useDebounce';
import { useThrottle } from '../../hooks/useThrottle'; import { useThrottle } from '../../hooks/useThrottle';
import { addRecentEmoji } from '../../plugins/recent-emoji'; import { addRecentEmoji } from '../../plugins/recent-emoji';
import { mobileOrTablet } from '../../utils/user-agent'; import { mobileOrTablet } from '../../utils/user-agent';
import { useSpecVersions } from '../../hooks/useSpecVersions';
const RECENT_GROUP_ID = 'recent_group'; const RECENT_GROUP_ID = 'recent_group';
const SEARCH_GROUP_ID = 'search_group'; const SEARCH_GROUP_ID = 'search_group';
@ -354,11 +355,13 @@ function ImagePackSidebarStack({
packs, packs,
usage, usage,
onItemClick, onItemClick,
useAuthentication,
}: { }: {
mx: MatrixClient; mx: MatrixClient;
packs: ImagePack[]; packs: ImagePack[];
usage: PackUsage; usage: PackUsage;
onItemClick: (id: string) => void; onItemClick: (id: string) => void;
useAuthentication?: boolean;
}) { }) {
const activeGroupId = useAtomValue(activeGroupIdAtom); const activeGroupId = useAtomValue(activeGroupIdAtom);
return ( return (
@ -381,7 +384,7 @@ function ImagePackSidebarStack({
height: toRem(24), height: toRem(24),
objectFit: 'contain', objectFit: 'contain',
}} }}
src={mx.mxcUrlToHttp(pack.getPackAvatarUrl(usage) ?? '') || pack.avatarUrl} src={mxcUrlToHttp(mx, pack.getPackAvatarUrl(usage) ?? '', useAuthentication) || pack.avatarUrl}
alt={label || 'Unknown Pack'} alt={label || 'Unknown Pack'}
/> />
</SidebarBtn> </SidebarBtn>
@ -453,12 +456,14 @@ export function SearchEmojiGroup({
label, label,
id, id,
emojis: searchResult, emojis: searchResult,
useAuthentication,
}: { }: {
mx: MatrixClient; mx: MatrixClient;
tab: EmojiBoardTab; tab: EmojiBoardTab;
label: string; label: string;
id: string; id: string;
emojis: Array<ExtendedPackImage | IEmoji>; emojis: Array<ExtendedPackImage | IEmoji>;
useAuthentication?: boolean;
}) { }) {
return ( return (
<EmojiGroup key={id} id={id} label={label}> <EmojiGroup key={id} id={id} label={label}>
@ -486,7 +491,7 @@ export function SearchEmojiGroup({
loading="lazy" loading="lazy"
className={css.CustomEmojiImg} className={css.CustomEmojiImg}
alt={emoji.body || emoji.shortcode} alt={emoji.body || emoji.shortcode}
src={mx.mxcUrlToHttp(emoji.url) ?? emoji.url} src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
/> />
</EmojiItem> </EmojiItem>
) )
@ -504,7 +509,7 @@ export function SearchEmojiGroup({
loading="lazy" loading="lazy"
className={css.StickerImg} className={css.StickerImg}
alt={emoji.body || emoji.shortcode} alt={emoji.body || emoji.shortcode}
src={mx.mxcUrlToHttp(emoji.url) ?? emoji.url} src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
/> />
</StickerItem> </StickerItem>
) )
@ -514,7 +519,7 @@ export function SearchEmojiGroup({
} }
export const CustomEmojiGroups = memo( export const CustomEmojiGroups = memo(
({ mx, groups }: { mx: MatrixClient; groups: ImagePack[] }) => ( ({ mx, groups, useAuthentication }: { mx: MatrixClient; groups: ImagePack[]; useAuthentication?: boolean }) => (
<> <>
{groups.map((pack) => ( {groups.map((pack) => (
<EmojiGroup key={pack.id} id={pack.id} label={pack.displayName || 'Unknown'}> <EmojiGroup key={pack.id} id={pack.id} label={pack.displayName || 'Unknown'}>
@ -530,7 +535,7 @@ export const CustomEmojiGroups = memo(
loading="lazy" loading="lazy"
className={css.CustomEmojiImg} className={css.CustomEmojiImg}
alt={image.body || image.shortcode} alt={image.body || image.shortcode}
src={mx.mxcUrlToHttp(image.url) ?? image.url} src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/> />
</EmojiItem> </EmojiItem>
))} ))}
@ -540,7 +545,7 @@ export const CustomEmojiGroups = memo(
) )
); );
export const StickerGroups = memo(({ mx, groups }: { mx: MatrixClient; groups: ImagePack[] }) => ( export const StickerGroups = memo(({ mx, groups, useAuthentication }: { mx: MatrixClient; groups: ImagePack[]; useAuthentication?: boolean }) => (
<> <>
{groups.length === 0 && ( {groups.length === 0 && (
<Box <Box
@ -573,7 +578,7 @@ export const StickerGroups = memo(({ mx, groups }: { mx: MatrixClient; groups: I
loading="lazy" loading="lazy"
className={css.StickerImg} className={css.StickerImg}
alt={image.body || image.shortcode} alt={image.body || image.shortcode}
src={mx.mxcUrlToHttp(image.url) ?? image.url} src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/> />
</StickerItem> </StickerItem>
))} ))}
@ -645,6 +650,8 @@ export function EmojiBoard({
const setActiveGroupId = useSetAtom(activeGroupIdAtom); const setActiveGroupId = useSetAtom(activeGroupIdAtom);
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const emojiGroupLabels = useEmojiGroupLabels(); const emojiGroupLabels = useEmojiGroupLabels();
const emojiGroupIcons = useEmojiGroupIcons(); const emojiGroupIcons = useEmojiGroupIcons();
const imagePacks = useRelevantImagePacks(mx, usage, imagePackRooms); const imagePacks = useRelevantImagePacks(mx, usage, imagePackRooms);
@ -729,14 +736,14 @@ export function EmojiBoard({
} else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) { } else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) {
const img = document.createElement('img'); const img = document.createElement('img');
img.className = css.CustomEmojiImg; img.className = css.CustomEmojiImg;
img.setAttribute('src', mx.mxcUrlToHttp(emojiInfo.data) || emojiInfo.data); img.setAttribute('src', mxcUrlToHttp(mx, emojiInfo.data, useAuthentication) || emojiInfo.data);
img.setAttribute('alt', emojiInfo.shortcode); img.setAttribute('alt', emojiInfo.shortcode);
emojiPreviewRef.current.textContent = ''; emojiPreviewRef.current.textContent = '';
emojiPreviewRef.current.appendChild(img); emojiPreviewRef.current.appendChild(img);
} }
emojiPreviewTextRef.current.textContent = `:${emojiInfo.shortcode}:`; emojiPreviewTextRef.current.textContent = `:${emojiInfo.shortcode}:`;
}, },
[mx] [mx, useAuthentication]
); );
const throttleEmojiHover = useThrottle(handleEmojiPreview, { const throttleEmojiHover = useThrottle(handleEmojiPreview, {
@ -829,6 +836,7 @@ export function EmojiBoard({
usage={usage} usage={usage}
packs={imagePacks} packs={imagePacks}
onItemClick={handleScrollToGroup} onItemClick={handleScrollToGroup}
useAuthentication={useAuthentication}
/> />
)} )}
{emojiTab && ( {emojiTab && (
@ -890,13 +898,14 @@ export function EmojiBoard({
id={SEARCH_GROUP_ID} id={SEARCH_GROUP_ID}
label={result.items.length ? 'Search Results' : 'No Results found'} label={result.items.length ? 'Search Results' : 'No Results found'}
emojis={result.items} emojis={result.items}
useAuthentication={useAuthentication}
/> />
)} )}
{emojiTab && recentEmojis.length > 0 && ( {emojiTab && recentEmojis.length > 0 && (
<RecentEmojiGroup id={RECENT_GROUP_ID} label="Recent" emojis={recentEmojis} /> <RecentEmojiGroup id={RECENT_GROUP_ID} label="Recent" emojis={recentEmojis} />
)} )}
{emojiTab && <CustomEmojiGroups mx={mx} groups={imagePacks} />} {emojiTab && <CustomEmojiGroups mx={mx} groups={imagePacks} useAuthentication={useAuthentication} />}
{stickerTab && <StickerGroups mx={mx} groups={imagePacks} />} {stickerTab && <StickerGroups mx={mx} groups={imagePacks} useAuthentication={useAuthentication} />}
{emojiTab && <NativeEmojiGroups groups={emojiGroups} labels={emojiGroupLabels} />} {emojiTab && <NativeEmojiGroups groups={emojiGroups} labels={emojiGroupLabels} />}
</Box> </Box>
</Scroll> </Scroll>

View file

@ -21,6 +21,7 @@ import * as css from './EventReaders.css';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { openProfileViewer } from '../../../client/action/navigation'; import { openProfileViewer } from '../../../client/action/navigation';
import { UserAvatar } from '../user-avatar'; import { UserAvatar } from '../user-avatar';
import { useSpecVersions } from '../../hooks/useSpecVersions';
export type EventReadersProps = { export type EventReadersProps = {
room: Room; room: Room;
@ -30,6 +31,8 @@ export type EventReadersProps = {
export const EventReaders = as<'div', EventReadersProps>( export const EventReaders = as<'div', EventReadersProps>(
({ className, room, eventId, requestClose, ...props }, ref) => { ({ className, room, eventId, requestClose, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const latestEventReaders = useRoomEventReaders(room, eventId); const latestEventReaders = useRoomEventReaders(room, eventId);
const getName = (userId: string) => const getName = (userId: string) =>
@ -55,9 +58,10 @@ export const EventReaders = as<'div', EventReadersProps>(
<Box className={css.Content} direction="Column"> <Box className={css.Content} direction="Column">
{latestEventReaders.map((readerId) => { {latestEventReaders.map((readerId) => {
const name = getName(readerId); const name = getName(readerId);
const avatarUrl = room const avatarMxcUrl = room
.getMember(readerId) .getMember(readerId)
?.getAvatarUrl(mx.baseUrl, 100, 100, 'crop', undefined, false); ?.getMxcAvatarUrl();
const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication) : undefined;
return ( return (
<MenuItem <MenuItem

View file

@ -5,7 +5,7 @@ import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
import * as css from './Reaction.css'; import * as css from './Reaction.css';
import { getHexcodeForEmoji, getShortcodeFor } from '../../plugins/emoji'; import { getHexcodeForEmoji, getShortcodeFor } from '../../plugins/emoji';
import { getMemberDisplayName } from '../../utils/room'; import { getMemberDisplayName } from '../../utils/room';
import { eventWithShortcode, getMxIdLocalPart } from '../../utils/matrix'; import { eventWithShortcode, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
export const Reaction = as< export const Reaction = as<
'button', 'button',
@ -13,8 +13,9 @@ export const Reaction = as<
mx: MatrixClient; mx: MatrixClient;
count: number; count: number;
reaction: string; reaction: string;
useAuthentication?: boolean;
} }
>(({ className, mx, count, reaction, ...props }, ref) => ( >(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => (
<Box <Box
as="button" as="button"
className={classNames(css.Reaction, className)} className={classNames(css.Reaction, className)}
@ -28,7 +29,8 @@ export const Reaction = as<
{reaction.startsWith('mxc://') ? ( {reaction.startsWith('mxc://') ? (
<img <img
className={css.ReactionImg} className={css.ReactionImg}
src={mx.mxcUrlToHttp(reaction) ?? reaction} src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction
}
alt={reaction} alt={reaction}
/> />
) : ( ) : (

View file

@ -1,13 +1,10 @@
import React from 'react'; import React from 'react';
import parse, { HTMLReactParserOptions } from 'html-react-parser'; import parse, { HTMLReactParserOptions } from 'html-react-parser';
import Linkify from 'linkify-react'; import Linkify from 'linkify-react';
import { Opts } from 'linkifyjs';
import { MessageEmptyContent } from './content'; import { MessageEmptyContent } from './content';
import { sanitizeCustomHtml } from '../../utils/sanitize'; import { sanitizeCustomHtml } from '../../utils/sanitize';
import { import { highlightText, scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
LINKIFY_OPTS,
highlightText,
scaleSystemEmoji,
} from '../../plugins/react-custom-html-parser';
type RenderBodyProps = { type RenderBodyProps = {
body: string; body: string;
@ -15,12 +12,14 @@ type RenderBodyProps = {
highlightRegex?: RegExp; highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions; htmlReactParserOptions: HTMLReactParserOptions;
linkifyOpts: Opts;
}; };
export function RenderBody({ export function RenderBody({
body, body,
customBody, customBody,
highlightRegex, highlightRegex,
htmlReactParserOptions, htmlReactParserOptions,
linkifyOpts,
}: RenderBodyProps) { }: RenderBodyProps) {
if (body === '') <MessageEmptyContent />; if (body === '') <MessageEmptyContent />;
if (customBody) { if (customBody) {
@ -28,7 +27,7 @@ export function RenderBody({
return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions); return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions);
} }
return ( return (
<Linkify options={LINKIFY_OPTS}> <Linkify options={linkifyOpts}>
{highlightRegex {highlightRegex
? highlightText(highlightRegex, scaleSystemEmoji(body)) ? highlightText(highlightRegex, scaleSystemEmoji(body))
: scaleSystemEmoji(body)} : scaleSystemEmoji(body)}

View file

@ -5,6 +5,25 @@ export const ReplyBend = style({
flexShrink: 0, flexShrink: 0,
}); });
export const ThreadIndicator = style({
opacity: config.opacity.P300,
gap: toRem(2),
selectors: {
'button&': {
cursor: 'pointer',
},
':hover&': {
opacity: config.opacity.P500,
},
},
});
export const ThreadIndicatorIcon = style({
width: toRem(14),
height: toRem(14),
});
export const Reply = style({ export const Reply = style({
marginBottom: toRem(1), marginBottom: toRem(1),
minWidth: 0, minWidth: 0,

View file

@ -1,7 +1,7 @@
import { Box, Icon, Icons, Text, as, color, toRem } from 'folds'; import { Box, Icon, Icons, Text, as, color, toRem } from 'folds';
import { EventTimelineSet, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk'; import { EventTimelineSet, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import React, { ReactNode, useEffect, useMemo, useState } from 'react'; import React, { MouseEventHandler, ReactNode, useEffect, useMemo, useState } from 'react';
import to from 'await-to-js'; import to from 'await-to-js';
import classNames from 'classnames'; import classNames from 'classnames';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
@ -22,6 +22,7 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>(
<Box <Box
className={classNames(css.Reply, className)} className={classNames(css.Reply, className)}
alignItems="Center" alignItems="Center"
alignSelf="Start"
gap="100" gap="100"
{...props} {...props}
ref={ref} ref={ref}
@ -37,16 +38,26 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>(
) )
); );
export const ThreadIndicator = as<'div'>(({ ...props }, ref) => (
<Box className={css.ThreadIndicator} alignItems="Center" alignSelf="Start" {...props} ref={ref}>
<Icon className={css.ThreadIndicatorIcon} src={Icons.Message} />
<Text size="T200">Threaded reply</Text>
</Box>
));
type ReplyProps = { type ReplyProps = {
mx: MatrixClient; mx: MatrixClient;
room: Room; room: Room;
timelineSet?: EventTimelineSet; timelineSet?: EventTimelineSet | undefined;
eventId: string; replyEventId: string;
threadRootId?: string | undefined;
onClick?: MouseEventHandler | undefined;
}; };
export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ...props }, ref) => { export const Reply = as<'div', ReplyProps>((_, ref) => {
const { mx, room, timelineSet, replyEventId, threadRootId, onClick, ...props } = _;
const [replyEvent, setReplyEvent] = useState<MatrixEvent | null | undefined>( const [replyEvent, setReplyEvent] = useState<MatrixEvent | null | undefined>(
timelineSet?.findEventById(eventId) timelineSet?.findEventById(replyEventId)
); );
const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []); const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []);
@ -62,7 +73,7 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
useEffect(() => { useEffect(() => {
let disposed = false; let disposed = false;
const loadEvent = async () => { const loadEvent = async () => {
const [err, evt] = await to(mx.fetchRoomEvent(room.roomId, eventId)); const [err, evt] = await to(mx.fetchRoomEvent(room.roomId, replyEventId));
const mEvent = new MatrixEvent(evt); const mEvent = new MatrixEvent(evt);
if (disposed) return; if (disposed) return;
if (err) { if (err) {
@ -78,13 +89,18 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
return () => { return () => {
disposed = true; disposed = true;
}; };
}, [replyEvent, mx, room, eventId]); }, [replyEvent, mx, room, replyEventId]);
const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted'; const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted';
const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody;
return ( return (
<Box direction="Column" {...props} ref={ref}>
{threadRootId && (
<ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
)}
<ReplyLayout <ReplyLayout
as="button"
userColor={sender ? colorMXID(sender) : undefined} userColor={sender ? colorMXID(sender) : undefined}
username={ username={
sender && ( sender && (
@ -93,8 +109,8 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
</Text> </Text>
) )
} }
{...props} data-event-id={replyEventId}
ref={ref} onClick={onClick}
> >
{replyEvent !== undefined ? ( {replyEvent !== undefined ? (
<Text size="T300" truncate> <Text size="T300" truncate>
@ -110,5 +126,6 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
/> />
)} )}
</ReplyLayout> </ReplyLayout>
</Box>
); );
}); });

View file

@ -17,6 +17,8 @@ import {
} from '../../../hooks/media'; } from '../../../hooks/media';
import { useThrottle } from '../../../hooks/useThrottle'; import { useThrottle } from '../../../hooks/useThrottle';
import { secondsToMinutesAndSeconds } from '../../../utils/common'; import { secondsToMinutesAndSeconds } from '../../../utils/common';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
const PLAY_TIME_THROTTLE_OPS = { const PLAY_TIME_THROTTLE_OPS = {
wait: 500, wait: 500,
@ -44,11 +46,13 @@ export function AudioContent({
renderMediaControl, renderMediaControl,
}: AudioContentProps) { }: AudioContentProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(
() => getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo), () => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo),
[mx, url, mimeType, encInfo] [mx, url, useAuthentication, mimeType, encInfo]
) )
); );

View file

@ -30,6 +30,8 @@ import {
} from '../../../utils/mimeTypes'; } from '../../../utils/mimeTypes';
import * as css from './style.css'; import * as css from './style.css';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
const renderErrorButton = (retry: () => void, text: string) => ( const renderErrorButton = (retry: () => void, text: string) => (
<TooltipProvider <TooltipProvider
@ -75,11 +77,13 @@ type ReadTextFileProps = {
}; };
export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: ReadTextFileProps) { export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: ReadTextFileProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [textViewer, setTextViewer] = useState(false); const [textViewer, setTextViewer] = useState(false);
const loadSrc = useCallback( const loadSrc = useCallback(
() => getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo), () => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo),
[mx, url, mimeType, encInfo] [mx, url, useAuthentication, mimeType, encInfo]
); );
const [textState, loadText] = useAsyncCallback( const [textState, loadText] = useAsyncCallback(
@ -166,14 +170,16 @@ export type ReadPdfFileProps = {
}; };
export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: ReadPdfFileProps) { export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: ReadPdfFileProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [pdfViewer, setPdfViewer] = useState(false); const [pdfViewer, setPdfViewer] = useState(false);
const [pdfState, loadPdf] = useAsyncCallback( const [pdfState, loadPdf] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const httpUrl = await getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo); const httpUrl = await getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo);
setPdfViewer(true); setPdfViewer(true);
return httpUrl; return httpUrl;
}, [mx, url, mimeType, encInfo]) }, [mx, url, useAuthentication, mimeType, encInfo])
); );
return ( return (
@ -240,13 +246,15 @@ export type DownloadFileProps = {
}; };
export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFileProps) { export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFileProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [downloadState, download] = useAsyncCallback( const [downloadState, download] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
const httpUrl = await getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo); const httpUrl = await getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo);
FileSaver.saveAs(httpUrl, body); FileSaver.saveAs(httpUrl, body);
return httpUrl; return httpUrl;
}, [mx, url, mimeType, encInfo, body]) }, [mx, url, useAuthentication, mimeType, encInfo, body])
); );
return downloadState.status === AsyncStatus.Error ? ( return downloadState.status === AsyncStatus.Error ? (

View file

@ -27,6 +27,8 @@ import * as css from './style.css';
import { bytesToSize } from '../../../utils/common'; import { bytesToSize } from '../../../utils/common';
import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes'; import { FALLBACK_MIMETYPE } from '../../../utils/mimeTypes';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
type RenderViewerProps = { type RenderViewerProps = {
src: string; src: string;
@ -69,6 +71,8 @@ export const ImageContent = as<'div', ImageContentProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const blurHash = info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]; const blurHash = info?.[MATRIX_BLUR_HASH_PROPERTY_NAME];
const [load, setLoad] = useState(false); const [load, setLoad] = useState(false);
@ -77,8 +81,8 @@ export const ImageContent = as<'div', ImageContentProps>(
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(
() => getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType || FALLBACK_MIMETYPE, encInfo), () => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType || FALLBACK_MIMETYPE, encInfo),
[mx, url, mimeType, encInfo] [mx, url, useAuthentication, mimeType, encInfo]
) )
); );

View file

@ -3,6 +3,8 @@ import { IThumbnailContent } from '../../../../types/matrix/common';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getFileSrcUrl } from './util'; import { getFileSrcUrl } from './util';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
export type ThumbnailContentProps = { export type ThumbnailContentProps = {
info: IThumbnailContent; info: IThumbnailContent;
@ -10,6 +12,8 @@ export type ThumbnailContentProps = {
}; };
export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) { export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [thumbSrcState, loadThumbSrc] = useAsyncCallback( const [thumbSrcState, loadThumbSrc] = useAsyncCallback(
useCallback(() => { useCallback(() => {
@ -19,11 +23,11 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
throw new Error('Failed to load thumbnail'); throw new Error('Failed to load thumbnail');
} }
return getFileSrcUrl( return getFileSrcUrl(
mx.mxcUrlToHttp(thumbMxcUrl) ?? '', mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? '',
thumbInfo.mimetype, thumbInfo.mimetype,
info.thumbnail_file info.thumbnail_file
); );
}, [mx, info]) }, [mx, info, useAuthentication])
); );
useEffect(() => { useEffect(() => {

View file

@ -25,6 +25,8 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getFileSrcUrl } from './util'; import { getFileSrcUrl } from './util';
import { bytesToSize } from '../../../../util/common'; import { bytesToSize } from '../../../../util/common';
import { millisecondsToMinutesAndSeconds } from '../../../utils/common'; import { millisecondsToMinutesAndSeconds } from '../../../utils/common';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
type RenderVideoProps = { type RenderVideoProps = {
title: string; title: string;
@ -61,6 +63,8 @@ export const VideoContent = as<'div', VideoContentProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const blurHash = info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME]; const blurHash = info.thumbnail_info?.[MATRIX_BLUR_HASH_PROPERTY_NAME];
const [load, setLoad] = useState(false); const [load, setLoad] = useState(false);
@ -68,8 +72,8 @@ export const VideoContent = as<'div', VideoContentProps>(
const [srcState, loadSrc] = useAsyncCallback( const [srcState, loadSrc] = useAsyncCallback(
useCallback( useCallback(
() => getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo), () => getFileSrcUrl(mxcUrlToHttp(mx, url, useAuthentication) ?? '', mimeType, encInfo),
[mx, url, mimeType, encInfo] [mx, url, useAuthentication, mimeType, encInfo]
) )
); );

View file

@ -87,15 +87,17 @@ export const Page = as<'div'>(({ className, ...props }, ref) => (
/> />
)); ));
export const PageHeader = as<'div'>(({ className, ...props }, ref) => ( export const PageHeader = as<'div', css.PageHeaderVariants>(
({ className, balance, ...props }, ref) => (
<Header <Header
as="header" as="header"
size="600" size="600"
className={classNames(css.PageHeader, className)} className={classNames(css.PageHeader({ balance }), className)}
{...props} {...props}
ref={ref} ref={ref}
/> />
)); )
);
export const PageContent = as<'div'>(({ className, ...props }, ref) => ( export const PageContent = as<'div'>(({ className, ...props }, ref) => (
<div className={classNames(css.PageContent, className)} {...props} ref={ref} /> <div className={classNames(css.PageContent, className)} {...props} ref={ref} />

View file

@ -1,4 +1,5 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { DefaultReset, color, config, toRem } from 'folds'; import { DefaultReset, color, config, toRem } from 'folds';
export const PageNav = style({ export const PageNav = style({
@ -33,11 +34,21 @@ export const PageNavContent = style({
paddingBottom: config.space.S700, paddingBottom: config.space.S700,
}); });
export const PageHeader = style({ export const PageHeader = recipe({
base: {
paddingLeft: config.space.S400, paddingLeft: config.space.S400,
paddingRight: config.space.S200, paddingRight: config.space.S200,
borderBottomWidth: config.borderWidth.B300, borderBottomWidth: config.borderWidth.B300,
},
variants: {
balance: {
true: {
paddingLeft: config.space.S200,
},
},
},
}); });
export type PageHeaderVariants = RecipeVariants<typeof PageHeader>;
export const PageContent = style([ export const PageContent = style([
DefaultReset, DefaultReset,

View file

@ -21,7 +21,7 @@ import classNames from 'classnames';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import * as css from './style.css'; import * as css from './style.css';
import { RoomAvatar } from '../room-avatar'; import { RoomAvatar } from '../room-avatar';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { nameInitials } from '../../utils/common'; import { nameInitials } from '../../utils/common';
import { millify } from '../../plugins/millify'; import { millify } from '../../plugins/millify';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
@ -32,6 +32,7 @@ import { useJoinedRoomId } from '../../hooks/useJoinedRoomId';
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver'; import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
import { getRoomAvatarUrl, getStateEvent } from '../../utils/room'; import { getRoomAvatarUrl, getStateEvent } from '../../utils/room';
import { useStateEventCallback } from '../../hooks/useStateEventCallback'; import { useStateEventCallback } from '../../hooks/useStateEventCallback';
import { useSpecVersions } from '../../hooks/useSpecVersions';
type GridColumnCount = '1' | '2' | '3'; type GridColumnCount = '1' | '2' | '3';
const getGridColumnCount = (gridWidth: number): GridColumnCount => { const getGridColumnCount = (gridWidth: number): GridColumnCount => {
@ -138,6 +139,7 @@ type RoomCardProps = {
topic?: string; topic?: string;
memberCount?: number; memberCount?: number;
roomType?: string; roomType?: string;
viaServers?: string[];
onView?: (roomId: string) => void; onView?: (roomId: string) => void;
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode; renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
}; };
@ -152,6 +154,7 @@ export const RoomCard = as<'div', RoomCardProps>(
topic, topic,
memberCount, memberCount,
roomType, roomType,
viaServers,
onView, onView,
renderTopicViewer, renderTopicViewer,
...props ...props
@ -159,6 +162,8 @@ export const RoomCard = as<'div', RoomCardProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias); const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias);
const joinedRoom = mx.getRoom(joinedRoomId); const joinedRoom = mx.getRoom(joinedRoomId);
const [topicEvent, setTopicEvent] = useState(() => const [topicEvent, setTopicEvent] = useState(() =>
@ -169,8 +174,8 @@ export const RoomCard = as<'div', RoomCardProps>(
const fallbackTopic = roomIdOrAlias; const fallbackTopic = roomIdOrAlias;
const avatar = joinedRoom const avatar = joinedRoom
? getRoomAvatarUrl(mx, joinedRoom, 96) ? getRoomAvatarUrl(mx, joinedRoom, 96, useAuthentication)
: avatarUrl && mx.mxcUrlToHttp(avatarUrl, 96, 96, 'crop'); : avatarUrl && mxcUrlToHttp(mx, avatarUrl, useAuthentication, 96, 96, 'crop');
const roomName = joinedRoom?.name || name || fallbackName; const roomName = joinedRoom?.name || name || fallbackName;
const roomTopic = const roomTopic =
@ -194,7 +199,7 @@ export const RoomCard = as<'div', RoomCardProps>(
); );
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>( const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
useCallback(() => mx.joinRoom(roomIdOrAlias), [mx, roomIdOrAlias]) useCallback(() => mx.joinRoom(roomIdOrAlias, { viaServers }), [mx, roomIdOrAlias, viaServers])
); );
const joining = const joining =
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success; joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;

View file

@ -6,7 +6,7 @@ import { openInviteUser } from '../../../client/action/navigation';
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room'; import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
import { getMemberDisplayName, getStateEvent } from '../../utils/room'; import { getMemberDisplayName, getStateEvent } from '../../utils/room';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { timeDayMonthYear, timeHourMinute } from '../../utils/time'; import { timeDayMonthYear, timeHourMinute } from '../../utils/time';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
@ -14,6 +14,7 @@ import { RoomAvatar } from '../room-avatar';
import { nameInitials } from '../../utils/common'; import { nameInitials } from '../../utils/common';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList'; import { mDirectAtom } from '../../state/mDirectList';
import { useSpecVersions } from '../../hooks/useSpecVersions';
export type RoomIntroProps = { export type RoomIntroProps = {
room: Room; room: Room;
@ -21,6 +22,8 @@ export type RoomIntroProps = {
export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => { export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const { navigateRoom } = useRoomNavigate(); const { navigateRoom } = useRoomNavigate();
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
@ -28,7 +31,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId)); const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
const name = useRoomName(room); const name = useRoomName(room);
const topic = useRoomTopic(room); const topic = useRoomTopic(room);
const avatarHttpUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc) : undefined; const avatarHttpUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication) : undefined;
const createContent = createEvent?.getContent<IRoomCreateContent>(); const createContent = createEvent?.getContent<IRoomCreateContent>();
const ts = createEvent?.getTs(); const ts = createEvent?.getTs();

View file

@ -9,12 +9,17 @@ import {
useIntersectionObserver, useIntersectionObserver,
} from '../../hooks/useIntersectionObserver'; } from '../../hooks/useIntersectionObserver';
import * as css from './UrlPreviewCard.css'; import * as css from './UrlPreviewCard.css';
import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useSpecVersions } from '../../hooks/useSpecVersions';
const linkStyles = { color: color.Success.Main }; const linkStyles = { color: color.Success.Main };
export const UrlPreviewCard = as<'div', { url: string; ts: number }>( export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
({ url, ts, ...props }, ref) => { ({ url, ts, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [previewStatus, loadPreview] = useAsyncCallback( const [previewStatus, loadPreview] = useAsyncCallback(
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx]) useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
); );
@ -26,7 +31,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null; if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => { const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mx.mxcUrlToHttp(prev['og:image'] || '', 256, 256, 'scale', false); const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication, 256, 256, 'scale', false);
return ( return (
<> <>
@ -43,7 +48,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
priority="300" priority="300"
> >
{typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `} {typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
{decodeURIComponent(url)} {tryDecodeURIComponent(url)}
</Text> </Text>
<Text truncate priority="400"> <Text truncate priority="400">
<b>{prev['og:title']}</b> <b>{prev['og:title']}</b>

View file

@ -1,5 +1,5 @@
import React from 'react'; 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 { useAtomValue } from 'jotai';
import { RoomCard } from '../../components/room-card'; import { RoomCard } from '../../components/room-card';
import { RoomTopicViewer } from '../../components/room-topic-viewer'; import { RoomTopicViewer } from '../../components/room-topic-viewer';
@ -8,29 +8,49 @@ import { RoomSummaryLoader } from '../../components/RoomSummaryLoader';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList'; import { allRoomsAtom } from '../../state/room-list/roomList';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { BackRouteHandler } from '../../components/BackRouteHandler';
type JoinBeforeNavigateProps = { roomIdOrAlias: string }; type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) { export function JoinBeforeNavigate({
roomIdOrAlias,
eventId,
viaServers,
}: JoinBeforeNavigateProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const { navigateRoom, navigateSpace } = useRoomNavigate(); const { navigateRoom, navigateSpace } = useRoomNavigate();
const screenSize = useScreenSizeContext();
const handleView = (roomId: string) => { const handleView = (roomId: string) => {
if (mx.getRoom(roomId)?.isSpaceRoom()) { if (mx.getRoom(roomId)?.isSpaceRoom()) {
navigateSpace(roomId); navigateSpace(roomId);
return; return;
} }
navigateRoom(roomId); navigateRoom(roomId, eventId);
}; };
return ( return (
<Page> <Page>
<PageHeader> <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"> <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
<Text size="H3" truncate> <Text size="H3" truncate>
{roomIdOrAlias} {roomIdOrAlias}
</Text> </Text>
</Box> </Box>
</Box>
</PageHeader> </PageHeader>
<Box grow="Yes"> <Box grow="Yes">
<Scroll hideTrack visibility="Hover" size="0"> <Scroll hideTrack visibility="Hover" size="0">
@ -46,6 +66,7 @@ export function JoinBeforeNavigate({ roomIdOrAlias }: JoinBeforeNavigateProps) {
topic={summary?.topic} topic={summary?.topic}
memberCount={summary?.num_joined_members} memberCount={summary?.num_joined_members}
roomType={summary?.room_type} roomType={summary?.room_type}
viaServers={viaServers}
renderTopicViewer={(name, topic, requestClose) => ( renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} /> <RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
)} )}

View file

@ -31,6 +31,10 @@ import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../components/leave-space-prompt'; import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { BackRouteHandler } from '../../components/BackRouteHandler';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useSpecVersions } from '../../hooks/useSpecVersions';
type LobbyMenuProps = { type LobbyMenuProps = {
roomId: string; roomId: string;
@ -120,21 +124,45 @@ type LobbyHeaderProps = {
}; };
export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) { export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const space = useSpace(); const space = useSpace();
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const screenSize = useScreenSizeContext();
const name = useRoomName(space); const name = useRoomName(space);
const avatarMxc = useRoomAvatar(space); const avatarMxc = useRoomAvatar(space);
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined; const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => { const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect()); setMenuAnchor(evt.currentTarget.getBoundingClientRect());
}; };
return ( return (
<PageHeader className={showProfile ? undefined : css.Header}> <PageHeader className={showProfile ? undefined : css.Header} balance>
<Box grow="Yes" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
{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 grow="Yes" basis="No" />
<Box justifyContent="Center" alignItems="Center" gap="300"> <Box justifyContent="Center" alignItems="Center" gap="300">
{showProfile && ( {showProfile && (
@ -153,7 +181,15 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
</> </>
)} )}
</Box> </Box>
<Box shrink="No" grow="Yes" basis="No" justifyContent="End"> </>
)}
<Box
shrink="No"
grow={screenSize === ScreenSize.Mobile ? 'No' : 'Yes'}
basis={screenSize === ScreenSize.Mobile ? 'Yes' : 'No'}
justifyContent="End"
>
{screenSize !== ScreenSize.Mobile && (
<TooltipProvider <TooltipProvider
position="Bottom" position="Bottom"
offset={4} offset={4}
@ -169,6 +205,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
</IconButton> </IconButton>
)} )}
</TooltipProvider> </TooltipProvider>
)}
<TooltipProvider <TooltipProvider
position="Bottom" position="Bottom"
align="End" align="End"

View file

@ -11,15 +11,19 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer';
import * as css from './LobbyHero.css'; import * as css from './LobbyHero.css';
import { PageHero } from '../../components/page'; import { PageHero } from '../../components/page';
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard'; import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useSpecVersions } from '../../hooks/useSpecVersions';
export function LobbyHero() { export function LobbyHero() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const space = useSpace(); const space = useSpace();
const name = useRoomName(space); const name = useRoomName(space);
const topic = useRoomTopic(space); const topic = useRoomTopic(space);
const avatarMxc = useRoomAvatar(space); const avatarMxc = useRoomAvatar(space);
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined; const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
return ( return (
<PageHero <PageHero

View file

@ -39,6 +39,8 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { ErrorCode } from '../../cs-errorcode'; import { ErrorCode } from '../../cs-errorcode';
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
import { ItemDraggableTarget, useDraggableItem } from './DnD'; import { ItemDraggableTarget, useDraggableItem } from './DnD';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useSpecVersions } from '../../hooks/useSpecVersions';
type RoomJoinButtonProps = { type RoomJoinButtonProps = {
roomId: string; roomId: string;
@ -334,6 +336,8 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const { roomId, content } = item; const { roomId, content } = item;
const room = getRoom(roomId); const room = getRoom(roomId);
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
@ -364,7 +368,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
name={localSummary.name} name={localSummary.name}
topic={localSummary.topic} topic={localSummary.topic}
avatarUrl={ avatarUrl={
dm ? getDirectRoomAvatarUrl(mx, room, 96) : getRoomAvatarUrl(mx, room, 96) dm ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)
} }
memberCount={localSummary.memberCount} memberCount={localSummary.memberCount}
suggested={content.suggested} suggested={content.suggested}
@ -418,7 +422,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
topic={summaryState.data.topic} topic={summaryState.data.topic}
avatarUrl={ avatarUrl={
summaryState.data?.avatar_url summaryState.data?.avatar_url
? mx.mxcUrlToHttp(summaryState.data.avatar_url, 96, 96, 'crop') ?? ? mxcUrlToHttp(mx, summaryState.data.avatar_url, useAuthentication, 96, 96, 'crop') ??
undefined undefined
: undefined : undefined
} }

View file

@ -35,6 +35,8 @@ import { ErrorCode } from '../../cs-errorcode';
import { useDraggableItem } from './DnD'; import { useDraggableItem } from './DnD';
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation'; import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useSpecVersions } from '../../hooks/useSpecVersions';
function SpaceProfileLoading() { function SpaceProfileLoading() {
return ( return (
@ -408,6 +410,8 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const { roomId, content } = item; const { roomId, content } = item;
const space = getRoom(roomId); const space = getRoom(roomId);
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
@ -432,7 +436,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
<SpaceProfile <SpaceProfile
roomId={roomId} roomId={roomId}
name={localSummary.name} name={localSummary.name}
avatarUrl={getRoomAvatarUrl(mx, space, 96)} avatarUrl={getRoomAvatarUrl(mx, space, 96, useAuthentication)}
suggested={content.suggested} suggested={content.suggested}
closed={closed} closed={closed}
categoryId={categoryId} categoryId={categoryId}
@ -469,7 +473,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
name={summaryState.data.name || summaryState.data.canonical_alias || roomId} name={summaryState.data.name || summaryState.data.canonical_alias || roomId}
avatarUrl={ avatarUrl={
summaryState.data?.avatar_url summaryState.data?.avatar_url
? mx.mxcUrlToHttp(summaryState.data.avatar_url, 96, 96, 'crop') ?? ? mxcUrlToHttp(mx, summaryState.data.avatar_url, useAuthentication, 96, 96, 'crop') ??
undefined undefined
: undefined : undefined
} }

View file

@ -3,13 +3,17 @@ import React, { MouseEventHandler, useMemo } from 'react';
import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk'; import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser'; import { HTMLReactParserOptions } from 'html-react-parser';
import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds'; import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { import {
factoryRenderLinkifyWithMention,
getReactCustomHtmlParser, getReactCustomHtmlParser,
LINKIFY_OPTS,
makeHighlightRegex, makeHighlightRegex,
makeMentionCustomProps,
renderMatrixMention,
} from '../../plugins/react-custom-html-parser'; } from '../../plugins/react-custom-html-parser';
import { getMxIdLocalPart, isRoomId, isUserId } from '../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { openJoinAlias, openProfileViewer } from '../../../client/action/navigation';
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer'; import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room'; import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
import { import {
@ -31,8 +35,10 @@ import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../.
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import { ResultItem } from './useMessageSearch'; import { ResultItem } from './useMessageSearch';
import { SequenceCard } from '../../components/sequence-card'; import { SequenceCard } from '../../components/sequence-card';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { UserAvatar } from '../../components/user-avatar'; import { UserAvatar } from '../../components/user-avatar';
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
import { useSpecVersions } from '../../hooks/useSpecVersions';
type SearchResultGroupProps = { type SearchResultGroupProps = {
room: Room; room: Room;
@ -51,38 +57,32 @@ export function SearchResultGroup({
onOpen, onOpen,
}: SearchResultGroupProps) { }: SearchResultGroupProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { navigateRoom, navigateSpace } = useRoomNavigate(); const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]); 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>( const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() => () =>
getReactCustomHtmlParser(mx, room, { getReactCustomHtmlParser(mx, room.roomId, {
linkifyOpts,
highlightRegex, highlightRegex,
handleSpoilerClick: (evt) => { useAuthentication,
const target = evt.currentTarget; handleSpoilerClick: spoilerClickHandler,
if (target.getAttribute('aria-pressed') === 'true') { handleMentionClick: mentionClickHandler,
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);
},
}), }),
[mx, room, highlightRegex, navigateRoom, navigateSpace] [mx, room, linkifyOpts, highlightRegex, mentionClickHandler, spoilerClickHandler, useAuthentication]
); );
const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>( const renderMatrixEvent = useMatrixEventRenderer<[IEventWithRoomId, string, GetContentCallback]>(
@ -101,6 +101,7 @@ export function SearchResultGroup({
mediaAutoLoad={mediaAutoLoad} mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview} urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
highlightRegex={highlightRegex} highlightRegex={highlightRegex}
outlineAttachment outlineAttachment
/> />
@ -151,7 +152,7 @@ export function SearchResultGroup({
} }
); );
const handleOpenClick: MouseEventHandler<HTMLButtonElement> = (evt) => { const handleOpenClick: MouseEventHandler = (evt) => {
const eventId = evt.currentTarget.getAttribute('data-event-id'); const eventId = evt.currentTarget.getAttribute('data-event-id');
if (!eventId) return; if (!eventId) return;
onOpen(room.roomId, eventId); onOpen(room.roomId, eventId);
@ -164,7 +165,7 @@ export function SearchResultGroup({
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={getRoomAvatarUrl(mx, room, 96)} src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name} alt={room.name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled /> <RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
@ -186,15 +187,16 @@ export function SearchResultGroup({
event.sender; event.sender;
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender); const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
const relation = event.content['m.relates_to'];
const mainEventId = const mainEventId =
event.content['m.relates_to']?.rel_type === RelationType.Replace relation?.rel_type === RelationType.Replace ? relation.event_id : event.event_id;
? event.content['m.relates_to'].event_id
: event.event_id;
const getContent = (() => const getContent = (() =>
event.content['m.new_content'] ?? event.content) as GetContentCallback; event.content['m.new_content'] ?? event.content) as GetContentCallback;
const replyEventId = event.content['m.relates_to']?.['m.in_reply_to']?.event_id; const replyEventId = relation?.['m.in_reply_to']?.event_id;
const threadRootId =
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
return ( return (
<SequenceCard <SequenceCard
@ -211,7 +213,7 @@ export function SearchResultGroup({
userId={event.sender} userId={event.sender}
src={ src={
senderAvatarMxc senderAvatarMxc
? mx.mxcUrlToHttp(senderAvatarMxc, 48, 48, 'crop') ?? undefined ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
: undefined : undefined
} }
alt={displayName} alt={displayName}
@ -243,11 +245,10 @@ export function SearchResultGroup({
</Box> </Box>
{replyEventId && ( {replyEventId && (
<Reply <Reply
as="button"
mx={mx} mx={mx}
room={room} room={room}
eventId={replyEventId} replyEventId={replyEventId}
data-event-id={replyEventId} threadRootId={threadRootId}
onClick={handleOpenClick} onClick={handleOpenClick}
/> />
)} )}

View file

@ -28,25 +28,25 @@ import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom'; import { copyToClipboard } from '../../utils/dom';
import { getOriginBaseUrl, withOriginBaseUrl } from '../../pages/pathUtils';
import { markAsRead } from '../../../client/action/notifications'; import { markAsRead } from '../../../client/action/notifications';
import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation'; import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation';
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useClientConfig } from '../../hooks/useClientConfig';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers'; import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { TypingIndicator } from '../../components/typing-indicator'; import { TypingIndicator } from '../../components/typing-indicator';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
import { getViaServers } from '../../plugins/via-servers';
import { useSpecVersions } from '../../hooks/useSpecVersions';
type RoomNavItemMenuProps = { type RoomNavItemMenuProps = {
room: Room; room: Room;
linkPath: string;
requestClose: () => void; requestClose: () => void;
}; };
const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>( const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
({ room, linkPath, requestClose }, ref) => { ({ room, requestClose }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { hashRouter } = useClientConfig();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
@ -63,7 +63,9 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
}; };
const handleCopyLink = () => { 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(); requestClose();
}; };
@ -174,6 +176,8 @@ export function RoomNavItem({
linkPath, linkPath,
}: RoomNavItemProps) { }: RoomNavItemProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
@ -216,7 +220,7 @@ export function RoomNavItem({
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={ src={
direct ? getDirectRoomAvatarUrl(mx, room, 96) : getRoomAvatarUrl(mx, room, 96) direct ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)
} }
alt={room.name} alt={room.name}
renderFallback={() => ( renderFallback={() => (
@ -273,11 +277,7 @@ export function RoomNavItem({
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<RoomNavItemMenu <RoomNavItemMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
room={room}
linkPath={linkPath}
requestClose={() => setMenuAnchor(undefined)}
/>
</FocusTrap> </FocusTrap>
} }
> >

View file

@ -55,6 +55,7 @@ import { ScrollTopContainer } from '../../components/scroll-top-container';
import { UserAvatar } from '../../components/user-avatar'; import { UserAvatar } from '../../components/user-avatar';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers'; import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { useSpecVersions } from '../../hooks/useSpecVersions';
export const MembershipFilters = { export const MembershipFilters = {
filterJoined: (m: RoomMember) => m.membership === Membership.Join, filterJoined: (m: RoomMember) => m.membership === Membership.Join,
@ -171,6 +172,8 @@ type MembersDrawerProps = {
}; };
export function MembersDrawer({ room, members }: MembersDrawerProps) { export function MembersDrawer({ room, members }: MembersDrawerProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const scrollTopAnchorRef = useRef<HTMLDivElement>(null); const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
@ -426,8 +429,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
}} }}
after={<Icon size="50" src={Icons.Cross} />} after={<Icon size="50" src={Icons.Cross} />}
> >
<Text size="B300">{`${result.items.length || 'No'} ${ <Text size="B300">{`${result.items.length || 'No'} ${result.items.length === 1 ? 'Result' : 'Results'
result.items.length === 1 ? 'Result' : 'Results'
}`}</Text> }`}</Text>
</Chip> </Chip>
) )
@ -483,14 +485,16 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const member = tagOrMember; const member = tagOrMember;
const name = getName(member); const name = getName(member);
const avatarUrl = member.getAvatarUrl( const avatarMxcUrl = member.getMxcAvatarUrl();
mx.baseUrl, const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(
avatarMxcUrl,
100, 100,
100, 100,
'crop', 'crop',
undefined, undefined,
false false,
); useAuthentication
) : undefined;
return ( return (
<MenuItem <MenuItem

View file

@ -10,7 +10,7 @@ import React, {
} from 'react'; } from 'react';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { EventType, IContent, MsgType, Room } from 'matrix-js-sdk'; import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk';
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import { Transforms, Editor } from 'slate'; import { Transforms, Editor } from 'slate';
import { import {
@ -56,7 +56,7 @@ import {
} from '../../components/editor'; } from '../../components/editor';
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board'; import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart } from '../../utils/matrix'; import { TUploadContent, encryptFile, getImageInfo, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater'; import { useTypingStatusUpdater } from '../../hooks/useTypingStatusUpdater';
import { useFilePicker } from '../../hooks/useFilePicker'; import { useFilePicker } from '../../hooks/useFilePicker';
import { useFilePasteHandler } from '../../hooks/useFilePasteHandler'; import { useFilePasteHandler } from '../../hooks/useFilePasteHandler';
@ -106,8 +106,9 @@ import { CommandAutocomplete } from './CommandAutocomplete';
import { Command, SHRUG, useCommands } from '../../hooks/useCommands'; import { Command, SHRUG, useCommands } from '../../hooks/useCommands';
import { mobileOrTablet } from '../../utils/user-agent'; import { mobileOrTablet } from '../../utils/user-agent';
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver'; import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
import { ReplyLayout } from '../../components/message'; import { ReplyLayout, ThreadIndicator } from '../../components/message';
import { roomToParentsAtom } from '../../state/room/roomToParents'; import { roomToParentsAtom } from '../../state/room/roomToParents';
import { useSpecVersions } from '../../hooks/useSpecVersions';
interface RoomInputProps { interface RoomInputProps {
editor: Editor; editor: Editor;
@ -118,6 +119,8 @@ interface RoomInputProps {
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>( export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
({ editor, fileDropContainerRef, roomId, room }, ref) => { ({ editor, fileDropContainerRef, roomId, room }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const commands = useCommands(mx, room); const commands = useCommands(mx, room);
@ -186,9 +189,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
Transforms.insertFragment(editor, msgDraft); Transforms.insertFragment(editor, msgDraft);
}, [editor, msgDraft]); }, [editor, msgDraft]);
useEffect(() => { useEffect(
if (!mobileOrTablet()) ReactEditor.focus(editor); () => () => {
return () => {
if (!isEmptyEditor(editor)) { if (!isEmptyEditor(editor)) {
const parsedDraft = JSON.parse(JSON.stringify(editor.children)); const parsedDraft = JSON.parse(JSON.stringify(editor.children));
setMsgDraft(parsedDraft); setMsgDraft(parsedDraft);
@ -197,8 +199,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
} }
resetEditor(editor); resetEditor(editor);
resetEditorHistory(editor); resetEditorHistory(editor);
}; },
}, [roomId, editor, setMsgDraft]); [roomId, editor, setMsgDraft]
);
const handleRemoveUpload = useCallback( const handleRemoveUpload = useCallback(
(upload: TUploadContent | TUploadContent[]) => { (upload: TUploadContent | TUploadContent[]) => {
@ -310,6 +313,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
event_id: replyDraft.eventId, event_id: replyDraft.eventId,
}, },
}; };
if (replyDraft.relation?.rel_type === RelationType.Thread) {
content['m.relates_to'].event_id = replyDraft.relation.event_id;
content['m.relates_to'].rel_type = RelationType.Thread;
content['m.relates_to'].is_falling_back = false;
}
} }
mx.sendMessage(roomId, content); mx.sendMessage(roomId, content);
resetEditor(editor); resetEditor(editor);
@ -361,7 +369,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
}; };
const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => { const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => {
const stickerUrl = mx.mxcUrlToHttp(mxc); const stickerUrl = mxcUrlToHttp(mx, mxc, useAuthentication);
if (!stickerUrl) return; if (!stickerUrl) return;
const info = await getImageInfo( const info = await getImageInfo(
@ -489,6 +497,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
> >
<Icon src={Icons.Cross} size="50" /> <Icon src={Icons.Cross} size="50" />
</IconButton> </IconButton>
<Box direction="Column">
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
<ReplyLayout <ReplyLayout
userColor={colorMXID(replyDraft.userId)} userColor={colorMXID(replyDraft.userId)}
username={ username={
@ -506,6 +516,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
</Text> </Text>
</ReplyLayout> </ReplyLayout>
</Box> </Box>
</Box>
</div> </div>
) )
} }

View file

@ -16,6 +16,7 @@ import {
EventTimeline, EventTimeline,
EventTimelineSet, EventTimelineSet,
EventTimelineSetHandlerMap, EventTimelineSetHandlerMap,
IContent,
IEncryptedFile, IEncryptedFile,
MatrixClient, MatrixClient,
MatrixEvent, MatrixEvent,
@ -45,13 +46,13 @@ import {
toRem, toRem,
} from 'folds'; } from 'folds';
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { useTranslation } from 'react-i18next';
import { import {
decryptFile, decryptFile,
eventWithShortcode, eventWithShortcode,
factoryEventSentBy, factoryEventSentBy,
getMxIdLocalPart, getMxIdLocalPart,
isRoomId,
isUserId,
} from '../../utils/matrix'; } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator'; import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
@ -70,7 +71,13 @@ import {
ImageContent, ImageContent,
EventContent, EventContent,
} from '../../components/message'; } 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 { import {
canEditEvent, canEditEvent,
decryptAllTimelineEvent, decryptAllTimelineEvent,
@ -85,7 +92,7 @@ import {
} from '../../utils/room'; } from '../../utils/room';
import { useSetting } from '../../state/hooks/settings'; import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/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 { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
import { Reactions, Message, Event, EncryptedContent } from './message'; import { Reactions, Message, Event, EncryptedContent } from './message';
import { useMemberEventParser } from '../../hooks/useMemberEventParser'; import { useMemberEventParser } from '../../hooks/useMemberEventParser';
@ -109,10 +116,13 @@ import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
import { RenderMessageContent } from '../../components/RenderMessageContent'; import { RenderMessageContent } from '../../components/RenderMessageContent';
import { Image } from '../../components/media'; import { Image } from '../../components/media';
import { ImageViewer } from '../../components/image-viewer'; import { ImageViewer } from '../../components/image-viewer';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { roomToParentsAtom } from '../../state/room/roomToParents'; import { roomToParentsAtom } from '../../state/room/roomToParents';
import { useRoomUnread } from '../../state/hooks/unread'; import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useSpecVersions } from '../../hooks/useSpecVersions';
const TimelineFloat = as<'div', css.TimelineFloatVariants>( const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => ( ({ position, className, ...props }, ref) => (
@ -430,6 +440,8 @@ const getRoomUnreadInfo = (room: Room, scrollTo = false) => {
export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) { export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const encryptedRoom = mx.isRoomEncrypted(room.roomId); const encryptedRoom = mx.isRoomEncrypted(room.roomId);
const [messageLayout] = useSetting(settingsAtom, 'messageLayout'); const [messageLayout] = useSetting(settingsAtom, 'messageLayout');
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing'); const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
@ -447,9 +459,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const canRedact = canDoAction('redact', myPowerLevel); const canRedact = canDoAction('redact', myPowerLevel);
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel); const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
const [editId, setEditId] = useState<string>(); const [editId, setEditId] = useState<string>();
const { navigateRoom, navigateSpace } = useRoomNavigate();
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const { navigateRoom } = useRoomNavigate();
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
const imagePackRooms: Room[] = useMemo(() => { const imagePackRooms: Room[] = useMemo(() => {
const allParentSpaces = [room.roomId].concat( const allParentSpaces = [room.roomId].concat(
@ -489,34 +503,24 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
>(); >();
const alive = useAlive(); 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>( const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() => () =>
getReactCustomHtmlParser(mx, room, { getReactCustomHtmlParser(mx, room.roomId, {
handleSpoilerClick: (evt) => { linkifyOpts,
const target = evt.currentTarget; useAuthentication,
if (target.getAttribute('aria-pressed') === 'true') { handleSpoilerClick: spoilerClickHandler,
evt.stopPropagation(); handleMentionClick: mentionClickHandler,
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);
},
}), }),
[mx, room, navigateRoom, navigateSpace] [mx, room, linkifyOpts, spoilerClickHandler, mentionClickHandler, useAuthentication]
); );
const parseMemberEvent = useMemberEventParser(); const parseMemberEvent = useMemberEventParser();
@ -599,7 +603,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
// so timeline can be updated with evt like: edits, reactions etc // so timeline can be updated with evt like: edits, reactions etc
if (atBottomRef.current) { if (atBottomRef.current) {
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) { if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId())); requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!));
} }
if (document.hasFocus()) { if (document.hasFocus()) {
@ -728,6 +732,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const editableEvtId = editableEvt?.getId(); const editableEvtId = editableEvt?.getId();
if (!editableEvtId) return; if (!editableEvtId) return;
setEditId(editableEvtId); setEditId(editableEvtId);
evt.preventDefault()
} }
}, },
[mx, room, editor] [mx, room, editor]
@ -821,6 +826,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
}, [scrollToElement, editId]); }, [scrollToElement, editId]);
const handleJumpToLatest = () => { const handleJumpToLatest = () => {
if (eventId) {
navigateRoom(room.roomId, undefined, { replace: true });
}
setTimeline(getInitialTimeline(room)); setTimeline(getInitialTimeline(room));
scrollToBottomRef.current.count += 1; scrollToBottomRef.current.count += 1;
scrollToBottomRef.current.smooth = false; scrollToBottomRef.current.smooth = false;
@ -837,13 +845,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
markAsRead(mx, room.roomId); markAsRead(mx, room.roomId);
}; };
const handleOpenReply: MouseEventHandler<HTMLButtonElement> = useCallback( const handleOpenReply: MouseEventHandler = useCallback(
async (evt) => { async (evt) => {
const replyId = evt.currentTarget.getAttribute('data-reply-id'); const targetId = evt.currentTarget.getAttribute('data-event-id');
if (typeof replyId !== 'string') return; if (!targetId) return;
const replyTimeline = getEventTimeline(room, replyId); const replyTimeline = getEventTimeline(room, targetId);
const absoluteIndex = const absoluteIndex =
replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, replyId); replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, targetId);
if (typeof absoluteIndex === 'number') { if (typeof absoluteIndex === 'number') {
scrollToItem(absoluteIndex, { scrollToItem(absoluteIndex, {
@ -858,7 +866,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
}); });
} else { } else {
setTimeline(getEmptyTimeline()); setTimeline(getEmptyTimeline());
loadEventTimeline(replyId); loadEventTimeline(targetId);
} }
}, },
[room, timeline, scrollToItem, loadEventTimeline] [room, timeline, scrollToItem, loadEventTimeline]
@ -909,8 +917,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const replyEvt = room.findEventById(replyId); const replyEvt = room.findEventById(replyId);
if (!replyEvt) return; if (!replyEvt) return;
const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet()); const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet());
const { body, formatted_body: formattedBody }: Record<string, string> = const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent(); const { body, formatted_body: formattedBody } = content;
const { 'm.relates_to': relation } = replyEvt.getOriginalContent();
const senderId = replyEvt.getSender(); const senderId = replyEvt.getSender();
if (senderId && typeof body === 'string') { if (senderId && typeof body === 'string') {
setReplyDraft({ setReplyDraft({
@ -918,6 +927,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
eventId: replyId, eventId: replyId,
body, body,
formattedBody, formattedBody,
relation,
}); });
setTimeout(() => ReactEditor.focus(editor), 100); setTimeout(() => ReactEditor.focus(editor), 100);
} }
@ -959,6 +969,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
}, },
[editor] [editor]
); );
const { t } = useTranslation();
const renderMatrixEvent = useMatrixEventRenderer< const renderMatrixEvent = useMatrixEventRenderer<
[string, MatrixEvent, number, EventTimelineSet, boolean] [string, MatrixEvent, number, EventTimelineSet, boolean]
@ -968,7 +979,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const reactionRelations = getEventReactions(timelineSet, mEventId); const reactionRelations = getEventReactions(timelineSet, mEventId);
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey(); const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
const hasReactions = reactions && reactions.length > 0; const hasReactions = reactions && reactions.length > 0;
const { replyEventId } = mEvent; const { replyEventId, threadRootId } = mEvent;
const highlighted = focusItem?.index === item && focusItem.highlight; const highlighted = focusItem?.index === item && focusItem.highlight;
const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet); const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet);
@ -1003,12 +1014,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
reply={ reply={
replyEventId && ( replyEventId && (
<Reply <Reply
as="button"
mx={mx} mx={mx}
room={room} room={room}
timelineSet={timelineSet} timelineSet={timelineSet}
eventId={replyEventId} replyEventId={replyEventId}
data-reply-id={replyEventId} threadRootId={threadRootId}
onClick={handleOpenReply} onClick={handleOpenReply}
/> />
) )
@ -1038,6 +1048,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
mediaAutoLoad={mediaAutoLoad} mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview} urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
outlineAttachment={messageLayout === 2} outlineAttachment={messageLayout === 2}
/> />
)} )}
@ -1048,7 +1059,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const reactionRelations = getEventReactions(timelineSet, mEventId); const reactionRelations = getEventReactions(timelineSet, mEventId);
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey(); const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
const hasReactions = reactions && reactions.length > 0; const hasReactions = reactions && reactions.length > 0;
const { replyEventId } = mEvent; const { replyEventId, threadRootId } = mEvent;
const highlighted = focusItem?.index === item && focusItem.highlight; const highlighted = focusItem?.index === item && focusItem.highlight;
return ( return (
@ -1075,12 +1086,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
reply={ reply={
replyEventId && ( replyEventId && (
<Reply <Reply
as="button"
mx={mx} mx={mx}
room={room} room={room}
timelineSet={timelineSet} timelineSet={timelineSet}
eventId={replyEventId} replyEventId={replyEventId}
data-reply-id={replyEventId} threadRootId={threadRootId}
onClick={handleOpenReply} onClick={handleOpenReply}
/> />
) )
@ -1134,6 +1144,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
mediaAutoLoad={mediaAutoLoad} mediaAutoLoad={mediaAutoLoad}
urlPreview={showUrlPreview} urlPreview={showUrlPreview}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
outlineAttachment={messageLayout === 2} outlineAttachment={messageLayout === 2}
/> />
); );
@ -1272,7 +1283,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Box grow="Yes" direction="Column"> <Box grow="Yes" direction="Column">
<Text size="T300" priority="300"> <Text size="T300" priority="300">
<b>{senderName}</b> <b>{senderName}</b>
{' changed room name'} {t('Organisms.RoomCommon.changed_room_name')}
</Text> </Text>
</Box> </Box>
} }
@ -1550,8 +1561,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
{!canPaginateBack && rangeAtStart && getItems().length > 0 && ( {!canPaginateBack && rangeAtStart && getItems().length > 0 && (
<div <div
style={{ style={{
padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${ padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${messageLayout === 1 ? config.space.S400 : toRem(64)
messageLayout === 1 ? config.space.S400 : toRem(64)
}`, }`,
}} }}
> >

View file

@ -3,11 +3,11 @@ import { Box, Button, Spinner, Text, color } from 'folds';
import * as css from './RoomTombstone.css'; import * as css from './RoomTombstone.css';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { genRoomVia } from '../../../util/matrixUtil';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { Membership } from '../../../types/matrix/room'; import { Membership } from '../../../types/matrix/room';
import { RoomInputPlaceholder } from './RoomInputPlaceholder'; import { RoomInputPlaceholder } from './RoomInputPlaceholder';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { getViaServers } from '../../plugins/via-servers';
type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string }; type RoomTombstoneProps = { roomId: string; body?: string; replacementRoomId: string };
export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) { export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstoneProps) {
@ -17,7 +17,7 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
const [joinState, handleJoin] = useAsyncCallback( const [joinState, handleJoin] = useAsyncCallback(
useCallback(() => { useCallback(() => {
const currentRoom = mx.getRoom(roomId); const currentRoom = mx.getRoom(roomId);
const via = currentRoom ? genRoomVia(currentRoom) : []; const via = currentRoom ? getViaServers(currentRoom) : [];
return mx.joinRoom(replacementRoomId, { return mx.joinRoom(replacementRoomId, {
viaServers: via, viaServers: via,
}); });

View file

@ -25,6 +25,7 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
if (evt.metaKey || evt.altKey || evt.ctrlKey) { if (evt.metaKey || evt.altKey || evt.ctrlKey) {
return false; return false;
} }
// do not focus on F keys // do not focus on F keys
if (/^F\d+$/.test(code)) return false; if (/^F\d+$/.test(code)) return false;
@ -36,6 +37,9 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
code.startsWith('Alt') || code.startsWith('Alt') ||
code.startsWith('Control') || code.startsWith('Control') ||
code.startsWith('Arrow') || code.startsWith('Arrow') ||
code.startsWith('Page') ||
code.startsWith('End') ||
code.startsWith('Home') ||
code === 'Tab' || code === 'Tab' ||
code === 'Space' || code === 'Space' ||
code === 'Enter' || code === 'Enter' ||

View file

@ -20,7 +20,7 @@ import {
PopOut, PopOut,
RectCords, RectCords,
} from 'folds'; } from 'folds';
import { useLocation, useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { JoinRule, Room } from 'matrix-js-sdk'; import { JoinRule, Room } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
@ -35,15 +35,8 @@ import { useRoom } from '../../hooks/useRoom';
import { useSetSetting } from '../../state/hooks/settings'; import { useSetSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace'; import { useSpaceOptionally } from '../../hooks/useSpace';
import { import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
getHomeSearchPath, import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '../../utils/matrix';
getOriginBaseUrl,
getSpaceSearchPath,
joinPathComponent,
withOriginBaseUrl,
withSearchParam,
} from '../../pages/pathUtils';
import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
import { _SearchPathSearchParams } from '../../pages/paths'; import { _SearchPathSearchParams } from '../../pages/paths';
import * as css from './RoomViewHeader.css'; import * as css from './RoomViewHeader.css';
import { useRoomUnread } from '../../state/hooks/unread'; import { useRoomUnread } from '../../state/hooks/unread';
@ -55,19 +48,19 @@ import { copyToClipboard } from '../../utils/dom';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta'; import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList'; import { mDirectAtom } from '../../state/mDirectList';
import { useClientConfig } from '../../hooks/useClientConfig';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getViaServers } from '../../plugins/via-servers';
import { BackRouteHandler } from '../../components/BackRouteHandler';
import { useSpecVersions } from '../../hooks/useSpecVersions';
type RoomMenuProps = { type RoomMenuProps = {
room: Room; room: Room;
linkPath: string;
requestClose: () => void; requestClose: () => void;
}; };
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>( const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose }, ref) => {
({ room, linkPath, requestClose }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { hashRouter } = useClientConfig();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevelsContext(); const powerLevels = usePowerLevelsContext();
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
@ -84,7 +77,9 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
}; };
const handleCopyLink = () => { 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(); requestClose();
}; };
@ -175,12 +170,13 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
</Box> </Box>
</Menu> </Menu>
); );
} });
);
export function RoomViewHeader() { export function RoomViewHeader() {
const navigate = useNavigate(); const navigate = useNavigate();
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const room = useRoom(); const room = useRoom();
const space = useSpaceOptionally(); const space = useSpaceOptionally();
@ -192,11 +188,9 @@ export function RoomViewHeader() {
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId)); const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
const name = useRoomName(room); const name = useRoomName(room);
const topic = useRoomTopic(room); const topic = useRoomTopic(room);
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined; const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
const location = useLocation();
const currentPath = joinPathComponent(location);
const handleSearchClick = () => { const handleSearchClick = () => {
const searchParams: _SearchPathSearchParams = { const searchParams: _SearchPathSearchParams = {
@ -213,19 +207,36 @@ export function RoomViewHeader() {
}; };
return ( return (
<PageHeader> <PageHeader balance={screenSize === ScreenSize.Mobile}>
<Box grow="Yes" gap="300"> <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"> <Box grow="Yes" alignItems="Center" gap="300">
{screenSize !== ScreenSize.Mobile && (
<Avatar size="300"> <Avatar size="300">
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={avatarUrl} src={avatarUrl}
alt={name} alt={name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon size="200" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled /> <RoomIcon
size="200"
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
filled
/>
)} )}
/> />
</Avatar> </Avatar>
)}
<Box direction="Column"> <Box direction="Column">
<Text size={topic ? 'H5' : 'H3'} truncate> <Text size={topic ? 'H5' : 'H3'} truncate>
{name} {name}
@ -336,11 +347,7 @@ export function RoomViewHeader() {
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<RoomMenu <RoomMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
room={room}
linkPath={currentPath}
requestClose={() => setMenuAnchor(undefined)}
/>
</FocusTrap> </FocusTrap>
} }
/> />

View file

@ -51,7 +51,7 @@ import {
getMemberAvatarMxc, getMemberAvatarMxc,
getMemberDisplayName, getMemberDisplayName,
} from '../../../utils/room'; } from '../../../utils/room';
import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../../utils/matrix'; import { getCanonicalAliasOrRoomId, getMxIdLocalPart, isRoomAlias, mxcUrlToHttp } from '../../../utils/matrix';
import { MessageLayout, MessageSpacing } from '../../../state/settings'; import { MessageLayout, MessageSpacing } from '../../../state/settings';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../../hooks/useRecentEmoji'; import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
@ -63,18 +63,11 @@ import { EmojiBoard } from '../../../components/emoji-board';
import { ReactionViewer } from '../reaction-viewer'; import { ReactionViewer } from '../reaction-viewer';
import { MessageEditor } from './MessageEditor'; import { MessageEditor } from './MessageEditor';
import { UserAvatar } from '../../../components/user-avatar'; 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 { copyToClipboard } from '../../../utils/dom';
import { useClientConfig } from '../../../hooks/useClientConfig';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoomEvent } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void; export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
@ -321,23 +314,13 @@ export const MessageCopyLinkItem = as<
} }
>(({ room, mEvent, onClose, ...props }, ref) => { >(({ room, mEvent, onClose, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { hashRouter } = useClientConfig();
const space = useSpaceOptionally();
const directSelected = useDirectSelected();
const handleCopy = () => { const handleCopy = () => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
let eventPath = getHomeRoomPath(roomIdOrAlias, mEvent.getId()); const eventId = mEvent.getId();
if (space) { const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
eventPath = getSpaceRoomPath( if (!eventId) return;
getCanonicalAliasOrRoomId(mx, space.roomId), copyToClipboard(getMatrixToRoomEvent(roomIdOrAlias, eventId, viaServers));
roomIdOrAlias,
mEvent.getId()
);
} else if (directSelected) {
eventPath = getDirectRoomPath(roomIdOrAlias, mEvent.getId());
}
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), eventPath));
onClose?.(); onClose?.();
}; };
@ -668,6 +651,8 @@ export const Message = as<'div', MessageProps>(
ref ref
) => { ) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const senderId = mEvent.getSender() ?? ''; const senderId = mEvent.getSender() ?? '';
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
@ -727,7 +712,7 @@ export const Message = as<'div', MessageProps>(
userId={senderId} userId={senderId}
src={ src={
senderAvatarMxc senderAvatarMxc
? mx.mxcUrlToHttp(senderAvatarMxc, 48, 48, 'crop') ?? undefined ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
: undefined : undefined
} }
alt={senderDisplayName} alt={senderDisplayName}

View file

@ -22,6 +22,7 @@ import { useRelations } from '../../../hooks/useRelations';
import * as css from './styles.css'; import * as css from './styles.css';
import { ReactionViewer } from '../reaction-viewer'; import { ReactionViewer } from '../reaction-viewer';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
export type ReactionsProps = { export type ReactionsProps = {
room: Room; room: Room;
@ -33,6 +34,8 @@ export type ReactionsProps = {
export const Reactions = as<'div', ReactionsProps>( export const Reactions = as<'div', ReactionsProps>(
({ className, room, relations, mEventId, canSendReaction, onReactionToggle, ...props }, ref) => { ({ className, room, relations, mEventId, canSendReaction, onReactionToggle, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [viewer, setViewer] = useState<boolean | string>(false); const [viewer, setViewer] = useState<boolean | string>(false);
const myUserId = mx.getUserId(); const myUserId = mx.getUserId();
const reactions = useRelations( const reactions = useRelations(
@ -86,6 +89,7 @@ export const Reactions = as<'div', ReactionsProps>(
onClick={canSendReaction ? () => onReactionToggle(mEventId, key) : undefined} onClick={canSendReaction ? () => onReactionToggle(mEventId, key) : undefined}
onContextMenu={handleViewReaction} onContextMenu={handleViewReaction}
aria-disabled={!canSendReaction} aria-disabled={!canSendReaction}
useAuthentication={useAuthentication}
/> />
)} )}
</TooltipProvider> </TooltipProvider>

View file

@ -25,6 +25,7 @@ import { useRelations } from '../../../hooks/useRelations';
import { Reaction } from '../../../components/message'; import { Reaction } from '../../../components/message';
import { getHexcodeForEmoji, getShortcodeFor } from '../../../plugins/emoji'; import { getHexcodeForEmoji, getShortcodeFor } from '../../../plugins/emoji';
import { UserAvatar } from '../../../components/user-avatar'; import { UserAvatar } from '../../../components/user-avatar';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
export type ReactionViewerProps = { export type ReactionViewerProps = {
room: Room; room: Room;
@ -35,6 +36,8 @@ export type ReactionViewerProps = {
export const ReactionViewer = as<'div', ReactionViewerProps>( export const ReactionViewer = as<'div', ReactionViewerProps>(
({ className, room, initialKey, relations, requestClose, ...props }, ref) => { ({ className, room, initialKey, relations, requestClose, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const reactions = useRelations( const reactions = useRelations(
relations, relations,
useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], []) useCallback((rel) => [...(rel.getSortedAnnotationsByKey() ?? [])], [])
@ -81,6 +84,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
count={evts.size} count={evts.size}
aria-selected={key === selectedKey} aria-selected={key === selectedKey}
onClick={() => setSelectedKey(key)} onClick={() => setSelectedKey(key)}
useAuthentication={useAuthentication}
/> />
); );
})} })}
@ -107,14 +111,16 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
const member = room.getMember(senderId); const member = room.getMember(senderId);
const name = (member ? getName(member) : getMxIdLocalPart(senderId)) ?? senderId; const name = (member ? getName(member) : getMxIdLocalPart(senderId)) ?? senderId;
const avatarUrl = member?.getAvatarUrl( const avatarMxcUrl = member?.getMxcAvatarUrl();
mx.baseUrl, const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(
avatarMxcUrl,
100, 100,
100, 100,
'crop', 'crop',
undefined, undefined,
false false,
); useAuthentication
) : undefined;
return ( return (
<MenuItem <MenuItem

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

View file

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

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

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

View file

@ -1,16 +1,10 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth'; import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth';
import { WithRequiredProp } from '../../types/utils';
export type Required_SSOFlow = WithRequiredProp<ISSOFlow, 'identity_providers'>; export const getSSOFlow = (loginFlows: LoginFlow[]): ISSOFlow | undefined =>
export const getSSOFlow = (loginFlows: LoginFlow[]): Required_SSOFlow | undefined => loginFlows.find((flow) => flow.type === 'm.login.sso' || flow.type === 'm.login.cas') as
loginFlows.find( | ISSOFlow
(flow) => | undefined;
(flow.type === 'm.login.sso' || flow.type === 'm.login.cas') &&
'identity_providers' in flow &&
Array.isArray(flow.identity_providers) &&
flow.identity_providers.length > 0
) as Required_SSOFlow | undefined;
export const getPasswordFlow = (loginFlows: LoginFlow[]): IPasswordFlow | undefined => export const getPasswordFlow = (loginFlows: LoginFlow[]): IPasswordFlow | undefined =>
loginFlows.find((flow) => flow.type === 'm.login.password') as IPasswordFlow; loginFlows.find((flow) => flow.type === 'm.login.password') as IPasswordFlow;
@ -22,7 +16,7 @@ export const getTokenFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined =>
export type ParsedLoginFlows = { export type ParsedLoginFlows = {
password?: LoginFlow; password?: LoginFlow;
token?: LoginFlow; token?: LoginFlow;
sso?: Required_SSOFlow; sso?: ISSOFlow;
}; };
export const useParsedLoginFlows = (loginFlows: LoginFlow[]) => { export const useParsedLoginFlows = (loginFlows: LoginFlow[]) => {
const parsedFlow: ParsedLoginFlows = useMemo<ParsedLoginFlows>( const parsedFlow: ParsedLoginFlows = useMemo<ParsedLoginFlows>(

View file

@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { NavigateOptions, useNavigate } from 'react-router-dom';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { getCanonicalAliasOrRoomId } from '../utils/matrix'; import { getCanonicalAliasOrRoomId } from '../utils/matrix';
import { import {
@ -12,12 +12,14 @@ import { useMatrixClient } from './useMatrixClient';
import { getOrphanParents } from '../utils/room'; import { getOrphanParents } from '../utils/room';
import { roomToParentsAtom } from '../state/room/roomToParents'; import { roomToParentsAtom } from '../state/room/roomToParents';
import { mDirectAtom } from '../state/mDirectList'; import { mDirectAtom } from '../state/mDirectList';
import { useSelectedSpace } from './router/useSelectedSpace';
export const useRoomNavigate = () => { export const useRoomNavigate = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const mx = useMatrixClient(); const mx = useMatrixClient();
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const spaceSelectedId = useSelectedSpace();
const navigateSpace = useCallback( const navigateSpace = useCallback(
(roomId: string) => { (roomId: string) => {
@ -28,24 +30,29 @@ export const useRoomNavigate = () => {
); );
const navigateRoom = useCallback( const navigateRoom = useCallback(
(roomId: string, eventId?: string) => { (roomId: string, eventId?: string, opts?: NavigateOptions) => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
const orphanParents = getOrphanParents(roomToParents, roomId); const orphanParents = getOrphanParents(roomToParents, roomId);
if (orphanParents.length > 0) { if (orphanParents.length > 0) {
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, orphanParents[0]); const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId)); mx,
spaceSelectedId && orphanParents.includes(spaceSelectedId)
? spaceSelectedId
: orphanParents[0]
);
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
return; return;
} }
if (mDirects.has(roomId)) { if (mDirects.has(roomId)) {
navigate(getDirectRoomPath(roomIdOrAlias, eventId)); navigate(getDirectRoomPath(roomIdOrAlias, eventId), opts);
return; return;
} }
navigate(getHomeRoomPath(roomIdOrAlias, eventId)); navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
}, },
[mx, navigate, roomToParents, mDirects] [mx, navigate, spaceSelectedId, roomToParents, mDirects]
); );
return { return {

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

31
src/app/i18n.ts Normal file
View file

@ -0,0 +1,31 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend, { HttpBackendOptions } from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
import { trimTrailingSlash } from './utils/common';
i18n
// i18next-http-backend
// loads translations from your server
// https://github.com/i18next/i18next-http-backend
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init<HttpBackendOptions>({
debug: false,
fallbackLng: 'en',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
load: 'languageOnly',
backend: {
loadPath: `${trimTrailingSlash(import.meta.env.BASE_URL)}/public/locales/{{lng}}.json`,
},
});
export default i18n;

View file

@ -5,7 +5,7 @@ import './SpaceAddExisting.scss';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation'; 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 { Debounce } from '../../../util/common';
import Text from '../../atoms/text/Text'; 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 { allRoomsAtom } from '../../state/room-list/roomList';
import { mDirectAtom } from '../../state/mDirectList'; import { mDirectAtom } from '../../state/mDirectList';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getViaServers } from '../../plugins/via-servers';
function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) { function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
const mountStore = useStore(roomId); const mountStore = useStore(roomId);
@ -69,7 +70,7 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
const promises = selected.map((rId) => { const promises = selected.map((rId) => {
const room = mx.getRoom(rId); const room = mx.getRoom(rId);
const via = genRoomVia(room); const via = getViaServers(room);
if (via.length === 0) { if (via.length === 0) {
via.push(getIdServer(rId)); via.push(getIdServer(rId));
} }

View file

@ -41,7 +41,7 @@ import {
} from './pathUtils'; } from './pathUtils';
import { ClientBindAtoms, ClientLayout, ClientRoot } from './client'; import { ClientBindAtoms, ClientLayout, ClientRoot } from './client';
import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home'; 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 { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
import { Explore, FeaturedRooms, PublicRooms } from './client/explore'; import { Explore, FeaturedRooms, PublicRooms } from './client/explore';
import { Notifications, Inbox, Invites } from './client/inbox'; import { Notifications, Inbox, Invites } from './client/inbox';
@ -160,7 +160,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
} }
> >
{mobile ? null : <Route index element={<WelcomePage />} />} {mobile ? null : <Route index element={<WelcomePage />} />}
<Route path={_CREATE_PATH} element={<p>create</p>} /> <Route path={_CREATE_PATH} element={<DirectCreate />} />
<Route <Route
path={_ROOM_PATH} path={_ROOM_PATH}
element={ element={

View file

@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
v4.0.3 v4.1.0
</Text> </Text>
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer"> <Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
Twitter Twitter

View file

@ -29,6 +29,7 @@ import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo';
import { AuthFlowsLoader } from '../../components/AuthFlowsLoader'; import { AuthFlowsLoader } from '../../components/AuthFlowsLoader';
import { AuthFlowsProvider } from '../../hooks/useAuthFlows'; import { AuthFlowsProvider } from '../../hooks/useAuthFlows';
import { AuthServerProvider } from '../../hooks/useAuthServer'; import { AuthServerProvider } from '../../hooks/useAuthServer';
import { tryDecodeURIComponent } from '../../utils/dom';
const currentAuthPath = (pathname: string): string => { const currentAuthPath = (pathname: string): string => {
if (matchPath(LOGIN_PATH, pathname)) { if (matchPath(LOGIN_PATH, pathname)) {
@ -72,7 +73,7 @@ export function AuthLayout() {
const clientConfig = useClientConfig(); const clientConfig = useClientConfig();
const defaultServer = clientDefaultServer(clientConfig); const defaultServer = clientDefaultServer(clientConfig);
let server: string = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer; let server: string = urlEncodedServer ? tryDecodeURIComponent(urlEncodedServer) : defaultServer;
if (!clientAllowedServer(clientConfig, server)) { if (!clientAllowedServer(clientConfig, server)) {
server = defaultServer; server = defaultServer;
@ -94,7 +95,7 @@ export function AuthLayout() {
// if server is mismatches with path server, update path // if server is mismatches with path server, update path
useEffect(() => { useEffect(() => {
if (!urlEncodedServer || decodeURIComponent(urlEncodedServer) !== server) { if (!urlEncodedServer || tryDecodeURIComponent(urlEncodedServer) !== server) {
navigate( navigate(
generatePath(currentAuthPath(location.pathname), { generatePath(currentAuthPath(location.pathname), {
server: encodeURIComponent(server), server: encodeURIComponent(server),

View file

@ -4,30 +4,35 @@ import React, { useMemo } from 'react';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
type SSOLoginProps = { type SSOLoginProps = {
providers: IIdentityProvider[]; providers?: IIdentityProvider[];
asIcons?: boolean;
redirectUrl: string; redirectUrl: string;
saveScreenSpace?: boolean;
}; };
export function SSOLogin({ providers, redirectUrl, asIcons }: SSOLoginProps) { export function SSOLogin({ providers, redirectUrl, saveScreenSpace }: SSOLoginProps) {
const discovery = useAutoDiscoveryInfo(); const discovery = useAutoDiscoveryInfo();
const baseUrl = discovery['m.homeserver'].base_url; const baseUrl = discovery['m.homeserver'].base_url;
const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
const getSSOIdUrl = (ssoId: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId); const getSSOIdUrl = (ssoId?: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId);
const anyAsBtn = providers.find( const withoutIcon = providers
? providers.find(
(provider) => !provider.icon || !mx.mxcUrlToHttp(provider.icon, 96, 96, 'crop', false) (provider) => !provider.icon || !mx.mxcUrlToHttp(provider.icon, 96, 96, 'crop', false)
); )
: true;
const renderAsIcons = withoutIcon ? false : saveScreenSpace && providers && providers.length > 2;
return ( return (
<Box justifyContent="Center" gap="600" wrap="Wrap"> <Box justifyContent="Center" gap="600" wrap="Wrap">
{providers.map((provider) => { {providers ? (
providers.map((provider) => {
const { id, name, icon } = provider; const { id, name, icon } = provider;
const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false); const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false);
const buttonTitle = `Continue with ${name}`; const buttonTitle = `Continue with ${name}`;
if (!anyAsBtn && iconUrl && asIcons) { if (renderAsIcons) {
return ( return (
<Avatar <Avatar
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
@ -38,7 +43,7 @@ export function SSOLogin({ providers, redirectUrl, asIcons }: SSOLoginProps) {
size="300" size="300"
radii="300" radii="300"
> >
<AvatarImage src={iconUrl} alt={name} title={buttonTitle} /> <AvatarImage src={iconUrl!} alt={name} title={buttonTitle} />
</Avatar> </Avatar>
); );
} }
@ -66,7 +71,22 @@ export function SSOLogin({ providers, redirectUrl, asIcons }: SSOLoginProps) {
</Text> </Text>
</Button> </Button>
); );
})} })
) : (
<Button
style={{ width: '100%' }}
as="a"
href={getSSOIdUrl()}
size="500"
variant="Secondary"
fill="Soft"
outlined
>
<Text align="Center" size="B500" truncate>
Continue with SSO
</Text>
</Button>
)}
</Box> </Box>
); );
} }

View file

@ -76,9 +76,7 @@ export function Login() {
<SSOLogin <SSOLogin
providers={parsedFlows.sso.identity_providers} providers={parsedFlows.sso.identity_providers}
redirectUrl={ssoRedirectUrl} redirectUrl={ssoRedirectUrl}
asIcons={ saveScreenSpace={parsedFlows.password !== undefined}
parsedFlows.password !== undefined && parsedFlows.sso.identity_providers.length > 2
}
/> />
<span data-spacing-node /> <span data-spacing-node />
</> </>

View file

@ -83,10 +83,7 @@ export function Register() {
<SSOLogin <SSOLogin
providers={sso.identity_providers} providers={sso.identity_providers}
redirectUrl={ssoRedirectUrl} redirectUrl={ssoRedirectUrl}
asIcons={ saveScreenSpace={registerFlows.status === RegisterFlowStatus.FlowRequired}
registerFlows.status === RegisterFlowStatus.FlowRequired &&
sso.identity_providers.length > 2
}
/> />
<span data-spacing-node /> <span data-spacing-node />
</> </>

View file

@ -22,9 +22,10 @@ import {
isNotificationEvent, isNotificationEvent,
} from '../../utils/room'; } from '../../utils/room';
import { NotificationType, UnreadInfo } from '../../../types/matrix/room'; import { NotificationType, UnreadInfo } from '../../../types/matrix/room';
import { getMxIdLocalPart } from '../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
import { useInboxNotificationsSelected } from '../../hooks/router/useInbox'; import { useInboxNotificationsSelected } from '../../hooks/router/useInbox';
import { useSpecVersions } from '../../hooks/useSpecVersions';
function SystemEmojiFeature() { function SystemEmojiFeature() {
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
@ -132,6 +133,8 @@ function MessageNotifications() {
const notifRef = useRef<Notification>(); const notifRef = useRef<Notification>();
const unreadCacheRef = useRef<Map<string, UnreadInfo>>(new Map()); const unreadCacheRef = useRef<Map<string, UnreadInfo>>(new Map());
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const [showNotifications] = useSetting(settingsAtom, 'showNotifications'); const [showNotifications] = useSetting(settingsAtom, 'showNotifications');
const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds'); const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds');
@ -183,17 +186,17 @@ function MessageNotifications() {
removed, removed,
data data
) => { ) => {
if (mx.getSyncState() !== 'SYNCING') return;
if (document.hasFocus() && (selectedRoomId === room?.roomId || notificationSelected)) return;
if ( if (
mx.getSyncState() !== 'SYNCING' ||
selectedRoomId === room?.roomId ||
notificationSelected ||
!room || !room ||
!data.liveEvent || !data.liveEvent ||
room.isSpaceRoom() || room.isSpaceRoom() ||
!isNotificationEvent(mEvent) || !isNotificationEvent(mEvent) ||
getNotificationType(mx, room.roomId) === NotificationType.Mute getNotificationType(mx, room.roomId) === NotificationType.Mute
) ) {
return; return;
}
const sender = mEvent.getSender(); const sender = mEvent.getSender();
const eventId = mEvent.getId(); const eventId = mEvent.getId();
@ -216,7 +219,7 @@ function MessageNotifications() {
notify({ notify({
roomName: room.name ?? 'Unknown', roomName: room.name ?? 'Unknown',
roomAvatar: avatarMxc roomAvatar: avatarMxc
? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined, : undefined,
username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender, username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender,
roomId: room.roomId, roomId: room.roomId,

View file

@ -10,7 +10,15 @@ import {
SidebarItemTooltip, SidebarItemTooltip,
SidebarItem, SidebarItem,
} from '../../components/sidebar'; } 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'; import { openCreateRoom, openSearch } from '../../../client/action/navigation';
export function SidebarNav() { export function SidebarNav() {
@ -65,6 +73,8 @@ export function SidebarNav() {
</SidebarItemTooltip> </SidebarItemTooltip>
</SidebarItem> </SidebarItem>
<UnverifiedTab />
<InboxTab /> <InboxTab />
<UserTab /> <UserTab />
</SidebarStack> </SidebarStack>

View file

@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
v4.0.3 v4.1.0
</a> </a>
</span> </span>
} }

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

View file

@ -10,12 +10,12 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const rooms = useDirectRooms(); const rooms = useDirectRooms();
const { roomIdOrAlias } = useParams(); const { roomIdOrAlias, eventId } = useParams();
const roomId = useSelectedRoom(); const roomId = useSelectedRoom();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
if (!room || !rooms.includes(room.roomId)) { if (!room || !rooms.includes(room.roomId)) {
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />; return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} eventId={eventId} />;
} }
return ( return (

View file

@ -1,2 +1,3 @@
export * from './Direct'; export * from './Direct';
export * from './RoomProvider'; export * from './RoomProvider';
export * from './DirectCreate';

View file

@ -1,5 +1,5 @@
import React from 'react'; 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 { useAtomValue } from 'jotai';
import { useClientConfig } from '../../../hooks/useClientConfig'; import { useClientConfig } from '../../../hooks/useClientConfig';
import { RoomCard, RoomCardGrid } from '../../../components/room-card'; import { RoomCard, RoomCardGrid } from '../../../components/room-card';
@ -9,21 +9,38 @@ import {
Page, Page,
PageContent, PageContent,
PageContentCenter, PageContentCenter,
PageHeader,
PageHero, PageHero,
PageHeroSection, PageHeroSection,
} from '../../../components/page'; } from '../../../components/page';
import { RoomTopicViewer } from '../../../components/room-topic-viewer'; import { RoomTopicViewer } from '../../../components/room-topic-viewer';
import * as css from './style.css'; import * as css from './style.css';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
export function FeaturedRooms() { export function FeaturedRooms() {
const { featuredCommunities } = useClientConfig(); const { featuredCommunities } = useClientConfig();
const { rooms, spaces } = featuredCommunities ?? {}; const { rooms, spaces } = featuredCommunities ?? {};
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const screenSize = useScreenSizeContext();
const { navigateSpace, navigateRoom } = useRoomNavigate(); const { navigateSpace, navigateRoom } = useRoomNavigate();
return ( return (
<Page> <Page>
{screenSize === ScreenSize.Mobile && (
<PageHeader>
<Box shrink="No">
<BackRouteHandler>
{(onBack) => (
<IconButton onClick={onBack}>
<Icon src={Icons.ArrowLeft} />
</IconButton>
)}
</BackRouteHandler>
</Box>
</PageHeader>
)}
<Box grow="Yes"> <Box grow="Yes">
<Scroll hideTrack visibility="Hover"> <Scroll hideTrack visibility="Hover">
<PageContent> <PageContent>

View file

@ -13,6 +13,7 @@ import {
Button, Button,
Chip, Chip,
Icon, Icon,
IconButton,
Icons, Icons,
Input, Input,
Line, Line,
@ -42,6 +43,8 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { getMxIdServer } from '../../../utils/matrix'; import { getMxIdServer } from '../../../utils/matrix';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams => const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
useMemo( useMemo(
@ -344,6 +347,7 @@ export function PublicRooms() {
const userServer = userId && getMxIdServer(userId); const userServer = userId && getMxIdServer(userId);
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const { navigateSpace, navigateRoom } = useRoomNavigate(); const { navigateSpace, navigateRoom } = useRoomNavigate();
const screenSize = useScreenSizeContext();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const serverSearchParams = useServerSearchParams(searchParams); const serverSearchParams = useServerSearchParams(searchParams);
@ -466,7 +470,7 @@ export function PublicRooms() {
return ( return (
<Page> <Page>
<PageHeader> <PageHeader balance>
{isSearch ? ( {isSearch ? (
<> <>
<Box grow="Yes" basis="No"> <Box grow="Yes" basis="No">
@ -482,20 +486,34 @@ export function PublicRooms() {
</Box> </Box>
<Box grow="No" justifyContent="Center" alignItems="Center" gap="200"> <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> <Text size="H3" truncate>
Search Search
</Text> </Text>
</Box> </Box>
<Box grow="Yes" /> <Box grow="Yes" basis="No" />
</> </>
) : ( ) : (
<>
<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"> <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
<Icon size="400" src={Icons.Category} /> {screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Category} />}
<Text size="H3" truncate> <Text size="H3" truncate>
{server} {server}
</Text> </Text>
</Box> </Box>
<Box grow="Yes" basis="No" />
</>
)} )}
</PageHeader> </PageHeader>
<Box grow="Yes"> <Box grow="Yes">

View file

@ -5,17 +5,25 @@ import { RoomProvider } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate'; import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
import { useHomeRooms } from './useHomeRooms'; import { useHomeRooms } from './useHomeRooms';
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
export function HomeRouteRoomProvider({ children }: { children: ReactNode }) { export function HomeRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const rooms = useHomeRooms(); const rooms = useHomeRooms();
const { roomIdOrAlias } = useParams(); const { roomIdOrAlias, eventId } = useParams();
const viaServers = useSearchParamsViaServers();
const roomId = useSelectedRoom(); const roomId = useSelectedRoom();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
if (!room || !rooms.includes(room.roomId)) { if (!room || !rooms.includes(room.roomId)) {
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />; return (
<JoinBeforeNavigate
roomIdOrAlias={roomIdOrAlias!}
eventId={eventId}
viaServers={viaServers}
/>
);
} }
return ( return (

View file

@ -1,22 +1,39 @@
import React, { useRef } from 'react'; 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 { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { MessageSearch } from '../../../features/message-search'; import { MessageSearch } from '../../../features/message-search';
import { useHomeRooms } from './useHomeRooms'; import { useHomeRooms } from './useHomeRooms';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
export function HomeSearch() { export function HomeSearch() {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const rooms = useHomeRooms(); const rooms = useHomeRooms();
const screenSize = useScreenSizeContext();
return ( return (
<Page> <Page>
<PageHeader> <PageHeader balance>
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
<Icon size="400" src={Icons.Search} /> <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> <Text size="H3" truncate>
Message Search Message Search
</Text> </Text>
</Box> </Box>
<Box grow="Yes" basis="No" />
</Box>
</PageHeader> </PageHeader>
<Box style={{ position: 'relative' }} grow="Yes"> <Box style={{ position: 'relative' }} grow="Yes">
<Scroll ref={scrollRef} hideTrack visibility="Hover"> <Scroll ref={scrollRef} hideTrack visibility="Hover">

View file

@ -4,6 +4,7 @@ import {
Box, Box,
Button, Button,
Icon, Icon,
IconButton,
Icons, Icons,
Overlay, Overlay,
OverlayBackdrop, OverlayBackdrop,
@ -39,6 +40,9 @@ import { RoomTopicViewer } from '../../../components/room-topic-viewer';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { useRoomTopic } from '../../../hooks/useRoomMeta'; import { useRoomTopic } from '../../../hooks/useRoomMeta';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
const COMPACT_CARD_WIDTH = 548; const COMPACT_CARD_WIDTH = 548;
@ -51,6 +55,8 @@ type InviteCardProps = {
}; };
function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardProps) { function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const roomName = room.name || room.getCanonicalAlias() || room.roomId; const roomName = room.name || room.getCanonicalAlias() || room.roomId;
const member = room.getMember(userId); const member = room.getMember(userId);
const memberEvent = member?.events.member; const memberEvent = member?.events.member;
@ -107,7 +113,7 @@ function InviteCard({ room, userId, direct, compact, onNavigate }: InviteCardPro
<Avatar size="300"> <Avatar size="300">
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={direct ? getDirectRoomAvatarUrl(mx, room, 96) : getRoomAvatarUrl(mx, room, 96)} src={direct ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={roomName} alt={roomName}
renderFallback={() => ( renderFallback={() => (
<Text as="span" size="H6"> <Text as="span" size="H6">
@ -205,6 +211,7 @@ export function Invites() {
useCallback(() => containerRef.current, []), useCallback(() => containerRef.current, []),
useCallback((width) => setCompact(width <= COMPACT_CARD_WIDTH), []) useCallback((width) => setCompact(width <= COMPACT_CARD_WIDTH), [])
); );
const screenSize = useScreenSizeContext();
const { navigateRoom, navigateSpace } = useRoomNavigate(); const { navigateRoom, navigateSpace } = useRoomNavigate();
@ -225,13 +232,27 @@ export function Invites() {
return ( return (
<Page> <Page>
<PageHeader> <PageHeader balance>
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> <Box grow="Yes" gap="200">
<Icon size="400" src={Icons.Mail} /> <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> <Text size="H3" truncate>
Invitations Invitations
</Text> </Text>
</Box> </Box>
<Box grow="Yes" basis="No" />
</Box>
</PageHeader> </PageHeader>
<Box grow="Yes"> <Box grow="Yes">
<Scroll hideTrack visibility="Hover"> <Scroll hideTrack visibility="Hover">

View file

@ -20,13 +20,15 @@ import {
IRoomEvent, IRoomEvent,
JoinRule, JoinRule,
Method, Method,
RelationType,
Room, Room,
} from 'matrix-js-sdk'; } from 'matrix-js-sdk';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { HTMLReactParserOptions } from 'html-react-parser'; import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page'; import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart, isRoomId, isUserId } from '../../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { InboxNotificationsPathSearchParams } from '../../paths'; import { InboxNotificationsPathSearchParams } from '../../paths';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
@ -52,8 +54,13 @@ import {
Username, Username,
} from '../../../components/message'; } from '../../../components/message';
import colorMXID from '../../../../util/colorMXID'; import colorMXID from '../../../../util/colorMXID';
import { getReactCustomHtmlParser } from '../../../plugins/react-custom-html-parser'; import {
import { openJoinAlias, openProfileViewer } from '../../../../client/action/navigation'; factoryRenderLinkifyWithMention,
getReactCustomHtmlParser,
LINKIFY_OPTS,
makeMentionCustomProps,
renderMatrixMention,
} from '../../../plugins/react-custom-html-parser';
import { RenderMessageContent } from '../../../components/RenderMessageContent'; import { RenderMessageContent } from '../../../components/RenderMessageContent';
import { useSetting } from '../../../state/hooks/settings'; import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings'; import { settingsAtom } from '../../../state/settings';
@ -70,6 +77,11 @@ import { ContainerColor } from '../../../styles/ContainerColor.css';
import { VirtualTile } from '../../../components/virtualizer'; import { VirtualTile } from '../../../components/virtualizer';
import { UserAvatar } from '../../../components/user-avatar'; import { UserAvatar } from '../../../components/user-avatar';
import { EncryptedContent } from '../../../features/room/message'; 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';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
type RoomNotificationsGroup = { type RoomNotificationsGroup = {
roomId: string; roomId: string;
@ -180,37 +192,30 @@ function RoomNotificationsGroupComp({
onOpen, onOpen,
}: RoomNotificationsGroupProps) { }: RoomNotificationsGroupProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); 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>( const htmlReactParserOptions = useMemo<HTMLReactParserOptions>(
() => () =>
getReactCustomHtmlParser(mx, room, { getReactCustomHtmlParser(mx, room.roomId, {
handleSpoilerClick: (evt) => { linkifyOpts,
const target = evt.currentTarget; useAuthentication,
if (target.getAttribute('aria-pressed') === 'true') { handleSpoilerClick: spoilerClickHandler,
evt.stopPropagation(); handleMentionClick: mentionClickHandler,
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);
},
}), }),
[mx, room, navigateRoom, navigateSpace] [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication]
); );
const renderMatrixEvent = useMatrixEventRenderer<[IRoomEvent, string, GetContentCallback]>( const renderMatrixEvent = useMatrixEventRenderer<[IRoomEvent, string, GetContentCallback]>(
@ -229,6 +234,7 @@ function RoomNotificationsGroupComp({
mediaAutoLoad={mediaAutoLoad} mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview} urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
outlineAttachment outlineAttachment
/> />
); );
@ -287,6 +293,7 @@ function RoomNotificationsGroupComp({
mediaAutoLoad={mediaAutoLoad} mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview} urlPreview={urlPreview}
htmlReactParserOptions={htmlReactParserOptions} htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/> />
); );
} }
@ -350,7 +357,7 @@ function RoomNotificationsGroupComp({
} }
); );
const handleOpenClick: MouseEventHandler<HTMLButtonElement> = (evt) => { const handleOpenClick: MouseEventHandler = (evt) => {
const eventId = evt.currentTarget.getAttribute('data-event-id'); const eventId = evt.currentTarget.getAttribute('data-event-id');
if (!eventId) return; if (!eventId) return;
onOpen(room.roomId, eventId); onOpen(room.roomId, eventId);
@ -366,7 +373,7 @@ function RoomNotificationsGroupComp({
<Avatar size="200" radii="300"> <Avatar size="200" radii="300">
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={getRoomAvatarUrl(mx, room, 96)} src={getRoomAvatarUrl(mx, room, 96, useAuthentication)}
alt={room.name} alt={room.name}
renderFallback={() => ( renderFallback={() => (
<RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled /> <RoomIcon size="50" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
@ -401,7 +408,10 @@ function RoomNotificationsGroupComp({
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender); const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
const getContent = (() => event.content) as GetContentCallback; const getContent = (() => event.content) as GetContentCallback;
const replyEventId = event.content['m.relates_to']?.['m.in_reply_to']?.event_id; const relation = event.content['m.relates_to'];
const replyEventId = relation?.['m.in_reply_to']?.event_id;
const threadRootId =
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
return ( return (
<SequenceCard <SequenceCard
@ -418,7 +428,7 @@ function RoomNotificationsGroupComp({
userId={event.sender} userId={event.sender}
src={ src={
senderAvatarMxc senderAvatarMxc
? mx.mxcUrlToHttp(senderAvatarMxc, 48, 48, 'crop') ?? undefined ? mxcUrlToHttp(mx, senderAvatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined
: undefined : undefined
} }
alt={displayName} alt={displayName}
@ -450,11 +460,10 @@ function RoomNotificationsGroupComp({
</Box> </Box>
{replyEventId && ( {replyEventId && (
<Reply <Reply
as="button"
mx={mx} mx={mx}
room={room} room={room}
eventId={replyEventId} replyEventId={replyEventId}
data-event-id={replyEventId} threadRootId={threadRootId}
onClick={handleOpenClick} onClick={handleOpenClick}
/> />
)} )}
@ -484,6 +493,7 @@ export function Notifications() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const screenSize = useScreenSizeContext();
const { navigateRoom } = useRoomNavigate(); const { navigateRoom } = useRoomNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -549,13 +559,27 @@ export function Notifications() {
return ( return (
<Page> <Page>
<PageHeader> <PageHeader balance>
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> <Box grow="Yes" gap="200">
<Icon size="400" src={Icons.Message} /> <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> <Text size="H3" truncate>
Notification Messages Notification Messages
</Text> </Text>
</Box> </Box>
<Box grow="Yes" basis="No" />
</Box>
</PageHeader> </PageHeader>
<Box style={{ position: 'relative' }} grow="Yes"> <Box style={{ position: 'relative' }} grow="Yes">

View file

@ -47,13 +47,7 @@ import {
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { import { getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils';
getOriginBaseUrl,
getSpaceLobbyPath,
getSpacePath,
joinPathComponent,
withOriginBaseUrl,
} from '../../pathUtils';
import { import {
SidebarAvatar, SidebarAvatar,
SidebarItem, SidebarItem,
@ -67,7 +61,7 @@ import {
import { RoomUnreadProvider, RoomsUnreadProvider } from '../../../components/RoomUnreadProvider'; import { RoomUnreadProvider, RoomsUnreadProvider } from '../../../components/RoomUnreadProvider';
import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace'; import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
import { UnreadBadge } from '../../../components/unread-badge'; import { UnreadBadge } from '../../../components/unread-badge';
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
import { RoomAvatar } from '../../../components/room-avatar'; import { RoomAvatar } from '../../../components/room-avatar';
import { nameInitials, randomStr } from '../../../utils/common'; import { nameInitials, randomStr } from '../../../utils/common';
import { import {
@ -83,7 +77,6 @@ import { AccountDataEvent } from '../../../../types/matrix/accountData';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath'; import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder'; import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder';
import { useClientConfig } from '../../../hooks/useClientConfig';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import { useRoomsUnread } from '../../../state/hooks/unread'; import { useRoomsUnread } from '../../../state/hooks/unread';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
@ -91,6 +84,10 @@ import { markAsRead } from '../../../../client/action/notifications';
import { copyToClipboard } from '../../../utils/dom'; import { copyToClipboard } from '../../../utils/dom';
import { openInviteUser, openSpaceSettings } from '../../../../client/action/navigation'; import { openInviteUser, openSpaceSettings } from '../../../../client/action/navigation';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoom } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers';
import { getRoomAvatarUrl } from '../../../utils/room';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
type SpaceMenuProps = { type SpaceMenuProps = {
room: Room; room: Room;
@ -100,7 +97,6 @@ type SpaceMenuProps = {
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>( const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
({ room, requestClose, onUnpin }, ref) => { ({ room, requestClose, onUnpin }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { hashRouter } = useClientConfig();
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
@ -124,8 +120,9 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
}; };
const handleCopyLink = () => { const handleCopyLink = () => {
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, room.roomId)); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), spacePath)); const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
requestClose(); requestClose();
}; };
@ -384,6 +381,8 @@ function SpaceTab({
onUnpin, onUnpin,
}: SpaceTabProps) { }: SpaceTabProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
const spaceDraggable: SidebarDraggable = useMemo( const spaceDraggable: SidebarDraggable = useMemo(
@ -436,7 +435,7 @@ function SpaceTab({
> >
<RoomAvatar <RoomAvatar
roomId={space.roomId} roomId={space.roomId}
src={space.getAvatarUrl(mx.baseUrl, 96, 96, 'crop') ?? undefined} src={getRoomAvatarUrl(mx, space, 96, useAuthentication) ?? undefined}
alt={space.name} alt={space.name}
renderFallback={() => ( renderFallback={() => (
<Text size={folder ? 'H6' : 'H4'}>{nameInitials(space.name, 2)}</Text> <Text size={folder ? 'H6' : 'H4'}>{nameInitials(space.name, 2)}</Text>
@ -529,6 +528,8 @@ function ClosedSpaceFolder({
disabled, disabled,
}: ClosedSpaceFolderProps) { }: ClosedSpaceFolderProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const handlerRef = useRef<HTMLDivElement>(null); const handlerRef = useRef<HTMLDivElement>(null);
const spaceDraggable: FolderDraggable = useMemo(() => ({ folder }), [folder]); const spaceDraggable: FolderDraggable = useMemo(() => ({ folder }), [folder]);
@ -561,7 +562,7 @@ function ClosedSpaceFolder({
<SidebarAvatar key={sId} size="200" radii="300"> <SidebarAvatar key={sId} size="200" radii="300">
<RoomAvatar <RoomAvatar
roomId={space.roomId} roomId={space.roomId}
src={space.getAvatarUrl(mx.baseUrl, 96, 96, 'crop') ?? undefined} src={getRoomAvatarUrl(mx, space, 96, useAuthentication) ?? undefined}
alt={space.name} alt={space.name}
renderFallback={() => ( renderFallback={() => (
<Text size="Inherit"> <Text size="Inherit">

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

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

View file

@ -5,8 +5,9 @@ import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../compone
import { openSettings } from '../../../../client/action/navigation'; import { openSettings } from '../../../../client/action/navigation';
import { UserAvatar } from '../../../components/user-avatar'; import { UserAvatar } from '../../../components/user-avatar';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart } from '../../../utils/matrix'; import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { nameInitials } from '../../../utils/common'; import { nameInitials } from '../../../utils/common';
import { useSpecVersions } from '../../../hooks/useSpecVersions';
type UserProfile = { type UserProfile = {
avatar_url?: string; avatar_url?: string;
@ -14,12 +15,14 @@ type UserProfile = {
}; };
export function UserTab() { export function UserTab() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { versions } = useSpecVersions();
const useAuthentication = versions.includes('v1.11');
const userId = mx.getUserId()!; const userId = mx.getUserId()!;
const [profile, setProfile] = useState<UserProfile>({}); const [profile, setProfile] = useState<UserProfile>({});
const displayName = profile.displayname ?? getMxIdLocalPart(userId) ?? userId; const displayName = profile.displayname ?? getMxIdLocalPart(userId) ?? userId;
const avatarUrl = profile.avatar_url const avatarUrl = profile.avatar_url
? mx.mxcUrlToHttp(profile.avatar_url, 96, 96, 'crop') ?? undefined ? mxcUrlToHttp(mx, profile.avatar_url, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined; : undefined;
useEffect(() => { useEffect(() => {

View file

@ -4,3 +4,4 @@ export * from './SpaceTabs';
export * from './InboxTab'; export * from './InboxTab';
export * from './ExploreTab'; export * from './ExploreTab';
export * from './UserTab'; export * from './UserTab';
export * from './UnverifiedTab';

View file

@ -9,6 +9,7 @@ import { useSpace } from '../../../hooks/useSpace';
import { getAllParents } from '../../../utils/room'; import { getAllParents } from '../../../utils/room';
import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) { export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
@ -16,7 +17,8 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const { roomIdOrAlias } = useParams(); const { roomIdOrAlias, eventId } = useParams();
const viaServers = useSearchParamsViaServers();
const roomId = useSelectedRoom(); const roomId = useSelectedRoom();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
@ -26,7 +28,13 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
!allRooms.includes(room.roomId) || !allRooms.includes(room.roomId) ||
!getAllParents(roomToParents, room.roomId).has(space.roomId) !getAllParents(roomToParents, room.roomId).has(space.roomId)
) { ) {
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} />; return (
<JoinBeforeNavigate
roomIdOrAlias={roomIdOrAlias!}
eventId={eventId}
viaServers={viaServers}
/>
);
} }
return ( return (

View file

@ -1,5 +1,5 @@
import React, { useRef } from 'react'; 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 { useAtomValue } from 'jotai';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page'; import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { MessageSearch } from '../../../features/message-search'; import { MessageSearch } from '../../../features/message-search';
@ -9,11 +9,14 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
import { mDirectAtom } from '../../../state/mDirectList'; import { mDirectAtom } from '../../../state/mDirectList';
import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
export function SpaceSearch() { export function SpaceSearch() {
const mx = useMatrixClient(); const mx = useMatrixClient();
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const space = useSpace(); const space = useSpace();
const screenSize = useScreenSizeContext();
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
@ -25,13 +28,27 @@ export function SpaceSearch() {
return ( return (
<Page> <Page>
<PageHeader> <PageHeader balance>
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
<Icon size="400" src={Icons.Search} /> <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> <Text size="H3" truncate>
Message Search Message Search
</Text> </Text>
</Box> </Box>
<Box grow="Yes" basis="No" />
</Box>
</PageHeader> </PageHeader>
<Box style={{ position: 'relative' }} grow="Yes"> <Box style={{ position: 'relative' }} grow="Yes">
<Scroll ref={scrollRef} hideTrack visibility="Hover"> <Scroll ref={scrollRef} hideTrack visibility="Hover">

View file

@ -34,15 +34,8 @@ import {
NavItemContent, NavItemContent,
NavLink, NavLink,
} from '../../../components/nav'; } from '../../../components/nav';
import { import { getSpaceLobbyPath, getSpaceRoomPath, getSpaceSearchPath } from '../../pathUtils';
getOriginBaseUrl, import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
getSpaceLobbyPath,
getSpacePath,
getSpaceRoomPath,
getSpaceSearchPath,
withOriginBaseUrl,
} from '../../pathUtils';
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { import {
useSpaceLobbySelected, useSpaceLobbySelected,
@ -69,11 +62,12 @@ import { useRoomsUnread } from '../../../state/hooks/unread';
import { UseStateProvider } from '../../../components/UseStateProvider'; import { UseStateProvider } from '../../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../../components/leave-space-prompt'; import { LeaveSpacePrompt } from '../../../components/leave-space-prompt';
import { copyToClipboard } from '../../../utils/dom'; import { copyToClipboard } from '../../../utils/dom';
import { useClientConfig } from '../../../hooks/useClientConfig';
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories'; import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
import { useStateEvent } from '../../../hooks/useStateEvent'; import { useStateEvent } from '../../../hooks/useStateEvent';
import { StateEvent } from '../../../../types/matrix/room'; import { StateEvent } from '../../../../types/matrix/room';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoom } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers';
type SpaceMenuProps = { type SpaceMenuProps = {
room: Room; room: Room;
@ -81,7 +75,6 @@ type SpaceMenuProps = {
}; };
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => { const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const { hashRouter } = useClientConfig();
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
@ -100,8 +93,9 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
}; };
const handleCopyLink = () => { const handleCopyLink = () => {
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, room.roomId)); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), spacePath)); const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
requestClose(); requestClose();
}; };

View file

@ -6,6 +6,7 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace'; import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
import { SpaceProvider } from '../../../hooks/useSpace'; import { SpaceProvider } from '../../../hooks/useSpace';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate'; import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
type RouteSpaceProviderProps = { type RouteSpaceProviderProps = {
children: ReactNode; children: ReactNode;
@ -13,13 +14,15 @@ type RouteSpaceProviderProps = {
export function RouteSpaceProvider({ children }: RouteSpaceProviderProps) { export function RouteSpaceProvider({ children }: RouteSpaceProviderProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const joinedSpaces = useSpaces(mx, allRoomsAtom); const joinedSpaces = useSpaces(mx, allRoomsAtom);
const { spaceIdOrAlias } = useParams(); const { spaceIdOrAlias } = useParams();
const viaServers = useSearchParamsViaServers();
const selectedSpaceId = useSelectedSpace(); const selectedSpaceId = useSelectedSpace();
const space = mx.getRoom(selectedSpaceId); const space = mx.getRoom(selectedSpaceId);
if (!space || !joinedSpaces.includes(space.roomId)) { if (!space || !joinedSpaces.includes(space.roomId)) {
return <JoinBeforeNavigate roomIdOrAlias={spaceIdOrAlias ?? ''} />; return <JoinBeforeNavigate roomIdOrAlias={spaceIdOrAlias ?? ''} viaServers={viaServers} />;
} }
return ( return (

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

View file

@ -35,6 +35,11 @@ export type _SearchPathSearchParams = {
senders?: string; senders?: string;
}; };
export const _SEARCH_PATH = 'search/'; export const _SEARCH_PATH = 'search/';
export type _RoomSearchParams = {
/* comma separated string of servers */
viaServers?: string;
};
export const _ROOM_PATH = ':roomIdOrAlias/:eventId?/'; export const _ROOM_PATH = ':roomIdOrAlias/:eventId?/';
export const HOME_PATH = '/home/'; 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 HOME_ROOM_PATH = `/home/${_ROOM_PATH}`;
export const DIRECT_PATH = '/direct/'; export const DIRECT_PATH = '/direct/';
export type DirectCreateSearchParams = {
userId?: string;
};
export const DIRECT_CREATE_PATH = `/direct/${_CREATE_PATH}`; export const DIRECT_CREATE_PATH = `/direct/${_CREATE_PATH}`;
export const DIRECT_ROOM_PATH = `/direct/${_ROOM_PATH}`; export const DIRECT_ROOM_PATH = `/direct/${_ROOM_PATH}`;

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

View file

@ -1,5 +1,5 @@
/* eslint-disable jsx-a11y/alt-text */ /* eslint-disable jsx-a11y/alt-text */
import React, { ReactEventHandler, Suspense, lazy } from 'react'; import React, { ComponentPropsWithoutRef, ReactEventHandler, Suspense, lazy } from 'react';
import { import {
Element, Element,
Text as DOMText, Text as DOMText,
@ -7,18 +7,26 @@ import {
attributesToProps, attributesToProps,
domToReact, domToReact,
} from 'html-react-parser'; } from 'html-react-parser';
import { MatrixClient, Room } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
import { Scroll, Text } from 'folds'; 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 Linkify from 'linkify-react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import * as css from '../styles/CustomHtml.css'; import * as css from '../styles/CustomHtml.css';
import { getMxIdLocalPart, getCanonicalAliasRoomId } from '../utils/matrix'; import { getMxIdLocalPart, getCanonicalAliasRoomId, isRoomAlias, mxcUrlToHttp } from '../utils/matrix';
import { getMemberDisplayName } from '../utils/room'; import { getMemberDisplayName } from '../utils/room';
import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex'; import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex';
import { getHexcodeForEmoji, getShortcodeFor } from './emoji'; import { getHexcodeForEmoji, getShortcodeFor } from './emoji';
import { findAndReplace } from '../utils/findAndReplace'; 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')); const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
@ -35,6 +43,107 @@ export const LINKIFY_OPTS: LinkifyOpts = {
ignoreTags: ['span'], 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)[] => export const scaleSystemEmoji = (text: string): (string | JSX.Element)[] =>
findAndReplace( findAndReplace(
text, text,
@ -76,11 +185,13 @@ export const highlightText = (
export const getReactCustomHtmlParser = ( export const getReactCustomHtmlParser = (
mx: MatrixClient, mx: MatrixClient,
room: Room, roomId: string | undefined,
params: { params: {
linkifyOpts: LinkifyOpts;
highlightRegex?: RegExp; highlightRegex?: RegExp;
handleSpoilerClick?: ReactEventHandler<HTMLElement>; handleSpoilerClick?: ReactEventHandler<HTMLElement>;
handleMentionClick?: ReactEventHandler<HTMLElement>; handleMentionClick?: ReactEventHandler<HTMLElement>;
useAuthentication?: boolean;
} }
): HTMLReactParserOptions => { ): HTMLReactParserOptions => {
const opts: HTMLReactParserOptions = { const opts: HTMLReactParserOptions = {
@ -215,54 +326,14 @@ export const getReactCustomHtmlParser = (
} }
} }
if (name === 'a') { if (name === 'a' && testMatrixTo(tryDecodeURIComponent(props.href))) {
const mention = decodeURIComponent(props.href).match( const mention = renderMatrixMention(
/^https?:\/\/matrix.to\/#\/((@|#|!).+:[^?/]+)/ mx,
roomId,
tryDecodeURIComponent(props.href),
makeMentionCustomProps(params.handleMentionClick)
); );
if (mention) { if (mention) return 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 (name === 'span' && 'data-mx-spoiler' in props) { if (name === 'span' && 'data-mx-spoiler' in props) {
@ -283,7 +354,7 @@ export const getReactCustomHtmlParser = (
} }
if (name === 'img') { if (name === 'img') {
const htmlSrc = mx.mxcUrlToHttp(props.src); const htmlSrc = mxcUrlToHttp(mx, props.src, params.useAuthentication);
if (htmlSrc && props.src.startsWith('mxc://') === false) { if (htmlSrc && props.src.startsWith('mxc://') === false) {
return ( return (
<a href={htmlSrc} target="_blank" rel="noreferrer noopener"> <a href={htmlSrc} target="_blank" rel="noreferrer noopener">
@ -316,7 +387,7 @@ export const getReactCustomHtmlParser = (
} }
if (linkify) { if (linkify) {
return <Linkify options={LINKIFY_OPTS}>{jsx}</Linkify>; return <Linkify options={params.linkifyOpts}>{jsx}</Linkify>;
} }
return jsx; return jsx;
} }

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

View file

@ -2,6 +2,7 @@ import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils'; import { atomFamily } from 'jotai/utils';
import { Descendant } from 'slate'; import { Descendant } from 'slate';
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { IEventRelation } from 'matrix-js-sdk';
import { TListAtom, createListAtom } from '../list'; import { TListAtom, createListAtom } from '../list';
import { createUploadAtomFamily } from '../upload'; import { createUploadAtomFamily } from '../upload';
import { TUploadContent } from '../../utils/matrix'; import { TUploadContent } from '../../utils/matrix';
@ -39,7 +40,8 @@ export type IReplyDraft = {
userId: string; userId: string;
eventId: string; eventId: string;
body: string; body: string;
formattedBody?: string; formattedBody?: string | undefined;
relation?: IEventRelation | undefined;
}; };
const createReplyDraftAtom = () => atom<IReplyDraft | undefined>(undefined); const createReplyDraftAtom = () => atom<IReplyDraft | undefined>(undefined);
export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>; export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>;

View file

@ -83,7 +83,7 @@ export const useBindRoomToParentsAtom = (
}; };
const handleMembershipChange = (room: Room, membership: string) => { const handleMembershipChange = (room: Room, membership: string) => {
if (room.getMyMembership() === Membership.Leave) { if (isSpace(room) && room.getMyMembership() === Membership.Leave) {
setRoomToParents({ type: 'DELETE', roomId: room.roomId }); setRoomToParents({ type: 'DELETE', roomId: room.roomId });
return; return;
} }

View file

@ -196,3 +196,11 @@ export const setFavicon = (url: string): void => {
if (!favicon) return; if (!favicon) return;
favicon.setAttribute('href', url); favicon.setAttribute('href', url);
}; };
export const tryDecodeURIComponent = (encodedURIComponent: string): string => {
try {
return decodeURIComponent(encodedURIComponent);
} catch {
return encodedURIComponent;
}
};

Some files were not shown because too many files have changed in this diff Show more