forked from mirror/cinny
Compare commits
27 commits
ad4ccb6512
...
4b71372ba8
Author | SHA1 | Date | |
---|---|---|---|
4b71372ba8 | |||
|
4dfce32730 | ||
|
388f606ad2 | ||
|
09444f9e08 | ||
|
c6a8fb1117 | ||
|
043012e809 | ||
|
5c9ee1a988 | ||
|
22b7f6dd7d | ||
|
bdba0332e1 | ||
|
16be69c104 | ||
|
830d05e217 | ||
|
7e7bee8f48 | ||
|
ac1797344c | ||
|
b4ce8a7cab | ||
|
e68c56b334 | ||
|
cabfdd47b5 | ||
|
cfe893f358 | ||
|
581211f13e | ||
|
8ed78d48fb | ||
|
96222de5bc | ||
|
681287c46a | ||
|
9cb5c70d51 | ||
|
c62050445b | ||
|
a8f5a6c2f4 | ||
|
e54bb2e423 | ||
|
5058136737 | ||
|
74dc76e22e |
110 changed files with 5420 additions and 1288 deletions
4
.github/workflows/build-pull-request.yml
vendored
4
.github/workflows/build-pull-request.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/docker-pr.yml
vendored
2
.github/workflows/docker-pr.yml
vendored
|
@ -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
|
||||||
|
|
4
.github/workflows/prod-deploy.yml
vendored
4
.github/workflows/prod-deploy.yml
vendored
|
@ -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
|
||||||
|
|
3
.npmrc
3
.npmrc
|
@ -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
128
CODE_OF_CONDUCT.md
Normal 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.
|
26
README.md
26
README.md
|
@ -19,22 +19,24 @@ A Matrix client focusing primarily on simple, elegant and secure interface. The
|
||||||
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
|
<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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
server {
|
server {
|
||||||
location / {
|
listen 80;
|
||||||
root /usr/share/nginx/html;
|
listen [::]:80;
|
||||||
|
|
||||||
rewrite ^/config.json$ /config.json break;
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
rewrite ^/config.json$ /config.json break;
|
||||||
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;
|
||||||
rewrite ^/assets/(.*)$ /assets/$1 break;
|
rewrite ^/assets/(.*)$ /assets/$1 break;
|
||||||
|
|
||||||
rewrite ^(.+)$ /index.html break;
|
rewrite ^(.+)$ /index.html break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,12 @@
|
||||||
from = "/manifest.json"
|
from = "/manifest.json"
|
||||||
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"
|
||||||
|
|
3505
package-lock.json
generated
3505
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
@ -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
7
public/locales/de.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Organisms": {
|
||||||
|
"RoomCommon": {
|
||||||
|
"changed_room_name": " hat den Raum Name geändert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
public/locales/en.json
Normal file
7
public/locales/en.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Organisms": {
|
||||||
|
"RoomCommon": {
|
||||||
|
"changed_room_name": " changed room name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
src/app/components/BackRouteHandler.tsx
Normal file
86
src/app/components/BackRouteHandler.tsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { ReactNode, useCallback } from 'react';
|
||||||
|
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
getDirectPath,
|
||||||
|
getExplorePath,
|
||||||
|
getHomePath,
|
||||||
|
getInboxPath,
|
||||||
|
getSpacePath,
|
||||||
|
} from '../pages/pathUtils';
|
||||||
|
import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '../pages/paths';
|
||||||
|
|
||||||
|
type BackRouteHandlerProps = {
|
||||||
|
children: (onBack: () => void) => ReactNode;
|
||||||
|
};
|
||||||
|
export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const goBack = useCallback(() => {
|
||||||
|
if (
|
||||||
|
matchPath(
|
||||||
|
{
|
||||||
|
path: HOME_PATH,
|
||||||
|
caseSensitive: true,
|
||||||
|
end: false,
|
||||||
|
},
|
||||||
|
location.pathname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
navigate(getHomePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
matchPath(
|
||||||
|
{
|
||||||
|
path: DIRECT_PATH,
|
||||||
|
caseSensitive: true,
|
||||||
|
end: false,
|
||||||
|
},
|
||||||
|
location.pathname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
navigate(getDirectPath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const spaceMatch = matchPath(
|
||||||
|
{
|
||||||
|
path: SPACE_PATH,
|
||||||
|
caseSensitive: true,
|
||||||
|
end: false,
|
||||||
|
},
|
||||||
|
location.pathname
|
||||||
|
);
|
||||||
|
if (spaceMatch?.params.spaceIdOrAlias) {
|
||||||
|
navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
matchPath(
|
||||||
|
{
|
||||||
|
path: EXPLORE_PATH,
|
||||||
|
caseSensitive: true,
|
||||||
|
end: false,
|
||||||
|
},
|
||||||
|
location.pathname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
navigate(getExplorePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
matchPath(
|
||||||
|
{
|
||||||
|
path: INBOX_PATH,
|
||||||
|
caseSensitive: true,
|
||||||
|
end: false,
|
||||||
|
},
|
||||||
|
location.pathname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
navigate(getInboxPath());
|
||||||
|
}
|
||||||
|
}, [navigate, location]);
|
||||||
|
|
||||||
|
return children(goBack);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import 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
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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' }}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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:
|
||||||
|
|
2
src/app/components/editor/slate.d.ts
vendored
2
src/app/components/editor/slate.d.ts
vendored
|
@ -29,6 +29,8 @@ export type LinkElement = {
|
||||||
export type MentionElement = {
|
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[];
|
||||||
|
|
|
@ -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: '' }],
|
||||||
|
|
|
@ -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,68 +456,70 @@ 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}>
|
||||||
{tab === EmojiBoardTab.Emoji
|
{tab === EmojiBoardTab.Emoji
|
||||||
? searchResult.map((emoji) =>
|
? searchResult.map((emoji) =>
|
||||||
'unicode' in emoji ? (
|
'unicode' in emoji ? (
|
||||||
<EmojiItem
|
<EmojiItem
|
||||||
key={emoji.unicode}
|
key={emoji.unicode}
|
||||||
label={emoji.label}
|
label={emoji.label}
|
||||||
type={EmojiType.Emoji}
|
type={EmojiType.Emoji}
|
||||||
data={emoji.unicode}
|
data={emoji.unicode}
|
||||||
shortcode={emoji.shortcode}
|
shortcode={emoji.shortcode}
|
||||||
>
|
>
|
||||||
{emoji.unicode}
|
{emoji.unicode}
|
||||||
</EmojiItem>
|
</EmojiItem>
|
||||||
) : (
|
) : (
|
||||||
<EmojiItem
|
<EmojiItem
|
||||||
key={emoji.shortcode}
|
key={emoji.shortcode}
|
||||||
label={emoji.body || emoji.shortcode}
|
label={emoji.body || emoji.shortcode}
|
||||||
type={EmojiType.CustomEmoji}
|
type={EmojiType.CustomEmoji}
|
||||||
data={emoji.url}
|
data={emoji.url}
|
||||||
shortcode={emoji.shortcode}
|
shortcode={emoji.shortcode}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
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>
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
: searchResult.map((emoji) =>
|
: searchResult.map((emoji) =>
|
||||||
'unicode' in emoji ? null : (
|
'unicode' in emoji ? null : (
|
||||||
<StickerItem
|
<StickerItem
|
||||||
key={emoji.shortcode}
|
key={emoji.shortcode}
|
||||||
label={emoji.body || emoji.shortcode}
|
label={emoji.body || emoji.shortcode}
|
||||||
type={EmojiType.Sticker}
|
type={EmojiType.Sticker}
|
||||||
data={emoji.url}
|
data={emoji.url}
|
||||||
shortcode={emoji.shortcode}
|
shortcode={emoji.shortcode}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</EmojiGroup>
|
</EmojiGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,37 +89,43 @@ 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 (
|
||||||
<ReplyLayout
|
<Box direction="Column" {...props} ref={ref}>
|
||||||
userColor={sender ? colorMXID(sender) : undefined}
|
{threadRootId && (
|
||||||
username={
|
<ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
|
||||||
sender && (
|
|
||||||
<Text size="T300" truncate>
|
|
||||||
<b>{getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)}</b>
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{replyEvent !== undefined ? (
|
|
||||||
<Text size="T300" truncate>
|
|
||||||
{badEncryption ? <MessageBadEncryptedContent /> : bodyJSX}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<LinePlaceholder
|
|
||||||
style={{
|
|
||||||
backgroundColor: color.SurfaceVariant.ContainerActive,
|
|
||||||
maxWidth: toRem(placeholderWidth),
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</ReplyLayout>
|
<ReplyLayout
|
||||||
|
as="button"
|
||||||
|
userColor={sender ? colorMXID(sender) : undefined}
|
||||||
|
username={
|
||||||
|
sender && (
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
<b>{getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)}</b>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data-event-id={replyEventId}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{replyEvent !== undefined ? (
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{badEncryption ? <MessageBadEncryptedContent /> : bodyJSX}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<LinePlaceholder
|
||||||
|
style={{
|
||||||
|
backgroundColor: color.SurfaceVariant.ContainerActive,
|
||||||
|
maxWidth: toRem(placeholderWidth),
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ReplyLayout>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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>(
|
||||||
<Header
|
({ className, balance, ...props }, ref) => (
|
||||||
as="header"
|
<Header
|
||||||
size="600"
|
as="header"
|
||||||
className={classNames(css.PageHeader, className)}
|
size="600"
|
||||||
{...props}
|
className={classNames(css.PageHeader({ balance }), className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
/>
|
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} />
|
||||||
|
|
|
@ -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({
|
||||||
paddingLeft: config.space.S400,
|
base: {
|
||||||
paddingRight: config.space.S200,
|
paddingLeft: config.space.S400,
|
||||||
borderBottomWidth: config.borderWidth.B300,
|
paddingRight: config.space.S200,
|
||||||
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
balance: {
|
||||||
|
true: {
|
||||||
|
paddingLeft: config.space.S200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
export type PageHeaderVariants = RecipeVariants<typeof PageHeader>;
|
||||||
|
|
||||||
export const PageContent = style([
|
export const PageContent = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,28 +8,48 @@ 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" justifyContent="Center" alignItems="Center" gap="200">
|
<Box grow="Yes" gap="200">
|
||||||
<Text size="H3" truncate>
|
<Box shrink="No">
|
||||||
{roomIdOrAlias}
|
{screenSize === ScreenSize.Mobile && (
|
||||||
</Text>
|
<BackRouteHandler>
|
||||||
|
{(onBack) => (
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</BackRouteHandler>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||||
|
<Text size="H3" truncate>
|
||||||
|
{roomIdOrAlias}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
|
@ -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} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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,55 +124,88 @@ 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">
|
||||||
<Box grow="Yes" basis="No" />
|
{screenSize === ScreenSize.Mobile ? (
|
||||||
<Box justifyContent="Center" alignItems="Center" gap="300">
|
<>
|
||||||
{showProfile && (
|
<Box shrink="No">
|
||||||
<>
|
<BackRouteHandler>
|
||||||
<Avatar size="300">
|
{(onBack) => (
|
||||||
<RoomAvatar
|
<IconButton onClick={onBack}>
|
||||||
roomId={space.roomId}
|
<Icon src={Icons.ArrowLeft} />
|
||||||
src={avatarUrl}
|
</IconButton>
|
||||||
alt={name}
|
)}
|
||||||
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}
|
</BackRouteHandler>
|
||||||
/>
|
</Box>
|
||||||
</Avatar>
|
<Box grow="Yes" justifyContent="Center">
|
||||||
<Text size="H3" truncate>
|
{showProfile && (
|
||||||
{name}
|
<Text size="H3" truncate>
|
||||||
</Text>
|
{name}
|
||||||
</>
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Box grow="Yes" basis="No" />
|
||||||
|
<Box justifyContent="Center" alignItems="Center" gap="300">
|
||||||
|
{showProfile && (
|
||||||
|
<>
|
||||||
|
<Avatar size="300">
|
||||||
|
<RoomAvatar
|
||||||
|
roomId={space.roomId}
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={name}
|
||||||
|
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
<Text size="H3" truncate>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
shrink="No"
|
||||||
|
grow={screenSize === ScreenSize.Mobile ? 'No' : 'Yes'}
|
||||||
|
basis={screenSize === ScreenSize.Mobile ? 'Yes' : 'No'}
|
||||||
|
justifyContent="End"
|
||||||
|
>
|
||||||
|
{screenSize !== ScreenSize.Mobile && (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Bottom"
|
||||||
|
offset={4}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text>Members</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(triggerRef) => (
|
||||||
|
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
|
||||||
|
<Icon size="400" src={Icons.User} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</Box>
|
|
||||||
<Box shrink="No" grow="Yes" basis="No" justifyContent="End">
|
|
||||||
<TooltipProvider
|
|
||||||
position="Bottom"
|
|
||||||
offset={4}
|
|
||||||
tooltip={
|
|
||||||
<Tooltip>
|
|
||||||
<Text>Members</Text>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(triggerRef) => (
|
|
||||||
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
|
|
||||||
<Icon size="400" src={Icons.User} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</TooltipProvider>
|
|
||||||
<TooltipProvider
|
<TooltipProvider
|
||||||
position="Bottom"
|
position="Bottom"
|
||||||
align="End"
|
align="End"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,8 +422,8 @@ 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
|
||||||
}
|
}
|
||||||
memberCount={summaryState.data.num_joined_members}
|
memberCount={summaryState.data.num_joined_members}
|
||||||
|
|
|
@ -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,8 +473,8 @@ 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
|
||||||
}
|
}
|
||||||
suggested={content.suggested}
|
suggested={content.suggested}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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,9 +429,8 @@ 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
|
||||||
|
|
|
@ -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,22 +497,25 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
>
|
>
|
||||||
<Icon src={Icons.Cross} size="50" />
|
<Icon src={Icons.Cross} size="50" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<ReplyLayout
|
<Box direction="Column">
|
||||||
userColor={colorMXID(replyDraft.userId)}
|
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
||||||
username={
|
<ReplyLayout
|
||||||
|
userColor={colorMXID(replyDraft.userId)}
|
||||||
|
username={
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
<b>
|
||||||
|
{getMemberDisplayName(room, replyDraft.userId) ??
|
||||||
|
getMxIdLocalPart(replyDraft.userId) ??
|
||||||
|
replyDraft.userId}
|
||||||
|
</b>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Text size="T300" truncate>
|
<Text size="T300" truncate>
|
||||||
<b>
|
{trimReplyFromBody(replyDraft.body)}
|
||||||
{getMemberDisplayName(room, replyDraft.userId) ??
|
|
||||||
getMxIdLocalPart(replyDraft.userId) ??
|
|
||||||
replyDraft.userId}
|
|
||||||
</b>
|
|
||||||
</Text>
|
</Text>
|
||||||
}
|
</ReplyLayout>
|
||||||
>
|
</Box>
|
||||||
<Text size="T300" truncate>
|
|
||||||
{trimReplyFromBody(replyDraft.body)}
|
|
||||||
</Text>
|
|
||||||
</ReplyLayout>
|
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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) => (
|
||||||
|
@ -301,9 +311,9 @@ const useTimelinePagination = (
|
||||||
range:
|
range:
|
||||||
offsetRange > 0
|
offsetRange > 0
|
||||||
? {
|
? {
|
||||||
start: currentTimeline.range.start + offsetRange,
|
start: currentTimeline.range.start + offsetRange,
|
||||||
end: currentTimeline.range.end + offsetRange,
|
end: currentTimeline.range.end + offsetRange,
|
||||||
}
|
}
|
||||||
: { ...currentTimeline.range },
|
: { ...currentTimeline.range },
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -323,7 +333,7 @@ const useTimelinePagination = (
|
||||||
if (
|
if (
|
||||||
!paginationToken &&
|
!paginationToken &&
|
||||||
getTimelinesEventsCount(lTimelines) !==
|
getTimelinesEventsCount(lTimelines) !==
|
||||||
getTimelinesEventsCount(getLinkedTimelines(timelineToPaginate))
|
getTimelinesEventsCount(getLinkedTimelines(timelineToPaginate))
|
||||||
) {
|
) {
|
||||||
recalibratePagination(lTimelines, timelinesEventsCount, backwards);
|
recalibratePagination(lTimelines, timelinesEventsCount, backwards);
|
||||||
return;
|
return;
|
||||||
|
@ -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(
|
||||||
|
@ -481,42 +495,32 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
|
|
||||||
const [focusItem, setFocusItem] = useState<
|
const [focusItem, setFocusItem] = useState<
|
||||||
| {
|
| {
|
||||||
index: number;
|
index: number;
|
||||||
scrollTo: boolean;
|
scrollTo: boolean;
|
||||||
highlight: boolean;
|
highlight: boolean;
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
>();
|
>();
|
||||||
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>
|
||||||
}
|
}
|
||||||
|
@ -1461,14 +1472,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
const eventJSX = reactionOrEditEvent(mEvent)
|
const eventJSX = reactionOrEditEvent(mEvent)
|
||||||
? null
|
? null
|
||||||
: renderMatrixEvent(
|
: renderMatrixEvent(
|
||||||
mEvent.getType(),
|
mEvent.getType(),
|
||||||
typeof mEvent.getStateKey() === 'string',
|
typeof mEvent.getStateKey() === 'string',
|
||||||
mEventId,
|
mEventId,
|
||||||
mEvent,
|
mEvent,
|
||||||
item,
|
item,
|
||||||
timelineSet,
|
timelineSet,
|
||||||
collapsed
|
collapsed
|
||||||
);
|
);
|
||||||
prevEvent = mEvent;
|
prevEvent = mEvent;
|
||||||
isPrevRendered = !!eventJSX;
|
isPrevRendered = !!eventJSX;
|
||||||
|
|
||||||
|
@ -1550,9 +1561,8 @@ 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)
|
}`,
|
||||||
}`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RoomIntro room={room} />
|
<RoomIntro room={room} />
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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' ||
|
||||||
|
|
|
@ -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,132 +48,135 @@ 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 unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||||
const { hashRouter } = useClientConfig();
|
const powerLevels = usePowerLevelsContext();
|
||||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||||
const powerLevels = usePowerLevelsContext();
|
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
|
||||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(mx, room.roomId);
|
markAsRead(mx, room.roomId);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
openInviteUser(room.roomId);
|
openInviteUser(room.roomId);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
copyToClipboard(withOriginBaseUrl(getOriginBaseUrl(hashRouter), linkPath));
|
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||||
requestClose();
|
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
||||||
};
|
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
|
||||||
|
requestClose();
|
||||||
|
};
|
||||||
|
|
||||||
const handleRoomSettings = () => {
|
const handleRoomSettings = () => {
|
||||||
toggleRoomSettings(room.roomId);
|
toggleRoomSettings(room.roomId);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleMarkAsRead}
|
onClick={handleMarkAsRead}
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.CheckTwice} />}
|
after={<Icon size="100" src={Icons.CheckTwice} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
disabled={!unread}
|
disabled={!unread}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Mark as Read
|
Mark as Read
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Box>
|
</Box>
|
||||||
<Line variant="Surface" size="300" />
|
<Line variant="Surface" size="300" />
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleInvite}
|
onClick={handleInvite}
|
||||||
variant="Primary"
|
variant="Primary"
|
||||||
fill="None"
|
fill="None"
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Invite
|
Invite
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleCopyLink}
|
onClick={handleCopyLink}
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.Link} />}
|
after={<Icon size="100" src={Icons.Link} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Copy Link
|
Copy Link
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleRoomSettings}
|
onClick={handleRoomSettings}
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.Setting} />}
|
after={<Icon size="100" src={Icons.Setting} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Room Settings
|
Room Settings
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Box>
|
</Box>
|
||||||
<Line variant="Surface" size="300" />
|
<Line variant="Surface" size="300" />
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<UseStateProvider initial={false}>
|
<UseStateProvider initial={false}>
|
||||||
{(promptLeave, setPromptLeave) => (
|
{(promptLeave, setPromptLeave) => (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => setPromptLeave(true)}
|
onClick={() => setPromptLeave(true)}
|
||||||
variant="Critical"
|
variant="Critical"
|
||||||
fill="None"
|
fill="None"
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.ArrowGoLeft} />}
|
after={<Icon size="100" src={Icons.ArrowGoLeft} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-pressed={promptLeave}
|
aria-pressed={promptLeave}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
Leave Room
|
Leave Room
|
||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{promptLeave && (
|
{promptLeave && (
|
||||||
<LeaveRoomPrompt
|
<LeaveRoomPrompt
|
||||||
roomId={room.roomId}
|
roomId={room.roomId}
|
||||||
onDone={requestClose}
|
onDone={requestClose}
|
||||||
onCancel={() => setPromptLeave(false)}
|
onCancel={() => setPromptLeave(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</UseStateProvider>
|
</UseStateProvider>
|
||||||
</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">
|
||||||
<Avatar size="300">
|
{screenSize !== ScreenSize.Mobile && (
|
||||||
<RoomAvatar
|
<Avatar size="300">
|
||||||
roomId={room.roomId}
|
<RoomAvatar
|
||||||
src={avatarUrl}
|
roomId={room.roomId}
|
||||||
alt={name}
|
src={avatarUrl}
|
||||||
renderFallback={() => (
|
alt={name}
|
||||||
<RoomIcon size="200" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
|
renderFallback={() => (
|
||||||
)}
|
<RoomIcon
|
||||||
/>
|
size="200"
|
||||||
</Avatar>
|
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
@ -242,9 +235,9 @@ export const MessageSourceCodeItem = as<
|
||||||
const getContent = (evt: MatrixEvent) =>
|
const getContent = (evt: MatrixEvent) =>
|
||||||
evt.isEncrypted()
|
evt.isEncrypted()
|
||||||
? {
|
? {
|
||||||
[`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(),
|
[`<== DECRYPTED_EVENT ==>`]: evt.getEffectiveEvent(),
|
||||||
[`<== ORIGINAL_EVENT ==>`]: evt.event,
|
[`<== ORIGINAL_EVENT ==>`]: evt.event,
|
||||||
}
|
}
|
||||||
: evt.event;
|
: evt.event;
|
||||||
|
|
||||||
const getText = (): string => {
|
const getText = (): string => {
|
||||||
|
@ -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}
|
||||||
|
@ -968,26 +953,26 @@ export const Message = as<'div', MessageProps>(
|
||||||
</Box>
|
</Box>
|
||||||
{((!mEvent.isRedacted() && canDelete) ||
|
{((!mEvent.isRedacted() && canDelete) ||
|
||||||
mEvent.getSender() !== mx.getUserId()) && (
|
mEvent.getSender() !== mx.getUserId()) && (
|
||||||
<>
|
<>
|
||||||
<Line size="300" />
|
<Line size="300" />
|
||||||
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||||
{!mEvent.isRedacted() && canDelete && (
|
{!mEvent.isRedacted() && canDelete && (
|
||||||
<MessageDeleteItem
|
<MessageDeleteItem
|
||||||
room={room}
|
room={room}
|
||||||
mEvent={mEvent}
|
mEvent={mEvent}
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mEvent.getSender() !== mx.getUserId() && (
|
{mEvent.getSender() !== mx.getUserId() && (
|
||||||
<MessageReportItem
|
<MessageReportItem
|
||||||
room={room}
|
room={room}
|
||||||
mEvent={mEvent}
|
mEvent={mEvent}
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
}
|
}
|
||||||
|
@ -1111,26 +1096,26 @@ export const Event = as<'div', EventProps>(
|
||||||
</Box>
|
</Box>
|
||||||
{((!mEvent.isRedacted() && canDelete && !stateEvent) ||
|
{((!mEvent.isRedacted() && canDelete && !stateEvent) ||
|
||||||
(mEvent.getSender() !== mx.getUserId() && !stateEvent)) && (
|
(mEvent.getSender() !== mx.getUserId() && !stateEvent)) && (
|
||||||
<>
|
<>
|
||||||
<Line size="300" />
|
<Line size="300" />
|
||||||
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
<Box direction="Column" gap="100" className={css.MessageMenuGroup}>
|
||||||
{!mEvent.isRedacted() && canDelete && (
|
{!mEvent.isRedacted() && canDelete && (
|
||||||
<MessageDeleteItem
|
<MessageDeleteItem
|
||||||
room={room}
|
room={room}
|
||||||
mEvent={mEvent}
|
mEvent={mEvent}
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mEvent.getSender() !== mx.getUserId() && (
|
{mEvent.getSender() !== mx.getUserId() && (
|
||||||
<MessageReportItem
|
<MessageReportItem
|
||||||
room={room}
|
room={room}
|
||||||
mEvent={mEvent}
|
mEvent={mEvent}
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
14
src/app/hooks/router/useSearchParamsViaServers.ts
Normal file
14
src/app/hooks/router/useSearchParamsViaServers.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { getRoomSearchParams } from '../../pages/pathSearchParam';
|
||||||
|
import { decodeSearchParamValueArray } from '../../pages/pathUtils';
|
||||||
|
|
||||||
|
export const useSearchParamsViaServers = (): string[] | undefined => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const roomSearchParams = useMemo(() => getRoomSearchParams(searchParams), [searchParams]);
|
||||||
|
const viaServers = roomSearchParams.viaServers
|
||||||
|
? decodeSearchParamValueArray(roomSearchParams.viaServers)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return viaServers;
|
||||||
|
};
|
|
@ -1,31 +0,0 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useMatrixClient } from './useMatrixClient';
|
|
||||||
|
|
||||||
export function useDeviceList() {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const [deviceList, setDeviceList] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
|
||||||
|
|
||||||
const updateDevices = () => mx.getDevices().then((data) => {
|
|
||||||
if (!isMounted) return;
|
|
||||||
setDeviceList(data.devices || []);
|
|
||||||
});
|
|
||||||
updateDevices();
|
|
||||||
|
|
||||||
const handleDevicesUpdate = (users) => {
|
|
||||||
if (users.includes(mx.getUserId())) {
|
|
||||||
updateDevices();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
|
|
||||||
return () => {
|
|
||||||
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
|
|
||||||
isMounted = false;
|
|
||||||
};
|
|
||||||
}, [mx]);
|
|
||||||
return deviceList;
|
|
||||||
}
|
|
35
src/app/hooks/useDeviceList.ts
Normal file
35
src/app/hooks/useDeviceList.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { CryptoEvent, IMyDevice } from 'matrix-js-sdk';
|
||||||
|
import { CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||||
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
|
|
||||||
|
export function useDeviceList() {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const [deviceList, setDeviceList] = useState<IMyDevice[] | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const updateDevices = () =>
|
||||||
|
mx.getDevices().then((data) => {
|
||||||
|
if (!isMounted) return;
|
||||||
|
setDeviceList(data.devices || []);
|
||||||
|
});
|
||||||
|
updateDevices();
|
||||||
|
|
||||||
|
const handleDevicesUpdate: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] = (users) => {
|
||||||
|
const userId = mx.getUserId();
|
||||||
|
if (userId && users.includes(userId)) {
|
||||||
|
updateDevices();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mx.on(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||||
|
return () => {
|
||||||
|
mx.removeListener(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [mx]);
|
||||||
|
return deviceList;
|
||||||
|
}
|
43
src/app/hooks/useMentionClickHandler.ts
Normal file
43
src/app/hooks/useMentionClickHandler.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { ReactEventHandler, useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useRoomNavigate } from './useRoomNavigate';
|
||||||
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
|
import { isRoomId, isUserId } from '../utils/matrix';
|
||||||
|
import { openProfileViewer } from '../../client/action/navigation';
|
||||||
|
import { getHomeRoomPath, withSearchParam } from '../pages/pathUtils';
|
||||||
|
import { _RoomSearchParams } from '../pages/paths';
|
||||||
|
|
||||||
|
export const useMentionClickHandler = (roomId: string): ReactEventHandler<HTMLElement> => {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleClick: ReactEventHandler<HTMLElement> = useCallback(
|
||||||
|
(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const target = evt.currentTarget;
|
||||||
|
const mentionId = target.getAttribute('data-mention-id');
|
||||||
|
if (typeof mentionId !== 'string') return;
|
||||||
|
|
||||||
|
if (isUserId(mentionId)) {
|
||||||
|
openProfileViewer(mentionId, roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventId = target.getAttribute('data-mention-event-id') || undefined;
|
||||||
|
if (isRoomId(mentionId) && mx.getRoom(mentionId)) {
|
||||||
|
if (mx.getRoom(mentionId)?.isSpaceRoom()) navigateSpace(mentionId);
|
||||||
|
else navigateRoom(mentionId, eventId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viaServers = target.getAttribute('data-mention-via') || undefined;
|
||||||
|
const path = getHomeRoomPath(mentionId, eventId);
|
||||||
|
|
||||||
|
navigate(viaServers ? withSearchParam<_RoomSearchParams>(path, { viaServers }) : path);
|
||||||
|
},
|
||||||
|
[mx, navigate, navigateRoom, navigateSpace, roomId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return handleClick;
|
||||||
|
};
|
|
@ -1,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>(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
14
src/app/hooks/useSpoilerClickHandler.ts
Normal file
14
src/app/hooks/useSpoilerClickHandler.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { ReactEventHandler, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const useSpoilerClickHandler = (): ReactEventHandler<HTMLElement> => {
|
||||||
|
const handleClick: ReactEventHandler<HTMLElement> = useCallback((evt) => {
|
||||||
|
const target = evt.currentTarget;
|
||||||
|
if (target.getAttribute('aria-pressed') === 'true') {
|
||||||
|
evt.stopPropagation();
|
||||||
|
target.setAttribute('aria-pressed', 'false');
|
||||||
|
target.style.cursor = 'initial';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return handleClick;
|
||||||
|
};
|
31
src/app/i18n.ts
Normal file
31
src/app/i18n.ts
Normal 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;
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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={
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -4,69 +4,89 @@ 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
|
||||||
(provider) => !provider.icon || !mx.mxcUrlToHttp(provider.icon, 96, 96, 'crop', false)
|
? providers.find(
|
||||||
);
|
(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 ? (
|
||||||
const { id, name, icon } = provider;
|
providers.map((provider) => {
|
||||||
const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false);
|
const { id, name, icon } = provider;
|
||||||
|
const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false);
|
||||||
|
|
||||||
const buttonTitle = `Continue with ${name}`;
|
const buttonTitle = `Continue with ${name}`;
|
||||||
|
|
||||||
|
if (renderAsIcons) {
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
key={id}
|
||||||
|
as="a"
|
||||||
|
href={getSSOIdUrl(id)}
|
||||||
|
aria-label={buttonTitle}
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<AvatarImage src={iconUrl!} alt={name} title={buttonTitle} />
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!anyAsBtn && iconUrl && asIcons) {
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Button
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ width: '100%' }}
|
||||||
key={id}
|
key={id}
|
||||||
as="a"
|
as="a"
|
||||||
href={getSSOIdUrl(id)}
|
href={getSSOIdUrl(id)}
|
||||||
aria-label={buttonTitle}
|
size="500"
|
||||||
size="300"
|
variant="Secondary"
|
||||||
radii="300"
|
fill="Soft"
|
||||||
|
outlined
|
||||||
|
before={
|
||||||
|
iconUrl && (
|
||||||
|
<Avatar size="200" radii="300">
|
||||||
|
<AvatarImage src={iconUrl} alt={name} />
|
||||||
|
</Avatar>
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AvatarImage src={iconUrl} alt={name} title={buttonTitle} />
|
<Text align="Center" size="B500" truncate>
|
||||||
</Avatar>
|
{buttonTitle}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
|
) : (
|
||||||
return (
|
<Button
|
||||||
<Button
|
style={{ width: '100%' }}
|
||||||
style={{ width: '100%' }}
|
as="a"
|
||||||
key={id}
|
href={getSSOIdUrl()}
|
||||||
as="a"
|
size="500"
|
||||||
href={getSSOIdUrl(id)}
|
variant="Secondary"
|
||||||
size="500"
|
fill="Soft"
|
||||||
variant="Secondary"
|
outlined
|
||||||
fill="Soft"
|
>
|
||||||
outlined
|
<Text align="Center" size="B500" truncate>
|
||||||
before={
|
Continue with SSO
|
||||||
iconUrl && (
|
</Text>
|
||||||
<Avatar size="200" radii="300">
|
</Button>
|
||||||
<AvatarImage src={iconUrl} alt={name} />
|
)}
|
||||||
</Avatar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text align="Center" size="B500" truncate>
|
|
||||||
{buttonTitle}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
33
src/app/pages/client/direct/DirectCreate.tsx
Normal file
33
src/app/pages/client/direct/DirectCreate.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { WelcomePage } from '../WelcomePage';
|
||||||
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import { getDirectCreateSearchParams } from '../../pathSearchParam';
|
||||||
|
import { getDirectPath, getDirectRoomPath } from '../../pathUtils';
|
||||||
|
import { getDMRoomFor } from '../../../utils/matrix';
|
||||||
|
import { openInviteUser } from '../../../../client/action/navigation';
|
||||||
|
import { useDirectRooms } from './useDirectRooms';
|
||||||
|
|
||||||
|
export function DirectCreate() {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { userId } = getDirectCreateSearchParams(searchParams);
|
||||||
|
const directs = useDirectRooms();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId) {
|
||||||
|
const room = getDMRoomFor(mx, userId);
|
||||||
|
const { roomId } = room ?? {};
|
||||||
|
if (roomId && directs.includes(roomId)) {
|
||||||
|
navigate(getDirectRoomPath(roomId), { replace: true });
|
||||||
|
} else {
|
||||||
|
openInviteUser(undefined, userId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigate(getDirectPath(), { replace: true });
|
||||||
|
}
|
||||||
|
}, [mx, navigate, directs, userId]);
|
||||||
|
|
||||||
|
return <WelcomePage />;
|
||||||
|
}
|
|
@ -10,12 +10,12 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||||
const mx = useMatrixClient();
|
const 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 (
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './Direct';
|
export * from './Direct';
|
||||||
export * from './RoomProvider';
|
export * from './RoomProvider';
|
||||||
|
export * from './DirectCreate';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" justifyContent="Center" alignItems="Center" gap="200">
|
<>
|
||||||
<Icon size="400" src={Icons.Category} />
|
<Box grow="Yes" basis="No">
|
||||||
<Text size="H3" truncate>
|
{screenSize === ScreenSize.Mobile && (
|
||||||
{server}
|
<BackRouteHandler>
|
||||||
</Text>
|
{(onBack) => (
|
||||||
</Box>
|
<IconButton onClick={onBack}>
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</BackRouteHandler>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||||
|
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Category} />}
|
||||||
|
<Text size="H3" truncate>
|
||||||
|
{server}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" basis="No" />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -1,21 +1,38 @@
|
||||||
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">
|
||||||
<Text size="H3" truncate>
|
{screenSize === ScreenSize.Mobile && (
|
||||||
Message Search
|
<BackRouteHandler>
|
||||||
</Text>
|
{(onBack) => (
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</BackRouteHandler>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||||
|
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
|
||||||
|
<Text size="H3" truncate>
|
||||||
|
Message Search
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" basis="No" />
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Box style={{ position: 'relative' }} grow="Yes">
|
<Box style={{ position: 'relative' }} grow="Yes">
|
||||||
|
|
|
@ -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,12 +232,26 @@ 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">
|
||||||
<Text size="H3" truncate>
|
{screenSize === ScreenSize.Mobile && (
|
||||||
Invitations
|
<BackRouteHandler>
|
||||||
</Text>
|
{(onBack) => (
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</BackRouteHandler>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box alignItems="Center" gap="200">
|
||||||
|
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Mail} />}
|
||||||
|
<Text size="H3" truncate>
|
||||||
|
Invitations
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" basis="No" />
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
|
|
|
@ -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,12 +559,26 @@ 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">
|
||||||
<Text size="H3" truncate>
|
{screenSize === ScreenSize.Mobile && (
|
||||||
Notification Messages
|
<BackRouteHandler>
|
||||||
</Text>
|
{(onBack) => (
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</BackRouteHandler>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box alignItems="Center" gap="200">
|
||||||
|
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Message} />}
|
||||||
|
<Text size="H3" truncate>
|
||||||
|
Notification Messages
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" basis="No" />
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -230,18 +227,18 @@ const useDraggableItem = (
|
||||||
return !target
|
return !target
|
||||||
? undefined
|
? undefined
|
||||||
: draggable({
|
: draggable({
|
||||||
element: target,
|
element: target,
|
||||||
dragHandle,
|
dragHandle,
|
||||||
getInitialData: () => ({ item }),
|
getInitialData: () => ({ item }),
|
||||||
onDragStart: () => {
|
onDragStart: () => {
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
onDragging?.(item);
|
onDragging?.(item);
|
||||||
},
|
},
|
||||||
onDrop: () => {
|
onDrop: () => {
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
onDragging?.(undefined);
|
onDragging?.(undefined);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [targetRef, dragHandleRef, item, onDragging]);
|
}, [targetRef, dragHandleRef, item, onDragging]);
|
||||||
|
|
||||||
return dragging;
|
return dragging;
|
||||||
|
@ -384,15 +381,17 @@ 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(
|
||||||
() =>
|
() =>
|
||||||
folder
|
folder
|
||||||
? {
|
? {
|
||||||
folder,
|
folder,
|
||||||
spaceId: space.roomId,
|
spaceId: space.roomId,
|
||||||
}
|
}
|
||||||
: space.roomId,
|
: space.roomId,
|
||||||
[folder, space]
|
[folder, space]
|
||||||
);
|
);
|
||||||
|
@ -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">
|
||||||
|
|
24
src/app/pages/client/sidebar/UnverifiedTab.css.ts
Normal file
24
src/app/pages/client/sidebar/UnverifiedTab.css.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
import { color, toRem } from 'folds';
|
||||||
|
|
||||||
|
const pushRight = keyframes({
|
||||||
|
from: {
|
||||||
|
transform: `translateX(${toRem(2)}) scale(1)`,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: 'translateX(0) scale(1)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UnverifiedTab = style({
|
||||||
|
animationName: pushRight,
|
||||||
|
animationDuration: '400ms',
|
||||||
|
animationIterationCount: 30,
|
||||||
|
animationDirection: 'alternate',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UnverifiedAvatar = style({
|
||||||
|
backgroundColor: color.Critical.Container,
|
||||||
|
color: color.Critical.OnContainer,
|
||||||
|
borderColor: color.Critical.ContainerLine,
|
||||||
|
});
|
49
src/app/pages/client/sidebar/UnverifiedTab.tsx
Normal file
49
src/app/pages/client/sidebar/UnverifiedTab.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Badge, color, Icon, Icons, Text } from 'folds';
|
||||||
|
import { openSettings } from '../../../../client/action/navigation';
|
||||||
|
import { isCrossVerified } from '../../../../util/matrixUtil';
|
||||||
|
import {
|
||||||
|
SidebarAvatar,
|
||||||
|
SidebarItem,
|
||||||
|
SidebarItemBadge,
|
||||||
|
SidebarItemTooltip,
|
||||||
|
} from '../../../components/sidebar';
|
||||||
|
import { useDeviceList } from '../../../hooks/useDeviceList';
|
||||||
|
import { tabText } from '../../../organisms/settings/Settings';
|
||||||
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import * as css from './UnverifiedTab.css';
|
||||||
|
|
||||||
|
export function UnverifiedTab() {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const deviceList = useDeviceList();
|
||||||
|
const unverified = deviceList?.filter(
|
||||||
|
(device) => isCrossVerified(mx, device.device_id) === false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!unverified?.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarItem className={css.UnverifiedTab}>
|
||||||
|
<SidebarItemTooltip tooltip="Unverified Sessions">
|
||||||
|
{(triggerRef) => (
|
||||||
|
<SidebarAvatar
|
||||||
|
className={css.UnverifiedAvatar}
|
||||||
|
as="button"
|
||||||
|
ref={triggerRef}
|
||||||
|
outlined
|
||||||
|
onClick={() => openSettings(tabText.SECURITY)}
|
||||||
|
>
|
||||||
|
<Icon style={{ color: color.Critical.Main }} src={Icons.ShieldUser} />
|
||||||
|
</SidebarAvatar>
|
||||||
|
)}
|
||||||
|
</SidebarItemTooltip>
|
||||||
|
<SidebarItemBadge hasCount>
|
||||||
|
<Badge variant="Critical" size="400" fill="Solid" radii="Pill" outlined={false}>
|
||||||
|
<Text as="span" size="L400">
|
||||||
|
{unverified.length}
|
||||||
|
</Text>
|
||||||
|
</Badge>
|
||||||
|
</SidebarItemBadge>
|
||||||
|
</SidebarItem>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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,12 +28,26 @@ 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">
|
||||||
<Text size="H3" truncate>
|
{screenSize === ScreenSize.Mobile && (
|
||||||
Message Search
|
<BackRouteHandler>
|
||||||
</Text>
|
{(onBack) => (
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<Icon src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</BackRouteHandler>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||||
|
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
|
||||||
|
<Text size="H3" truncate>
|
||||||
|
Message Search
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" basis="No" />
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Box style={{ position: 'relative' }} grow="Yes">
|
<Box style={{ position: 'relative' }} grow="Yes">
|
||||||
|
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
13
src/app/pages/pathSearchParam.ts
Normal file
13
src/app/pages/pathSearchParam.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { _RoomSearchParams, DirectCreateSearchParams } from './paths';
|
||||||
|
|
||||||
|
type SearchParamsGetter<T> = (searchParams: URLSearchParams) => T;
|
||||||
|
|
||||||
|
export const getRoomSearchParams: SearchParamsGetter<_RoomSearchParams> = (searchParams) => ({
|
||||||
|
viaServers: searchParams.get('viaServers') ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDirectCreateSearchParams: SearchParamsGetter<DirectCreateSearchParams> = (
|
||||||
|
searchParams
|
||||||
|
) => ({
|
||||||
|
userId: searchParams.get('userId') ?? undefined,
|
||||||
|
});
|
|
@ -35,6 +35,11 @@ export type _SearchPathSearchParams = {
|
||||||
senders?: string;
|
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}`;
|
||||||
|
|
||||||
|
|
84
src/app/plugins/matrix-to.ts
Normal file
84
src/app/plugins/matrix-to.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
const MATRIX_TO_BASE = 'https://matrix.to';
|
||||||
|
|
||||||
|
export const getMatrixToUser = (userId: string): string => `${MATRIX_TO_BASE}/#/${userId}`;
|
||||||
|
|
||||||
|
const withViaServers = (fragment: string, viaServers: string[]): string =>
|
||||||
|
`${fragment}?${viaServers.map((server) => `via=${server}`).join('&')}`;
|
||||||
|
|
||||||
|
export const getMatrixToRoom = (roomIdOrAlias: string, viaServers?: string[]): string => {
|
||||||
|
let fragment = roomIdOrAlias;
|
||||||
|
|
||||||
|
if (Array.isArray(viaServers) && viaServers.length > 0) {
|
||||||
|
fragment = withViaServers(fragment, viaServers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${MATRIX_TO_BASE}/#/${fragment}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMatrixToRoomEvent = (
|
||||||
|
roomIdOrAlias: string,
|
||||||
|
eventId: string,
|
||||||
|
viaServers?: string[]
|
||||||
|
): string => {
|
||||||
|
let fragment = `${roomIdOrAlias}/${eventId}`;
|
||||||
|
|
||||||
|
if (Array.isArray(viaServers) && viaServers.length > 0) {
|
||||||
|
fragment = withViaServers(fragment, viaServers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${MATRIX_TO_BASE}/#/${fragment}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixToRoom = {
|
||||||
|
roomIdOrAlias: string;
|
||||||
|
viaServers?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatrixToRoomEvent = MatrixToRoom & {
|
||||||
|
eventId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/;
|
||||||
|
export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href);
|
||||||
|
|
||||||
|
const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/;
|
||||||
|
const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/?(\?[\S]*)?$/;
|
||||||
|
const MATRIX_TO_ROOM_EVENT =
|
||||||
|
/^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/;
|
||||||
|
|
||||||
|
export const parseMatrixToUser = (href: string): string | undefined => {
|
||||||
|
const match = href.match(MATRIX_TO_USER);
|
||||||
|
if (!match) return undefined;
|
||||||
|
const userId = match[1];
|
||||||
|
return userId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseMatrixToRoom = (href: string): MatrixToRoom | undefined => {
|
||||||
|
const match = href.match(MATRIX_TO_ROOM);
|
||||||
|
if (!match) return undefined;
|
||||||
|
|
||||||
|
const roomIdOrAlias = match[1];
|
||||||
|
const viaSearchStr = match[2];
|
||||||
|
const viaServers = new URLSearchParams(viaSearchStr).getAll('via');
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomIdOrAlias,
|
||||||
|
viaServers: viaServers.length === 0 ? undefined : viaServers,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseMatrixToRoomEvent = (href: string): MatrixToRoomEvent | undefined => {
|
||||||
|
const match = href.match(MATRIX_TO_ROOM_EVENT);
|
||||||
|
if (!match) return undefined;
|
||||||
|
|
||||||
|
const roomIdOrAlias = match[1];
|
||||||
|
const eventId = match[2];
|
||||||
|
const viaSearchStr = match[3];
|
||||||
|
const viaServers = new URLSearchParams(viaSearchStr).getAll('via');
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomIdOrAlias,
|
||||||
|
eventId,
|
||||||
|
viaServers: viaServers.length === 0 ? undefined : viaServers,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable jsx-a11y/alt-text */
|
/* 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;
|
||||||
}
|
}
|
||||||
|
|
65
src/app/plugins/via-servers.ts
Normal file
65
src/app/plugins/via-servers.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
import { IPowerLevels } from '../hooks/usePowerLevels';
|
||||||
|
import { getMxIdServer } from '../utils/matrix';
|
||||||
|
import { StateEvent } from '../../types/matrix/room';
|
||||||
|
import { getStateEvent } from '../utils/room';
|
||||||
|
|
||||||
|
export const getViaServers = (room: Room): string[] => {
|
||||||
|
const getHighestPowerUserId = (): string | undefined => {
|
||||||
|
const powerLevels = getStateEvent(room, StateEvent.RoomPowerLevels)?.getContent<IPowerLevels>();
|
||||||
|
|
||||||
|
if (!powerLevels) return undefined;
|
||||||
|
const userIdToPower = powerLevels.users;
|
||||||
|
if (!userIdToPower) return undefined;
|
||||||
|
let powerUserId: string | undefined;
|
||||||
|
|
||||||
|
Object.keys(userIdToPower).forEach((userId) => {
|
||||||
|
if (userIdToPower[userId] <= (powerLevels.users_default ?? 0)) return;
|
||||||
|
|
||||||
|
if (!powerUserId) {
|
||||||
|
powerUserId = userId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (userIdToPower[userId] > userIdToPower[powerUserId]) {
|
||||||
|
powerUserId = userId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return powerUserId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerToPopulation = (): Record<string, number> => {
|
||||||
|
const members = room.getMembers();
|
||||||
|
const serverToPop: Record<string, number> = {};
|
||||||
|
|
||||||
|
members?.forEach((member) => {
|
||||||
|
const { userId } = member;
|
||||||
|
const server = getMxIdServer(userId);
|
||||||
|
if (!server) return;
|
||||||
|
const serverPop = serverToPop[server];
|
||||||
|
if (serverPop === undefined) {
|
||||||
|
serverToPop[server] = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serverToPop[server] = serverPop + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return serverToPop;
|
||||||
|
};
|
||||||
|
|
||||||
|
const via: string[] = [];
|
||||||
|
const userId = getHighestPowerUserId();
|
||||||
|
if (userId) {
|
||||||
|
const server = getMxIdServer(userId);
|
||||||
|
if (server) via.push(server);
|
||||||
|
}
|
||||||
|
const serverToPop = getServerToPopulation();
|
||||||
|
const sortedServers = Object.keys(serverToPop).sort(
|
||||||
|
(svrA, svrB) => serverToPop[svrB] - serverToPop[svrA]
|
||||||
|
);
|
||||||
|
const mostPop3 = sortedServers.slice(0, 3);
|
||||||
|
if (via.length === 0) return mostPop3;
|
||||||
|
if (mostPop3.includes(via[0])) {
|
||||||
|
mostPop3.splice(mostPop3.indexOf(via[0]), 1);
|
||||||
|
}
|
||||||
|
return via.concat(mostPop3.slice(0, 2));
|
||||||
|
};
|
|
@ -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>;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue