Compare commits
16 commits
22e9e82189
...
a8d78f19bf
Author | SHA1 | Date | |
---|---|---|---|
a8d78f19bf | |||
6c5b8a1d14 | |||
|
830d05e217 | ||
|
7e7bee8f48 | ||
|
ac1797344c | ||
|
b4ce8a7cab | ||
|
e68c56b334 | ||
|
cabfdd47b5 | ||
|
cfe893f358 | ||
|
581211f13e | ||
|
8ed78d48fb | ||
|
96222de5bc | ||
|
681287c46a | ||
|
9cb5c70d51 | ||
|
c62050445b | ||
|
a8f5a6c2f4 |
52 changed files with 894 additions and 297 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'
|
||||
run: npm run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
with:
|
||||
name: preview
|
||||
path: dist
|
||||
|
@ -33,7 +33,7 @@ jobs:
|
|||
- name: Save pr number
|
||||
run: echo ${PR_NUMBER} > ./pr.txt
|
||||
- name: Upload pr number
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
with:
|
||||
name: pr
|
||||
path: ./pr.txt
|
||||
|
|
2
.github/workflows/docker-pr.yml
vendored
2
.github/workflows/docker-pr.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v6.5.0
|
||||
uses: docker/build-push-action@v6.6.1
|
||||
with:
|
||||
context: .
|
||||
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
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.5.0
|
||||
uses: docker/setup-buildx-action@v3.6.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
|
@ -90,7 +90,7 @@ jobs:
|
|||
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||
ghcr.io/${{ github.repository }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6.5.0
|
||||
uses: docker/build-push-action@v6.6.1
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
|
26
README.md
26
README.md
|
@ -19,22 +19,24 @@ A Matrix client focusing primarily on simple, elegant and secure interface. The
|
|||
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
|
||||
|
||||
## Getting started
|
||||
Web app is available at https://app.cinny.in and gets updated on each new release. The `dev` branch is continuously deployed at https://dev.cinny.in but keep in mind that it could have things broken.
|
||||
* Web app is available at https://app.cinny.in and gets updated on each new release. The `dev` branch is continuously deployed at https://dev.cinny.in but keep in mind that it could have things broken.
|
||||
|
||||
You can also download our desktop app from [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
||||
* You can also download our desktop app from [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
||||
|
||||
To host Cinny on your own, download tarball of the app from [GitHub release](https://github.com/cinnyapp/cinny/releases/latest).
|
||||
* To host Cinny on your own, download tarball of the app from [GitHub release](https://github.com/cinnyapp/cinny/releases/latest).
|
||||
You can serve the application with a webserver of your choice by simply copying `dist/` directory to the webroot.
|
||||
To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
|
||||
To set default Homeserver on login, register and Explore Community page, place a customized [`config.json`](config.json) in webroot of your choice.
|
||||
You will also need to setup redirects to serve the assests. An example setting of redirects for netlify is done in [`netlify.toml`](netlify.toml). You can also set `hashRouter.enabled = true` in [`config.json`](config.json) if you have trouble setting redirects.
|
||||
To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts). For example, if you want to deploy on `https://cinny.in/app`, then change `base: '/app'`.
|
||||
|
||||
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by:
|
||||
```
|
||||
docker pull ajbura/cinny
|
||||
```
|
||||
or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by:
|
||||
```
|
||||
docker pull ghcr.io/cinnyapp/cinny:latest
|
||||
```
|
||||
* Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by:
|
||||
```
|
||||
docker pull ajbura/cinny
|
||||
```
|
||||
or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by:
|
||||
```
|
||||
docker pull ghcr.io/cinnyapp/cinny:latest
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>PGP Public Key to verify tarball</summary>
|
||||
|
|
32
config.json
32
config.json
|
@ -1,34 +1,16 @@
|
|||
{
|
||||
"defaultHomeserver": 2,
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": [
|
||||
"converser.eu",
|
||||
"envs.net",
|
||||
"matrix.org",
|
||||
"monero.social",
|
||||
"mozilla.org",
|
||||
"xmr.se"
|
||||
"chat.naskya.net",
|
||||
"chat.sup39.dev"
|
||||
],
|
||||
"allowCustomHomeservers": true,
|
||||
"allowCustomHomeservers": false,
|
||||
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
"spaces": [
|
||||
"#cinny-space:matrix.org",
|
||||
"#community:matrix.org",
|
||||
"#space:envs.net",
|
||||
"#science-space:matrix.org",
|
||||
"#libregaming-games:tchncs.de",
|
||||
"#mathematics-on:matrix.org"
|
||||
],
|
||||
"rooms": [
|
||||
"#cinny:matrix.org",
|
||||
"#freesoftware:matrix.org",
|
||||
"#pcapdroid:matrix.org",
|
||||
"#gentoo:matrix.org",
|
||||
"#PrivSec.dev:arcticfoxes.net",
|
||||
"#disroot:aria-net.org"
|
||||
],
|
||||
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
|
||||
"spaces": [],
|
||||
"rooms": [],
|
||||
"servers": []
|
||||
},
|
||||
|
||||
"hashRouter": {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||
<title>Cinny</title>
|
||||
<title>Cinny@さぽなす</title>
|
||||
<meta name="name" content="Cinny" />
|
||||
<meta name="author" content="Ajay Bura" />
|
||||
<meta
|
||||
|
@ -26,6 +26,7 @@
|
|||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||
<link rel="stylesheet" href="/mx-uc.css" />
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
|
138
package-lock.json
generated
138
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "cinny",
|
||||
"version": "4.0.3",
|
||||
"version": "4.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cinny",
|
||||
"version": "4.0.3",
|
||||
"version": "4.1.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||
|
@ -37,6 +37,9 @@
|
|||
"formik": "2.4.6",
|
||||
"html-dom-parser": "4.0.0",
|
||||
"html-react-parser": "4.2.0",
|
||||
"i18next": "23.12.2",
|
||||
"i18next-browser-languagedetector": "8.0.0",
|
||||
"i18next-http-backend": "2.5.2",
|
||||
"immer": "9.0.16",
|
||||
"is-hotkey": "0.2.0",
|
||||
"jotai": "2.6.0",
|
||||
|
@ -54,6 +57,7 @@
|
|||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "4.0.13",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-i18next": "15.0.0",
|
||||
"react-modal": "3.16.1",
|
||||
"react-range": "1.8.14",
|
||||
"react-router-dom": "6.20.0",
|
||||
|
@ -438,11 +442,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.20.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz",
|
||||
"integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==",
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
|
||||
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -467,11 +472,6 @@
|
|||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
|
||||
|
@ -6146,6 +6146,15 @@
|
|||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-react-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-4.2.0.tgz",
|
||||
|
@ -6191,6 +6200,76 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "23.12.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.12.2.tgz",
|
||||
"integrity": "sha512-XIeh5V+bi8SJSWGL3jqbTEBW5oD6rbP5L+E7dVQh1MNTxxYef0x15rhJVcRb7oiuq4jLtgy2SD8eFlf6P2cmqg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
|
||||
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz",
|
||||
"integrity": "sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-fetch": "4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend/node_modules/cross-fetch": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-http-backend/node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
@ -7717,6 +7796,28 @@
|
|||
"react": ">=16.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.0.tgz",
|
||||
"integrity": "sha512-2O3IgF4zivg57Q6p6i+ChDgJ371IDcEWbuWC6gvoh5NbkDMs0Q+O7RPr4v61+Se32E0V+LmtwePAeqWZW0bi6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.8",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
@ -7824,6 +7925,12 @@
|
|||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
|
||||
|
@ -9294,6 +9401,15 @@
|
|||
"@esbuild/win32-x64": "0.19.12"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cinny",
|
||||
"version": "4.0.3",
|
||||
"version": "4.1.0",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
@ -48,6 +48,9 @@
|
|||
"formik": "2.4.6",
|
||||
"html-dom-parser": "4.0.0",
|
||||
"html-react-parser": "4.2.0",
|
||||
"i18next": "23.12.2",
|
||||
"i18next-browser-languagedetector": "8.0.0",
|
||||
"i18next-http-backend": "2.5.2",
|
||||
"immer": "9.0.16",
|
||||
"is-hotkey": "0.2.0",
|
||||
"jotai": "2.6.0",
|
||||
|
@ -65,6 +68,7 @@
|
|||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "4.0.13",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-i18next": "15.0.0",
|
||||
"react-modal": "3.16.1",
|
||||
"react-range": "1.8.14",
|
||||
"react-router-dom": "6.20.0",
|
||||
|
|
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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Cinny",
|
||||
"name": "Cinny@さぽなす",
|
||||
"short_name": "Cinny",
|
||||
"description": "Yet another matrix client",
|
||||
"description": "Yet another matrix client for supnas",
|
||||
"dir": "auto",
|
||||
"lang": "en-US",
|
||||
"display": "standalone",
|
||||
|
|
4
public/mx-uc.css
Normal file
4
public/mx-uc.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
:root {
|
||||
--mx-uc--sup39-chat_sup39_dev: #2EE5B8;
|
||||
--mx-uc--naskya-chat_naskya_net: #F25A85;
|
||||
}
|
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);
|
||||
}
|
|
@ -25,6 +25,7 @@ import {
|
|||
parseMatrixToUser,
|
||||
testMatrixTo,
|
||||
} from '../../plugins/matrix-to';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
|
||||
const markNodeToType: Record<string, MarkType> = {
|
||||
b: MarkType.Bold,
|
||||
|
@ -73,7 +74,7 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
|
|||
return createEmoticonElement(src, alt || 'Unknown Emoji');
|
||||
}
|
||||
if (node.name === 'a') {
|
||||
const href = decodeURIComponent(node.attribs.href);
|
||||
const href = tryDecodeURIComponent(node.attribs.href);
|
||||
if (typeof href !== 'string') return undefined;
|
||||
if (testMatrixTo(href)) {
|
||||
const userMention = parseMatrixToUser(href);
|
||||
|
|
|
@ -5,6 +5,25 @@ export const ReplyBend = style({
|
|||
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({
|
||||
marginBottom: toRem(1),
|
||||
minWidth: 0,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Box, Icon, Icons, Text, as, color, toRem } from 'folds';
|
||||
import { EventTimelineSet, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
|
||||
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 classNames from 'classnames';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
|
@ -22,6 +22,7 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>(
|
|||
<Box
|
||||
className={classNames(css.Reply, className)}
|
||||
alignItems="Center"
|
||||
alignSelf="Start"
|
||||
gap="100"
|
||||
{...props}
|
||||
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 = {
|
||||
mx: MatrixClient;
|
||||
room: Room;
|
||||
timelineSet?: EventTimelineSet;
|
||||
eventId: string;
|
||||
timelineSet?: EventTimelineSet | undefined;
|
||||
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>(
|
||||
timelineSet?.findEventById(eventId)
|
||||
timelineSet?.findEventById(replyEventId)
|
||||
);
|
||||
const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []);
|
||||
|
||||
|
@ -62,7 +73,7 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
|
|||
useEffect(() => {
|
||||
let disposed = false;
|
||||
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);
|
||||
if (disposed) return;
|
||||
if (err) {
|
||||
|
@ -78,13 +89,18 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
|
|||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, [replyEvent, mx, room, eventId]);
|
||||
}, [replyEvent, mx, room, replyEventId]);
|
||||
|
||||
const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted';
|
||||
const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody;
|
||||
|
||||
return (
|
||||
<Box direction="Column" {...props} ref={ref}>
|
||||
{threadRootId && (
|
||||
<ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
|
||||
)}
|
||||
<ReplyLayout
|
||||
as="button"
|
||||
userColor={sender ? colorMXID(sender) : undefined}
|
||||
username={
|
||||
sender && (
|
||||
|
@ -93,8 +109,8 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
|
|||
</Text>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
ref={ref}
|
||||
data-event-id={replyEventId}
|
||||
onClick={onClick}
|
||||
>
|
||||
{replyEvent !== undefined ? (
|
||||
<Text size="T300" truncate>
|
||||
|
@ -110,5 +126,6 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
|
|||
/>
|
||||
)}
|
||||
</ReplyLayout>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -87,15 +87,17 @@ export const Page = as<'div'>(({ className, ...props }, ref) => (
|
|||
/>
|
||||
));
|
||||
|
||||
export const PageHeader = as<'div'>(({ className, ...props }, ref) => (
|
||||
export const PageHeader = as<'div', css.PageHeaderVariants>(
|
||||
({ className, balance, ...props }, ref) => (
|
||||
<Header
|
||||
as="header"
|
||||
size="600"
|
||||
className={classNames(css.PageHeader, className)}
|
||||
className={classNames(css.PageHeader({ balance }), className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
)
|
||||
);
|
||||
|
||||
export const PageContent = as<'div'>(({ className, ...props }, ref) => (
|
||||
<div className={classNames(css.PageContent, className)} {...props} ref={ref} />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
export const PageNav = style({
|
||||
|
@ -33,11 +34,21 @@ export const PageNavContent = style({
|
|||
paddingBottom: config.space.S700,
|
||||
});
|
||||
|
||||
export const PageHeader = style({
|
||||
export const PageHeader = recipe({
|
||||
base: {
|
||||
paddingLeft: config.space.S400,
|
||||
paddingRight: config.space.S200,
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
},
|
||||
variants: {
|
||||
balance: {
|
||||
true: {
|
||||
paddingLeft: config.space.S200,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export type PageHeaderVariants = RecipeVariants<typeof PageHeader>;
|
||||
|
||||
export const PageContent = style([
|
||||
DefaultReset,
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
useIntersectionObserver,
|
||||
} from '../../hooks/useIntersectionObserver';
|
||||
import * as css from './UrlPreviewCard.css';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
|
||||
const linkStyles = { color: color.Success.Main };
|
||||
|
||||
|
@ -43,7 +44,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||
priority="300"
|
||||
>
|
||||
{typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
|
||||
{decodeURIComponent(url)}
|
||||
{tryDecodeURIComponent(url)}
|
||||
</Text>
|
||||
<Text truncate priority="400">
|
||||
<b>{prev['og:title']}</b>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Box, Scroll, Text, toRem } from 'folds';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text, toRem } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { RoomCard } from '../../components/room-card';
|
||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||
|
@ -8,6 +8,8 @@ import { RoomSummaryLoader } from '../../components/RoomSummaryLoader';
|
|||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
||||
|
||||
type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] };
|
||||
export function JoinBeforeNavigate({
|
||||
|
@ -18,6 +20,7 @@ export function JoinBeforeNavigate({
|
|||
const mx = useMatrixClient();
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const handleView = (roomId: string) => {
|
||||
if (mx.getRoom(roomId)?.isSpaceRoom()) {
|
||||
|
@ -29,12 +32,25 @@ export function JoinBeforeNavigate({
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box shrink="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
{roomIdOrAlias}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover" size="0">
|
||||
|
|
|
@ -31,6 +31,8 @@ import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
|||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
||||
|
||||
type LobbyMenuProps = {
|
||||
roomId: string;
|
||||
|
@ -123,6 +125,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||
const space = useSpace();
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const name = useRoomName(space);
|
||||
const avatarMxc = useRoomAvatar(space);
|
||||
|
@ -133,8 +136,29 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<PageHeader className={showProfile ? undefined : css.Header}>
|
||||
<PageHeader className={showProfile ? undefined : css.Header} balance>
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
{screenSize === ScreenSize.Mobile ? (
|
||||
<>
|
||||
<Box shrink="No">
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
</Box>
|
||||
<Box grow="Yes" justifyContent="Center">
|
||||
{showProfile && (
|
||||
<Text size="H3" truncate>
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box grow="Yes" basis="No" />
|
||||
<Box justifyContent="Center" alignItems="Center" gap="300">
|
||||
{showProfile && (
|
||||
|
@ -153,7 +177,15 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box shrink="No" grow="Yes" basis="No" justifyContent="End">
|
||||
</>
|
||||
)}
|
||||
<Box
|
||||
shrink="No"
|
||||
grow={screenSize === ScreenSize.Mobile ? 'No' : 'Yes'}
|
||||
basis={screenSize === ScreenSize.Mobile ? 'Yes' : 'No'}
|
||||
justifyContent="End"
|
||||
>
|
||||
{screenSize !== ScreenSize.Mobile && (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
offset={4}
|
||||
|
@ -169,6 +201,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
|||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
|
|
|
@ -148,7 +148,7 @@ export function SearchResultGroup({
|
|||
}
|
||||
);
|
||||
|
||||
const handleOpenClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const handleOpenClick: MouseEventHandler = (evt) => {
|
||||
const eventId = evt.currentTarget.getAttribute('data-event-id');
|
||||
if (!eventId) return;
|
||||
onOpen(room.roomId, eventId);
|
||||
|
@ -183,15 +183,16 @@ export function SearchResultGroup({
|
|||
event.sender;
|
||||
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
|
||||
|
||||
const relation = event.content['m.relates_to'];
|
||||
const mainEventId =
|
||||
event.content['m.relates_to']?.rel_type === RelationType.Replace
|
||||
? event.content['m.relates_to'].event_id
|
||||
: event.event_id;
|
||||
relation?.rel_type === RelationType.Replace ? relation.event_id : event.event_id;
|
||||
|
||||
const getContent = (() =>
|
||||
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 (
|
||||
<SequenceCard
|
||||
|
@ -240,11 +241,10 @@ export function SearchResultGroup({
|
|||
</Box>
|
||||
{replyEventId && (
|
||||
<Reply
|
||||
as="button"
|
||||
mx={mx}
|
||||
room={room}
|
||||
eventId={replyEventId}
|
||||
data-event-id={replyEventId}
|
||||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenClick}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { useKeyDown } from '../../hooks/useKeyDown';
|
|||
import { markAsRead } from '../../../client/action/notifications';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||
import { editableActiveElement } from '../../utils/dom';
|
||||
|
||||
export function Room() {
|
||||
const { eventId } = useParams();
|
||||
|
@ -28,7 +29,7 @@ export function Room() {
|
|||
window,
|
||||
useCallback(
|
||||
(evt) => {
|
||||
if (isKeyHotkey('escape', evt)) {
|
||||
if (isKeyHotkey('escape', evt) && !editableActiveElement()) {
|
||||
markAsRead(mx, room.roomId);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import React, {
|
|||
} from 'react';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
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 { Transforms, Editor } from 'slate';
|
||||
import {
|
||||
|
@ -106,7 +106,7 @@ import { CommandAutocomplete } from './CommandAutocomplete';
|
|||
import { Command, SHRUG, useCommands } from '../../hooks/useCommands';
|
||||
import { mobileOrTablet } from '../../utils/user-agent';
|
||||
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
||||
import { ReplyLayout } from '../../components/message';
|
||||
import { ReplyLayout, ThreadIndicator } from '../../components/message';
|
||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
|
||||
interface RoomInputProps {
|
||||
|
@ -186,9 +186,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
Transforms.insertFragment(editor, msgDraft);
|
||||
}, [editor, msgDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||
return () => {
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (!isEmptyEditor(editor)) {
|
||||
const parsedDraft = JSON.parse(JSON.stringify(editor.children));
|
||||
setMsgDraft(parsedDraft);
|
||||
|
@ -197,8 +196,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
}
|
||||
resetEditor(editor);
|
||||
resetEditorHistory(editor);
|
||||
};
|
||||
}, [roomId, editor, setMsgDraft]);
|
||||
},
|
||||
[roomId, editor, setMsgDraft]
|
||||
);
|
||||
|
||||
const handleRemoveUpload = useCallback(
|
||||
(upload: TUploadContent | TUploadContent[]) => {
|
||||
|
@ -312,6 +312,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
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;
|
||||
}
|
||||
}
|
||||
await mx.sendMessage(roomId, content);
|
||||
resetEditor(editor);
|
||||
|
@ -491,6 +496,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
>
|
||||
<Icon src={Icons.Cross} size="50" />
|
||||
</IconButton>
|
||||
<Box direction="Column">
|
||||
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
||||
<ReplyLayout
|
||||
userColor={colorMXID(replyDraft.userId)}
|
||||
username={
|
||||
|
@ -508,6 +515,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
</Text>
|
||||
</ReplyLayout>
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventTimelineSetHandlerMap,
|
||||
IContent,
|
||||
IEncryptedFile,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
|
@ -46,6 +47,7 @@ import {
|
|||
} from 'folds';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
decryptFile,
|
||||
eventWithShortcode,
|
||||
|
@ -836,13 +838,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
markAsRead(mx, room.roomId);
|
||||
};
|
||||
|
||||
const handleOpenReply: MouseEventHandler<HTMLButtonElement> = useCallback(
|
||||
const handleOpenReply: MouseEventHandler = useCallback(
|
||||
async (evt) => {
|
||||
const replyId = evt.currentTarget.getAttribute('data-reply-id');
|
||||
if (typeof replyId !== 'string') return;
|
||||
const replyTimeline = getEventTimeline(room, replyId);
|
||||
const targetId = evt.currentTarget.getAttribute('data-event-id');
|
||||
if (!targetId) return;
|
||||
const replyTimeline = getEventTimeline(room, targetId);
|
||||
const absoluteIndex =
|
||||
replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, replyId);
|
||||
replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, targetId);
|
||||
|
||||
if (typeof absoluteIndex === 'number') {
|
||||
scrollToItem(absoluteIndex, {
|
||||
|
@ -857,7 +859,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
});
|
||||
} else {
|
||||
setTimeline(getEmptyTimeline());
|
||||
loadEventTimeline(replyId);
|
||||
loadEventTimeline(targetId);
|
||||
}
|
||||
},
|
||||
[room, timeline, scrollToItem, loadEventTimeline]
|
||||
|
@ -908,8 +910,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const replyEvt = room.findEventById(replyId);
|
||||
if (!replyEvt) return;
|
||||
const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet());
|
||||
const { body, formatted_body: formattedBody }: Record<string, string> =
|
||||
editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
|
||||
const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
|
||||
const { body, formatted_body: formattedBody } = content;
|
||||
const { 'm.relates_to': relation } = replyEvt.getOriginalContent();
|
||||
const senderId = replyEvt.getSender();
|
||||
if (senderId && typeof body === 'string') {
|
||||
setReplyDraft({
|
||||
|
@ -917,6 +920,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
eventId: replyId,
|
||||
body,
|
||||
formattedBody,
|
||||
relation,
|
||||
});
|
||||
setTimeout(() => ReactEditor.focus(editor), 100);
|
||||
}
|
||||
|
@ -958,6 +962,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
},
|
||||
[editor]
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderMatrixEvent = useMatrixEventRenderer<
|
||||
[string, MatrixEvent, number, EventTimelineSet, boolean]
|
||||
|
@ -967,7 +972,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const reactionRelations = getEventReactions(timelineSet, mEventId);
|
||||
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
|
||||
const hasReactions = reactions && reactions.length > 0;
|
||||
const { replyEventId } = mEvent;
|
||||
const { replyEventId, threadRootId } = mEvent;
|
||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
|
||||
const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet);
|
||||
|
@ -1002,12 +1007,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
reply={
|
||||
replyEventId && (
|
||||
<Reply
|
||||
as="button"
|
||||
mx={mx}
|
||||
room={room}
|
||||
timelineSet={timelineSet}
|
||||
eventId={replyEventId}
|
||||
data-reply-id={replyEventId}
|
||||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenReply}
|
||||
/>
|
||||
)
|
||||
|
@ -1048,7 +1052,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const reactionRelations = getEventReactions(timelineSet, mEventId);
|
||||
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
|
||||
const hasReactions = reactions && reactions.length > 0;
|
||||
const { replyEventId } = mEvent;
|
||||
const { replyEventId, threadRootId } = mEvent;
|
||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
|
||||
return (
|
||||
|
@ -1075,12 +1079,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
reply={
|
||||
replyEventId && (
|
||||
<Reply
|
||||
as="button"
|
||||
mx={mx}
|
||||
room={room}
|
||||
timelineSet={timelineSet}
|
||||
eventId={replyEventId}
|
||||
data-reply-id={replyEventId}
|
||||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenReply}
|
||||
/>
|
||||
)
|
||||
|
@ -1273,7 +1276,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T300" priority="300">
|
||||
<b>{senderName}</b>
|
||||
{' changed room name'}
|
||||
{t('Organisms.RoomCommon.changed_room_name')}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
|||
if (evt.metaKey || evt.altKey || evt.ctrlKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// do not focus on F keys
|
||||
if (/^F\d+$/.test(code)) return false;
|
||||
|
||||
|
@ -36,6 +37,9 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
|||
code.startsWith('Alt') ||
|
||||
code.startsWith('Control') ||
|
||||
code.startsWith('Arrow') ||
|
||||
code.startsWith('Page') ||
|
||||
code.startsWith('End') ||
|
||||
code.startsWith('Home') ||
|
||||
code === 'Tab' ||
|
||||
code === 'Space' ||
|
||||
code === 'Enter' ||
|
||||
|
|
|
@ -52,6 +52,7 @@ import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
|||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
||||
import { getViaServers } from '../../plugins/via-servers';
|
||||
import { BackRouteHandler } from '../../components/BackRouteHandler';
|
||||
|
||||
type RoomMenuProps = {
|
||||
room: Room;
|
||||
|
@ -203,19 +204,36 @@ export function RoomViewHeader() {
|
|||
};
|
||||
|
||||
return (
|
||||
<PageHeader>
|
||||
<PageHeader balance={screenSize === ScreenSize.Mobile}>
|
||||
<Box grow="Yes" gap="300">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<Box shrink="No" alignItems="Center">
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
<Box grow="Yes" alignItems="Center" gap="300">
|
||||
{screenSize !== ScreenSize.Mobile && (
|
||||
<Avatar size="300">
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<RoomIcon size="200" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled />
|
||||
<RoomIcon
|
||||
size="200"
|
||||
joinRule={room.getJoinRule() ?? JoinRule.Restricted}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
<Box direction="Column">
|
||||
<Text size={topic ? 'H5' : 'H3'} truncate>
|
||||
{name}
|
||||
|
|
|
@ -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;
|
||||
}
|
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;
|
|
@ -15,7 +15,7 @@ export function AuthFooter() {
|
|||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
v4.0.3
|
||||
v4.1.0
|
||||
</Text>
|
||||
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
|
||||
Twitter
|
||||
|
|
|
@ -29,6 +29,7 @@ import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo';
|
|||
import { AuthFlowsLoader } from '../../components/AuthFlowsLoader';
|
||||
import { AuthFlowsProvider } from '../../hooks/useAuthFlows';
|
||||
import { AuthServerProvider } from '../../hooks/useAuthServer';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
|
||||
const currentAuthPath = (pathname: string): string => {
|
||||
if (matchPath(LOGIN_PATH, pathname)) {
|
||||
|
@ -72,7 +73,7 @@ export function AuthLayout() {
|
|||
const clientConfig = useClientConfig();
|
||||
|
||||
const defaultServer = clientDefaultServer(clientConfig);
|
||||
let server: string = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer;
|
||||
let server: string = urlEncodedServer ? tryDecodeURIComponent(urlEncodedServer) : defaultServer;
|
||||
|
||||
if (!clientAllowedServer(clientConfig, server)) {
|
||||
server = defaultServer;
|
||||
|
@ -94,7 +95,7 @@ export function AuthLayout() {
|
|||
|
||||
// if server is mismatches with path server, update path
|
||||
useEffect(() => {
|
||||
if (!urlEncodedServer || decodeURIComponent(urlEncodedServer) !== server) {
|
||||
if (!urlEncodedServer || tryDecodeURIComponent(urlEncodedServer) !== server) {
|
||||
navigate(
|
||||
generatePath(currentAuthPath(location.pathname), {
|
||||
server: encodeURIComponent(server),
|
||||
|
|
|
@ -183,17 +183,17 @@ function MessageNotifications() {
|
|||
removed,
|
||||
data
|
||||
) => {
|
||||
if (mx.getSyncState() !== 'SYNCING') return;
|
||||
if (document.hasFocus() && (selectedRoomId === room?.roomId || notificationSelected)) return;
|
||||
if (
|
||||
mx.getSyncState() !== 'SYNCING' ||
|
||||
selectedRoomId === room?.roomId ||
|
||||
notificationSelected ||
|
||||
!room ||
|
||||
!data.liveEvent ||
|
||||
room.isSpaceRoom() ||
|
||||
!isNotificationEvent(mEvent) ||
|
||||
getNotificationType(mx, room.roomId) === NotificationType.Mute
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sender = mEvent.getSender();
|
||||
const eventId = mEvent.getId();
|
||||
|
|
|
@ -10,7 +10,15 @@ import {
|
|||
SidebarItemTooltip,
|
||||
SidebarItem,
|
||||
} from '../../components/sidebar';
|
||||
import { DirectTab, HomeTab, SpaceTabs, InboxTab, ExploreTab, UserTab } from './sidebar';
|
||||
import {
|
||||
DirectTab,
|
||||
HomeTab,
|
||||
SpaceTabs,
|
||||
InboxTab,
|
||||
ExploreTab,
|
||||
UserTab,
|
||||
UnverifiedTab,
|
||||
} from './sidebar';
|
||||
import { openCreateRoom, openSearch } from '../../../client/action/navigation';
|
||||
|
||||
export function SidebarNav() {
|
||||
|
@ -65,6 +73,8 @@ export function SidebarNav() {
|
|||
</SidebarItemTooltip>
|
||||
</SidebarItem>
|
||||
|
||||
<UnverifiedTab />
|
||||
|
||||
<InboxTab />
|
||||
<UserTab />
|
||||
</SidebarStack>
|
||||
|
|
|
@ -24,7 +24,7 @@ export function WelcomePage() {
|
|||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
v4.0.3
|
||||
v4.1.0
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Box, Icon, Icons, Scroll, Text } from 'folds';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
import { RoomCard, RoomCardGrid } from '../../../components/room-card';
|
||||
|
@ -9,21 +9,38 @@ import {
|
|||
Page,
|
||||
PageContent,
|
||||
PageContentCenter,
|
||||
PageHeader,
|
||||
PageHero,
|
||||
PageHeroSection,
|
||||
} from '../../../components/page';
|
||||
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
|
||||
import * as css from './style.css';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
export function FeaturedRooms() {
|
||||
const { featuredCommunities } = useClientConfig();
|
||||
const { rooms, spaces } = featuredCommunities ?? {};
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const screenSize = useScreenSizeContext();
|
||||
const { navigateSpace, navigateRoom } = useRoomNavigate();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<PageHeader>
|
||||
<Box shrink="No">
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
)}
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent>
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
Button,
|
||||
Chip,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Line,
|
||||
|
@ -42,6 +43,8 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
|
|||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { getMxIdServer } from '../../../utils/matrix';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
|
||||
useMemo(
|
||||
|
@ -344,6 +347,7 @@ export function PublicRooms() {
|
|||
const userServer = userId && getMxIdServer(userId);
|
||||
const allRooms = useAtomValue(allRoomsAtom);
|
||||
const { navigateSpace, navigateRoom } = useRoomNavigate();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const serverSearchParams = useServerSearchParams(searchParams);
|
||||
|
@ -466,7 +470,7 @@ export function PublicRooms() {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageHeader balance>
|
||||
{isSearch ? (
|
||||
<>
|
||||
<Box grow="Yes" basis="No">
|
||||
|
@ -482,20 +486,34 @@ export function PublicRooms() {
|
|||
</Box>
|
||||
|
||||
<Box grow="No" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Search} />
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
|
||||
<Text size="H3" truncate>
|
||||
Search
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" />
|
||||
<Box grow="Yes" basis="No" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Category} />
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Category} />}
|
||||
<Text size="H3" truncate>
|
||||
{server}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</>
|
||||
)}
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
|
|
|
@ -1,22 +1,39 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { Box, Icon, Icons, Text, Scroll } from 'folds';
|
||||
import { Box, Icon, Icons, Text, Scroll, IconButton } from 'folds';
|
||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
|
||||
import { MessageSearch } from '../../../features/message-search';
|
||||
import { useHomeRooms } from './useHomeRooms';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
export function HomeSearch() {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const rooms = useHomeRooms();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Search} />
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
|
||||
<Text size="H3" truncate>
|
||||
Message Search
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box style={{ position: 'relative' }} grow="Yes">
|
||||
<Scroll ref={scrollRef} hideTrack visibility="Hover">
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
Box,
|
||||
Button,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
|
@ -39,6 +40,8 @@ import { RoomTopicViewer } from '../../../components/room-topic-viewer';
|
|||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { useRoomTopic } from '../../../hooks/useRoomMeta';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
const COMPACT_CARD_WIDTH = 548;
|
||||
|
||||
|
@ -205,6 +208,7 @@ export function Invites() {
|
|||
useCallback(() => containerRef.current, []),
|
||||
useCallback((width) => setCompact(width <= COMPACT_CARD_WIDTH), [])
|
||||
);
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
|
||||
|
@ -225,13 +229,27 @@ export function Invites() {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Mail} />
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Mail} />}
|
||||
<Text size="H3" truncate>
|
||||
Invitations
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
IRoomEvent,
|
||||
JoinRule,
|
||||
Method,
|
||||
RelationType,
|
||||
Room,
|
||||
} from 'matrix-js-sdk';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
@ -78,6 +79,8 @@ import { UserAvatar } from '../../../components/user-avatar';
|
|||
import { EncryptedContent } from '../../../features/room/message';
|
||||
import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler';
|
||||
import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
type RoomNotificationsGroup = {
|
||||
roomId: string;
|
||||
|
@ -350,7 +353,7 @@ function RoomNotificationsGroupComp({
|
|||
}
|
||||
);
|
||||
|
||||
const handleOpenClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const handleOpenClick: MouseEventHandler = (evt) => {
|
||||
const eventId = evt.currentTarget.getAttribute('data-event-id');
|
||||
if (!eventId) return;
|
||||
onOpen(room.roomId, eventId);
|
||||
|
@ -401,7 +404,10 @@ function RoomNotificationsGroupComp({
|
|||
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
|
||||
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 (
|
||||
<SequenceCard
|
||||
|
@ -450,11 +456,10 @@ function RoomNotificationsGroupComp({
|
|||
</Box>
|
||||
{replyEventId && (
|
||||
<Reply
|
||||
as="button"
|
||||
mx={mx}
|
||||
room={room}
|
||||
eventId={replyEventId}
|
||||
data-event-id={replyEventId}
|
||||
replyEventId={replyEventId}
|
||||
threadRootId={threadRootId}
|
||||
onClick={handleOpenClick}
|
||||
/>
|
||||
)}
|
||||
|
@ -484,6 +489,7 @@ export function Notifications() {
|
|||
const mx = useMatrixClient();
|
||||
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
|
||||
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
@ -549,13 +555,27 @@ export function Notifications() {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Message} />
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Message} />}
|
||||
<Text size="H3" truncate>
|
||||
Notification Messages
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
|
||||
<Box style={{ position: 'relative' }} grow="Yes">
|
||||
|
|
24
src/app/pages/client/sidebar/UnverifiedTab.css.ts
Normal file
24
src/app/pages/client/sidebar/UnverifiedTab.css.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
import { color, toRem } from 'folds';
|
||||
|
||||
const pushRight = keyframes({
|
||||
from: {
|
||||
transform: `translateX(${toRem(2)}) scale(1)`,
|
||||
},
|
||||
to: {
|
||||
transform: 'translateX(0) scale(1)',
|
||||
},
|
||||
});
|
||||
|
||||
export const UnverifiedTab = style({
|
||||
animationName: pushRight,
|
||||
animationDuration: '400ms',
|
||||
animationIterationCount: 30,
|
||||
animationDirection: 'alternate',
|
||||
});
|
||||
|
||||
export const UnverifiedAvatar = style({
|
||||
backgroundColor: color.Critical.Container,
|
||||
color: color.Critical.OnContainer,
|
||||
borderColor: color.Critical.ContainerLine,
|
||||
});
|
49
src/app/pages/client/sidebar/UnverifiedTab.tsx
Normal file
49
src/app/pages/client/sidebar/UnverifiedTab.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { Badge, color, Icon, Icons, Text } from 'folds';
|
||||
import { openSettings } from '../../../../client/action/navigation';
|
||||
import { isCrossVerified } from '../../../../util/matrixUtil';
|
||||
import {
|
||||
SidebarAvatar,
|
||||
SidebarItem,
|
||||
SidebarItemBadge,
|
||||
SidebarItemTooltip,
|
||||
} from '../../../components/sidebar';
|
||||
import { useDeviceList } from '../../../hooks/useDeviceList';
|
||||
import { tabText } from '../../../organisms/settings/Settings';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import * as css from './UnverifiedTab.css';
|
||||
|
||||
export function UnverifiedTab() {
|
||||
const mx = useMatrixClient();
|
||||
const deviceList = useDeviceList();
|
||||
const unverified = deviceList?.filter(
|
||||
(device) => isCrossVerified(mx, device.device_id) === false
|
||||
);
|
||||
|
||||
if (!unverified?.length) return null;
|
||||
|
||||
return (
|
||||
<SidebarItem className={css.UnverifiedTab}>
|
||||
<SidebarItemTooltip tooltip="Unverified Sessions">
|
||||
{(triggerRef) => (
|
||||
<SidebarAvatar
|
||||
className={css.UnverifiedAvatar}
|
||||
as="button"
|
||||
ref={triggerRef}
|
||||
outlined
|
||||
onClick={() => openSettings(tabText.SECURITY)}
|
||||
>
|
||||
<Icon style={{ color: color.Critical.Main }} src={Icons.ShieldUser} />
|
||||
</SidebarAvatar>
|
||||
)}
|
||||
</SidebarItemTooltip>
|
||||
<SidebarItemBadge hasCount>
|
||||
<Badge variant="Critical" size="400" fill="Solid" radii="Pill" outlined={false}>
|
||||
<Text as="span" size="L400">
|
||||
{unverified.length}
|
||||
</Text>
|
||||
</Badge>
|
||||
</SidebarItemBadge>
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
|
@ -4,3 +4,4 @@ export * from './SpaceTabs';
|
|||
export * from './InboxTab';
|
||||
export * from './ExploreTab';
|
||||
export * from './UserTab';
|
||||
export * from './UnverifiedTab';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { Box, Icon, Icons, Text, Scroll } from 'folds';
|
||||
import { Box, Icon, Icons, Text, Scroll, IconButton } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
|
||||
import { MessageSearch } from '../../../features/message-search';
|
||||
|
@ -9,11 +9,14 @@ import { allRoomsAtom } from '../../../state/room-list/roomList';
|
|||
import { mDirectAtom } from '../../../state/mDirectList';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
|
||||
import { BackRouteHandler } from '../../../components/BackRouteHandler';
|
||||
|
||||
export function SpaceSearch() {
|
||||
const mx = useMatrixClient();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const space = useSpace();
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
|
@ -25,13 +28,27 @@ export function SpaceSearch() {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||
<Icon size="400" src={Icons.Search} />
|
||||
<PageHeader balance>
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<IconButton onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
</BackRouteHandler>
|
||||
)}
|
||||
</Box>
|
||||
<Box justifyContent="Center" alignItems="Center" gap="200">
|
||||
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
|
||||
<Text size="H3" truncate>
|
||||
Message Search
|
||||
</Text>
|
||||
</Box>
|
||||
<Box grow="Yes" basis="No" />
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box style={{ position: 'relative' }} grow="Yes">
|
||||
<Scroll ref={scrollRef} hideTrack visibility="Hover">
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
testMatrixTo,
|
||||
} from './matrix-to';
|
||||
import { onEnterOrSpace } from '../utils/keyboard';
|
||||
import { tryDecodeURIComponent } from '../utils/dom';
|
||||
|
||||
const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
|
||||
|
||||
|
@ -134,8 +135,8 @@ export const factoryRenderLinkifyWithMention = (
|
|||
attributes,
|
||||
content,
|
||||
}) => {
|
||||
if (tagName === 'a' && testMatrixTo(decodeURIComponent(attributes.href))) {
|
||||
const mention = mentionRender(decodeURIComponent(attributes.href));
|
||||
if (tagName === 'a' && testMatrixTo(tryDecodeURIComponent(attributes.href))) {
|
||||
const mention = mentionRender(tryDecodeURIComponent(attributes.href));
|
||||
if (mention) return mention;
|
||||
}
|
||||
|
||||
|
@ -334,11 +335,11 @@ export const getReactCustomHtmlParser = (
|
|||
}
|
||||
}
|
||||
|
||||
if (name === 'a' && testMatrixTo(decodeURIComponent(props.href))) {
|
||||
if (name === 'a' && testMatrixTo(tryDecodeURIComponent(props.href))) {
|
||||
const mention = renderMatrixMention(
|
||||
mx,
|
||||
roomId,
|
||||
decodeURIComponent(props.href),
|
||||
tryDecodeURIComponent(props.href),
|
||||
makeMentionCustomProps(params.handleMentionClick)
|
||||
);
|
||||
if (mention) return mention;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { atom } from 'jotai';
|
|||
import { atomFamily } from 'jotai/utils';
|
||||
import { Descendant } from 'slate';
|
||||
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
||||
import { IEventRelation } from 'matrix-js-sdk';
|
||||
import { TListAtom, createListAtom } from '../list';
|
||||
import { createUploadAtomFamily } from '../upload';
|
||||
import { TUploadContent } from '../../utils/matrix';
|
||||
|
@ -39,7 +40,8 @@ export type IReplyDraft = {
|
|||
userId: string;
|
||||
eventId: string;
|
||||
body: string;
|
||||
formattedBody?: string;
|
||||
formattedBody?: string | undefined;
|
||||
relation?: IEventRelation | undefined;
|
||||
};
|
||||
const createReplyDraftAtom = () => atom<IReplyDraft | undefined>(undefined);
|
||||
export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>;
|
||||
|
|
|
@ -196,3 +196,11 @@ export const setFavicon = (url: string): void => {
|
|||
if (!favicon) return;
|
||||
favicon.setAttribute('href', url);
|
||||
};
|
||||
|
||||
export const tryDecodeURIComponent = (encodedURIComponent: string): string => {
|
||||
try {
|
||||
return decodeURIComponent(encodedURIComponent);
|
||||
} catch {
|
||||
return encodedURIComponent;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -389,13 +389,18 @@ export const getEditedEvent = (
|
|||
return edits && getLatestEdit(mEvent, edits.getRelations());
|
||||
};
|
||||
|
||||
export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) =>
|
||||
export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) => {
|
||||
const content = mEvent.getContent();
|
||||
const relationType = content['m.relates_to']?.rel_type;
|
||||
return (
|
||||
mEvent.getSender() === mx.getUserId() &&
|
||||
!mEvent.isRelation() &&
|
||||
(!relationType || relationType === RelationType.Thread) &&
|
||||
mEvent.getType() === MessageEvent.RoomMessage &&
|
||||
(mEvent.getContent().msgtype === MsgType.Text ||
|
||||
mEvent.getContent().msgtype === MsgType.Emote ||
|
||||
mEvent.getContent().msgtype === MsgType.Notice);
|
||||
(content.msgtype === MsgType.Text ||
|
||||
content.msgtype === MsgType.Emote ||
|
||||
content.msgtype === MsgType.Notice)
|
||||
);
|
||||
};
|
||||
|
||||
export const getLatestEditableEvt = (
|
||||
timeline: EventTimeline,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const cons = {
|
||||
version: '4.0.3',
|
||||
version: '4.1.0',
|
||||
secretKey: {
|
||||
ACCESS_TOKEN: 'cinny_access_token',
|
||||
DEVICE_ID: 'cinny_device_id',
|
||||
|
|
|
@ -14,6 +14,9 @@ import settings from './client/state/settings';
|
|||
|
||||
import App from './app/pages/App';
|
||||
|
||||
// import i18n (needs to be bundled ;))
|
||||
import './app/i18n';
|
||||
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
settings.applyTheme();
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ export function isCrossVerified(mx, deviceId) {
|
|||
const deviceInfo = mx.getStoredDevice(mx.getUserId(), deviceId);
|
||||
const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true);
|
||||
return deviceTrust.isCrossSigningVerified();
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// device does not support encryption
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -35,10 +35,18 @@ const copyFiles = {
|
|||
src: 'public/res/android',
|
||||
dest: 'public/',
|
||||
},
|
||||
{
|
||||
src: 'public/locales',
|
||||
dest: 'public/',
|
||||
},
|
||||
{
|
||||
src: 'twemoji/assets/svg/*',
|
||||
dest: 'twemoji/',
|
||||
},
|
||||
{
|
||||
src: 'public/mx-uc.css',
|
||||
dest: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue