mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-23 20:59:21 +00:00
Compare commits
1 Commits
dbkr/sentr
...
robin/sand
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e53b0c0244 |
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()
|
||||
}
|
||||
@@ -106,7 +106,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",
|
||||
|
||||
@@ -24,6 +24,7 @@ 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";
|
||||
@@ -70,11 +71,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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -80,7 +80,11 @@ export const RegisterPage: FC = () => {
|
||||
passwordlessUser
|
||||
);
|
||||
|
||||
if (client && client?.groupCallEventHandler && passwordlessUser) {
|
||||
if (!client || !client.groupCallEventHandler || !setClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordlessUser) {
|
||||
// Migrate the user's rooms
|
||||
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
|
||||
const roomId = groupCall.room.roomId;
|
||||
@@ -103,7 +107,7 @@ export const RegisterPage: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
setClient?.({ client: newClient, session });
|
||||
setClient({ client: newClient, session });
|
||||
PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date());
|
||||
};
|
||||
|
||||
|
||||
@@ -245,6 +245,23 @@ export function SettingsButton({
|
||||
);
|
||||
}
|
||||
|
||||
export function SimulateJoinButton(props: {
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => "Add a fake participant", [t]);
|
||||
|
||||
return (
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
<Button variant="toolbar" {...props}>
|
||||
<AddUserIcon width={20} height={20} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function InviteButton({
|
||||
className,
|
||||
variant = "toolbar",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -71,9 +71,8 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
||||
setLoading(true);
|
||||
|
||||
const [roomAlias] = await createRoom(client, roomName, ptt);
|
||||
|
||||
if (roomAlias) {
|
||||
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
|
||||
history.push(`/room/${roomAlias}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ export const UnauthenticatedView: FC = () => {
|
||||
setOnFinished(() => {
|
||||
setClient({ client, session });
|
||||
const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName);
|
||||
history.push(`/${aliasLocalpart}`);
|
||||
const [, serverName] = client.getUserId()!.split(":");
|
||||
history.push(`/room/#${aliasLocalpart}:${serverName}`);
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
@@ -110,7 +111,7 @@ export const UnauthenticatedView: FC = () => {
|
||||
}
|
||||
|
||||
setClient({ client, session });
|
||||
history.push(`/${roomAlias.substring(1).split(":")[0]}`);
|
||||
history.push(`/room/${roomAlias}`);
|
||||
}
|
||||
|
||||
submit().catch((error) => {
|
||||
|
||||
@@ -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;
|
||||
@@ -89,13 +89,12 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
||||
|
||||
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)!;
|
||||
|
||||
return {
|
||||
roomAlias: room.getCanonicalAlias(),
|
||||
roomId: room.getCanonicalAlias() || room.roomId,
|
||||
roomName: room.name,
|
||||
avatarUrl: room.getMxcAvatarUrl()!,
|
||||
room,
|
||||
@@ -104,7 +103,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
||||
};
|
||||
});
|
||||
|
||||
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",
|
||||
|
||||
@@ -34,7 +34,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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -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,8 +15,6 @@ export type DeviceChoices = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
setLogLevel("debug");
|
||||
|
||||
export function useLiveKit(
|
||||
userChoices: UserChoices,
|
||||
sfuConfig?: SFUConfig
|
||||
|
||||
@@ -345,11 +345,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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
ScreenshareButton,
|
||||
SettingsButton,
|
||||
InviteButton,
|
||||
SimulateJoinButton,
|
||||
} from "../button";
|
||||
import {
|
||||
Header,
|
||||
@@ -58,6 +59,7 @@ import {
|
||||
import {
|
||||
useShowInspector,
|
||||
useShowConnectionStats,
|
||||
useVideoGridSandboxMode,
|
||||
} from "../settings/useSetting";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
@@ -83,6 +85,8 @@ import { useFullscreen } from "./useFullscreen";
|
||||
import { useLayoutStates } from "../video-grid/Layout";
|
||||
import { useSFUConfig } from "../livekit/OpenIDLoader";
|
||||
import { E2EELock } from "../E2EELock";
|
||||
import { useFakeTiles } from "./useFakeTiles";
|
||||
import { FakeItemData, FakeVideoTile } from "../video-grid/FakeVideoTile";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
@@ -160,6 +164,9 @@ export function InCallView({
|
||||
|
||||
const [showInspector] = useShowInspector();
|
||||
const [showConnectionStats] = useShowConnectionStats();
|
||||
const [videoGridSandboxMode] = useVideoGridSandboxMode()
|
||||
|
||||
const { fakeTiles: fakeItems, simulateJoin } = useFakeTiles()
|
||||
|
||||
const { hideScreensharing } = useUrlParams();
|
||||
|
||||
@@ -227,9 +234,11 @@ export function InCallView({
|
||||
const reducedControls = boundsValid && bounds.width <= 400;
|
||||
const noControls = reducedControls && bounds.height <= 400;
|
||||
|
||||
const items = useParticipantTiles(livekitRoom, participants);
|
||||
const realItems = useParticipantTiles(livekitRoom, participants);
|
||||
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
||||
useFullscreen(items);
|
||||
useFullscreen(realItems);
|
||||
|
||||
const items = useMemo<TileDescriptor<ItemData | FakeItemData>[]>(() => videoGridSandboxMode ? [...realItems, ...fakeItems] : realItems, [videoGridSandboxMode, realItems, fakeItems])
|
||||
|
||||
// The maximised participant: either the participant that the user has
|
||||
// manually put in fullscreen, or the focused (active) participant if the
|
||||
@@ -238,9 +247,9 @@ export function InCallView({
|
||||
() =>
|
||||
fullscreenItem ??
|
||||
(noControls
|
||||
? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null
|
||||
? realItems.find((item) => item.isSpeaker) ?? realItems.at(0) ?? null
|
||||
: null),
|
||||
[fullscreenItem, noControls, items]
|
||||
[fullscreenItem, noControls, realItems]
|
||||
);
|
||||
|
||||
const Grid =
|
||||
@@ -283,16 +292,19 @@ export function InCallView({
|
||||
disableAnimations={prefersReducedMotion || isSafari}
|
||||
layoutStates={layoutStates}
|
||||
>
|
||||
{(props) => (
|
||||
{(props) => props.data.type === "real" ? (
|
||||
<VideoTile
|
||||
maximised={false}
|
||||
fullscreen={false}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
showSpeakingIndicator={items.length > 2}
|
||||
showSpeakingIndicator={realItems.length > 2}
|
||||
showConnectionStats={showConnectionStats}
|
||||
{...props}
|
||||
data={props.data}
|
||||
ref={props.ref as Ref<HTMLDivElement>}
|
||||
/>
|
||||
) : (
|
||||
<FakeVideoTile {...props} data={props.data} />
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
@@ -381,6 +393,8 @@ export function InCallView({
|
||||
);
|
||||
}
|
||||
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
|
||||
if (videoGridSandboxMode)
|
||||
buttons.push(<SimulateJoinButton key="5" onPress={simulateJoin} />)
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
@@ -480,7 +494,7 @@ function useParticipantTiles(
|
||||
const member = matrixParticipants.get(id);
|
||||
allGhosts &&= member === undefined;
|
||||
|
||||
const userMediaTile = {
|
||||
const userMediaTile: TileDescriptor<ItemData> = {
|
||||
id,
|
||||
focused: false,
|
||||
isPresenter: sfuParticipant.isScreenShareEnabled,
|
||||
@@ -491,6 +505,7 @@ function useParticipantTiles(
|
||||
local: sfuParticipant.isLocal,
|
||||
largeBaseSize: false,
|
||||
data: {
|
||||
type: "real",
|
||||
id,
|
||||
member,
|
||||
sfuParticipant,
|
||||
|
||||
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 />;
|
||||
}
|
||||
@@ -106,11 +106,11 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
|
||||
onUserChoicesChanged({
|
||||
video: {
|
||||
selectedId: videoIn.selectedId,
|
||||
enabled: videoEnabled,
|
||||
enabled: videoEnabled && !!videoTrack,
|
||||
},
|
||||
audio: {
|
||||
selectedId: audioIn.selectedId,
|
||||
enabled: audioEnabled,
|
||||
enabled: audioEnabled && !!audioTrack,
|
||||
},
|
||||
});
|
||||
}, [
|
||||
|
||||
96
src/room/useFakeTiles.ts
Normal file
96
src/room/useFakeTiles.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
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 { TileDescriptor } from '../video-grid/VideoGrid'
|
||||
import { FakeItemData } from '../video-grid/FakeVideoTile'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useLatest } from '../useLatest'
|
||||
import { generateRandomName } from '../auth/generateRandomName'
|
||||
|
||||
interface FakeTiles {
|
||||
fakeTiles: TileDescriptor<FakeItemData>[]
|
||||
simulateJoin: () => void
|
||||
}
|
||||
|
||||
export const useFakeTiles = (): FakeTiles => {
|
||||
const [tiles, setTiles] = useState<TileDescriptor<FakeItemData>[]>([])
|
||||
const latestSetTiles = useLatest(setTiles)
|
||||
|
||||
const simulateJoin = useCallback(() => {
|
||||
const name = generateRandomName()
|
||||
const newTile: TileDescriptor<FakeItemData> = {
|
||||
id: name,
|
||||
focused: false,
|
||||
isPresenter: false,
|
||||
isSpeaker: false,
|
||||
hasVideo: false,
|
||||
local: false,
|
||||
largeBaseSize: false,
|
||||
data: {
|
||||
type: "fake",
|
||||
name,
|
||||
screenshare: false,
|
||||
speaking: false,
|
||||
simulateScreenshare: () => latestSetTiles.current(ts => {
|
||||
if (ts.some(t => t.data.name === newTile.id && t.data.screenshare)) {
|
||||
// No-op since they're already screensharing
|
||||
return ts
|
||||
} else {
|
||||
const newScreenshareTile: TileDescriptor<FakeItemData> = {
|
||||
id: `${name}:screenshare`,
|
||||
focused: false,
|
||||
isPresenter: false,
|
||||
isSpeaker: false,
|
||||
hasVideo: true,
|
||||
local: false,
|
||||
largeBaseSize: true,
|
||||
placeNear: newTile.id,
|
||||
data: {
|
||||
type: "fake",
|
||||
name,
|
||||
screenshare: true,
|
||||
speaking: false,
|
||||
remove: () => latestSetTiles.current(ts => ts.filter(t => t.id !== newScreenshareTile.id))
|
||||
}
|
||||
}
|
||||
return [...ts, newScreenshareTile]
|
||||
}
|
||||
}),
|
||||
simulateSpeaking: () => latestSetTiles.current(ts => ts.map(t => {
|
||||
if (t.id === newTile.id) {
|
||||
return {
|
||||
...t,
|
||||
isSpeaker: !t.isSpeaker,
|
||||
data: {
|
||||
...t.data,
|
||||
speaking: !t.isSpeaker,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return t
|
||||
}
|
||||
})),
|
||||
remove: () => latestSetTiles.current(ts => ts.filter(t => t.id !== newTile.id))
|
||||
}
|
||||
}
|
||||
setTiles(ts => [...ts, newTile])
|
||||
}, [setTiles, latestSetTiles])
|
||||
|
||||
return {
|
||||
fakeTiles: tiles,
|
||||
simulateJoin,
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useCallback, useLayoutEffect, useRef } from "react";
|
||||
import { useCallback, useLayoutEffect } from "react";
|
||||
|
||||
import { TileDescriptor } from "../video-grid/VideoGrid";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { useEventTarget } from "../useEvents";
|
||||
import { useLatest } from "../useLatest";
|
||||
|
||||
const isFullscreen = () =>
|
||||
Boolean(document.fullscreenElement) ||
|
||||
@@ -69,11 +70,8 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
|
||||
[items]
|
||||
);
|
||||
|
||||
const latestItems = useRef<TileDescriptor<T>[]>(items);
|
||||
latestItems.current = items;
|
||||
|
||||
const latestFullscreenItem = useRef<TileDescriptor<T> | null>(fullscreenItem);
|
||||
latestFullscreenItem.current = fullscreenItem;
|
||||
const latestItems = useLatest(items)
|
||||
const latestFullscreenItem = useLatest(fullscreenItem)
|
||||
|
||||
const toggleFullscreen = useCallback(
|
||||
(itemId: string) => {
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useReducer, useState } from "react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import {
|
||||
GroupCallEvent,
|
||||
GroupCallState,
|
||||
@@ -332,7 +331,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);
|
||||
|
||||
@@ -77,7 +77,7 @@ export function ProfileSettingsTab({ client }: Props) {
|
||||
return (
|
||||
<form onChange={onFormChange} ref={formRef} className={styles.content}>
|
||||
<FieldRow className={styles.avatarFieldRow}>
|
||||
{displayName && (
|
||||
{avatarUrl && displayName && (
|
||||
<AvatarInputField
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
useOptInAnalytics,
|
||||
useDeveloperSettingsTab,
|
||||
useShowConnectionStats,
|
||||
useVideoGridSandboxMode,
|
||||
} from "./useSetting";
|
||||
import { FieldRow, InputField } from "../input/Input";
|
||||
import { Button } from "../button";
|
||||
@@ -68,6 +69,7 @@ export const SettingsModal = (props: Props) => {
|
||||
useDeveloperSettingsTab();
|
||||
const [showConnectionStats, setShowConnectionStats] =
|
||||
useShowConnectionStats();
|
||||
const [videoGridSandboxMode, setVideoGridSandboxMode] = useVideoGridSandboxMode()
|
||||
|
||||
const downloadDebugLog = useDownloadDebugLog();
|
||||
|
||||
@@ -118,7 +120,7 @@ export const SettingsModal = (props: Props) => {
|
||||
|
||||
const devices = props.mediaDevicesSwitcher;
|
||||
|
||||
const audioTab = (
|
||||
const tabs = [
|
||||
<TabItem
|
||||
key="audio"
|
||||
title={
|
||||
@@ -130,10 +132,7 @@ export const SettingsModal = (props: Props) => {
|
||||
>
|
||||
{devices && generateDeviceSelection(devices.audioIn, t("Microphone"))}
|
||||
{devices && generateDeviceSelection(devices.audioOut, t("Speaker"))}
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
const videoTab = (
|
||||
</TabItem>,
|
||||
<TabItem
|
||||
key="video"
|
||||
title={
|
||||
@@ -144,24 +143,7 @@ export const SettingsModal = (props: Props) => {
|
||||
}
|
||||
>
|
||||
{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>,
|
||||
<TabItem
|
||||
key="feedback"
|
||||
title={
|
||||
@@ -172,10 +154,7 @@ export const SettingsModal = (props: Props) => {
|
||||
}
|
||||
>
|
||||
<FeedbackSettingsTab roomId={props.roomId} />
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
const moreTab = (
|
||||
</TabItem>,
|
||||
<TabItem
|
||||
key="more"
|
||||
title={
|
||||
@@ -211,61 +190,85 @@ export const SettingsModal = (props: Props) => {
|
||||
}}
|
||||
/>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
);
|
||||
</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>
|
||||
);
|
||||
if (!isEmbedded) {
|
||||
tabs.push(
|
||||
<TabItem
|
||||
key="profile"
|
||||
title={
|
||||
<>
|
||||
<UserIcon width={15} height={15} />
|
||||
<span>{t("Profile")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ProfileSettingsTab client={props.client} />
|
||||
</TabItem>
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: JSX.Element[] = [];
|
||||
tabs.push(audioTab, videoTab);
|
||||
if (!isEmbedded) tabs.push(profileTab);
|
||||
tabs.push(feedbackTab, moreTab);
|
||||
if (developerSettingsTab) tabs.push(developerTab);
|
||||
if (developerSettingsTab) {
|
||||
tabs.push(
|
||||
<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>
|
||||
<InputField
|
||||
id="videoGridSandboxMode"
|
||||
name="video-grid-sandbox-mode"
|
||||
label="Video grid sandbox mode"
|
||||
type="checkbox"
|
||||
checked={videoGridSandboxMode}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setVideoGridSandboxMode(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Button onPress={downloadDebugLog}>{t("Download debug logs")}</Button>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -104,6 +104,8 @@ export const useDeveloperSettingsTab = () =>
|
||||
export const useShowConnectionStats = () =>
|
||||
useSetting("show-connection-stats", false);
|
||||
|
||||
export const useVideoGridSandboxMode = () => useSetting("video-grid-sandbox-mode", false)
|
||||
|
||||
export const useDefaultDevices = () =>
|
||||
useSetting("defaultDevices", {
|
||||
audioinput: "",
|
||||
|
||||
27
src/useLatest.ts
Normal file
27
src/useLatest.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
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 { useRef } from "react"
|
||||
|
||||
/**
|
||||
* Returns a ref that always holds the latest value passed to this hook.
|
||||
*/
|
||||
export const useLatest = <T,>(t: T): { current: T } => {
|
||||
const latest = useRef(t)
|
||||
latest.current = t
|
||||
return latest
|
||||
}
|
||||
42
src/video-grid/FakeVideoTile.module.css
Normal file
42
src/video-grid/FakeVideoTile.module.css
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.tile {
|
||||
background-color: var(--system);
|
||||
--tileRadius: 20px;
|
||||
border-radius: var(--tileRadius);
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.speaking {
|
||||
border: 4px solid var(--accent);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: var(--font-size-caption);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
55
src/video-grid/FakeVideoTile.tsx
Normal file
55
src/video-grid/FakeVideoTile.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
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 { animated } from "@react-spring/web";
|
||||
import classNames from "classnames";
|
||||
import { ComponentProps, forwardRef, Ref } from "react"
|
||||
import { Avatar } from "../Avatar";
|
||||
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||
import styles from "./FakeVideoTile.module.css";
|
||||
|
||||
|
||||
export interface FakeItemData {
|
||||
type: "fake", // To differentiate from ItemData
|
||||
name: string,
|
||||
screenshare: boolean,
|
||||
speaking: boolean,
|
||||
simulateScreenshare?: () => void
|
||||
simulateSpeaking?: () => void
|
||||
remove: () => void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: FakeItemData
|
||||
className?: string;
|
||||
style?: ComponentProps<typeof animated.div>["style"];
|
||||
}
|
||||
|
||||
export const FakeVideoTile = forwardRef<HTMLElement, Props>(({ data, className, style }, ref) => {
|
||||
return <animated.div className={classNames(className, styles.tile, { [styles.speaking]: data.speaking })} ref={ref as Ref<HTMLDivElement>} style={style}>
|
||||
<Avatar fallback={data.name[0].toUpperCase()} />
|
||||
<div className={styles.name}>
|
||||
{data.screenshare ? `${data.name} (sharing screen)` : data.name}
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
{data.simulateSpeaking && <MicIcon onClick={data.simulateSpeaking} />}
|
||||
{data.simulateScreenshare && <ScreenshareIcon onClick={data.simulateScreenshare} />}
|
||||
<HangupIcon onClick={data.remove} />
|
||||
</div>
|
||||
</animated.div>
|
||||
})
|
||||
23
src/video-grid/TileWrapper.module.css
Normal file
23
src/video-grid/TileWrapper.module.css
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.tile {
|
||||
position: absolute;
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { EventTypes, Handler, useDrag } from "@use-gesture/react";
|
||||
import { SpringValue, to } from "@react-spring/web";
|
||||
|
||||
import { ChildrenProperties } from "./VideoGrid";
|
||||
import styles from "./TileWrapper.module.css"
|
||||
|
||||
interface Props<T> {
|
||||
id: string;
|
||||
@@ -77,6 +78,7 @@ export const TileWrapper = memo(
|
||||
<>
|
||||
{children({
|
||||
ref,
|
||||
className: styles.tile,
|
||||
style: {
|
||||
opacity,
|
||||
scale,
|
||||
|
||||
@@ -802,6 +802,7 @@ interface DragTileData {
|
||||
|
||||
export interface ChildrenProperties<T> {
|
||||
ref: Ref<HTMLElement>;
|
||||
className?: string;
|
||||
style: ComponentProps<typeof animated.div>["style"];
|
||||
/**
|
||||
* The width this tile will have once its animations have settled.
|
||||
|
||||
@@ -15,15 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
.videoTile {
|
||||
position: absolute;
|
||||
contain: strict;
|
||||
top: 0;
|
||||
container-name: videoTile;
|
||||
container-type: size;
|
||||
--tileRadius: 8px;
|
||||
border-radius: var(--tileRadius);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
/* HACK: This has no visual effect due to the short duration, but allows the
|
||||
JS to detect movement via the transform property for audio spatialization */
|
||||
@@ -154,7 +150,7 @@ limitations under the License.
|
||||
.videoMutedOverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #21262c;
|
||||
background-color: var(--system);
|
||||
}
|
||||
|
||||
.presenterLabel {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentProps, forwardRef, useCallback, useEffect } from "react";
|
||||
import { ComponentProps, forwardRef, Ref, useCallback, useEffect } from "react";
|
||||
import { animated } from "@react-spring/web";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -39,6 +39,7 @@ import { useModalTriggerState } from "../Modal";
|
||||
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
||||
|
||||
export interface ItemData {
|
||||
type: "real", // To differentiate from FakeItemData
|
||||
id: string;
|
||||
member?: RoomMember;
|
||||
sfuParticipant: LocalParticipant | RemoteParticipant;
|
||||
@@ -64,7 +65,7 @@ interface Props {
|
||||
showConnectionStats: boolean;
|
||||
}
|
||||
|
||||
export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
export const VideoTile = forwardRef<HTMLElement, Props>(
|
||||
(
|
||||
{
|
||||
data,
|
||||
@@ -159,7 +160,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
[styles.maximised]: maximised,
|
||||
})}
|
||||
style={style}
|
||||
ref={tileRef}
|
||||
ref={tileRef as Ref<HTMLDivElement>}
|
||||
data-testid="videoTile"
|
||||
>
|
||||
{toolbarButtons.length > 0 && (!maximised || fullscreen) && (
|
||||
|
||||
@@ -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);
|
||||
|
||||
45
yarn.lock
45
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"
|
||||
@@ -10333,15 +10309,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 +10451,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"
|
||||
|
||||
Reference in New Issue
Block a user