diff --git a/.github/workflows/deploy-pull-request.yml b/.github/workflows/deploy-pull-request.yml
index 9c0bea7897..b330c3c1c9 100644
--- a/.github/workflows/deploy-pull-request.yml
+++ b/.github/workflows/deploy-pull-request.yml
@@ -15,7 +15,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download pr number
- uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295
+ uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
@@ -24,7 +24,7 @@ jobs:
id: pr
run: echo "id=$(> $GITHUB_OUTPUT
- name: Download artifact
- uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295
+ uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5
with:
workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }}
diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml
index 0a758c5193..24edda96fd 100644
--- a/.github/workflows/prod-deploy.yml
+++ b/.github/workflows/prod-deploy.yml
@@ -52,7 +52,7 @@ jobs:
gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release
- uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda
+ uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8
with:
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
@@ -72,19 +72,19 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
- name: Login to Docker Hub
- uses: docker/login-action@v3.4.0
+ uses: docker/login-action@v3.5.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Container registry
- uses: docker/login-action@v3.4.0
+ uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@v5.7.0
+ uses: docker/metadata-action@v5.8.0
with:
images: |
${{ secrets.DOCKER_USERNAME }}/cinny
diff --git a/.gitignore b/.gitignore
index 1af58a970d..d088373743 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,6 @@ node_modules
devAssets
.DS_Store
-.idea
\ No newline at end of file
+.idea
+
+.env
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index abb65ee515..718fed729b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,7 @@ RUN npm run build
## App
-FROM nginx:1.27.4-alpine
+FROM nginx:1.29.0-alpine
COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
diff --git a/config.json b/config.json
index de6015a104..dc03d4879f 100644
--- a/config.json
+++ b/config.json
@@ -10,6 +10,12 @@
],
"allowCustomHomeservers": true,
+ "pushNotificationDetails": {
+ "pushNotifyUrl": "https://cinny.cc/_matrix/push/v1/notify",
+ "vapidPublicKey": "BHLwykXs79AbKNiblEtZZRAgnt7o5_ieImhVJD8QZ01MVwAHnXwZzNgQEJJEU3E5CVsihoKtb7yaNe5x3vmkWkI",
+ "webPushAppID": "cc.cinny.web"
+ },
+
"featuredCommunities": {
"openAsDefault": false,
"spaces": [
diff --git a/docs/Caddyfile b/docs/Caddyfile
new file mode 100644
index 0000000000..3d03c2f49c
--- /dev/null
+++ b/docs/Caddyfile
@@ -0,0 +1,10 @@
+(tls_cloudflare) {
+ tls {
+ dns cloudflare {env.CLOUDFLARE_API_TOKEN}
+ }
+}
+
+ {
+ import tls_cloudflare
+ reverse_proxy sygnal:5000
+}
diff --git a/docs/Dockerfile b/docs/Dockerfile
new file mode 100644
index 0000000000..6900bc6779
--- /dev/null
+++ b/docs/Dockerfile
@@ -0,0 +1,9 @@
+FROM caddy:builder AS builder
+
+RUN xcaddy build \
+ --with github.com/caddy-dns/cloudflare
+FROM caddy:latest
+
+COPY --from=builder /usr/bin/caddy /usr/bin/caddy
+COPY Caddyfile /etc/caddy/Caddyfile
+COPY .env /etc/caddy/.env
\ No newline at end of file
diff --git a/docs/sample.env b/docs/sample.env
new file mode 100644
index 0000000000..dc3b650cd6
--- /dev/null
+++ b/docs/sample.env
@@ -0,0 +1 @@
+CLOUDFLARE_API_TOKEN=
\ No newline at end of file
diff --git a/docs/sygnal-setup.md b/docs/sygnal-setup.md
new file mode 100644
index 0000000000..0619433fd7
--- /dev/null
+++ b/docs/sygnal-setup.md
@@ -0,0 +1,308 @@
+## Sygnal with Caddy & Cloudflare on Vultr
+
+This document walks you through setting up a [Sygnal](https://github.com/matrix-org/sygnal) push gateway for Matrix, running in a Docker container. We will use [Caddy](https://caddyserver.com/) as a reverse proxy, also in Docker, to handle HTTPS automatically using DNS challenges with [Cloudflare](https://www.cloudflare.com/).
+
+Now Cloudflare and Vultr have a deal in place where traffic from Cloudflare to Vultr and vice versa does not incur bandwidth usage. So you can pass endless amounts through without any extra billing. This is why the docs utilize Vultr, but you're free to use whatever cloud provider you want and not use Cloudflare if you so choose.
+
+### Prerequisites
+
+1. **Vultr Server**: A running server instance. This guide assumes a fresh server running a common Linux distribution like Debian, Ubuntu, or Alpine.
+2. **Domain Name**: A domain name managed through Cloudflare.
+3. **Cloudflare Account**: Your domain must be using Cloudflare's DNS.
+4. **Docker & Docker Compose**: Docker and `docker-compose` must be installed on your Vultr server.
+5. **A Matrix Client**: A client like [Cinny](https://github.com/cinnyapp/cinny) that you want to point to your new push gateway.
+
+---
+
+### Step 1: Cloudflare Configuration
+
+Before touching the server, we need to configure Cloudflare.
+
+#### 1.1. DNS Record
+
+In your Cloudflare dashboard, create an **A** (for IPv4) or **AAAA** (for IPv6) record for the subdomain you'll use for Sygnal. Point it to your Vultr server's IP address.
+
+- **Type**: `A` or `AAAA`
+- **Name**: `sygnal.your-domain.com` (or your chosen subdomain)
+- **Content**: Your Vultr server's IP address
+- **Proxy status**: **Proxied** (Orange Cloud). This is important for Caddy's setup.
+
+#### 1.2. API Token
+
+Caddy needs an API token to prove to Cloudflare that you own the domain so it can create the necessary DNS records for issuing an SSL certificate.
+
+1. Go to **My Profile** \> **API Tokens** in Cloudflare.
+2. Click **Create Token**.
+3. Use the **Edit zone DNS** template.
+4. Under **Permissions**, ensure `Zone:DNS:Edit` is selected.
+5. Under **Zone Resources**, select the specific zone for `your-domain.com`.
+6. Continue to summary and create the token.
+7. **Copy the generated token immediately.** You will not be able to see it again. We will use this as your `CLOUDFLARE_API_TOKEN`.
+
+---
+
+### Step 2: Server Preparation
+
+#### 2.1. Connect to your Server (SSH)
+
+If your Vultr instance uses an IPv6 address, connecting via SSH can sometimes be tricky. You can create an alias in your local `~/.ssh/config` file to make it easier.
+
+Open or create `~/.ssh/config` on your local machine and add:
+
+```
+Host vultr-sygnal
+ # Replace with your server's IPv6 or IPv4 address
+ Hostname 2001:19f0:5400:1532:5400:05ff:fe78:fb25
+ User root
+ # For IPv6, uncomment the line below
+ # AddressFamily inet6
+```
+
+Now you can connect simply by typing `ssh vultr-sygnal`.
+
+#### 2.2. Install Docker and Docker Compose
+
+Follow the official Docker documentation to install the Docker Engine and Docker Compose for your server's operating system.
+
+#### 2.3. Configure Firewall
+
+We need to allow HTTP and HTTPS traffic so Caddy can obtain certificates and serve requests. If you are using `ufw`:
+
+```sh
+sudo ufw allow 80/tcp
+sudo ufw allow 443/tcp
+sudo ufw enable
+sudo ufw status
+```
+
+---
+
+### Step 3: Project Structure and Configuration
+
+On your Vultr server, let's create a directory to hold all our configuration files.
+
+```sh
+mkdir -p /opt/matrix-sygnal
+cd /opt/matrix-sygnal
+```
+
+We will create all subsequent files inside this `/opt/matrix-sygnal` directory.
+
+#### 3.1. Sygnal VAPID Keys
+
+WebPush requires a VAPID key pair. The private key stays on your server, and the public key is given to clients.
+
+1. **Generate the Private Key**:
+ Use `openssl` to generate an EC private key.
+
+ ```sh
+ # This command needs to be run in the /opt/matrix-sygnal directory
+ openssl ecparam -name prime256v1 -genkey -noout -out sygnal_private_key.pem
+ ```
+
+2. **Extract the Public Key**:
+ Extract the corresponding public key from the private key. You will need this for your client configuration later.
+
+ ```sh
+ # This command extracts the public key in the correct format
+ openssl ec -in sygnal_private_key.pem -pubout -outform DER | tail -c 65 | base64 | tr '/+' '_-' | tr -d '='
+ ```
+
+ **Save the output of this command.** This is your `vapidPublicKey`. It should look similar to the one from the `cinny.cc` example.
+
+#### 3.2. Sygnal Configuration (`sygnal.yaml`)
+
+Create a file named `sygnal.yaml`. This file tells Sygnal how to run.
+
+```yaml
+# /opt/matrix-sygnal/sygnal.yaml
+http:
+ bind_addresses: ['0.0.0.0']
+ port: 5000
+
+# This is where we configure our push gateway app
+apps:
+ # This app_id must match the one used in your client's configuration
+ cc.cinny.web:
+ type: webpush
+ # This path is *inside the container*. We will map our generated key to it.
+ vapid_private_key: /data/private_key.pem
+ # An email for VAPID contact details
+ vapid_contact_email: your-email@your-domain.com
+```
+
+#### 3.3. Caddy Configuration (`Caddyfile`)
+
+Create a file named `Caddyfile`. This tells Caddy how to proxy requests.
+
+**Replace `sygnal.your-domain.com`** with the domain you configured in Step 1.
+
+```caddyfile
+# /opt/matrix-sygnal/Caddyfile
+
+# Reusable snippet for Cloudflare TLS
+(tls_cloudflare) {
+ tls {
+ dns cloudflare {env.CLOUDFLARE_API_TOKEN}
+ }
+}
+
+# Your public-facing URL
+sygnal.your-domain.com {
+ # Get an SSL certificate from Let's Encrypt using the Cloudflare DNS challenge
+ import tls_cloudflare
+
+ # Log requests to standard output
+ log
+
+ # Reverse proxy requests to the sygnal container on port 5000
+ # 'sygnal' is the service name we will define in docker-compose.yml
+ reverse_proxy sygnal:5000
+}
+```
+
+#### 3.4. Caddy Dockerfile
+
+While you can use the standard `caddy:latest` image, you need one with the Cloudflare DNS provider plugin. Create a file named `Dockerfile` for Caddy.
+
+```dockerfile
+# /opt/matrix-sygnal/Dockerfile
+FROM caddy:builder AS builder
+
+RUN xcaddy build \
+ --with github.com/caddy-dns/cloudflare
+
+FROM caddy:latest
+
+COPY --from=builder /usr/bin/caddy /usr/bin/caddy
+```
+
+#### 3.5. Environment File (`.env`)
+
+Create a file named `.env` to securely store your Cloudflare API Token.
+
+```.env
+# /opt/matrix-sygnal/.env
+CLOUDFLARE_API_TOKEN=your-cloudflare-api-token-from-step-1
+```
+
+---
+
+### Step 4: Docker Compose
+
+Using `docker-compose` simplifies managing our multi-container application. Create a `docker-compose.yml` file.
+
+```yaml
+# /opt/matrix-sygnal/docker-compose.yml
+version: '3.7'
+
+services:
+ caddy:
+ # Build the Caddy image from our Dockerfile in the current directory
+ build: .
+ container_name: caddy
+ hostname: caddy
+ restart: unless-stopped
+ networks:
+ - matrix
+ ports:
+ # Expose standard web ports to the host
+ - '80:80'
+ - '443:443'
+ volumes:
+ # Mount the Caddyfile into the container
+ - ./Caddyfile:/etc/caddy/Caddyfile
+ # Create a volume for Caddy's data (certs, etc.)
+ - caddy_data:/data
+ # Load the Cloudflare token from the .env file
+ env_file:
+ - ./.env
+
+ sygnal:
+ # Use the official Sygnal image
+ image: matrixdotorg/sygnal:latest
+ container_name: sygnal
+ hostname: sygnal
+ restart: unless-stopped
+ networks:
+ - matrix
+ volumes:
+ # Mount the Sygnal config file
+ - ./sygnal.yaml:/sygnal.yaml
+ # Mount the generated private key to the path specified in sygnal.yaml
+ - ./sygnal_private_key.pem:/data/private_key.pem
+ # Create a volume for any other data Sygnal might store
+ - sygnal_data:/data
+ command: ['--config-path=/sygnal.yaml']
+
+volumes:
+ caddy_data:
+ sygnal_data:
+
+networks:
+ matrix:
+ driver: bridge
+```
+
+---
+
+### Step 5: Launch the Services
+
+Your directory `/opt/matrix-sygnal` should now look like this:
+
+```
+/opt/matrix-sygnal/
+├── Caddyfile
+├── docker-compose.yml
+├── Dockerfile
+├── .env
+├── sygnal.yaml
+└── sygnal_private_key.pem
+```
+
+Now, you can build and run everything with a single command:
+
+```sh
+cd /opt/matrix-sygnal
+sudo docker-compose up --build -d
+```
+
+- `--build` tells Docker Compose to build the Caddy image from your `Dockerfile`.
+- `-d` runs the containers in detached mode (in the background).
+
+To check the status and logs:
+
+```sh
+# See if containers are running
+sudo docker-compose ps
+
+# View the live logs for both services
+sudo docker-compose logs -f
+
+# View logs for a specific service (e.g., caddy)
+sudo docker-compose logs -f caddy
+```
+
+Caddy will automatically start, obtain an SSL certificate for `sygnal.your-domain.com`, and begin proxying requests to the Sygnal container.
+
+---
+
+### Step 6: Client Configuration
+
+The final step is to configure your Matrix client to use your new push gateway. In Cinny, for example, you would modify its `config.json` or use a homeserver that advertises these settings.
+
+Update the `pushNotificationDetails` section with the information from your server:
+
+```json
+"pushNotificationDetails": {
+ "pushNotifyUrl": "https://sygnal.your-domain.com/_matrix/push/v1/notify",
+ "vapidPublicKey": "YOUR_VAPID_PUBLIC_KEY_FROM_STEP_3.1",
+ "webPushAppID": "cc.cinny.web"
+}
+```
+
+- **`pushNotifyUrl`**: The public URL of your new Sygnal instance.
+- **`vapidPublicKey`**: The public key you generated in step 3.1.
+- **`webPushAppID`**: The application ID you defined in your `sygnal.yaml`. This must match exactly.
+
+After configuring your client, it will register for push notifications with your Sygnal instance, which will then handle delivering them.
diff --git a/docs/sygnal.yaml b/docs/sygnal.yaml
new file mode 100644
index 0000000000..3f5613fc1d
--- /dev/null
+++ b/docs/sygnal.yaml
@@ -0,0 +1,9 @@
+http:
+ bind_addresses: ['0.0.0.0']
+ port: 5000
+
+apps:
+ cc.cinny.web:
+ type: webpush
+ vapid_private_key: /data/private_key.pem
+ vapid_contact_email: help@cinny.cc
diff --git a/package-lock.json b/package-lock.json
index 7dd2bf0a29..8d8ead2304 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cinny",
- "version": "4.8.1",
+ "version": "4.9.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cinny",
- "version": "4.8.1",
+ "version": "4.9.1",
"license": "AGPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
@@ -16,7 +16,6 @@
"@tanstack/react-query": "5.24.1",
"@tanstack/react-query-devtools": "5.24.1",
"@tanstack/react-virtual": "3.2.0",
- "@tippyjs/react": "4.2.6",
"@vanilla-extract/css": "1.9.3",
"@vanilla-extract/recipes": "0.3.0",
"@vanilla-extract/vite-plugin": "3.7.1",
@@ -32,10 +31,8 @@
"emojibase": "15.3.1",
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
- "flux": "4.0.3",
"focus-trap-react": "10.0.2",
"folds": "2.2.0",
- "formik": "2.4.6",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
@@ -50,17 +47,14 @@
"millify": "6.1.0",
"pdfjs-dist": "4.2.67",
"prismjs": "1.30.0",
- "prop-types": "15.8.1",
"react": "18.2.0",
"react-aria": "3.29.1",
- "react-autosize-textarea": "7.1.0",
"react-blurhash": "0.2.0",
"react-colorful": "5.6.1",
"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",
"sanitize-html": "2.12.1",
@@ -68,7 +62,6 @@
"slate-dom": "0.112.2",
"slate-history": "0.110.3",
"slate-react": "0.112.1",
- "tippy.js": "6.3.7",
"ua-parser-js": "1.0.35"
},
"devDependencies": {
@@ -97,12 +90,12 @@
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
"prettier": "2.8.1",
- "sass": "1.56.2",
"typescript": "4.9.4",
- "vite": "5.4.15",
+ "vite": "5.4.19",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4",
- "vite-plugin-top-level-await": "1.4.4"
+ "vite-plugin-top-level-await": "1.4.4",
+ "workbox-precaching": "7.3.0"
},
"engines": {
"node": ">=16.0.0"
@@ -2313,15 +2306,6 @@
"node": ">= 8"
}
},
- "node_modules/@popperjs/core": {
- "version": "2.11.8",
- "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
- "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/popperjs"
- }
- },
"node_modules/@react-aria/breadcrumbs": {
"version": "3.5.20",
"resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.20.tgz",
@@ -4524,18 +4508,6 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
- "node_modules/@tippyjs/react": {
- "version": "4.2.6",
- "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
- "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==",
- "dependencies": {
- "tippy.js": "^6.3.1"
- },
- "peerDependencies": {
- "react": ">=16.8",
- "react-dom": ">=16.8"
- }
- },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4601,15 +4573,6 @@
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
"dev": true
},
- "node_modules/@types/hoist-non-react-statics": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
- "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==",
- "dependencies": {
- "@types/react": "*",
- "hoist-non-react-statics": "^3.3.0"
- }
- },
"node_modules/@types/is-hotkey": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz",
@@ -4643,12 +4606,14 @@
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
- "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
+ "dev": true
},
"node_modules/@types/react": {
"version": "18.2.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
+ "dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4697,7 +4662,8 @@
"node_modules/@types/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz",
- "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw=="
+ "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==",
+ "dev": true
},
"node_modules/@types/semver": {
"version": "7.5.8",
@@ -5320,11 +5286,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
- },
"node_modules/ast-types-flow": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
@@ -5346,11 +5307,6 @@
"node": ">= 4.0.0"
}
},
- "node_modules/autosize": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.4.tgz",
- "integrity": "sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ=="
- },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -5830,11 +5786,6 @@
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
"license": "MIT"
},
- "node_modules/computed-style": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz",
- "integrity": "sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w=="
- },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -5896,14 +5847,6 @@
"url": "https://opencollective.com/core-js"
}
},
- "node_modules/cross-fetch": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
- "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
- "dependencies": {
- "node-fetch": "^2.7.0"
- }
- },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -7019,11 +6962,6 @@
"node": ">=0.8.x"
}
},
- "node_modules/exenv": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
- "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -7095,33 +7033,6 @@
"reusify": "^1.0.4"
}
},
- "node_modules/fbemitter": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz",
- "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==",
- "dependencies": {
- "fbjs": "^3.0.0"
- }
- },
- "node_modules/fbjs": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
- "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
- "dependencies": {
- "cross-fetch": "^3.1.5",
- "fbjs-css-vars": "^1.0.0",
- "loose-envify": "^1.0.0",
- "object-assign": "^4.1.0",
- "promise": "^7.1.1",
- "setimmediate": "^1.0.5",
- "ua-parser-js": "^1.0.35"
- }
- },
- "node_modules/fbjs-css-vars": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
- "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
- },
"node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
@@ -7230,18 +7141,6 @@
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"dev": true
},
- "node_modules/flux": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.3.tgz",
- "integrity": "sha512-yKAbrp7JhZhj6uiT1FTuVMlIAT1J4jqEyBpFApi1kxpGZCvacMVc/t1pMQyotqHhAgvoE3bNvAykhCo2CLjnYw==",
- "dependencies": {
- "fbemitter": "^3.0.0",
- "fbjs": "^3.0.1"
- },
- "peerDependencies": {
- "react": "^15.0.2 || ^16.0.0 || ^17.0.0"
- }
- },
"node_modules/focus-trap": {
"version": "7.6.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz",
@@ -7286,38 +7185,6 @@
"is-callable": "^1.1.3"
}
},
- "node_modules/formik": {
- "version": "2.4.6",
- "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
- "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
- "funding": [
- {
- "type": "individual",
- "url": "https://opencollective.com/formik"
- }
- ],
- "dependencies": {
- "@types/hoist-non-react-statics": "^3.3.1",
- "deepmerge": "^2.1.1",
- "hoist-non-react-statics": "^3.3.0",
- "lodash": "^4.17.21",
- "lodash-es": "^4.17.21",
- "react-fast-compare": "^2.0.1",
- "tiny-warning": "^1.0.2",
- "tslib": "^2.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8.0"
- }
- },
- "node_modules/formik/node_modules/deepmerge": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
- "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/fs-extra": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
@@ -7896,12 +7763,6 @@
"url": "https://opencollective.com/immer"
}
},
- "node_modules/immutable": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
- "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
- "dev": true
- },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -8636,17 +8497,6 @@
"node": ">=10"
}
},
- "node_modules/line-height": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz",
- "integrity": "sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w==",
- "dependencies": {
- "computed-style": "~0.1.3"
- },
- "engines": {
- "node": ">= 4.0.0"
- }
- },
"node_modules/linkify-react": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz",
@@ -8680,11 +8530,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
- "node_modules/lodash-es": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
- "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
- },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -9541,14 +9386,6 @@
"node": ">=6"
}
},
- "node_modules/promise": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
- "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
- "dependencies": {
- "asap": "~2.0.3"
- }
- },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -9672,20 +9509,6 @@
"react": ">=16.4.1"
}
},
- "node_modules/react-autosize-textarea": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz",
- "integrity": "sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g==",
- "dependencies": {
- "autosize": "^4.0.2",
- "line-height": "^0.3.1",
- "prop-types": "^15.5.6"
- },
- "peerDependencies": {
- "react": "^0.14.0 || ^15.0.0 || ^16.0.0",
- "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0"
- }
- },
"node_modules/react-blurhash": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.2.0.tgz",
@@ -9728,11 +9551,6 @@
"react": ">=16.13.1"
}
},
- "node_modules/react-fast-compare": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
- "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
- },
"node_modules/react-google-recaptcha": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz",
@@ -9771,29 +9589,6 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
- "node_modules/react-lifecycles-compat": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
- "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
- },
- "node_modules/react-modal": {
- "version": "3.16.1",
- "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
- "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
- "dependencies": {
- "exenv": "^1.2.0",
- "prop-types": "^15.7.2",
- "react-lifecycles-compat": "^3.0.0",
- "warning": "^4.0.3"
- },
- "engines": {
- "node": ">=8"
- },
- "peerDependencies": {
- "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18",
- "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18"
- }
- },
"node_modules/react-property": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz",
@@ -10252,23 +10047,6 @@
"postcss": "^8.3.11"
}
},
- "node_modules/sass": {
- "version": "1.56.2",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz",
- "integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==",
- "dev": true,
- "dependencies": {
- "chokidar": ">=3.0.0 <4.0.0",
- "immutable": "^4.0.0",
- "source-map-js": ">=0.6.2 <2.0.0"
- },
- "bin": {
- "sass": "sass.js"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -10368,11 +10146,6 @@
"node": ">= 0.4"
}
},
- "node_modules/setimmediate": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
- },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -10969,14 +10742,6 @@
"node": ">=12.0.0"
}
},
- "node_modules/tippy.js": {
- "version": "6.3.7",
- "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
- "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
- "dependencies": {
- "@popperjs/core": "^2.9.0"
- }
- },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -11331,9 +11096,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.15",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
- "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
+ "version": "5.4.19",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
+ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
@@ -11891,14 +11656,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/warning": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
- "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
- "dependencies": {
- "loose-envify": "^1.0.0"
- }
- },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -12212,6 +11969,7 @@
"resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.3.0.tgz",
"integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"workbox-core": "7.3.0",
"workbox-routing": "7.3.0",
diff --git a/package.json b/package.json
index 3c1cef8cbc..485e472910 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cinny",
- "version": "4.8.1",
+ "version": "4.9.1",
"description": "Yet another matrix client",
"main": "index.js",
"type": "module",
@@ -27,7 +27,6 @@
"@tanstack/react-query": "5.24.1",
"@tanstack/react-query-devtools": "5.24.1",
"@tanstack/react-virtual": "3.2.0",
- "@tippyjs/react": "4.2.6",
"@vanilla-extract/css": "1.9.3",
"@vanilla-extract/recipes": "0.3.0",
"@vanilla-extract/vite-plugin": "3.7.1",
@@ -43,10 +42,8 @@
"emojibase": "15.3.1",
"emojibase-data": "15.3.2",
"file-saver": "2.0.5",
- "flux": "4.0.3",
"focus-trap-react": "10.0.2",
"folds": "2.2.0",
- "formik": "2.4.6",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
"i18next": "23.12.2",
@@ -61,17 +58,14 @@
"millify": "6.1.0",
"pdfjs-dist": "4.2.67",
"prismjs": "1.30.0",
- "prop-types": "15.8.1",
"react": "18.2.0",
"react-aria": "3.29.1",
- "react-autosize-textarea": "7.1.0",
"react-blurhash": "0.2.0",
"react-colorful": "5.6.1",
"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",
"sanitize-html": "2.12.1",
@@ -79,7 +73,6 @@
"slate-dom": "0.112.2",
"slate-history": "0.110.3",
"slate-react": "0.112.1",
- "tippy.js": "6.3.7",
"ua-parser-js": "1.0.35"
},
"devDependencies": {
@@ -108,11 +101,11 @@
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
"prettier": "2.8.1",
- "sass": "1.56.2",
"typescript": "4.9.4",
- "vite": "5.4.15",
+ "vite": "5.4.19",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4",
- "vite-plugin-top-level-await": "1.4.4"
+ "vite-plugin-top-level-await": "1.4.4",
+ "workbox-precaching": "7.3.0"
}
}
diff --git a/public/res/ic/filled/category.svg b/public/res/ic/filled/category.svg
deleted file mode 100644
index 87b2588dd4..0000000000
--- a/public/res/ic/filled/category.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/filled/pin.svg b/public/res/ic/filled/pin.svg
deleted file mode 100644
index 6a70147405..0000000000
--- a/public/res/ic/filled/pin.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/filled/star.svg b/public/res/ic/filled/star.svg
deleted file mode 100644
index 378c891e46..0000000000
--- a/public/res/ic/filled/star.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/add-pin.svg b/public/res/ic/outlined/add-pin.svg
deleted file mode 100644
index 9634bede59..0000000000
--- a/public/res/ic/outlined/add-pin.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/add-user.svg b/public/res/ic/outlined/add-user.svg
deleted file mode 100644
index c3803d80aa..0000000000
--- a/public/res/ic/outlined/add-user.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/ball.svg b/public/res/ic/outlined/ball.svg
deleted file mode 100644
index d4b89ff53f..0000000000
--- a/public/res/ic/outlined/ball.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/bell-off.svg b/public/res/ic/outlined/bell-off.svg
deleted file mode 100644
index 79ce8a33f0..0000000000
--- a/public/res/ic/outlined/bell-off.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/bell-ping.svg b/public/res/ic/outlined/bell-ping.svg
deleted file mode 100644
index 3431bea1d1..0000000000
--- a/public/res/ic/outlined/bell-ping.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/bell-ring.svg b/public/res/ic/outlined/bell-ring.svg
deleted file mode 100644
index 57fc267967..0000000000
--- a/public/res/ic/outlined/bell-ring.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/bell.svg b/public/res/ic/outlined/bell.svg
deleted file mode 100644
index 43d470b532..0000000000
--- a/public/res/ic/outlined/bell.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/bin.svg b/public/res/ic/outlined/bin.svg
deleted file mode 100644
index 984be62567..0000000000
--- a/public/res/ic/outlined/bin.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/bulb.svg b/public/res/ic/outlined/bulb.svg
deleted file mode 100644
index 00e8088615..0000000000
--- a/public/res/ic/outlined/bulb.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/category.svg b/public/res/ic/outlined/category.svg
deleted file mode 100644
index c7c33b3824..0000000000
--- a/public/res/ic/outlined/category.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/check.svg b/public/res/ic/outlined/check.svg
deleted file mode 100644
index 72a1832728..0000000000
--- a/public/res/ic/outlined/check.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/chevron-bottom.svg b/public/res/ic/outlined/chevron-bottom.svg
deleted file mode 100644
index 5562b7aaf0..0000000000
--- a/public/res/ic/outlined/chevron-bottom.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/chevron-left.svg b/public/res/ic/outlined/chevron-left.svg
deleted file mode 100644
index ba9e12cca7..0000000000
--- a/public/res/ic/outlined/chevron-left.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/chevron-right.svg b/public/res/ic/outlined/chevron-right.svg
deleted file mode 100644
index 7f6a806e66..0000000000
--- a/public/res/ic/outlined/chevron-right.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/chevron-top.svg b/public/res/ic/outlined/chevron-top.svg
deleted file mode 100644
index f5948fe902..0000000000
--- a/public/res/ic/outlined/chevron-top.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/circle-plus.svg b/public/res/ic/outlined/circle-plus.svg
deleted file mode 100644
index 41690a08ab..0000000000
--- a/public/res/ic/outlined/circle-plus.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/cmd.svg b/public/res/ic/outlined/cmd.svg
deleted file mode 100644
index 75ae0d9824..0000000000
--- a/public/res/ic/outlined/cmd.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/coin.svg b/public/res/ic/outlined/coin.svg
deleted file mode 100644
index 025424e853..0000000000
--- a/public/res/ic/outlined/coin.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/cross.svg b/public/res/ic/outlined/cross.svg
deleted file mode 100644
index 0acda8842a..0000000000
--- a/public/res/ic/outlined/cross.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/cup.svg b/public/res/ic/outlined/cup.svg
deleted file mode 100644
index 8921e2c9ae..0000000000
--- a/public/res/ic/outlined/cup.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/dog.svg b/public/res/ic/outlined/dog.svg
deleted file mode 100644
index 3b252956f1..0000000000
--- a/public/res/ic/outlined/dog.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/download.svg b/public/res/ic/outlined/download.svg
deleted file mode 100644
index 677014f366..0000000000
--- a/public/res/ic/outlined/download.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/emoji-add.svg b/public/res/ic/outlined/emoji-add.svg
deleted file mode 100644
index c4cacef25b..0000000000
--- a/public/res/ic/outlined/emoji-add.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/emoji.svg b/public/res/ic/outlined/emoji.svg
deleted file mode 100644
index 0daac8796e..0000000000
--- a/public/res/ic/outlined/emoji.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/explore.svg b/public/res/ic/outlined/explore.svg
deleted file mode 100644
index 7cc2a4793f..0000000000
--- a/public/res/ic/outlined/explore.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/external.svg b/public/res/ic/outlined/external.svg
deleted file mode 100644
index adade1bd9a..0000000000
--- a/public/res/ic/outlined/external.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/public/res/ic/outlined/eye-blind.svg b/public/res/ic/outlined/eye-blind.svg
deleted file mode 100644
index fbc8e2ae8a..0000000000
--- a/public/res/ic/outlined/eye-blind.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/public/res/ic/outlined/eye.svg b/public/res/ic/outlined/eye.svg
deleted file mode 100644
index 1ce868bfdb..0000000000
--- a/public/res/ic/outlined/eye.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/public/res/ic/outlined/file.svg b/public/res/ic/outlined/file.svg
deleted file mode 100644
index d6a2a27a57..0000000000
--- a/public/res/ic/outlined/file.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/flag.svg b/public/res/ic/outlined/flag.svg
deleted file mode 100644
index 8fce98d689..0000000000
--- a/public/res/ic/outlined/flag.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/hash-globe.svg b/public/res/ic/outlined/hash-globe.svg
deleted file mode 100644
index ce3df08317..0000000000
--- a/public/res/ic/outlined/hash-globe.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/hash-lock.svg b/public/res/ic/outlined/hash-lock.svg
deleted file mode 100644
index ae263ced5b..0000000000
--- a/public/res/ic/outlined/hash-lock.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/hash-plus.svg b/public/res/ic/outlined/hash-plus.svg
deleted file mode 100644
index 69737fd552..0000000000
--- a/public/res/ic/outlined/hash-plus.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/hash-search.svg b/public/res/ic/outlined/hash-search.svg
deleted file mode 100644
index f135e8986b..0000000000
--- a/public/res/ic/outlined/hash-search.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/hash-shield.svg b/public/res/ic/outlined/hash-shield.svg
deleted file mode 100644
index dfd344b142..0000000000
--- a/public/res/ic/outlined/hash-shield.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/hash.svg b/public/res/ic/outlined/hash.svg
deleted file mode 100644
index dcb8b9647c..0000000000
--- a/public/res/ic/outlined/hash.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/heart.svg b/public/res/ic/outlined/heart.svg
deleted file mode 100644
index c5b940b605..0000000000
--- a/public/res/ic/outlined/heart.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/home.svg b/public/res/ic/outlined/home.svg
deleted file mode 100644
index 3c7a02df40..0000000000
--- a/public/res/ic/outlined/home.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/horizontal-menu.svg b/public/res/ic/outlined/horizontal-menu.svg
deleted file mode 100644
index a19b3c3553..0000000000
--- a/public/res/ic/outlined/horizontal-menu.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/inbox.svg b/public/res/ic/outlined/inbox.svg
deleted file mode 100644
index 6543587621..0000000000
--- a/public/res/ic/outlined/inbox.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/info.svg b/public/res/ic/outlined/info.svg
deleted file mode 100644
index 30a578873b..0000000000
--- a/public/res/ic/outlined/info.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/invite-arrow.svg b/public/res/ic/outlined/invite-arrow.svg
deleted file mode 100644
index 370bf8e8cc..0000000000
--- a/public/res/ic/outlined/invite-arrow.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/invite-cancel-arrow.svg b/public/res/ic/outlined/invite-cancel-arrow.svg
deleted file mode 100644
index 795a773a60..0000000000
--- a/public/res/ic/outlined/invite-cancel-arrow.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/invite.svg b/public/res/ic/outlined/invite.svg
deleted file mode 100644
index 3896e15e81..0000000000
--- a/public/res/ic/outlined/invite.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/join-arrow.svg b/public/res/ic/outlined/join-arrow.svg
deleted file mode 100644
index 90cfa6517e..0000000000
--- a/public/res/ic/outlined/join-arrow.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/leave-arrow.svg b/public/res/ic/outlined/leave-arrow.svg
deleted file mode 100644
index a51ac1d125..0000000000
--- a/public/res/ic/outlined/leave-arrow.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/lock.svg b/public/res/ic/outlined/lock.svg
deleted file mode 100644
index 77021f0f4f..0000000000
--- a/public/res/ic/outlined/lock.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/markdown.svg b/public/res/ic/outlined/markdown.svg
deleted file mode 100644
index 775afbfb0c..0000000000
--- a/public/res/ic/outlined/markdown.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/message-unread.svg b/public/res/ic/outlined/message-unread.svg
deleted file mode 100644
index fc5e9ff00a..0000000000
--- a/public/res/ic/outlined/message-unread.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/message.svg b/public/res/ic/outlined/message.svg
deleted file mode 100644
index d36e9a307a..0000000000
--- a/public/res/ic/outlined/message.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/pause.svg b/public/res/ic/outlined/pause.svg
deleted file mode 100644
index c312613b4e..0000000000
--- a/public/res/ic/outlined/pause.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/peace.svg b/public/res/ic/outlined/peace.svg
deleted file mode 100644
index 8a7c81a3cc..0000000000
--- a/public/res/ic/outlined/peace.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/pencil.svg b/public/res/ic/outlined/pencil.svg
deleted file mode 100644
index 1b8ac24a7b..0000000000
--- a/public/res/ic/outlined/pencil.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/photo.svg b/public/res/ic/outlined/photo.svg
deleted file mode 100644
index af01a3305f..0000000000
--- a/public/res/ic/outlined/photo.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/pin.svg b/public/res/ic/outlined/pin.svg
deleted file mode 100644
index 211242cd8d..0000000000
--- a/public/res/ic/outlined/pin.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/play.svg b/public/res/ic/outlined/play.svg
deleted file mode 100644
index 87b3a8f617..0000000000
--- a/public/res/ic/outlined/play.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/plus.svg b/public/res/ic/outlined/plus.svg
deleted file mode 100644
index ce37594e87..0000000000
--- a/public/res/ic/outlined/plus.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/power.svg b/public/res/ic/outlined/power.svg
deleted file mode 100644
index 8aeb6db831..0000000000
--- a/public/res/ic/outlined/power.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/recent-clock.svg b/public/res/ic/outlined/recent-clock.svg
deleted file mode 100644
index 30b10d594a..0000000000
--- a/public/res/ic/outlined/recent-clock.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/reply-arrow.svg b/public/res/ic/outlined/reply-arrow.svg
deleted file mode 100644
index 3cda01cde9..0000000000
--- a/public/res/ic/outlined/reply-arrow.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/search.svg b/public/res/ic/outlined/search.svg
deleted file mode 100644
index 75dd63207f..0000000000
--- a/public/res/ic/outlined/search.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/send.svg b/public/res/ic/outlined/send.svg
deleted file mode 100644
index aa48713250..0000000000
--- a/public/res/ic/outlined/send.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/settings.svg b/public/res/ic/outlined/settings.svg
deleted file mode 100644
index ee640b3936..0000000000
--- a/public/res/ic/outlined/settings.svg
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/shield-empty.svg b/public/res/ic/outlined/shield-empty.svg
deleted file mode 100644
index 6bc9d3044d..0000000000
--- a/public/res/ic/outlined/shield-empty.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/shield-user.svg b/public/res/ic/outlined/shield-user.svg
deleted file mode 100644
index bd5f07c551..0000000000
--- a/public/res/ic/outlined/shield-user.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/shield.svg b/public/res/ic/outlined/shield.svg
deleted file mode 100644
index 9bb46fa10b..0000000000
--- a/public/res/ic/outlined/shield.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/space-globe.svg b/public/res/ic/outlined/space-globe.svg
deleted file mode 100644
index 63d71f1d05..0000000000
--- a/public/res/ic/outlined/space-globe.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/space-lock.svg b/public/res/ic/outlined/space-lock.svg
deleted file mode 100644
index b15705caa6..0000000000
--- a/public/res/ic/outlined/space-lock.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/space-plus.svg b/public/res/ic/outlined/space-plus.svg
deleted file mode 100644
index 4d69a1ef1b..0000000000
--- a/public/res/ic/outlined/space-plus.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/space.svg b/public/res/ic/outlined/space.svg
deleted file mode 100644
index a4b54b3e70..0000000000
--- a/public/res/ic/outlined/space.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/star.svg b/public/res/ic/outlined/star.svg
deleted file mode 100644
index 290f159a6f..0000000000
--- a/public/res/ic/outlined/star.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/sticker.svg b/public/res/ic/outlined/sticker.svg
deleted file mode 100644
index bc486e5e9e..0000000000
--- a/public/res/ic/outlined/sticker.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/public/res/ic/outlined/sun.svg b/public/res/ic/outlined/sun.svg
deleted file mode 100644
index d8ed06fd90..0000000000
--- a/public/res/ic/outlined/sun.svg
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/tick-mark.svg b/public/res/ic/outlined/tick-mark.svg
deleted file mode 100644
index 8e76ed555f..0000000000
--- a/public/res/ic/outlined/tick-mark.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/user.svg b/public/res/ic/outlined/user.svg
deleted file mode 100644
index 6756a1b2f6..0000000000
--- a/public/res/ic/outlined/user.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/vertical-menu.svg b/public/res/ic/outlined/vertical-menu.svg
deleted file mode 100644
index ec5c544c80..0000000000
--- a/public/res/ic/outlined/vertical-menu.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/vlc.svg b/public/res/ic/outlined/vlc.svg
deleted file mode 100644
index 8a2b844f1e..0000000000
--- a/public/res/ic/outlined/vlc.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/volume-full.svg b/public/res/ic/outlined/volume-full.svg
deleted file mode 100644
index 20419e7281..0000000000
--- a/public/res/ic/outlined/volume-full.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/public/res/ic/outlined/volume-mute.svg b/public/res/ic/outlined/volume-mute.svg
deleted file mode 100644
index beb067719d..0000000000
--- a/public/res/ic/outlined/volume-mute.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
diff --git a/src/app/atoms/avatar/Avatar.jsx b/src/app/atoms/avatar/Avatar.jsx
deleted file mode 100644
index 27bf7c906b..0000000000
--- a/src/app/atoms/avatar/Avatar.jsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './Avatar.scss';
-
-import Text from '../text/Text';
-import RawIcon from '../system-icons/RawIcon';
-
-import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
-import { avatarInitials } from '../../../util/common';
-
-const Avatar = React.forwardRef(({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
- let textSize = 's1';
- if (size === 'large') textSize = 'h1';
- if (size === 'small') textSize = 'b1';
- if (size === 'extra-small') textSize = 'b3';
-
- return (
-
- {imageSrc !== null ? (
-

{
- e.target.style.backgroundColor = 'transparent';
- }}
- onError={(e) => {
- e.target.src = ImageBrokenSVG;
- }}
- alt=""
- />
- ) : (
-
- {iconSrc !== null ? (
-
- ) : (
- text !== null && (
-
- {avatarInitials(text)}
-
- )
- )}
-
- )}
-
- );
-});
-
-Avatar.defaultProps = {
- text: null,
- bgColor: 'transparent',
- iconSrc: null,
- iconColor: null,
- imageSrc: null,
- size: 'normal',
-};
-
-Avatar.propTypes = {
- text: PropTypes.string,
- bgColor: PropTypes.string,
- iconSrc: PropTypes.string,
- iconColor: PropTypes.string,
- imageSrc: PropTypes.string,
- size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']),
-};
-
-export default Avatar;
diff --git a/src/app/atoms/avatar/Avatar.scss b/src/app/atoms/avatar/Avatar.scss
deleted file mode 100644
index ea69c9e8ac..0000000000
--- a/src/app/atoms/avatar/Avatar.scss
+++ /dev/null
@@ -1,56 +0,0 @@
-@use '../../partials/flex';
-
-.avatar-container {
- display: inline-flex;
- width: 42px;
- height: 42px;
- border-radius: var(--bo-radius);
- position: relative;
-
- &__large {
- width: var(--av-large);
- height: var(--av-large);
- }
- &__normal {
- width: var(--av-normal);
- height: var(--av-normal);
- }
-
- &__small {
- width: var(--av-small);
- height: var(--av-small);
- }
-
- &__extra-small {
- width: var(--av-extra-small);
- height: var(--av-extra-small);
- }
-
- > img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- border-radius: inherit;
- background-color: var(--bg-surface-hover);
- }
-
- .avatar__border {
- @extend .cp-fx__row--c-c;
-
- position: absolute;
- top: 0;
- left: 0;
-
- width: 100%;
- height: 100%;
- border-radius: inherit;
-
- .text {
- color: white;
- }
- &--active {
- @extend .avatar__border;
- box-shadow: var(--bs-surface-border);
- }
- }
-}
\ No newline at end of file
diff --git a/src/app/atoms/avatar/render.js b/src/app/atoms/avatar/render.js
deleted file mode 100644
index e8cf1a6613..0000000000
--- a/src/app/atoms/avatar/render.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { avatarInitials, cssVar } from '../../../util/common';
-
-// renders the avatar and returns it as an URL
-export default async function renderAvatar({
- text, bgColor, imageSrc, size, borderRadius, scale,
-}) {
- try {
- const canvas = document.createElement('canvas');
- canvas.width = size * scale;
- canvas.height = size * scale;
-
- const ctx = canvas.getContext('2d');
-
- ctx.scale(scale, scale);
-
- // rounded corners
- ctx.beginPath();
- ctx.moveTo(size, size);
- ctx.arcTo(0, size, 0, 0, borderRadius);
- ctx.arcTo(0, 0, size, 0, borderRadius);
- ctx.arcTo(size, 0, size, size, borderRadius);
- ctx.arcTo(size, size, 0, size, borderRadius);
-
- if (imageSrc) {
- // clip corners of image
- ctx.closePath();
- ctx.clip();
-
- const img = new Image();
- img.crossOrigin = 'anonymous';
- const promise = new Promise((resolve, reject) => {
- img.onerror = reject;
- img.onload = resolve;
- });
- img.src = imageSrc;
- await promise;
-
- ctx.drawImage(img, 0, 0, size, size);
- } else {
- // colored background
- ctx.fillStyle = cssVar(bgColor);
- ctx.fill();
-
- // centered letter
- ctx.fillStyle = '#fff';
- ctx.font = `${cssVar('--fs-s1')} ${cssVar('--font-primary')}`;
- ctx.textBaseline = 'middle';
- ctx.textAlign = 'center';
- ctx.fillText(avatarInitials(text), size / 2, size / 2);
- }
-
- return canvas.toDataURL();
- } catch (e) {
- console.error(e);
- return imageSrc;
- }
-}
diff --git a/src/app/atoms/badge/NotificationBadge.jsx b/src/app/atoms/badge/NotificationBadge.jsx
deleted file mode 100644
index 12c1bd4473..0000000000
--- a/src/app/atoms/badge/NotificationBadge.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './NotificationBadge.scss';
-
-import Text from '../text/Text';
-
-function NotificationBadge({ alert, content }) {
- const notificationClass = alert ? ' notification-badge--alert' : '';
- return (
-
- {content !== null && {content}}
-
- );
-}
-
-NotificationBadge.defaultProps = {
- alert: false,
- content: null,
-};
-
-NotificationBadge.propTypes = {
- alert: PropTypes.bool,
- content: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]),
-};
-
-export default NotificationBadge;
diff --git a/src/app/atoms/badge/NotificationBadge.scss b/src/app/atoms/badge/NotificationBadge.scss
deleted file mode 100644
index f5cfa73f39..0000000000
--- a/src/app/atoms/badge/NotificationBadge.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.notification-badge {
- min-width: 16px;
- min-height: 8px;
- padding: 0 var(--sp-ultra-tight);
- background-color: var(--bg-badge);
- border-radius: var(--bo-radius);
-
- .text {
- color: var(--tc-badge);
- text-align: center;
- }
-
- &--alert {
- background-color: var(--bg-positive);
- }
-
- &:empty {
- min-width: 8px;
- margin: 0 var(--sp-ultra-tight);
- }
-}
\ No newline at end of file
diff --git a/src/app/atoms/button/Button.jsx b/src/app/atoms/button/Button.jsx
deleted file mode 100644
index 1c1c950c3d..0000000000
--- a/src/app/atoms/button/Button.jsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './Button.scss';
-
-import Text from '../text/Text';
-import RawIcon from '../system-icons/RawIcon';
-import { blurOnBubbling } from './script';
-
-const Button = React.forwardRef(({
- id, className, variant, iconSrc,
- type, onClick, children, disabled,
-}, ref) => {
- const iconClass = (iconSrc === null) ? '' : `btn-${variant}--icon`;
- return (
-
- );
-});
-
-Button.defaultProps = {
- id: '',
- className: null,
- variant: 'surface',
- iconSrc: null,
- type: 'button',
- onClick: null,
- disabled: false,
-};
-
-Button.propTypes = {
- id: PropTypes.string,
- className: PropTypes.string,
- variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
- iconSrc: PropTypes.string,
- type: PropTypes.oneOf(['button', 'submit', 'reset']),
- onClick: PropTypes.func,
- children: PropTypes.node.isRequired,
- disabled: PropTypes.bool,
-};
-
-export default Button;
diff --git a/src/app/atoms/button/Button.scss b/src/app/atoms/button/Button.scss
deleted file mode 100644
index e1a01bb05e..0000000000
--- a/src/app/atoms/button/Button.scss
+++ /dev/null
@@ -1,81 +0,0 @@
-@use 'state';
-@use '../../partials/dir';
-@use '../../partials/text';
-
-.btn-surface,
-.btn-primary,
-.btn-positive,
-.btn-caution,
-.btn-danger {
- display: inline-flex;
- align-items: center;
- justify-content: center;
-
- min-width: 80px;
- padding: var(--sp-extra-tight) var(--sp-normal);
- background-color: transparent;
- border: none;
- border-radius: var(--bo-radius);
- cursor: pointer;
- @include state.disabled;
-
- & .text {
- @extend .cp-txt__ellipsis;
- }
-
- &--icon {
- @include dir.side(padding, var(--sp-tight), var(--sp-loose));
-
- }
- .ic-raw {
- @include dir.side(margin, 0, var(--sp-extra-tight));
- flex-shrink: 0;
- }
-}
-
-@mixin color($textColor, $iconColor) {
- .text {
- color: $textColor;
- }
- .ic-raw {
- background-color: $iconColor;
- }
-}
-
-
-.btn-surface {
- box-shadow: var(--bs-surface-border);
- @include color(var(--tc-surface-high), var(--ic-surface-normal));
- @include state.hover(var(--bg-surface-hover));
- @include state.focus(var(--bs-surface-outline));
- @include state.active(var(--bg-surface-active));
-}
-
-.btn-primary {
- background-color: var(--bg-primary);
- @include color(var(--tc-primary-high), var(--ic-primary-normal));
- @include state.hover(var(--bg-primary-hover));
- @include state.focus(var(--bs-primary-outline));
- @include state.active(var(--bg-primary-active));
-}
-.btn-positive {
- box-shadow: var(--bs-positive-border);
- @include color(var(--tc-positive-high), var(--ic-positive-normal));
- @include state.hover(var(--bg-positive-hover));
- @include state.focus(var(--bs-positive-outline));
- @include state.active(var(--bg-positive-active));
-}
-.btn-caution {
- box-shadow: var(--bs-caution-border);
- @include color(var(--tc-caution-high), var(--ic-caution-normal));
- @include state.hover(var(--bg-caution-hover));
- @include state.focus(var(--bs-caution-outline));
- @include state.active(var(--bg-caution-active));
-}
-.btn-danger {
- box-shadow: var(--bs-danger-border);
- @include color(var(--tc-danger-high), var(--ic-danger-normal));
- @include state.hover(var(--bg-danger-hover));
- @include state.focus(var(--bs-danger-outline));
- @include state.active(var(--bg-danger-active));
-}
\ No newline at end of file
diff --git a/src/app/atoms/button/Checkbox.jsx b/src/app/atoms/button/Checkbox.jsx
deleted file mode 100644
index 7fcea3b55f..0000000000
--- a/src/app/atoms/button/Checkbox.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './Checkbox.scss';
-
-function Checkbox({
- variant, isActive, onToggle,
- disabled, tabIndex,
-}) {
- const className = `checkbox checkbox-${variant}${isActive ? ' checkbox--active' : ''}`;
- if (onToggle === null) return ;
- return (
- // eslint-disable-next-line jsx-a11y/control-has-associated-label
-
+ }
+ />
+
+ )}
{devices
.sort((d1, d2) => {
if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
@@ -89,12 +149,20 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
refreshDeviceList={refreshDeviceList}
disabled={deleting}
options={
-
+ authMetadata ? (
+
+ ) : (
+
+ )
}
/>
{showVerification && crypto && (
diff --git a/src/app/features/settings/devices/Verification.tsx b/src/app/features/settings/devices/Verification.tsx
index 59fa6b67bb..6c7eab17ba 100644
--- a/src/app/features/settings/devices/Verification.tsx
+++ b/src/app/features/settings/devices/Verification.tsx
@@ -32,6 +32,9 @@ import {
DeviceVerificationSetup,
} from '../../../components/DeviceVerificationSetup';
import { stopPropagation } from '../../../utils/keyboard';
+import { useAuthMetadata } from '../../../hooks/useAuthMetadata';
+import { withSearchParam } from '../../../pages/pathUtils';
+import { useAccountManagementActions } from '../../../hooks/useAccountManagement';
type VerificationStatusBadgeProps = {
verificationStatus: VerificationStatus;
@@ -252,6 +255,8 @@ export function EnableVerification({ visible }: EnableVerificationProps) {
export function DeviceVerificationOptions() {
const [menuCords, setMenuCords] = useState();
+ const authMetadata = useAuthMetadata();
+ const accountManagementActions = useAccountManagementActions();
const [reset, setReset] = useState(false);
@@ -265,6 +270,18 @@ export function DeviceVerificationOptions() {
const handleReset = () => {
setMenuCords(undefined);
+
+ if (authMetadata) {
+ const authUrl = authMetadata.account_management_uri ?? authMetadata.issuer;
+ window.open(
+ withSearchParam(authUrl, {
+ action: accountManagementActions.crossSigningReset,
+ }),
+ '_blank'
+ );
+ return;
+ }
+
setReset(true);
};
diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx
index 04e2728b50..b861a060b7 100644
--- a/src/app/features/settings/general/General.tsx
+++ b/src/app/features/settings/general/General.tsx
@@ -1,15 +1,19 @@
import React, {
ChangeEventHandler,
+ FormEventHandler,
KeyboardEventHandler,
MouseEventHandler,
+ useEffect,
useState,
} from 'react';
+import dayjs from 'dayjs';
import {
as,
Box,
Button,
Chip,
config,
+ Header,
Icon,
IconButton,
Icons,
@@ -28,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
import { useSetting } from '../../../state/hooks/settings';
-import { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
+import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
import { SettingTile } from '../../../components/setting-tile';
import { KeySymbol } from '../../../utils/key-symbol';
import { isMacOS } from '../../../utils/user-agent';
@@ -44,6 +48,7 @@ import {
import { stopPropagation } from '../../../utils/keyboard';
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
+import { useDateFormatItems } from '../../../hooks/useDateFormat';
import { SequenceCardStyle } from '../styles.css';
type ThemeSelectorProps = {
@@ -300,6 +305,7 @@ function PageZoomInput() {
function Appearance() {
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
+ const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
return (
@@ -327,6 +333,13 @@ function Appearance() {
/>
+
+ }
+ />
+
+
void;
+};
+function DateHint({ hasChanges, handleReset }: DateHintProps) {
+ const [anchor, setAnchor] = useState();
+ const categoryPadding = { padding: config.space.S200, paddingTop: 0 };
+
+ const handleOpenMenu: MouseEventHandler = (evt) => {
+ setAnchor(evt.currentTarget.getBoundingClientRect());
+ };
+ return (
+ setAnchor(undefined),
+ clickOutsideDeactivates: true,
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+
+
+
+
+
+
+
+
+ YY
+
+ {': '}
+ Two-digit year
+ {' '}
+
+
+ YYYY
+
+ {': '}Four-digit year
+
+
+
+
+
+
+
+
+
+ M
+
+ {': '}The month
+
+
+
+ MM
+
+ {': '}Two-digit month
+ {' '}
+
+
+ MMM
+
+ {': '}Short month name
+
+
+
+ MMMM
+
+ {': '}Full month name
+
+
+
+
+
+
+
+
+
+ D
+
+ {': '}Day of the month
+
+
+
+ DD
+
+ {': '}Two-digit day of the month
+
+
+
+
+
+
+
+
+ d
+
+ {': '}Day of the week (Sunday = 0)
+
+
+
+ dd
+
+ {': '}Two-letter day name
+
+
+
+ ddd
+
+ {': '}Short day name
+
+
+
+ dddd
+
+ {': '}Full day name
+
+
+
+
+
+
+
+ }
+ >
+ {hasChanges ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+}
+
+type CustomDateFormatProps = {
+ value: string;
+ onChange: (format: string) => void;
+};
+function CustomDateFormat({ value, onChange }: CustomDateFormatProps) {
+ const [dateFormatCustom, setDateFormatCustom] = useState(value);
+
+ useEffect(() => {
+ setDateFormatCustom(value);
+ }, [value]);
+
+ const handleChange: ChangeEventHandler = (evt) => {
+ const format = evt.currentTarget.value;
+ setDateFormatCustom(format);
+ };
+
+ const handleReset = () => {
+ setDateFormatCustom(value);
+ };
+
+ const handleSubmit: FormEventHandler = (evt) => {
+ evt.preventDefault();
+
+ const target = evt.target as HTMLFormElement | undefined;
+ const customDateFormatInput = target?.customDateFormatInput as HTMLInputElement | undefined;
+ const format = customDateFormatInput?.value;
+ if (!format) return;
+
+ onChange(format);
+ };
+
+ const hasChanges = dateFormatCustom !== value;
+ return (
+
+
+
+ }
+ />
+
+
+ Save
+
+
+
+ );
+}
+
+type PresetDateFormatProps = {
+ value: string;
+ onChange: (format: string) => void;
+};
+function PresetDateFormat({ value, onChange }: PresetDateFormatProps) {
+ const [menuCords, setMenuCords] = useState();
+ const dateFormatItems = useDateFormatItems();
+
+ const getDisplayDate = (format: string): string =>
+ format !== '' ? dayjs().format(format) : 'Custom';
+
+ const handleMenu: MouseEventHandler = (evt) => {
+ setMenuCords(evt.currentTarget.getBoundingClientRect());
+ };
+
+ const handleSelect = (format: DateFormat) => {
+ onChange(format);
+ setMenuCords(undefined);
+ };
+
+ return (
+ <>
+ }
+ onClick={handleMenu}
+ >
+
+ {getDisplayDate(dateFormatItems.find((i) => i.format === value)?.format ?? value)}
+
+
+ setMenuCords(undefined),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) =>
+ evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+ isKeyBackward: (evt: KeyboardEvent) =>
+ evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+
+
+ {dateFormatItems.map((item) => (
+
+ ))}
+
+
+
+ }
+ />
+ >
+ );
+}
+
+function SelectDateFormat() {
+ const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+ const [selectedDateFormat, setSelectedDateFormat] = useState(dateFormatString);
+ const customDateFormat = selectedDateFormat === '';
+
+ const handlePresetChange = (format: string) => {
+ setSelectedDateFormat(format);
+ if (format !== '') {
+ setDateFormatString(format);
+ }
+ };
+
+ return (
+ <>
+ }
+ />
+ {customDateFormat && (
+
+ )}
+ >
+ );
+}
+
+function DateAndTime() {
+ const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+
+ return (
+
+ Date & Time
+
+ }
+ />
+
+
+
+
+
+
+ );
+}
+
function Editor() {
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
@@ -637,6 +1003,7 @@ export function General({ requestClose }: GeneralProps) {
+
diff --git a/src/app/features/settings/notifications/DeregisterPushNotifications.tsx b/src/app/features/settings/notifications/DeregisterPushNotifications.tsx
new file mode 100644
index 0000000000..bc6b65d453
--- /dev/null
+++ b/src/app/features/settings/notifications/DeregisterPushNotifications.tsx
@@ -0,0 +1,149 @@
+import React, { useState } from 'react';
+import FocusTrap from 'focus-trap-react';
+import {
+ Box,
+ Button,
+ color,
+ config,
+ Dialog,
+ Header,
+ Icon,
+ IconButton,
+ Icons,
+ Overlay,
+ OverlayBackdrop,
+ OverlayCenter,
+ Spinner,
+ Text,
+} from 'folds';
+import { useAtom } from 'jotai';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { useSetting } from '../../../state/hooks/settings';
+import { settingsAtom } from '../../../state/settings';
+import { pushSubscriptionAtom } from '../../../state/pushSubscription';
+import { deRegisterAllPushers } from './PushNotifications';
+import { SettingTile } from '../../../components/setting-tile';
+
+type ConfirmDeregisterDialogProps = {
+ onClose: () => void;
+ onConfirm: () => void;
+ isLoading: boolean;
+};
+
+function ConfirmDeregisterDialog({ onClose, onConfirm, isLoading }: ConfirmDeregisterDialogProps) {
+ return (
+ }>
+
+
+
+
+
+
+ );
+}
+
+export function DeregisterAllPushersSetting() {
+ const mx = useMatrixClient();
+ const [deregisterState] = useAsyncCallback(deRegisterAllPushers);
+ const [isConfirming, setIsConfirming] = useState(false);
+ const [usePushNotifications, setPushNotifications] = useSetting(
+ settingsAtom,
+ 'usePushNotifications'
+ );
+
+ const [pushSubscription, setPushSubscription] = useAtom(pushSubscriptionAtom);
+
+ const handleOpenConfirmDialog = () => {
+ setIsConfirming(true);
+ };
+
+ const handleCloseConfirmDialog = () => {
+ if (deregisterState.status === AsyncStatus.Loading) return;
+ setIsConfirming(false);
+ };
+
+ const handleConfirmDeregister = async () => {
+ await deRegisterAllPushers(mx);
+ setPushNotifications(false);
+ setPushSubscription(null);
+ setIsConfirming(false);
+ };
+
+ return (
+ <>
+ {isConfirming && (
+
+ )}
+
+
+
+ This will remove push notifications from all your sessions/devices. You will need to
+ re-enable them on each device individually.
+
+ {deregisterState.status === AsyncStatus.Error && (
+
+
+ Failed to deregister devices. Please try again.
+
+ )}
+ {deregisterState.status === AsyncStatus.Success && (
+
+
+ Successfully deregistered all devices.
+
+ )}
+
+ }
+ after={
+
+
+ Reset All
+
+
+ }
+ />
+ >
+ );
+}
diff --git a/src/app/features/settings/notifications/PushNotifications.tsx b/src/app/features/settings/notifications/PushNotifications.tsx
new file mode 100644
index 0000000000..a67667ca1d
--- /dev/null
+++ b/src/app/features/settings/notifications/PushNotifications.tsx
@@ -0,0 +1,156 @@
+import { MatrixClient } from 'matrix-js-sdk';
+import { ClientConfig } from '../../../hooks/useClientConfig';
+
+export async function requestBrowserNotificationPermission(): Promise {
+ if (!('Notification' in window)) {
+ return 'denied';
+ }
+ try {
+ const permission: NotificationPermission = await Notification.requestPermission();
+ return permission;
+ } catch (error) {
+ console.error('Error requesting notification permission:', error);
+ return 'denied';
+ }
+}
+
+export async function enablePushNotifications(
+ mx: MatrixClient,
+ clientConfig: ClientConfig,
+ pushSubscriptionAtom: Atom
+): Promise {
+ if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
+ throw new Error('Push messaging is not supported in this browser.');
+ }
+ const [pushSubAtom, setPushSubscription] = pushSubscriptionAtom;
+ const registration = await navigator.serviceWorker.ready;
+ const currentBrowserSub = await registration.pushManager.getSubscription();
+
+ /* Self-Healing Check. Effectively checks if the browser has invalidated our subscription and recreates it
+ only when necessary. This prevents us from needing an external call to get back the web push info.
+ */
+ if (currentBrowserSub && pushSubAtom && currentBrowserSub.endpoint === pushSubAtom.endpoint) {
+ console.error('Valid saved subscription found. Ensuring pusher is enabled on homeserver...');
+ const pusherData = {
+ kind: 'http' as const,
+ app_id: clientConfig.pushNotificationDetails?.webPushAppID,
+ pushkey: pushSubAtom.keys!.p256dh!,
+ app_display_name: 'Cinny',
+ device_display_name: 'This Browser',
+ lang: navigator.language || 'en',
+ data: {
+ url: clientConfig.pushNotificationDetails?.pushNotifyUrl,
+ // format: 'event_id_only' as const,
+ events_only: true,
+ endpoint: pushSubAtom.endpoint,
+ p256dh: pushSubAtom.keys!.p256dh!,
+ auth: pushSubAtom.keys!.auth!,
+ },
+ append: false,
+ };
+ navigator.serviceWorker.controller?.postMessage({
+ url: mx.baseUrl,
+ type: 'togglePush',
+ pusherData,
+ token: mx.getAccessToken(),
+ });
+ return;
+ }
+
+ console.error('No valid saved subscription. Performing full, new subscription...');
+
+ if (currentBrowserSub) {
+ await currentBrowserSub.unsubscribe();
+ }
+
+ const newSubscription = await registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: clientConfig.pushNotificationDetails?.vapidPublicKey,
+ });
+
+ setPushSubscription(newSubscription);
+
+ const subJson = newSubscription.toJSON();
+ const pusherData = {
+ kind: 'http' as const,
+ app_id: clientConfig.pushNotificationDetails?.webPushAppID,
+ pushkey: subJson.keys!.p256dh!,
+ app_display_name: 'Cinny',
+ device_display_name:
+ (await mx.getDevice(mx.getDeviceId() ?? '')).display_name ?? 'Unknown Device',
+ lang: navigator.language || 'en',
+ data: {
+ url: clientConfig.pushNotificationDetails?.pushNotifyUrl,
+ // format: 'event_id_only' as const,
+ endpoint: newSubscription.endpoint,
+ p256dh: subJson.keys!.p256dh!,
+ auth: subJson.keys!.auth!,
+ },
+ append: false,
+ };
+
+ navigator.serviceWorker.controller?.postMessage({
+ url: mx.baseUrl,
+ type: 'togglePush',
+ pusherData,
+ token: mx.getAccessToken(),
+ });
+}
+
+/**
+ * Disables push notifications by telling the homeserver to delete the pusher,
+ * but keeps the browser subscription locally for a fast re-enable.
+ */
+export async function disablePushNotifications(
+ mx: MatrixClient,
+ clientConfig: ClientConfig,
+ pushSubscriptionAtom: Atom
+): Promise {
+ const [pushSubAtom] = pushSubscriptionAtom;
+
+ const pusherData = {
+ kind: null,
+ app_id: clientConfig.pushNotificationDetails?.webPushAppID,
+ pushkey: pushSubAtom?.keys?.p256dh,
+ };
+
+ navigator.serviceWorker.controller?.postMessage({
+ url: mx.baseUrl,
+ type: 'togglePush',
+ pusherData,
+ token: mx.getAccessToken(),
+ });
+}
+
+export async function deRegisterAllPushers(mx: MatrixClient): Promise {
+ const response = await mx.getPushers();
+ const pushers = response.pushers || [];
+ if (pushers.length === 0) return;
+
+ const deletionPromises = pushers.map((pusher) => {
+ const pusherToDelete = {
+ kind: null,
+ app_id: pusher.app_id,
+ pushkey: pusher.pushkey,
+ };
+ return mx.setPusher(pusherToDelete as any);
+ });
+
+ await Promise.allSettled(deletionPromises);
+}
+
+export async function togglePusher(
+ mx: MatrixClient,
+ clientConfig: ClientConfig,
+ visible: boolean,
+ usePushNotifications: boolean,
+ pushSubscriptionAtom: Atom
+): Promise {
+ if (usePushNotifications) {
+ if (visible) {
+ await disablePushNotifications(mx, clientConfig, pushSubscriptionAtom);
+ } else {
+ await enablePushNotifications(mx, clientConfig, pushSubscriptionAtom);
+ }
+ }
+}
diff --git a/src/app/features/settings/notifications/SystemNotification.tsx b/src/app/features/settings/notifications/SystemNotification.tsx
index e0df06df91..276b64219e 100644
--- a/src/app/features/settings/notifications/SystemNotification.tsx
+++ b/src/app/features/settings/notifications/SystemNotification.tsx
@@ -1,6 +1,8 @@
-import React, { useCallback } from 'react';
+/* eslint-disable no-nested-ternary */
+import React, { useCallback, useEffect, useState } from 'react';
import { Box, Text, Switch, Button, color, Spinner } from 'folds';
import { IPusherRequest } from 'matrix-js-sdk';
+import { useAtom } from 'jotai';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
@@ -10,6 +12,14 @@ import { getNotificationState, usePermissionState } from '../../../hooks/usePerm
import { useEmailNotifications } from '../../../hooks/useEmailNotifications';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import {
+ requestBrowserNotificationPermission,
+ enablePushNotifications,
+ disablePushNotifications,
+} from './PushNotifications';
+import { useClientConfig } from '../../../hooks/useClientConfig';
+import { pushSubscriptionAtom } from '../../../state/pushSubscription';
+import { DeregisterAllPushersSetting } from './DeregisterPushNotifications';
function EmailNotification() {
const mx = useMatrixClient();
@@ -84,21 +94,93 @@ function EmailNotification() {
);
}
+function WebPushNotificationSetting() {
+ const mx = useMatrixClient();
+ const clientConfig = useClientConfig();
+ const [isLoading, setIsLoading] = useState(true);
+ const [usePushNotifications, setPushNotifications] = useSetting(
+ settingsAtom,
+ 'usePushNotifications'
+ );
+ const pushSubAtom = useAtom(pushSubscriptionAtom);
+
+ const browserPermission = usePermissionState('notifications', getNotificationState());
+ useEffect(() => {
+ setIsLoading(false);
+ }, []);
+ const handleRequestPermissionAndEnable = async () => {
+ setIsLoading(true);
+ try {
+ const permissionResult = await requestBrowserNotificationPermission();
+ if (permissionResult === 'granted') {
+ await enablePushNotifications(mx, clientConfig, pushSubAtom);
+ setPushNotifications(true);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handlePushSwitchChange = async (wantsPush: boolean) => {
+ setIsLoading(true);
+
+ try {
+ if (wantsPush) {
+ await enablePushNotifications(mx, clientConfig, pushSubAtom);
+ } else {
+ await disablePushNotifications(mx, clientConfig, pushSubAtom);
+ }
+ setPushNotifications(wantsPush);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ Permission blocked. Please allow notifications in your browser settings.
+
+ ) : (
+ 'Receive notifications when the app is closed or in the background.'
+ )
+ }
+ after={
+ isLoading ? (
+
+ ) : browserPermission === 'prompt' ? (
+
+ Enable
+
+ ) : browserPermission === 'granted' ? (
+
+ ) : null
+ }
+ />
+ );
+}
+
export function SystemNotification() {
- const notifPermission = usePermissionState('notifications', getNotificationState());
- const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications');
+ const [showInAppNotifs, setShowInAppNotifs] = useSetting(settingsAtom, 'useInAppNotifications');
const [isNotificationSounds, setIsNotificationSounds] = useSetting(
settingsAtom,
'isNotificationSounds'
);
- const requestNotificationPermission = () => {
- window.Notification.requestPermission();
- };
-
return (
- System
+ System & Notifications
+
+
+
- {'Notification' in window
- ? 'Notification permission is blocked. Please allow notification permission from browser address bar.'
- : 'Notifications are not supported by the system.'}
-
- ) : (
- Show desktop notifications when message arrive.
- )
- }
- after={
- notifPermission === 'prompt' ? (
-
- Enable
-
- ) : (
-
- )
- }
+ title="In-App Notifications"
+ description="Show a notification when a message arrives while the app is open (but not focused on the room)."
+ after={}
/>
}
/>
@@ -153,6 +213,15 @@ export function SystemNotification() {
>
+
+
+
+
);
}
diff --git a/src/app/features/space-settings/SpaceSettingsRenderer.tsx b/src/app/features/space-settings/SpaceSettingsRenderer.tsx
index 085c5e2b54..79947e0cac 100644
--- a/src/app/features/space-settings/SpaceSettingsRenderer.tsx
+++ b/src/app/features/space-settings/SpaceSettingsRenderer.tsx
@@ -16,7 +16,7 @@ function RenderSettings({ state }: RenderSettingsProps) {
const allJoinedRooms = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allJoinedRooms);
const room = getRoom(roomId);
- const space = spaceId ? getRoom(spaceId) : undefined;
+ const space = spaceId && spaceId !== roomId ? getRoom(spaceId) : undefined;
if (!room) return null;
diff --git a/src/app/features/space-settings/general/General.tsx b/src/app/features/space-settings/general/General.tsx
index 6f4d8d38a4..641bfa7a7b 100644
--- a/src/app/features/space-settings/general/General.tsx
+++ b/src/app/features/space-settings/general/General.tsx
@@ -11,6 +11,8 @@ import {
RoomPublish,
RoomUpgrade,
} from '../../common-settings/general';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type GeneralProps = {
requestClose: () => void;
@@ -18,6 +20,8 @@ type GeneralProps = {
export function General({ requestClose }: GeneralProps) {
const room = useRoom();
const powerLevels = usePowerLevels(room);
+ const creators = useRoomCreators(room);
+ const permissions = useRoomPermissions(creators, powerLevels);
return (
@@ -39,20 +43,20 @@ export function General({ requestClose }: GeneralProps) {
-
+
Options
-
-
+
+
Addresses
-
-
+
+
Advance Options
-
+
diff --git a/src/app/features/space-settings/permissions/Permissions.tsx b/src/app/features/space-settings/permissions/Permissions.tsx
index ae3769bf18..7572a71bf4 100644
--- a/src/app/features/space-settings/permissions/Permissions.tsx
+++ b/src/app/features/space-settings/permissions/Permissions.tsx
@@ -2,11 +2,13 @@ import React, { useState } from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom';
-import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { StateEvent } from '../../../../types/matrix/room';
import { usePermissionGroups } from './usePermissionItems';
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type PermissionsProps = {
requestClose: () => void;
@@ -15,11 +17,12 @@ export function Permissions({ requestClose }: PermissionsProps) {
const mx = useMatrixClient();
const room = useRoom();
const powerLevels = usePowerLevels(room);
- const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
- const canEditPowers = canSendStateEvent(
- StateEvent.PowerLevelTags,
- getPowerLevel(mx.getSafeUserId())
- );
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+
+ const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
+ const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
const permissionGroups = usePermissionGroups();
const [powerEditor, setPowerEditor] = useState(false);
@@ -57,7 +60,11 @@ export function Permissions({ requestClose }: PermissionsProps) {
onEdit={canEditPowers ? handleEditPowers : undefined}
permissionGroups={permissionGroups}
/>
-
+
diff --git a/src/app/hooks/router/useCreateSelected.ts b/src/app/hooks/router/useCreateSelected.ts
new file mode 100644
index 0000000000..2034a44927
--- /dev/null
+++ b/src/app/hooks/router/useCreateSelected.ts
@@ -0,0 +1,12 @@
+import { useMatch } from 'react-router-dom';
+import { getCreatePath } from '../../pages/pathUtils';
+
+export const useCreateSelected = (): boolean => {
+ const match = useMatch({
+ path: getCreatePath(),
+ caseSensitive: true,
+ end: false,
+ });
+
+ return !!match;
+};
diff --git a/src/app/hooks/useAccountManagement.ts b/src/app/hooks/useAccountManagement.ts
new file mode 100644
index 0000000000..5eafedc48b
--- /dev/null
+++ b/src/app/hooks/useAccountManagement.ts
@@ -0,0 +1,17 @@
+import { useMemo } from 'react';
+
+export const useAccountManagementActions = () => {
+ const actions = useMemo(
+ () => ({
+ profile: 'org.matrix.profile',
+ sessionsList: 'org.matrix.sessions_list',
+ sessionView: 'org.matrix.session_view',
+ sessionEnd: 'org.matrix.session_end',
+ accountDeactivate: 'org.matrix.account_deactivate',
+ crossSigningReset: 'org.matrix.cross_signing_reset',
+ }),
+ []
+ );
+
+ return actions;
+};
diff --git a/src/app/hooks/useAppVisibility.ts b/src/app/hooks/useAppVisibility.ts
new file mode 100644
index 0000000000..f53974c1a7
--- /dev/null
+++ b/src/app/hooks/useAppVisibility.ts
@@ -0,0 +1,45 @@
+import { useEffect } from 'react';
+import { MatrixClient } from 'matrix-js-sdk';
+import { useAtom } from 'jotai';
+import { togglePusher } from '../features/settings/notifications/PushNotifications';
+import { appEvents } from '../utils/appEvents';
+import { useClientConfig } from './useClientConfig';
+import { useSetting } from '../state/hooks/settings';
+import { settingsAtom } from '../state/settings';
+import { pushSubscriptionAtom } from '../state/pushSubscription';
+
+export function useAppVisibility(mx: MatrixClient | undefined) {
+ const clientConfig = useClientConfig();
+ const [usePushNotifications] = useSetting(settingsAtom, 'usePushNotifications');
+ const pushSubAtom = useAtom(pushSubscriptionAtom);
+
+ useEffect(() => {
+ const handleVisibilityChange = () => {
+ const isVisible = document.visibilityState === 'visible';
+ appEvents.onVisibilityChange?.(isVisible);
+ if (!isVisible) {
+ appEvents.onVisibilityHidden?.();
+ }
+ };
+
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+
+ return () => {
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!mx) return;
+
+ const handleVisibilityForNotifications = (isVisible: boolean) => {
+ togglePusher(mx, clientConfig, isVisible, usePushNotifications, pushSubAtom);
+ };
+
+ appEvents.onVisibilityChange = handleVisibilityForNotifications;
+ // eslint-disable-next-line consistent-return
+ return () => {
+ appEvents.onVisibilityChange = null;
+ };
+ }, [mx, clientConfig, usePushNotifications, pushSubAtom]);
+}
diff --git a/src/app/hooks/useAuthMetadata.ts b/src/app/hooks/useAuthMetadata.ts
new file mode 100644
index 0000000000..db96746335
--- /dev/null
+++ b/src/app/hooks/useAuthMetadata.ts
@@ -0,0 +1,12 @@
+import { ValidatedAuthMetadata } from 'matrix-js-sdk';
+import { createContext, useContext } from 'react';
+
+const AuthMetadataContext = createContext(undefined);
+
+export const AuthMetadataProvider = AuthMetadataContext.Provider;
+
+export const useAuthMetadata = (): ValidatedAuthMetadata | undefined => {
+ const metadata = useContext(AuthMetadataContext);
+
+ return metadata;
+};
diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts
index e5fc6cc617..4e9fb74936 100644
--- a/src/app/hooks/useClientConfig.ts
+++ b/src/app/hooks/useClientConfig.ts
@@ -10,6 +10,12 @@ export type ClientConfig = {
homeserverList?: string[];
allowCustomHomeservers?: boolean;
+ pushNotificationDetails?: {
+ pushNotifyUrl?: string;
+ vapidPublicKey?: string;
+ webPushAppID?: string;
+ };
+
featuredCommunities?: {
openAsDefault?: boolean;
spaces?: string[];
diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts
index c95142e80a..c02b0e94e6 100644
--- a/src/app/hooks/useCommands.ts
+++ b/src/app/hooks/useCommands.ts
@@ -1,20 +1,32 @@
-import { Direction, IContextResponse, MatrixClient, Method, Room, RoomMember } from 'matrix-js-sdk';
+import {
+ Direction,
+ EventTimeline,
+ IContextResponse,
+ MatrixClient,
+ Method,
+ Preset,
+ Room,
+ RoomMember,
+ Visibility,
+} from 'matrix-js-sdk';
import { RoomServerAclEventContent } from 'matrix-js-sdk/lib/types';
import { useMemo } from 'react';
import {
+ addRoomIdToMDirect,
getDMRoomFor,
+ guessDmRoomUserId,
isRoomAlias,
isRoomId,
isServerName,
isUserId,
rateLimitedActions,
+ removeRoomIdFromMDirect,
} from '../utils/matrix';
-import { hasDevices } from '../../util/matrixUtil';
-import * as roomActions from '../../client/action/room';
import { useRoomNavigate } from './useRoomNavigate';
import { Membership, StateEvent } from '../../types/matrix/room';
import { getStateEvent } from '../utils/room';
import { splitWithSpace } from '../utils/common';
+import { createRoomEncryptionState } from '../components/create-room';
export const SHRUG = '¯\\_(ツ)_/¯';
export const TABLEFLIP = '(╯°□°)╯︵ ┻━┻';
@@ -192,7 +204,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
description: 'Start direct message with user. Example: /startdm userId1',
exe: async (payload) => {
const rawIds = splitWithSpace(payload);
- const userIds = rawIds.filter((id) => isUserId(id) && id !== mx.getUserId());
+ const userIds = rawIds.filter((id) => isUserId(id) && id !== mx.getSafeUserId());
if (userIds.length === 0) return;
if (userIds.length === 1) {
const dmRoomId = getDMRoomFor(mx, userIds[0])?.roomId;
@@ -201,9 +213,14 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
return;
}
}
- const devices = await Promise.all(userIds.map((uid) => hasDevices(mx, uid)));
- const isEncrypt = devices.every((hasDevice) => hasDevice);
- const result = await roomActions.createDM(mx, userIds, isEncrypt);
+ const result = await mx.createRoom({
+ is_direct: true,
+ invite: userIds,
+ visibility: Visibility.Private,
+ preset: Preset.TrustedPrivateChat,
+ initial_state: [createRoomEncryptionState()],
+ });
+ addRoomIdToMDirect(mx, result.room_id, userIds[0]);
navigateRoom(result.room_id);
},
},
@@ -212,10 +229,12 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
description: 'Join room with address. Example: /join address1 address2',
exe: async (payload) => {
const rawIds = splitWithSpace(payload);
- const roomIds = rawIds.filter(
+ const roomIdOrAliases = rawIds.filter(
(idOrAlias) => isRoomId(idOrAlias) || isRoomAlias(idOrAlias)
);
- roomIds.map((id) => roomActions.join(mx, id));
+ roomIdOrAliases.forEach(async (idOrAlias) => {
+ await mx.joinRoom(idOrAlias);
+ });
},
},
[Command.Leave]: {
@@ -314,7 +333,11 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
exe: async (payload) => {
const rawIds = splitWithSpace(payload);
const userIds = rawIds.filter((id) => isUserId(id));
- if (userIds.length > 0) roomActions.ignore(mx, userIds);
+ if (userIds.length > 0) {
+ let ignoredUsers = mx.getIgnoredUsers().concat(userIds);
+ ignoredUsers = [...new Set(ignoredUsers)];
+ await mx.setIgnoredUsers(ignoredUsers);
+ }
},
},
[Command.UnIgnore]: {
@@ -323,7 +346,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
exe: async (payload) => {
const rawIds = splitWithSpace(payload);
const userIds = rawIds.filter((id) => isUserId(id));
- if (userIds.length > 0) roomActions.unignore(mx, userIds);
+ if (userIds.length > 0) {
+ const ignoredUsers = mx.getIgnoredUsers();
+ await mx.setIgnoredUsers(ignoredUsers.filter((id) => !userIds.includes(id)));
+ }
},
},
[Command.MyRoomNick]: {
@@ -332,7 +358,21 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
exe: async (payload) => {
const nick = payload.trim();
if (nick === '') return;
- roomActions.setMyRoomNick(mx, room.roomId, nick);
+ const mEvent = room
+ .getLiveTimeline()
+ .getState(EventTimeline.FORWARDS)
+ ?.getStateEvents(StateEvent.RoomMember, mx.getSafeUserId());
+ const content = mEvent?.getContent();
+ if (!content) return;
+ await mx.sendStateEvent(
+ room.roomId,
+ StateEvent.RoomMember as any,
+ {
+ ...content,
+ displayname: nick,
+ },
+ mx.getSafeUserId()
+ );
},
},
[Command.MyRoomAvatar]: {
@@ -340,7 +380,21 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
description: 'Change profile picture in current room. Example /myroomavatar mxc://xyzabc',
exe: async (payload) => {
if (payload.match(/^mxc:\/\/\S+$/)) {
- roomActions.setMyRoomAvatar(mx, room.roomId, payload);
+ const mEvent = room
+ .getLiveTimeline()
+ .getState(EventTimeline.FORWARDS)
+ ?.getStateEvents(StateEvent.RoomMember, mx.getSafeUserId());
+ const content = mEvent?.getContent();
+ if (!content) return;
+ await mx.sendStateEvent(
+ room.roomId,
+ StateEvent.RoomMember as any,
+ {
+ ...content,
+ avatar_url: payload,
+ },
+ mx.getSafeUserId()
+ );
}
},
},
@@ -348,14 +402,15 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name: Command.ConvertToDm,
description: 'Convert room to direct message',
exe: async () => {
- roomActions.convertToDm(mx, room.roomId);
+ const dmUserId = guessDmRoomUserId(room, mx.getSafeUserId());
+ await addRoomIdToMDirect(mx, room.roomId, dmUserId);
},
},
[Command.ConvertToRoom]: {
name: Command.ConvertToRoom,
description: 'Convert direct message to room',
exe: async () => {
- roomActions.convertToRoom(mx, room.roomId);
+ await removeRoomIdFromMDirect(mx, room.roomId);
},
},
[Command.Delete]: {
diff --git a/src/app/hooks/useDateFormat.ts b/src/app/hooks/useDateFormat.ts
new file mode 100644
index 0000000000..520d4b0d50
--- /dev/null
+++ b/src/app/hooks/useDateFormat.ts
@@ -0,0 +1,34 @@
+import { useMemo } from 'react';
+import { DateFormat } from '../state/settings';
+
+export type DateFormatItem = {
+ name: string;
+ format: DateFormat;
+};
+
+export const useDateFormatItems = (): DateFormatItem[] =>
+ useMemo(
+ () => [
+ {
+ format: 'D MMM YYYY',
+ name: 'D MMM YYYY',
+ },
+ {
+ format: 'DD/MM/YYYY',
+ name: 'DD/MM/YYYY',
+ },
+ {
+ format: 'MM/DD/YYYY',
+ name: 'MM/DD/YYYY',
+ },
+ {
+ format: 'YYYY/MM/DD',
+ name: 'YYYY/MM/DD',
+ },
+ {
+ format: '',
+ name: 'Custom',
+ },
+ ],
+ []
+ );
diff --git a/src/app/hooks/useDirectUsers.ts b/src/app/hooks/useDirectUsers.ts
new file mode 100644
index 0000000000..3aa189280b
--- /dev/null
+++ b/src/app/hooks/useDirectUsers.ts
@@ -0,0 +1,27 @@
+import { useMemo } from 'react';
+import { AccountDataEvent, MDirectContent } from '../../types/matrix/accountData';
+import { useAccountData } from './useAccountData';
+import { useAllJoinedRoomsSet, useGetRoom } from './useGetRoom';
+
+export const useDirectUsers = (): string[] => {
+ const directEvent = useAccountData(AccountDataEvent.Direct);
+ const content = directEvent?.getContent();
+
+ const allJoinedRooms = useAllJoinedRoomsSet();
+ const getRoom = useGetRoom(allJoinedRooms);
+
+ const users = useMemo(() => {
+ if (typeof content !== 'object') return [];
+
+ const u = Object.keys(content).filter((userId) => {
+ const rooms = content[userId];
+ if (!Array.isArray(rooms)) return false;
+ const hasDM = rooms.some((roomId) => typeof roomId === 'string' && !!getRoom(roomId));
+ return hasDM;
+ });
+
+ return u;
+ }, [content, getRoom]);
+
+ return users;
+};
diff --git a/src/app/hooks/useListFocusIndex.ts b/src/app/hooks/useListFocusIndex.ts
new file mode 100644
index 0000000000..e4f3bf2c1d
--- /dev/null
+++ b/src/app/hooks/useListFocusIndex.ts
@@ -0,0 +1,36 @@
+import { useCallback, useState } from 'react';
+
+export const useListFocusIndex = (size: number, initialIndex: number) => {
+ const [index, setIndex] = useState(initialIndex);
+
+ const next = useCallback(() => {
+ setIndex((i) => {
+ const nextIndex = i + 1;
+ if (nextIndex >= size) {
+ return 0;
+ }
+ return nextIndex;
+ });
+ }, [size]);
+
+ const previous = useCallback(() => {
+ setIndex((i) => {
+ const previousIndex = i - 1;
+ if (previousIndex < 0) {
+ return size - 1;
+ }
+ return previousIndex;
+ });
+ }, [size]);
+
+ const reset = useCallback(() => {
+ setIndex(initialIndex);
+ }, [initialIndex]);
+
+ return {
+ index,
+ next,
+ previous,
+ reset,
+ };
+};
diff --git a/src/app/hooks/useMemberPowerCompare.ts b/src/app/hooks/useMemberPowerCompare.ts
new file mode 100644
index 0000000000..72163edfc5
--- /dev/null
+++ b/src/app/hooks/useMemberPowerCompare.ts
@@ -0,0 +1,28 @@
+import { useCallback } from 'react';
+import { IPowerLevels, readPowerLevel } from './usePowerLevels';
+
+export const useMemberPowerCompare = (creators: Set, powerLevels: IPowerLevels) => {
+ /**
+ * returns `true` if `userIdA` has more power than `userIdB`
+ * returns `false` otherwise
+ */
+ const hasMorePower = useCallback(
+ (userIdA: string, userIdB: string): boolean => {
+ const aIsCreator = creators.has(userIdA);
+ const bIsCreator = creators.has(userIdB);
+ if (aIsCreator && bIsCreator) return false;
+ if (aIsCreator) return true;
+ if (bIsCreator) return false;
+
+ const aPower = readPowerLevel.user(powerLevels, userIdA);
+ const bPower = readPowerLevel.user(powerLevels, userIdB);
+
+ return aPower > bPower;
+ },
+ [creators, powerLevels]
+ );
+
+ return {
+ hasMorePower,
+ };
+};
diff --git a/src/app/hooks/useMemberPowerTag.ts b/src/app/hooks/useMemberPowerTag.ts
new file mode 100644
index 0000000000..31e52aa01c
--- /dev/null
+++ b/src/app/hooks/useMemberPowerTag.ts
@@ -0,0 +1,87 @@
+import { useCallback, useMemo } from 'react';
+import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
+import { getPowerLevelTag, PowerLevelTags, usePowerLevelTags } from './usePowerLevelTags';
+import { IPowerLevels, readPowerLevel } from './usePowerLevels';
+import { MemberPowerTag, MemberPowerTagIcon } from '../../types/matrix/room';
+import { useRoomCreatorsTag } from './useRoomCreatorsTag';
+import { ThemeKind } from './useTheme';
+import { accessibleColor } from '../plugins/color';
+
+export type GetMemberPowerTag = (userId: string) => MemberPowerTag;
+
+export const useGetMemberPowerTag = (
+ room: Room,
+ creators: Set,
+ powerLevels: IPowerLevels
+) => {
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+
+ const getMemberPowerTag: GetMemberPowerTag = useCallback(
+ (userId) => {
+ if (creators.has(userId)) {
+ return creatorsTag;
+ }
+
+ const power = readPowerLevel.user(powerLevels, userId);
+ return getPowerLevelTag(powerLevelTags, power);
+ },
+ [creators, creatorsTag, powerLevels, powerLevelTags]
+ );
+
+ return getMemberPowerTag;
+};
+
+export const getPowerTagIconSrc = (
+ mx: MatrixClient,
+ useAuthentication: boolean,
+ icon: MemberPowerTagIcon
+): string | undefined =>
+ icon?.key?.startsWith('mxc://')
+ ? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
+ : icon?.key;
+
+export const useAccessiblePowerTagColors = (
+ themeKind: ThemeKind,
+ creatorsTag: MemberPowerTag,
+ powerLevelTags: PowerLevelTags
+): Map => {
+ const accessibleColors: Map = useMemo(() => {
+ const colors: Map = new Map();
+ if (creatorsTag.color) {
+ colors.set(creatorsTag.color, accessibleColor(themeKind, creatorsTag.color));
+ }
+
+ Object.values(powerLevelTags).forEach((tag) => {
+ const { color } = tag;
+ if (!color) return;
+
+ colors.set(color, accessibleColor(themeKind, color));
+ });
+
+ return colors;
+ }, [powerLevelTags, creatorsTag, themeKind]);
+
+ return accessibleColors;
+};
+
+export const useFlattenPowerTagMembers = (
+ members: RoomMember[],
+ getTag: GetMemberPowerTag
+): Array => {
+ const PLTagOrRoomMember = useMemo(() => {
+ let prevTag: MemberPowerTag | undefined;
+ const tagOrMember: Array = [];
+ members.forEach((member) => {
+ const tag = getTag(member.userId);
+ if (tag !== prevTag) {
+ prevTag = tag;
+ tagOrMember.push(tag);
+ }
+ tagOrMember.push(member);
+ });
+ return tagOrMember;
+ }, [members, getTag]);
+
+ return PLTagOrRoomMember;
+};
diff --git a/src/app/hooks/useMemberSort.ts b/src/app/hooks/useMemberSort.ts
index da95570161..d8e403c522 100644
--- a/src/app/hooks/useMemberSort.ts
+++ b/src/app/hooks/useMemberSort.ts
@@ -1,5 +1,5 @@
import { RoomMember } from 'matrix-js-sdk';
-import { useMemo } from 'react';
+import { useCallback, useMemo } from 'react';
export const MemberSort = {
Ascending: (a: RoomMember, b: RoomMember) =>
@@ -46,3 +46,20 @@ export const useMemberSort = (index: number, memberSort: MemberSortItem[]): Memb
const item = memberSort[index] ?? memberSort[0];
return item;
};
+
+export const useMemberPowerSort = (creators: Set): MemberSortFn => {
+ const sort: MemberSortFn = useCallback(
+ (a, b) => {
+ if (creators.has(a.userId) && creators.has(b.userId)) {
+ return 0;
+ }
+ if (creators.has(a.userId)) return -1;
+ if (creators.has(b.userId)) return 1;
+
+ return b.powerLevel - a.powerLevel;
+ },
+ [creators]
+ );
+
+ return sort;
+};
diff --git a/src/app/hooks/useMembership.ts b/src/app/hooks/useMembership.ts
new file mode 100644
index 0000000000..dbdd527e2b
--- /dev/null
+++ b/src/app/hooks/useMembership.ts
@@ -0,0 +1,28 @@
+import { useEffect, useState } from 'react';
+import { Room, RoomMemberEvent, RoomMemberEventHandlerMap } from 'matrix-js-sdk';
+import { Membership } from '../../types/matrix/room';
+
+export const useMembership = (room: Room, userId: string): Membership => {
+ const member = room.getMember(userId);
+
+ const [membership, setMembership] = useState(
+ () => (member?.membership as Membership | undefined) ?? Membership.Leave
+ );
+
+ useEffect(() => {
+ const handleMembershipChange: RoomMemberEventHandlerMap[RoomMemberEvent.Membership] = (
+ event,
+ m
+ ) => {
+ if (event.getRoomId() === room.roomId && m.userId === userId) {
+ setMembership((m.membership as Membership | undefined) ?? Membership.Leave);
+ }
+ };
+ member?.on(RoomMemberEvent.Membership, handleMembershipChange);
+ return () => {
+ member?.removeListener(RoomMemberEvent.Membership, handleMembershipChange);
+ };
+ }, [room, member, userId]);
+
+ return membership;
+};
diff --git a/src/app/hooks/useMentionClickHandler.ts b/src/app/hooks/useMentionClickHandler.ts
index 49c291c72e..9dc81d4e3c 100644
--- a/src/app/hooks/useMentionClickHandler.ts
+++ b/src/app/hooks/useMentionClickHandler.ts
@@ -3,14 +3,17 @@ 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';
+import { useOpenUserRoomProfile } from '../state/hooks/userRoomProfile';
+import { useSpaceOptionally } from './useSpace';
export const useMentionClickHandler = (roomId: string): ReactEventHandler => {
const mx = useMatrixClient();
const { navigateRoom, navigateSpace } = useRoomNavigate();
const navigate = useNavigate();
+ const openProfile = useOpenUserRoomProfile();
+ const space = useSpaceOptionally();
const handleClick: ReactEventHandler = useCallback(
(evt) => {
@@ -21,7 +24,7 @@ export const useMentionClickHandler = (roomId: string): ReactEventHandler(path, { viaServers }) : path);
},
- [mx, navigate, navigateRoom, navigateSpace, roomId]
+ [mx, navigate, navigateRoom, navigateSpace, roomId, space, openProfile]
);
return handleClick;
diff --git a/src/app/hooks/useMutualRooms.ts b/src/app/hooks/useMutualRooms.ts
new file mode 100644
index 0000000000..a7b3893894
--- /dev/null
+++ b/src/app/hooks/useMutualRooms.ts
@@ -0,0 +1,30 @@
+import { useCallback } from 'react';
+import { useMatrixClient } from './useMatrixClient';
+import { AsyncState, useAsyncCallbackValue } from './useAsyncCallback';
+import { useSpecVersions } from './useSpecVersions';
+
+export const useMutualRoomsSupport = (): boolean => {
+ const { unstable_features: unstableFeatures } = useSpecVersions();
+
+ const supported =
+ unstableFeatures?.['uk.half-shot.msc2666'] ||
+ unstableFeatures?.['uk.half-shot.msc2666.mutual_rooms'] ||
+ unstableFeatures?.['uk.half-shot.msc2666.query_mutual_rooms'];
+
+ return !!supported;
+};
+
+export const useMutualRooms = (userId: string): AsyncState => {
+ const mx = useMatrixClient();
+
+ const supported = useMutualRoomsSupport();
+
+ const [mutualRoomsState] = useAsyncCallbackValue(
+ useCallback(
+ () => (supported ? mx._unstable_getSharedRooms(userId) : Promise.resolve([])),
+ [mx, userId, supported]
+ )
+ );
+
+ return mutualRoomsState;
+};
diff --git a/src/app/hooks/usePowerLevelTags.ts b/src/app/hooks/usePowerLevelTags.ts
index bdcb9bcc27..0a6cca508b 100644
--- a/src/app/hooks/usePowerLevelTags.ts
+++ b/src/app/hooks/usePowerLevelTags.ts
@@ -1,29 +1,24 @@
-import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
-import { useCallback, useMemo } from 'react';
+import { Room } from 'matrix-js-sdk';
+import { useMemo } from 'react';
import { IPowerLevels } from './usePowerLevels';
import { useStateEvent } from './useStateEvent';
-import { StateEvent } from '../../types/matrix/room';
-import { IImageInfo } from '../../types/matrix/common';
-import { ThemeKind } from './useTheme';
-import { accessibleColor } from '../plugins/color';
-
-export type PowerLevelTagIcon = {
- key?: string;
- info?: IImageInfo;
-};
-export type PowerLevelTag = {
- name: string;
- color?: string;
- icon?: PowerLevelTagIcon;
-};
+import { MemberPowerTag, StateEvent } from '../../types/matrix/room';
-export type PowerLevelTags = Record;
+export type PowerLevelTags = Record;
-export const powerSortFn = (a: number, b: number) => b - a;
-export const sortPowers = (powers: number[]): number[] => powers.sort(powerSortFn);
+const powerSortFn = (a: number, b: number) => b - a;
+const sortPowers = (powers: number[]): number[] => powers.sort(powerSortFn);
export const getPowers = (tags: PowerLevelTags): number[] => {
- const powers: number[] = Object.keys(tags).map((p) => parseInt(p, 10));
+ const powers: number[] = Object.keys(tags)
+ .map((p) => {
+ const power = parseInt(p, 10);
+ if (Number.isNaN(power)) {
+ return undefined;
+ }
+ return power;
+ })
+ .filter((power) => typeof power === 'number');
return sortPowers(powers);
};
@@ -55,8 +50,8 @@ const DEFAULT_TAGS: PowerLevelTags = {
name: 'Goku',
color: '#ff6a00',
},
- 102: {
- name: 'Goku Reborn',
+ 150: {
+ name: 'Manager',
color: '#ff6a7f',
},
101: {
@@ -81,7 +76,7 @@ const DEFAULT_TAGS: PowerLevelTags = {
},
};
-const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): PowerLevelTag => {
+const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): MemberPowerTag => {
const highToLow = sortPowers(getPowers(powerLevelTags));
const tagPower = highToLow.find((p) => p < power);
@@ -92,12 +87,7 @@ const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): Pow
};
};
-export type GetPowerLevelTag = (powerLevel: number) => PowerLevelTag;
-
-export const usePowerLevelTags = (
- room: Room,
- powerLevels: IPowerLevels
-): [PowerLevelTags, GetPowerLevelTag] => {
+export const usePowerLevelTags = (room: Room, powerLevels: IPowerLevels): PowerLevelTags => {
const tagsEvent = useStateEvent(room, StateEvent.PowerLevelTags);
const powerLevelTags: PowerLevelTags = useMemo(() => {
@@ -114,66 +104,13 @@ export const usePowerLevelTags = (
return powerToTags;
}, [powerLevels, tagsEvent]);
- const getTag: GetPowerLevelTag = useCallback(
- (power) => {
- const tag: PowerLevelTag | undefined = powerLevelTags[power];
- return tag ?? generateFallbackTag(DEFAULT_TAGS, power);
- },
- [powerLevelTags]
- );
-
- return [powerLevelTags, getTag];
+ return powerLevelTags;
};
-export const useFlattenPowerLevelTagMembers = (
- members: RoomMember[],
- getPowerLevel: (userId: string) => number,
- getTag: GetPowerLevelTag
-): Array => {
- const PLTagOrRoomMember = useMemo(() => {
- let prevTag: PowerLevelTag | undefined;
- const tagOrMember: Array = [];
- members.forEach((member) => {
- const memberPL = getPowerLevel(member.userId);
- const tag = getTag(memberPL);
- if (tag !== prevTag) {
- prevTag = tag;
- tagOrMember.push(tag);
- }
- tagOrMember.push(member);
- });
- return tagOrMember;
- }, [members, getTag, getPowerLevel]);
-
- return PLTagOrRoomMember;
-};
-
-export const getTagIconSrc = (
- mx: MatrixClient,
- useAuthentication: boolean,
- icon: PowerLevelTagIcon
-): string | undefined =>
- icon?.key?.startsWith('mxc://')
- ? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
- : icon?.key;
-
-export const useAccessibleTagColors = (
- themeKind: ThemeKind,
- powerLevelTags: PowerLevelTags
-): Map => {
- const accessibleColors: Map = useMemo(() => {
- const colors: Map = new Map();
-
- getPowers(powerLevelTags).forEach((power) => {
- const tag = powerLevelTags[power];
- const { color } = tag;
- if (!color) return;
-
- colors.set(color, accessibleColor(themeKind, color));
- });
-
- return colors;
- }, [powerLevelTags, themeKind]);
-
- return accessibleColors;
+export const getPowerLevelTag = (
+ powerLevelTags: PowerLevelTags,
+ powerLevel: number
+): MemberPowerTag => {
+ const tag: MemberPowerTag | undefined = powerLevelTags[powerLevel];
+ return tag ?? generateFallbackTag(powerLevelTags, powerLevel);
};
diff --git a/src/app/hooks/usePowerLevels.ts b/src/app/hooks/usePowerLevels.ts
index 8bf8b74743..0281b23c31 100644
--- a/src/app/hooks/usePowerLevels.ts
+++ b/src/app/hooks/usePowerLevels.ts
@@ -58,10 +58,11 @@ const fillMissingPowers = (powerLevels: IPowerLevels): IPowerLevels =>
});
const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
- const pl = mEvent?.getContent();
- if (!pl) return DEFAULT_POWER_LEVELS;
+ const plContent = mEvent?.getContent();
- return fillMissingPowers(pl);
+ const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent);
+
+ return powerLevels;
};
export function usePowerLevels(room: Room): IPowerLevels {
@@ -120,33 +121,8 @@ export const useRoomsPowerLevels = (rooms: Room[]): Map =>
return roomToPowerLevels;
};
-export type GetPowerLevel = (powerLevels: IPowerLevels, userId: string | undefined) => number;
-export type CanSend = (
- powerLevels: IPowerLevels,
- eventType: string | undefined,
- powerLevel: number
-) => boolean;
-export type CanDoAction = (
- powerLevels: IPowerLevels,
- action: PowerLevelActions,
- powerLevel: number
-) => boolean;
-export type CanDoNotificationAction = (
- powerLevels: IPowerLevels,
- action: PowerLevelNotificationsAction,
- powerLevel: number
-) => boolean;
-
-export type PowerLevelsAPI = {
- getPowerLevel: GetPowerLevel;
- canSendEvent: CanSend;
- canSendStateEvent: CanSend;
- canDoAction: CanDoAction;
- canDoNotificationAction: CanDoNotificationAction;
-};
-
export type ReadPowerLevelAPI = {
- user: GetPowerLevel;
+ user: (powerLevels: IPowerLevels, userId: string | undefined) => number;
event: (powerLevels: IPowerLevels, eventType: string | undefined) => number;
state: (powerLevels: IPowerLevels, eventType: string | undefined) => number;
action: (powerLevels: IPowerLevels, action: PowerLevelActions) => number;
@@ -156,6 +132,7 @@ export type ReadPowerLevelAPI = {
export const readPowerLevel: ReadPowerLevelAPI = {
user: (powerLevels, userId) => {
const { users_default: usersDefault, users } = powerLevels;
+
if (userId && users && typeof users[userId] === 'number') {
return users[userId];
}
@@ -191,63 +168,13 @@ export const readPowerLevel: ReadPowerLevelAPI = {
},
};
-export const powerLevelAPI: PowerLevelsAPI = {
- getPowerLevel: (powerLevels, userId) => readPowerLevel.user(powerLevels, userId),
- canSendEvent: (powerLevels, eventType, powerLevel) => {
- const requiredPL = readPowerLevel.event(powerLevels, eventType);
- return powerLevel >= requiredPL;
- },
- canSendStateEvent: (powerLevels, eventType, powerLevel) => {
- const requiredPL = readPowerLevel.state(powerLevels, eventType);
- return powerLevel >= requiredPL;
- },
- canDoAction: (powerLevels, action, powerLevel) => {
- const requiredPL = readPowerLevel.action(powerLevels, action);
- return powerLevel >= requiredPL;
- },
- canDoNotificationAction: (powerLevels, action, powerLevel) => {
- const requiredPL = readPowerLevel.notification(powerLevels, action);
- return powerLevel >= requiredPL;
- },
-};
-
-export const usePowerLevelsAPI = (powerLevels: IPowerLevels) => {
- const getPowerLevel = useCallback(
- (userId: string | undefined) => powerLevelAPI.getPowerLevel(powerLevels, userId),
- [powerLevels]
- );
-
- const canSendEvent = useCallback(
- (eventType: string | undefined, powerLevel: number) =>
- powerLevelAPI.canSendEvent(powerLevels, eventType, powerLevel),
- [powerLevels]
- );
-
- const canSendStateEvent = useCallback(
- (eventType: string | undefined, powerLevel: number) =>
- powerLevelAPI.canSendStateEvent(powerLevels, eventType, powerLevel),
- [powerLevels]
- );
-
- const canDoAction = useCallback(
- (action: PowerLevelActions, powerLevel: number) =>
- powerLevelAPI.canDoAction(powerLevels, action, powerLevel),
- [powerLevels]
- );
-
- const canDoNotificationAction = useCallback(
- (action: PowerLevelNotificationsAction, powerLevel: number) =>
- powerLevelAPI.canDoNotificationAction(powerLevels, action, powerLevel),
+export const useGetMemberPowerLevel = (powerLevels: IPowerLevels) => {
+ const callback = useCallback(
+ (userId?: string): number => readPowerLevel.user(powerLevels, userId),
[powerLevels]
);
- return {
- getPowerLevel,
- canSendEvent,
- canSendStateEvent,
- canDoAction,
- canDoNotificationAction,
- };
+ return callback;
};
/**
diff --git a/src/app/hooks/useRoomCreators.ts b/src/app/hooks/useRoomCreators.ts
new file mode 100644
index 0000000000..269d11afdc
--- /dev/null
+++ b/src/app/hooks/useRoomCreators.ts
@@ -0,0 +1,49 @@
+import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
+import { useMemo } from 'react';
+import { useStateEvent } from './useStateEvent';
+import { IRoomCreateContent, StateEvent } from '../../types/matrix/room';
+import { creatorsSupported } from '../utils/matrix';
+import { getStateEvent } from '../utils/room';
+
+export const getRoomCreators = (createEvent: MatrixEvent): Set => {
+ const createContent = createEvent.getContent();
+
+ const creators: Set = new Set();
+
+ if (!creatorsSupported(createContent.room_version)) return creators;
+
+ if (createEvent.event.sender) {
+ creators.add(createEvent.event.sender);
+ }
+
+ if ('additional_creators' in createContent && Array.isArray(createContent.additional_creators)) {
+ createContent.additional_creators.forEach((creator) => {
+ if (typeof creator === 'string') {
+ creators.add(creator);
+ }
+ });
+ }
+
+ return creators;
+};
+
+export const useRoomCreators = (room: Room): Set => {
+ const createEvent = useStateEvent(room, StateEvent.RoomCreate);
+
+ const creators = useMemo(
+ () => (createEvent ? getRoomCreators(createEvent) : new Set()),
+ [createEvent]
+ );
+
+ return creators;
+};
+
+export const getRoomCreatorsForRoomId = (mx: MatrixClient, roomId: string): Set => {
+ const room = mx.getRoom(roomId);
+ if (!room) return new Set();
+
+ const createEvent = getStateEvent(room, StateEvent.RoomCreate);
+ if (!createEvent) return new Set();
+
+ return getRoomCreators(createEvent);
+};
diff --git a/src/app/hooks/useRoomCreatorsTag.ts b/src/app/hooks/useRoomCreatorsTag.ts
new file mode 100644
index 0000000000..2d6db0eca6
--- /dev/null
+++ b/src/app/hooks/useRoomCreatorsTag.ts
@@ -0,0 +1,8 @@
+import { MemberPowerTag } from '../../types/matrix/room';
+
+const DEFAULT_TAG: MemberPowerTag = {
+ name: 'Founder',
+ color: '#0000ff',
+};
+
+export const useRoomCreatorsTag = (): MemberPowerTag => DEFAULT_TAG;
diff --git a/src/app/hooks/useRoomEventReaders.ts b/src/app/hooks/useRoomEventReaders.ts
index 6222bf92d9..f634e58676 100644
--- a/src/app/hooks/useRoomEventReaders.ts
+++ b/src/app/hooks/useRoomEventReaders.ts
@@ -21,7 +21,6 @@ const getEventReaders = (room: Room, evtId?: string) => {
export const useRoomEventReaders = (room: Room, eventId?: string): string[] => {
const [readers, setReaders] = useState(() => getEventReaders(room, eventId));
-
useEffect(() => {
setReaders(getEventReaders(room, eventId));
@@ -46,6 +45,7 @@ export const useRoomEventReaders = (room: Room, eventId?: string): string[] => {
room.on(RoomEvent.Receipt, handleReceipt);
room.on(RoomEvent.LocalEchoUpdated, handleLocalEcho);
+
return () => {
room.removeListener(RoomEvent.Receipt, handleReceipt);
room.removeListener(RoomEvent.LocalEchoUpdated, handleLocalEcho);
diff --git a/src/app/hooks/useRoomNavigate.ts b/src/app/hooks/useRoomNavigate.ts
index e626c06bff..b2d7a91a18 100644
--- a/src/app/hooks/useRoomNavigate.ts
+++ b/src/app/hooks/useRoomNavigate.ts
@@ -9,7 +9,7 @@ import {
getSpaceRoomPath,
} from '../pages/pathUtils';
import { useMatrixClient } from './useMatrixClient';
-import { getOrphanParents } from '../utils/room';
+import { getOrphanParents, guessPerfectParent } from '../utils/room';
import { roomToParentsAtom } from '../state/room/roomToParents';
import { mDirectAtom } from '../state/mDirectList';
import { useSelectedSpace } from './router/useSelectedSpace';
@@ -39,19 +39,19 @@ export const useRoomNavigate = () => {
const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
if (orphanParents.length > 0) {
- const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
- mx,
- spaceSelectedId && orphanParents.includes(spaceSelectedId)
- ? spaceSelectedId
- : orphanParents[0] // TODO: better orphan parent selection.
- );
-
- if (openSpaceTimeline) {
- navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomId, eventId), opts);
- return;
+ let parentSpace: string;
+ if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) {
+ parentSpace = spaceSelectedId;
+ } else {
+ parentSpace = guessPerfectParent(mx, roomId, orphanParents) ?? orphanParents[0];
}
- navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
+ const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
+
+ navigate(
+ getSpaceRoomPath(pSpaceIdOrAlias, openSpaceTimeline ? roomId : roomIdOrAlias, eventId),
+ opts
+ );
return;
}
diff --git a/src/app/hooks/useRoomPermissions.ts b/src/app/hooks/useRoomPermissions.ts
new file mode 100644
index 0000000000..cb6f69a259
--- /dev/null
+++ b/src/app/hooks/useRoomPermissions.ts
@@ -0,0 +1,60 @@
+import { useMemo } from 'react';
+import {
+ IPowerLevels,
+ PowerLevelActions,
+ PowerLevelNotificationsAction,
+ readPowerLevel,
+} from './usePowerLevels';
+
+export type RoomPermissionsAPI = {
+ event: (type: string, userId: string) => boolean;
+ stateEvent: (type: string, userId: string) => boolean;
+ action: (action: PowerLevelActions, userId: string) => boolean;
+ notificationAction: (action: PowerLevelNotificationsAction, userId: string) => boolean;
+};
+
+export const getRoomPermissionsAPI = (
+ creators: Set,
+ powerLevels: IPowerLevels
+): RoomPermissionsAPI => {
+ const api: RoomPermissionsAPI = {
+ event: (type, userId) => {
+ if (creators.has(userId)) return true;
+ const userPower = readPowerLevel.user(powerLevels, userId);
+ const requiredPL = readPowerLevel.event(powerLevels, type);
+ return userPower >= requiredPL;
+ },
+ stateEvent: (type, userId) => {
+ if (creators.has(userId)) return true;
+ const userPower = readPowerLevel.user(powerLevels, userId);
+ const requiredPL = readPowerLevel.state(powerLevels, type);
+ return userPower >= requiredPL;
+ },
+ action: (action, userId) => {
+ if (creators.has(userId)) return true;
+ const userPower = readPowerLevel.user(powerLevels, userId);
+ const requiredPL = readPowerLevel.action(powerLevels, action);
+ return userPower >= requiredPL;
+ },
+ notificationAction: (action, userId) => {
+ if (creators.has(userId)) return true;
+ const userPower = readPowerLevel.user(powerLevels, userId);
+ const requiredPL = readPowerLevel.notification(powerLevels, action);
+ return userPower >= requiredPL;
+ },
+ };
+
+ return api;
+};
+
+export const useRoomPermissions = (
+ creators: Set,
+ powerLevels: IPowerLevels
+): RoomPermissionsAPI => {
+ const api: RoomPermissionsAPI = useMemo(
+ () => getRoomPermissionsAPI(creators, powerLevels),
+ [creators, powerLevels]
+ );
+
+ return api;
+};
diff --git a/src/app/hooks/useStore.js b/src/app/hooks/useStore.js
deleted file mode 100644
index f216406b3e..0000000000
--- a/src/app/hooks/useStore.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/* eslint-disable import/prefer-default-export */
-import { useEffect, useRef } from 'react';
-
-export function useStore(...args) {
- const itemRef = useRef(null);
-
- const getItem = () => itemRef.current;
-
- const setItem = (event) => {
- itemRef.current = event;
- return itemRef.current;
- };
-
- useEffect(() => {
- itemRef.current = null;
- return () => {
- itemRef.current = null;
- };
- }, args);
-
- return { getItem, setItem };
-}
diff --git a/src/app/hooks/useTimeoutToggle.ts b/src/app/hooks/useTimeoutToggle.ts
new file mode 100644
index 0000000000..7eda99c173
--- /dev/null
+++ b/src/app/hooks/useTimeoutToggle.ts
@@ -0,0 +1,37 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+/**
+ * Temporarily sets a boolean state.
+ *
+ * @param duration - Duration in milliseconds before resetting (default: 1500)
+ * @param initial - Initial value (default: false)
+ */
+export function useTimeoutToggle(duration = 1500, initial = false): [boolean, () => void] {
+ const [active, setActive] = useState(initial);
+ const timeoutRef = useRef(null);
+
+ const clear = () => {
+ if (timeoutRef.current !== null) {
+ clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ }
+ };
+
+ const trigger = useCallback(() => {
+ setActive(!initial);
+ clear();
+ timeoutRef.current = window.setTimeout(() => {
+ setActive(initial);
+ timeoutRef.current = null;
+ }, duration);
+ }, [duration, initial]);
+
+ useEffect(
+ () => () => {
+ clear();
+ },
+ []
+ );
+
+ return [active, trigger];
+}
diff --git a/src/app/hooks/useUserPresence.ts b/src/app/hooks/useUserPresence.ts
new file mode 100644
index 0000000000..5137950edc
--- /dev/null
+++ b/src/app/hooks/useUserPresence.ts
@@ -0,0 +1,58 @@
+import { useEffect, useMemo, useState } from 'react';
+import { User, UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
+import { useMatrixClient } from './useMatrixClient';
+
+export enum Presence {
+ Online = 'online',
+ Unavailable = 'unavailable',
+ Offline = 'offline',
+}
+
+export type UserPresence = {
+ presence: Presence;
+ status?: string;
+ active: boolean;
+ lastActiveTs?: number;
+};
+
+const getUserPresence = (user: User): UserPresence => ({
+ presence: user.presence as Presence,
+ status: user.presenceStatusMsg,
+ active: user.currentlyActive,
+ lastActiveTs: user.getLastActiveTs(),
+});
+
+export const useUserPresence = (userId: string): UserPresence | undefined => {
+ const mx = useMatrixClient();
+ const user = mx.getUser(userId);
+
+ const [presence, setPresence] = useState(() => (user ? getUserPresence(user) : undefined));
+
+ useEffect(() => {
+ const updatePresence: UserEventHandlerMap[UserEvent.Presence] = (event, u) => {
+ if (u.userId === user?.userId) {
+ setPresence(getUserPresence(user));
+ }
+ };
+ user?.on(UserEvent.Presence, updatePresence);
+ user?.on(UserEvent.CurrentlyActive, updatePresence);
+ user?.on(UserEvent.LastPresenceTs, updatePresence);
+ return () => {
+ user?.removeListener(UserEvent.Presence, updatePresence);
+ user?.removeListener(UserEvent.CurrentlyActive, updatePresence);
+ user?.removeListener(UserEvent.LastPresenceTs, updatePresence);
+ };
+ }, [user]);
+
+ return presence;
+};
+
+export const usePresenceLabel = (): Record =>
+ useMemo(
+ () => ({
+ [Presence.Online]: 'Active',
+ [Presence.Unavailable]: 'Busy',
+ [Presence.Offline]: 'Away',
+ }),
+ []
+ );
diff --git a/src/app/molecules/confirm-dialog/ConfirmDialog.jsx b/src/app/molecules/confirm-dialog/ConfirmDialog.jsx
deleted file mode 100644
index 5771f2c1e2..0000000000
--- a/src/app/molecules/confirm-dialog/ConfirmDialog.jsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './ConfirmDialog.scss';
-
-import { openReusableDialog } from '../../../client/action/navigation';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-
-function ConfirmDialog({
- desc, actionTitle, actionType, onComplete,
-}) {
- return (
-
-
{desc}
-
- onComplete(true)}>{actionTitle}
- onComplete(false)}>Cancel
-
-
- );
-}
-ConfirmDialog.propTypes = {
- desc: PropTypes.string.isRequired,
- actionTitle: PropTypes.string.isRequired,
- actionType: PropTypes.oneOf(['primary', 'positive', 'danger', 'caution']).isRequired,
- onComplete: PropTypes.func.isRequired,
-};
-
-/**
- * @param {string} title title of confirm dialog
- * @param {string} desc description of confirm dialog
- * @param {string} actionTitle title of main action to take
- * @param {'primary' | 'positive' | 'danger' | 'caution'} actionType type of action. default=primary
- * @return {Promise} does it get's confirmed or not
- */
-// eslint-disable-next-line import/prefer-default-export
-export const confirmDialog = (title, desc, actionTitle, actionType = 'primary') => new Promise((resolve) => {
- let isCompleted = false;
- openReusableDialog(
- {title},
- (requestClose) => (
- {
- isCompleted = true;
- resolve(isConfirmed);
- requestClose();
- }}
- />
- ),
- () => {
- if (!isCompleted) resolve(false);
- },
- );
-});
diff --git a/src/app/molecules/confirm-dialog/ConfirmDialog.scss b/src/app/molecules/confirm-dialog/ConfirmDialog.scss
deleted file mode 100644
index 05f88be145..0000000000
--- a/src/app/molecules/confirm-dialog/ConfirmDialog.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-.confirm-dialog {
- padding: var(--sp-normal);
-
- & > .text {
- padding-bottom: var(--sp-normal);
- }
- &__btn {
- display: flex;
- gap: var(--sp-normal);
- }
-}
\ No newline at end of file
diff --git a/src/app/molecules/dialog/Dialog.jsx b/src/app/molecules/dialog/Dialog.jsx
deleted file mode 100644
index 478a085585..0000000000
--- a/src/app/molecules/dialog/Dialog.jsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './Dialog.scss';
-
-import Text from '../../atoms/text/Text';
-import Header, { TitleWrapper } from '../../atoms/header/Header';
-import ScrollView from '../../atoms/scroll/ScrollView';
-import RawModal from '../../atoms/modal/RawModal';
-
-function Dialog({
- className,
- isOpen,
- title,
- onAfterOpen,
- onAfterClose,
- contentOptions,
- onRequestClose,
- closeFromOutside,
- children,
- invisibleScroll,
-}) {
- return (
-
-
-
-
-
- {typeof title === 'string' ? (
-
- {title}
-
- ) : (
- title
- )}
-
- {contentOptions}
-
-
-
-
-
- );
-}
-
-Dialog.defaultProps = {
- className: null,
- contentOptions: null,
- onAfterOpen: null,
- onAfterClose: null,
- onRequestClose: null,
- closeFromOutside: true,
- invisibleScroll: false,
-};
-
-Dialog.propTypes = {
- className: PropTypes.string,
- isOpen: PropTypes.bool.isRequired,
- title: PropTypes.node.isRequired,
- contentOptions: PropTypes.node,
- onAfterOpen: PropTypes.func,
- onAfterClose: PropTypes.func,
- onRequestClose: PropTypes.func,
- closeFromOutside: PropTypes.bool,
- children: PropTypes.node.isRequired,
- invisibleScroll: PropTypes.bool,
-};
-
-export default Dialog;
diff --git a/src/app/molecules/dialog/Dialog.scss b/src/app/molecules/dialog/Dialog.scss
deleted file mode 100644
index 269d909e1b..0000000000
--- a/src/app/molecules/dialog/Dialog.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-.dialog-modal {
- --modal-height: 656px;
- max-height: min(100%, var(--modal-height));
- display: flex;
-}
-
-.dialog,
-.dialog__content,
-.dialog__content__wrapper {
- flex: 1;
- min-height: 0;
- min-width: 0;
-
- display: flex;
-}
-
-.dialog {
- background-color: var(--bg-surface);
-
- &__content {
- flex-direction: column;
- }
-}
diff --git a/src/app/molecules/dialog/ReusableDialog.jsx b/src/app/molecules/dialog/ReusableDialog.jsx
deleted file mode 100644
index 7340e119b1..0000000000
--- a/src/app/molecules/dialog/ReusableDialog.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { useState, useEffect } from 'react';
-
-import cons from '../../../client/state/cons';
-
-import navigation from '../../../client/state/navigation';
-import IconButton from '../../atoms/button/IconButton';
-import Dialog from './Dialog';
-
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-
-function ReusableDialog() {
- const [isOpen, setIsOpen] = useState(false);
- const [data, setData] = useState(null);
-
- useEffect(() => {
- const handleOpen = (title, render, afterClose) => {
- setIsOpen(true);
- setData({ title, render, afterClose });
- };
- navigation.on(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
- return () => {
- navigation.removeListener(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
- };
- }, []);
-
- const handleAfterClose = () => {
- data.afterClose?.();
- setData(null);
- };
-
- const handleRequestClose = () => {
- setIsOpen(false);
- };
-
- return (
- }
- invisibleScroll
- >
- {data?.render(handleRequestClose) || }
-
- );
-}
-
-export default ReusableDialog;
diff --git a/src/app/molecules/image-upload/ImageUpload.jsx b/src/app/molecules/image-upload/ImageUpload.jsx
deleted file mode 100644
index 5213381f8c..0000000000
--- a/src/app/molecules/image-upload/ImageUpload.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React, { useState, useRef } from 'react';
-import PropTypes from 'prop-types';
-import './ImageUpload.scss';
-
-
-import Text from '../../atoms/text/Text';
-import Avatar from '../../atoms/avatar/Avatar';
-import Spinner from '../../atoms/spinner/Spinner';
-import RawIcon from '../../atoms/system-icons/RawIcon';
-
-import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function ImageUpload({
- text, bgColor, imageSrc, onUpload, onRequestRemove,
- size,
-}) {
- const [uploadPromise, setUploadPromise] = useState(null);
- const uploadImageRef = useRef(null);
- const mx = useMatrixClient();
-
- async function uploadImage(e) {
- const file = e.target.files.item(0);
- if (file === null) return;
- try {
- const uPromise = mx.uploadContent(file);
- setUploadPromise(uPromise);
-
- const res = await uPromise;
- if (typeof res?.content_uri === 'string') onUpload(res.content_uri);
- setUploadPromise(null);
- } catch {
- setUploadPromise(null);
- }
- uploadImageRef.current.value = null;
- }
-
- function cancelUpload() {
- mx.cancelUpload(uploadPromise);
- setUploadPromise(null);
- uploadImageRef.current.value = null;
- }
-
- return (
-
-
{
- if (uploadPromise !== null) return;
- uploadImageRef.current.click();
- }}
- >
-
-
- {uploadPromise === null && (
- size === 'large'
- ? Upload
- :
- )}
- {uploadPromise !== null && }
-
-
- { (typeof imageSrc === 'string' || uploadPromise !== null) && (
-
- {uploadPromise ? 'Cancel' : 'Remove'}
-
- )}
-
-
- );
-}
-
-ImageUpload.defaultProps = {
- text: null,
- bgColor: 'transparent',
- imageSrc: null,
- size: 'large',
-};
-
-ImageUpload.propTypes = {
- text: PropTypes.string,
- bgColor: PropTypes.string,
- imageSrc: PropTypes.string,
- onUpload: PropTypes.func.isRequired,
- onRequestRemove: PropTypes.func.isRequired,
- size: PropTypes.oneOf(['large', 'normal']),
-};
-
-export default ImageUpload;
diff --git a/src/app/molecules/image-upload/ImageUpload.scss b/src/app/molecules/image-upload/ImageUpload.scss
deleted file mode 100644
index 3ae38bc05d..0000000000
--- a/src/app/molecules/image-upload/ImageUpload.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-.img-upload__wrapper {
- display: flex;
- flex-direction: column;
- align-items: center;
-}
-
-.img-upload {
- display: flex;
- cursor: pointer;
- position: relative;
-
- &__process {
- width: 100%;
- height: 100%;
- border-radius: var(--bo-radius);
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: rgba(0, 0, 0, .6);
-
- position: absolute;
- left: 0;
- right: 0;
- z-index: 1;
- & .text {
- text-transform: uppercase;
- color: white;
- }
- &--stopped {
- display: none;
- }
- & .donut-spinner {
- border-color: rgb(255, 255, 255, .3);
- border-left-color: white;
- }
- }
- &:hover .img-upload__process--stopped {
- display: flex;
- }
-
-
- &__btn-cancel {
- margin-top: var(--sp-extra-tight);
- cursor: pointer;
- & .text {
- color: var(--tc-danger-normal)
- }
- }
-}
diff --git a/src/app/molecules/people-selector/PeopleSelector.jsx b/src/app/molecules/people-selector/PeopleSelector.jsx
deleted file mode 100644
index 7025aa7c4e..0000000000
--- a/src/app/molecules/people-selector/PeopleSelector.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './PeopleSelector.scss';
-
-import { blurOnBubbling } from '../../atoms/button/script';
-
-import Text from '../../atoms/text/Text';
-import Avatar from '../../atoms/avatar/Avatar';
-
-function PeopleSelector({ avatarSrc, name, color, peopleRole, onClick }) {
- return (
-
-
blurOnBubbling(e, '.people-selector')}
- onClick={onClick}
- type="button"
- >
-
-
- {name}
-
- {peopleRole !== null && (
-
- {peopleRole}
-
- )}
-
-
- );
-}
-
-PeopleSelector.defaultProps = {
- avatarSrc: null,
- peopleRole: null,
-};
-
-PeopleSelector.propTypes = {
- avatarSrc: PropTypes.string,
- name: PropTypes.string.isRequired,
- color: PropTypes.string.isRequired,
- peopleRole: PropTypes.string,
- onClick: PropTypes.func.isRequired,
-};
-
-export default PeopleSelector;
diff --git a/src/app/molecules/people-selector/PeopleSelector.scss b/src/app/molecules/people-selector/PeopleSelector.scss
deleted file mode 100644
index 65907e6c61..0000000000
--- a/src/app/molecules/people-selector/PeopleSelector.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-@use '../../partials/text';
-
-.people-selector {
- width: 100%;
- padding: var(--sp-extra-tight) var(--sp-normal);
- display: flex;
- align-items: center;
- cursor: pointer;
-
- &__container {
- display: flex;
- }
-
- @media (hover: hover) {
- &:hover {
- background-color: var(--bg-surface-hover);
- }
- }
- &:focus {
- outline: none;
- background-color: var(--bg-surface-hover);
- }
- &:active {
- background-color: var(--bg-surface-active);
- }
-
- &__name {
- @extend .cp-txt__ellipsis;
- flex: 1;
- min-width: 0;
- margin: 0 var(--sp-tight);
- color: var(--tc-surface-normal);
- }
- &__role {
- color: var(--tc-surface-low);
- }
-}
\ No newline at end of file
diff --git a/src/app/molecules/popup-window/PopupWindow.jsx b/src/app/molecules/popup-window/PopupWindow.jsx
deleted file mode 100644
index 55872d6aee..0000000000
--- a/src/app/molecules/popup-window/PopupWindow.jsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './PopupWindow.scss';
-
-import Text from '../../atoms/text/Text';
-import IconButton from '../../atoms/button/IconButton';
-import { MenuItem } from '../../atoms/context-menu/ContextMenu';
-import Header, { TitleWrapper } from '../../atoms/header/Header';
-import ScrollView from '../../atoms/scroll/ScrollView';
-import RawModal from '../../atoms/modal/RawModal';
-
-import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
-
-function PWContentSelector({ selected, variant, iconSrc, type, onClick, children }) {
- const pwcsClass = selected ? ' pw-content-selector--selected' : '';
- return (
-
-
-
- );
-}
-
-PWContentSelector.defaultProps = {
- selected: false,
- variant: 'surface',
- iconSrc: 'none',
- type: 'button',
-};
-
-PWContentSelector.propTypes = {
- selected: PropTypes.bool,
- variant: PropTypes.oneOf(['surface', 'caution', 'danger']),
- iconSrc: PropTypes.string,
- type: PropTypes.oneOf(['button', 'submit']),
- onClick: PropTypes.func.isRequired,
- children: PropTypes.string.isRequired,
-};
-
-function PopupWindow({
- className,
- isOpen,
- title,
- contentTitle,
- drawer,
- drawerOptions,
- contentOptions,
- onAfterClose,
- onRequestClose,
- children,
-}) {
- const haveDrawer = drawer !== null;
- const cTitle = contentTitle !== null ? contentTitle : title;
-
- return (
-
-
- {haveDrawer && (
-
-
-
-
- {typeof title === 'string' ? (
-
- {title}
-
- ) : (
- title
- )}
-
- {drawerOptions}
-
-
-
- )}
-
-
-
- {typeof cTitle === 'string' ? (
-
- {cTitle}
-
- ) : (
- cTitle
- )}
-
- {contentOptions}
-
-
-
-
-
- );
-}
-
-PopupWindow.defaultProps = {
- className: null,
- drawer: null,
- contentTitle: null,
- drawerOptions: null,
- contentOptions: null,
- onAfterClose: null,
- onRequestClose: null,
-};
-
-PopupWindow.propTypes = {
- className: PropTypes.string,
- isOpen: PropTypes.bool.isRequired,
- title: PropTypes.node.isRequired,
- contentTitle: PropTypes.node,
- drawer: PropTypes.node,
- drawerOptions: PropTypes.node,
- contentOptions: PropTypes.node,
- onAfterClose: PropTypes.func,
- onRequestClose: PropTypes.func,
- children: PropTypes.node.isRequired,
-};
-
-export { PopupWindow as default, PWContentSelector };
diff --git a/src/app/molecules/popup-window/PopupWindow.scss b/src/app/molecules/popup-window/PopupWindow.scss
deleted file mode 100644
index 3251b02d7e..0000000000
--- a/src/app/molecules/popup-window/PopupWindow.scss
+++ /dev/null
@@ -1,84 +0,0 @@
-@use '../../partials/dir';
-@use '../../partials/screen';
-
-.pw-modal {
- --modal-height: 774px;
- max-height: var(--modal-height) !important;
- height: 100%;
-
- @include screen.smallerThan(mobileBreakpoint) {
- --modal-height: 100%;
- border-radius: 0 !important;
- &__overlay {
- padding: 0 !important;
- }
- }
-}
-
-.pw {
- width: 100%;
- height: 100%;
- background-color: var(--bg-surface);
-
- display: flex;
-
- &__drawer {
- width: var(--popup-window-drawer-width);
- background-color: var(--bg-surface-low);
- @include dir.side(border, none, 1px solid var(--bg-surface-border));
- }
- &__content {
- flex: 1;
- min-width: 0;
- }
-
- &__drawer,
- &__content {
- display: flex;
- flex-direction: column;
- }
-}
-
-
-.pw__drawer__content,
-.pw__content-container {
- padding-top: var(--sp-extra-tight);
- padding-bottom: var(--sp-extra-loose);
-}
-.pw__drawer__content__wrapper,
-.pw__content__wrapper {
- flex: 1;
- min-height: 0;
-}
-
-.pw__drawer {
- & .header {
- padding-left: var(--sp-tight);
- @include dir.side(padding, var(--sp-tight), var(--sp-tight));
- & .header__title-wrapper {
- @include dir.side(margin, var(--sp-ultra-tight), var(--sp-extra-tight));
- }
- }
-}
-
-.pw-content-selector {
- margin: 0 var(--sp-extra-tight);
- border-radius: var(--bo-radius);
- &--selected {
- box-shadow: var(--bs-surface-border);
- background-color: var(--bg-surface);
-
- & .context-menu__item > button {
- &:hover {
- background-color: transparent;
- }
- }
- }
-
- & .context-menu__item > button {
- border-radius: var(--bo-radius);
- & .ic-raw {
- @include dir.side(margin, 0, var(--sp-tight));
- }
- }
-}
diff --git a/src/app/molecules/power-level-selector/PowerLevelSelector.jsx b/src/app/molecules/power-level-selector/PowerLevelSelector.jsx
deleted file mode 100644
index ca3e841773..0000000000
--- a/src/app/molecules/power-level-selector/PowerLevelSelector.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './PowerLevelSelector.scss';
-
-import IconButton from '../../atoms/button/IconButton';
-import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
-
-import CheckIC from '../../../../public/res/ic/outlined/check.svg';
-
-function PowerLevelSelector({
- value, max, onSelect,
-}) {
- const handleSubmit = (e) => {
- const powerLevel = e.target.elements['power-level']?.value;
- if (!powerLevel) return;
- onSelect(Number(powerLevel));
- };
-
- return (
-
- Power level selector
-
- {max >= 0 && Presets}
- {max >= 100 && }
- {max >= 50 && }
- {max >= 0 && }
-
- );
-}
-
-PowerLevelSelector.propTypes = {
- value: PropTypes.number.isRequired,
- max: PropTypes.number.isRequired,
- onSelect: PropTypes.func.isRequired,
-};
-
-export default PowerLevelSelector;
diff --git a/src/app/molecules/power-level-selector/PowerLevelSelector.scss b/src/app/molecules/power-level-selector/PowerLevelSelector.scss
deleted file mode 100644
index 14a151ddb7..0000000000
--- a/src/app/molecules/power-level-selector/PowerLevelSelector.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-@use '../../partials/flex';
-@use '../../partials/dir';
-
-.power-level-selector {
- & .context-menu__item .text {
- margin: 0 !important;
- }
-
- & form {
- margin: var(--sp-normal);
- display: flex;
-
- & input {
- @extend .cp-fx__item-one;
- @include dir.side(margin, 0, var(--sp-tight));
- width: 148px;
- padding: 9px var(--sp-tight);
- }
- }
-}
\ No newline at end of file
diff --git a/src/app/molecules/room-selector/RoomSelector.jsx b/src/app/molecules/room-selector/RoomSelector.jsx
deleted file mode 100644
index f865c95d02..0000000000
--- a/src/app/molecules/room-selector/RoomSelector.jsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './RoomSelector.scss';
-
-import colorMXID from '../../../util/colorMXID';
-
-import Text from '../../atoms/text/Text';
-import Avatar from '../../atoms/avatar/Avatar';
-import NotificationBadge from '../../atoms/badge/NotificationBadge';
-import { blurOnBubbling } from '../../atoms/button/script';
-
-function RoomSelectorWrapper({
- isSelected,
- isMuted,
- isUnread,
- onClick,
- content,
- options,
- onContextMenu,
-}) {
- const classes = ['room-selector'];
- if (isMuted) classes.push('room-selector--muted');
- if (isUnread) classes.push('room-selector--unread');
- if (isSelected) classes.push('room-selector--selected');
-
- return (
-
-
blurOnBubbling(e, '.room-selector__content')}
- onContextMenu={onContextMenu}
- >
- {content}
-
-
{options}
-
- );
-}
-RoomSelectorWrapper.defaultProps = {
- isMuted: false,
- options: null,
- onContextMenu: null,
-};
-RoomSelectorWrapper.propTypes = {
- isSelected: PropTypes.bool.isRequired,
- isMuted: PropTypes.bool,
- isUnread: PropTypes.bool.isRequired,
- onClick: PropTypes.func.isRequired,
- content: PropTypes.node.isRequired,
- options: PropTypes.node,
- onContextMenu: PropTypes.func,
-};
-
-function RoomSelector({
- name,
- parentName,
- roomId,
- imageSrc,
- iconSrc,
- isSelected,
- isMuted,
- isUnread,
- notificationCount,
- isAlert,
- options,
- onClick,
- onContextMenu,
-}) {
- return (
-
-
-
- {name}
- {parentName && (
-
- {' — '}
- {parentName}
-
- )}
-
- {isUnread && (
-
- )}
- >
- }
- options={options}
- onClick={onClick}
- onContextMenu={onContextMenu}
- />
- );
-}
-RoomSelector.defaultProps = {
- parentName: null,
- isSelected: false,
- imageSrc: null,
- iconSrc: null,
- isMuted: false,
- options: null,
- onContextMenu: null,
-};
-RoomSelector.propTypes = {
- name: PropTypes.string.isRequired,
- parentName: PropTypes.string,
- roomId: PropTypes.string.isRequired,
- imageSrc: PropTypes.string,
- iconSrc: PropTypes.string,
- isSelected: PropTypes.bool,
- isMuted: PropTypes.bool,
- isUnread: PropTypes.bool.isRequired,
- notificationCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
- isAlert: PropTypes.bool.isRequired,
- options: PropTypes.node,
- onClick: PropTypes.func.isRequired,
- onContextMenu: PropTypes.func,
-};
-
-export default RoomSelector;
diff --git a/src/app/molecules/room-selector/RoomSelector.scss b/src/app/molecules/room-selector/RoomSelector.scss
deleted file mode 100644
index 59e474733c..0000000000
--- a/src/app/molecules/room-selector/RoomSelector.scss
+++ /dev/null
@@ -1,87 +0,0 @@
-@use '../../partials/flex';
-@use '../../partials/text';
-@use '../../partials/dir';
-
-.room-selector {
- @extend .cp-fx__row--s-c;
-
- border: 1px solid transparent;
- border-radius: var(--bo-radius);
- cursor: pointer;
-
- &--muted {
- opacity: 0.6;
- }
-
- &--unread {
- .room-selector__content > .text {
- color: var(--tc-surface-high);
- }
- }
-
- &--selected {
- background-color: var(--bg-surface);
- border-color: var(--bg-surface-border);
-
- & .room-selector__options {
- display: flex;
- }
- }
-
- @media (hover: hover) {
- &:hover {
- background-color: var(--bg-surface-hover);
- & .room-selector__options {
- display: flex;
- }
- }
- }
- &:focus-within {
- background-color: var(--bg-surface-hover);
- & button {
- outline: none;
- }
- }
- &:active {
- background-color: var(--bg-surface-active);
- }
- &--selected:hover,
- &--selected:focus,
- &--selected:active {
- background-color: var(--bg-surface);
- }
-}
-
-.room-selector__content {
- @extend .cp-fx__item-one;
- @extend .cp-fx__row--s-c;
- padding: 0 var(--sp-extra-tight);
- min-height: 40px;
- cursor: inherit;
-
- & > .avatar-container .avatar__border--active {
- box-shadow: none;
- }
-
- & > .text {
- @extend .cp-fx__item-one;
- @extend .cp-txt__ellipsis;
- margin: 0 var(--sp-extra-tight);
-
- color: var(--tc-surface-normal-low);
- }
-}
-.room-selector__options {
- @extend .cp-fx__row--s-c;
- @include dir.side(margin, 0, var(--sp-ultra-tight));
- display: none;
-
- &:empty {
- margin: 0 !important;
- }
-
- & .ic-btn {
- padding: 6px;
- border-radius: calc(var(--bo-radius) / 2);
- }
-}
\ No newline at end of file
diff --git a/src/app/molecules/room-tile/RoomTile.jsx b/src/app/molecules/room-tile/RoomTile.jsx
deleted file mode 100644
index 2e0a63f608..0000000000
--- a/src/app/molecules/room-tile/RoomTile.jsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './RoomTile.scss';
-
-import colorMXID from '../../../util/colorMXID';
-
-import Text from '../../atoms/text/Text';
-import Avatar from '../../atoms/avatar/Avatar';
-
-function RoomTile({ avatarSrc, name, id, inviterName, memberCount, desc, options }) {
- return (
-
-
-
- {name}
-
- {inviterName !== null
- ? `Invited by ${inviterName} to ${id}${
- memberCount === null ? '' : ` • ${memberCount} members`
- }`
- : id + (memberCount === null ? '' : ` • ${memberCount} members`)}
-
- {desc !== null && typeof desc === 'string' ? (
-
- {desc}
-
- ) : (
- desc
- )}
-
- {options !== null &&
{options}
}
-
- );
-}
-
-RoomTile.defaultProps = {
- avatarSrc: null,
- inviterName: null,
- options: null,
- desc: null,
- memberCount: null,
-};
-RoomTile.propTypes = {
- avatarSrc: PropTypes.string,
- name: PropTypes.string.isRequired,
- id: PropTypes.string.isRequired,
- inviterName: PropTypes.string,
- memberCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- desc: PropTypes.node,
- options: PropTypes.node,
-};
-
-export default RoomTile;
diff --git a/src/app/molecules/room-tile/RoomTile.scss b/src/app/molecules/room-tile/RoomTile.scss
deleted file mode 100644
index bbed710737..0000000000
--- a/src/app/molecules/room-tile/RoomTile.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.room-tile {
- display: flex;
-
- &__content {
- flex: 1;
- min-width: 0;
-
- margin: 0 var(--sp-normal);
-
- &__desc {
- white-space: pre-wrap;
- & a {
- white-space: wrap;
- }
- }
-
- & .text:not(:first-child) {
- margin-top: var(--sp-ultra-tight);
- }
- }
-}
\ No newline at end of file
diff --git a/src/app/molecules/setting-tile/SettingTile.jsx b/src/app/molecules/setting-tile/SettingTile.jsx
deleted file mode 100644
index 6b221965d0..0000000000
--- a/src/app/molecules/setting-tile/SettingTile.jsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import './SettingTile.scss';
-
-import Text from '../../atoms/text/Text';
-
-function SettingTile({ title, options, content }) {
- return (
-
-
-
- {
- typeof title === 'string'
- ? {title}
- : title
- }
-
- {content}
-
- {options !== null &&
{options}
}
-
- );
-}
-
-SettingTile.defaultProps = {
- options: null,
- content: null,
-};
-
-SettingTile.propTypes = {
- title: PropTypes.node.isRequired,
- options: PropTypes.node,
- content: PropTypes.node,
-};
-
-export default SettingTile;
diff --git a/src/app/molecules/setting-tile/SettingTile.scss b/src/app/molecules/setting-tile/SettingTile.scss
deleted file mode 100644
index 322f0019c6..0000000000
--- a/src/app/molecules/setting-tile/SettingTile.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-@use '../../partials/dir';
-
-.setting-tile {
- display: flex;
- &__content {
- flex: 1;
- min-width: 0;
- }
- &__title {
- margin-bottom: var(--sp-ultra-tight);
- }
- &__options {
- @include dir.side(margin, var(--sp-tight), 0);
- }
-}
\ No newline at end of file
diff --git a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx
deleted file mode 100644
index b084a1ad37..0000000000
--- a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
-import { useAtomValue } from 'jotai';
-import './SpaceAddExisting.scss';
-
-import cons from '../../../client/state/cons';
-import navigation from '../../../client/state/navigation';
-import { joinRuleToIconSrc, getIdServer } from '../../../util/matrixUtil';
-import { Debounce } from '../../../util/common';
-
-import Text from '../../atoms/text/Text';
-import RawIcon from '../../atoms/system-icons/RawIcon';
-import Button from '../../atoms/button/Button';
-import IconButton from '../../atoms/button/IconButton';
-import Checkbox from '../../atoms/button/Checkbox';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-import RoomSelector from '../room-selector/RoomSelector';
-import Dialog from '../dialog/Dialog';
-
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-import SearchIC from '../../../../public/res/ic/outlined/search.svg';
-
-import { useStore } from '../../hooks/useStore';
-import { roomToParentsAtom } from '../../state/room/roomToParents';
-import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
-import { allRoomsAtom } from '../../state/room-list/roomList';
-import { mDirectAtom } from '../../state/mDirectList';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { getViaServers } from '../../plugins/via-servers';
-import { rateLimitedActions } from '../../utils/matrix';
-import { useAlive } from '../../hooks/useAlive';
-
-function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
- const alive = useAlive();
- const [debounce] = useState(new Debounce());
- const [process, setProcess] = useState(null);
- const [allRoomIds, setAllRoomIds] = useState([]);
- const [selected, setSelected] = useState([]);
- const [searchIds, setSearchIds] = useState(null);
- const mx = useMatrixClient();
- const roomIdToParents = useAtomValue(roomToParentsAtom);
- const mDirects = useAtomValue(mDirectAtom);
- const spaces = useSpaces(mx, allRoomsAtom);
- const rooms = useRooms(mx, allRoomsAtom, mDirects);
- const directs = useDirects(mx, allRoomsAtom, mDirects);
-
- useEffect(() => {
- const roomIds = onlySpaces ? [...spaces] : [...rooms, ...directs];
- const allIds = roomIds.filter(
- (rId) => rId !== roomId && !roomIdToParents.get(rId)?.has(roomId)
- );
- setAllRoomIds(allIds);
- }, [spaces, rooms, directs, roomIdToParents, roomId, onlySpaces]);
-
- const toggleSelection = (rId) => {
- if (process !== null) return;
- const newSelected = [...selected];
- const selectedIndex = newSelected.indexOf(rId);
-
- if (selectedIndex > -1) {
- newSelected.splice(selectedIndex, 1);
- setSelected(newSelected);
- return;
- }
- newSelected.push(rId);
- setSelected(newSelected);
- };
-
- const handleAdd = async () => {
- setProcess(`Adding ${selected.length} items...`);
-
- await rateLimitedActions(selected, async (rId) => {
- const room = mx.getRoom(rId);
- const via = getViaServers(room);
- if (via.length === 0) {
- via.push(getIdServer(rId));
- }
-
- await mx.sendStateEvent(
- roomId,
- 'm.space.child',
- {
- auto_join: false,
- suggested: false,
- via,
- },
- rId
- );
- });
-
- if (!alive()) return;
-
- const roomIds = onlySpaces ? [...spaces] : [...rooms, ...directs];
- const allIds = roomIds.filter(
- (rId) => rId !== roomId && !roomIdToParents.get(rId)?.has(roomId) && !selected.includes(rId)
- );
- setAllRoomIds(allIds);
- setProcess(null);
- setSelected([]);
- };
-
- const handleSearch = (ev) => {
- const term = ev.target.value.toLocaleLowerCase().replace(/\s/g, '');
- if (term === '') {
- setSearchIds(null);
- return;
- }
-
- debounce._(() => {
- const searchedIds = allRoomIds.filter((rId) => {
- let name = mx.getRoom(rId)?.name;
- if (!name) return false;
- name = name.normalize('NFKC').toLocaleLowerCase().replace(/\s/g, '');
- return name.includes(term);
- });
- setSearchIds(searchedIds);
- }, 200)();
- };
- const handleSearchClear = (ev) => {
- const btn = ev.currentTarget;
- btn.parentElement.searchInput.value = '';
- setSearchIds(null);
- };
-
- return (
- <>
-
- {searchIds?.length === 0 && No results found}
- {(searchIds || allRoomIds).map((rId) => {
- const room = mx.getRoom(rId);
- let imageSrc =
- room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
- if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
-
- const parentSet = roomIdToParents.get(rId);
- const parentNames = parentSet
- ? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
- : undefined;
- const parents = parentNames ? parentNames.join(', ') : null;
-
- const handleSelect = () => toggleSelection(rId);
-
- return (
-
- }
- />
- );
- })}
- {selected.length !== 0 && (
-
- {process && }
- {process || `${selected.length} item selected`}
- {!process && (
-
- Add
-
- )}
-
- )}
- >
- );
-}
-SpaceAddExistingContent.propTypes = {
- roomId: PropTypes.string.isRequired,
- spaces: PropTypes.bool.isRequired,
-};
-
-function useVisibilityToggle() {
- const [data, setData] = useState(null);
-
- useEffect(() => {
- const handleOpen = (roomId, spaces) =>
- setData({
- roomId,
- spaces,
- });
- navigation.on(cons.events.navigation.SPACE_ADDEXISTING_OPENED, handleOpen);
- return () => {
- navigation.removeListener(cons.events.navigation.SPACE_ADDEXISTING_OPENED, handleOpen);
- };
- }, []);
-
- const requestClose = () => setData(null);
-
- return [data, requestClose];
-}
-
-function SpaceAddExisting() {
- const [data, requestClose] = useVisibilityToggle();
- const mx = useMatrixClient();
- const room = mx.getRoom(data?.roomId);
-
- return (
-
- );
-}
-
-export default SpaceAddExisting;
diff --git a/src/app/molecules/space-add-existing/SpaceAddExisting.scss b/src/app/molecules/space-add-existing/SpaceAddExisting.scss
deleted file mode 100644
index 3eb3d96a48..0000000000
--- a/src/app/molecules/space-add-existing/SpaceAddExisting.scss
+++ /dev/null
@@ -1,77 +0,0 @@
-@use '../../partials/dir';
-@use '../../partials/flex';
-
-.space-add-existing {
- height: 100%;
-
- .dialog__content-container {
- padding: 0;
- padding-bottom: 80px;
- @include dir.side(padding, var(--sp-extra-tight), 0);
-
- & > .text {
- margin: var(--sp-loose) var(--sp-normal);
- text-align: center;
- }
- }
-
- & form {
- @extend .cp-fx__row--s-c;
- padding: var(--sp-extra-tight);
- padding-top: var(--sp-normal);
-
- position: sticky;
- top: 0;
- z-index: 999;
- background-color: var(--bg-surface);
-
- & > .ic-raw,
- & > .ic-btn {
- position: absolute;
- }
- & > .ic-raw {
- margin: 0 var(--sp-tight);
- }
- & > .ic-btn {
- border-radius: calc(var(--bo-radius) / 2);
- @include dir.prop(right, var(--sp-tight), unset);
- @include dir.prop(left, unset, var(--sp-tight));
- }
- & input {
- padding: var(--sp-tight) 40px;
- }
- }
-
- .input-container {
- @extend .cp-fx__item-one;
- }
-
- .room-selector {
- margin: 0 var(--sp-extra-tight);
- }
- .room-selector__options {
- display: flex;
- margin: 0 10px;
- }
-}
-
-.space-add-existing__footer {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- padding: var(--sp-normal);
- background-color: var(--bg-surface);
- border-top: 1px solid var(--bg-surface-border);
- display: flex;
- align-items: center;
-
- & > .text {
- @extend .cp-fx__item-one;
- padding: 0 var(--sp-tight);
- }
-
- & > button {
- @include dir.side(margin, var(--sp-normal), 0);
- }
-}
\ No newline at end of file
diff --git a/src/app/organisms/create-room/CreateRoom.jsx b/src/app/organisms/create-room/CreateRoom.jsx
deleted file mode 100644
index 04b2faebe7..0000000000
--- a/src/app/organisms/create-room/CreateRoom.jsx
+++ /dev/null
@@ -1,307 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import PropTypes from 'prop-types';
-import './CreateRoom.scss';
-
-import cons from '../../../client/state/cons';
-import navigation from '../../../client/state/navigation';
-import { openReusableContextMenu } from '../../../client/action/navigation';
-import * as roomActions from '../../../client/action/room';
-import { isRoomAliasAvailable, getIdServer } from '../../../util/matrixUtil';
-import { getEventCords } from '../../../util/common';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import Toggle from '../../atoms/button/Toggle';
-import IconButton from '../../atoms/button/IconButton';
-import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-import SegmentControl from '../../atoms/segmented-controls/SegmentedControls';
-import Dialog from '../../molecules/dialog/Dialog';
-import SettingTile from '../../molecules/setting-tile/SettingTile';
-
-import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg';
-import SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg';
-import HashIC from '../../../../public/res/ic/outlined/hash.svg';
-import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg';
-import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
-import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
-import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
-import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
-import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-import { useRoomNavigate } from '../../hooks/useRoomNavigate';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
- const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite');
- const [isEncrypted, setIsEncrypted] = useState(true);
- const [isCreatingRoom, setIsCreatingRoom] = useState(false);
- const [creatingError, setCreatingError] = useState(null);
- const { navigateRoom, navigateSpace } = useRoomNavigate();
-
- const [isValidAddress, setIsValidAddress] = useState(null);
- const [addressValue, setAddressValue] = useState(undefined);
- const [roleIndex, setRoleIndex] = useState(0);
-
- const addressRef = useRef(null);
-
- const mx = useMatrixClient();
- const userHs = getIdServer(mx.getUserId());
-
- const handleSubmit = async (evt) => {
- evt.preventDefault();
- const { target } = evt;
-
- if (isCreatingRoom) return;
- setIsCreatingRoom(true);
- setCreatingError(null);
-
- const name = target.name.value;
- let topic = target.topic.value;
- if (topic.trim() === '') topic = undefined;
- let roomAlias;
- if (joinRule === 'public') {
- roomAlias = addressRef?.current?.value;
- if (roomAlias.trim() === '') roomAlias = undefined;
- }
-
- const powerLevel = roleIndex === 1 ? 101 : undefined;
-
- try {
- const data = await roomActions.createRoom(mx, {
- name,
- topic,
- joinRule,
- alias: roomAlias,
- isEncrypted: isSpace || joinRule === 'public' ? false : isEncrypted,
- powerLevel,
- isSpace,
- parentId,
- });
- setIsCreatingRoom(false);
- setCreatingError(null);
- setIsValidAddress(null);
- setAddressValue(undefined);
- onRequestClose();
- if (isSpace) {
- navigateSpace(data.room_id);
- } else {
- navigateRoom(data.room_id);
- }
- } catch (e) {
- if (e.message === 'M_UNKNOWN: Invalid characters in room alias') {
- setCreatingError('ERROR: Invalid characters in address');
- setIsValidAddress(false);
- } else if (e.message === 'M_ROOM_IN_USE: Room alias already taken') {
- setCreatingError('ERROR: This address is already in use');
- setIsValidAddress(false);
- } else setCreatingError(e.message);
- setIsCreatingRoom(false);
- }
- };
-
- const validateAddress = (e) => {
- const myAddress = e.target.value;
- setIsValidAddress(null);
- setAddressValue(e.target.value);
- setCreatingError(null);
-
- setTimeout(async () => {
- if (myAddress !== addressRef.current.value) return;
- const roomAlias = addressRef.current.value;
- if (roomAlias === '') return;
- const roomAddress = `#${roomAlias}:${userHs}`;
-
- if (await isRoomAliasAvailable(mx, roomAddress)) {
- setIsValidAddress(true);
- } else {
- setIsValidAddress(false);
- }
- }, 1000);
- };
-
- const joinRules = ['invite', 'restricted', 'public'];
- const joinRuleShortText = ['Private', 'Restricted', 'Public'];
- const joinRuleText = [
- 'Private (invite only)',
- 'Restricted (space member can join)',
- 'Public (anyone can join)',
- ];
- const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
- const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
- const handleJoinRule = (evt) => {
- openReusableContextMenu('bottom', getEventCords(evt, '.btn-surface'), (closeMenu) => (
- <>
- Visibility (who can join)
- {joinRules.map((rule) => (
-
- ))}
- >
- ));
- };
-
- return (
-
- );
-}
-CreateRoomContent.defaultProps = {
- parentId: null,
-};
-CreateRoomContent.propTypes = {
- isSpace: PropTypes.bool.isRequired,
- parentId: PropTypes.string,
- onRequestClose: PropTypes.func.isRequired,
-};
-
-function useWindowToggle() {
- const [create, setCreate] = useState(null);
-
- useEffect(() => {
- const handleOpen = (isSpace, parentId) => {
- setCreate({
- isSpace,
- parentId,
- });
- };
- navigation.on(cons.events.navigation.CREATE_ROOM_OPENED, handleOpen);
- return () => {
- navigation.removeListener(cons.events.navigation.CREATE_ROOM_OPENED, handleOpen);
- };
- }, []);
-
- const onRequestClose = () => setCreate(null);
-
- return [create, onRequestClose];
-}
-
-function CreateRoom() {
- const [create, onRequestClose] = useWindowToggle();
- const { isSpace, parentId } = create ?? {};
- const mx = useMatrixClient();
- const room = mx.getRoom(parentId);
-
- return (
-
- );
-}
-
-export default CreateRoom;
diff --git a/src/app/organisms/create-room/CreateRoom.scss b/src/app/organisms/create-room/CreateRoom.scss
deleted file mode 100644
index ccd3ea3b1f..0000000000
--- a/src/app/organisms/create-room/CreateRoom.scss
+++ /dev/null
@@ -1,90 +0,0 @@
-@use '../../partials/flex';
-@use '../../partials/dir';
-
-.create-room {
- margin: var(--sp-normal);
- @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
-
- &__form > * {
- margin-top: var(--sp-normal);
- &:first-child {
- margin-top: var(--sp-extra-tight);
- }
- }
-
- & .segment-btn {
- padding: var(--sp-ultra-tight) 0;
- &__base {
- padding: 0 var(--sp-tight);
- }
- }
-
- &__address {
- display: flex;
- &__label {
- color: var(--tc-surface-low);
- margin-bottom: var(--sp-ultra-tight);
- }
- &__tip {
- margin-top: var(--sp-ultra-tight);
- @include dir.side(margin, 46px, 0);
- }
- & .text {
- display: flex;
- align-items: center;
- padding: 0 var(--sp-normal);
- border: 1px solid var(--bg-surface-border);
- border-radius: var(--bo-radius);
- color: var(--tc-surface-low);
- }
- & *:nth-child(2) {
- flex: 1;
- min-width: 0;
- & .input {
- border-radius: 0;
- }
- }
- & .text:first-child {
- @include dir.prop(border-width, 1px 0 1px 1px, 1px 1px 1px 0);
- @include dir.prop(
- border-radius,
- var(--bo-radius) 0 0 var(--bo-radius),
- 0 var(--bo-radius) var(--bo-radius) 0,
- );
- }
- & .text:last-child {
- @include dir.prop(border-width, 1px 1px 1px 0, 1px 0 1px 1px);
- @include dir.prop(
- border-radius,
- 0 var(--bo-radius) var(--bo-radius) 0,
- var(--bo-radius) 0 0 var(--bo-radius),
- );
- }
- }
-
- &__name-wrapper {
- display: flex;
- align-items: flex-end;
-
- & .input-container {
- flex: 1;
- min-width: 0;
- @include dir.side(margin, 0, var(--sp-normal));
- }
- & .btn-primary {
- padding-top: 11px;
- padding-bottom: 11px;
- }
- }
-
- &__loading {
- @extend .cp-fx__row--c-c;
- & .text {
- @include dir.side(margin, var(--sp-normal), 0);
- }
- }
- &__error {
- text-align: center;
- color: var(--bg-danger) !important;
- }
-}
\ No newline at end of file
diff --git a/src/app/organisms/emoji-board/custom-emoji.js b/src/app/organisms/emoji-board/custom-emoji.js
deleted file mode 100644
index 4ca2088f71..0000000000
--- a/src/app/organisms/emoji-board/custom-emoji.js
+++ /dev/null
@@ -1,142 +0,0 @@
-// https://github.com/Sorunome/matrix-doc/blob/soru/emotes/proposals/2545-emotes.md
-
-export class ImagePack {
- static parsePack(eventId, packContent) {
- if (!eventId || typeof packContent?.images !== 'object') {
- return null;
- }
-
- return new ImagePack(eventId, packContent);
- }
-
- constructor(eventId, content) {
- this.id = eventId;
- this.content = JSON.parse(JSON.stringify(content));
-
- this.applyPack(content);
- this.applyImages(content);
- }
-
- applyPack(content) {
- const pack = content.pack ?? {};
-
- this.displayName = pack.display_name;
- this.avatarUrl = pack.avatar_url;
- this.usage = pack.usage ?? ['emoticon', 'sticker'];
- this.attribution = pack.attribution;
- }
-
- applyImages(content) {
- this.images = new Map();
- this.emoticons = [];
- this.stickers = [];
-
- Object.entries(content.images).forEach(([shortcode, data]) => {
- const mxc = data.url;
- const body = data.body ?? shortcode;
- const usage = data.usage ?? this.usage;
- const { info } = data;
-
- if (!mxc) return;
- const image = {
- shortcode, mxc, body, usage, info,
- };
-
- this.images.set(shortcode, image);
- if (usage.includes('emoticon')) {
- this.emoticons.push(image);
- }
- if (usage.includes('sticker')) {
- this.stickers.push(image);
- }
- });
- }
-
- getImages() {
- return this.images;
- }
-
- getEmojis() {
- return this.emoticons;
- }
-
- getStickers() {
- return this.stickers;
- }
-
- getContent() {
- return this.content;
- }
-
- _updatePackProperty(property, value) {
- if (this.content.pack === undefined) {
- this.content.pack = {};
- }
- this.content.pack[property] = value;
- this.applyPack(this.content);
- }
-
- setAvatarUrl(avatarUrl) {
- this._updatePackProperty('avatar_url', avatarUrl);
- }
-
- setDisplayName(displayName) {
- this._updatePackProperty('display_name', displayName);
- }
-
- setAttribution(attribution) {
- this._updatePackProperty('attribution', attribution);
- }
-
- setUsage(usage) {
- this._updatePackProperty('usage', usage);
- }
-
- addImage(key, imgContent) {
- this.content.images = {
- [key]: imgContent,
- ...this.content.images,
- };
- this.applyImages(this.content);
- }
-
- removeImage(key) {
- if (this.content.images[key] === undefined) return;
- delete this.content.images[key];
- this.applyImages(this.content);
- }
-
- updateImageKey(key, newKey) {
- if (this.content.images[key] === undefined) return;
- const copyImages = {};
- Object.keys(this.content.images).forEach((imgKey) => {
- copyImages[imgKey === key ? newKey : imgKey] = this.content.images[imgKey];
- });
- this.content.images = copyImages;
- this.applyImages(this.content);
- }
-
- _updateImageProperty(key, property, value) {
- if (this.content.images[key] === undefined) return;
- this.content.images[key][property] = value;
- this.applyImages(this.content);
- }
-
- setImageUrl(key, url) {
- this._updateImageProperty(key, 'url', url);
- }
-
- setImageBody(key, body) {
- this._updateImageProperty(key, 'body', body);
- }
-
- setImageInfo(key, info) {
- this._updateImageProperty(key, 'info', info);
- }
-
- setImageUsage(key, usage) {
- this._updateImageProperty(key, 'usage', usage);
- }
-}
-
-
diff --git a/src/app/organisms/invite-user/InviteUser.jsx b/src/app/organisms/invite-user/InviteUser.jsx
deleted file mode 100644
index 271c22a961..0000000000
--- a/src/app/organisms/invite-user/InviteUser.jsx
+++ /dev/null
@@ -1,315 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import PropTypes from 'prop-types';
-import './InviteUser.scss';
-
-import * as roomActions from '../../../client/action/room';
-import { hasDevices } from '../../../util/matrixUtil';
-
-import Text from '../../atoms/text/Text';
-import Button from '../../atoms/button/Button';
-import IconButton from '../../atoms/button/IconButton';
-import Spinner from '../../atoms/spinner/Spinner';
-import Input from '../../atoms/input/Input';
-import PopupWindow from '../../molecules/popup-window/PopupWindow';
-import RoomTile from '../../molecules/room-tile/RoomTile';
-
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-import UserIC from '../../../../public/res/ic/outlined/user.svg';
-import { useRoomNavigate } from '../../hooks/useRoomNavigate';
-import { getDMRoomFor } from '../../utils/matrix';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
-
-function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) {
- const [isSearching, updateIsSearching] = useState(false);
- const [searchQuery, updateSearchQuery] = useState({});
- const [users, updateUsers] = useState([]);
- const useAuthentication = useMediaAuthentication();
-
- const [procUsers, updateProcUsers] = useState(new Set()); // proc stands for processing.
- const [procUserError, updateUserProcError] = useState(new Map());
-
- const [createdDM, updateCreatedDM] = useState(new Map());
- const [roomIdToUserId, updateRoomIdToUserId] = useState(new Map());
-
- const [invitedUserIds, updateInvitedUserIds] = useState(new Set());
-
- const usernameRef = useRef(null);
-
- const mx = useMatrixClient();
- const { navigateRoom } = useRoomNavigate();
-
- function getMapCopy(myMap) {
- const newMap = new Map();
- myMap.forEach((data, key) => {
- newMap.set(key, data);
- });
- return newMap;
- }
- function addUserToProc(userId) {
- procUsers.add(userId);
- updateProcUsers(new Set(Array.from(procUsers)));
- }
- function deleteUserFromProc(userId) {
- procUsers.delete(userId);
- updateProcUsers(new Set(Array.from(procUsers)));
- }
-
- function onDMCreated(newRoomId) {
- const myDMPartnerId = roomIdToUserId.get(newRoomId);
- if (typeof myDMPartnerId === 'undefined') return;
-
- createdDM.set(myDMPartnerId, newRoomId);
- roomIdToUserId.delete(newRoomId);
-
- deleteUserFromProc(myDMPartnerId);
- updateCreatedDM(getMapCopy(createdDM));
- updateRoomIdToUserId(getMapCopy(roomIdToUserId));
- }
-
- async function searchUser(username) {
- const inputUsername = username.trim();
- if (isSearching || inputUsername === '' || inputUsername === searchQuery.username) return;
- const isInputUserId = inputUsername[0] === '@' && inputUsername.indexOf(':') > 1;
- updateIsSearching(true);
- updateSearchQuery({ username: inputUsername });
-
- if (isInputUserId) {
- try {
- const result = await mx.getProfileInfo(inputUsername);
- updateUsers([
- {
- user_id: inputUsername,
- display_name: result.displayname,
- avatar_url: result.avatar_url,
- },
- ]);
- } catch (e) {
- updateSearchQuery({ error: `${inputUsername} not found!` });
- }
- } else {
- try {
- const result = await mx.searchUserDirectory({
- term: inputUsername,
- limit: 20,
- });
- if (result.results.length === 0) {
- updateSearchQuery({ error: `No matches found for "${inputUsername}"!` });
- updateIsSearching(false);
- return;
- }
- updateUsers(result.results);
- } catch (e) {
- updateSearchQuery({ error: 'Something went wrong!' });
- }
- }
- updateIsSearching(false);
- }
-
- async function createDM(userId) {
- if (mx.getUserId() === userId) return;
- const dmRoomId = getDMRoomFor(mx, userId)?.roomId;
- if (dmRoomId) {
- navigateRoom(dmRoomId);
- onRequestClose();
- return;
- }
-
- try {
- addUserToProc(userId);
- procUserError.delete(userId);
- updateUserProcError(getMapCopy(procUserError));
-
- const result = await roomActions.createDM(mx, userId, await hasDevices(mx, userId));
- roomIdToUserId.set(result.room_id, userId);
- updateRoomIdToUserId(getMapCopy(roomIdToUserId));
- onDMCreated(result.room_id);
- } catch (e) {
- deleteUserFromProc(userId);
- if (typeof e.message === 'string') procUserError.set(userId, e.message);
- else procUserError.set(userId, 'Something went wrong!');
- updateUserProcError(getMapCopy(procUserError));
- }
- }
-
- async function inviteToRoom(userId) {
- if (typeof roomId === 'undefined') return;
- try {
- addUserToProc(userId);
- procUserError.delete(userId);
- updateUserProcError(getMapCopy(procUserError));
-
- await mx.invite(roomId, userId);
-
- invitedUserIds.add(userId);
- updateInvitedUserIds(new Set(Array.from(invitedUserIds)));
- deleteUserFromProc(userId);
- } catch (e) {
- deleteUserFromProc(userId);
- if (typeof e.message === 'string') procUserError.set(userId, e.message);
- else procUserError.set(userId, 'Something went wrong!');
- updateUserProcError(getMapCopy(procUserError));
- }
- }
-
- function renderUserList() {
- const renderOptions = (userId) => {
- const messageJSX = (message, isPositive) => (
-
-
- {message}
-
-
- );
-
- if (mx.getUserId() === userId) return null;
- if (procUsers.has(userId)) {
- return ;
- }
- if (createdDM.has(userId)) {
- // eslint-disable-next-line max-len
- return (
- {
- navigateRoom(createdDM.get(userId));
- onRequestClose();
- }}
- >
- Open
-
- );
- }
- if (invitedUserIds.has(userId)) {
- return messageJSX('Invited', true);
- }
- if (typeof roomId === 'string') {
- const member = mx.getRoom(roomId).getMember(userId);
- if (member !== null) {
- const userMembership = member.membership;
- switch (userMembership) {
- case 'join':
- return messageJSX('Already joined', true);
- case 'invite':
- return messageJSX('Already Invited', true);
- case 'ban':
- return messageJSX('Banned', false);
- default:
- }
- }
- }
- return typeof roomId === 'string' ? (
- inviteToRoom(userId)} variant="primary">
- Invite
-
- ) : (
- createDM(userId)} variant="primary">
- Message
-
- );
- };
- const renderError = (userId) => {
- if (!procUserError.has(userId)) return null;
- return (
-
- {procUserError.get(userId)}
-
- );
- };
-
- return users.map((user) => {
- const userId = user.user_id;
- const name = typeof user.display_name === 'string' ? user.display_name : userId;
- return (
-
- );
- });
- }
-
- useEffect(() => {
- if (isOpen && typeof searchTerm === 'string') searchUser(searchTerm);
- return () => {
- updateIsSearching(false);
- updateSearchQuery({});
- updateUsers([]);
- updateProcUsers(new Set());
- updateUserProcError(new Map());
- updateCreatedDM(new Map());
- updateRoomIdToUserId(new Map());
- updateInvitedUserIds(new Set());
- };
- }, [isOpen, searchTerm]);
-
- return (
- }
- onRequestClose={onRequestClose}
- >
-
-
-
- {typeof searchQuery.username !== 'undefined' && isSearching && (
-
-
- {`Searching for user "${searchQuery.username}"...`}
-
- )}
- {typeof searchQuery.username !== 'undefined' && !isSearching && (
-
{`Search result for user "${searchQuery.username}"`}
- )}
- {searchQuery.error && (
-
- {searchQuery.error}
-
- )}
-
- {users.length !== 0 &&
{renderUserList()}
}
-
-
- );
-}
-
-InviteUser.defaultProps = {
- roomId: undefined,
- searchTerm: undefined,
-};
-
-InviteUser.propTypes = {
- isOpen: PropTypes.bool.isRequired,
- roomId: PropTypes.string,
- searchTerm: PropTypes.string,
- onRequestClose: PropTypes.func.isRequired,
-};
-
-export default InviteUser;
diff --git a/src/app/organisms/invite-user/InviteUser.scss b/src/app/organisms/invite-user/InviteUser.scss
deleted file mode 100644
index 30c9b92161..0000000000
--- a/src/app/organisms/invite-user/InviteUser.scss
+++ /dev/null
@@ -1,45 +0,0 @@
-@use '../../partials/dir';
-
-.invite-user {
- margin-top: var(--sp-extra-tight);
- @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
-
- &__form {
- display: flex;
- align-items: flex-end;
-
- & .input-container {
- flex: 1;
- min-width: 0;
- @include dir.side(margin, 0, var(--sp-normal));
- }
-
- & .btn-primary {
- padding: {
- top: 11px;
- bottom: 11px;
- }
- }
- }
-
- &__search-status {
- margin-top: var(--sp-extra-loose);
- margin-bottom: var(--sp-tight);
- & .donut-spinner {
- margin: 0 var(--sp-tight);
- }
- }
- &__search-error {
- color: var(--bg-danger);
- }
- &__content {
- border-top: 1px solid var(--bg-surface-border);
- }
-
- & .room-tile {
- margin-top: var(--sp-normal);
- &__options {
- align-self: flex-end;
- }
- }
-}
\ No newline at end of file
diff --git a/src/app/organisms/join-alias/JoinAlias.jsx b/src/app/organisms/join-alias/JoinAlias.jsx
deleted file mode 100644
index d4e313af8c..0000000000
--- a/src/app/organisms/join-alias/JoinAlias.jsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
-import './JoinAlias.scss';
-
-import cons from '../../../client/state/cons';
-import navigation from '../../../client/state/navigation';
-import { join } from '../../../client/action/room';
-
-import Text from '../../atoms/text/Text';
-import IconButton from '../../atoms/button/IconButton';
-import Button from '../../atoms/button/Button';
-import Input from '../../atoms/input/Input';
-import Spinner from '../../atoms/spinner/Spinner';
-import Dialog from '../../molecules/dialog/Dialog';
-
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-
-import { useStore } from '../../hooks/useStore';
-import { useRoomNavigate } from '../../hooks/useRoomNavigate';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-
-const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/;
-
-function JoinAliasContent({ term, requestClose }) {
- const [process, setProcess] = useState(false);
- const [error, setError] = useState(undefined);
-
- const mx = useMatrixClient();
- const mountStore = useStore();
-
- const { navigateRoom } = useRoomNavigate();
-
- const openRoom = (roomId) => {
- navigateRoom(roomId);
- requestClose();
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- mountStore.setItem(true);
- const alias = e.target.alias.value;
- if (alias?.trim() === '') return;
- if (alias.match(ALIAS_OR_ID_REG) === null) {
- setError('Invalid address.');
- return;
- }
- setProcess('Looking for address...');
- setError(undefined);
- let via;
- if (alias.startsWith('#')) {
- try {
- const aliasData = await mx.getRoomIdForAlias(alias);
- via = aliasData?.servers.slice(0, 3) || [];
- if (mountStore.getItem()) {
- setProcess(`Joining ${alias}...`);
- }
- } catch (err) {
- if (!mountStore.getItem()) return;
- setProcess(false);
- setError(
- `Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`
- );
- }
- }
- try {
- const roomId = await join(mx, alias, false, via);
- if (!mountStore.getItem()) return;
- openRoom(roomId);
- } catch {
- if (!mountStore.getItem()) return;
- setProcess(false);
- setError(`Unable to join ${alias}. Either room/space is private or doesn't exist.`);
- }
- };
-
- return (
-
- );
-}
-JoinAliasContent.defaultProps = {
- term: undefined,
-};
-JoinAliasContent.propTypes = {
- term: PropTypes.string,
- requestClose: PropTypes.func.isRequired,
-};
-
-function useWindowToggle() {
- const [data, setData] = useState(null);
-
- useEffect(() => {
- const handleOpen = (term) => {
- setData({ term });
- };
- navigation.on(cons.events.navigation.JOIN_ALIAS_OPENED, handleOpen);
- return () => {
- navigation.removeListener(cons.events.navigation.JOIN_ALIAS_OPENED, handleOpen);
- };
- }, []);
-
- const onRequestClose = () => setData(null);
-
- return [data, onRequestClose];
-}
-
-function JoinAlias() {
- const [data, requestClose] = useWindowToggle();
-
- return (
-
- );
-}
-
-export default JoinAlias;
diff --git a/src/app/organisms/join-alias/JoinAlias.scss b/src/app/organisms/join-alias/JoinAlias.scss
deleted file mode 100644
index b3684b0bed..0000000000
--- a/src/app/organisms/join-alias/JoinAlias.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-@use '../../partials/dir';
-
-.join-alias {
- padding: var(--sp-normal);
- @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
-
- & > *:not(:first-child) {
- margin-top: var(--sp-normal);
- }
-
- &__error {
- color: var(--tc-danger-high);
- margin-top: var(--sp-extra-tight) !important;
- }
-
- &__btn {
- display: flex;
- gap: var(--sp-normal);
- }
-}
\ No newline at end of file
diff --git a/src/app/organisms/profile-viewer/ProfileViewer.jsx b/src/app/organisms/profile-viewer/ProfileViewer.jsx
deleted file mode 100644
index 1ed896920a..0000000000
--- a/src/app/organisms/profile-viewer/ProfileViewer.jsx
+++ /dev/null
@@ -1,439 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import PropTypes from 'prop-types';
-import './ProfileViewer.scss';
-import { EventTimeline } from 'matrix-js-sdk';
-
-import cons from '../../../client/state/cons';
-import navigation from '../../../client/state/navigation';
-import { openReusableContextMenu } from '../../../client/action/navigation';
-import * as roomActions from '../../../client/action/room';
-
-import {
- getUsername,
- getUsernameOfRoomMember,
- getPowerLabel,
- hasDevices,
-} from '../../../util/matrixUtil';
-import { getEventCords } from '../../../util/common';
-import colorMXID from '../../../util/colorMXID';
-
-import Text from '../../atoms/text/Text';
-import Chip from '../../atoms/chip/Chip';
-import IconButton from '../../atoms/button/IconButton';
-import Input from '../../atoms/input/Input';
-import Avatar from '../../atoms/avatar/Avatar';
-import Button from '../../atoms/button/Button';
-import { MenuItem } from '../../atoms/context-menu/ContextMenu';
-import PowerLevelSelector from '../../molecules/power-level-selector/PowerLevelSelector';
-import Dialog from '../../molecules/dialog/Dialog';
-
-import ShieldEmptyIC from '../../../../public/res/ic/outlined/shield-empty.svg';
-import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
-import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-
-import { useForceUpdate } from '../../hooks/useForceUpdate';
-import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
-import { useRoomNavigate } from '../../hooks/useRoomNavigate';
-import { getDMRoomFor } from '../../utils/matrix';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
-
-function ModerationTools({ roomId, userId }) {
- const mx = useMatrixClient();
- const room = mx.getRoom(roomId);
- const roomMember = room.getMember(userId);
-
- const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
- const powerLevel = roomMember?.powerLevel || 0;
- const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
- const canIKick =
- roomMember?.membership === 'join' &&
- roomState?.hasSufficientPowerLevelFor('kick', myPowerLevel) &&
- powerLevel < myPowerLevel;
- const canIBan =
- ['join', 'leave'].includes(roomMember?.membership) &&
- roomState?.hasSufficientPowerLevelFor('ban', myPowerLevel) &&
- powerLevel < myPowerLevel;
-
- const handleKick = (e) => {
- e.preventDefault();
- const kickReason = e.target.elements['kick-reason']?.value.trim();
- mx.kick(roomId, userId, kickReason !== '' ? kickReason : undefined);
- };
-
- const handleBan = (e) => {
- e.preventDefault();
- const banReason = e.target.elements['ban-reason']?.value.trim();
- mx.ban(roomId, userId, banReason !== '' ? banReason : undefined);
- };
-
- return (
-
- {canIKick && (
-
- )}
- {canIBan && (
-
- )}
-
- );
-}
-ModerationTools.propTypes = {
- roomId: PropTypes.string.isRequired,
- userId: PropTypes.string.isRequired,
-};
-
-function SessionInfo({ userId }) {
- const [devices, setDevices] = useState(null);
- const [isVisible, setIsVisible] = useState(false);
- const mx = useMatrixClient();
-
- useEffect(() => {
- let isUnmounted = false;
-
- async function loadDevices() {
- try {
- const crypto = mx.getCrypto();
- const userToDevices = await crypto.getUserDeviceInfo([userId], true);
- const myDevices = Array.from(userToDevices.get(userId).values());
-
- if (isUnmounted) return;
- setDevices(myDevices);
- } catch {
- setDevices([]);
- }
- }
- loadDevices();
-
- return () => {
- isUnmounted = true;
- };
- }, [mx, userId]);
-
- function renderSessionChips() {
- if (!isVisible) return null;
- return (
-
- {devices === null && Loading sessions...}
- {devices?.length === 0 && No session found.}
- {devices !== null &&
- devices.map((device) => (
-
- ))}
-
- );
- }
-
- return (
-
-
- {renderSessionChips()}
-
- );
-}
-
-SessionInfo.propTypes = {
- userId: PropTypes.string.isRequired,
-};
-
-function ProfileFooter({ roomId, userId, onRequestClose }) {
- const [isCreatingDM, setIsCreatingDM] = useState(false);
- const [isIgnoring, setIsIgnoring] = useState(false);
- const mx = useMatrixClient();
- const [isUserIgnored, setIsUserIgnored] = useState(mx.isUserIgnored(userId));
-
- const isMountedRef = useRef(true);
- const { navigateRoom } = useRoomNavigate();
- const room = mx.getRoom(roomId);
- const member = room.getMember(userId);
- const isInvitable = member?.membership !== 'join' && member?.membership !== 'ban';
-
- const [isInviting, setIsInviting] = useState(false);
- const [isInvited, setIsInvited] = useState(member?.membership === 'invite');
-
- const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
- const userPL = room.getMember(userId)?.powerLevel || 0;
- const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
-
- const canIKick =
- roomState?.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
-
- const isBanned = member?.membership === 'ban';
-
- const onCreated = (dmRoomId) => {
- if (isMountedRef.current === false) return;
- setIsCreatingDM(false);
- navigateRoom(dmRoomId);
- onRequestClose();
- };
-
- useEffect(() => {
- setIsUserIgnored(mx.isUserIgnored(userId));
- setIsIgnoring(false);
- setIsInviting(false);
- }, [mx, userId]);
-
- const openDM = async () => {
- // Check and open if user already have a DM with userId.
- const dmRoomId = getDMRoomFor(mx, userId)?.roomId;
- if (dmRoomId) {
- navigateRoom(dmRoomId);
- onRequestClose();
- return;
- }
-
- // Create new DM
- try {
- setIsCreatingDM(true);
- const result = await roomActions.createDM(mx, userId, await hasDevices(mx, userId));
- onCreated(result.room_id);
- } catch {
- if (isMountedRef.current === false) return;
- setIsCreatingDM(false);
- }
- };
-
- const toggleIgnore = async () => {
- const isIgnored = mx.getIgnoredUsers().includes(userId);
-
- try {
- setIsIgnoring(true);
- if (isIgnored) {
- await roomActions.unignore(mx, [userId]);
- } else {
- await roomActions.ignore(mx, [userId]);
- }
-
- if (isMountedRef.current === false) return;
- setIsUserIgnored(!isIgnored);
- setIsIgnoring(false);
- } catch {
- setIsIgnoring(false);
- }
- };
-
- const toggleInvite = async () => {
- try {
- setIsInviting(true);
- let isInviteSent = false;
- if (isInvited) await mx.kick(roomId, userId);
- else {
- await mx.invite(roomId, userId);
- isInviteSent = true;
- }
- if (isMountedRef.current === false) return;
- setIsInvited(isInviteSent);
- setIsInviting(false);
- } catch {
- setIsInviting(false);
- }
- };
-
- return (
-
-
- {isCreatingDM ? 'Creating room...' : 'Message'}
-
- {isBanned && canIKick && (
- mx.unban(roomId, userId)}>
- Unban
-
- )}
- {(isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
-
- {isInvited
- ? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
- : `${isInviting ? 'Inviting...' : 'Invite'}`}
-
- )}
-
- {isUserIgnored
- ? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
- : `${isIgnoring ? 'Ignoring...' : 'Ignore'}`}
-
-
- );
-}
-ProfileFooter.propTypes = {
- roomId: PropTypes.string.isRequired,
- userId: PropTypes.string.isRequired,
- onRequestClose: PropTypes.func.isRequired,
-};
-
-function useToggleDialog() {
- const [isOpen, setIsOpen] = useState(false);
- const [roomId, setRoomId] = useState(null);
- const [userId, setUserId] = useState(null);
-
- useEffect(() => {
- const loadProfile = (uId, rId) => {
- setIsOpen(true);
- setUserId(uId);
- setRoomId(rId);
- };
- navigation.on(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
- return () => {
- navigation.removeListener(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
- };
- }, []);
-
- const closeDialog = () => setIsOpen(false);
-
- const afterClose = () => {
- setUserId(null);
- setRoomId(null);
- };
-
- return [isOpen, roomId, userId, closeDialog, afterClose];
-}
-
-function useRerenderOnProfileChange(roomId, userId) {
- const mx = useMatrixClient();
- const [, forceUpdate] = useForceUpdate();
- useEffect(() => {
- const handleProfileChange = (mEvent, member) => {
- if (
- mEvent.getRoomId() === roomId &&
- (member.userId === userId || member.userId === mx.getUserId())
- ) {
- forceUpdate();
- }
- };
- mx.on('RoomMember.powerLevel', handleProfileChange);
- mx.on('RoomMember.membership', handleProfileChange);
- return () => {
- mx.removeListener('RoomMember.powerLevel', handleProfileChange);
- mx.removeListener('RoomMember.membership', handleProfileChange);
- };
- }, [mx, roomId, userId]);
-}
-
-function ProfileViewer() {
- const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog();
- useRerenderOnProfileChange(roomId, userId);
- const useAuthentication = useMediaAuthentication();
-
- const mx = useMatrixClient();
- const room = mx.getRoom(roomId);
-
- const renderProfile = () => {
- const roomMember = room.getMember(userId);
- const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(mx, userId);
- const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
- const avatarUrl =
- avatarMxc && avatarMxc !== 'null'
- ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop', undefined, undefined, useAuthentication)
- : null;
-
- const powerLevel = roomMember?.powerLevel || 0;
- const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
-
- const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
- const canChangeRole =
- roomState?.maySendEvent('m.room.power_levels', mx.getUserId()) &&
- (powerLevel < myPowerLevel || userId === mx.getUserId());
-
- const handleChangePowerLevel = async (newPowerLevel) => {
- if (newPowerLevel === powerLevel) return;
- const SHARED_POWER_MSG =
- 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
- const DEMOTING_MYSELF_MSG =
- 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
-
- const isSharedPower = newPowerLevel === myPowerLevel;
- const isDemotingMyself = userId === mx.getUserId();
- if (isSharedPower || isDemotingMyself) {
- const isConfirmed = await confirmDialog(
- 'Change power level',
- isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
- 'Change',
- 'caution'
- );
- if (!isConfirmed) return;
- roomActions.setPowerLevel(mx, roomId, userId, newPowerLevel);
- } else {
- roomActions.setPowerLevel(mx, roomId, userId, newPowerLevel);
- }
- };
-
- const handlePowerSelector = (e) => {
- openReusableContextMenu('bottom', getEventCords(e, '.btn-surface'), (closeMenu) => (
- {
- closeMenu();
- handleChangePowerLevel(pl);
- }}
- />
- ));
- };
-
- return (
-
-
-
-
-
- {username}
-
- {userId}
-
-
- Role
-
- {`${getPowerLabel(powerLevel) || 'Member'} - ${powerLevel}`}
-
-
-
-
-
- {userId !== mx.getUserId() && (
-
- )}
-
- );
- };
-
- return (
- }
- >
- {roomId ? renderProfile() : }
-
- );
-}
-
-export default ProfileViewer;
diff --git a/src/app/organisms/profile-viewer/ProfileViewer.scss b/src/app/organisms/profile-viewer/ProfileViewer.scss
deleted file mode 100644
index 1401b7779f..0000000000
--- a/src/app/organisms/profile-viewer/ProfileViewer.scss
+++ /dev/null
@@ -1,110 +0,0 @@
-@use '../../partials/flex';
-@use '../../partials/dir';
-
-.profile-viewer__dialog {
- & .dialog__content__wrapper {
- position: relative;
- }
- & .dialog__content-container {
- padding-top: var(--sp-normal);
- padding-bottom: 89px;
- @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
- }
-}
-
-.profile-viewer {
- &__user {
- display: flex;
- padding-bottom: var(--sp-normal);
-
- &__info {
- align-self: flex-end;
- flex: 1;
- min-width: 0;
-
- margin: 0 var(--sp-normal);
-
- & .text {
- white-space: pre-wrap;
- word-break: break-word;
- }
- }
- &__role {
- align-self: flex-end;
- & > .text {
- margin-bottom: var(--sp-ultra-tight);
- }
- }
- }
-
- & .session-info {
- margin-top: var(--sp-normal);
- }
-
- &__buttons {
- position: absolute;
- left: 0;
- bottom: 0;
-
- width: 100%;
- padding: var(--sp-normal);
- background-color: var(--bg-surface);
- border-top: 1px solid var(--bg-surface-border);
- display: flex;
-
- & > *:nth-child(2n) {
- margin: 0 var(--sp-normal)
- }
- & > *:last-child {
- @include dir.side(margin, auto, 0);
- }
- }
-}
-
-.profile-viewer__admin-tool {
- .setting-tile {
- margin-top: var(--sp-loose);
- }
-}
-
-.moderation-tools {
- & > form {
- margin: var(--sp-normal) 0;
- display: flex;
- align-items: flex-end;
- & .input-container {
- @extend .cp-fx__item-one;
- @include dir.side(margin, 0, var(--sp-tight));
- }
- & button {
- height: 46px;
- }
- }
-}
-
-.session-info {
- box-shadow: var(--bs-surface-border);
- border-radius: var(--bo-radius);
- overflow: hidden;
-
- & .context-menu__item button {
- padding: var(--sp-extra-tight);
- & .ic-raw {
- @include dir.side(margin, 0, var(--sp-extra-tight));
- }
- }
-
- &__chips {
- border-top: 1px solid var(--bg-surface-border);
- padding: var(--sp-tight);
- padding-top: var(--sp-ultra-tight);
-
- & > .text {
- margin-top: var(--sp-extra-tight);
- }
- & .chip {
- margin-top: var(--sp-extra-tight);
- @include dir.side(margin, 0, var(--sp-extra-tight));
- }
- }
-}
\ No newline at end of file
diff --git a/src/app/organisms/pw/Dialogs.jsx b/src/app/organisms/pw/Dialogs.jsx
deleted file mode 100644
index 7fb18daa33..0000000000
--- a/src/app/organisms/pw/Dialogs.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-
-import ProfileViewer from '../profile-viewer/ProfileViewer';
-import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting';
-import Search from '../search/Search';
-import CreateRoom from '../create-room/CreateRoom';
-import JoinAlias from '../join-alias/JoinAlias';
-
-import ReusableDialog from '../../molecules/dialog/ReusableDialog';
-
-function Dialogs() {
- return (
- <>
-
-
-
-
-
-
-
- >
- );
-}
-
-export default Dialogs;
diff --git a/src/app/organisms/pw/Windows.jsx b/src/app/organisms/pw/Windows.jsx
deleted file mode 100644
index 77452d15e1..0000000000
--- a/src/app/organisms/pw/Windows.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React, { useState, useEffect } from 'react';
-
-import cons from '../../../client/state/cons';
-import navigation from '../../../client/state/navigation';
-
-import InviteUser from '../invite-user/InviteUser';
-
-function Windows() {
- const [inviteUser, changeInviteUser] = useState({
- isOpen: false,
- roomId: undefined,
- term: undefined,
- });
-
- function openInviteUser(roomId, searchTerm) {
- changeInviteUser({
- isOpen: true,
- roomId,
- searchTerm,
- });
- }
-
- useEffect(() => {
- navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
- return () => {
- navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
- };
- }, []);
-
- return (
- changeInviteUser({ isOpen: false, roomId: undefined })}
- />
- );
-}
-
-export default Windows;
diff --git a/src/app/organisms/search/Search.jsx b/src/app/organisms/search/Search.jsx
deleted file mode 100644
index ebdac3962e..0000000000
--- a/src/app/organisms/search/Search.jsx
+++ /dev/null
@@ -1,265 +0,0 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
-import { useAtomValue } from 'jotai';
-import './Search.scss';
-
-import cons from '../../../client/state/cons';
-import navigation from '../../../client/state/navigation';
-import AsyncSearch from '../../../util/AsyncSearch';
-import { joinRuleToIconSrc } from '../../../util/matrixUtil';
-
-import Text from '../../atoms/text/Text';
-import RawIcon from '../../atoms/system-icons/RawIcon';
-import IconButton from '../../atoms/button/IconButton';
-import Input from '../../atoms/input/Input';
-import RawModal from '../../atoms/modal/RawModal';
-import ScrollView from '../../atoms/scroll/ScrollView';
-import RoomSelector from '../../molecules/room-selector/RoomSelector';
-
-import SearchIC from '../../../../public/res/ic/outlined/search.svg';
-import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
-import { useRoomNavigate } from '../../hooks/useRoomNavigate';
-import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
-import { roomToUnreadAtom } from '../../state/room/roomToUnread';
-import { roomToParentsAtom } from '../../state/room/roomToParents';
-import { allRoomsAtom } from '../../state/room-list/roomList';
-import { mDirectAtom } from '../../state/mDirectList';
-import { useKeyDown } from '../../hooks/useKeyDown';
-import { openSearch } from '../../../client/action/navigation';
-import { useMatrixClient } from '../../hooks/useMatrixClient';
-import { factoryRoomIdByActivity } from '../../utils/sort';
-
-function useVisiblityToggle(setResult) {
- const [isOpen, setIsOpen] = useState(false);
-
- useEffect(() => {
- const handleSearchOpen = (term) => {
- setResult({
- term,
- chunk: [],
- });
- setIsOpen(true);
- };
- navigation.on(cons.events.navigation.SEARCH_OPENED, handleSearchOpen);
- return () => {
- navigation.removeListener(cons.events.navigation.SEARCH_OPENED, handleSearchOpen);
- };
- }, []);
-
- useEffect(() => {
- if (isOpen === false) {
- setResult(undefined);
- }
- }, [isOpen]);
-
- useKeyDown(
- window,
- useCallback((event) => {
- // Ctrl/Cmd +
- if (event.ctrlKey || event.metaKey) {
- // open search modal
- if (event.key === 'k') {
- event.preventDefault();
- // means some menu or modal window is open
- if (
- document.body.lastChild.className !== 'ReactModalPortal' ||
- navigation.isRawModalVisible
- ) {
- return;
- }
- openSearch();
- }
- }
- }, [])
- );
-
- const requestClose = () => setIsOpen(false);
-
- return [isOpen, requestClose];
-}
-
-function mapRoomIds(mx, roomIds, directs, roomIdToParents) {
- return roomIds.map((roomId) => {
- const room = mx.getRoom(roomId);
- const parentSet = roomIdToParents.get(roomId);
- const parentNames = parentSet ? [] : undefined;
- parentSet?.forEach((parentId) => parentNames.push(mx.getRoom(parentId).name));
-
- const parents = parentNames ? parentNames.join(', ') : null;
-
- let type = 'room';
- if (room.isSpaceRoom()) type = 'space';
- else if (directs.includes(roomId)) type = 'direct';
-
- return {
- type,
- name: room.name,
- parents,
- roomId,
- room,
- };
- });
-}
-
-function Search() {
- const [result, setResult] = useState(null);
- const [asyncSearch] = useState(new AsyncSearch());
- const [isOpen, requestClose] = useVisiblityToggle(setResult);
- const searchRef = useRef(null);
- const mx = useMatrixClient();
- const { navigateRoom, navigateSpace } = useRoomNavigate();
- const mDirects = useAtomValue(mDirectAtom);
- const spaces = useSpaces(mx, allRoomsAtom);
- const rooms = useRooms(mx, allRoomsAtom, mDirects);
- const directs = useDirects(mx, allRoomsAtom, mDirects);
- const roomToUnread = useAtomValue(roomToUnreadAtom);
- const roomToParents = useAtomValue(roomToParentsAtom);
-
- const handleSearchResults = (chunk, term) => {
- setResult({
- term,
- chunk,
- });
- };
-
- const generateResults = (term) => {
- const prefix = term.match(/^[#@*]/)?.[0];
-
- if (term.length > 1) {
- asyncSearch.search(prefix ? term.slice(1) : term);
- return;
- }
-
- let ids = null;
-
- if (prefix) {
- if (prefix === '#') ids = [...rooms];
- else if (prefix === '@') ids = [...directs];
- else ids = [...spaces];
- } else {
- ids = [...rooms].concat([...directs], [...spaces]);
- }
-
- ids.sort(factoryRoomIdByActivity(mx));
- const mappedIds = mapRoomIds(mx, ids, directs, roomToParents);
- asyncSearch.setup(mappedIds, { keys: 'name', isContain: true, limit: 20 });
- if (prefix) handleSearchResults(mappedIds, prefix);
- else asyncSearch.search(term);
- };
-
- const loadRecentRooms = () => {
- const recentRooms = [];
- handleSearchResults(mapRoomIds(mx, recentRooms, directs, roomToParents).reverse());
- };
-
- const handleAfterOpen = () => {
- searchRef.current.focus();
- loadRecentRooms();
- asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchResults);
-
- if (typeof result.term === 'string') {
- generateResults(result.term);
- searchRef.current.value = result.term;
- }
- };
-
- const handleAfterClose = () => {
- asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchResults);
- };
-
- const handleOnChange = () => {
- const { value } = searchRef.current;
- if (value.length === 0) {
- loadRecentRooms();
- return;
- }
- generateResults(value);
- };
-
- const handleCross = (e) => {
- e.preventDefault();
- const { value } = searchRef.current;
- if (value.length === 0) requestClose();
- else {
- searchRef.current.value = '';
- searchRef.current.focus();
- loadRecentRooms();
- }
- };
-
- const openItem = (roomId, type) => {
- if (type === 'space') navigateSpace(roomId);
- else navigateRoom(roomId);
- requestClose();
- };
-
- const openFirstResult = () => {
- const { chunk } = result;
- if (chunk?.length > 0) {
- const item = chunk[0];
- openItem(item.roomId, item.type);
- }
- };
-
- const renderRoomSelector = (item) => {
- let imageSrc = null;
- let iconSrc = null;
- if (item.type === 'direct') {
- imageSrc =
- item.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
- } else {
- iconSrc = joinRuleToIconSrc(item.room.getJoinRule(), item.type === 'space');
- }
-
- return (
- 0}
- onClick={() => openItem(item.roomId, item.type)}
- />
- );
- };
-
- return (
-
-
-
-
-
-
- {Array.isArray(result?.chunk) && result.chunk.map(renderRoomSelector)}
-
-
-
-
- Type # for rooms, @ for DMs and * for spaces. Hotkey: Ctrl + k
-
-
-
- );
-}
-
-export default Search;
diff --git a/src/app/organisms/search/Search.scss b/src/app/organisms/search/Search.scss
deleted file mode 100644
index 3612d742bb..0000000000
--- a/src/app/organisms/search/Search.scss
+++ /dev/null
@@ -1,80 +0,0 @@
-@use '../../partials/dir';
-
-.search-dialog__modal {
- --modal-height: 380px;
- height: 100%;
- background-color: var(--bg-surface);
-}
-
-.search-dialog {
- display: flex;
- flex-direction: column;
- height: 100%;
- width: 100%;
-
- &__input {
- padding: var(--sp-normal);
- display: flex;
- align-items: center;
- position: relative;
-
- & > .ic-raw {
- position: absolute;
- --away: calc(var(--sp-normal) + var(--sp-tight));
- @include dir.prop(left, var(--away), unset);
- @include dir.prop(right, unset, var(--away));
- }
- & > .ic-btn {
- border-radius: calc(var(--bo-radius) / 2);
- position: absolute;
- --away: calc(var(--sp-normal) + var(--sp-extra-tight));
- @include dir.prop(right, var(--away), unset);
- @include dir.prop(left, unset, var(--away));
- }
- & .input-container {
- min-width: 0;
- flex: 1;
- }
-
- & input {
- padding-left: 40px;
- padding-right: 40px;
- font-size: var(--fs-s1);
- letter-spacing: var(--ls-s1);
- line-height: var(--lh-s1);
- color: var(--tc-surface-high);
- }
- }
- &__content-wrapper {
- min-height: 0;
- flex: 1;
- position: relative;
- &::before,
- &::after {
- position: absolute;
- top: 0;
- z-index: 99;
- content: "";
- display: inline-block;
- width: 100%;
- height: 8px;
- background-image: linear-gradient(to bottom, var(--bg-surface), var(--bg-surface-transparent));
- }
- &::after {
- top: unset;
- bottom: 0;
- background-image: linear-gradient(to bottom, var(--bg-surface-transparent), var(--bg-surface));
- }
- }
-
- &__content {
- padding: var(--sp-extra-tight);
- @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
- }
-
- &__footer {
- padding: var(--sp-tight) var(--sp-normal);
- text-align: center;
- }
-
-}
\ No newline at end of file
diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx
index b16462dffd..935790aca8 100644
--- a/src/app/pages/App.tsx
+++ b/src/app/pages/App.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { RouterProvider } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@@ -13,9 +13,19 @@ import { ScreenSizeProvider, useScreenSize } from '../hooks/useScreenSize';
const queryClient = new QueryClient();
+const useLastNodeToDetectReactPortalEntry = () => {
+ useEffect(() => {
+ const lastDiv = document.createElement('div');
+ lastDiv.setAttribute('data-last-node', 'true');
+ document.body.appendChild(lastDiv);
+ }, []);
+};
+
function App() {
const screenSize = useScreenSize();
+ useLastNodeToDetectReactPortalEntry();
+
return (
diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx
index 89743693c5..ab44c3b27c 100644
--- a/src/app/pages/Router.tsx
+++ b/src/app/pages/Router.tsx
@@ -28,8 +28,9 @@ import {
_ROOM_PATH,
_SEARCH_PATH,
_SERVER_PATH,
+ CREATE_PATH,
+ TO_ROOM_EVENT_PATH,
} from './paths';
-import { isAuthenticated } from '../../client/state/auth';
import {
getAppPathFromHref,
getExploreFeaturedPath,
@@ -61,6 +62,14 @@ import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
import { RoomSettingsRenderer } from '../features/room-settings';
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
import { SpaceSettingsRenderer } from '../features/space-settings';
+import { UserRoomProfileRenderer } from '../components/UserRoomProfileRenderer';
+import { CreateRoomModalRenderer } from '../features/create-room';
+import { HomeCreateRoom } from './client/home/CreateRoom';
+import { Create } from './client/create';
+import { CreateSpaceModalRenderer } from '../features/create-space';
+import { SearchModalRenderer } from '../features/search';
+import { getFallbackSession } from '../state/sessions';
+import { ToRoomEvent } from './client/ToRoomEvent';
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
const { hashRouter } = clientConfig;
@@ -71,7 +80,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
{
- if (isAuthenticated()) return redirect(getHomePath());
+ if (getFallbackSession()) return redirect(getHomePath());
const afterLoginPath = getAppPathFromHref(getOriginBaseUrl(), window.location.href);
if (afterLoginPath) setAfterLoginRedirectPath(afterLoginPath);
return redirect(getLoginPath());
@@ -79,7 +88,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
/>
{
- if (isAuthenticated()) {
+ if (getFallbackSession()) {
return redirect(getHomePath());
}
@@ -99,7 +108,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
{
- if (!isAuthenticated()) {
+ if (!getFallbackSession()) {
const afterLoginPath = getAppPathFromHref(
getOriginBaseUrl(hashRouter),
window.location.href
@@ -125,6 +134,10 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
>
+
+
+
+
@@ -152,7 +165,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
}
>
{mobile ? null : } />}
- create
} />
+ } />
join} />
} />
} />
} />
+ } />
} />
} />
+ } />
Page not found} />
diff --git a/src/app/pages/ThemeManager.tsx b/src/app/pages/ThemeManager.tsx
index 7e6039a82d..69d50cdb9c 100644
--- a/src/app/pages/ThemeManager.tsx
+++ b/src/app/pages/ThemeManager.tsx
@@ -8,6 +8,8 @@ import {
useActiveTheme,
useSystemThemeKind,
} from '../hooks/useTheme';
+import { useSetting } from '../state/hooks/settings';
+import { settingsAtom } from '../state/settings';
export function UnAuthRouteThemeManager() {
const systemThemeKind = useSystemThemeKind();
@@ -28,13 +30,20 @@ export function UnAuthRouteThemeManager() {
export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
const activeTheme = useActiveTheme();
+ const [monochromeMode] = useSetting(settingsAtom, 'monochromeMode');
useEffect(() => {
document.body.className = '';
document.body.classList.add(configClass, varsClass);
document.body.classList.add(...activeTheme.classNames);
- }, [activeTheme]);
+
+ if (monochromeMode) {
+ document.body.style.filter = 'grayscale(1)';
+ } else {
+ document.body.style.filter = '';
+ }
+ }, [activeTheme, monochromeMode]);
return {children};
}
diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx
index ff2fdb9b89..53191f33fe 100644
--- a/src/app/pages/auth/AuthFooter.tsx
+++ b/src/app/pages/auth/AuthFooter.tsx
@@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank"
rel="noreferrer"
>
- v4.8.1
+ v4.9.1
Twitter
diff --git a/src/app/pages/auth/SSOLogin.tsx b/src/app/pages/auth/SSOLogin.tsx
index d0cdaeb68a..3ff1a22950 100644
--- a/src/app/pages/auth/SSOLogin.tsx
+++ b/src/app/pages/auth/SSOLogin.tsx
@@ -1,19 +1,21 @@
import { Avatar, AvatarImage, Box, Button, Text } from 'folds';
-import { IIdentityProvider, createClient } from 'matrix-js-sdk';
+import { IIdentityProvider, SSOAction, createClient } from 'matrix-js-sdk';
import React, { useMemo } from 'react';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
type SSOLoginProps = {
providers?: IIdentityProvider[];
redirectUrl: string;
+ action?: SSOAction;
saveScreenSpace?: boolean;
};
-export function SSOLogin({ providers, redirectUrl, saveScreenSpace }: SSOLoginProps) {
+export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SSOLoginProps) {
const discovery = useAutoDiscoveryInfo();
const baseUrl = discovery['m.homeserver'].base_url;
const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
- const getSSOIdUrl = (ssoId?: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId);
+ const getSSOIdUrl = (ssoId?: string): string =>
+ mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId, action);
const withoutIcon = providers
? providers.find(
diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx
index 6b9f1223c6..2f04a73372 100644
--- a/src/app/pages/auth/login/Login.tsx
+++ b/src/app/pages/auth/login/Login.tsx
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import { Box, Text, color } from 'folds';
import { Link, useSearchParams } from 'react-router-dom';
+import { SSOAction } from 'matrix-js-sdk';
import { useAuthFlows } from '../../../hooks/useAuthFlows';
import { useAuthServer } from '../../../hooks/useAuthServer';
import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
@@ -76,6 +77,7 @@ export function Login() {
diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx
index 90c305d001..62f46dd2cf 100644
--- a/src/app/pages/auth/login/PasswordLoginForm.tsx
+++ b/src/app/pages/auth/login/PasswordLoginForm.tsx
@@ -72,19 +72,19 @@ function UsernameHint({ server }: { server: string }) {
Username:
{' '}
- johndoe
+ user123
Matrix ID:
- {` @johndoe:${server}`}
+ {` @user123:${server}`}
Email:
- {` johndoe@${server}`}
+ {` user123@${server}`}
diff --git a/src/app/pages/auth/login/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts
index 7e1c715340..ba5b0bc9b1 100644
--- a/src/app/pages/auth/login/loginUtil.ts
+++ b/src/app/pages/auth/login/loginUtil.ts
@@ -4,13 +4,13 @@ import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ClientConfig, clientAllowedServer } from '../../../hooks/useClientConfig';
import { autoDiscovery, specVersions } from '../../../cs-api';
-import { updateLocalStore } from '../../../../client/action/auth';
import { ErrorCode } from '../../../cs-errorcode';
import {
deleteAfterLoginRedirectPath,
getAfterLoginRedirectPath,
} from '../../afterLoginRedirectPath';
import { getHomePath } from '../../pathUtils';
+import { setFallbackSession } from '../../../state/sessions';
export enum GetBaseUrlError {
NotAllow = 'NotAllow',
@@ -114,7 +114,7 @@ export const useLoginComplete = (data?: CustomLoginResponse) => {
useEffect(() => {
if (data) {
const { response: loginRes, baseUrl: loginBaseUrl } = data;
- updateLocalStore(loginRes.access_token, loginRes.device_id, loginRes.user_id, loginBaseUrl);
+ setFallbackSession(loginRes.access_token, loginRes.device_id, loginRes.user_id, loginBaseUrl);
const afterLoginRedirectUrl = getAfterLoginRedirectPath();
deleteAfterLoginRedirectPath();
navigate(afterLoginRedirectUrl ?? getHomePath(), { replace: true });
diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx
index d2986d701d..7176489ba3 100644
--- a/src/app/pages/auth/register/Register.tsx
+++ b/src/app/pages/auth/register/Register.tsx
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import { Box, Text, color } from 'folds';
import { Link, useSearchParams } from 'react-router-dom';
+import { SSOAction } from 'matrix-js-sdk';
import { useAuthServer } from '../../../hooks/useAuthServer';
import { RegisterFlowStatus, useAuthFlows } from '../../../hooks/useAuthFlows';
import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
@@ -83,6 +84,7 @@ export function Register() {
diff --git a/src/app/pages/auth/register/registerUtil.ts b/src/app/pages/auth/register/registerUtil.ts
index e8145780a2..86a38cb527 100644
--- a/src/app/pages/auth/register/registerUtil.ts
+++ b/src/app/pages/auth/register/registerUtil.ts
@@ -8,7 +8,6 @@ import {
} from 'matrix-js-sdk';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
-import { updateLocalStore } from '../../../../client/action/auth';
import { LoginPathSearchParams } from '../../paths';
import { ErrorCode } from '../../../cs-errorcode';
import {
@@ -17,6 +16,7 @@ import {
} from '../../afterLoginRedirectPath';
import { getHomePath, getLoginPath, withSearchParam } from '../../pathUtils';
import { getMxIdLocalPart, getMxIdServer } from '../../../utils/matrix';
+import { setFallbackSession } from '../../../state/sessions';
export enum RegisterError {
UserTaken = 'UserTaken',
@@ -119,7 +119,7 @@ export const useRegisterComplete = (data?: CustomRegisterResponse) => {
const deviceId = response.device_id;
if (accessToken && deviceId) {
- updateLocalStore(accessToken, deviceId, userId, baseUrl);
+ setFallbackSession(accessToken, deviceId, userId, baseUrl);
const afterLoginRedirectPath = getAfterLoginRedirectPath();
deleteAfterLoginRedirectPath();
navigate(afterLoginRedirectPath ?? getHomePath(), { replace: true });
diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx
index ce952bfc67..67b984c653 100644
--- a/src/app/pages/client/ClientNonUIFeatures.tsx
+++ b/src/app/pages/client/ClientNonUIFeatures.tsx
@@ -1,7 +1,7 @@
import { useAtomValue } from 'jotai';
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
-import { RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
+import { EventType, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
import { roomToUnreadAtom, unreadEqual, unreadInfoToUnread } from '../../state/room/roomToUnread';
import LogoSVG from '../../../../public/res/svg/cinny.svg';
import LogoUnreadSVG from '../../../../public/res/svg/cinny-unread.svg';
@@ -26,6 +26,8 @@ import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useSelectedRoom } from '../../hooks/router/useSelectedRoom';
import { useInboxNotificationsSelected } from '../../hooks/router/useInbox';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useRoomNavigate } from '../../hooks/useRoomNavigate';
+import { registrationAtom } from '../../state/serviceWorkerRegistration';
function SystemEmojiFeature() {
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
@@ -53,11 +55,17 @@ function PageZoomFeature() {
function FaviconUpdater() {
const roomToUnread = useAtomValue(roomToUnreadAtom);
+ const [usePushNotifications] = useSetting(settingsAtom, 'usePushNotifications');
+ const registration = useAtomValue(registrationAtom);
useEffect(() => {
let notification = false;
let highlight = false;
+ let total = 0;
roomToUnread.forEach((unread) => {
+ if (unread.from === null) {
+ total += unread.total;
+ }
if (unread.total > 0) {
notification = true;
}
@@ -71,7 +79,18 @@ function FaviconUpdater() {
} else {
setFavicon(LogoSVG);
}
- }, [roomToUnread]);
+ try {
+ navigator.setAppBadge(total);
+ if (usePushNotifications && total === 0) {
+ registration.getNotifications()
+ .then((pushNotifications) => pushNotifications
+ .forEach((pushNotification) => pushNotification.close()));
+ navigator.clearAppBadge();
+ }
+ } catch (e) {
+ // Likely Firefox/Gecko-based and doesn't support badging API
+ }
+ }, [roomToUnread, usePushNotifications, registration]);
return null;
}
@@ -83,7 +102,8 @@ function InviteNotifications() {
const mx = useMatrixClient();
const navigate = useNavigate();
- const [showNotifications] = useSetting(settingsAtom, 'showNotifications');
+ const [showNotifications] = useSetting(settingsAtom, 'useInAppNotifications');
+ const [usePushNotifications] = useSetting(settingsAtom, 'usePushNotifications');
const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds');
const notify = useCallback(
@@ -109,6 +129,7 @@ function InviteNotifications() {
}, []);
useEffect(() => {
+ if (usePushNotifications && document.visibilityState !== "visible") return;
if (invites.length > perviousInviteLen && mx.getSyncState() === 'SYNCING') {
if (showNotifications && notificationPermission('granted')) {
notify(invites.length - perviousInviteLen);
@@ -118,7 +139,16 @@ function InviteNotifications() {
playSound();
}
}
- }, [mx, invites, perviousInviteLen, showNotifications, notificationSound, notify, playSound]);
+ }, [
+ mx,
+ invites,
+ perviousInviteLen,
+ showNotifications,
+ usePushNotifications,
+ notificationSound,
+ notify,
+ playSound
+ ]);
return (
// eslint-disable-next-line jsx-a11y/media-has-caption
@@ -134,7 +164,8 @@ function MessageNotifications() {
const unreadCacheRef = useRef