Compare commits
169 Commits
336aea463b
...
renovate/e
| Author | SHA1 | Date | |
|---|---|---|---|
| 874410770c | |||
| e88961a43a | |||
| f6169d690f | |||
| 5221260e26 | |||
| c562152019 | |||
| 2ea20e6de4 | |||
| 29939d598f | |||
| b2ab5aceca | |||
| 2a5def11eb | |||
| c9c3eca8f8 | |||
| 1bdb130fb4 | |||
| 2c2a81c271 | |||
| 09099bce9b | |||
| 16595f1fe2 | |||
| f19b3e802b | |||
| f68bfb9bee | |||
| 2d17188fbe | |||
| ea0464a5b0 | |||
| ed92669aba | |||
| 50b0f6d479 | |||
| 953764a3a4 | |||
| fa4cb2c556 | |||
| 23cfda38c6 | |||
| 894dbe033c | |||
| ac32758de6 | |||
| c2944f203b | |||
| 01dc8bf910 | |||
| 4f3ad57d3a | |||
| 94f8a34288 | |||
| 1df0855e47 | |||
| f87328a1b2 | |||
| 8b1356064e | |||
| b0ceeba74b | |||
| 418bffe746 | |||
| b7c06eb1b6 | |||
| 7805412b07 | |||
| d98854e4dd | |||
| 8cd9da28e0 | |||
| edfcbdd1f8 | |||
| 7c6520a2ee | |||
| 45609a2ae7 | |||
| 286a48b3b3 | |||
| 054940716a | |||
| 0f374f7125 | |||
| 9e35af3341 | |||
| e864925f63 | |||
| f544946d06 | |||
| 0671911d20 | |||
| e5f5fe6e90 | |||
| 37d31b42b3 | |||
| 474bacfd23 | |||
| 893ff2cbbe | |||
| a738ab5525 | |||
| 78c35524d0 | |||
| f566847809 | |||
| d8e6090717 | |||
| 6a26d9a4c0 | |||
| c2bed332da | |||
| 7d2aedef97 | |||
| f12a2ca1ba | |||
| f0d7fbf82c | |||
| 5220f3e873 | |||
| 7e6f9bd648 | |||
| e3e53abe34 | |||
| 65ee2ac6dc | |||
| 8faa3592c4 | |||
| 690c305207 | |||
| 5c8b7a2082 | |||
| 9f9fd28b6a | |||
| 3d98c276b0 | |||
| aec1594a98 | |||
| 366238e9b8 | |||
| 888f629e8a | |||
| 924433ca40 | |||
| 99b4f67275 | |||
| a6ff89e80d | |||
| 6d3ba0ccae | |||
| ec3377692d | |||
| 574761d39b | |||
| 09c5b9f187 | |||
| 11f03f88c6 | |||
| 2ebf17f065 | |||
| b9bfe17314 | |||
| f56c646596 | |||
| 697fcc481e | |||
| 2fbca2c411 | |||
| 399b13fdbc | |||
| 166b860c1a | |||
| 96b02dd73a | |||
| 5dd4aa6c0e | |||
| 8f4480e555 | |||
| 06e1f60314 | |||
| a7432a4014 | |||
| be5e7eb328 | |||
| c1e703c4b4 | |||
| 071aad8147 | |||
| f4b3c0aa16 | |||
| daca7410d7 | |||
| 23074ac354 | |||
| 36bd8d0672 | |||
| 6162555702 | |||
| 951d0db0b7 | |||
| ef90aba489 | |||
| 6b39fd11bd | |||
| bf6561fa87 | |||
| e5feecc703 | |||
| 153ad14a51 | |||
| e5494e51a3 | |||
| f04ab4591b | |||
| ca2cdb2f79 | |||
| 795a12c8d0 | |||
| 79e78006fa | |||
| 18c0fbef3c | |||
| 0ac6fc4ac3 | |||
| 2aaced17d8 | |||
| 9da2a9e9b3 | |||
| 9595ff2e71 | |||
| 5bd62d7683 | |||
| 7e747b50f3 | |||
| a9a5d60edd | |||
| 89dbc252e8 | |||
| 3a6b2c6cf2 | |||
| 98c813b220 | |||
| ceb7859169 | |||
| 96e597ca59 | |||
| d9630fbc4c | |||
| 4d7db2de2a | |||
| 4c0be88570 | |||
| 2f933a247f | |||
| f6a7132d43 | |||
| 1089b5a6a6 | |||
| 28a1b5f4f0 | |||
| acf91c3f0e | |||
| fadb9e6d46 | |||
| 17bad4fcfd | |||
| b3dfc35103 | |||
| 602f663217 | |||
| 5ebfbf6aec | |||
| aad0a74ad5 | |||
| 382e24e17b | |||
| 1876c7b43d | |||
| 73af601a16 | |||
| 6247463c70 | |||
| 9425ed9a12 | |||
| 430ad85c37 | |||
| 29e50bd70c | |||
| 0e83e804d8 | |||
| bd674bfb67 | |||
| fec81ac92e | |||
| d3e25eed9e | |||
| ba5f5f2557 | |||
| 7e548ad5d1 | |||
| 7b63bb0d05 | |||
| 788018451a | |||
| 7b3a2d6a3f | |||
| 0d462f848d | |||
| 9c6c338919 | |||
| 8a4570a044 | |||
| e51fc6b4bb | |||
| 0f68d59798 | |||
| 5ad23005be | |||
| 4e096a1d49 | |||
| ac2a361b77 | |||
| 24f8d67020 | |||
| 5bcee2ea9d | |||
| 48d9444dde | |||
| bcdfe87107 | |||
| 5088699c15 | |||
| 854b474970 |
@@ -6,7 +6,7 @@ name: default
|
||||
steps:
|
||||
# Frontend
|
||||
- name: web_build
|
||||
image: node:23
|
||||
image: node:25
|
||||
volumes:
|
||||
- name: web_app
|
||||
path: /tmp/web_build
|
||||
@@ -89,7 +89,7 @@ steps:
|
||||
path: /tmp/release
|
||||
environment:
|
||||
PLUGIN_API_KEY:
|
||||
from_secret: API_KEY
|
||||
from_secret: GITEA_API_KEY # needs permission write:repository
|
||||
settings:
|
||||
base_url: https://gitea.communiquons.org
|
||||
files: /tmp/release/*
|
||||
|
||||
6
docker_prod/.env.sample
Normal file
6
docker_prod/.env.sample
Normal file
@@ -0,0 +1,6 @@
|
||||
REDIS_PASS=redis_password
|
||||
WEBSITE_ORIGIN=http://localhost:8000
|
||||
APP_SECRET=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret
|
||||
AUTH_SECRET_KEY=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret
|
||||
OIDC_CLIENT_ID=bar
|
||||
OIDC_CLIENT_SECRET=foo
|
||||
3
docker_prod/.gitignore
vendored
Normal file
3
docker_prod/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
storage
|
||||
auth/users.json
|
||||
44
docker_prod/README.md
Normal file
44
docker_prod/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Setup production environment
|
||||
> Sample release deployment configuration. **MUST BE ADAPTED BEFORE REAL PRODUCTION DEPLOYMENT!**
|
||||
|
||||
1. Install prerequisites:
|
||||
1. `docker`
|
||||
2. `docker compose`
|
||||
3. `git`
|
||||
|
||||
2. Clone this git repository:
|
||||
```bash
|
||||
git clone https://gitea.communiquons.org/pierre/MatrixGW
|
||||
cd MatrixGW/docker_prod
|
||||
```
|
||||
|
||||
3. Copy and adapt env values
|
||||
```bash
|
||||
cp .env.sample .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
4. Create required directories:
|
||||
|
||||
```bash
|
||||
mkdir -p storage/{redis-data,redis-conf,synapse,maspostgres,matrixgw}
|
||||
```
|
||||
|
||||
5. Start containers
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
> Note: Before running `docker compose up`, if your user does not belong to the `docker` group, you should run the following command to be able to run docker in rootless mode:
|
||||
>
|
||||
> ```bash
|
||||
> sudo -g docker bash
|
||||
> ```
|
||||
|
||||
6. Done !
|
||||
|
||||
|
||||
* Matrix GW: http://localhost:8000/, the default credentials are `admin` / `admin`
|
||||
* Element: http://localhost:8080 (you will need to create your accounts)
|
||||
* Auth platform: http://localhost:5001
|
||||
5
docker_prod/auth/clients.yaml
Normal file
5
docker_prod/auth/clients.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
- id: ${OIDC_CLIENT_ID}
|
||||
name: MatrixGW
|
||||
description: Matrix Gateway
|
||||
secret: ${OIDC_CLIENT_SECRET}
|
||||
redirect_uri: ${APP_ORIGIN}/oidc_cb
|
||||
102
docker_prod/docker-compose.yaml
Normal file
102
docker_prod/docker-compose.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
services:
|
||||
oidc:
|
||||
image: pierre42100/basic_oidc
|
||||
user: "1000"
|
||||
environment:
|
||||
- LISTEN_ADDRESS=0.0.0.0:9001
|
||||
- STORAGE_PATH=/storage
|
||||
- TOKEN_KEY=$AUTH_SECRET_KEY
|
||||
- WEBSITE_ORIGIN=http://localhost:9001
|
||||
- OIDC_CLIENT_ID=$OIDC_CLIENT_ID
|
||||
- OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET
|
||||
- APP_ORIGIN=$WEBSITE_ORIGIN
|
||||
expose:
|
||||
- 9001
|
||||
ports:
|
||||
- 9001:9001
|
||||
volumes:
|
||||
- ./auth:/storage
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
user: "1000"
|
||||
command: redis-server --requirepass ${REDIS_PASS:-secretredis}
|
||||
expose:
|
||||
- 6379
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
volumes:
|
||||
- ./storage/redis-data:/data
|
||||
- ./storage/redis-conf:/usr/local/etc/redis/redis.conf
|
||||
|
||||
mas:
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:main
|
||||
user: "1000"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- masdb
|
||||
volumes:
|
||||
- ./mas:/config:ro
|
||||
command: server -c /config/config.yaml
|
||||
ports:
|
||||
- "8778:8778/tcp"
|
||||
|
||||
synapse:
|
||||
image: docker.io/matrixdotorg/synapse:latest
|
||||
user: "1000"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SYNAPSE_CONFIG_PATH=/config/homeserver.yaml
|
||||
volumes:
|
||||
- ./storage/synapse:/data
|
||||
- ./synapse:/config:ro
|
||||
ports:
|
||||
- "8448:8448/tcp"
|
||||
|
||||
masdb:
|
||||
image: docker.io/postgres:18-alpine
|
||||
user: "1000"
|
||||
environment:
|
||||
- POSTGRES_DB=masdb
|
||||
- POSTGRES_USER=masdb
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||
- PGDATA=/data
|
||||
volumes:
|
||||
- ./storage/maspostgres:/data
|
||||
|
||||
element:
|
||||
image: docker.io/vectorim/element-web
|
||||
ports:
|
||||
- "8080:80/tcp"
|
||||
volumes:
|
||||
- ./element/config.json:/app/config.json:ro
|
||||
|
||||
matrixgw:
|
||||
image: pierre42100/matrix_gateway
|
||||
user: "1000"
|
||||
ports:
|
||||
- 8000:8000
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./storage/matrixgw:/data
|
||||
network_mode: host
|
||||
environment:
|
||||
- WEBSITE_ORIGIN=${WEBSITE_ORIGIN}
|
||||
- SECRET=${APP_SECRET}
|
||||
- OIDC_CONFIGURATION_URL=http://localhost:9001/.well-known/openid-configuration
|
||||
- OIDC_PROVIDER_NAME=OIDC
|
||||
- OIDC_CLIENT_ID=$OIDC_CLIENT_ID
|
||||
- OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET
|
||||
- REDIS_HOSTNAME=localhost #redis
|
||||
- REDIS_PASSWORD=${REDIS_PASS:-secretredis}
|
||||
- UNSECURE_AUTO_LOGIN_EMAIL=$UNSECURE_AUTO_LOGIN_EMAIL
|
||||
- STORAGE_PATH=/data
|
||||
- MATRIX_HOMESERVER=http://localhost:8448
|
||||
49
docker_prod/element/config.json
Normal file
49
docker_prod/element/config.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "http://localhost:8448",
|
||||
"server_name": "devserver"
|
||||
},
|
||||
"m.identity_server": {
|
||||
"base_url": "https://vector.im"
|
||||
}
|
||||
},
|
||||
"disable_custom_urls": false,
|
||||
"disable_guests": false,
|
||||
"disable_login_language_selector": false,
|
||||
"disable_3pid_login": false,
|
||||
"brand": "Element",
|
||||
"integrations_ui_url": "https://scalar.vector.im/",
|
||||
"integrations_rest_url": "https://scalar.vector.im/api",
|
||||
"integrations_widgets_urls": [
|
||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar.vector.im/api",
|
||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar-staging.vector.im/api",
|
||||
"https://scalar-staging.riot.im/scalar/api"
|
||||
],
|
||||
"default_country_code": "GB",
|
||||
"show_labs_settings": false,
|
||||
"features": {},
|
||||
"default_federate": true,
|
||||
"default_theme": "light",
|
||||
"room_directory": {
|
||||
"servers": ["matrix.org"]
|
||||
},
|
||||
"enable_presence_by_hs_url": {
|
||||
"https://matrix.org": false,
|
||||
"https://matrix-client.matrix.org": false
|
||||
},
|
||||
"setting_defaults": {
|
||||
"breadcrumbs": true
|
||||
},
|
||||
"jitsi": {
|
||||
"preferred_domain": "meet.element.io"
|
||||
},
|
||||
"element_call": {
|
||||
"url": "https://call.element.io",
|
||||
"participant_limit": 8,
|
||||
"brand": "Element Call"
|
||||
},
|
||||
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
|
||||
}
|
||||
113
docker_prod/mas/config.yaml
Normal file
113
docker_prod/mas/config.yaml
Normal file
@@ -0,0 +1,113 @@
|
||||
http:
|
||||
listeners:
|
||||
- name: web
|
||||
resources:
|
||||
- name: discovery
|
||||
- name: human
|
||||
- name: oauth
|
||||
- name: compat
|
||||
- name: graphql
|
||||
- name: assets
|
||||
binds:
|
||||
- address: '[::]:8778'
|
||||
proxy_protocol: false
|
||||
- name: internal
|
||||
resources:
|
||||
- name: health
|
||||
binds:
|
||||
- host: localhost
|
||||
port: 8081
|
||||
proxy_protocol: false
|
||||
trusted_proxies:
|
||||
- 192.168.0.0/16
|
||||
- 172.16.0.0/12
|
||||
- 10.0.0.0/10
|
||||
- 127.0.0.1/8
|
||||
- fd00::/8
|
||||
- ::1/128
|
||||
public_base: http://localhost:8778/
|
||||
issuer: http://localhost:8778/
|
||||
database:
|
||||
uri: postgresql://masdb:changeme@masdb/masdb
|
||||
max_connections: 10
|
||||
min_connections: 0
|
||||
connect_timeout: 30
|
||||
idle_timeout: 600
|
||||
max_lifetime: 1800
|
||||
email:
|
||||
from: '"Authentication Service" <root@localhost>'
|
||||
reply_to: '"Authentication Service" <root@localhost>'
|
||||
transport: blackhole
|
||||
secrets:
|
||||
encryption: 12de9ad7bc2bacfa2ab9b1e3f7f1b3feb802195c8ebe66a8293cdb27f00be471
|
||||
keys:
|
||||
- kid: Bj2PICQ7mf
|
||||
key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAsCYCrrCJA7IuGbTYzP5yZN74QszbzudBUCX6MyN/+36HO2r6
|
||||
xL8x1PRJ+Klx9Y90J9pWuo+cIuEmFLqO+Yfblo9fSQgZVvkWAFpO6Xh8J4z9qg49
|
||||
M8xm0Ct8EnRDZDCEOBnwoDaAB9RTbpJGa1RPVCiamfi+xU+j47Zl4Er5jvLm81O7
|
||||
DSlH9eK8Eih8AxuKTkAbKE1zyXquImE26Mj2dmMRfjDrWV/I8oqE3WFViAKR12Av
|
||||
zw6TUyduiz8nK9pONCF3NIcQvBdHntBz1HlDXv6i0fRvlGIhjNL5LBgo6XQ3rNM1
|
||||
bW2KYOw/iFP0YbfD4/xRjkBPvK2coQ8aRzK2VwIDAQABAoH/G4XU5Xav8ePlUB7x
|
||||
wRYAycINCGL59Vos2lkUvujNFn6uopoUlKlLH/sLk87l/3hqrc9vvbayrsB/Mr3z
|
||||
mQmhReUg/khFrVE+Hs/9hH1O6N8ew3N2HKHTbrNcr4V7AiySfDGRZ3ccihyi7KPu
|
||||
XNbPjlbJ0UUMicfn06ysPl94nt0So0UAmXg+c7sDDqyzh3cY8emedYZ5FCljo/jA
|
||||
F8k40rs7CywLJYMJB9O1vtomgt1xkDRO4F8UrZrriMIcYn0iFKe7i4AH8D6nkgNu
|
||||
/v9Z43Leu8yRKrUvbpH3NaX8DlUSFWAXKpwUWr4sAQgWcLkVgjAXG1v9jCE97qW2
|
||||
f0nBAoGBAOaKrnY5rWeZ74dERnPhSCsYiqRMneQAh7eJR+Er+xu1yF/bxwkhq2tK
|
||||
/txheTK448DqhQRtr095t/v7TMZcPl3bSmybT1CQg/wiMJsgDMZqlC9tofvcq6uz
|
||||
xP8vxMFHd0YSMSP693dkny4MzNY6LuoVWDLT+HxKPJyzGs1alruzAoGBAMOZp5J2
|
||||
3ODcHQlcsGBtj1yVpQ4UXMvrSZF2ygiGK9bagL/f1iAtwACVOh5rgmbiOLSVgmR2
|
||||
n4nupTgSAXMYkjmAmDyEh0PDaRl4WWvYEKp8GMvTPVPvjc6N0dT+y8Mf9bu+LcEt
|
||||
+uZqPOZNbO5Vi+UgGeM9zZpxq/K7dpJmM/jNAoGBALsYHRGxKTsEwFEkZZCxaWIg
|
||||
HpPL4e8hRwL6FC13BeitFBpHQDX27yi5yi+Lo1I4ngz3xk+bvERhYaDLhrkML0j4
|
||||
KGQPfsTBI3vBO3UJA5Ua9XuwG19M7L0BvYPjfmfk2bUyGlM63w4zyMMUfD/3JA+w
|
||||
ls1ZHTWxAZOh/sRdGirlAoGAX16B1+XgmDp6ZeAtlzaUGd5U1eKTxFF6U1SJ+VIB
|
||||
+gYblHI84v+riB06cy6ULDnM0C+9neJAs24KXKZa0pV+Zk8O6yLrGN0kV2jYoL5+
|
||||
kcFkDa13T3+TssxvLNz22LKyi9GUWYZjuQi/nMLPg/1t8k+Oj7/Iia822WkRzRvL
|
||||
51kCgYEAwrN5Us8LR+fThm3C0vhvwv2wap6ccw0qq5+FTN+igAZAmmvKKvhow2Vi
|
||||
LnPKBkc7QvxvQSNoXkdUo4qs3zOQ7DGvJLqSG9pwxFW5X1+78pNEm5OWe8AlT1uZ
|
||||
Jz8Z1/Ae7fr/fFaucW9LkWjcuoPwPLiZ3b7ZQ6phs8qzoL+FpBI=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
- kid: HcRvLHat12
|
||||
key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIOCCFSnkfz1ksln6kus8enQstBTu0q62IGJVzuX0WiXPoAoGCCqGSM49
|
||||
AwEHoUQDQgAEVWPLbvSdxquLAjU3zJLcCWdaxr6QK1tPVbV1IS+87QUMv/zKiCMa
|
||||
fNpwgBXwU7dF0gY507R2yY9pcdTmRtnRug==
|
||||
-----END EC PRIVATE KEY-----
|
||||
- kid: YjMITk5VSn
|
||||
key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDCoPSjaN7qqnPz+vdzHeIy8RZCCtFOqLTkvylM1gz6xOGaVsS63VJw9
|
||||
Td9BtpolZ0egBwYFK4EEACKhZANiAAT8tH88HYBHNiQTSqZzlxElSuSDC0+Xn0O9
|
||||
ukj0xTTVBp8rUM9lCJQAlB8PjS2XK/n0YvYdzysQb3AYqszJa45/rOGvSar30YNE
|
||||
gwpJvu36xNIKZT+nHalNwg069FdjNBc=
|
||||
-----END EC PRIVATE KEY-----
|
||||
- kid: NvFzzeMRU3
|
||||
key: |
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHQCAQEEILJEmFPDGFZoBVBQf1P6h4YfasYsFiu8a6FrFxiJvKXPoAcGBSuBBAAK
|
||||
oUQDQgAE4NY5H3+D8r9GNOhrpbUn2dvLZIzi4A+SiwfqvtvPEmZkW+KDbd2tzKmx
|
||||
maydZBn52QWedVY65snGAEoh9mV1TQ==
|
||||
-----END EC PRIVATE KEY-----
|
||||
passwords:
|
||||
enabled: true
|
||||
schemes:
|
||||
- version: 1
|
||||
algorithm: argon2id
|
||||
minimum_complexity: 0
|
||||
account:
|
||||
password_registration_enabled: true
|
||||
password_registration_email_required: false
|
||||
matrix:
|
||||
kind: synapse
|
||||
homeserver: localhost
|
||||
secret: IhKoLn6jWf1qRRZWvqgaKuIdwD6H0Mvx
|
||||
endpoint: http://synapse:8448/
|
||||
|
||||
policy:
|
||||
data:
|
||||
client_registration:
|
||||
allow_insecure_uris: true
|
||||
41
docker_prod/synapse/homeserver.yaml
Normal file
41
docker_prod/synapse/homeserver.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
# Configuration file for Synapse.
|
||||
#
|
||||
# This is a YAML file: see [1] for a quick introduction. Note in particular
|
||||
# that *indentation is important*: all the elements of a list or dictionary
|
||||
# should have the same indentation.
|
||||
#
|
||||
# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html
|
||||
#
|
||||
# For more information on how to configure Synapse, including a complete accounting of
|
||||
# each option, go to docs/usage/configuration/config_documentation.md or
|
||||
# https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html
|
||||
server_name: "localhost"
|
||||
pid_file: /data/homeserver.pid
|
||||
listeners:
|
||||
- port: 8448
|
||||
tls: false
|
||||
type: http
|
||||
x_forwarded: true
|
||||
resources:
|
||||
- names: [client, federation]
|
||||
compress: false
|
||||
database:
|
||||
name: sqlite3
|
||||
args:
|
||||
database: /data/homeserver.db
|
||||
log_config: "/config/localhost.log.config"
|
||||
media_store_path: /data/media_store
|
||||
registration_shared_secret: "+oJd9zgvkQpXN-tt;95Wy,AFAdRH+FSTg&LxUXh6ZSvwMJHT;h"
|
||||
report_stats: false
|
||||
macaroon_secret_key: "d@ck1QkQLxlRg^aB#c#oZeII.oxOS6E2DX;YobP^Vm#iB5pQpd"
|
||||
form_secret: "P.uleBJUYc6AM.UOrFF1q7OKH2N5T*Ae2;fGh46;vIHLIQ#JBP"
|
||||
signing_key_path: "/config/localhost.signing.key"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
# vim:ft=yaml
|
||||
matrix_authentication_service:
|
||||
enabled: true
|
||||
endpoint: http://mas:8778/
|
||||
secret: "IhKoLn6jWf1qRRZWvqgaKuIdwD6H0Mvx"
|
||||
# Alternatively, using a file:
|
||||
#secret_file: /path/to/secret.txt
|
||||
39
docker_prod/synapse/localhost.log.config
Normal file
39
docker_prod/synapse/localhost.log.config
Normal file
@@ -0,0 +1,39 @@
|
||||
version: 1
|
||||
|
||||
formatters:
|
||||
precise:
|
||||
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||
|
||||
|
||||
handlers:
|
||||
|
||||
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: precise
|
||||
|
||||
loggers:
|
||||
# This is just here so we can leave `loggers` in the config regardless of whether
|
||||
# we configure other loggers below (avoid empty yaml dict error).
|
||||
_placeholder:
|
||||
level: "INFO"
|
||||
|
||||
|
||||
|
||||
synapse.storage.SQL:
|
||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||
# information such as access tokens.
|
||||
level: INFO
|
||||
|
||||
|
||||
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
|
||||
|
||||
handlers: [console]
|
||||
|
||||
|
||||
disable_existing_loggers: false
|
||||
1
docker_prod/synapse/localhost.signing.key
Normal file
1
docker_prod/synapse/localhost.signing.key
Normal file
@@ -0,0 +1 @@
|
||||
ed25519 a_HEcG Q2iG1Yy5WTiZ/VIy+zHPyHCRUpqyE3qrVttGULrVQK4
|
||||
2274
matrixgw_backend/Cargo.lock
generated
2274
matrixgw_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,35 +4,36 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.11.8"
|
||||
env_logger = "0.11.9"
|
||||
log = "0.4.29"
|
||||
clap = { version = "4.5.53", features = ["derive", "env"] }
|
||||
lazy_static = "1.5.0"
|
||||
anyhow = "1.0.100"
|
||||
clap = { version = "4.5.60", features = ["derive", "env"] }
|
||||
anyhow = "1.0.102"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
actix-web = "4.12.1"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
actix-web = "4.13.0"
|
||||
actix-session = { version = "0.11.0", features = ["redis-session"] }
|
||||
actix-remote-ip = "0.1.0"
|
||||
actix-cors = "0.7.1"
|
||||
light-openid = "1.0.4"
|
||||
bytes = "1.11.0"
|
||||
sha2 = "0.10.9"
|
||||
base16ct = { version = "0.3.0", features = ["alloc"] }
|
||||
futures-util = "0.3.31"
|
||||
jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] }
|
||||
thiserror = "2.0.17"
|
||||
uuid = { version = "1.19.0", features = ["v4", "serde"] }
|
||||
actix-multipart = "0.7.2"
|
||||
light-openid = "1.1.0"
|
||||
bytes = "1.11.1"
|
||||
sha2 = "0.11.0-rc.5"
|
||||
base16ct = { version = "1.0.0", features = ["alloc"] }
|
||||
futures-util = "0.3.32"
|
||||
jwt-simple = { version = "0.12.14", default-features = false, features = ["pure-rust"] }
|
||||
thiserror = "2.0.18"
|
||||
uuid = { version = "1.21.0", features = ["v4", "serde"] }
|
||||
ipnet = { version = "2.11.0", features = ["serde"] }
|
||||
rand = "0.9.2"
|
||||
rand = "0.10.0"
|
||||
hex = "0.4.3"
|
||||
mailchecker = "6.0.19"
|
||||
matrix-sdk = { version = "0.14.0" }
|
||||
url = "2.5.7"
|
||||
ractor = "0.15.9"
|
||||
serde_json = "1.0.145"
|
||||
lazy-regex = "3.4.2"
|
||||
actix-ws = "0.3.0"
|
||||
matrix-sdk = { version = "0.16.0", features = ["e2e-encryption"] }
|
||||
matrix-sdk-ui = "0.16.0"
|
||||
url = "2.5.8"
|
||||
ractor = "0.15.10"
|
||||
serde_json = "1.0.149"
|
||||
lazy-regex = "3.6.0"
|
||||
actix-ws = "0.4.0"
|
||||
infer = "0.19.0"
|
||||
rust-embed = "8.9.0"
|
||||
rust-embed = "8.11.0"
|
||||
mime_guess = "2.0.5"
|
||||
@@ -5,6 +5,7 @@ use matrix_sdk::authentication::oauth::registration::{
|
||||
ApplicationType, ClientMetadata, Localized, OAuthGrantType,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
use url::Url;
|
||||
|
||||
/// Matrix gateway backend API
|
||||
@@ -89,16 +90,12 @@ pub struct AppConfig {
|
||||
storage_path: String,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ARGS: AppConfig = {
|
||||
AppConfig::parse()
|
||||
};
|
||||
}
|
||||
static ARGS: OnceLock<AppConfig> = OnceLock::new();
|
||||
|
||||
impl AppConfig {
|
||||
/// Get parsed command line arguments
|
||||
pub fn get() -> &'static AppConfig {
|
||||
&ARGS
|
||||
ARGS.get_or_init(AppConfig::parse)
|
||||
}
|
||||
|
||||
/// Get auto login email (if not empty)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::broadcast_messages::BroadcastSender;
|
||||
use crate::controllers::{HttpFailure, HttpResult};
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use crate::extractors::session_extractor::MatrixGWSession;
|
||||
@@ -63,9 +63,7 @@ pub async fn finish_oidc(
|
||||
|
||||
let prov = AppConfig::get().openid_provider();
|
||||
|
||||
let conf = OpenIDConfig::load_from_url(prov.configuration_url)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
let conf = OpenIDConfig::load_from_url(prov.configuration_url).await?;
|
||||
|
||||
let (token, _) = conf
|
||||
.request_token(
|
||||
@@ -74,12 +72,8 @@ pub async fn finish_oidc(
|
||||
&req.code,
|
||||
&AppConfig::get().openid_provider().redirect_url,
|
||||
)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
let (user_info, _) = conf
|
||||
.request_user_info(&token)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
.await?;
|
||||
let (user_info, _) = conf.request_user_info(&token).await?;
|
||||
|
||||
if user_info.email_verified != Some(true) {
|
||||
log::error!("Email is not verified!");
|
||||
|
||||
@@ -2,11 +2,14 @@ use crate::controllers::HttpResult;
|
||||
use crate::controllers::matrix::matrix_media_controller;
|
||||
use crate::controllers::matrix::matrix_media_controller::MediaQuery;
|
||||
use crate::controllers::matrix::matrix_room_controller::RoomIdInPath;
|
||||
use crate::controllers::server_controller::ServerConstraints;
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use actix_multipart::form::MultipartForm;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
||||
use futures_util::{StreamExt, stream};
|
||||
use matrix_sdk::Room;
|
||||
use matrix_sdk::attachment::AttachmentConfig;
|
||||
use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
|
||||
use matrix_sdk::media::MediaEventContent;
|
||||
use matrix_sdk::room::MessagesOptions;
|
||||
@@ -23,6 +26,7 @@ use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent};
|
||||
use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::io::Read;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct APIEvent {
|
||||
@@ -136,6 +140,55 @@ pub async fn send_text_message(
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
#[derive(Debug, actix_multipart::form::MultipartForm)]
|
||||
pub struct SendFileForm {
|
||||
#[multipart]
|
||||
file: actix_multipart::form::tempfile::TempFile,
|
||||
}
|
||||
|
||||
pub async fn send_file(
|
||||
client: MatrixClientExtractor,
|
||||
path: web::Path<RoomIdInPath>,
|
||||
req: HttpRequest,
|
||||
) -> HttpResult {
|
||||
let Some(payload) = client.auth.payload else {
|
||||
return Ok(HttpResponse::BadRequest().body("No payload included in request!"));
|
||||
};
|
||||
|
||||
// Reconstruct multipart form from authenticated request
|
||||
let mut form = MultipartForm::<SendFileForm>::from_request(
|
||||
&req,
|
||||
&mut Payload::from(bytes::Bytes::from(payload)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Read attachment to end
|
||||
let mut buff = Vec::with_capacity(form.file.size);
|
||||
form.file.file.read_to_end(&mut buff)?;
|
||||
|
||||
if form.file.size > ServerConstraints::default().max_upload_file_size {
|
||||
return Ok(HttpResponse::NotFound().json("Uploaded file is too large!"));
|
||||
}
|
||||
|
||||
let Some(room) = client.client.client.get_room(&path.room_id) else {
|
||||
return Ok(HttpResponse::NotFound().json("Room not found!"));
|
||||
};
|
||||
|
||||
let Some(file_name) = form.file.file_name.as_deref() else {
|
||||
return Ok(HttpResponse::BadRequest().json("File name must be specified!"));
|
||||
};
|
||||
|
||||
let Some(mime_type) = form.file.content_type.as_ref() else {
|
||||
return Ok(HttpResponse::BadRequest().json("File content type must be specified!"));
|
||||
};
|
||||
|
||||
// Do send the file
|
||||
room.send_attachment(file_name, mime_type, buff, AttachmentConfig::new())
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct EventIdInPath {
|
||||
pub(crate) event_id: OwnedEventId,
|
||||
|
||||
@@ -4,11 +4,9 @@ use crate::utils::crypt_utils::sha512;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::http::header;
|
||||
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
|
||||
use matrix_sdk::crypto::{AttachmentDecryptor, MediaEncryptionInfo};
|
||||
use matrix_sdk::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
|
||||
use matrix_sdk::ruma::events::room::MediaSource;
|
||||
use matrix_sdk::ruma::{OwnedMxcUri, UInt};
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct MediaMXCInPath {
|
||||
@@ -54,21 +52,6 @@ pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool)
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Decrypt file if needed
|
||||
let media = if let MediaSource::Encrypted(file) = source {
|
||||
let mut cursor = Cursor::new(media);
|
||||
let mut decryptor =
|
||||
AttachmentDecryptor::new(&mut cursor, MediaEncryptionInfo::from(*file))?;
|
||||
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
decryptor.read_to_end(&mut decrypted_data)?;
|
||||
|
||||
decrypted_data
|
||||
} else {
|
||||
media
|
||||
};
|
||||
|
||||
let digest = sha512(&media);
|
||||
|
||||
let mime_type = infer::get(&media).map(|x| x.mime_type());
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||
use actix_web::HttpResponse;
|
||||
use matrix_sdk_ui::spaces::SpaceService;
|
||||
use matrix_sdk_ui::spaces::room_list::SpaceRoomListPaginationState;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Get space hierarchy
|
||||
pub async fn hierarchy(client: MatrixClientExtractor) -> HttpResult {
|
||||
let spaces = client.client.client.joined_space_rooms();
|
||||
let space_service = SpaceService::new(client.client.client);
|
||||
let mut hierarchy = HashMap::new();
|
||||
for space in spaces {
|
||||
let rooms = space_service
|
||||
.space_room_list(space.room_id().to_owned())
|
||||
.await;
|
||||
while !matches!(
|
||||
rooms.pagination_state(),
|
||||
SpaceRoomListPaginationState::Idle { end_reached: true }
|
||||
) {
|
||||
rooms.paginate().await?;
|
||||
}
|
||||
|
||||
hierarchy.insert(
|
||||
space.room_id().to_owned(),
|
||||
rooms
|
||||
.rooms()
|
||||
.into_iter()
|
||||
.map(|room| room.room_id)
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
Ok(HttpResponse::Ok().json(hierarchy))
|
||||
}
|
||||
@@ -2,3 +2,4 @@ pub mod matrix_event_controller;
|
||||
pub mod matrix_media_controller;
|
||||
pub mod matrix_profile_controller;
|
||||
pub mod matrix_room_controller;
|
||||
pub mod matrix_space_controller;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use std::error::Error;
|
||||
use light_openid::errors::OpenIdError;
|
||||
|
||||
pub mod auth_controller;
|
||||
pub mod matrix;
|
||||
@@ -18,7 +18,7 @@ pub enum HttpFailure {
|
||||
#[error("this resource was not found")]
|
||||
NotFound,
|
||||
#[error("an unspecified open id error occurred: {0}")]
|
||||
OpenID(Box<dyn Error>),
|
||||
OpenID(#[from] OpenIdError),
|
||||
#[error("an unspecified internal error occurred: {0}")]
|
||||
InternalError(#[from] anyhow::Error),
|
||||
#[error("Actix web error: {0}")]
|
||||
|
||||
@@ -39,6 +39,7 @@ pub struct ServerConstraints {
|
||||
pub token_name: LenConstraints,
|
||||
pub token_ip_net: LenConstraints,
|
||||
pub token_max_inactivity: LenConstraints,
|
||||
pub max_upload_file_size: usize,
|
||||
}
|
||||
|
||||
impl Default for ServerConstraints {
|
||||
@@ -47,6 +48,7 @@ impl Default for ServerConstraints {
|
||||
token_name: LenConstraints::new(5, 255),
|
||||
token_ip_net: LenConstraints::max_only(44),
|
||||
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
|
||||
max_upload_file_size: 20_000_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub enum WsMessage {
|
||||
/// Room reaction event
|
||||
RoomReactionEvent(WsRoomEvent<ReactionEventContent>),
|
||||
|
||||
/// Room reaction event
|
||||
/// Room redaction event
|
||||
RoomRedactionEvent(WsRoomEvent<RoomRedactionEventContent>),
|
||||
|
||||
/// Fully read message event
|
||||
|
||||
@@ -11,8 +11,9 @@ use matrixgw_backend::broadcast_messages::BroadcastMessage;
|
||||
use matrixgw_backend::constants;
|
||||
use matrixgw_backend::controllers::matrix::{
|
||||
matrix_event_controller, matrix_media_controller, matrix_profile_controller,
|
||||
matrix_room_controller,
|
||||
matrix_room_controller, matrix_space_controller,
|
||||
};
|
||||
use matrixgw_backend::controllers::server_controller::ServerConstraints;
|
||||
use matrixgw_backend::controllers::{
|
||||
auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller,
|
||||
static_controller, tokens_controller, ws_controller,
|
||||
@@ -75,6 +76,9 @@ async fn main() -> std::io::Result<()> {
|
||||
.wrap(Logger::default())
|
||||
.wrap(session_mw)
|
||||
.wrap(cors)
|
||||
.app_data(web::PayloadConfig::new(
|
||||
ServerConstraints::default().max_upload_file_size,
|
||||
))
|
||||
.app_data(web::Data::new(manager_actor_clone.clone()))
|
||||
.app_data(web::Data::new(RemoteIPConfig {
|
||||
proxy: AppConfig::get().proxy_ip.clone(),
|
||||
@@ -138,6 +142,11 @@ async fn main() -> std::io::Result<()> {
|
||||
web::get().to(matrix_sync_thread_controller::status),
|
||||
)
|
||||
.service(web::resource("/api/ws").route(web::get().to(ws_controller::ws)))
|
||||
// Matrix spaces controller
|
||||
.route(
|
||||
"/api/matrix/space/hierarchy",
|
||||
web::get().to(matrix_space_controller::hierarchy),
|
||||
)
|
||||
// Matrix room controller
|
||||
.route(
|
||||
"/api/matrix/room/joined",
|
||||
@@ -177,6 +186,10 @@ async fn main() -> std::io::Result<()> {
|
||||
"/api/matrix/room/{room_id}/send_text_message",
|
||||
web::post().to(matrix_event_controller::send_text_message),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/send_file",
|
||||
web::post().to(matrix_event_controller::send_file),
|
||||
)
|
||||
.route(
|
||||
"/api/matrix/room/{room_id}/event/{event_id}/set_text_content",
|
||||
web::post().to(matrix_event_controller::set_text_content),
|
||||
|
||||
742
matrixgw_frontend/package-lock.json
generated
742
matrixgw_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,37 +12,38 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/roboto": "^5.2.9",
|
||||
"@mui/icons-material": "^7.3.6",
|
||||
"@mui/material": "^7.3.6",
|
||||
"@mui/x-data-grid": "^8.20.0",
|
||||
"@mui/x-date-pickers": "^8.19.0",
|
||||
"date-and-time": "^4.1.1",
|
||||
"@fontsource/roboto": "^5.2.10",
|
||||
"@mui/icons-material": "^7.3.8",
|
||||
"@mui/material": "^7.3.8",
|
||||
"@mui/x-data-grid": "^8.27.3",
|
||||
"@mui/x-date-pickers": "^8.27.2",
|
||||
"date-and-time": "^4.3.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"emoji-picker-react": "^4.16.1",
|
||||
"is-cidr": "^6.0.1",
|
||||
"emoji-picker-react": "^4.18.0",
|
||||
"filesize": "^11.0.13",
|
||||
"is-cidr": "^6.0.3",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-favicon": "^2.0.7",
|
||||
"react-json-view-lite": "^2.5.0",
|
||||
"react-router": "^7.10.0"
|
||||
"react-router": "^7.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@eslint/js": "^9.39.3",
|
||||
"@types/node": "^25.3.3",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.5.0",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vite": "npm:rolldown-vite@7.2.10"
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@7.2.10"
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface ServerConstraints {
|
||||
token_name: LenConstraint;
|
||||
token_ip_net: LenConstraint;
|
||||
token_max_inactivity: LenConstraint;
|
||||
max_upload_file_size: number;
|
||||
}
|
||||
|
||||
export interface LenConstraint {
|
||||
|
||||
@@ -17,6 +17,9 @@ export interface RoomMessageEvent extends BaseRoomEvent {
|
||||
"m.relates_to"?: {
|
||||
rel_type?: "m.replace" | string;
|
||||
event_id?: string;
|
||||
"m.in_reply_to"?:{
|
||||
event_id?:string
|
||||
}
|
||||
};
|
||||
"m.new_content"?: {
|
||||
msgtype?: MessageType;
|
||||
|
||||
@@ -15,8 +15,11 @@ export interface MatrixRoomMessage {
|
||||
body: string;
|
||||
msgtype: MessageType;
|
||||
"m.relates_to"?: {
|
||||
event_id: string;
|
||||
rel_type: "m.replace" | string;
|
||||
event_id?: string;
|
||||
rel_type?: "m.replace" | string;
|
||||
"m.in_reply_to"?: {
|
||||
event_id?: string;
|
||||
};
|
||||
};
|
||||
url?: string;
|
||||
file?: {
|
||||
@@ -65,14 +68,14 @@ export class MatrixApiEvent {
|
||||
*/
|
||||
static async GetRoomEvents(
|
||||
room: Room,
|
||||
from?: string
|
||||
from?: string,
|
||||
): Promise<MatrixEventsList> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri:
|
||||
`/matrix/room/${encodeURIComponent(room.id)}/events` +
|
||||
(from ? `?from=${from}` : ""),
|
||||
`/matrix/room/${encodeURIComponent(room.id)}/events?limit=200` +
|
||||
(from ? `&from=${from}` : ""),
|
||||
})
|
||||
).data as MatrixEventsList;
|
||||
}
|
||||
@@ -83,7 +86,7 @@ export class MatrixApiEvent {
|
||||
static GetEventFileURL(
|
||||
room: Room,
|
||||
event_id: string,
|
||||
thumbnail: boolean
|
||||
thumbnail: boolean,
|
||||
): string {
|
||||
return `${APIClient.ActualBackendURL()}/matrix/room/${
|
||||
room.id
|
||||
@@ -101,13 +104,26 @@ export class MatrixApiEvent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send file message
|
||||
*/
|
||||
static async SendFileMessage(room: Room, file: Blob): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.set("file", file);
|
||||
await APIClient.exec({
|
||||
method: "POST",
|
||||
uri: `/matrix/room/${room.id}/send_file`,
|
||||
formData,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit text message content
|
||||
*/
|
||||
static async SetTextMessageContent(
|
||||
room: Room,
|
||||
event_id: string,
|
||||
content: string
|
||||
content: string,
|
||||
): Promise<void> {
|
||||
await APIClient.exec({
|
||||
method: "POST",
|
||||
@@ -122,7 +138,7 @@ export class MatrixApiEvent {
|
||||
static async ReactToEvent(
|
||||
room: Room,
|
||||
event_id: string,
|
||||
key: string
|
||||
key: string,
|
||||
): Promise<void> {
|
||||
await APIClient.exec({
|
||||
method: "POST",
|
||||
|
||||
40
matrixgw_frontend/src/api/matrix/MatrixApiSpace.ts
Normal file
40
matrixgw_frontend/src/api/matrix/MatrixApiSpace.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { APIClient } from "../ApiClient";
|
||||
|
||||
export type SpaceHierarchy = Map<string, string[]>;
|
||||
|
||||
export class MatrixApiSpace {
|
||||
/**
|
||||
* Request Matrix space hierarchy
|
||||
*/
|
||||
static async Hierarchy(): Promise<SpaceHierarchy> {
|
||||
const hierarchy = new Map(
|
||||
Object.entries(
|
||||
(
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: "/matrix/space/hierarchy",
|
||||
})
|
||||
).data as { [s: string]: string[] }
|
||||
)
|
||||
) as SpaceHierarchy;
|
||||
|
||||
// Simplify hierarchy
|
||||
while (true) {
|
||||
let changed = false;
|
||||
for (const [roomid, children] of hierarchy) {
|
||||
for (const child of children) {
|
||||
if (!hierarchy.has(child)) continue;
|
||||
hierarchy.set(roomid, [
|
||||
...hierarchy.get(roomid)!,
|
||||
...hierarchy.get(child)!,
|
||||
]);
|
||||
hierarchy.delete(child);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) break;
|
||||
}
|
||||
|
||||
return hierarchy;
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,8 @@ export function MatrixAuthCallback(): React.ReactElement {
|
||||
};
|
||||
|
||||
load();
|
||||
}, [code, info, navigate, snackbar, state]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [code, state]);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
|
||||
33
matrixgw_frontend/src/utils/FilesUtils.ts
Normal file
33
matrixgw_frontend/src/utils/FilesUtils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { filesize } from "filesize";
|
||||
|
||||
/**
|
||||
* Select a file to upload
|
||||
*/
|
||||
export async function selectFileToUpload(p: {
|
||||
allowedTypes?: string[];
|
||||
maxSize?: number;
|
||||
}): Promise<Blob | null> {
|
||||
// Create file element
|
||||
const fileEl = document.createElement("input");
|
||||
fileEl.type = "file";
|
||||
if (p.allowedTypes && p.allowedTypes.length > 0)
|
||||
fileEl.accept = p.allowedTypes.join(",");
|
||||
fileEl.click();
|
||||
|
||||
// Wait for a file to be chosen
|
||||
await new Promise((res) =>
|
||||
fileEl.addEventListener("change", () => res(null)),
|
||||
);
|
||||
|
||||
if ((fileEl.files?.length ?? 0) === 0) return null;
|
||||
const file = fileEl.files![0];
|
||||
|
||||
// Check file size
|
||||
if (p.maxSize && file.size > p.maxSize) {
|
||||
throw new Error(
|
||||
`The file is too big ! (max accepted file size : ${filesize(p.maxSize)})`,
|
||||
);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export interface Message {
|
||||
time_sent: number;
|
||||
time_sent_dayjs: dayjs.Dayjs;
|
||||
modified: boolean;
|
||||
inReplyTo?: string;
|
||||
reactions: Map<string, MessageReaction[]>;
|
||||
content: string;
|
||||
type: MessageType;
|
||||
@@ -37,7 +38,7 @@ export class RoomEventsManager {
|
||||
receiptsEventsMap: Map<string, Receipt[]>;
|
||||
|
||||
get canLoadOlder(): boolean {
|
||||
return !!this.endToken;
|
||||
return !!this.endToken && this.events.length > 0;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -92,13 +93,7 @@ export class RoomEventsManager {
|
||||
content: {
|
||||
body: m.data["m.new_content"]?.body ?? m.data.body,
|
||||
msgtype: m.data.msgtype,
|
||||
"m.relates_to":
|
||||
m.data["m.relates_to"] && m.data["m.relates_to"].event_id
|
||||
? {
|
||||
event_id: m.data["m.relates_to"].event_id!,
|
||||
rel_type: m.data["m.relates_to"].rel_type ?? "",
|
||||
}
|
||||
: undefined,
|
||||
"m.relates_to": m.data["m.relates_to"],
|
||||
url: m.data.url,
|
||||
file: m.data.file,
|
||||
},
|
||||
@@ -174,7 +169,7 @@ export class RoomEventsManager {
|
||||
// Message
|
||||
if (data.type === "m.room.message") {
|
||||
// Check if this message replaces another one
|
||||
if (data.content["m.relates_to"]) {
|
||||
if (data.content["m.relates_to"]?.rel_type === "replace") {
|
||||
const message = this.messages.find(
|
||||
(m) => m.event_id === data.content["m.relates_to"]?.event_id
|
||||
);
|
||||
@@ -206,6 +201,7 @@ export class RoomEventsManager {
|
||||
event_id: evt.id,
|
||||
account: evt.sender,
|
||||
modified: false,
|
||||
inReplyTo: data.content["m.relates_to"]?.["m.in_reply_to"]?.event_id,
|
||||
reactions: new Map(),
|
||||
time_sent: evt.time,
|
||||
time_sent_dayjs: dayjs.unix(evt.time / 1000),
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
type UsersMap,
|
||||
} from "../../api/matrix/MatrixApiProfile";
|
||||
import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom";
|
||||
import {
|
||||
MatrixApiSpace,
|
||||
type SpaceHierarchy,
|
||||
} from "../../api/matrix/MatrixApiSpace";
|
||||
import { MatrixSyncApi } from "../../api/MatrixSyncApi";
|
||||
import type { WsMessage } from "../../api/WsApi";
|
||||
import { RoomEventsManager } from "../../utils/RoomEventsManager";
|
||||
@@ -19,13 +23,19 @@ import { SpaceSelector } from "./SpaceSelector";
|
||||
|
||||
export function MainMessageWidget(): React.ReactElement {
|
||||
const [rooms, setRooms] = React.useState<Room[] | undefined>();
|
||||
const [hierarchy, setHierarchy] = React.useState<
|
||||
SpaceHierarchy | undefined
|
||||
>();
|
||||
const [users, setUsers] = React.useState<UsersMap | undefined>();
|
||||
|
||||
const loadRoomsList = async () => {
|
||||
await MatrixSyncApi.Start();
|
||||
|
||||
const rooms = await MatrixApiRoom.ListJoined();
|
||||
const hierarchy = await MatrixApiSpace.Hierarchy();
|
||||
|
||||
setRooms(rooms);
|
||||
setHierarchy(hierarchy);
|
||||
|
||||
// Get the list of users in rooms
|
||||
const users = rooms.reduce((prev, r) => {
|
||||
@@ -40,11 +50,12 @@ export function MainMessageWidget(): React.ReactElement {
|
||||
<AsyncWidget
|
||||
loadKey={1}
|
||||
load={loadRoomsList}
|
||||
ready={!!rooms && !!users}
|
||||
ready={!!rooms && !!users && !!hierarchy}
|
||||
errMsg="Failed to initialize messaging component!"
|
||||
build={() => (
|
||||
<MainMessageWidgetInner
|
||||
rooms={rooms!}
|
||||
hierarchy={hierarchy!}
|
||||
users={users!}
|
||||
onRoomsListUpdate={(cb) => setRooms((r) => cb(r!))}
|
||||
/>
|
||||
@@ -55,6 +66,7 @@ export function MainMessageWidget(): React.ReactElement {
|
||||
|
||||
function MainMessageWidgetInner(p: {
|
||||
rooms: Room[];
|
||||
hierarchy: SpaceHierarchy;
|
||||
users: UsersMap;
|
||||
onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void;
|
||||
}): React.ReactElement {
|
||||
@@ -65,11 +77,13 @@ function MainMessageWidgetInner(p: {
|
||||
|
||||
const spaceRooms = React.useMemo(() => {
|
||||
return p.rooms
|
||||
.filter((r) => !r.is_space && (!space || r.parents.includes(space)))
|
||||
.filter(
|
||||
(r) => !r.is_space && (!space || p.hierarchy.get(space)?.includes(r.id))
|
||||
)
|
||||
.sort(
|
||||
(a, b) => (b.latest_event?.time ?? 0) - (a.latest_event?.time ?? 0)
|
||||
);
|
||||
}, [space, p.rooms]);
|
||||
}, [space, p.rooms, p.hierarchy]);
|
||||
|
||||
const unreadRooms = React.useMemo(
|
||||
() =>
|
||||
@@ -111,7 +125,7 @@ function MainMessageWidgetInner(p: {
|
||||
p.onRoomsListUpdate((r) => {
|
||||
const n = [...r];
|
||||
const idx = r.findIndex((el) => el.id === m.room_id);
|
||||
if (idx && n[idx].notifications === "AllMessages")
|
||||
if (idx && n[idx]?.notifications === "AllMessages")
|
||||
n[idx] = {
|
||||
...n[idx],
|
||||
number_unread_messages: n[idx].number_unread_messages + 1,
|
||||
|
||||
@@ -55,16 +55,8 @@ export function RoomMessagesList(p: {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "instant" });
|
||||
}, [lastEventId, messagesEndRef]);
|
||||
|
||||
// Watch scroll to detect when user reach the top to load older messages
|
||||
const handleScroll = async () => {
|
||||
if (!listContainerRef.current || loadingOlder || !p.manager.canLoadOlder)
|
||||
return;
|
||||
|
||||
const { scrollTop } = listContainerRef.current;
|
||||
|
||||
if (scrollTop !== 0) {
|
||||
return;
|
||||
}
|
||||
const loadOlderMessages = async () => {
|
||||
if (loadingOlder || !p.manager.canLoadOlder) return;
|
||||
|
||||
setLoadingOlder(true);
|
||||
|
||||
@@ -82,6 +74,19 @@ export function RoomMessagesList(p: {
|
||||
}
|
||||
};
|
||||
|
||||
// Watch scroll to detect when user reach the top to load older messages
|
||||
const handleScroll = async () => {
|
||||
if (!listContainerRef.current) return;
|
||||
|
||||
const { scrollTop } = listContainerRef.current;
|
||||
|
||||
if (scrollTop !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadOlderMessages();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onScroll={handleScroll}
|
||||
@@ -94,22 +99,8 @@ export function RoomMessagesList(p: {
|
||||
paddingLeft: "20px",
|
||||
}}
|
||||
>
|
||||
{/* Empty conversation notice */}
|
||||
{p.manager.messages.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
No message in this conversation yet!
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/** Begining of conversation */}
|
||||
{!p.manager.canLoadOlder && (
|
||||
{!p.manager.canLoadOlder && p.manager.messages.length > 0 && (
|
||||
<Typography
|
||||
component={"div"}
|
||||
variant="caption"
|
||||
@@ -119,6 +110,22 @@ export function RoomMessagesList(p: {
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/** Load older messages button */}
|
||||
{p.manager.canLoadOlder && !loadingOlder && (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
<Button onClick={loadOlderMessages} variant="outlined">
|
||||
Load older messages
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/** Loading older messages spinner */}
|
||||
{loadingOlder && (
|
||||
<div
|
||||
@@ -133,6 +140,21 @@ export function RoomMessagesList(p: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty conversation notice */}
|
||||
{p.manager.messages.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
No message in this conversation yet!
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/** Messages themselves */}
|
||||
{p.manager.messages.map((m, idx) => (
|
||||
<RoomMessage
|
||||
key={m.event_id}
|
||||
@@ -149,6 +171,11 @@ export function RoomMessagesList(p: {
|
||||
p.manager.messages[idx - 1].time_sent_dayjs.startOf("day").unix()
|
||||
}
|
||||
receipts={p.manager.receiptsEventsMap.get(m.event_id)}
|
||||
repliedMessage={
|
||||
(m.inReplyTo &&
|
||||
p.manager.messages.find((s) => s.event_id === m.inReplyTo)) ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -164,6 +191,7 @@ function RoomMessage(p: {
|
||||
previousFromSamePerson: boolean;
|
||||
firstMessageOfDay: boolean;
|
||||
receipts?: Receipt[];
|
||||
repliedMessage?: Message;
|
||||
}): React.ReactElement {
|
||||
const theme = useTheme();
|
||||
const user = useUserInfo();
|
||||
@@ -180,6 +208,8 @@ function RoomMessage(p: {
|
||||
const closeImageFullScreen = () => setShowImageFullScreen(false);
|
||||
|
||||
const sender = p.users.get(p.message.account);
|
||||
const repliedMsgSender =
|
||||
p.repliedMessage && p.users.get(p.repliedMessage.account);
|
||||
|
||||
const handleDeleteMessage = async () => {
|
||||
if (!(await confirm(`Do you really want to delete this message?`))) return;
|
||||
@@ -292,12 +322,37 @@ function RoomMessage(p: {
|
||||
"&:hover *": { visibility: "visible" },
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
{p.message.time_sent_dayjs.format("HH:mm")}
|
||||
<Typography
|
||||
variant="caption"
|
||||
style={{
|
||||
paddingLeft: "2px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{p.message.time_sent_dayjs.format("HH:mm")}
|
||||
</Typography>
|
||||
|
||||
{/** Message itself */}
|
||||
<div style={{ marginLeft: "15px", whiteSpace: "pre-wrap", flex: 1 }}>
|
||||
{/** In case of reply */}
|
||||
{p.repliedMessage && repliedMsgSender && (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
borderLeft: "1px red solid",
|
||||
paddingLeft: "10px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<AccountIcon user={repliedMsgSender} size={16} />
|
||||
<div style={{ marginLeft: "10px" }}>
|
||||
{p.repliedMessage?.content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image */}
|
||||
{p.message.type === "m.image" && (
|
||||
<img
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import {
|
||||
Chip,
|
||||
List,
|
||||
@@ -5,6 +6,7 @@ import {
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||
@@ -22,11 +24,19 @@ export function RoomSelector(p: {
|
||||
}): React.ReactElement {
|
||||
const user = useUserInfo();
|
||||
|
||||
const [filter, setFilter] = React.useState("");
|
||||
const [unread, setUnread] = React.useState(false);
|
||||
|
||||
const shownRooms = React.useMemo(
|
||||
() => p.rooms.filter((r) => !unread || r.number_unread_messages > 0),
|
||||
[p.rooms, unread]
|
||||
() =>
|
||||
p.rooms
|
||||
.filter((r) => !unread || r.number_unread_messages > 0)
|
||||
.filter(
|
||||
(r) =>
|
||||
filter === "" ||
|
||||
r.name?.toLocaleLowerCase()?.includes(filter.toLocaleLowerCase())
|
||||
),
|
||||
[p.rooms, unread, filter]
|
||||
);
|
||||
|
||||
if (p.rooms.length === 0)
|
||||
@@ -45,6 +55,19 @@ export function RoomSelector(p: {
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{/** Filter bar */}
|
||||
<TextField
|
||||
placeholder="Filter rooms"
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: <SearchIcon style={{ marginRight: "10px" }} />,
|
||||
},
|
||||
}}
|
||||
style={{ margin: "5px" }}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
|
||||
{/** Chip bar */}
|
||||
<div style={{ padding: "5px 10px", marginTop: "5px" }}>
|
||||
<span onClick={() => setUnread(!unread)} style={{ cursor: "pointer" }}>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import { IconButton, TextField } from "@mui/material";
|
||||
import React, { type FormEvent } from "react";
|
||||
import { IconButton, TextField, Tooltip } from "@mui/material";
|
||||
import React, { type SyntheticEvent } from "react";
|
||||
import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
|
||||
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
||||
import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider";
|
||||
import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider";
|
||||
import { selectFileToUpload } from "../../utils/FilesUtils";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider";
|
||||
|
||||
export function SendMessageForm(p: { room: Room }): React.ReactElement {
|
||||
const loadingMessage = useLoadingMessage();
|
||||
const alert = useAlert();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const [text, setText] = React.useState("");
|
||||
|
||||
const handleTextSubmit = async (e: FormEvent) => {
|
||||
const handleTextSubmit = async (e: SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (text === "") return;
|
||||
@@ -31,6 +36,26 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSubmit = async () => {
|
||||
try {
|
||||
const file = await selectFileToUpload({
|
||||
maxSize: ServerApi.Config.constraints.max_upload_file_size,
|
||||
});
|
||||
|
||||
if (!file) return;
|
||||
|
||||
loadingMessage.show("Uploading file...");
|
||||
await MatrixApiEvent.SendFileMessage(p.room, file);
|
||||
|
||||
snackbar("The file was successfully uploaded!");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`Failed to upload file! ${e}`);
|
||||
} finally {
|
||||
loadingMessage.hide();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleTextSubmit}>
|
||||
<div
|
||||
@@ -50,6 +75,13 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
/>
|
||||
<span style={{ width: "10px" }}></span>
|
||||
<Tooltip title="Send a file">
|
||||
<IconButton size="small" onClick={handleFileSubmit}>
|
||||
<AttachFileIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<span style={{ width: "10px" }}></span>
|
||||
<IconButton
|
||||
size="small"
|
||||
style={{ visibility: text === "" ? "hidden" : "visible" }}
|
||||
|
||||
@@ -3,17 +3,19 @@ import { Button } from "@mui/material";
|
||||
import React from "react";
|
||||
import type { UsersMap } from "../../api/matrix/MatrixApiProfile";
|
||||
import type { Room } from "../../api/matrix/MatrixApiRoom";
|
||||
import type { SpaceHierarchy } from "../../api/matrix/MatrixApiSpace";
|
||||
import { RoomIcon } from "./RoomIcon";
|
||||
|
||||
export function SpaceSelector(p: {
|
||||
rooms: Room[];
|
||||
hierarchy: SpaceHierarchy;
|
||||
users: UsersMap;
|
||||
selectedSpace?: string;
|
||||
onChange: (space?: string) => void;
|
||||
}): React.ReactElement {
|
||||
const spaces = React.useMemo(
|
||||
() => p.rooms.filter((r) => r.is_space),
|
||||
[p.rooms]
|
||||
() => p.rooms.filter((r) => r.is_space && p.hierarchy.has(r.id)),
|
||||
[p.rooms, p.hierarchy]
|
||||
);
|
||||
|
||||
// Do not display space bar if your is not member of any space
|
||||
|
||||
Reference in New Issue
Block a user