mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Compare commits
10 Commits
dbkr/sentr
...
v0.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d65d5b261 | ||
|
|
3d885597ae | ||
|
|
04781ab3a4 | ||
|
|
89d42cde7b | ||
|
|
02f6416741 | ||
|
|
73e1ce547d | ||
|
|
587973fb9b | ||
|
|
bb68e63f54 | ||
|
|
4fe6ac6b2d | ||
|
|
c11259d2aa |
@@ -1,16 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org"],
|
||||
extends: [
|
||||
"prettier",
|
||||
"plugin:matrix-org/react",
|
||||
"plugin:matrix-org/a11y",
|
||||
"plugin:matrix-org/typescript",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: "module",
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
extends: ["plugin:matrix-org/react", "plugin:matrix-org/a11y", "prettier"],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
||||
2
.github/workflows/lint.yaml
vendored
2
.github/workflows/lint.yaml
vendored
@@ -19,6 +19,6 @@ jobs:
|
||||
- name: i18n
|
||||
run: "yarn run i18n:check"
|
||||
- name: ESLint
|
||||
run: "yarn run lint:eslint"
|
||||
run: "yarn run lint:js"
|
||||
- name: Type check
|
||||
run: "yarn run lint:types"
|
||||
|
||||
16
README.md
16
README.md
@@ -88,16 +88,20 @@ yarn dev
|
||||
|
||||
### Backend
|
||||
|
||||
A docker compose file is provided to start a LiveKit server and auth
|
||||
service for development. These use a test 'secret' published in this
|
||||
repository, so this must be used only for local development and
|
||||
**_never be exposed to the public Internet._**
|
||||
Add in you `.env` in root dir with:
|
||||
|
||||
To use it, add SFU parameter in your local config `./public/config.yml`:
|
||||
```yaml
|
||||
# Develop backend settings:
|
||||
LIVEKIT_KEY="devkey"
|
||||
LIVEKIT_SECRET="secret"
|
||||
```
|
||||
|
||||
Add SFU parameter in your local config `./public/config.yml`:
|
||||
|
||||
```yaml
|
||||
"livekit": {
|
||||
"jwt_service_url": "http://localhost:8881"
|
||||
"server_url": "ws://localhost:7880",
|
||||
"jwt_service_url": "http:/localhost:8881"
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# LiveKit requires host networking, which is only available on Linux
|
||||
# This compose will not function correctly on Mac or Windows
|
||||
version: "3.9"
|
||||
|
||||
networks:
|
||||
@@ -5,14 +7,15 @@ networks:
|
||||
|
||||
services:
|
||||
auth-service:
|
||||
image: ghcr.io/vector-im/lk-jwt-service:latest-ci
|
||||
build:
|
||||
context: ./backend/auth
|
||||
container_name: auth-server
|
||||
hostname: auth-server
|
||||
ports:
|
||||
- 8881:8080
|
||||
environment:
|
||||
- LIVEKIT_URL=ws://localhost:7880
|
||||
- LIVEKIT_KEY=devkey
|
||||
- LIVEKIT_SECRET=secret
|
||||
- LIVEKIT_KEY=${LIVEKIT_KEY}
|
||||
- LIVEKIT_SECRET=${LIVEKIT_SECRET}
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
15
backend/auth/Dockerfile
Normal file
15
backend/auth/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM golang:1.20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod ./
|
||||
COPY go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY *.go ./
|
||||
|
||||
RUN go build -o /auth-server
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD [ "/auth-server" ]
|
||||
20
backend/auth/go.mod
Normal file
20
backend/auth/go.mod
Normal file
@@ -0,0 +1,20 @@
|
||||
module vector-auth-server
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/livekit/protocol v1.5.7
|
||||
|
||||
require (
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd // indirect
|
||||
google.golang.org/grpc v1.55.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
94
backend/auth/go.sum
Normal file
94
backend/auth/go.sum
Normal file
@@ -0,0 +1,94 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/frostbyte73/core v0.0.9 h1:AmE9GjgGpPsWk9ZkmY3HsYUs2hf2tZt+/W6r49URBQI=
|
||||
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
|
||||
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58=
|
||||
github.com/livekit/protocol v1.5.7 h1:jZeFQEmLuIhFblXDGPRCBbfjVJHb+YU7AsO+SMoXF70=
|
||||
github.com/livekit/protocol v1.5.7/go.mod h1:ZaOnsvP+JS4s7vI1UO+JVdBagvvLp/lBXDAl2hkDS0I=
|
||||
github.com/livekit/psrpc v0.3.0 h1:giBZsfM3CWA0oIYXofsMITbVQtyW7u/ES9sQmVspHPM=
|
||||
github.com/mackerelio/go-osstat v0.2.4 h1:qxGbdPkFo65PXOb/F/nhDKpF2nGmGaCFDLXoZjJTtUs=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/nats-io/nats.go v1.25.0 h1:t5/wCPGciR7X3Mu8QOi4jiJaXaWM8qtkLu4lzGZvYHE=
|
||||
github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
|
||||
github.com/pion/ice/v2 v2.3.4 h1:tjYjTLpWyZzUjpDnzk6T1y3oQyhyY2DiM2t095iDhyQ=
|
||||
github.com/pion/interceptor v0.1.16 h1:0GDZrfNO+BmVNWymS31fMlVtPO2IJVBzy2Qq5XCYMIg=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||
github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/srtp/v2 v2.0.14 h1:Glt0MqEvINrDxL+aanmK4DiFjvs+uN2iYc6XD/iKpoY=
|
||||
github.com/pion/stun v0.5.2 h1:J/8glQnDV91dfk2+ZnGN0o9bUJgABhTNljwfQWByoXE=
|
||||
github.com/pion/transport/v2 v2.2.0 h1:u5lFqFHkXLMXMzai8tixZDfVjb8eOjH35yCunhPeb1c=
|
||||
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
|
||||
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
|
||||
github.com/pion/webrtc/v3 v3.2.4 h1:gWSx4dqQb77051qBT9ipDrOyP6/sGYcAQP3UPjM8pU8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd h1:sLpv7bNL1AsX3fdnWh9WVh7ejIzXdOc1RRHGeAmeStU=
|
||||
google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
98
backend/auth/server.go
Normal file
98
backend/auth/server.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
key, secret string
|
||||
}
|
||||
|
||||
func (h *Handler) handle(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Request from %s", r.RemoteAddr)
|
||||
|
||||
// Set the CORS headers
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
|
||||
// Handle preflight request (CORS)
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
roomName := r.URL.Query().Get("roomName")
|
||||
name := r.URL.Query().Get("name")
|
||||
identity := r.URL.Query().Get("identity")
|
||||
|
||||
log.Printf("roomName: %s, name: %s, identity: %s", roomName, name, identity)
|
||||
|
||||
if roomName == "" || name == "" || identity == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := getJoinToken(h.key, h.secret, roomName, identity, name)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res := Response{token}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
key := os.Getenv("LIVEKIT_KEY")
|
||||
secret := os.Getenv("LIVEKIT_SECRET")
|
||||
|
||||
// Check if the key and secret are empty.
|
||||
if key == "" || secret == "" {
|
||||
log.Fatal("LIVEKIT_KEY and LIVEKIT_SECRET environment variables must be set")
|
||||
}
|
||||
|
||||
log.Printf("LIVEKIT_KEY: %s and LIVEKIT_SECRET %s", key, secret)
|
||||
|
||||
handler := &Handler{
|
||||
key: key,
|
||||
secret: secret,
|
||||
}
|
||||
|
||||
http.HandleFunc("/token", handler.handle)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Token string `json:"accessToken"`
|
||||
}
|
||||
|
||||
func getJoinToken(apiKey, apiSecret, room, identity, name string) (string, error) {
|
||||
at := auth.NewAccessToken(apiKey, apiSecret)
|
||||
|
||||
canPublish := true
|
||||
canSubscribe := true
|
||||
grant := &auth.VideoGrant{
|
||||
RoomJoin: true,
|
||||
RoomCreate: true,
|
||||
CanPublish: &canPublish,
|
||||
CanSubscribe: &canSubscribe,
|
||||
Room: room,
|
||||
}
|
||||
|
||||
at.AddGrant(grant).
|
||||
SetIdentity(identity).
|
||||
SetValidFor(time.Hour).
|
||||
SetName(name)
|
||||
|
||||
return at.ToJWT()
|
||||
}
|
||||
14
package.json
14
package.json
@@ -9,9 +9,8 @@
|
||||
"build-storybook": "build-storybook",
|
||||
"prettier:check": "prettier -c .",
|
||||
"prettier:format": "prettier -w .",
|
||||
"lint": "yarn lint:types && yarn lint:eslint",
|
||||
"lint:eslint": "eslint --max-warnings 0 src",
|
||||
"lint:eslint-fix": "eslint --max-warnings 0 src --fix",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src",
|
||||
"lint:types": "tsc",
|
||||
"i18n": "node_modules/i18next-parser/bin/cli.js",
|
||||
"i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update",
|
||||
@@ -47,10 +46,8 @@
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"classnames": "^2.3.1",
|
||||
"color-hash": "^2.0.1",
|
||||
@@ -72,15 +69,13 @@
|
||||
"react-dom": "18",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-router": "6",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-use-clipboard": "^1.0.7",
|
||||
"react-use-measure": "^2.1.1",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"tinyqueue": "^2.0.3",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"uuid": "9",
|
||||
"@types/uuid": "9",
|
||||
"@types/content-type": "^1.1.5"
|
||||
"unique-names-generator": "^4.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.5",
|
||||
@@ -106,7 +101,6 @@
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.2.2",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"jest-mock": "^29.5.0",
|
||||
"prettier": "^2.6.2",
|
||||
"sass": "^1.42.1",
|
||||
"storybook-builder-vite": "^0.1.12",
|
||||
|
||||
@@ -115,6 +115,6 @@
|
||||
"Show connection stats": "Verbindungsstatistiken zeigen",
|
||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Anruf beitreten“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
|
||||
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Mit einem Klick auf „Los geht’s“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6>. <9></9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call ist temporär nicht Ende-zu-Ende-verschlüsselt, während wir die Skalierbarkeit testen."
|
||||
"Element Call is temporarily not encrypted while we test scalability.": "Element Call ist temporär nicht verschlüsselt, während wir die Skalierbarkeit testen.",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Diese Seite wird durch reCAPTCHA geschützt und es gelten Googles <2>Datenschutzerklärung</2> und <6>Nutzungsbedingungen</6>. <9></9>Mit einem Klick auf „Registrieren“ akzeptierst du unseren <2>Endbenutzer-Lizenzvertrag (EULA)</2>"
|
||||
}
|
||||
|
||||
@@ -98,23 +98,5 @@
|
||||
"Expose developer settings in the settings window.": "Muestra los ajustes de desarrollador en la ventana de ajustes.",
|
||||
"Developer Settings": "Ajustes de desarrollador",
|
||||
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Al participar en esta beta, consientes a la recogida de datos anónimos, los cuales usaremos para mejorar el producto. Puedes encontrar más información sobre que datos recogemos en nuestra <2>Política de privacidad</2> y en nuestra <5>Política sobre Cookies</5>.",
|
||||
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta.",
|
||||
"{{displayName}} is presenting": "{{displayName}} está presentando",
|
||||
"<0>Thanks for your feedback!</0>": "<0>¡Gracias por tus comentarios!</0>",
|
||||
"How did it go?": "¿Cómo ha ido?",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Este sitio está protegido por ReCAPTCHA y se aplican la <2>Política de Privacidad</2> y los <6>Términos de Servicio de Google.<9></9>Al hacer clic en \"Registrar\", aceptas nuestro <12>Contrato de Licencia de Usuario Final (CLUF)</12>",
|
||||
"Show connection stats": "Mostrar estadísticas de conexión",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call no está encriptado de extremo a extremo de manera temporal mientras probamos la escalabilidad del servicio.",
|
||||
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Al hacer clic en \"Comenzar\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
|
||||
"Thanks, we received your feedback!": "¡Gracias, hemos recibido tus comentarios!",
|
||||
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Si tienes algún problema o simplemente quieres darnos tu opinión, por favor envíanos una breve descripción.",
|
||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Al hacer clic en \"Unirse a la llamada ahora\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
|
||||
"<0>We'd love to hear your feedback so we can improve your experience.</0>": "<0>Nos encantaría conocer tu opinión para que podamos mejorar tu experiencia</0>",
|
||||
"Feedback": "Danos tu opinión",
|
||||
"Submit": "Enviar",
|
||||
"{{count}} stars|one": "{{count}} estrella",
|
||||
"{{count}} stars|other": "{{count}} estrellas",
|
||||
"{{displayName}}, your call has ended.": "{{displayName}}, tu llamada ha finalizado.",
|
||||
"Submitting…": "Enviando…",
|
||||
"Your feedback": "Tus comentarios"
|
||||
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta."
|
||||
}
|
||||
|
||||
@@ -115,6 +115,6 @@
|
||||
"{{displayName}} is presenting": "{{displayName}} on esitlemas",
|
||||
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Jätka“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
|
||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Selles saidis on kasutusel ReCAPTCHA ja kehtivad Google'i <2>Privaatsuspoliitika</2> ning <6>Teenusetingimused</6>.<9></9>Klõpsides „Registreeru“, sa nõustud meie <12>Lõppkasutaja litsentsilepingu (EULA) tingimustega</12>",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Seni kuni me testime skaleeritavust, siis Element Call ajutiselt pole läbivalt krüptitud."
|
||||
"Element Call is temporarily not encrypted while we test scalability.": "Seni kuni me testime skaleeritavust, siis Element Call ajutiselt pole krüptitud.",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Selles saidis on kasutusel ReCAPTCHA ja kehtivad Google'i <2>Privaatsuspoliitika</2> ning <6>Teenusetingimused</6>.<9></9>Klõpsides „Registreeru“, sa nõustud meie <12>Lõppkasutaja litsentsilepingu (EULA) tingimustega</12>"
|
||||
}
|
||||
|
||||
@@ -115,6 +115,5 @@
|
||||
"Show connection stats": "Afficher les statistiques de la connexion",
|
||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "En cliquant sur « Rejoindre l’appel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
|
||||
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "En cliquant sur « Commencer », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions d’utilisation</6> de Google s’appliquent.<9></9>En cliquant sur « S’enregistrer » vous acceptez également notre <12>Contrat de Licence Utilisateur Final (CLUF)</12>",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call n’est temporairement plus chiffré de bout en bout le temps de tester l’extensibilité."
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions d’utilisation</6> de Google s’appliquent.<9></9>En cliquant sur « S’enregistrer » vous acceptez également notre <12>Contrat de Licence Utilisateur Final (CLUF)</12>"
|
||||
}
|
||||
|
||||
@@ -116,5 +116,5 @@
|
||||
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Dengan mengeklik \"Bergabung\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2>",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Situs ini dilindungi oleh reCAPTCHA dan <2>Kebijakan Privasi</2> dan <6>Ketentuan Layanan</6> Google berlaku.<9></9>Dengan mengeklik \"Daftar\", Anda menyetujui <12>Perjanjian Lisensi Pengguna Akhir (EULA)</12> kami",
|
||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2> kami",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call sementara tidak dienkripsi secara ujung ke ujung selagi kami menguji skalabilitas."
|
||||
"Element Call is temporarily not encrypted while we test scalability.": "Element Call sementara tidak dienkripsi selagi kami menguji skalabilitas."
|
||||
}
|
||||
|
||||
@@ -116,5 +116,5 @@
|
||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
|
||||
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Kliknutím na tlačidlo \"Prejsť\" vyjadrujete súhlas s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Táto stránka je chránená systémom ReCAPTCHA a platia na ňu <2>Pravidlá ochrany osobných údajov spoločnosti Google</2> a <6>Podmienky poskytovania služieb</6>.<9></9>Kliknutím na tlačidlo \"Registrovať sa\" súhlasíte s našou <12>Licenčnou zmluvou s koncovým používateľom (EULA)</12>",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call nie je dočasne šifrovaný, kým testujeme škálovateľnosť."
|
||||
"Element Call is temporarily not encrypted while we test scalability.": "Element Call nie je dočasne šifrovaný, kým testujeme škálovateľnosť."
|
||||
}
|
||||
|
||||
@@ -116,5 +116,5 @@
|
||||
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Натискаючи \"Далі\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
|
||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Цей сайт захищений ReCAPTCHA і до нього застосовується <2>Політика приватності</2> і <6>Умови надання послуг</6> Google.<9></9>Натискаючи \"Зареєструватися\", ви погоджуєтеся з нашою <12>Ліцензійною угодою з кінцевим користувачем (EULA)</12>",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Виклики Element тимчасово не захищаються наскрізним шифруванням, поки ми тестуємо масштабованість."
|
||||
"Element Call is temporarily not encrypted while we test scalability.": "Element Call тимчасово не шифрується, поки ми тестуємо масштабованість."
|
||||
}
|
||||
|
||||
@@ -116,5 +116,5 @@
|
||||
"By clicking \"Go\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "點擊「前往」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
|
||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
|
||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "此網站被 ReCAPTCHA 保護,並適用 Google 的<2>隱私權政策</2>與<6>服務條款</6>。<9></9>點擊「註冊」即表示您同意我們的<12>終端使用者授權協議 (EULA)</12>",
|
||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "在我們測試可擴展性時,Element Call 暫時未進行端到端加密。"
|
||||
"Element Call is temporarily not encrypted while we test scalability.": "在我們測試可擴展性時,Element Call 暫時未加密。"
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import { Suspense, useEffect, useState } from "react";
|
||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
import { History } from "history";
|
||||
|
||||
import { HomePage } from "./home/HomePage";
|
||||
import { LoginPage } from "./auth/LoginPage";
|
||||
import { RegisterPage } from "./auth/RegisterPage";
|
||||
import { RoomPage } from "./room/RoomPage";
|
||||
import { RoomRedirect } from "./room/RoomRedirect";
|
||||
import { ClientProvider } from "./ClientContext";
|
||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||
@@ -51,8 +51,6 @@ export default function App({ history }: AppProps) {
|
||||
const errorPage = <CrashView />;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<Router history={history}>
|
||||
{loaded ? (
|
||||
<Suspense fallback={null}>
|
||||
@@ -70,11 +68,14 @@ export default function App({ history }: AppProps) {
|
||||
<SentryRoute exact path="/register">
|
||||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/inspector">
|
||||
<SequenceDiagramViewerPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomPage />
|
||||
<RoomRedirect />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
</OverlayProvider>
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import { useMemo, CSSProperties, HTMLAttributes, FC } from "react";
|
||||
import classNames from "classnames";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { getAvatarUrl } from "./matrix-utils";
|
||||
import { useClient } from "./ClientContext";
|
||||
@@ -58,6 +59,9 @@ function hashStringToArrIndex(str: string, arrLength: number) {
|
||||
return sum % arrLength;
|
||||
}
|
||||
|
||||
const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
|
||||
src?.startsWith("mxc://") ? client && getAvatarUrl(client, src, size) : src;
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
bgKey?: string;
|
||||
src?: string;
|
||||
@@ -95,10 +99,10 @@ export const Avatar: FC<Props> = ({
|
||||
[size]
|
||||
);
|
||||
|
||||
const resolvedSrc = useMemo(() => {
|
||||
if (!client || !src || !sizePx) return undefined;
|
||||
return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src;
|
||||
}, [client, src, sizePx]);
|
||||
const resolvedSrc = useMemo(
|
||||
() => resolveAvatarSrc(client, src, sizePx),
|
||||
[client, src, sizePx]
|
||||
);
|
||||
|
||||
const backgroundColor = useMemo(() => {
|
||||
const index = hashStringToArrIndex(
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
createContext,
|
||||
useMemo,
|
||||
useContext,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
@@ -31,9 +31,9 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import {
|
||||
initClient,
|
||||
CryptoStoreIntegrityError,
|
||||
fallbackICEServerAllowed,
|
||||
initClient,
|
||||
} from "./matrix-utils";
|
||||
import { widget } from "./widget";
|
||||
import {
|
||||
@@ -47,324 +47,7 @@ import { Config } from "./config/Config";
|
||||
declare global {
|
||||
interface Window {
|
||||
matrixclient: MatrixClient;
|
||||
passwordlessUser: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export type ClientState = ValidClientState | ErrorState;
|
||||
|
||||
export type ValidClientState = {
|
||||
state: "valid";
|
||||
authenticated?: AuthenticatedClient;
|
||||
setClient: (params?: SetClientParams) => void;
|
||||
};
|
||||
|
||||
export type AuthenticatedClient = {
|
||||
client: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
changePassword: (password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
export type ErrorState = {
|
||||
state: "error";
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export type SetClientParams = {
|
||||
client: MatrixClient;
|
||||
session: Session;
|
||||
};
|
||||
|
||||
const ClientContext = createContext<ClientState | undefined>(undefined);
|
||||
|
||||
export const useClientState = () => useContext(ClientContext);
|
||||
|
||||
export function useClient(): {
|
||||
client?: MatrixClient;
|
||||
setClient?: (params?: SetClientParams) => void;
|
||||
} {
|
||||
let client;
|
||||
let setClient;
|
||||
|
||||
const clientState = useClientState();
|
||||
if (clientState?.state === "valid") {
|
||||
client = clientState.authenticated?.client;
|
||||
setClient = clientState.setClient;
|
||||
}
|
||||
|
||||
return { client, setClient };
|
||||
}
|
||||
|
||||
// Plain representation of the `ClientContext` as a helper for old components that expected an object with multiple fields.
|
||||
export function useClientLegacy(): {
|
||||
client?: MatrixClient;
|
||||
setClient?: (params?: SetClientParams) => void;
|
||||
passwordlessUser: boolean;
|
||||
loading: boolean;
|
||||
authenticated: boolean;
|
||||
logout?: () => void;
|
||||
error?: Error;
|
||||
} {
|
||||
const clientState = useClientState();
|
||||
|
||||
let client;
|
||||
let setClient;
|
||||
let passwordlessUser = false;
|
||||
let loading = true;
|
||||
let error;
|
||||
let authenticated = false;
|
||||
let logout;
|
||||
|
||||
if (clientState?.state === "valid") {
|
||||
client = clientState.authenticated?.client;
|
||||
setClient = clientState.setClient;
|
||||
passwordlessUser = clientState.authenticated?.isPasswordlessUser ?? false;
|
||||
loading = false;
|
||||
authenticated = client !== undefined;
|
||||
logout = clientState.authenticated?.logout;
|
||||
} else if (clientState?.state === "error") {
|
||||
error = clientState.error;
|
||||
loading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
client,
|
||||
setClient,
|
||||
passwordlessUser,
|
||||
loading,
|
||||
authenticated,
|
||||
logout,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
const loadChannel =
|
||||
"BroadcastChannel" in window ? new BroadcastChannel("load") : null;
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
const history = useHistory();
|
||||
|
||||
const [initClientState, setInitClientState] = useState<
|
||||
InitResult | undefined
|
||||
>(undefined);
|
||||
|
||||
const initializing = useRef(false);
|
||||
useEffect(() => {
|
||||
// In case the component is mounted, unmounted, and remounted quickly (as
|
||||
// React does in strict mode), we need to make sure not to doubly initialize
|
||||
// the client.
|
||||
if (initializing.current) return;
|
||||
initializing.current = true;
|
||||
|
||||
loadClient()
|
||||
.then((maybeClient) => {
|
||||
if (!maybeClient) {
|
||||
logger.error("Failed to initialize client");
|
||||
return;
|
||||
}
|
||||
|
||||
setInitClientState(maybeClient);
|
||||
})
|
||||
.catch((err) => logger.error(err))
|
||||
.finally(() => (initializing.current = false));
|
||||
}, []);
|
||||
|
||||
const changePassword = useCallback(
|
||||
async (password: string) => {
|
||||
const session = loadSession();
|
||||
if (!initClientState?.client || !session) {
|
||||
return;
|
||||
}
|
||||
|
||||
await initClientState.client.setPassword(
|
||||
{
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: session.user_id,
|
||||
},
|
||||
user: session.user_id,
|
||||
password: session.tempPassword,
|
||||
},
|
||||
password
|
||||
);
|
||||
|
||||
saveSession({ ...session, passwordlessUser: false });
|
||||
|
||||
setInitClientState({
|
||||
client: initClientState.client,
|
||||
passwordlessUser: false,
|
||||
});
|
||||
},
|
||||
[initClientState?.client]
|
||||
);
|
||||
|
||||
const setClient = useCallback(
|
||||
(clientParams?: SetClientParams) => {
|
||||
const oldClient = initClientState?.client;
|
||||
const newClient = clientParams?.client;
|
||||
if (oldClient && oldClient !== newClient) {
|
||||
oldClient.stopClient();
|
||||
}
|
||||
|
||||
if (clientParams) {
|
||||
saveSession(clientParams.session);
|
||||
setInitClientState({
|
||||
client: clientParams.client,
|
||||
passwordlessUser: clientParams.session.passwordlessUser,
|
||||
});
|
||||
} else {
|
||||
clearSession();
|
||||
setInitClientState(undefined);
|
||||
}
|
||||
},
|
||||
[initClientState?.client]
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
const client = initClientState?.client;
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
await client.logout(true);
|
||||
await client.clearStores();
|
||||
clearSession();
|
||||
setInitClientState(undefined);
|
||||
history.push("/");
|
||||
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
|
||||
}, [history, initClientState?.client]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// To protect against multiple sessions writing to the same storage
|
||||
// simultaneously, we send a broadcast message that shuts down all other
|
||||
// running instances of the app. This isn't necessary if the app is running in
|
||||
// a widget though, since then it'll be mostly stateless.
|
||||
useEffect(() => {
|
||||
if (!widget) loadChannel?.postMessage({});
|
||||
}, []);
|
||||
|
||||
const [alreadyOpenedErr, setAlreadyOpenedErr] = useState<Error | undefined>(
|
||||
undefined
|
||||
);
|
||||
useEventTarget(
|
||||
loadChannel,
|
||||
"message",
|
||||
useCallback(() => {
|
||||
initClientState?.client.stopClient();
|
||||
setAlreadyOpenedErr(
|
||||
translatedError("This application has been opened in another tab.", t)
|
||||
);
|
||||
}, [initClientState?.client, setAlreadyOpenedErr, t])
|
||||
);
|
||||
|
||||
const state: ClientState = useMemo(() => {
|
||||
if (alreadyOpenedErr) {
|
||||
return { state: "error", error: alreadyOpenedErr };
|
||||
}
|
||||
|
||||
let authenticated = undefined;
|
||||
if (initClientState) {
|
||||
authenticated = {
|
||||
client: initClientState.client,
|
||||
isPasswordlessUser: initClientState.passwordlessUser,
|
||||
changePassword,
|
||||
logout,
|
||||
};
|
||||
}
|
||||
|
||||
return { state: "valid", authenticated, setClient };
|
||||
}, [alreadyOpenedErr, changePassword, initClientState, logout, setClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initClientState) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.matrixclient = initClientState.client;
|
||||
window.passwordlessUser = initClientState.passwordlessUser;
|
||||
|
||||
if (PosthogAnalytics.hasInstance())
|
||||
PosthogAnalytics.instance.onLoginStatusChanged();
|
||||
}, [initClientState]);
|
||||
|
||||
if (alreadyOpenedErr) {
|
||||
return <ErrorView error={alreadyOpenedErr} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientContext.Provider value={state}>{children}</ClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
type InitResult = {
|
||||
client: MatrixClient;
|
||||
passwordlessUser: boolean;
|
||||
};
|
||||
|
||||
async function loadClient(): Promise<InitResult> {
|
||||
if (widget) {
|
||||
// We're inside a widget, so let's engage *matryoshka mode*
|
||||
logger.log("Using a matryoshka client");
|
||||
const client = await widget.client;
|
||||
return {
|
||||
client,
|
||||
passwordlessUser: false,
|
||||
};
|
||||
} else {
|
||||
// We're running as a standalone application
|
||||
try {
|
||||
const session = loadSession();
|
||||
if (!session) {
|
||||
throw new Error("No session stored");
|
||||
}
|
||||
|
||||
logger.log("Using a standalone client");
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const { user_id, device_id, access_token, passwordlessUser } = session;
|
||||
const initClientParams = {
|
||||
baseUrl: Config.defaultHomeserverUrl()!,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
livekitServiceURL: Config.get().livekit!.livekit_service_url,
|
||||
};
|
||||
|
||||
try {
|
||||
const client = await initClient(initClientParams, true);
|
||||
return {
|
||||
client,
|
||||
passwordlessUser,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoStoreIntegrityError) {
|
||||
// We can't use this session anymore, so let's log it out
|
||||
try {
|
||||
const client = await initClient(initClientParams, false); // Don't need the crypto store just to log out)
|
||||
await client.logout(true);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
"The previous session was lost, and we couldn't log it out, " +
|
||||
err +
|
||||
"either"
|
||||
);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
} catch (err) {
|
||||
clearSession();
|
||||
throw err;
|
||||
}
|
||||
isPasswordlessUser: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,14 +59,305 @@ export interface Session {
|
||||
tempPassword?: string;
|
||||
}
|
||||
|
||||
const clearSession = () => localStorage.removeItem("matrix-auth-store");
|
||||
const saveSession = (s: Session) =>
|
||||
localStorage.setItem("matrix-auth-store", JSON.stringify(s));
|
||||
const loadSession = (): Session | undefined => {
|
||||
const loadChannel =
|
||||
"BroadcastChannel" in window ? new BroadcastChannel("load") : null;
|
||||
|
||||
const loadSession = (): Session => {
|
||||
const data = localStorage.getItem("matrix-auth-store");
|
||||
if (!data) {
|
||||
return undefined;
|
||||
if (data) return JSON.parse(data);
|
||||
return null;
|
||||
};
|
||||
const saveSession = (session: Session) =>
|
||||
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
||||
const clearSession = () => localStorage.removeItem("matrix-auth-store");
|
||||
|
||||
interface ClientState {
|
||||
loading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
isPasswordlessUser: boolean;
|
||||
client: MatrixClient;
|
||||
userName: string;
|
||||
changePassword: (password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
setClient: (client: MatrixClient, session: Session) => void;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
const ClientContext = createContext<ClientState>(null);
|
||||
|
||||
type ClientProviderState = Omit<
|
||||
ClientState,
|
||||
"changePassword" | "logout" | "setClient"
|
||||
> & { error?: Error };
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
const history = useHistory();
|
||||
const initializing = useRef(false);
|
||||
const [
|
||||
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
||||
setState,
|
||||
] = useState<ClientProviderState>({
|
||||
loading: true,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
client: undefined,
|
||||
userName: null,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// In case the component is mounted, unmounted, and remounted quickly (as
|
||||
// React does in strict mode), we need to make sure not to doubly initialize
|
||||
// the client
|
||||
if (initializing.current) return;
|
||||
initializing.current = true;
|
||||
|
||||
const init = async (): Promise<
|
||||
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
||||
> => {
|
||||
if (widget) {
|
||||
// We're inside a widget, so let's engage *matryoshka mode*
|
||||
logger.log("Using a matryoshka client");
|
||||
return {
|
||||
client: await widget.client,
|
||||
isPasswordlessUser: false,
|
||||
};
|
||||
} else {
|
||||
// We're running as a standalone application
|
||||
try {
|
||||
const session = loadSession();
|
||||
if (!session) return { client: undefined, isPasswordlessUser: false };
|
||||
|
||||
logger.log("Using a standalone client");
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const { user_id, device_id, access_token, passwordlessUser } =
|
||||
session;
|
||||
|
||||
const livekit = Config.get().livekit;
|
||||
|
||||
try {
|
||||
return {
|
||||
client: await initClient(
|
||||
{
|
||||
baseUrl: Config.defaultHomeserverUrl(),
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
livekitServiceURL: livekit?.livekit_service_url,
|
||||
},
|
||||
true
|
||||
),
|
||||
isPasswordlessUser: passwordlessUser,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoStoreIntegrityError) {
|
||||
// We can't use this session anymore, so let's log it out
|
||||
try {
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: Config.defaultHomeserverUrl(),
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
livekitServiceURL: livekit?.livekit_service_url,
|
||||
},
|
||||
false // Don't need the crypto store just to log out
|
||||
);
|
||||
await client.logout(true);
|
||||
} catch (err_) {
|
||||
logger.warn(
|
||||
"The previous session was lost, and we couldn't log it out, " +
|
||||
"either"
|
||||
);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
} catch (err) {
|
||||
clearSession();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
init()
|
||||
.then(({ client, isPasswordlessUser }) => {
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isAuthenticated: Boolean(client),
|
||||
isPasswordlessUser,
|
||||
userName: client?.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
userName: null,
|
||||
error: undefined,
|
||||
});
|
||||
})
|
||||
.finally(() => (initializing.current = false));
|
||||
}, []);
|
||||
|
||||
const changePassword = useCallback(
|
||||
async (password: string) => {
|
||||
const { tempPassword, ...session } = loadSession();
|
||||
|
||||
await client.setPassword(
|
||||
{
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: session.user_id,
|
||||
},
|
||||
user: session.user_id,
|
||||
password: tempPassword,
|
||||
},
|
||||
password
|
||||
);
|
||||
|
||||
saveSession({ ...session, passwordlessUser: false });
|
||||
|
||||
setState({
|
||||
client,
|
||||
loading: false,
|
||||
isAuthenticated: true,
|
||||
isPasswordlessUser: false,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const setClient = useCallback(
|
||||
(newClient: MatrixClient, session: Session) => {
|
||||
if (client && client !== newClient) {
|
||||
client.stopClient();
|
||||
}
|
||||
|
||||
if (newClient) {
|
||||
saveSession(session);
|
||||
|
||||
setState({
|
||||
client: newClient,
|
||||
loading: false,
|
||||
isAuthenticated: true,
|
||||
isPasswordlessUser: session.passwordlessUser,
|
||||
userName: newClient.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
} else {
|
||||
clearSession();
|
||||
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
userName: null,
|
||||
error: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await client.logout(true);
|
||||
await client.clearStores();
|
||||
clearSession();
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: true,
|
||||
userName: "",
|
||||
error: undefined,
|
||||
});
|
||||
history.push("/");
|
||||
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
|
||||
}, [history, client]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// To protect against multiple sessions writing to the same storage
|
||||
// simultaneously, we send a broadcast message that shuts down all other
|
||||
// running instances of the app. This isn't necessary if the app is running in
|
||||
// a widget though, since then it'll be mostly stateless.
|
||||
useEffect(() => {
|
||||
if (!widget) loadChannel?.postMessage({});
|
||||
}, []);
|
||||
|
||||
useEventTarget(
|
||||
loadChannel,
|
||||
"message",
|
||||
useCallback(() => {
|
||||
client?.stopClient();
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error: translatedError(
|
||||
"This application has been opened in another tab.",
|
||||
t
|
||||
),
|
||||
}));
|
||||
}, [client, setState, t])
|
||||
);
|
||||
|
||||
const context = useMemo<ClientState>(
|
||||
() => ({
|
||||
loading,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
client,
|
||||
changePassword,
|
||||
logout,
|
||||
userName,
|
||||
setClient,
|
||||
error: undefined,
|
||||
}),
|
||||
[
|
||||
loading,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
client,
|
||||
changePassword,
|
||||
logout,
|
||||
userName,
|
||||
setClient,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.matrixclient = client;
|
||||
window.isPasswordlessUser = isPasswordlessUser;
|
||||
|
||||
if (PosthogAnalytics.hasInstance())
|
||||
PosthogAnalytics.instance.onLoginStatusChanged();
|
||||
}, [client, isPasswordlessUser]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return JSON.parse(data);
|
||||
return (
|
||||
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useClient = () => useContext(ClientContext);
|
||||
|
||||
@@ -47,8 +47,8 @@ export function Facepile({
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const _size = sizes.get(size)!;
|
||||
const _overlap = overlapMap[size]!;
|
||||
const _size = sizes.get(size);
|
||||
const _overlap = overlapMap[size];
|
||||
|
||||
const title = useMemo(() => {
|
||||
return members.reduce<string | null>(
|
||||
|
||||
@@ -18,7 +18,6 @@ import { ReactNode, useCallback, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||
import { LinkButton, Button } from "./button";
|
||||
@@ -59,7 +58,6 @@ export function ErrorView({ error }: ErrorViewProps) {
|
||||
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
const onReload = useCallback(() => {
|
||||
|
||||
@@ -36,16 +36,15 @@ export function ListBox<T>({
|
||||
listBoxRef,
|
||||
...rest
|
||||
}: ListBoxProps<T>) {
|
||||
const ref = useRef<HTMLUListElement>(null);
|
||||
const ref = useRef<HTMLUListElement>();
|
||||
if (!listBoxRef) listBoxRef = ref;
|
||||
|
||||
const listRef = listBoxRef ?? ref;
|
||||
|
||||
const { listBoxProps } = useListBox(rest, state, listRef);
|
||||
const { listBoxProps } = useListBox(rest, state, listBoxRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...listBoxProps}
|
||||
ref={listRef}
|
||||
ref={listBoxRef}
|
||||
className={classNames(styles.listBox, className)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
@@ -67,7 +66,7 @@ interface OptionProps<T> {
|
||||
}
|
||||
|
||||
function Option<T>({ item, state, className }: OptionProps<T>) {
|
||||
const ref = useRef(null);
|
||||
const ref = useRef();
|
||||
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
||||
{ key: item.key },
|
||||
state,
|
||||
@@ -84,11 +83,7 @@ function Option<T>({ item, state, className }: OptionProps<T>) {
|
||||
const origPointerUp = optionProps.onPointerUp;
|
||||
delete optionProps.onPointerUp;
|
||||
optionProps.onClick = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
origPointerUp(e as unknown as PointerEvent<HTMLElement>);
|
||||
},
|
||||
[origPointerUp]
|
||||
|
||||
@@ -26,7 +26,7 @@ import styles from "./Menu.module.css";
|
||||
|
||||
interface MenuProps<T> extends AriaMenuOptions<T> {
|
||||
className?: String;
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
onAction: (value: Key) => void;
|
||||
label?: string;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export function Menu<T extends object>({
|
||||
...rest
|
||||
}: MenuProps<T>) {
|
||||
const state = useTreeState<T>({ ...rest, selectionMode: "none" });
|
||||
const menuRef = useRef(null);
|
||||
const menuRef = useRef();
|
||||
const { menuProps } = useMenu<T>(rest, state, menuRef);
|
||||
|
||||
return (
|
||||
@@ -69,7 +69,7 @@ interface MenuItemProps<T> {
|
||||
}
|
||||
|
||||
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
|
||||
const ref = useRef(null);
|
||||
const ref = useRef();
|
||||
const { menuItemProps } = useMenuItem(
|
||||
{
|
||||
key: item.key,
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Modal({
|
||||
...rest
|
||||
}: ModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const modalRef = useRef(null);
|
||||
const modalRef = useRef();
|
||||
const { overlayProps, underlayProps } = useOverlay(
|
||||
{ ...rest, onClose },
|
||||
modalRef
|
||||
@@ -63,7 +63,7 @@ export function Modal({
|
||||
usePreventScroll();
|
||||
const { modalProps } = useModal();
|
||||
const { dialogProps, titleProps } = useDialog(rest, modalRef);
|
||||
const closeButtonRef = useRef(null);
|
||||
const closeButtonRef = useRef();
|
||||
const { buttonProps: closeButtonProps } = useButton(
|
||||
{
|
||||
onPress: () => onClose(),
|
||||
|
||||
@@ -36,9 +36,6 @@ export function SequenceDiagramViewerPage() {
|
||||
|
||||
const [debugLog, setDebugLog] = useState<DebugLog>();
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const onChangeDebugLog = useCallback((e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
e.target.files[0].text().then((text: string) => {
|
||||
@@ -58,7 +55,7 @@ export function SequenceDiagramViewerPage() {
|
||||
onChange={onChangeDebugLog}
|
||||
/>
|
||||
</FieldRow>
|
||||
{debugLog && selectedUserId && (
|
||||
{debugLog && (
|
||||
<SequenceDiagramViewer
|
||||
localUserId={debugLog.localUserId}
|
||||
selectedUserId={selectedUserId}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
||||
const tooltipTriggerProps = { delay: 250, ...rest };
|
||||
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
|
||||
const triggerRef = useObjectRef<HTMLElement>(ref);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const overlayRef = useRef();
|
||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||
tooltipTriggerProps,
|
||||
tooltipState,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 New Vector Ltd
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -17,8 +17,6 @@ limitations under the License.
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
interface UrlParams {
|
||||
roomAlias: string | null;
|
||||
roomId: string | null;
|
||||
@@ -90,46 +88,19 @@ interface UrlParams {
|
||||
|
||||
/**
|
||||
* Gets the app parameters for the current URL.
|
||||
* @param ignoreRoomAlias If true, does not try to parse a room alias from the URL
|
||||
* @param search The URL search string
|
||||
* @param pathname The URL path name
|
||||
* @param hash The URL hash
|
||||
* @param query The URL query string
|
||||
* @param fragment The URL fragment string
|
||||
* @returns The app parameters encoded in the URL
|
||||
*/
|
||||
export const getUrlParams = (
|
||||
ignoreRoomAlias?: boolean,
|
||||
search = window.location.search,
|
||||
pathname = window.location.pathname,
|
||||
hash = window.location.hash
|
||||
query: string = window.location.search,
|
||||
fragment: string = window.location.hash
|
||||
): UrlParams => {
|
||||
let roomAlias: string | undefined;
|
||||
if (!ignoreRoomAlias) {
|
||||
if (hash === "") {
|
||||
roomAlias = pathname.substring(1); // Strip the "/"
|
||||
|
||||
// Delete "/room/" and "?", if present
|
||||
if (roomAlias.startsWith("room/")) {
|
||||
roomAlias = roomAlias.substring("room/".length);
|
||||
}
|
||||
// Add "#", if not present
|
||||
if (!roomAlias.startsWith("#")) {
|
||||
roomAlias = `#${roomAlias}`;
|
||||
}
|
||||
} else {
|
||||
roomAlias = hash;
|
||||
}
|
||||
|
||||
// Add server part, if not present
|
||||
if (!roomAlias.includes(":")) {
|
||||
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
|
||||
}
|
||||
}
|
||||
|
||||
const fragmentQueryStart = hash.indexOf("?");
|
||||
const fragmentQueryStart = fragment.indexOf("?");
|
||||
const fragmentParams = new URLSearchParams(
|
||||
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart)
|
||||
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
|
||||
);
|
||||
const queryParams = new URLSearchParams(search);
|
||||
const queryParams = new URLSearchParams(query);
|
||||
|
||||
// Normally, URL params should be encoded in the fragment so as to avoid
|
||||
// leaking them to the server. However, we also check the normal query
|
||||
@@ -143,10 +114,16 @@ export const getUrlParams = (
|
||||
...queryParams.getAll(name),
|
||||
];
|
||||
|
||||
// The part of the fragment before the ?
|
||||
const fragmentRoute =
|
||||
fragmentQueryStart === -1
|
||||
? fragment
|
||||
: fragment.substring(0, fragmentQueryStart);
|
||||
|
||||
const fontScale = parseFloat(getParam("fontScale") ?? "");
|
||||
|
||||
return {
|
||||
roomAlias: !roomAlias || roomAlias.includes("!") ? null : roomAlias,
|
||||
roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null,
|
||||
roomId: getParam("roomId"),
|
||||
viaServers: getAllParams("via"),
|
||||
isEmbedded: hasParam("embed"),
|
||||
@@ -172,9 +149,6 @@ export const getUrlParams = (
|
||||
* @returns The app parameters for the current URL
|
||||
*/
|
||||
export const useUrlParams = (): UrlParams => {
|
||||
const { search, pathname, hash } = useLocation();
|
||||
return useMemo(
|
||||
() => getUrlParams(false, search, pathname, hash),
|
||||
[search, pathname, hash]
|
||||
);
|
||||
const { hash, search } = useLocation();
|
||||
return useMemo(() => getUrlParams(search, hash), [search, hash]);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ interface UserMenuProps {
|
||||
isAuthenticated: boolean;
|
||||
isPasswordlessUser: boolean;
|
||||
displayName: string;
|
||||
avatarUrl?: string;
|
||||
avatarUrl: string;
|
||||
onAction: (value: string) => void;
|
||||
}
|
||||
|
||||
@@ -119,24 +119,21 @@ export function UserMenu({
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(props: any) => (
|
||||
<Menu {...props} label={t("User menu")} onAction={onAction}>
|
||||
{items.map(({ key, icon: Icon, label, dataTestid }) => (
|
||||
<Item key={key} textValue={label}>
|
||||
<Icon
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.menuIcon}
|
||||
data-testid={dataTestid}
|
||||
/>
|
||||
<Body overflowEllipsis>{label}</Body>
|
||||
</Item>
|
||||
))}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
{(props) => (
|
||||
<Menu {...props} label={t("User menu")} onAction={onAction}>
|
||||
{items.map(({ key, icon: Icon, label, dataTestid }) => (
|
||||
<Item key={key} textValue={label}>
|
||||
<Icon
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.menuIcon}
|
||||
data-testid={dataTestid}
|
||||
/>
|
||||
<Body overflowEllipsis>{label}</Body>
|
||||
</Item>
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</PopoverMenuTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { useCallback, useState } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
|
||||
import { useClientLegacy } from "./ClientContext";
|
||||
import { useClient } from "./ClientContext";
|
||||
import { useProfile } from "./profile/useProfile";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { SettingsModal } from "./settings/SettingsModal";
|
||||
@@ -30,7 +30,8 @@ interface Props {
|
||||
export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
|
||||
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
||||
useClient();
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
@@ -48,7 +49,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
modalState.open();
|
||||
break;
|
||||
case "logout":
|
||||
logout?.();
|
||||
logout();
|
||||
break;
|
||||
case "login":
|
||||
history.push("/login", { state: { from: location } });
|
||||
@@ -58,18 +59,19 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
[history, location, logout, modalState]
|
||||
);
|
||||
|
||||
const userName = client?.getUserIdLocalpart() ?? "";
|
||||
return (
|
||||
<>
|
||||
<UserMenu
|
||||
preventNavigation={preventNavigation}
|
||||
isAuthenticated={authenticated}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
isAuthenticated={isAuthenticated}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
avatarUrl={avatarUrl}
|
||||
onAction={onAction}
|
||||
displayName={displayName || (userName ? userName.replace("@", "") : "")}
|
||||
displayName={
|
||||
displayName || (userName ? userName.replace("@", "") : undefined)
|
||||
}
|
||||
/>
|
||||
{modalState.isOpen && client && (
|
||||
{modalState.isOpen && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
defaultTab={defaultSettingsTab}
|
||||
|
||||
@@ -98,7 +98,7 @@ export class PosthogAnalytics {
|
||||
// set true during the constructor if posthog config is present, otherwise false
|
||||
private static internalInstance: PosthogAnalytics | null = null;
|
||||
|
||||
private identificationPromise?: Promise<void>;
|
||||
private identificationPromise: Promise<void>;
|
||||
private readonly enabled: boolean = false;
|
||||
private anonymity = Anonymity.Disabled;
|
||||
private platformSuperProperties = {};
|
||||
@@ -255,9 +255,7 @@ export class PosthogAnalytics {
|
||||
} catch (e) {
|
||||
// The above could fail due to network requests, but not essential to starting the application,
|
||||
// so swallow it.
|
||||
logger.log(
|
||||
"Unable to identify user for tracking" + (e as Error)?.toString()
|
||||
);
|
||||
logger.log("Unable to identify user for tracking" + e.toString());
|
||||
}
|
||||
if (analyticsID) {
|
||||
this.posthog.identify(analyticsID);
|
||||
@@ -368,7 +366,7 @@ export class PosthogAnalytics {
|
||||
|
||||
if (anonymity === Anonymity.Pseudonymous) {
|
||||
this.setRegistrationType(
|
||||
window.matrixclient.isGuest() || window.passwordlessUser
|
||||
window.matrixclient.isGuest() || window.isPasswordlessUser
|
||||
? RegistrationType.Guest
|
||||
: RegistrationType.Registered
|
||||
);
|
||||
|
||||
@@ -35,8 +35,8 @@ export const LoginPage: FC = () => {
|
||||
const { setClient } = useClient();
|
||||
const login = useInteractiveLogin();
|
||||
const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const usernameRef = useRef<HTMLInputElement>();
|
||||
const passwordRef = useRef<HTMLInputElement>();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -49,27 +49,12 @@ export const LoginPage: FC = () => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (!homeserver || !usernameRef.current || !passwordRef.current) {
|
||||
setError(Error("Login parameters are undefined"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||
.then(([client, session]) => {
|
||||
if (!setClient) {
|
||||
return;
|
||||
}
|
||||
setClient(client, session);
|
||||
|
||||
setClient({ client, session });
|
||||
|
||||
const locationState = location.state;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (locationState && locationState.from) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
history.push(locationState.from);
|
||||
if (location.state && location.state.from) {
|
||||
history.push(location.state.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
import { useClientLegacy } from "../ClientContext";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { useInteractiveRegistration } from "./useInteractiveRegistration";
|
||||
import styles from "./LoginPage.module.css";
|
||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||
@@ -45,10 +45,9 @@ export const RegisterPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t("Register"));
|
||||
|
||||
const { loading, authenticated, passwordlessUser, client, setClient } =
|
||||
useClientLegacy();
|
||||
|
||||
const confirmPasswordRef = useRef<HTMLInputElement>(null);
|
||||
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
|
||||
useClient();
|
||||
const confirmPasswordRef = useRef<HTMLInputElement>();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [registering, setRegistering] = useState(false);
|
||||
@@ -76,11 +75,10 @@ export const RegisterPage: FC = () => {
|
||||
userName,
|
||||
password,
|
||||
userName,
|
||||
recaptchaResponse,
|
||||
passwordlessUser
|
||||
recaptchaResponse
|
||||
);
|
||||
|
||||
if (client && client?.groupCallEventHandler && passwordlessUser) {
|
||||
if (client && isPasswordlessUser) {
|
||||
// Migrate the user's rooms
|
||||
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
|
||||
const roomId = groupCall.room.roomId;
|
||||
@@ -88,11 +86,7 @@ export const RegisterPage: FC = () => {
|
||||
try {
|
||||
await newClient.joinRoom(roomId);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (error.errcode === "M_LIMIT_EXCEEDED") {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
await sleep(error.data.retry_after_ms);
|
||||
await newClient.joinRoom(roomId);
|
||||
} else {
|
||||
@@ -103,17 +97,13 @@ export const RegisterPage: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
setClient?.({ client: newClient, session });
|
||||
setClient(newClient, session);
|
||||
PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date());
|
||||
};
|
||||
|
||||
submit()
|
||||
.then(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (location.state?.from) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
history.push(location.state?.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
@@ -129,7 +119,7 @@ export const RegisterPage: FC = () => {
|
||||
register,
|
||||
location,
|
||||
history,
|
||||
passwordlessUser,
|
||||
isPasswordlessUser,
|
||||
reset,
|
||||
execute,
|
||||
client,
|
||||
@@ -146,10 +136,10 @@ export const RegisterPage: FC = () => {
|
||||
}, [password, passwordConfirmation, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && authenticated && !passwordlessUser && !registering) {
|
||||
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
||||
history.push("/");
|
||||
}
|
||||
}, [loading, history, authenticated, passwordlessUser, registering]);
|
||||
}, [loading, history, isAuthenticated, isPasswordlessUser, registering]);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
|
||||
@@ -41,10 +41,8 @@ export const useInteractiveLogin = () =>
|
||||
},
|
||||
password,
|
||||
}),
|
||||
stateUpdated: (...args) => {},
|
||||
requestEmailToken: (...args): Promise<{ sid: string }> => {
|
||||
return Promise.resolve({ sid: "" });
|
||||
},
|
||||
stateUpdated: null,
|
||||
requestEmailToken: null,
|
||||
});
|
||||
|
||||
// XXX: This claims to return an IAuthData which contains none of these
|
||||
|
||||
@@ -23,32 +23,28 @@ import { Session } from "../ClientContext";
|
||||
import { Config } from "../config/Config";
|
||||
|
||||
export const useInteractiveRegistration = (): {
|
||||
privacyPolicyUrl?: string;
|
||||
recaptchaKey?: string;
|
||||
privacyPolicyUrl: string;
|
||||
recaptchaKey: string;
|
||||
register: (
|
||||
username: string,
|
||||
password: string,
|
||||
displayName: string,
|
||||
recaptchaResponse: string,
|
||||
passwordlessUser: boolean
|
||||
passwordlessUser?: boolean
|
||||
) => Promise<[MatrixClient, Session]>;
|
||||
} => {
|
||||
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [recaptchaKey, setRecaptchaKey] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string>();
|
||||
const [recaptchaKey, setRecaptchaKey] = useState<string>();
|
||||
|
||||
const authClient = useRef<MatrixClient>();
|
||||
if (!authClient.current) {
|
||||
authClient.current = createClient({
|
||||
baseUrl: Config.defaultHomeserverUrl()!,
|
||||
baseUrl: Config.defaultHomeserverUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
authClient.current!.registerRequest({}).catch((error) => {
|
||||
authClient.current.registerRequest({}).catch((error) => {
|
||||
setPrivacyPolicyUrl(
|
||||
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
|
||||
);
|
||||
@@ -62,12 +58,12 @@ export const useInteractiveRegistration = (): {
|
||||
password: string,
|
||||
displayName: string,
|
||||
recaptchaResponse: string,
|
||||
passwordlessUser: boolean
|
||||
passwordlessUser?: boolean
|
||||
): Promise<[MatrixClient, Session]> => {
|
||||
const interactiveAuth = new InteractiveAuth({
|
||||
matrixClient: authClient.current!,
|
||||
matrixClient: authClient.current,
|
||||
doRequest: (auth) =>
|
||||
authClient.current!.registerRequest({
|
||||
authClient.current.registerRequest({
|
||||
username,
|
||||
password,
|
||||
auth: auth || undefined,
|
||||
@@ -88,9 +84,7 @@ export const useInteractiveRegistration = (): {
|
||||
});
|
||||
}
|
||||
},
|
||||
requestEmailToken: (...args) => {
|
||||
return Promise.resolve({ sid: "dummy" });
|
||||
},
|
||||
requestEmailToken: null,
|
||||
});
|
||||
|
||||
// XXX: This claims to return an IAuthData which contains none of these
|
||||
@@ -101,7 +95,7 @@ export const useInteractiveRegistration = (): {
|
||||
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: Config.defaultHomeserverUrl()!,
|
||||
baseUrl: Config.defaultHomeserverUrl(),
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
@@ -123,7 +117,7 @@ export const useInteractiveRegistration = (): {
|
||||
session.tempPassword = password;
|
||||
}
|
||||
|
||||
const user = client.getUser(client.getUserId()!)!;
|
||||
const user = client.getUser(client.getUserId());
|
||||
user.setRawDisplayName(displayName);
|
||||
user.setDisplayName(displayName);
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ interface RecaptchaPromiseRef {
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
export const useRecaptcha = (sitekey?: string) => {
|
||||
export const useRecaptcha = (sitekey: string) => {
|
||||
const { t } = useTranslation();
|
||||
const [recaptchaId] = useState(() => randomString(16));
|
||||
const promiseRef = useRef<RecaptchaPromiseRef>();
|
||||
@@ -68,9 +68,9 @@ export const useRecaptcha = (sitekey?: string) => {
|
||||
}
|
||||
}, [recaptchaId, sitekey]);
|
||||
|
||||
const execute = useCallback((): Promise<string> => {
|
||||
const execute = useCallback(() => {
|
||||
if (!sitekey) {
|
||||
return Promise.resolve("");
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (!window.grecaptcha) {
|
||||
|
||||
@@ -23,9 +23,9 @@ import { generateRandomName } from "../auth/generateRandomName";
|
||||
import { useRecaptcha } from "../auth/useRecaptcha";
|
||||
|
||||
interface UseRegisterPasswordlessUserType {
|
||||
privacyPolicyUrl?: string;
|
||||
privacyPolicyUrl: string;
|
||||
registerPasswordlessUser: (displayName: string) => Promise<void>;
|
||||
recaptchaId?: string;
|
||||
recaptchaId: string;
|
||||
}
|
||||
|
||||
export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
|
||||
@@ -36,10 +36,6 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
|
||||
|
||||
const registerPasswordlessUser = useCallback(
|
||||
async (displayName: string) => {
|
||||
if (!setClient) {
|
||||
throw new Error("No client context");
|
||||
}
|
||||
|
||||
try {
|
||||
const recaptchaResponse = await execute();
|
||||
const userName = generateRandomName();
|
||||
@@ -50,7 +46,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
|
||||
recaptchaResponse,
|
||||
true
|
||||
);
|
||||
setClient({ client, session });
|
||||
setClient(client, session);
|
||||
} catch (e) {
|
||||
reset();
|
||||
throw e;
|
||||
|
||||
@@ -46,7 +46,7 @@ export function LinkButton({
|
||||
<Link
|
||||
className={classNames(
|
||||
variantToClassName[variant || "secondary"],
|
||||
size ? sizeToClassName[size] : [],
|
||||
sizeToClassName[size],
|
||||
className
|
||||
)}
|
||||
to={to}
|
||||
|
||||
@@ -45,11 +45,11 @@ export class Config {
|
||||
|
||||
// Convenience accessors
|
||||
public static defaultHomeserverUrl(): string | undefined {
|
||||
return Config.get().default_server_config?.["m.homeserver"].base_url;
|
||||
return Config.get().default_server_config["m.homeserver"].base_url;
|
||||
}
|
||||
|
||||
public static defaultServerName(): string | undefined {
|
||||
return Config.get().default_server_config?.["m.homeserver"].server_name;
|
||||
return Config.get().default_server_config["m.homeserver"].server_name;
|
||||
}
|
||||
|
||||
public config?: ResolvedConfigOptions;
|
||||
|
||||
@@ -35,13 +35,13 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.callList}>
|
||||
{rooms.map(({ roomAlias, roomName, avatarUrl, participants }) => (
|
||||
{rooms.map(({ roomId, roomName, avatarUrl, participants }) => (
|
||||
<CallTile
|
||||
key={roomAlias}
|
||||
key={roomId}
|
||||
client={client}
|
||||
name={roomName}
|
||||
avatarUrl={avatarUrl}
|
||||
roomAlias={roomAlias}
|
||||
roomId={roomId}
|
||||
participants={participants}
|
||||
disableFacepile={disableFacepile}
|
||||
/>
|
||||
@@ -59,7 +59,7 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
||||
interface CallTileProps {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
roomAlias: string;
|
||||
roomId: string;
|
||||
participants: RoomMember[];
|
||||
client: MatrixClient;
|
||||
disableFacepile?: boolean;
|
||||
@@ -67,17 +67,14 @@ interface CallTileProps {
|
||||
function CallTile({
|
||||
name,
|
||||
avatarUrl,
|
||||
roomAlias,
|
||||
roomId,
|
||||
participants,
|
||||
client,
|
||||
disableFacepile,
|
||||
}: CallTileProps) {
|
||||
return (
|
||||
<div className={styles.callTile}>
|
||||
<Link
|
||||
to={`/${roomAlias.substring(1).split(":")[0]}`}
|
||||
className={styles.callTileLink}
|
||||
>
|
||||
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
||||
<Avatar
|
||||
size={Size.LG}
|
||||
bgKey={name}
|
||||
@@ -89,7 +86,7 @@ function CallTile({
|
||||
<Body overflowEllipsis fontWeight="semiBold">
|
||||
{name}
|
||||
</Body>
|
||||
<Caption overflowEllipsis>{getRoomUrl(roomAlias)}</Caption>
|
||||
<Caption overflowEllipsis>{getRoomUrl(roomId)}</Caption>
|
||||
{participants && !disableFacepile && (
|
||||
<Facepile
|
||||
className={styles.facePile}
|
||||
@@ -103,7 +100,7 @@ function CallTile({
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
variant="icon"
|
||||
value={getRoomUrl(roomAlias)}
|
||||
value={getRoomUrl(roomId)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,12 +42,6 @@ interface Props {
|
||||
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onAction = (key: React.Key) => {
|
||||
setCallType(key.toString() as CallType);
|
||||
};
|
||||
|
||||
const onClose = () => {};
|
||||
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom">
|
||||
<Button variant="dropdown" className={commonStyles.headline}>
|
||||
@@ -58,12 +52,7 @@ export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
|
||||
</Headline>
|
||||
</Button>
|
||||
{(props: JSX.IntrinsicAttributes) => (
|
||||
<Menu
|
||||
{...props}
|
||||
label={t("Call type menu")}
|
||||
onAction={onAction}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Menu {...props} label={t("Call type menu")} onAction={setCallType}>
|
||||
<Item key={CallType.Video} textValue={t("Video call")}>
|
||||
<VideoIcon />
|
||||
<span>{t("Video call")}</span>
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useClientState } from "../ClientContext";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { UnauthenticatedView } from "./UnauthenticatedView";
|
||||
import { RegisteredView } from "./RegisteredView";
|
||||
@@ -26,18 +26,16 @@ export function HomePage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t("Home"));
|
||||
|
||||
const clientState = useClientState();
|
||||
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
||||
useClient();
|
||||
|
||||
if (!clientState) {
|
||||
if (loading) {
|
||||
return <LoadingView />;
|
||||
} else if (clientState.state === "error") {
|
||||
return <ErrorView error={clientState.error} />;
|
||||
} else if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
} else {
|
||||
return clientState.authenticated ? (
|
||||
<RegisteredView
|
||||
isPasswordlessUser={clientState.authenticated.isPasswordlessUser}
|
||||
client={clientState.authenticated.client}
|
||||
/>
|
||||
return isAuthenticated ? (
|
||||
<RegisteredView isPasswordlessUser={isPasswordlessUser} client={client} />
|
||||
) : (
|
||||
<UnauthenticatedView />
|
||||
);
|
||||
|
||||
@@ -70,10 +70,10 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
||||
setError(undefined);
|
||||
setLoading(true);
|
||||
|
||||
const [roomAlias] = await createRoom(client, roomName, ptt);
|
||||
const [roomIdOrAlias] = await createRoom(client, roomName, ptt);
|
||||
|
||||
if (roomAlias) {
|
||||
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
|
||||
if (roomIdOrAlias) {
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,21 +79,16 @@ export const UnauthenticatedView: FC = () => {
|
||||
true
|
||||
);
|
||||
|
||||
let roomAlias: string;
|
||||
let roomIdOrAlias: string;
|
||||
try {
|
||||
[roomAlias] = await createRoom(client, roomName, ptt);
|
||||
[roomIdOrAlias] = await createRoom(client, roomName, ptt);
|
||||
} catch (error) {
|
||||
if (!setClient) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (error.errcode === "M_ROOM_IN_USE") {
|
||||
setOnFinished(() => {
|
||||
setClient({ client, session });
|
||||
setClient(client, session);
|
||||
const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName);
|
||||
history.push(`/${aliasLocalpart}`);
|
||||
const [, serverName] = client.getUserId().split(":");
|
||||
history.push(`/room/#${aliasLocalpart}:${serverName}`);
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
@@ -105,12 +100,8 @@ export const UnauthenticatedView: FC = () => {
|
||||
}
|
||||
|
||||
// Only consider the registration successful if we managed to create the room, too
|
||||
if (!setClient) {
|
||||
throw new Error("setClient is undefined");
|
||||
}
|
||||
|
||||
setClient({ client, session });
|
||||
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
|
||||
setClient(client, session);
|
||||
history.push(`/room/${roomIdOrAlias}`);
|
||||
}
|
||||
|
||||
submit().catch((error) => {
|
||||
@@ -213,7 +204,7 @@ export const UnauthenticatedView: FC = () => {
|
||||
</Body>
|
||||
</footer>
|
||||
</div>
|
||||
{modalState.isOpen && onFinished && (
|
||||
{modalState.isOpen && (
|
||||
<JoinExistingCallModal onJoin={onFinished} {...modalProps} />
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEv
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export interface GroupCallRoom {
|
||||
roomAlias: string;
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
room: Room;
|
||||
@@ -42,7 +42,7 @@ function getLastTs(client: MatrixClient, r: Room) {
|
||||
return ts;
|
||||
}
|
||||
|
||||
const myUserId = client.getUserId()!;
|
||||
const myUserId = client.getUserId();
|
||||
|
||||
if (r.getMyMembership() !== "join") {
|
||||
const membershipEvent = r.currentState.getStateEvents(
|
||||
@@ -79,32 +79,26 @@ function sortRooms(client: MatrixClient, rooms: Room[]): Room[] {
|
||||
}
|
||||
|
||||
export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
||||
const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
|
||||
const [rooms, setRooms] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateRooms() {
|
||||
if (!client.groupCallEventHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
||||
const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room);
|
||||
const filteredRooms = rooms.filter((r) => r.getCanonicalAlias()); // We don't display rooms without an alias
|
||||
const sortedRooms = sortRooms(client, filteredRooms);
|
||||
const sortedRooms = sortRooms(client, rooms);
|
||||
const items = sortedRooms.map((room) => {
|
||||
const groupCall = client.getGroupCallForRoom(room.roomId)!;
|
||||
const groupCall = client.getGroupCallForRoom(room.roomId);
|
||||
|
||||
return {
|
||||
roomAlias: room.getCanonicalAlias(),
|
||||
roomId: room.getCanonicalAlias() || room.roomId,
|
||||
roomName: room.name,
|
||||
avatarUrl: room.getMxcAvatarUrl()!,
|
||||
avatarUrl: room.getMxcAvatarUrl(),
|
||||
room,
|
||||
groupCall,
|
||||
participants: [...groupCall!.participants.keys()],
|
||||
participants: [...groupCall.participants],
|
||||
};
|
||||
});
|
||||
|
||||
setRooms(items as GroupCallRoom[]);
|
||||
setRooms(items);
|
||||
}
|
||||
|
||||
updateRooms();
|
||||
|
||||
@@ -55,7 +55,7 @@ export class Initializer {
|
||||
languageDetector.addDetector({
|
||||
name: "urlFragment",
|
||||
// Look for a language code in the URL's fragment
|
||||
lookup: () => getUrlParams(true).lang ?? undefined,
|
||||
lookup: () => getUrlParams().lang ?? undefined,
|
||||
});
|
||||
|
||||
i18n
|
||||
@@ -140,7 +140,7 @@ export class Initializer {
|
||||
}
|
||||
|
||||
// Custom fonts
|
||||
const { fonts, fontScale } = getUrlParams(true);
|
||||
const { fonts, fontScale } = getUrlParams();
|
||||
if (fontScale !== null) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-scale",
|
||||
@@ -229,6 +229,5 @@ export class Initializer {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
private initPromise?: Promise<void>;
|
||||
private initPromise: Promise<void> | null;
|
||||
}
|
||||
|
||||
@@ -15,14 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { useObjectRef } from "@react-aria/utils";
|
||||
import {
|
||||
AllHTMLAttributes,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useState,
|
||||
forwardRef,
|
||||
ChangeEvent,
|
||||
} from "react";
|
||||
import { AllHTMLAttributes, ChangeEvent, useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useState } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -34,7 +30,7 @@ import styles from "./AvatarInputField.module.css";
|
||||
interface Props extends AllHTMLAttributes<HTMLInputElement> {
|
||||
id: string;
|
||||
label: string;
|
||||
avatarUrl: string | undefined;
|
||||
avatarUrl: string;
|
||||
displayName: string;
|
||||
onRemoveAvatar: () => void;
|
||||
}
|
||||
@@ -47,7 +43,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [removed, setRemoved] = useState(false);
|
||||
const [objUrl, setObjUrl] = useState<string | undefined>(undefined);
|
||||
const [objUrl, setObjUrl] = useState<string>(null);
|
||||
|
||||
const fileInputRef = useObjectRef(ref);
|
||||
|
||||
@@ -56,11 +52,11 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
||||
|
||||
const onChange = (e: Event) => {
|
||||
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
|
||||
if (inputEvent.target.files && inputEvent.target.files.length > 0) {
|
||||
if (inputEvent.target.files.length > 0) {
|
||||
setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
|
||||
setRemoved(false);
|
||||
} else {
|
||||
setObjUrl(undefined);
|
||||
setObjUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -81,7 +77,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
size={Size.XL}
|
||||
src={removed ? undefined : objUrl || avatarUrl}
|
||||
src={removed ? null : objUrl || avatarUrl}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
<input
|
||||
|
||||
@@ -82,7 +82,7 @@ interface InputFieldProps {
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
defaultChecked?: boolean;
|
||||
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onChange?: (event: ChangeEvent) => void;
|
||||
}
|
||||
|
||||
export const InputField = forwardRef<
|
||||
@@ -119,8 +119,6 @@ export const InputField = forwardRef<
|
||||
>
|
||||
{prefix && <span>{prefix}</span>}
|
||||
{type === "textarea" ? (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<textarea
|
||||
id={id}
|
||||
ref={ref as ForwardedRef<HTMLTextAreaElement>}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function SelectInput(props: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const state = useSelectState(props);
|
||||
|
||||
const ref = useRef(null);
|
||||
const ref = useRef();
|
||||
const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
|
||||
props,
|
||||
state,
|
||||
|
||||
@@ -38,7 +38,7 @@ interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const SFUConfigContext = createContext<SFUConfig | undefined>(undefined);
|
||||
const SFUConfigContext = createContext<SFUConfig>(undefined);
|
||||
|
||||
export const useSFUConfig = () => useContext(SFUConfigContext);
|
||||
|
||||
@@ -58,7 +58,7 @@ export function OpenIDLoader({ client, groupCall, roomName, children }: Props) {
|
||||
setState({ kind: "loaded", sfuConfig: result });
|
||||
} catch (e) {
|
||||
logger.error("Failed to fetch SFU config: ", e);
|
||||
setState({ kind: "failed", error: e as Error });
|
||||
setState({ kind: "failed", error: e });
|
||||
}
|
||||
})();
|
||||
}, [client, groupCall, roomName]);
|
||||
|
||||
@@ -41,16 +41,13 @@ export async function getSFUConfigWithOpenID(
|
||||
// if the call has a livekit service URL, try it.
|
||||
if (groupCall.livekitServiceURL) {
|
||||
try {
|
||||
logger.info(
|
||||
`Trying to get JWT from call's configured URL of ${groupCall.livekitServiceURL}...`
|
||||
);
|
||||
logger.info(`Trying to get JWT from ${groupCall.livekitServiceURL}...`);
|
||||
const sfuConfig = await getLiveKitJWT(
|
||||
client,
|
||||
groupCall.livekitServiceURL,
|
||||
roomName,
|
||||
openIdToken
|
||||
);
|
||||
logger.info(`Got JWT from call state event URL.`);
|
||||
|
||||
return sfuConfig;
|
||||
} catch (e) {
|
||||
@@ -64,7 +61,7 @@ export async function getSFUConfigWithOpenID(
|
||||
// otherwise, try our configured one and, if it works, update the call's service URL in the state event
|
||||
// NB. This wuill update it for everyone so we may end up with multiple clients updating this when they
|
||||
// join at similar times, but we don't have a huge number of options here.
|
||||
const urlFromConf = Config.get().livekit!.livekit_service_url;
|
||||
const urlFromConf = Config.get().livekit.livekit_service_url;
|
||||
logger.info(`Trying livekit service URL from our config: ${urlFromConf}...`);
|
||||
try {
|
||||
const sfuConfig = await getLiveKitJWT(
|
||||
@@ -74,12 +71,9 @@ export async function getSFUConfigWithOpenID(
|
||||
openIdToken
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Got JWT, updating call livekit service URL with: ${urlFromConf}...`
|
||||
);
|
||||
logger.info(`Updating call livekit service URL with: ${urlFromConf}...`);
|
||||
try {
|
||||
await groupCall.updateLivekitServiceURL(urlFromConf);
|
||||
logger.info(`Call livekit service URL updated.`);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Failed to update call livekit service URL: continuing anyway.`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Room, RoomOptions, setLogLevel } from "livekit-client";
|
||||
import { Room, RoomOptions } from "livekit-client";
|
||||
import { useLiveKitRoom } from "@livekit/components-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
@@ -15,11 +15,9 @@ export type DeviceChoices = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
setLogLevel("debug");
|
||||
|
||||
export function useLiveKit(
|
||||
userChoices: UserChoices,
|
||||
sfuConfig?: SFUConfig
|
||||
sfuConfig: SFUConfig
|
||||
): Room | undefined {
|
||||
const roomOptions = useMemo((): RoomOptions => {
|
||||
const options = defaultLiveKitOptions;
|
||||
@@ -35,8 +33,8 @@ export function useLiveKit(
|
||||
}, [userChoices.video, userChoices.audio]);
|
||||
|
||||
const { room } = useLiveKitRoom({
|
||||
token: sfuConfig?.jwt,
|
||||
serverUrl: sfuConfig?.url,
|
||||
token: sfuConfig.jwt,
|
||||
serverUrl: sfuConfig.url,
|
||||
audio: userChoices.audio?.enabled ?? false,
|
||||
video: userChoices.video?.enabled ?? false,
|
||||
options: roomOptions,
|
||||
|
||||
@@ -19,7 +19,8 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
|
||||
import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
@@ -56,8 +57,8 @@ function waitForSync(client: MatrixClient) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const onSync = (
|
||||
state: SyncState,
|
||||
_old: SyncState | null,
|
||||
data?: ISyncStateData
|
||||
_old: SyncState,
|
||||
data: ISyncStateData
|
||||
) => {
|
||||
if (state === "PREPARED") {
|
||||
resolve();
|
||||
@@ -86,7 +87,7 @@ export async function initClient(
|
||||
): Promise<MatrixClient> {
|
||||
await loadOlm();
|
||||
|
||||
let indexedDB: IDBFactory | undefined;
|
||||
let indexedDB: IDBFactory;
|
||||
try {
|
||||
indexedDB = window.indexedDB;
|
||||
} catch (e) {}
|
||||
@@ -246,7 +247,7 @@ export function sanitiseRoomNameInput(input: string): string {
|
||||
*/
|
||||
export function roomNameFromRoomId(roomId: string): string {
|
||||
return roomId
|
||||
.match(/([^:]+):.*$/)![1]
|
||||
.match(/([^:]+):.*$/)[1]
|
||||
.substring(1)
|
||||
.split("-")
|
||||
.map((part) =>
|
||||
@@ -261,7 +262,7 @@ export function isLocalRoomId(roomId: string, client: MatrixClient): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = roomId.match(/[^:]+:(.*)$/)!;
|
||||
const parts = roomId.match(/[^:]+:(.*)$/);
|
||||
|
||||
if (parts.length < 2) {
|
||||
return false;
|
||||
@@ -301,7 +302,7 @@ export async function createRoom(
|
||||
"org.matrix.msc3401.call.member": 0,
|
||||
},
|
||||
users: {
|
||||
[client.getUserId()!]: 100,
|
||||
[client.getUserId()]: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -345,11 +346,15 @@ export async function createRoom(
|
||||
// Returns a URL to that will load Element Call with the given room
|
||||
export function getRoomUrl(roomIdOrAlias: string): string {
|
||||
if (roomIdOrAlias.startsWith("#")) {
|
||||
return `${window.location.protocol}//${window.location.host}/${
|
||||
roomIdOrAlias.substring(1).split(":")[0]
|
||||
}`;
|
||||
const [localPart, host] = roomIdOrAlias.replace("#", "").split(":");
|
||||
|
||||
if (host !== Config.defaultServerName()) {
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`;
|
||||
} else {
|
||||
return `${window.location.protocol}//${window.location.host}/${localPart}`;
|
||||
}
|
||||
} else {
|
||||
return `${window.location.protocol}//${window.location.host}/room?roomId=${roomIdOrAlias}`;
|
||||
return `${window.location.protocol}//${window.location.host}/room/#?roomId=${roomIdOrAlias}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,46 +61,46 @@ export class OTelCall {
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.call.peerConn?.removeEventListener(
|
||||
this.call.peerConn.removeEventListener(
|
||||
"connectionstatechange",
|
||||
this.onCallConnectionStateChanged
|
||||
);
|
||||
this.call.peerConn?.removeEventListener(
|
||||
this.call.peerConn.removeEventListener(
|
||||
"signalingstatechange",
|
||||
this.onCallSignalingStateChanged
|
||||
);
|
||||
this.call.peerConn?.removeEventListener(
|
||||
this.call.peerConn.removeEventListener(
|
||||
"iceconnectionstatechange",
|
||||
this.onIceConnectionStateChanged
|
||||
);
|
||||
this.call.peerConn?.removeEventListener(
|
||||
this.call.peerConn.removeEventListener(
|
||||
"icegatheringstatechange",
|
||||
this.onIceGatheringStateChanged
|
||||
);
|
||||
this.call.peerConn?.removeEventListener(
|
||||
this.call.peerConn.removeEventListener(
|
||||
"icecandidateerror",
|
||||
this.onIceCandidateError
|
||||
);
|
||||
}
|
||||
|
||||
private addCallPeerConnListeners = (): void => {
|
||||
this.call.peerConn?.addEventListener(
|
||||
this.call.peerConn.addEventListener(
|
||||
"connectionstatechange",
|
||||
this.onCallConnectionStateChanged
|
||||
);
|
||||
this.call.peerConn?.addEventListener(
|
||||
this.call.peerConn.addEventListener(
|
||||
"signalingstatechange",
|
||||
this.onCallSignalingStateChanged
|
||||
);
|
||||
this.call.peerConn?.addEventListener(
|
||||
this.call.peerConn.addEventListener(
|
||||
"iceconnectionstatechange",
|
||||
this.onIceConnectionStateChanged
|
||||
);
|
||||
this.call.peerConn?.addEventListener(
|
||||
this.call.peerConn.addEventListener(
|
||||
"icegatheringstatechange",
|
||||
this.onIceGatheringStateChanged
|
||||
);
|
||||
this.call.peerConn?.addEventListener(
|
||||
this.call.peerConn.addEventListener(
|
||||
"icecandidateerror",
|
||||
this.onIceCandidateError
|
||||
);
|
||||
@@ -108,25 +108,25 @@ export class OTelCall {
|
||||
|
||||
public onCallConnectionStateChanged = (): void => {
|
||||
this.span.addEvent("matrix.call.callConnectionStateChange", {
|
||||
callConnectionState: this.call.peerConn?.connectionState,
|
||||
callConnectionState: this.call.peerConn.connectionState,
|
||||
});
|
||||
};
|
||||
|
||||
public onCallSignalingStateChanged = (): void => {
|
||||
this.span.addEvent("matrix.call.callSignalingStateChange", {
|
||||
callSignalingState: this.call.peerConn?.signalingState,
|
||||
callSignalingState: this.call.peerConn.signalingState,
|
||||
});
|
||||
};
|
||||
|
||||
public onIceConnectionStateChanged = (): void => {
|
||||
this.span.addEvent("matrix.call.iceConnectionStateChange", {
|
||||
iceConnectionState: this.call.peerConn?.iceConnectionState,
|
||||
iceConnectionState: this.call.peerConn.iceConnectionState,
|
||||
});
|
||||
};
|
||||
|
||||
public onIceGatheringStateChanged = (): void => {
|
||||
this.span.addEvent("matrix.call.iceGatheringStateChange", {
|
||||
iceGatheringState: this.call.peerConn?.iceGatheringState,
|
||||
iceGatheringState: this.call.peerConn.iceGatheringState,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ export class OTelGroupCallMembership {
|
||||
if (
|
||||
!userCalls ||
|
||||
!userCalls.has(callTrackingInfo.deviceId) ||
|
||||
userCalls.get(callTrackingInfo.deviceId)?.callId !==
|
||||
userCalls.get(callTrackingInfo.deviceId).callId !==
|
||||
callTrackingInfo.call.callId
|
||||
) {
|
||||
callTrackingInfo.end();
|
||||
@@ -420,7 +420,7 @@ export class OTelGroupCallMembership {
|
||||
ctx
|
||||
);
|
||||
|
||||
span.setAttribute("matrix.callId", callId ?? "unknown");
|
||||
span.setAttribute("matrix.callId", callId);
|
||||
span.setAttribute(
|
||||
"matrix.opponentMemberId",
|
||||
report.opponentMemberId ? report.opponentMemberId : "unknown"
|
||||
|
||||
@@ -23,6 +23,7 @@ import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor";
|
||||
import { Anonymity } from "../analytics/PosthogAnalytics";
|
||||
import { Config } from "../config/Config";
|
||||
import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor";
|
||||
|
||||
@@ -33,7 +34,8 @@ let sharedInstance: ElementCallOpenTelemetry;
|
||||
export class ElementCallOpenTelemetry {
|
||||
private _provider: WebTracerProvider;
|
||||
private _tracer: Tracer;
|
||||
private otlpExporter?: OTLPTraceExporter;
|
||||
private _anonymity: Anonymity;
|
||||
private otlpExporter: OTLPTraceExporter;
|
||||
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
|
||||
|
||||
static globalInit(): void {
|
||||
@@ -98,7 +100,7 @@ export class ElementCallOpenTelemetry {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
opentelemetry.trace.disable();
|
||||
opentelemetry.trace.setGlobalTracerProvider(null);
|
||||
this._provider?.shutdown();
|
||||
}
|
||||
|
||||
@@ -113,4 +115,8 @@ export class ElementCallOpenTelemetry {
|
||||
public get provider(): WebTracerProvider {
|
||||
return this._provider;
|
||||
}
|
||||
|
||||
public get anonymity(): Anonymity {
|
||||
return this._anonymity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const PopoverMenuTrigger = forwardRef<
|
||||
buttonRef
|
||||
);
|
||||
|
||||
const popoverRef = useRef(null);
|
||||
const popoverRef = useRef();
|
||||
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
targetRef: buttonRef,
|
||||
|
||||
@@ -21,10 +21,10 @@ import { FileType } from "matrix-js-sdk/src/http-api";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
|
||||
interface ProfileLoadState {
|
||||
success: boolean;
|
||||
loading: boolean;
|
||||
displayName?: string;
|
||||
avatarUrl?: string;
|
||||
success?: boolean;
|
||||
loading?: boolean;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
@@ -38,26 +38,23 @@ type ProfileSaveCallback = ({
|
||||
removeAvatar: boolean;
|
||||
}) => Promise<void>;
|
||||
|
||||
export function useProfile(client: MatrixClient | undefined) {
|
||||
const [{ success, loading, displayName, avatarUrl, error }, setState] =
|
||||
export function useProfile(client: MatrixClient) {
|
||||
const [{ loading, displayName, avatarUrl, error, success }, setState] =
|
||||
useState<ProfileLoadState>(() => {
|
||||
let user: User | undefined = undefined;
|
||||
if (client) {
|
||||
user = client.getUser(client.getUserId()!) ?? undefined;
|
||||
}
|
||||
const user = client?.getUser(client.getUserId());
|
||||
|
||||
return {
|
||||
success: false,
|
||||
loading: false,
|
||||
displayName: user?.rawDisplayName,
|
||||
avatarUrl: user?.avatarUrl,
|
||||
error: undefined,
|
||||
error: null,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const onChangeUser = (
|
||||
_event: MatrixEvent | undefined,
|
||||
_event: MatrixEvent,
|
||||
{ displayName, avatarUrl }: User
|
||||
) => {
|
||||
setState({
|
||||
@@ -65,16 +62,17 @@ export function useProfile(client: MatrixClient | undefined) {
|
||||
loading: false,
|
||||
displayName,
|
||||
avatarUrl,
|
||||
error: undefined,
|
||||
error: null,
|
||||
});
|
||||
};
|
||||
|
||||
let user: User | null;
|
||||
let user: User;
|
||||
|
||||
if (client) {
|
||||
const userId = client.getUserId()!;
|
||||
const userId = client.getUserId();
|
||||
user = client.getUser(userId);
|
||||
user?.on(UserEvent.DisplayName, onChangeUser);
|
||||
user?.on(UserEvent.AvatarUrl, onChangeUser);
|
||||
user.on(UserEvent.DisplayName, onChangeUser);
|
||||
user.on(UserEvent.AvatarUrl, onChangeUser);
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -91,7 +89,7 @@ export function useProfile(client: MatrixClient | undefined) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: true,
|
||||
error: undefined,
|
||||
error: null,
|
||||
success: false,
|
||||
}));
|
||||
|
||||
@@ -112,9 +110,7 @@ export function useProfile(client: MatrixClient | undefined) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
displayName,
|
||||
avatarUrl: removeAvatar
|
||||
? undefined
|
||||
: mxcAvatarUrl ?? prev.avatarUrl,
|
||||
avatarUrl: removeAvatar ? null : mxcAvatarUrl ?? prev.avatarUrl,
|
||||
loading: false,
|
||||
success: true,
|
||||
}));
|
||||
|
||||
@@ -38,15 +38,6 @@ export function GridLayoutMenu({ layout, setLayout }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Change layout"), [t]);
|
||||
|
||||
const onAction = useCallback(
|
||||
(key: React.Key) => {
|
||||
setLayout(key.toString() as Layout);
|
||||
},
|
||||
[setLayout]
|
||||
);
|
||||
|
||||
const onClose = useCallback(() => {}, []);
|
||||
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
@@ -55,12 +46,7 @@ export function GridLayoutMenu({ layout, setLayout }: Props) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{(props: JSX.IntrinsicAttributes) => (
|
||||
<Menu
|
||||
{...props}
|
||||
label={t("Grid layout menu")}
|
||||
onAction={onAction}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Menu {...props} label={t("Grid layout menu")} onAction={setLayout}>
|
||||
<Item key="freedom" textValue={t("Freedom")}>
|
||||
<FreedomIcon />
|
||||
<span>Freedom</span>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
@@ -73,7 +70,7 @@ const defaultCollapsedFields = [
|
||||
];
|
||||
|
||||
function shouldCollapse({ name }: CollapsedFieldProps) {
|
||||
return name ? defaultCollapsedFields.includes(name) : false;
|
||||
return defaultCollapsedFields.includes(name);
|
||||
}
|
||||
|
||||
function getUserName(userId: string) {
|
||||
@@ -199,7 +196,7 @@ export function SequenceDiagramViewer({
|
||||
onSelectUserId,
|
||||
events,
|
||||
}: SequenceDiagramViewerProps) {
|
||||
const mermaidElRef = useRef<HTMLDivElement>(null);
|
||||
const mermaidElRef = useRef<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.initialize({
|
||||
@@ -220,7 +217,6 @@ export function SequenceDiagramViewer({
|
||||
`;
|
||||
|
||||
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
|
||||
if (!mermaidElRef.current) return;
|
||||
mermaidElRef.current.innerHTML = svgCode;
|
||||
});
|
||||
}, [events, localUserId, selectedUserId]);
|
||||
@@ -232,7 +228,7 @@ export function SequenceDiagramViewer({
|
||||
className={styles.selectInput}
|
||||
label="Remote User"
|
||||
selectedKey={selectedUserId}
|
||||
onSelectionChange={(key) => onSelectUserId(key.toString())}
|
||||
onSelectionChange={onSelectUserId}
|
||||
>
|
||||
{remoteUserIds.map((userId) => (
|
||||
<Item key={userId}>{userId}</Item>
|
||||
@@ -502,7 +498,7 @@ export function GroupCallInspector({
|
||||
return (
|
||||
<Resizable
|
||||
enable={{ top: true }}
|
||||
defaultSize={{ height: 200, width: 0 }}
|
||||
defaultSize={{ height: 200, width: undefined }}
|
||||
className={styles.inspector}
|
||||
>
|
||||
<div className={styles.toolbar}>
|
||||
@@ -511,19 +507,15 @@ export function GroupCallInspector({
|
||||
</button>
|
||||
<button onClick={() => setCurrentTab("inspector")}>Inspector</button>
|
||||
</div>
|
||||
{currentTab === "sequence-diagrams" &&
|
||||
state.localUserId &&
|
||||
selectedUserId &&
|
||||
state.eventsByUserId &&
|
||||
state.remoteUserIds && (
|
||||
<SequenceDiagramViewer
|
||||
localUserId={state.localUserId}
|
||||
selectedUserId={selectedUserId}
|
||||
onSelectUserId={setSelectedUserId}
|
||||
remoteUserIds={state.remoteUserIds}
|
||||
events={state.eventsByUserId[selectedUserId]}
|
||||
/>
|
||||
)}
|
||||
{currentTab === "sequence-diagrams" && (
|
||||
<SequenceDiagramViewer
|
||||
localUserId={state.localUserId}
|
||||
selectedUserId={selectedUserId}
|
||||
onSelectUserId={setSelectedUserId}
|
||||
remoteUserIds={state.remoteUserIds}
|
||||
events={state.eventsByUserId[selectedUserId]}
|
||||
/>
|
||||
)}
|
||||
{currentTab === "inspector" && (
|
||||
<ReactJson
|
||||
theme="monokai"
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
@@ -38,23 +39,26 @@ export function GroupCallLoader({
|
||||
createPtt,
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const groupCallState = useLoadGroupCall(
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
client,
|
||||
roomIdOrAlias,
|
||||
viaServers,
|
||||
createPtt
|
||||
);
|
||||
|
||||
switch (groupCallState.kind) {
|
||||
case "loading":
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("Loading…")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
case "loaded":
|
||||
return <>{children(groupCallState.groupCall)}</>;
|
||||
case "failed":
|
||||
return <ErrorView error={groupCallState.error} />;
|
||||
usePageTitle(groupCall ? groupCall.room.name : t("Loading…"));
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("Loading…")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return <>{children(groupCall)}</>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
@@ -49,6 +49,7 @@ interface Props {
|
||||
isEmbedded: boolean;
|
||||
preload: boolean;
|
||||
hideHeader: boolean;
|
||||
roomIdOrAlias: string;
|
||||
groupCall: GroupCall;
|
||||
}
|
||||
|
||||
@@ -58,6 +59,7 @@ export function GroupCallView({
|
||||
isEmbedded,
|
||||
preload,
|
||||
hideHeader,
|
||||
roomIdOrAlias,
|
||||
groupCall,
|
||||
}: Props) {
|
||||
const {
|
||||
@@ -80,14 +82,13 @@ export function GroupCallView({
|
||||
}, [groupCall]);
|
||||
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const matrixInfo = useMemo((): MatrixInfo => {
|
||||
return {
|
||||
displayName: displayName!,
|
||||
avatarUrl: avatarUrl!,
|
||||
roomId: groupCall.room.roomId,
|
||||
roomName: groupCall.room.name,
|
||||
};
|
||||
}, [displayName, avatarUrl, groupCall]);
|
||||
|
||||
const matrixInfo: MatrixInfo = {
|
||||
displayName,
|
||||
avatarUrl,
|
||||
roomName: groupCall.room.name,
|
||||
roomIdOrAlias,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (widget && preload) {
|
||||
@@ -138,14 +139,14 @@ export function GroupCallView({
|
||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
||||
|
||||
await Promise.all([
|
||||
widget!.api.setAlwaysOnScreen(true),
|
||||
widget!.api.transport.reply(ev.detail, {}),
|
||||
widget.api.setAlwaysOnScreen(true),
|
||||
widget.api.transport.reply(ev.detail, {}),
|
||||
]);
|
||||
};
|
||||
|
||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||
return () => {
|
||||
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
};
|
||||
}
|
||||
}, [groupCall, preload, enter]);
|
||||
@@ -204,12 +205,12 @@ export function GroupCallView({
|
||||
if (widget && state === GroupCallState.Entered) {
|
||||
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
leave();
|
||||
await widget!.api.transport.reply(ev.detail, {});
|
||||
widget!.api.setAlwaysOnScreen(false);
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
widget.api.setAlwaysOnScreen(false);
|
||||
};
|
||||
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
||||
return () => {
|
||||
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||
};
|
||||
}
|
||||
}, [groupCall, state, leave]);
|
||||
@@ -234,6 +235,7 @@ export function GroupCallView({
|
||||
onLeave={onLeave}
|
||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
||||
hideHeader={hideHeader}
|
||||
matrixInfo={matrixInfo}
|
||||
userChoices={userChoices}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
/>
|
||||
|
||||
@@ -68,15 +68,17 @@ import { ElementWidgetActions, widget } from "../widget";
|
||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||
import { GroupCallInspector } from "./GroupCallInspector";
|
||||
import styles from "./InCallView.module.css";
|
||||
import { MatrixInfo } from "./VideoPreview";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { ParticipantInfo } from "./useGroupCall";
|
||||
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
|
||||
import { ItemData, TileContent } from "../video-grid/VideoTile";
|
||||
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
|
||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||
import { SettingsModal } from "../settings/SettingsModal";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||
import { VideoTile } from "../video-grid/VideoTile";
|
||||
import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
|
||||
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
|
||||
import { useFullscreen } from "./useFullscreen";
|
||||
@@ -98,14 +100,12 @@ export function ActiveCall(props: ActiveCallProps) {
|
||||
const sfuConfig = useSFUConfig();
|
||||
const livekitRoom = useLiveKit(props.userChoices, sfuConfig);
|
||||
|
||||
if (!livekitRoom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={livekitRoom}>
|
||||
<InCallView {...props} livekitRoom={livekitRoom} />
|
||||
</RoomContext.Provider>
|
||||
livekitRoom && (
|
||||
<RoomContext.Provider value={livekitRoom}>
|
||||
<InCallView {...props} livekitRoom={livekitRoom} />
|
||||
</RoomContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +117,8 @@ export interface InCallViewProps {
|
||||
onLeave: () => void;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
hideHeader: boolean;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
matrixInfo: MatrixInfo;
|
||||
otelGroupCallMembership: OTelGroupCallMembership;
|
||||
}
|
||||
|
||||
export function InCallView({
|
||||
@@ -128,6 +129,7 @@ export function InCallView({
|
||||
onLeave,
|
||||
unencryptedEventsFromUsers,
|
||||
hideHeader,
|
||||
matrixInfo,
|
||||
otelGroupCallMembership,
|
||||
}: InCallViewProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -201,11 +203,11 @@ export function InCallView({
|
||||
if (widget) {
|
||||
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
setLayout("freedom");
|
||||
await widget!.api.transport.reply(ev.detail, {});
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
};
|
||||
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
setLayout("spotlight");
|
||||
await widget!.api.transport.reply(ev.detail, {});
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
};
|
||||
|
||||
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
|
||||
@@ -215,8 +217,8 @@ export function InCallView({
|
||||
);
|
||||
|
||||
return () => {
|
||||
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||
widget!.lazyActions.off(
|
||||
widget.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||
widget.lazyActions.off(
|
||||
ElementWidgetActions.SpotlightLayout,
|
||||
onSpotlightLayout
|
||||
);
|
||||
@@ -394,7 +396,7 @@ export function InCallView({
|
||||
{!hideHeader && maximisedParticipant === null && (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={groupCall.room.name} />
|
||||
<RoomHeaderInfo roomName={matrixInfo.roomName} />
|
||||
<VersionMismatchWarning
|
||||
users={unencryptedEventsFromUsers}
|
||||
room={groupCall.room}
|
||||
@@ -414,18 +416,16 @@ export function InCallView({
|
||||
{renderContent()}
|
||||
{footer}
|
||||
</div>
|
||||
{otelGroupCallMembership && (
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
show={showInspector}
|
||||
/>
|
||||
)}
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
show={showInspector}
|
||||
/>
|
||||
{rageshakeRequestModalState.isOpen && !noControls && (
|
||||
<RageshakeRequestModal
|
||||
{...rageshakeRequestModalProps}
|
||||
roomId={groupCall.room.roomId}
|
||||
roomIdOrAlias={matrixInfo.roomIdOrAlias}
|
||||
/>
|
||||
)}
|
||||
{settingsModalState.isOpen && (
|
||||
@@ -437,7 +437,10 @@ export function InCallView({
|
||||
/>
|
||||
)}
|
||||
{inviteModalState.isOpen && (
|
||||
<InviteModal roomId={groupCall.room.roomId} {...inviteModalProps} />
|
||||
<InviteModal
|
||||
roomIdOrAlias={matrixInfo.roomIdOrAlias}
|
||||
{...inviteModalProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,10 +23,10 @@ import { getRoomUrl } from "../matrix-utils";
|
||||
import styles from "./InviteModal.module.css";
|
||||
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
}
|
||||
|
||||
export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
|
||||
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -40,7 +40,7 @@ export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
|
||||
<p>{t("Copy and share this call link")}</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getRoomUrl(roomId)}
|
||||
value={getRoomUrl(roomIdOrAlias)}
|
||||
data-testid="modal_inviteLink"
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function LobbyView(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
useLocationNavigation();
|
||||
|
||||
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const joinCallButtonRef = useRef<HTMLButtonElement>();
|
||||
useEffect(() => {
|
||||
if (joinCallButtonRef.current) {
|
||||
joinCallButtonRef.current.focus();
|
||||
@@ -81,7 +81,7 @@ export function LobbyView(props: Props) {
|
||||
<Body>Or</Body>
|
||||
<CopyButton
|
||||
variant="secondaryCopy"
|
||||
value={getRoomUrl(props.matrixInfo.roomId)}
|
||||
value={getRoomUrl(props.matrixInfo.roomName)}
|
||||
className={styles.copyButton}
|
||||
copiedMessage={t("Call link copied")}
|
||||
data-testid="lobby_inviteLink"
|
||||
|
||||
@@ -25,13 +25,13 @@ import { Body } from "../typography/Typography";
|
||||
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
rageshakeRequestId: string;
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const RageshakeRequestModal: FC<Props> = ({
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
...rest
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -57,7 +57,7 @@ export const RageshakeRequestModal: FC<Props> = ({
|
||||
submitRageshake({
|
||||
sendLogs: true,
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
roomId: roomIdOrAlias, // Possibly not a room ID, but oh well
|
||||
})
|
||||
}
|
||||
disabled={sending}
|
||||
|
||||
@@ -36,8 +36,6 @@ export function RoomAuthView() {
|
||||
useRegisterPasswordlessUser();
|
||||
|
||||
const onSubmit = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { FC, useEffect, useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { useClientLegacy } from "../ClientContext";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { RoomAuthView } from "./RoomAuthView";
|
||||
import { GroupCallLoader } from "./GroupCallLoader";
|
||||
@@ -30,6 +30,8 @@ import { useOptInAnalytics } from "../settings/useSetting";
|
||||
|
||||
export const RoomPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||
useClient();
|
||||
|
||||
const {
|
||||
roomAlias,
|
||||
@@ -50,41 +52,39 @@ export const RoomPage: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
// During the beta, opt into analytics by default
|
||||
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
|
||||
if (optInAnalytics === null) setOptInAnalytics(true);
|
||||
}, [optInAnalytics, setOptInAnalytics]);
|
||||
|
||||
const { loading, authenticated, client, error, passwordlessUser } =
|
||||
useClientLegacy();
|
||||
|
||||
useEffect(() => {
|
||||
// If we've finished loading, are not already authed and we've been given a display name as
|
||||
// a URL param, automatically register a passwordless user
|
||||
if (!loading && !authenticated && displayName) {
|
||||
if (!loading && !isAuthenticated && displayName) {
|
||||
setIsRegistering(true);
|
||||
registerPasswordlessUser(displayName).finally(() => {
|
||||
setIsRegistering(false);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
authenticated,
|
||||
isAuthenticated,
|
||||
displayName,
|
||||
setIsRegistering,
|
||||
registerPasswordlessUser,
|
||||
loading,
|
||||
]);
|
||||
|
||||
const groupCallView = useCallback(
|
||||
(groupCall: GroupCall) => (
|
||||
<GroupCallView
|
||||
client={client!}
|
||||
client={client}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
groupCall={groupCall}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
isEmbedded={isEmbedded}
|
||||
preload={preload}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
),
|
||||
[client, passwordlessUser, isEmbedded, preload, hideHeader]
|
||||
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader]
|
||||
);
|
||||
|
||||
if (loading || isRegistering) {
|
||||
@@ -95,7 +95,7 @@ export const RoomPage: FC = () => {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
if (!isAuthenticated) {
|
||||
return <RoomAuthView />;
|
||||
}
|
||||
|
||||
|
||||
44
src/room/RoomRedirect.tsx
Normal file
44
src/room/RoomRedirect.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useLocation, useHistory } from "react-router-dom";
|
||||
|
||||
import { Config } from "../config/Config";
|
||||
import { LoadingView } from "../FullScreenView";
|
||||
|
||||
// A component that, when loaded, redirects the client to a full room URL
|
||||
// based on the current URL being an abbreviated room URL
|
||||
export function RoomRedirect() {
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
let roomId = pathname;
|
||||
|
||||
if (pathname.startsWith("/")) {
|
||||
roomId = roomId.substring(1, roomId.length);
|
||||
}
|
||||
|
||||
if (!roomId.startsWith("#") && !roomId.startsWith("!")) {
|
||||
roomId = `#${roomId}:${Config.defaultServerName()}`;
|
||||
}
|
||||
|
||||
history.replace(`/room/${roomId.toLowerCase()}`);
|
||||
}, [pathname, history]);
|
||||
|
||||
return <LoadingView />;
|
||||
}
|
||||
@@ -34,8 +34,8 @@ import { useDefaultDevices } from "../settings/useSetting";
|
||||
export type MatrixInfo = {
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
roomIdOrAlias: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@@ -66,6 +66,11 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
|
||||
const [videoEnabled, setVideoEnabled] = useState<boolean>(true);
|
||||
const [audioEnabled, setAudioEnabled] = useState<boolean>(true);
|
||||
|
||||
// we store if the tracks are currently initializing to not show them as muted.
|
||||
// showing them as muted while they are not yet available makes the buttons flicker undesirable during startup.
|
||||
const [initializingVideo, setInitializingVideo] = useState<boolean>(true);
|
||||
const [initializingAudio, setInitializingAudio] = useState<boolean>(true);
|
||||
|
||||
// The settings are updated as soon as the device changes. We wrap the settings value in a ref to store their initial value.
|
||||
// Not changing the device options prohibits the usePreviewTracks hook to recreate the tracks.
|
||||
const initialDefaultDevices = useRef(useDefaultDevices()[0]);
|
||||
@@ -94,23 +99,32 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
|
||||
const requestPermissions = !!audioTrack && !!videoTrack;
|
||||
const mediaSwitcher = useMediaDevicesSwitcher(
|
||||
undefined,
|
||||
{ videoTrack, audioTrack },
|
||||
{
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
},
|
||||
requestPermissions
|
||||
);
|
||||
const { videoIn, audioIn } = mediaSwitcher;
|
||||
|
||||
const videoEl = React.useRef(null);
|
||||
|
||||
// pretend the video is available until the initialization is over
|
||||
const videoAvailableAndEnabled =
|
||||
videoEnabled && (!!videoTrack || initializingVideo);
|
||||
const audioAvailableAndEnabled =
|
||||
audioEnabled && (!!videoTrack || initializingAudio);
|
||||
|
||||
useEffect(() => {
|
||||
// Effect to update the settings
|
||||
onUserChoicesChanged({
|
||||
video: {
|
||||
selectedId: videoIn.selectedId,
|
||||
enabled: videoEnabled,
|
||||
enabled: videoAvailableAndEnabled,
|
||||
},
|
||||
audio: {
|
||||
selectedId: audioIn.selectedId,
|
||||
enabled: audioEnabled,
|
||||
enabled: audioAvailableAndEnabled,
|
||||
},
|
||||
});
|
||||
}, [
|
||||
@@ -119,18 +133,24 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
|
||||
videoEnabled,
|
||||
audioIn.selectedId,
|
||||
audioEnabled,
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoAvailableAndEnabled,
|
||||
audioAvailableAndEnabled,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// Effect to update the initial device selection for the ui elements based on the current preview track.
|
||||
if (!videoIn.selectedId || videoIn.selectedId == "") {
|
||||
if (videoTrack) {
|
||||
setInitializingVideo(false);
|
||||
}
|
||||
videoTrack?.getDeviceId().then((videoId) => {
|
||||
videoIn.setSelected(videoId ?? "default");
|
||||
});
|
||||
}
|
||||
if (!audioIn.selectedId || audioIn.selectedId == "") {
|
||||
if (audioTrack) {
|
||||
setInitializingAudio(false);
|
||||
}
|
||||
audioTrack?.getDeviceId().then((audioId) => {
|
||||
// getDeviceId() can return undefined for audio devices. This happens if
|
||||
// the devices list uses "default" as the device id for the current
|
||||
@@ -177,19 +197,19 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
|
||||
)}
|
||||
<div className={styles.previewButtons}>
|
||||
<MicButton
|
||||
muted={!audioEnabled}
|
||||
onPress={() => setAudioEnabled(!audioEnabled)}
|
||||
muted={!audioAvailableAndEnabled}
|
||||
onPress={() => setAudioEnabled(!audioAvailableAndEnabled)}
|
||||
disabled={!audioTrack}
|
||||
/>
|
||||
<VideoButton
|
||||
muted={!videoEnabled}
|
||||
onPress={() => setVideoEnabled(!videoEnabled)}
|
||||
muted={!videoAvailableAndEnabled}
|
||||
onPress={() => setVideoEnabled(!videoAvailableAndEnabled)}
|
||||
disabled={!videoTrack}
|
||||
/>
|
||||
<SettingsButton onPress={openSettings} />
|
||||
</div>
|
||||
</>
|
||||
{settingsModalState.isOpen && client && (
|
||||
{settingsModalState.isOpen && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
mediaDevicesSwitcher={mediaSwitcher}
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useReducer, useState } from "react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import {
|
||||
GroupCallEvent,
|
||||
GroupCallState,
|
||||
@@ -59,12 +58,12 @@ export interface ParticipantInfo {
|
||||
|
||||
interface UseGroupCallReturnType {
|
||||
state: GroupCallState;
|
||||
localCallFeed?: CallFeed;
|
||||
activeSpeaker?: CallFeed;
|
||||
localCallFeed: CallFeed;
|
||||
activeSpeaker: CallFeed | null;
|
||||
userMediaFeeds: CallFeed[];
|
||||
microphoneMuted: boolean;
|
||||
localVideoMuted: boolean;
|
||||
error?: TranslatedError;
|
||||
error: TranslatedError | null;
|
||||
initLocalCallFeed: () => void;
|
||||
enter: () => Promise<void>;
|
||||
leave: () => void;
|
||||
@@ -75,21 +74,23 @@ interface UseGroupCallReturnType {
|
||||
requestingScreenshare: boolean;
|
||||
isScreensharing: boolean;
|
||||
screenshareFeeds: CallFeed[];
|
||||
localDesktopCapturerSourceId: string; // XXX: This looks unused?
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
hasLocalParticipant: boolean;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
otelGroupCallMembership: OTelGroupCallMembership;
|
||||
}
|
||||
|
||||
interface State {
|
||||
state: GroupCallState;
|
||||
localCallFeed?: CallFeed;
|
||||
activeSpeaker?: CallFeed;
|
||||
localCallFeed: CallFeed;
|
||||
activeSpeaker: CallFeed | null;
|
||||
userMediaFeeds: CallFeed[];
|
||||
error?: TranslatedError;
|
||||
error: TranslatedError | null;
|
||||
microphoneMuted: boolean;
|
||||
localVideoMuted: boolean;
|
||||
screenshareFeeds: CallFeed[];
|
||||
localDesktopCapturerSourceId: string;
|
||||
isScreensharing: boolean;
|
||||
requestingScreenshare: boolean;
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
@@ -100,7 +101,7 @@ interface State {
|
||||
// level so that it doesn't pop in & out of existence as react mounts & unmounts
|
||||
// components. The right solution is probably for this to live in the js-sdk and have
|
||||
// the same lifetime as groupcalls themselves.
|
||||
let groupCallOTelMembership: OTelGroupCallMembership | undefined;
|
||||
let groupCallOTelMembership: OTelGroupCallMembership;
|
||||
let groupCallOTelMembershipGroupCallId: string;
|
||||
|
||||
function getParticipants(
|
||||
@@ -158,6 +159,7 @@ export function useGroupCall(
|
||||
localVideoMuted,
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
localDesktopCapturerSourceId,
|
||||
participants,
|
||||
hasLocalParticipant,
|
||||
requestingScreenshare,
|
||||
@@ -165,11 +167,15 @@ export function useGroupCall(
|
||||
setState,
|
||||
] = useState<State>({
|
||||
state: GroupCallState.LocalCallFeedUninitialized,
|
||||
localCallFeed: null,
|
||||
activeSpeaker: null,
|
||||
userMediaFeeds: [],
|
||||
error: null,
|
||||
microphoneMuted: false,
|
||||
localVideoMuted: false,
|
||||
isScreensharing: false,
|
||||
screenshareFeeds: [],
|
||||
localDesktopCapturerSourceId: null,
|
||||
requestingScreenshare: false,
|
||||
participants: new Map(),
|
||||
hasLocalParticipant: false,
|
||||
@@ -242,11 +248,12 @@ export function useGroupCall(
|
||||
updateState({
|
||||
state: groupCall.state,
|
||||
localCallFeed: groupCall.localCallFeed,
|
||||
activeSpeaker: groupCall.activeSpeaker,
|
||||
activeSpeaker: groupCall.activeSpeaker ?? null,
|
||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||
isScreensharing: groupCall.isScreensharing(),
|
||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||
});
|
||||
}
|
||||
@@ -296,7 +303,7 @@ export function useGroupCall(
|
||||
|
||||
function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void {
|
||||
updateState({
|
||||
activeSpeaker: activeSpeaker,
|
||||
activeSpeaker: activeSpeaker ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,11 +319,12 @@ export function useGroupCall(
|
||||
|
||||
function onLocalScreenshareStateChanged(
|
||||
isScreensharing: boolean,
|
||||
_localScreenshareFeed?: CallFeed,
|
||||
localDesktopCapturerSourceId?: string
|
||||
_localScreenshareFeed: CallFeed,
|
||||
localDesktopCapturerSourceId: string
|
||||
): void {
|
||||
updateState({
|
||||
isScreensharing,
|
||||
localDesktopCapturerSourceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,7 +340,6 @@ export function useGroupCall(
|
||||
}
|
||||
|
||||
function onError(e: GroupCallError): void {
|
||||
Sentry.captureException(e);
|
||||
if (e.code === GroupCallErrorCode.UnknownDevice) {
|
||||
const unknownDeviceError = e as GroupCallUnknownDeviceError;
|
||||
addUnencryptedEventUser(unknownDeviceError.userId);
|
||||
@@ -398,14 +405,15 @@ export function useGroupCall(
|
||||
);
|
||||
|
||||
updateState({
|
||||
error: undefined,
|
||||
error: null,
|
||||
state: groupCall.state,
|
||||
localCallFeed: groupCall.localCallFeed,
|
||||
activeSpeaker: groupCall.activeSpeaker,
|
||||
activeSpeaker: groupCall.activeSpeaker ?? null,
|
||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||
isScreensharing: groupCall.isScreensharing(),
|
||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||
participants: getParticipants(groupCall),
|
||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||
@@ -508,7 +516,7 @@ export function useGroupCall(
|
||||
}, [groupCall]);
|
||||
|
||||
const setMicrophoneMuted = useCallback(
|
||||
(setMuted: boolean) => {
|
||||
(setMuted) => {
|
||||
groupCall.setMicrophoneMuted(setMuted);
|
||||
groupCallOTelMembership?.onSetMicrophoneMuted(setMuted);
|
||||
PosthogAnalytics.instance.eventMuteMicrophone.track(
|
||||
@@ -567,7 +575,7 @@ export function useGroupCall(
|
||||
desktopCapturerSourceId: data.desktopCapturerSourceId as string,
|
||||
audio: !data.desktopCapturerSourceId,
|
||||
});
|
||||
await widget?.api.transport.reply(ev.detail, {});
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
},
|
||||
[groupCall, updateState]
|
||||
);
|
||||
@@ -576,7 +584,7 @@ export function useGroupCall(
|
||||
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
updateState({ requestingScreenshare: false });
|
||||
await groupCall.setScreensharingEnabled(false);
|
||||
await widget?.api.transport.reply(ev.detail, {});
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
},
|
||||
[groupCall, updateState]
|
||||
);
|
||||
@@ -593,11 +601,11 @@ export function useGroupCall(
|
||||
);
|
||||
|
||||
return () => {
|
||||
widget?.lazyActions.off(
|
||||
widget.lazyActions.off(
|
||||
ElementWidgetActions.ScreenshareStart,
|
||||
onScreenshareStart
|
||||
);
|
||||
widget?.lazyActions.off(
|
||||
widget.lazyActions.off(
|
||||
ElementWidgetActions.ScreenshareStop,
|
||||
onScreenshareStop
|
||||
);
|
||||
@@ -636,6 +644,7 @@ export function useGroupCall(
|
||||
requestingScreenshare,
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
localDesktopCapturerSourceId,
|
||||
participants,
|
||||
hasLocalParticipant,
|
||||
unencryptedEventsFromUsers,
|
||||
|
||||
@@ -34,26 +34,8 @@ import { widget } from "../widget";
|
||||
|
||||
const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
|
||||
|
||||
export type GroupCallLoaded = {
|
||||
kind: "loaded";
|
||||
groupCall: GroupCall;
|
||||
};
|
||||
|
||||
export type GroupCallLoadFailed = {
|
||||
kind: "failed";
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export type GroupCallLoading = {
|
||||
kind: "loading";
|
||||
};
|
||||
|
||||
export type GroupCallStatus =
|
||||
| GroupCallLoaded
|
||||
| GroupCallLoadFailed
|
||||
| GroupCallLoading;
|
||||
|
||||
export interface GroupCallLoadState {
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
groupCall?: GroupCall;
|
||||
}
|
||||
@@ -63,11 +45,13 @@ export const useLoadGroupCall = (
|
||||
roomIdOrAlias: string,
|
||||
viaServers: string[],
|
||||
createPtt: boolean
|
||||
): GroupCallStatus => {
|
||||
): GroupCallLoadState => {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||
const [state, setState] = useState<GroupCallLoadState>({ loading: true });
|
||||
|
||||
useEffect(() => {
|
||||
setState({ loading: true });
|
||||
|
||||
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||
try {
|
||||
// We lowercase the localpart when we create the room, so we must lowercase
|
||||
@@ -90,14 +74,8 @@ export const useLoadGroupCall = (
|
||||
} catch (error) {
|
||||
if (
|
||||
isLocalRoomId(roomIdOrAlias, client) &&
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(error.errcode === "M_NOT_FOUND" ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(error.message &&
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
error.message.indexOf("Failed to fetch alias") !== -1))
|
||||
) {
|
||||
// The room doesn't exist, but we can create it
|
||||
@@ -108,7 +86,7 @@ export const useLoadGroupCall = (
|
||||
);
|
||||
// likewise, wait for the room
|
||||
await client.waitUntilRoomReadyForGroupCalls(roomId);
|
||||
return client.getRoom(roomId)!;
|
||||
return client.getRoom(roomId);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -192,8 +170,12 @@ export const useLoadGroupCall = (
|
||||
|
||||
waitForClientSyncing()
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((groupCall) => setState({ kind: "loaded", groupCall }))
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
.then((groupCall) =>
|
||||
setState((prevState) => ({ ...prevState, loading: false, groupCall }))
|
||||
)
|
||||
.catch((error) =>
|
||||
setState((prevState) => ({ ...prevState, loading: false, error }))
|
||||
);
|
||||
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
|
||||
|
||||
return state;
|
||||
|
||||
@@ -55,22 +55,14 @@ export function usePageUnload(callback: () => void) {
|
||||
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
|
||||
if (isIOS()) {
|
||||
window.addEventListener("pagehide", onBeforeUnload);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
document.addEventListener("visibilitychange", onBeforeUnload);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.addEventListener("beforeunload", onBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pagehide", onBeforeUnload);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
document.removeEventListener("visibilitychange", onBeforeUnload);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.removeEventListener("beforeunload", onBeforeUnload);
|
||||
clearTimeout(pageVisibilityTimeout);
|
||||
};
|
||||
|
||||
@@ -35,8 +35,6 @@ export function FeedbackSettingsTab({ roomId }: Props) {
|
||||
const sendRageshakeRequest = useRageshakeRequest();
|
||||
|
||||
const onSubmitFeedback = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
|
||||
@@ -59,14 +59,8 @@ export function ProfileSettingsTab({ client }: Props) {
|
||||
? displayNameDataEntry
|
||||
: displayNameDataEntry?.name ?? null;
|
||||
|
||||
if (!displayName) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveProfile({
|
||||
displayName,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
avatar: avatar && avatarSize > 0 ? avatar : undefined,
|
||||
removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0),
|
||||
});
|
||||
@@ -77,16 +71,14 @@ export function ProfileSettingsTab({ client }: Props) {
|
||||
return (
|
||||
<form onChange={onFormChange} ref={formRef} className={styles.content}>
|
||||
<FieldRow className={styles.avatarFieldRow}>
|
||||
{displayName && (
|
||||
<AvatarInputField
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
label={t("Avatar")}
|
||||
avatarUrl={avatarUrl}
|
||||
displayName={displayName}
|
||||
onRemoveAvatar={onRemoveAvatar}
|
||||
/>
|
||||
)}
|
||||
<AvatarInputField
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
label={t("Avatar")}
|
||||
avatarUrl={avatarUrl}
|
||||
displayName={displayName}
|
||||
onRemoveAvatar={onRemoveAvatar}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ChangeEvent, Key, useCallback, useState } from "react";
|
||||
import { ChangeEvent, useCallback, useState } from "react";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { MatrixClient } from "matrix-js-sdk";
|
||||
@@ -99,8 +99,8 @@ export const SettingsModal = (props: Props) => {
|
||||
const [selectedTab, setSelectedTab] = useState<string | undefined>();
|
||||
|
||||
const onSelectedTabChanged = useCallback(
|
||||
(tab: Key) => {
|
||||
setSelectedTab(tab.toString());
|
||||
(tab) => {
|
||||
setSelectedTab(tab);
|
||||
},
|
||||
[setSelectedTab]
|
||||
);
|
||||
@@ -118,155 +118,6 @@ export const SettingsModal = (props: Props) => {
|
||||
|
||||
const devices = props.mediaDevicesSwitcher;
|
||||
|
||||
const audioTab = (
|
||||
<TabItem
|
||||
key="audio"
|
||||
title={
|
||||
<>
|
||||
<AudioIcon width={16} height={16} />
|
||||
<span className={styles.tabLabel}>{t("Audio")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{devices && generateDeviceSelection(devices.audioIn, t("Microphone"))}
|
||||
{devices && generateDeviceSelection(devices.audioOut, t("Speaker"))}
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
const videoTab = (
|
||||
<TabItem
|
||||
key="video"
|
||||
title={
|
||||
<>
|
||||
<VideoIcon width={16} height={16} />
|
||||
<span>{t("Video")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{devices && generateDeviceSelection(devices.videoIn, t("Camera"))}
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
const profileTab = (
|
||||
<TabItem
|
||||
key="profile"
|
||||
title={
|
||||
<>
|
||||
<UserIcon width={15} height={15} />
|
||||
<span>{t("Profile")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ProfileSettingsTab client={props.client} />
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
const feedbackTab = (
|
||||
<TabItem
|
||||
key="feedback"
|
||||
title={
|
||||
<>
|
||||
<FeedbackIcon width={16} height={16} />
|
||||
<span>{t("Feedback")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FeedbackSettingsTab roomId={props.roomId} />
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
const moreTab = (
|
||||
<TabItem
|
||||
key="more"
|
||||
title={
|
||||
<>
|
||||
<OverflowIcon width={16} height={16} />
|
||||
<span>{t("More")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<h4>Developer</h4>
|
||||
<p>Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"}</p>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="developerSettingsTab"
|
||||
type="checkbox"
|
||||
checked={developerSettingsTab}
|
||||
label={t("Developer Settings")}
|
||||
description={t("Expose developer settings in the settings window.")}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
setDeveloperSettingsTab(event.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<h4>Analytics</h4>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="optInAnalytics"
|
||||
type="checkbox"
|
||||
checked={optInAnalytics ?? undefined}
|
||||
description={optInDescription}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setOptInAnalytics?.(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
const developerTab = (
|
||||
<TabItem
|
||||
key="developer"
|
||||
title={
|
||||
<>
|
||||
<DeveloperIcon width={16} height={16} />
|
||||
<span>{t("Developer")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FieldRow>
|
||||
<Body className={styles.fieldRowText}>
|
||||
{t("Version: {{version}}", {
|
||||
version: import.meta.env.VITE_APP_VERSION || "dev",
|
||||
})}
|
||||
</Body>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showInspector"
|
||||
name="inspector"
|
||||
label={t("Show call inspector")}
|
||||
type="checkbox"
|
||||
checked={showInspector}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setShowInspector(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showConnectionStats"
|
||||
name="connection-stats"
|
||||
label={t("Show connection stats")}
|
||||
type="checkbox"
|
||||
checked={showConnectionStats}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setShowConnectionStats(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
const tabs: JSX.Element[] = [];
|
||||
tabs.push(audioTab, videoTab);
|
||||
if (!isEmbedded) tabs.push(profileTab);
|
||||
tabs.push(feedbackTab, moreTab);
|
||||
if (developerSettingsTab) tabs.push(developerTab);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("Settings")}
|
||||
@@ -280,7 +131,141 @@ export const SettingsModal = (props: Props) => {
|
||||
selectedKey={selectedTab ?? props.defaultTab ?? "audio"}
|
||||
className={styles.tabContainer}
|
||||
>
|
||||
{tabs}
|
||||
<TabItem
|
||||
key="audio"
|
||||
title={
|
||||
<>
|
||||
<AudioIcon width={16} height={16} />
|
||||
<span className={styles.tabLabel}>{t("Audio")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{devices && generateDeviceSelection(devices.audioIn, t("Microphone"))}
|
||||
{devices && generateDeviceSelection(devices.audioOut, t("Speaker"))}
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="video"
|
||||
title={
|
||||
<>
|
||||
<VideoIcon width={16} height={16} />
|
||||
<span>{t("Video")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{devices && generateDeviceSelection(devices.videoIn, t("Camera"))}
|
||||
</TabItem>
|
||||
{!isEmbedded && (
|
||||
<TabItem
|
||||
key="profile"
|
||||
title={
|
||||
<>
|
||||
<UserIcon width={15} height={15} />
|
||||
<span>{t("Profile")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ProfileSettingsTab client={props.client} />
|
||||
</TabItem>
|
||||
)}
|
||||
<TabItem
|
||||
key="feedback"
|
||||
title={
|
||||
<>
|
||||
<FeedbackIcon width={16} height={16} />
|
||||
<span>{t("Feedback")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FeedbackSettingsTab roomId={props.roomId} />
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="more"
|
||||
title={
|
||||
<>
|
||||
<OverflowIcon width={16} height={16} />
|
||||
<span>{t("More")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<h4>Developer</h4>
|
||||
<p>
|
||||
Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"}
|
||||
</p>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="developerSettingsTab"
|
||||
type="checkbox"
|
||||
checked={developerSettingsTab}
|
||||
label={t("Developer Settings")}
|
||||
description={t(
|
||||
"Expose developer settings in the settings window."
|
||||
)}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
setDeveloperSettingsTab(event.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<h4>Analytics</h4>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="optInAnalytics"
|
||||
type="checkbox"
|
||||
checked={optInAnalytics}
|
||||
description={optInDescription}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
setOptInAnalytics(event.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
{developerSettingsTab && (
|
||||
<TabItem
|
||||
key="developer"
|
||||
title={
|
||||
<>
|
||||
<DeveloperIcon width={16} height={16} />
|
||||
<span>{t("Developer")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FieldRow>
|
||||
<Body className={styles.fieldRowText}>
|
||||
{t("Version: {{version}}", {
|
||||
version: import.meta.env.VITE_APP_VERSION || "dev",
|
||||
})}
|
||||
</Body>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showInspector"
|
||||
name="inspector"
|
||||
label={t("Show call inspector")}
|
||||
type="checkbox"
|
||||
checked={showInspector}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setShowInspector(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showConnectionStats"
|
||||
name="connection-stats"
|
||||
label={t("Show connection stats")}
|
||||
type="checkbox"
|
||||
checked={showConnectionStats}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setShowConnectionStats(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Button onPress={downloadDebugLog}>
|
||||
{t("Download debug logs")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
)}
|
||||
</TabContainer>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -79,17 +79,11 @@ class ConsoleLogger extends EventEmitter {
|
||||
warn: "W",
|
||||
error: "E",
|
||||
};
|
||||
|
||||
Object.entries(consoleFunctionsToLevels).forEach(([name, level]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const originalFn = consoleObj[name].bind(consoleObj);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.originalFunctions[name] = originalFn;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
consoleObj[name] = (...args) => {
|
||||
Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
|
||||
const level = consoleFunctionsToLevels[fnName];
|
||||
const originalFn = consoleObj[fnName].bind(consoleObj);
|
||||
this.originalFunctions[fnName] = originalFn;
|
||||
consoleObj[fnName] = (...args) => {
|
||||
this.log(level, ...args);
|
||||
originalFn(...args);
|
||||
};
|
||||
@@ -153,9 +147,9 @@ class ConsoleLogger extends EventEmitter {
|
||||
// A class which stores log lines in an IndexedDB instance.
|
||||
class IndexedDBLogStore {
|
||||
private index = 0;
|
||||
private db?: IDBDatabase;
|
||||
private flushPromise?: Promise<void>;
|
||||
private flushAgainPromise?: Promise<void>;
|
||||
private db: IDBDatabase = null;
|
||||
private flushPromise: Promise<void> = null;
|
||||
private flushAgainPromise: Promise<void> = null;
|
||||
private id: string;
|
||||
|
||||
constructor(
|
||||
@@ -181,7 +175,7 @@ class IndexedDBLogStore {
|
||||
};
|
||||
|
||||
req.onerror = () => {
|
||||
const err = "Failed to open log database: " + req?.error?.name;
|
||||
const err = "Failed to open log database: " + req.error.name;
|
||||
logger.error(err);
|
||||
reject(new Error(err));
|
||||
};
|
||||
@@ -270,7 +264,7 @@ class IndexedDBLogStore {
|
||||
return this.flush();
|
||||
})
|
||||
.then(() => {
|
||||
this.flushAgainPromise = undefined;
|
||||
this.flushAgainPromise = null;
|
||||
});
|
||||
return this.flushAgainPromise;
|
||||
}
|
||||
@@ -294,13 +288,13 @@ class IndexedDBLogStore {
|
||||
};
|
||||
txn.onerror = (event) => {
|
||||
logger.error("Failed to flush logs : ", event);
|
||||
reject(new Error("Failed to write logs: " + txn?.error?.message));
|
||||
reject(new Error("Failed to write logs: " + txn.error.message));
|
||||
};
|
||||
objStore.add(this.generateLogEntry(lines));
|
||||
const lastModStore = txn.objectStore("logslastmod");
|
||||
lastModStore.put(this.generateLastModifiedTime());
|
||||
}).then(() => {
|
||||
this.flushPromise = undefined;
|
||||
this.flushPromise = null;
|
||||
});
|
||||
return this.flushPromise;
|
||||
};
|
||||
@@ -317,14 +311,11 @@ class IndexedDBLogStore {
|
||||
*/
|
||||
public async consume(): Promise<LogEntry[]> {
|
||||
const db = this.db;
|
||||
if (!db) {
|
||||
return Promise.reject(new Error("No connected database"));
|
||||
}
|
||||
|
||||
// Returns: a string representing the concatenated logs for this ID.
|
||||
// Stops adding log fragments when the size exceeds maxSize
|
||||
function fetchLogs(id: string, maxSize: number): Promise<string> {
|
||||
const objectStore = db!
|
||||
const objectStore = db
|
||||
.transaction("logs", "readonly")
|
||||
.objectStore("logs");
|
||||
|
||||
@@ -334,7 +325,7 @@ class IndexedDBLogStore {
|
||||
.openCursor(IDBKeyRange.only(id), "prev");
|
||||
let lines = "";
|
||||
query.onerror = () => {
|
||||
reject(new Error("Query failed: " + query?.error?.message));
|
||||
reject(new Error("Query failed: " + query.error.message));
|
||||
};
|
||||
query.onsuccess = () => {
|
||||
const cursor = query.result;
|
||||
@@ -355,7 +346,7 @@ class IndexedDBLogStore {
|
||||
// Returns: A sorted array of log IDs. (newest first)
|
||||
function fetchLogIds(): Promise<string[]> {
|
||||
// To gather all the log IDs, query for all records in logslastmod.
|
||||
const o = db!
|
||||
const o = db
|
||||
.transaction("logslastmod", "readonly")
|
||||
.objectStore("logslastmod");
|
||||
return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => {
|
||||
@@ -375,7 +366,7 @@ class IndexedDBLogStore {
|
||||
|
||||
function deleteLogs(id: number): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const txn = db!.transaction(["logs", "logslastmod"], "readwrite");
|
||||
const txn = db.transaction(["logs", "logslastmod"], "readwrite");
|
||||
const o = txn.objectStore("logs");
|
||||
// only load the key path, not the data which may be huge
|
||||
const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
|
||||
@@ -393,7 +384,7 @@ class IndexedDBLogStore {
|
||||
txn.onerror = () => {
|
||||
reject(
|
||||
new Error(
|
||||
"Failed to delete logs for " + `'${id}' : ${txn?.error?.message}`
|
||||
"Failed to delete logs for " + `'${id}' : ${txn.error.message}`
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -404,7 +395,7 @@ class IndexedDBLogStore {
|
||||
}
|
||||
|
||||
const allLogIds = await fetchLogIds();
|
||||
let removeLogIds: number[] = [];
|
||||
let removeLogIds = [];
|
||||
const logs: LogEntry[] = [];
|
||||
let size = 0;
|
||||
for (let i = 0; i < allLogIds.length; i++) {
|
||||
@@ -423,7 +414,7 @@ class IndexedDBLogStore {
|
||||
if (size >= MAX_LOG_SIZE) {
|
||||
// the remaining log IDs should be removed. If we go out of
|
||||
// bounds this is just []
|
||||
removeLogIds = allLogIds.slice(i + 1).map((id) => parseInt(id, 10));
|
||||
removeLogIds = allLogIds.slice(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -471,14 +462,14 @@ class IndexedDBLogStore {
|
||||
*/
|
||||
function selectQuery<T>(
|
||||
store: IDBObjectStore,
|
||||
keyRange: IDBKeyRange | undefined,
|
||||
keyRange: IDBKeyRange,
|
||||
resultMapper: (cursor: IDBCursorWithValue) => T
|
||||
): Promise<T[]> {
|
||||
const query = store.openCursor(keyRange);
|
||||
return new Promise((resolve, reject) => {
|
||||
const results: T[] = [];
|
||||
const results = [];
|
||||
query.onerror = () => {
|
||||
reject(new Error("Query failed: " + query?.error?.message));
|
||||
reject(new Error("Query failed: " + query.error.message));
|
||||
};
|
||||
// collect results
|
||||
query.onsuccess = () => {
|
||||
|
||||
@@ -15,12 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import pako from "pako";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { getLogsForReport } from "./rageshake";
|
||||
import { useClient } from "../ClientContext";
|
||||
@@ -48,27 +46,20 @@ export function useSubmitRageshake(): {
|
||||
submitRageshake: (opts: RageShakeSubmitOptions) => Promise<void>;
|
||||
sending: boolean;
|
||||
sent: boolean;
|
||||
error?: Error;
|
||||
error: Error;
|
||||
} {
|
||||
const { client } = useClient();
|
||||
|
||||
const client: MatrixClient = useClient().client;
|
||||
// The value of the context is the whole tuple returned from setState,
|
||||
// so we just want the current state.
|
||||
const [inspectorState] = useContext(InspectorContext);
|
||||
|
||||
const [{ sending, sent, error }, setState] = useState<{
|
||||
sending: boolean;
|
||||
sent: boolean;
|
||||
error?: Error;
|
||||
}>({
|
||||
const [{ sending, sent, error }, setState] = useState({
|
||||
sending: false,
|
||||
sent: false,
|
||||
error: undefined,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const submitRageshake = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
async (opts) => {
|
||||
if (!Config.get().rageshake?.submit_url) {
|
||||
throw new Error("No rageshake URL is configured");
|
||||
@@ -79,7 +70,7 @@ export function useSubmitRageshake(): {
|
||||
}
|
||||
|
||||
try {
|
||||
setState({ sending: true, sent: false, error: undefined });
|
||||
setState({ sending: true, sent: false, error: null });
|
||||
|
||||
let userAgent = "UNKNOWN";
|
||||
if (window.navigator && window.navigator.userAgent) {
|
||||
@@ -113,11 +104,11 @@ export function useSubmitRageshake(): {
|
||||
body.append("call_backend", "livekit");
|
||||
|
||||
if (client) {
|
||||
const userId = client.getUserId()!;
|
||||
const userId = client.getUserId();
|
||||
const user = client.getUser(userId);
|
||||
body.append("display_name", user?.displayName ?? "");
|
||||
body.append("user_id", client.credentials.userId ?? "");
|
||||
body.append("device_id", client.deviceId ?? "");
|
||||
body.append("display_name", user?.displayName);
|
||||
body.append("user_id", client.credentials.userId);
|
||||
body.append("device_id", client.deviceId);
|
||||
|
||||
if (opts.roomId) {
|
||||
body.append("room_id", opts.roomId);
|
||||
@@ -129,11 +120,11 @@ export function useSubmitRageshake(): {
|
||||
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
|
||||
}
|
||||
body.append("device_keys", keys.join(", "));
|
||||
body.append("cross_signing_key", client.getCrossSigningId()!);
|
||||
body.append("cross_signing_key", client.getCrossSigningId());
|
||||
|
||||
// add cross-signing status information
|
||||
const crossSigning = client.crypto!.crossSigningInfo;
|
||||
const secretStorage = client.crypto!.secretStorage;
|
||||
const crossSigning = client.crypto.crossSigningInfo;
|
||||
const secretStorage = client.crypto.secretStorage;
|
||||
|
||||
body.append(
|
||||
"cross_signing_ready",
|
||||
@@ -147,7 +138,7 @@ export function useSubmitRageshake(): {
|
||||
)
|
||||
)
|
||||
);
|
||||
body.append("cross_signing_key", crossSigning.getId()!);
|
||||
body.append("cross_signing_key", crossSigning.getId());
|
||||
body.append(
|
||||
"cross_signing_privkey_in_secret_storage",
|
||||
String(
|
||||
@@ -159,17 +150,14 @@ export function useSubmitRageshake(): {
|
||||
body.append(
|
||||
"cross_signing_master_privkey_cached",
|
||||
String(
|
||||
!!(
|
||||
pkCache?.getCrossSigningKeyCache &&
|
||||
(await pkCache.getCrossSigningKeyCache("master"))
|
||||
)
|
||||
!!(pkCache && (await pkCache.getCrossSigningKeyCache("master")))
|
||||
)
|
||||
);
|
||||
body.append(
|
||||
"cross_signing_self_signing_privkey_cached",
|
||||
String(
|
||||
!!(
|
||||
pkCache?.getCrossSigningKeyCache &&
|
||||
pkCache &&
|
||||
(await pkCache.getCrossSigningKeyCache("self_signing"))
|
||||
)
|
||||
)
|
||||
@@ -178,7 +166,7 @@ export function useSubmitRageshake(): {
|
||||
"cross_signing_user_signing_privkey_cached",
|
||||
String(
|
||||
!!(
|
||||
pkCache?.getCrossSigningKeyCache &&
|
||||
pkCache &&
|
||||
(await pkCache.getCrossSigningKeyCache("user_signing"))
|
||||
)
|
||||
)
|
||||
@@ -198,7 +186,7 @@ export function useSubmitRageshake(): {
|
||||
String(!!(await client.isKeyBackupKeyStored()))
|
||||
);
|
||||
const sessionBackupKeyFromCache =
|
||||
await client.crypto!.getSessionBackupPrivateKey();
|
||||
await client.crypto.getSessionBackupPrivateKey();
|
||||
body.append(
|
||||
"session_backup_key_cached",
|
||||
String(!!sessionBackupKeyFromCache)
|
||||
@@ -245,7 +233,7 @@ export function useSubmitRageshake(): {
|
||||
Object.keys(estimate.usageDetails).forEach((k) => {
|
||||
body.append(
|
||||
`storageManager_usage_${k}`,
|
||||
String(estimate.usageDetails![k])
|
||||
String(estimate.usageDetails[k])
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -283,14 +271,14 @@ export function useSubmitRageshake(): {
|
||||
);
|
||||
}
|
||||
|
||||
await fetch(Config.get().rageshake!.submit_url, {
|
||||
await fetch(Config.get().rageshake?.submit_url, {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
setState({ sending: false, sent: true, error: undefined });
|
||||
setState({ sending: false, sent: true, error: null });
|
||||
} catch (error) {
|
||||
setState({ sending: false, sent: false, error: error as Error });
|
||||
setState({ sending: false, sent: false, error });
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
@@ -319,7 +307,7 @@ export function useDownloadDebugLog(): () => void {
|
||||
el.click();
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
el.parentNode!.removeChild(el);
|
||||
el.parentNode.removeChild(el);
|
||||
}, 0);
|
||||
}, [json]);
|
||||
|
||||
@@ -333,8 +321,8 @@ export function useRageshakeRequest(): (
|
||||
const { client } = useClient();
|
||||
|
||||
const sendRageshakeRequest = useCallback(
|
||||
(roomId: string, rageshakeRequestId: string) => {
|
||||
client!.sendEvent(roomId, "org.matrix.rageshake_request", {
|
||||
(roomId, rageshakeRequestId) => {
|
||||
client.sendEvent(roomId, "org.matrix.rageshake_request", {
|
||||
request_id: rageshakeRequestId,
|
||||
});
|
||||
},
|
||||
@@ -359,12 +347,10 @@ export function useRageshakeRequestModal(roomId: string): {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: ModalProps;
|
||||
};
|
||||
const { client } = useClient();
|
||||
const client: MatrixClient = useClient().client;
|
||||
const [rageshakeRequestId, setRageshakeRequestId] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!client) return;
|
||||
|
||||
const onEvent = (event: MatrixEvent) => {
|
||||
const type = event.getType();
|
||||
|
||||
@@ -385,8 +371,5 @@ export function useRageshakeRequestModal(roomId: string): {
|
||||
};
|
||||
}, [modalState.open, roomId, client, modalState]);
|
||||
|
||||
return {
|
||||
modalState,
|
||||
modalProps: { ...modalProps, rageshakeRequestId: rageshakeRequestId ?? "" },
|
||||
};
|
||||
return { modalState, modalProps: { ...modalProps, rageshakeRequestId } };
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function TabContainer<T extends object>(
|
||||
props: TabContainerProps<T>
|
||||
): JSX.Element {
|
||||
const state = useTabListState<T>(props);
|
||||
const ref = useRef<HTMLUListElement>(null);
|
||||
const ref = useRef<HTMLUListElement>();
|
||||
const { tabListProps } = useTabList(props, state, ref);
|
||||
return (
|
||||
<div className={classNames(styles.tabContainer, props.className)}>
|
||||
@@ -53,7 +53,7 @@ interface TabProps<T> {
|
||||
|
||||
function Tab<T>({ item, state }: TabProps<T>): JSX.Element {
|
||||
const { key, rendered } = item;
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
const ref = useRef<HTMLLIElement>();
|
||||
const { tabProps } = useTab({ key }, state, ref);
|
||||
|
||||
return (
|
||||
@@ -75,7 +75,7 @@ interface TabPanelProps<T> extends AriaTabPanelProps {
|
||||
}
|
||||
|
||||
function TabPanel<T>({ state, ...props }: TabPanelProps<T>): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const { tabPanelProps } = useTabPanel(props, state, ref);
|
||||
return (
|
||||
<div {...tabPanelProps} ref={ref} className={styles.tabPanel}>
|
||||
|
||||
@@ -46,7 +46,7 @@ export const Headline = forwardRef<HTMLHeadingElement, TypographyProps>(
|
||||
{
|
||||
...rest,
|
||||
className: classNames(
|
||||
styles[fontWeight ?? ""],
|
||||
styles[fontWeight],
|
||||
{ [styles.overflowEllipsis]: overflowEllipsis },
|
||||
className
|
||||
),
|
||||
@@ -74,7 +74,7 @@ export const Title = forwardRef<HTMLHeadingElement, TypographyProps>(
|
||||
{
|
||||
...rest,
|
||||
className: classNames(
|
||||
styles[fontWeight ?? ""],
|
||||
styles[fontWeight],
|
||||
{ [styles.overflowEllipsis]: overflowEllipsis },
|
||||
className
|
||||
),
|
||||
@@ -102,7 +102,7 @@ export const Subtitle = forwardRef<HTMLParagraphElement, TypographyProps>(
|
||||
{
|
||||
...rest,
|
||||
className: classNames(
|
||||
styles[fontWeight ?? ""],
|
||||
styles[fontWeight],
|
||||
{ [styles.overflowEllipsis]: overflowEllipsis },
|
||||
className
|
||||
),
|
||||
@@ -130,7 +130,7 @@ export const Body = forwardRef<HTMLParagraphElement, TypographyProps>(
|
||||
{
|
||||
...rest,
|
||||
className: classNames(
|
||||
styles[fontWeight ?? ""],
|
||||
styles[fontWeight],
|
||||
{ [styles.overflowEllipsis]: overflowEllipsis },
|
||||
className
|
||||
),
|
||||
@@ -159,7 +159,7 @@ export const Caption = forwardRef<HTMLParagraphElement, TypographyProps>(
|
||||
...rest,
|
||||
className: classNames(
|
||||
styles.caption,
|
||||
styles[fontWeight ?? ""],
|
||||
styles[fontWeight],
|
||||
{ [styles.overflowEllipsis]: overflowEllipsis },
|
||||
className
|
||||
),
|
||||
@@ -188,7 +188,7 @@ export const Micro = forwardRef<HTMLParagraphElement, TypographyProps>(
|
||||
...rest,
|
||||
className: classNames(
|
||||
styles.micro,
|
||||
styles[fontWeight ?? ""],
|
||||
styles[fontWeight],
|
||||
{ [styles.overflowEllipsis]: overflowEllipsis },
|
||||
className
|
||||
),
|
||||
@@ -219,8 +219,6 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const Component: string | RouterLink = as || (to ? RouterLink : "a");
|
||||
let externalLinkProps: { href: string; target: string; rel: string };
|
||||
|
||||
@@ -235,16 +233,12 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
return createElement(
|
||||
Component,
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
...externalLinkProps,
|
||||
...rest,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
to: to,
|
||||
className: classNames(
|
||||
styles[color],
|
||||
styles[fontWeight ?? ""],
|
||||
styles[fontWeight],
|
||||
{ [styles.overflowEllipsis]: overflowEllipsis },
|
||||
className
|
||||
),
|
||||
|
||||
@@ -31,13 +31,8 @@ export const useEventTarget = <T extends Event>(
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (target) {
|
||||
target.addEventListener(eventType, listener as EventListener, options);
|
||||
return () =>
|
||||
target.removeEventListener(
|
||||
eventType,
|
||||
listener as EventListener,
|
||||
options
|
||||
);
|
||||
target.addEventListener(eventType, listener, options);
|
||||
return () => target.removeEventListener(eventType, listener, options);
|
||||
}
|
||||
}, [target, eventType, listener, options]);
|
||||
};
|
||||
|
||||
@@ -21,9 +21,7 @@ export function useLocationNavigation(enabled = false): void {
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
let unblock = undefined;
|
||||
let unblock;
|
||||
|
||||
if (enabled) {
|
||||
unblock = history.block((tx) => {
|
||||
@@ -35,8 +33,6 @@ export function useLocationNavigation(enabled = false): void {
|
||||
}
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (unblock) {
|
||||
unblock();
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface Grid {
|
||||
cells: Cell[];
|
||||
}
|
||||
|
||||
export interface SparseGrid {
|
||||
interface SparseGrid {
|
||||
columns: number;
|
||||
/**
|
||||
* The cells of the grid, in left-to-right top-to-bottom order.
|
||||
@@ -803,7 +803,7 @@ export function setTileSize<G extends Grid | SparseGrid>(
|
||||
|
||||
const result: SparseGrid = gridWithoutTile;
|
||||
placeTile(to, toEnd, result);
|
||||
return fillGaps(result, true, (i: number) => inArea(i, to, toEnd, g)) as G;
|
||||
return fillGaps(result, true, (i) => inArea(i, to, toEnd, g)) as G;
|
||||
} else if (toWidth >= fromWidth && toHeight >= fromHeight) {
|
||||
// The tile is growing, which might be able to happen in-place
|
||||
const to = findNearestCell(
|
||||
@@ -1054,8 +1054,7 @@ export const BigGrid: Layout<Grid> = {
|
||||
emptyState: { columns: 4, cells: [] },
|
||||
updateTiles,
|
||||
updateBounds,
|
||||
getTiles: <T,>(g: Grid) =>
|
||||
g.cells.filter((c) => c.origin).map((c) => c!.item as T),
|
||||
getTiles: <T,>(g) => g.cells.filter((c) => c.origin).map((c) => c!.item as T),
|
||||
canDragTile: () => true,
|
||||
dragTile,
|
||||
toggleFocus: cycleTileSize,
|
||||
|
||||
@@ -271,19 +271,10 @@ export function NewVideoGrid<T>({
|
||||
// gesture using the much more sensible ref-based method.
|
||||
const onTileDrag = (
|
||||
tileId: string,
|
||||
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
tap,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
initial: [initialX, initialY],
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delta: [dx, dy],
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
last,
|
||||
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
|
||||
) => {
|
||||
@@ -334,8 +325,6 @@ export function NewVideoGrid<T>({
|
||||
const scrollOffset = useRef(0);
|
||||
|
||||
useScroll(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
({ xy: [, y], delta: [, dy] }) => {
|
||||
scrollOffset.current = y;
|
||||
|
||||
|
||||
@@ -695,7 +695,7 @@ function getSubGridPositions(
|
||||
// Calculates the number of possible tiles that can be displayed
|
||||
function displayedTileCount(
|
||||
layout: Layout,
|
||||
tileCount: number,
|
||||
tileCount,
|
||||
gridWidth: number,
|
||||
gridHeight: number
|
||||
): number {
|
||||
@@ -854,7 +854,7 @@ export function VideoGrid<T>({
|
||||
tilePositions: [],
|
||||
});
|
||||
const [scrollPosition, setScrollPosition] = useState<number>(0);
|
||||
const draggingTileRef = useRef<DragTileData | null>(null);
|
||||
const draggingTileRef = useRef<DragTileData>(null);
|
||||
const lastTappedRef = useRef<{ [index: Key]: number }>({});
|
||||
const lastLayoutRef = useRef<Layout>(layout);
|
||||
const isMounted = useIsMounted();
|
||||
@@ -1189,23 +1189,11 @@ export function VideoGrid<T>({
|
||||
const onTileDrag = (
|
||||
tileId: string,
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
active,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
xy,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
movement,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
tap,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
last,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
event,
|
||||
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
|
||||
) => {
|
||||
@@ -1357,11 +1345,7 @@ export function VideoGrid<T>({
|
||||
|
||||
const bindGrid = useGesture(
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
onWheel: (e) => onGridGesture(e, true),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
onDrag: (e) => onGridGesture(e, false),
|
||||
},
|
||||
{}
|
||||
|
||||
@@ -136,7 +136,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
<AudioButton
|
||||
key="localVolume"
|
||||
className={styles.button}
|
||||
volume={(sfuParticipant as RemoteParticipant).getVolume() ?? 0}
|
||||
volume={(sfuParticipant as RemoteParticipant).getVolume()}
|
||||
onPress={onOptionsPress}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ const LocalVolume: React.FC<LocalVolumeProps> = ({
|
||||
participant,
|
||||
}: LocalVolumeProps) => {
|
||||
const [localVolume, setLocalVolume] = useState<number>(
|
||||
participant.getVolume() ?? 0
|
||||
participant.getVolume()
|
||||
);
|
||||
|
||||
const onLocalVolumeChanged = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -109,7 +109,7 @@ export const widget: WidgetHelpers | null = (() => {
|
||||
baseUrl,
|
||||
e2eEnabled,
|
||||
allowIceFallback,
|
||||
} = getUrlParams(true);
|
||||
} = getUrlParams();
|
||||
if (!roomId) throw new Error("Room ID must be supplied");
|
||||
if (!userId) throw new Error("User ID must be supplied");
|
||||
if (!deviceId) throw new Error("Device ID must be supplied");
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { getUrlParams } from "../src/UrlParams";
|
||||
import { Config } from "../src/config/Config";
|
||||
|
||||
const ROOM_NAME = "roomNameHere";
|
||||
const ROOM_ID = "d45f138fsd";
|
||||
const ORIGIN = "https://call.element.io";
|
||||
const HOMESERVER = "call.ems.host";
|
||||
|
||||
jest.mock("../src/config/Config");
|
||||
|
||||
describe("UrlParams", () => {
|
||||
beforeAll(() => {
|
||||
mocked(Config.defaultServerName).mockReturnValue("call.ems.host");
|
||||
});
|
||||
|
||||
describe("handles URL with /room/", () => {
|
||||
it("and nothing else", () => {
|
||||
expect(getUrlParams(false, "", `/room/${ROOM_NAME}`, "").roomAlias).toBe(
|
||||
`#${ROOM_NAME}:${HOMESERVER}`
|
||||
);
|
||||
});
|
||||
|
||||
it("and #", () => {
|
||||
expect(
|
||||
getUrlParams(false, "", `${ORIGIN}/room/`, `#${ROOM_NAME}`).roomAlias
|
||||
).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
|
||||
});
|
||||
|
||||
it("and # and server part", () => {
|
||||
expect(
|
||||
getUrlParams(false, "", `/room/`, `#${ROOM_NAME}:${HOMESERVER}`)
|
||||
.roomAlias
|
||||
).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
|
||||
});
|
||||
|
||||
it("and server part", () => {
|
||||
expect(
|
||||
getUrlParams(false, "", `/room/${ROOM_NAME}:${HOMESERVER}`, "")
|
||||
.roomAlias
|
||||
).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handles URL without /room/", () => {
|
||||
it("and nothing else", () => {
|
||||
expect(getUrlParams(false, "", `/${ROOM_NAME}`, "").roomAlias).toBe(
|
||||
`#${ROOM_NAME}:${HOMESERVER}`
|
||||
);
|
||||
});
|
||||
|
||||
it("and with #", () => {
|
||||
expect(getUrlParams(false, "", "", `#${ROOM_NAME}`).roomAlias).toBe(
|
||||
`#${ROOM_NAME}:${HOMESERVER}`
|
||||
);
|
||||
});
|
||||
|
||||
it("and with # and server part", () => {
|
||||
expect(
|
||||
getUrlParams(false, "", "", `#${ROOM_NAME}:${HOMESERVER}`).roomAlias
|
||||
).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
|
||||
});
|
||||
|
||||
it("and with server part", () => {
|
||||
expect(
|
||||
getUrlParams(false, "", `/${ROOM_NAME}:${HOMESERVER}`, "").roomAlias
|
||||
).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handles search params", () => {
|
||||
it("(roomId)", () => {
|
||||
expect(getUrlParams(true, `?roomId=${ROOM_ID}`).roomId).toBe(ROOM_ID);
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores room alias", () => {
|
||||
expect(
|
||||
getUrlParams(true, "", `/room/${ROOM_NAME}:${HOMESERVER}`).roomAlias
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,7 @@ describe("CallList", () => {
|
||||
|
||||
it("should show room", async () => {
|
||||
const rooms = [
|
||||
{ roomName: "Room #1", roomAlias: "#room-name:server.org" },
|
||||
{ roomName: "Room #1", roomId: "!roomId" },
|
||||
] as GroupCallRoom[];
|
||||
|
||||
const result = renderComponent(rooms);
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("ObjectFlattener", () => {
|
||||
localCandidateType: "srfx",
|
||||
remoteCandidateType: "srfx",
|
||||
networkType: "ethernet",
|
||||
rtt: 0,
|
||||
rtt: null,
|
||||
},
|
||||
],
|
||||
audioConcealment: new Map([
|
||||
@@ -167,7 +167,7 @@ describe("ObjectFlattener", () => {
|
||||
"matrix.call.stats.connection.transport.1.localCandidateType": "srfx",
|
||||
"matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx",
|
||||
"matrix.call.stats.connection.transport.1.networkType": "ethernet",
|
||||
"matrix.call.stats.connection.transport.1.rtt": 0,
|
||||
"matrix.call.stats.connection.transport.1.rtt": "null",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -234,7 +234,7 @@ describe("ObjectFlattener", () => {
|
||||
"matrix.call.stats.connection.transport.1.localCandidateType": "srfx",
|
||||
"matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx",
|
||||
"matrix.call.stats.connection.transport.1.networkType": "ethernet",
|
||||
"matrix.call.stats.connection.transport.1.rtt": 0,
|
||||
"matrix.call.stats.connection.transport.1.rtt": "null",
|
||||
"matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0,
|
||||
"matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0,
|
||||
"matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0,
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
fillGaps,
|
||||
forEachCellInArea,
|
||||
Grid,
|
||||
SparseGrid,
|
||||
resize,
|
||||
row,
|
||||
moveTile,
|
||||
@@ -340,9 +339,7 @@ function testAddItems(
|
||||
output: string
|
||||
): void {
|
||||
test(`addItems ${title}`, () => {
|
||||
expect(showGrid(addItems(items, mkGrid(input) as SparseGrid) as Grid)).toBe(
|
||||
output
|
||||
);
|
||||
expect(showGrid(addItems(items, mkGrid(input)))).toBe(output);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"esModuleInterop": true,
|
||||
"module": "es2020",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["es2020", "dom", "dom.iterable"],
|
||||
|
||||
// From Matrix-JS-SDK
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"noEmitOnError": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": true
|
||||
|
||||
// TODO: Enable the following options later.
|
||||
// "forceConsistentCasingInFileNames": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noImplicitReturns": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
// "noUncheckedIndexedAccess": true,
|
||||
// "noUnusedParameters": true,
|
||||
"strict": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "typescript-strict-plugin",
|
||||
"paths": ["src"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./node_modules/matrix-js-sdk/src/@types/*.d.ts",
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx",
|
||||
"./test/**/*.ts",
|
||||
|
||||
@@ -19,7 +19,6 @@ import svgrPlugin from "vite-plugin-svgr";
|
||||
import htmlTemplate from "vite-plugin-html-template";
|
||||
import sentryVitePlugin from "@sentry/vite-plugin";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import basicSsl from "@vitejs/plugin-basic-ssl";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
@@ -27,7 +26,6 @@ export default defineConfig(({ mode }) => {
|
||||
|
||||
const plugins = [
|
||||
react(),
|
||||
basicSsl(),
|
||||
svgrPlugin(),
|
||||
htmlTemplate.default({
|
||||
data: {
|
||||
|
||||
96
yarn.lock
96
yarn.lock
@@ -2014,13 +2014,6 @@
|
||||
dependencies:
|
||||
"@sinclair/typebox" "^0.24.1"
|
||||
|
||||
"@jest/schemas@^29.4.3":
|
||||
version "29.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788"
|
||||
integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==
|
||||
dependencies:
|
||||
"@sinclair/typebox" "^0.25.16"
|
||||
|
||||
"@jest/source-map@^29.2.0":
|
||||
version "29.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.2.0.tgz#ab3420c46d42508dcc3dc1c6deee0b613c235744"
|
||||
@@ -2095,18 +2088,6 @@
|
||||
"@types/yargs" "^17.0.8"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@jest/types@^29.5.0":
|
||||
version "29.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593"
|
||||
integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==
|
||||
dependencies:
|
||||
"@jest/schemas" "^29.4.3"
|
||||
"@types/istanbul-lib-coverage" "^2.0.0"
|
||||
"@types/istanbul-reports" "^3.0.0"
|
||||
"@types/node" "*"
|
||||
"@types/yargs" "^17.0.8"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@joshwooding/vite-plugin-react-docgen-typescript@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.0.2.tgz#e0ae8c94f468da3a273a7b0acf23ba3565f86cbc"
|
||||
@@ -3117,11 +3098,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f"
|
||||
integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==
|
||||
|
||||
"@sinclair/typebox@^0.25.16":
|
||||
version "0.25.24"
|
||||
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718"
|
||||
integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
|
||||
@@ -3917,11 +3893,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
|
||||
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
|
||||
|
||||
"@types/content-type@^1.1.5":
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.5.tgz#aa02dca40864749a9e2bf0161a6216da57e3ede5"
|
||||
integrity sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==
|
||||
|
||||
"@types/eslint-scope@^3.7.3":
|
||||
version "3.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
|
||||
@@ -3985,11 +3956,6 @@
|
||||
dependencies:
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/history@^4.7.11":
|
||||
version "4.7.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
|
||||
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
|
||||
|
||||
"@types/html-minifier-terser@^5.0.0":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57"
|
||||
@@ -4138,23 +4104,6 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-router-dom@^5.3.3":
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
|
||||
integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
|
||||
dependencies:
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
"@types/react-router" "*"
|
||||
|
||||
"@types/react-router@*":
|
||||
version "5.1.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c"
|
||||
integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==
|
||||
dependencies:
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-syntax-highlighter@11.0.5":
|
||||
version "11.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz#0d546261b4021e1f9d85b50401c0a42acb106087"
|
||||
@@ -4245,11 +4194,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||
|
||||
"@types/uuid@9":
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b"
|
||||
integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==
|
||||
|
||||
"@types/webpack-env@^1.16.0":
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0"
|
||||
@@ -4384,11 +4328,6 @@
|
||||
dependencies:
|
||||
"@use-gesture/core" "10.2.16"
|
||||
|
||||
"@vitejs/plugin-basic-ssl@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz#48c46eab21e0730921986ce742563ae83fe7fe34"
|
||||
integrity sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==
|
||||
|
||||
"@vitejs/plugin-react@^1.0.8":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz#2fcf0b6ce9bcdcd4cec5c760c199779d5657ece1"
|
||||
@@ -9236,6 +9175,13 @@ history@^4.9.0:
|
||||
tiny-warning "^1.0.0"
|
||||
value-equal "^1.0.1"
|
||||
|
||||
history@^5.2.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
|
||||
integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.7.6"
|
||||
|
||||
hmac-drbg@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
@@ -10333,15 +10279,6 @@ jest-mock@^29.3.1:
|
||||
"@types/node" "*"
|
||||
jest-util "^29.3.1"
|
||||
|
||||
jest-mock@^29.5.0:
|
||||
version "29.5.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed"
|
||||
integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==
|
||||
dependencies:
|
||||
"@jest/types" "^29.5.0"
|
||||
"@types/node" "*"
|
||||
jest-util "^29.5.0"
|
||||
|
||||
jest-pnp-resolver@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
|
||||
@@ -10484,18 +10421,6 @@ jest-util@^29.3.1:
|
||||
graceful-fs "^4.2.9"
|
||||
picomatch "^2.2.3"
|
||||
|
||||
jest-util@^29.5.0:
|
||||
version "29.5.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f"
|
||||
integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==
|
||||
dependencies:
|
||||
"@jest/types" "^29.5.0"
|
||||
"@types/node" "*"
|
||||
chalk "^4.0.0"
|
||||
ci-info "^3.2.0"
|
||||
graceful-fs "^4.2.9"
|
||||
picomatch "^2.2.3"
|
||||
|
||||
jest-validate@^29.2.2:
|
||||
version "29.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.2.2.tgz#e43ce1931292dfc052562a11bc681af3805eadce"
|
||||
@@ -13076,6 +13001,13 @@ react-router@5.3.3:
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
react-router@6:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557"
|
||||
integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==
|
||||
dependencies:
|
||||
history "^5.2.0"
|
||||
|
||||
react-syntax-highlighter@^15.4.5:
|
||||
version "15.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
|
||||
|
||||
Reference in New Issue
Block a user