Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c13040f0b0 | ||
|
|
24a1091954 | ||
|
|
9fd7329554 | ||
|
|
2a19a9964d | ||
|
|
3fc9c1b74a | ||
|
|
f6f0c20b08 | ||
|
|
26a1c165d9 | ||
|
|
2af87fa8b8 | ||
|
|
d34c8d08a4 | ||
|
|
0f687fb8b8 | ||
|
|
603dd3786a | ||
|
|
9fbe4278c2 | ||
|
|
b222b4f708 | ||
|
|
abc2449b07 | ||
|
|
e6459de0d9 | ||
|
|
323505fbb4 | ||
|
|
2b06c6f2e6 | ||
|
|
5a56e46f7b | ||
|
|
abe9ece38f | ||
|
|
cb8d837370 | ||
|
|
500a19d655 | ||
|
|
0d3daf5fa3 | ||
|
|
66aede01dc | ||
|
|
6d7be57dcf | ||
|
|
5b913205af | ||
|
|
fd93d89b26 | ||
|
|
abdfcd879d | ||
|
|
b231424f96 | ||
|
|
b2418d5384 | ||
|
|
f2232a0740 | ||
|
|
04c6d990bd | ||
|
|
455bb09108 | ||
|
|
d8fe617535 | ||
|
|
970568fd17 | ||
|
|
f6677889e0 | ||
|
|
04780ab7aa | ||
|
|
b7df8019f0 | ||
|
|
0a9115248d | ||
|
|
27d492e9e2 | ||
|
|
bc22d36ef8 | ||
|
|
cf9625f33e | ||
|
|
446fd9c7c0 | ||
|
|
adc7892d8c | ||
|
|
f805f4ead6 | ||
|
|
00ffa1b6cd | ||
|
|
055fbe786d | ||
|
|
7a561bd034 | ||
|
|
5fb1f556d5 | ||
|
|
f4ba315cef | ||
|
|
9ba12da544 | ||
|
|
657096fd9a | ||
|
|
9374900ce0 | ||
|
|
7e5610eb36 | ||
|
|
1253638861 | ||
|
|
83feb28909 | ||
|
|
5422cb76f1 | ||
|
|
a6eb52ae76 | ||
|
|
4488947eed | ||
|
|
bf8f164f55 | ||
|
|
5487fbc048 | ||
|
|
a70dbb130f | ||
|
|
7edf544d73 | ||
|
|
ad3bde9920 | ||
|
|
85a98b3706 | ||
|
|
85e3f3761a | ||
|
|
f0b116714b | ||
|
|
dbef06269b | ||
|
|
894815268a | ||
|
|
8ecec0bc7e | ||
|
|
66839e02f6 | ||
|
|
bad8f36bf5 | ||
|
|
f5c50230a9 | ||
|
|
0136fd3cab | ||
|
|
2d18953344 | ||
|
|
d930ab869a | ||
|
|
dbdb82bd74 | ||
|
|
61309bacd9 | ||
|
|
b3e88d33a7 | ||
|
|
73fda641c8 | ||
|
|
be01a4bd81 | ||
|
|
0814e3c905 | ||
|
|
c7dd2e2093 | ||
|
|
cfa525f957 | ||
|
|
43d579744f | ||
|
|
48a008093b | ||
|
|
70c099c4b5 | ||
|
|
363f2340a0 | ||
|
|
3a6346aa63 | ||
|
|
9ef9680e07 | ||
|
|
e3cec93669 | ||
|
|
b6c926d2c8 | ||
|
|
c430ebb3a3 | ||
|
|
ae13814449 | ||
|
|
dc75c1cfb4 | ||
|
|
38f9a79bd3 | ||
|
|
fc1aaf02bf | ||
|
|
c05b6c5118 | ||
|
|
72197c1a0a | ||
|
|
46bcb8ac75 | ||
|
|
2ba1bab82d | ||
|
|
3c56f7f481 | ||
|
|
fcd8a41fc9 | ||
|
|
35f8b1ed85 | ||
|
|
7969e13fc1 | ||
|
|
4d433ab22d | ||
|
|
d7f46607ad | ||
|
|
1e59390599 |
41
.eslintrc.js
Normal file
41
.eslintrc.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
"matrix-org",
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
"plugin:matrix-org/react",
|
||||||
|
"plugin:matrix-org/a11y",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// We break this rule in a few places: dial it back to a warning
|
||||||
|
// (and run with max warnings) to tolerate the existing code
|
||||||
|
"react-hooks/exhaustive-deps": ["warn"],
|
||||||
|
"jsx-a11y/media-has-caption": ["off"],
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"src/**/*.{ts,tsx}",
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
"plugin:matrix-org/typescript",
|
||||||
|
"plugin:matrix-org/react",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
31
.github/workflows/build.yaml
vendored
Normal file
31
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
env:
|
||||||
|
VITE_DEFAULT_HOMESERVER: "https://call.ems.host"
|
||||||
|
VITE_SENTRY_DSN: https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
|
||||||
|
VITE_SENTRY_ENVIRONMENT: main-branch-cd
|
||||||
|
VITE_RAGESHAKE_SUBMIT_URL: https://element.io/bugreports/submit
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Yarn cache
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: "yarn install"
|
||||||
|
- name: Build
|
||||||
|
run: "yarn run build"
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: dist
|
||||||
|
# We'll only use this in a triggered job, then we're done with it
|
||||||
|
retention-days: 1
|
||||||
22
.github/workflows/lint.yaml
vendored
Normal file
22
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Lint, format & type check
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
jobs:
|
||||||
|
prettier:
|
||||||
|
name: Lint, format & type check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Yarn cache
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: "yarn install"
|
||||||
|
- name: Prettier
|
||||||
|
run: "yarn run prettier:check"
|
||||||
|
- name: ESLint
|
||||||
|
run: "yarn run lint:js"
|
||||||
|
- name: Type check
|
||||||
|
run: "yarn run lint:types"
|
||||||
79
.github/workflows/netlify-main.yaml
vendored
Normal file
79
.github/workflows/netlify-main.yaml
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
name: Netlify Main
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
deployments: write
|
||||||
|
if: github.event.workflow_run.conclusion == 'success'
|
||||||
|
steps:
|
||||||
|
- name: Create Deployment
|
||||||
|
uses: bobheadxi/deployments@v1
|
||||||
|
id: deployment
|
||||||
|
with:
|
||||||
|
step: start
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
env: main-branch-cd
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
|
||||||
|
- name: 'Download artifact'
|
||||||
|
uses: actions/github-script@v3.1.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const artifacts = await github.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: ${{ github.event.workflow_run.id }},
|
||||||
|
});
|
||||||
|
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "build"
|
||||||
|
})[0];
|
||||||
|
const download = await github.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: matchArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
const fs = require('fs');
|
||||||
|
fs.writeFileSync('${{github.workspace}}/build.zip', Buffer.from(download.data));
|
||||||
|
|
||||||
|
- name: Extract Artifacts
|
||||||
|
run: unzip -d dist build.zip && rm build.zip
|
||||||
|
|
||||||
|
- name: Add redirects file
|
||||||
|
# We fetch from github directly as we don't bother checking out the repo
|
||||||
|
run: curl -s https://raw.githubusercontent.com/vector-im/element-call/main/config/netlify_redirects > dist/_redirects
|
||||||
|
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
id: netlify
|
||||||
|
uses: nwtgck/actions-netlify@v1.2.3
|
||||||
|
with:
|
||||||
|
publish-dir: dist
|
||||||
|
deploy-message: "Deploy from GitHub Actions"
|
||||||
|
production-branch: main
|
||||||
|
production-deploy: true
|
||||||
|
# These don't work because we're in workflow_run
|
||||||
|
enable-pull-request-comment: false
|
||||||
|
enable-commit-comment: false
|
||||||
|
github-deployment-environment: main
|
||||||
|
env:
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||||
|
timeout-minutes: 1
|
||||||
|
|
||||||
|
- name: Update deployment status
|
||||||
|
uses: bobheadxi/deployments@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
step: finish
|
||||||
|
override: false
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
status: ${{ job.status }}
|
||||||
|
env: ${{ steps.deployment.outputs.env }}
|
||||||
|
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
env_url: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.idea/
|
||||||
|
|||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -18,11 +18,6 @@ module.exports = {
|
|||||||
);
|
);
|
||||||
config.plugins.push(svgrPlugin());
|
config.plugins.push(svgrPlugin());
|
||||||
config.resolve = config.resolve || {};
|
config.resolve = config.resolve || {};
|
||||||
config.resolve.alias = config.resolve.alias || {};
|
|
||||||
config.resolve.alias["$(res)"] = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../node_modules/matrix-react-sdk/res"
|
|
||||||
);
|
|
||||||
config.resolve.dedupe = config.resolve.dedupe || [];
|
config.resolve.dedupe = config.resolve.dedupe || [];
|
||||||
config.resolve.dedupe.push("react", "react-dom", "matrix-js-sdk");
|
config.resolve.dedupe.push("react", "react-dom", "matrix-js-sdk");
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.insertSpaces": true,
|
"editor.insertSpaces": true,
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ FROM node:16-buster as builder
|
|||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src/matrix-video-chat
|
COPY . /src/element-call
|
||||||
RUN matrix-video-chat/scripts/dockerbuild.sh
|
RUN element-call/scripts/dockerbuild.sh
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginxinc/nginx-unprivileged:alpine
|
FROM nginxinc/nginx-unprivileged:alpine
|
||||||
|
|
||||||
COPY --from=builder /src/matrix-video-chat/dist /app
|
COPY --from=builder /src/element-call/dist /app
|
||||||
COPY scripts/default.conf /etc/nginx/conf.d/
|
COPY config/default.conf /etc/nginx/conf.d/
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -1,10 +1,12 @@
|
|||||||
# Matrix Video Chat
|
# Element Call
|
||||||
|
|
||||||
Testbed for full mesh video chat.
|
Showcase for full mesh video chat powered by Matrix, implementing [MSC3401](https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/group-voip/proposals/3401-group-voip.md).
|
||||||
|
|
||||||
|
Discussion in [#webrtc:matrix.org: ](https://matrix.to/#/#webrtc:matrix.org)
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
`matrix-video-chat` is built against the `robertlong/group-call` branch of both [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902) and [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/pull/6848). Because of how these packages are configured and Vite's requirements, you will need to clone them locally and use `yarn link` to stich things together.
|
`element-call` is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902). Because of how this package is configured and Vite's requirements, you will need to clone it locally and use `yarn link` to stich things together.
|
||||||
|
|
||||||
First clone, install, and link `matrix-js-sdk`
|
First clone, install, and link `matrix-js-sdk`
|
||||||
|
|
||||||
@@ -16,30 +18,36 @@ yarn
|
|||||||
yarn link
|
yarn link
|
||||||
```
|
```
|
||||||
|
|
||||||
Then clone, install, link `matrix-js-sdk` into `matrix-react-sdk`, and link `matrix-react-sdk`
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/matrix-org/matrix-react-sdk.git
|
|
||||||
cd matrix-react-sdk
|
|
||||||
git checkout robertlong/group-call
|
|
||||||
yarn
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn link
|
|
||||||
```
|
|
||||||
|
|
||||||
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
|
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
|
||||||
|
|
||||||
Finally we can set up this project.
|
Finally we can set up this project.
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/vector-im/matrix-video-chat.git
|
git clone https://github.com/vector-im/element-call.git
|
||||||
cd matrix-video-chat
|
cd element-call
|
||||||
yarn
|
yarn
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link matrix-react-sdk
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
Configuration options are documented in the `.env` file.
|
Configuration options are documented in the `.env` file.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
All files in this project are:
|
||||||
|
|
||||||
|
Copyright 2021-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.
|
||||||
|
|||||||
4
config/netlify_redirects
Normal file
4
config/netlify_redirects
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# This file is copied to the netlify deploy dir in the upload stage
|
||||||
|
|
||||||
|
# Redirect any unknown path to index.html
|
||||||
|
/* /index.html 200
|
||||||
26
package.json
26
package.json
@@ -5,7 +5,11 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"storybook": "start-storybook -p 6006",
|
"storybook": "start-storybook -p 6006",
|
||||||
"build-storybook": "build-storybook"
|
"build-storybook": "build-storybook",
|
||||||
|
"prettier:check": "prettier -c src",
|
||||||
|
"prettier:format": "prettier -w src",
|
||||||
|
"lint:js": "eslint --max-warnings 2 src",
|
||||||
|
"lint:types": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
@@ -18,6 +22,7 @@
|
|||||||
"@react-aria/tabs": "^3.1.0",
|
"@react-aria/tabs": "^3.1.0",
|
||||||
"@react-aria/tooltip": "^3.1.3",
|
"@react-aria/tooltip": "^3.1.3",
|
||||||
"@react-aria/utils": "^3.10.0",
|
"@react-aria/utils": "^3.10.0",
|
||||||
|
"@react-spring/web": "^9.4.4",
|
||||||
"@react-stately/collections": "^3.3.4",
|
"@react-stately/collections": "^3.3.4",
|
||||||
"@react-stately/overlays": "^3.1.3",
|
"@react-stately/overlays": "^3.1.3",
|
||||||
"@react-stately/select": "^3.1.3",
|
"@react-stately/select": "^3.1.3",
|
||||||
@@ -25,11 +30,12 @@
|
|||||||
"@react-stately/tree": "^3.2.0",
|
"@react-stately/tree": "^3.2.0",
|
||||||
"@sentry/react": "^6.13.3",
|
"@sentry/react": "^6.13.3",
|
||||||
"@sentry/tracing": "^6.13.3",
|
"@sentry/tracing": "^6.13.3",
|
||||||
|
"@use-gesture/react": "^10.2.11",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"color-hash": "^2.0.1",
|
"color-hash": "^2.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
"lodash-move": "^1.1.1",
|
||||||
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#acef1d7dd0b915368730efabee94deb42b2e4058",
|
||||||
"mermaid": "^8.13.8",
|
"mermaid": "^8.13.8",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
@@ -46,10 +52,24 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.5",
|
"@babel/core": "^7.16.5",
|
||||||
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||||
"@storybook/react": "^6.5.0-alpha.5",
|
"@storybook/react": "^6.5.0-alpha.5",
|
||||||
|
"@types/request": "^2.48.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.22.0",
|
||||||
|
"@typescript-eslint/parser": "^5.22.0",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
|
"eslint": "^8.14.0",
|
||||||
|
"eslint-config-google": "^0.14.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
|
"eslint-plugin-matrix-org": "^0.4.0",
|
||||||
|
"eslint-plugin-react": "^7.29.4",
|
||||||
|
"eslint-plugin-react-hooks": "^4.5.0",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
"storybook-builder-vite": "^0.1.12",
|
"storybook-builder-vite": "^0.1.12",
|
||||||
|
"typescript": "^4.6.4",
|
||||||
"vite": "^2.4.2",
|
"vite": "^2.4.2",
|
||||||
"vite-plugin-html-template": "^1.1.0",
|
"vite-plugin-html-template": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^0.4.0"
|
"vite-plugin-svgr": "^0.4.0"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ set -ex
|
|||||||
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
|
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
|
||||||
export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
|
export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
|
||||||
export VITE_RAGESHAKE_SUBMIT_URL=https://element.io/bugreports/submit
|
export VITE_RAGESHAKE_SUBMIT_URL=https://element.io/bugreports/submit
|
||||||
|
export VITE_PRODUCT_NAME="Element Call"
|
||||||
|
|
||||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||||
cd matrix-js-sdk
|
cd matrix-js-sdk
|
||||||
@@ -12,22 +13,11 @@ git checkout robertlong/group-call
|
|||||||
yarn install
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
yarn link
|
yarn link
|
||||||
cd ..
|
|
||||||
|
|
||||||
git clone https://github.com/matrix-org/matrix-react-sdk.git
|
cd ../element-call
|
||||||
cd matrix-react-sdk
|
|
||||||
git checkout robertlong/group-call
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn install
|
|
||||||
yarn run build
|
|
||||||
yarn link
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
cd matrix-video-chat
|
|
||||||
|
|
||||||
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||||
|
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link matrix-react-sdk
|
|
||||||
yarn install
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
|
|||||||
17
src/@types/global.d.ts
vendored
Normal file
17
src/@types/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 "matrix-js-sdk/src/@types/global";
|
||||||
2
src/@types/modules.d.ts
vendored
Normal file
2
src/@types/modules.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import styles from "./Avatar.module.css";
|
import styles from "./Avatar.module.css";
|
||||||
|
|
||||||
const backgroundColors = [
|
const backgroundColors = [
|
||||||
@@ -13,7 +14,7 @@ const backgroundColors = [
|
|||||||
"#74D12C",
|
"#74D12C",
|
||||||
];
|
];
|
||||||
|
|
||||||
function hashStringToArrIndex(str, arrLength) {
|
function hashStringToArrIndex(str: string, arrLength: number) {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
@@ -23,7 +24,16 @@ function hashStringToArrIndex(str, arrLength) {
|
|||||||
return sum % arrLength;
|
return sum % arrLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Avatar({
|
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
bgKey?: string;
|
||||||
|
src: string;
|
||||||
|
fallback: string;
|
||||||
|
size?: number;
|
||||||
|
className: string;
|
||||||
|
style: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Avatar: React.FC<Props> = ({
|
||||||
bgKey,
|
bgKey,
|
||||||
src,
|
src,
|
||||||
fallback,
|
fallback,
|
||||||
@@ -31,7 +41,7 @@ export function Avatar({
|
|||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}) => {
|
||||||
const backgroundColor = useMemo(() => {
|
const backgroundColor = useMemo(() => {
|
||||||
const index = hashStringToArrIndex(
|
const index = hashStringToArrIndex(
|
||||||
bgKey || fallback || src || "",
|
bgKey || fallback || src || "",
|
||||||
@@ -40,6 +50,7 @@ export function Avatar({
|
|||||||
return backgroundColors[index];
|
return backgroundColors[index];
|
||||||
}, [bgKey, src, fallback]);
|
}, [bgKey, src, fallback]);
|
||||||
|
|
||||||
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.avatar, styles[size || "md"], className)}
|
className={classNames(styles.avatar, styles[size || "md"], className)}
|
||||||
@@ -55,4 +66,4 @@ export function Avatar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
@@ -4,35 +4,63 @@ import classNames from "classnames";
|
|||||||
import { Avatar } from "./Avatar";
|
import { Avatar } from "./Avatar";
|
||||||
import { getAvatarUrl } from "./matrix-utils";
|
import { getAvatarUrl } from "./matrix-utils";
|
||||||
|
|
||||||
export function Facepile({ className, client, participants, ...rest }) {
|
const overlapMap = {
|
||||||
|
xs: 2,
|
||||||
|
sm: 4,
|
||||||
|
md: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeMap = {
|
||||||
|
xs: 24,
|
||||||
|
sm: 32,
|
||||||
|
md: 36,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Facepile({
|
||||||
|
className,
|
||||||
|
client,
|
||||||
|
participants,
|
||||||
|
max,
|
||||||
|
size,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const _size = sizeMap[size];
|
||||||
|
const _overlap = overlapMap[size];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.facepile, className)}
|
className={classNames(styles.facepile, styles[size], className)}
|
||||||
title={participants.map((member) => member.name).join(", ")}
|
title={participants.map((member) => member.name).join(", ")}
|
||||||
|
style={{ width: participants.length * (_size - _overlap) + _overlap }}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{participants.slice(0, 3).map((member, i) => {
|
{participants.slice(0, max).map((member, i) => {
|
||||||
const avatarUrl = member.user?.avatarUrl;
|
const avatarUrl = member.user?.avatarUrl;
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
key={member.userId}
|
key={member.userId}
|
||||||
size="xs"
|
size={size}
|
||||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 22)}
|
src={avatarUrl && getAvatarUrl(client, avatarUrl, _size)}
|
||||||
fallback={member.name.slice(0, 1).toUpperCase()}
|
fallback={member.name.slice(0, 1).toUpperCase()}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
style={{ left: i * 22 }}
|
style={{ left: i * (_size - _overlap) }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{participants.length > 3 && (
|
{participants.length > max && (
|
||||||
<Avatar
|
<Avatar
|
||||||
key="additional"
|
key="additional"
|
||||||
size="xs"
|
size={size}
|
||||||
fallback={`+${participants.length - 3}`}
|
fallback={`+${participants.length - max}`}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
style={{ left: 3 * 22 }}
|
style={{ left: max * (_size - _overlap) }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Facepile.defaultProps = {
|
||||||
|
max: 3,
|
||||||
|
size: "xs",
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
.facepile {
|
.facepile {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 24px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.facepile.xs {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facepile.sm {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facepile.md {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
.facepile .avatar {
|
.facepile .avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
border: 1px solid var(--bgColor2);
|
border: 1px solid var(--bgColor2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.facepile.md .avatar {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 {
|
import {
|
||||||
uniqueNamesGenerator,
|
uniqueNamesGenerator,
|
||||||
adjectives,
|
adjectives,
|
||||||
colors,
|
colors,
|
||||||
animals,
|
animals,
|
||||||
|
Config,
|
||||||
} from "unique-names-generator";
|
} from "unique-names-generator";
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
@@ -126,7 +143,7 @@ const elements = [
|
|||||||
"oganesson",
|
"oganesson",
|
||||||
];
|
];
|
||||||
|
|
||||||
export function generateRandomName(config) {
|
export function generateRandomName(config: Config): string {
|
||||||
return uniqueNamesGenerator({
|
return uniqueNamesGenerator({
|
||||||
dictionaries: [colors, adjectives, animals, elements],
|
dictionaries: [colors, adjectives, animals, elements],
|
||||||
style: "lowerCase",
|
style: "lowerCase",
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
@@ -7,39 +23,42 @@ export function useInteractiveLogin() {
|
|||||||
const { setClient } = useClient();
|
const { setClient } = useClient();
|
||||||
const [state, setState] = useState({ loading: false });
|
const [state, setState] = useState({ loading: false });
|
||||||
|
|
||||||
const auth = useCallback(async (homeserver, username, password) => {
|
const auth = useCallback(
|
||||||
const authClient = matrix.createClient(homeserver);
|
async (homeserver, username, password) => {
|
||||||
|
const authClient = matrix.createClient(homeserver);
|
||||||
|
|
||||||
const interactiveAuth = new InteractiveAuth({
|
const interactiveAuth = new InteractiveAuth({
|
||||||
matrixClient: authClient,
|
matrixClient: authClient,
|
||||||
busyChanged(loading) {
|
busyChanged(loading) {
|
||||||
setState((prev) => ({ ...prev, loading }));
|
setState((prev) => ({ ...prev, loading }));
|
||||||
},
|
},
|
||||||
async doRequest(_auth, _background) {
|
async doRequest(_auth, _background) {
|
||||||
return authClient.login("m.login.password", {
|
return authClient.login("m.login.password", {
|
||||||
identifier: {
|
identifier: {
|
||||||
type: "m.id.user",
|
type: "m.id.user",
|
||||||
user: username,
|
user: username,
|
||||||
},
|
},
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { user_id, access_token, device_id } =
|
const { user_id, access_token, device_id } =
|
||||||
await interactiveAuth.attemptAuth();
|
await interactiveAuth.attemptAuth();
|
||||||
|
|
||||||
const client = await initClient({
|
const client = await initClient({
|
||||||
baseUrl: defaultHomeserver,
|
baseUrl: defaultHomeserver,
|
||||||
accessToken: access_token,
|
accessToken: access_token,
|
||||||
userId: user_id,
|
userId: user_id,
|
||||||
deviceId: device_id,
|
deviceId: device_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
setClient(client, { user_id, access_token, device_id });
|
setClient(client, { user_id, access_token, device_id });
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}, []);
|
},
|
||||||
|
[setClient]
|
||||||
|
);
|
||||||
|
|
||||||
return [state, auth];
|
return [state, auth];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
@@ -89,7 +105,7 @@ export function useInteractiveRegistration() {
|
|||||||
|
|
||||||
return client;
|
return client;
|
||||||
},
|
},
|
||||||
[]
|
[setClient]
|
||||||
);
|
);
|
||||||
|
|
||||||
return [state, register];
|
return [state, register];
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { useEffect, useCallback, useRef, useState } from "react";
|
import { useEffect, useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
@@ -7,6 +23,8 @@ import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
|||||||
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
|
||||||
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
|
||||||
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
|
||||||
|
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
|
||||||
|
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
|
||||||
import { useButton } from "@react-aria/button";
|
import { useButton } from "@react-aria/button";
|
||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
import { TooltipTrigger } from "../Tooltip";
|
import { TooltipTrigger } from "../Tooltip";
|
||||||
@@ -20,6 +38,7 @@ export const variantToClassName = {
|
|||||||
copy: [styles.copyButton],
|
copy: [styles.copyButton],
|
||||||
iconCopy: [styles.iconCopyButton],
|
iconCopy: [styles.iconCopyButton],
|
||||||
secondaryCopy: [styles.copyButton],
|
secondaryCopy: [styles.copyButton],
|
||||||
|
secondaryHangup: [styles.secondaryHangup],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sizeToClassName = {
|
export const sizeToClassName = {
|
||||||
@@ -126,3 +145,25 @@ export function HangupButton({ className, ...rest }) {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SettingsButton({ className, ...rest }) {
|
||||||
|
return (
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button variant="toolbar" {...rest}>
|
||||||
|
<SettingsIcon />
|
||||||
|
</Button>
|
||||||
|
{() => "Settings"}
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InviteButton({ className, ...rest }) {
|
||||||
|
return (
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button variant="toolbar" {...rest}>
|
||||||
|
<AddUserIcon />
|
||||||
|
</Button>
|
||||||
|
{() => "Invite"}
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ limitations under the License.
|
|||||||
.iconButton,
|
.iconButton,
|
||||||
.iconCopyButton,
|
.iconCopyButton,
|
||||||
.secondary,
|
.secondary,
|
||||||
|
.secondaryHangup,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -34,6 +35,7 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.secondary,
|
.secondary,
|
||||||
|
.secondaryHangup,
|
||||||
.button,
|
.button,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
padding: 7px 15px;
|
padding: 7px 15px;
|
||||||
@@ -53,6 +55,7 @@ limitations under the License.
|
|||||||
.iconButton:focus,
|
.iconButton:focus,
|
||||||
.iconCopyButton:focus,
|
.iconCopyButton:focus,
|
||||||
.secondary:focus,
|
.secondary:focus,
|
||||||
|
.secondaryHangup:focus,
|
||||||
.copyButton:focus {
|
.copyButton:focus {
|
||||||
outline: auto;
|
outline: auto;
|
||||||
}
|
}
|
||||||
@@ -119,6 +122,12 @@ limitations under the License.
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondaryHangup {
|
||||||
|
color: #ff5b55;
|
||||||
|
border: 2px solid #ff5b55;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.copyButton.secondaryCopy {
|
.copyButton.secondaryCopy {
|
||||||
color: var(--textColor1);
|
color: var(--textColor1);
|
||||||
border-color: var(--textColor1);
|
border-color: var(--textColor1);
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import useClipboard from "react-use-clipboard";
|
import useClipboard from "react-use-clipboard";
|
||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
export * from "./Button";
|
export * from "./Button";
|
||||||
export * from "./CopyButton";
|
export * from "./CopyButton";
|
||||||
export * from "./LinkButton";
|
export * from "./LinkButton";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import styles from "./Form.module.css";
|
import styles from "./Form.module.css";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useState, useCallback } from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
import { createRoom, roomAliasFromRoomName } from "../matrix-utils";
|
import { createRoom, roomAliasFromRoomName } from "../matrix-utils";
|
||||||
import { useGroupCallRooms } from "./useGroupCallRooms";
|
import { useGroupCallRooms } from "./useGroupCallRooms";
|
||||||
@@ -13,22 +29,25 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
|||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Headline, Title } from "../typography/Typography";
|
import { Headline, Title } from "../typography/Typography";
|
||||||
import { Form } from "../form/Form";
|
import { Form } from "../form/Form";
|
||||||
|
import { useShouldShowPtt } from "../useShouldShowPtt";
|
||||||
|
|
||||||
export function RegisteredView({ client }) {
|
export function RegisteredView({ client }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const shouldShowPtt = useShouldShowPtt();
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target);
|
||||||
const roomName = data.get("callName");
|
const roomName = data.get("callName");
|
||||||
|
const ptt = data.get("ptt") !== null;
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const roomIdOrAlias = await createRoom(client, roomName);
|
const roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
if (roomIdOrAlias) {
|
||||||
history.push(`/room/${roomIdOrAlias}`);
|
history.push(`/room/${roomIdOrAlias}`);
|
||||||
@@ -87,6 +106,7 @@ export function RegisteredView({ client }) {
|
|||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -96,6 +116,16 @@ export function RegisteredView({ client }) {
|
|||||||
{loading ? "Loading..." : "Go"}
|
{loading ? "Loading..." : "Go"}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
{shouldShowPtt && (
|
||||||
|
<FieldRow className={styles.fieldRow}>
|
||||||
|
<InputField
|
||||||
|
id="ptt"
|
||||||
|
name="ptt"
|
||||||
|
label="Push to Talk"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow className={styles.fieldRow}>
|
<FieldRow className={styles.fieldRow}>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fieldRow {
|
.fieldRow {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldRow:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
@@ -15,8 +31,10 @@ import { Form } from "../form/Form";
|
|||||||
import styles from "./UnauthenticatedView.module.css";
|
import styles from "./UnauthenticatedView.module.css";
|
||||||
import commonStyles from "./common.module.css";
|
import commonStyles from "./common.module.css";
|
||||||
import { generateRandomName } from "../auth/generateRandomName";
|
import { generateRandomName } from "../auth/generateRandomName";
|
||||||
|
import { useShouldShowPtt } from "../useShouldShowPtt";
|
||||||
|
|
||||||
export function UnauthenticatedView() {
|
export function UnauthenticatedView() {
|
||||||
|
const shouldShowPtt = useShouldShowPtt();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
const [{ privacyPolicyUrl, recaptchaKey }, register] =
|
const [{ privacyPolicyUrl, recaptchaKey }, register] =
|
||||||
@@ -28,6 +46,7 @@ export function UnauthenticatedView() {
|
|||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target);
|
||||||
const roomName = data.get("callName");
|
const roomName = data.get("callName");
|
||||||
const displayName = data.get("displayName");
|
const displayName = data.get("displayName");
|
||||||
|
const ptt = data.get("ptt") !== null;
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
@@ -41,7 +60,7 @@ export function UnauthenticatedView() {
|
|||||||
recaptchaResponse,
|
recaptchaResponse,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
const roomIdOrAlias = await createRoom(client, roomName);
|
const roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
if (roomIdOrAlias) {
|
||||||
history.push(`/room/${roomIdOrAlias}`);
|
history.push(`/room/${roomIdOrAlias}`);
|
||||||
@@ -111,6 +130,16 @@ export function UnauthenticatedView() {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
{shouldShowPtt && (
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="ptt"
|
||||||
|
name="ptt"
|
||||||
|
label="Push to Talk"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
)}
|
||||||
<Caption>
|
<Caption>
|
||||||
By clicking "Go", you agree to our{" "}
|
By clicking "Go", you agree to our{" "}
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
const tsCache = {};
|
const tsCache = {};
|
||||||
@@ -81,7 +97,7 @@ export function useGroupCallRooms(client) {
|
|||||||
client.removeListener("GroupCall.incoming", updateRooms);
|
client.removeListener("GroupCall.incoming", updateRooms);
|
||||||
client.removeListener("GroupCall.participants", updateRooms);
|
client.removeListener("GroupCall.participants", updateRooms);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [client]);
|
||||||
|
|
||||||
return rooms;
|
return rooms;
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/icons/MicMuted.svg
Normal file
5
src/icons/MicMuted.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.9206 1.0544C1.68141 0.815201 1.29359 0.815201 1.0544 1.0544C0.815201 1.29359 0.815201 1.68141 1.0544 1.9206L4.55 5.41621V7C4.55 8.3531 5.6469 9.45 7 9.45C7.45436 9.45 7.87983 9.32632 8.24458 9.11079L9.12938 9.99558C8.52863 10.4234 7.7937 10.675 7 10.675C4.97035 10.675 3.325 9.02965 3.325 7C3.325 6.66173 3.05077 6.3875 2.7125 6.3875C2.37423 6.3875 2.1 6.66173 2.1 7C2.1 9.49877 3.97038 11.5607 6.3875 11.8621V12.5125C6.3875 12.8508 6.66173 13.125 7 13.125C7.33827 13.125 7.6125 12.8508 7.6125 12.5125V11.8621C8.50718 11.7505 9.32696 11.3978 10.0047 10.8709L12.0794 12.9456C12.3186 13.1848 12.7064 13.1848 12.9456 12.9456C13.1848 12.7064 13.1848 12.3186 12.9456 12.0794L1.9206 1.0544Z" fill="white"/>
|
||||||
|
<path d="M10.5474 7.96338L11.5073 8.92525C11.7601 8.33424 11.9 7.68346 11.9 7C11.9 6.66173 11.6258 6.3875 11.2875 6.3875C10.9492 6.3875 10.675 6.66173 10.675 7C10.675 7.33336 10.6306 7.65634 10.5474 7.96338Z" fill="white"/>
|
||||||
|
<path d="M4.81385 2.21784L9.45 6.86366V3.325C9.45 1.9719 8.3531 0.875 7 0.875C6.04532 0.875 5.21818 1.42104 4.81385 2.21784Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
6
src/icons/VideoMuted.svg
Normal file
6
src/icons/VideoMuted.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.20333 0.963373C0.474437 0.690007 0.913989 0.690007 1.1851 0.963373L11.5983 11.4633C11.8694 11.7367 11.8694 12.1799 11.5983 12.4533C11.3272 12.7267 10.8876 12.7267 10.6165 12.4533L0.20333 1.95332C-0.0677768 1.67995 -0.0677768 1.23674 0.20333 0.963373Z" fill="white"/>
|
||||||
|
<path d="M0.418261 3.63429C0.226267 3.95219 0.115674 4.32557 0.115674 4.725V9.85832C0.115674 11.0181 1.0481 11.9583 2.19831 11.9583H8.65411L0.447396 3.66596C0.437225 3.65568 0.427513 3.64511 0.418261 3.63429Z" fill="white"/>
|
||||||
|
<path d="M9.95036 4.725V8.33212L4.30219 2.625H7.86772C9.01793 2.625 9.95036 3.5652 9.95036 4.725Z" fill="white"/>
|
||||||
|
<path d="M12.8721 4.11817L11.1074 5.54167V9.04166L12.8721 10.4652C13.3266 10.8318 14 10.5055 14 9.91855V4.66478C14 4.07782 13.3266 3.7515 12.8721 4.11817Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 892 B |
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { useObjectRef } from "@react-aria/utils";
|
import { useObjectRef } from "@react-aria/utils";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styles from "./Input.module.css";
|
import styles from "./Input.module.css";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { HiddenSelect, useSelect } from "@react-aria/select";
|
import { HiddenSelect, useSelect } from "@react-aria/select";
|
||||||
import { useButton } from "@react-aria/button";
|
import { useButton } from "@react-aria/button";
|
||||||
|
|||||||
57
src/input/Toggle.jsx
Normal file
57
src/input/Toggle.jsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useRef } from "react";
|
||||||
|
import styles from "./Toggle.module.css";
|
||||||
|
import { useToggleButton } from "@react-aria/button";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Field } from "./Input";
|
||||||
|
|
||||||
|
export function Toggle({ id, label, className, onChange, isSelected }) {
|
||||||
|
const buttonRef = useRef();
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
onChange(!isSelected);
|
||||||
|
});
|
||||||
|
const { buttonProps } = useToggleButton(
|
||||||
|
{ isSelected },
|
||||||
|
{ toggle },
|
||||||
|
buttonRef
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
className={classNames(
|
||||||
|
styles.toggle,
|
||||||
|
{ [styles.on]: isSelected },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
{...buttonProps}
|
||||||
|
ref={buttonRef}
|
||||||
|
id={id}
|
||||||
|
className={classNames(styles.button, {
|
||||||
|
[styles.isPressed]: isSelected,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={styles.ball} />
|
||||||
|
</button>
|
||||||
|
<label className={styles.label} htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/input/Toggle.module.css
Normal file
46
src/input/Toggle.module.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.toggle {
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
transition: background-color 0.2s ease-out 0.1s;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 21px;
|
||||||
|
background-color: #6f7882;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball {
|
||||||
|
transition: left 0.15s ease-out 0.1s;
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 21px;
|
||||||
|
background-color: #15191e;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 10px 8px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #6f7882;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .button {
|
||||||
|
background-color: #0dbd8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .ball {
|
||||||
|
left: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .label {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
@@ -22,10 +22,10 @@ import App from "./App";
|
|||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { Integrations } from "@sentry/tracing";
|
import { Integrations } from "@sentry/tracing";
|
||||||
import { ErrorView } from "./FullScreenView";
|
import { ErrorView } from "./FullScreenView";
|
||||||
import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
|
import { init as initRageshake } from "./settings/rageshake";
|
||||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||||
|
|
||||||
rageshake.init();
|
initRageshake();
|
||||||
|
|
||||||
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ const history = createBrowserHistory();
|
|||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||||
|
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? "production",
|
||||||
integrations: [
|
integrations: [
|
||||||
new Integrations.BrowserTracing({
|
new Integrations.BrowserTracing({
|
||||||
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
|
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function isLocalRoomId(roomId) {
|
|||||||
return parts[1] === defaultHomeserverHost;
|
return parts[1] === defaultHomeserverHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createRoom(client, name) {
|
export async function createRoom(client, name, isPtt = false) {
|
||||||
const { room_id, room_alias } = await client.createRoom({
|
const { room_id, room_alias } = await client.createRoom({
|
||||||
visibility: "private",
|
visibility: "private",
|
||||||
preset: "public_chat",
|
preset: "public_chat",
|
||||||
@@ -107,9 +107,12 @@ export async function createRoom(client, name) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log({ isPtt });
|
||||||
|
|
||||||
await client.createGroupCall(
|
await client.createGroupCall(
|
||||||
room_id,
|
room_id,
|
||||||
GroupCallType.Video,
|
isPtt ? GroupCallType.Voice : GroupCallType.Video,
|
||||||
|
isPtt,
|
||||||
GroupCallIntent.Prompt
|
GroupCallIntent.Prompt
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { forwardRef, useRef } from "react";
|
import React, { forwardRef, useRef } from "react";
|
||||||
import { DismissButton, useOverlay } from "@react-aria/overlays";
|
import { DismissButton, useOverlay } from "@react-aria/overlays";
|
||||||
import { FocusScope } from "@react-aria/focus";
|
import { FocusScope } from "@react-aria/focus";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { forwardRef, useRef } from "react";
|
import React, { forwardRef, useRef } from "react";
|
||||||
import styles from "./PopoverMenu.module.css";
|
import styles from "./PopoverMenu.module.css";
|
||||||
import { useMenuTriggerState } from "@react-stately/menu";
|
import { useMenuTriggerState } from "@react-stately/menu";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useProfile } from "./useProfile";
|
import { useProfile } from "./useProfile";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import { getAvatarUrl } from "../matrix-utils";
|
import { getAvatarUrl } from "../matrix-utils";
|
||||||
|
|
||||||
|
|||||||
77
src/room/AudioPreview.jsx
Normal file
77
src/room/AudioPreview.jsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
||||||
|
import styles from "./AudioPreview.module.css";
|
||||||
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { SelectInput } from "../input/SelectInput";
|
||||||
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
|
export function AudioPreview({
|
||||||
|
state,
|
||||||
|
roomName,
|
||||||
|
audioInput,
|
||||||
|
audioInputs,
|
||||||
|
setAudioInput,
|
||||||
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>{`${roomName} - Radio Call`}</h1>
|
||||||
|
<div className={styles.preview}>
|
||||||
|
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
|
||||||
|
Microphone permissions needed to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
|
||||||
|
Accept microphone permissions to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||||
|
<>
|
||||||
|
<SelectInput
|
||||||
|
label="Microphone"
|
||||||
|
selectedKey={audioInput}
|
||||||
|
onSelectionChange={setAudioInput}
|
||||||
|
className={styles.inputField}
|
||||||
|
>
|
||||||
|
{audioInputs.map(({ deviceId, label }) => (
|
||||||
|
<Item key={deviceId}>{label}</Item>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
{audioOutputs.length > 0 && (
|
||||||
|
<SelectInput
|
||||||
|
label="Speaker"
|
||||||
|
selectedKey={audioOutput}
|
||||||
|
onSelectionChange={setAudioOutput}
|
||||||
|
className={styles.inputField}
|
||||||
|
>
|
||||||
|
{audioOutputs.map(({ deviceId, label }) => (
|
||||||
|
<Item key={deviceId}>{label}</Item>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/room/AudioPreview.module.css
Normal file
27
src/room/AudioPreview.module.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
.preview {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 24px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
max-width: 414px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphonePermissions {
|
||||||
|
margin: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.preview {
|
||||||
|
margin-top: 40px;
|
||||||
|
background-color: #21262c;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import styles from "./CallEndedView.module.css";
|
import styles from "./CallEndedView.module.css";
|
||||||
import { LinkButton } from "../button";
|
import { LinkButton } from "../button";
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { useSubmitRageshake, useRageshakeRequest } from "../settings/rageshake";
|
import {
|
||||||
|
useSubmitRageshake,
|
||||||
|
useRageshakeRequest,
|
||||||
|
} from "../settings/submit-rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { Resizable } from "re-resizable";
|
import { Resizable } from "re-resizable";
|
||||||
import React, {
|
import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
|
import { useGroupCall } from "./useGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
import { InCallView } from "./InCallView";
|
import { InCallView } from "./InCallView";
|
||||||
|
import { PTTCallView } from "./PTTCallView";
|
||||||
import { CallEndedView } from "./CallEndedView";
|
import { CallEndedView } from "./CallEndedView";
|
||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
@@ -14,7 +31,6 @@ export function GroupCallView({
|
|||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
roomId,
|
roomId,
|
||||||
groupCall,
|
groupCall,
|
||||||
simpleGrid,
|
|
||||||
}) {
|
}) {
|
||||||
const [showInspector, setShowInspector] = useState(
|
const [showInspector, setShowInspector] = useState(
|
||||||
() => !!localStorage.getItem("matrix-group-call-inspector")
|
() => !!localStorage.getItem("matrix-group-call-inspector")
|
||||||
@@ -48,6 +64,7 @@ export function GroupCallView({
|
|||||||
localScreenshareFeed,
|
localScreenshareFeed,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
hasLocalParticipant,
|
hasLocalParticipant,
|
||||||
|
participants,
|
||||||
} = useGroupCall(groupCall);
|
} = useGroupCall(groupCall);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,28 +90,43 @@ export function GroupCallView({
|
|||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorView error={error} />;
|
return <ErrorView error={error} />;
|
||||||
} else if (state === GroupCallState.Entered) {
|
} else if (state === GroupCallState.Entered) {
|
||||||
return (
|
if (groupCall.isPtt) {
|
||||||
<InCallView
|
return (
|
||||||
groupCall={groupCall}
|
<PTTCallView
|
||||||
client={client}
|
client={client}
|
||||||
roomName={groupCall.room.name}
|
roomId={roomId}
|
||||||
microphoneMuted={microphoneMuted}
|
roomName={groupCall.room.name}
|
||||||
localVideoMuted={localVideoMuted}
|
groupCall={groupCall}
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
participants={participants}
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
userMediaFeeds={userMediaFeeds}
|
||||||
userMediaFeeds={userMediaFeeds}
|
onLeave={onLeave}
|
||||||
activeSpeaker={activeSpeaker}
|
setShowInspector={onChangeShowInspector}
|
||||||
onLeave={onLeave}
|
showInspector={showInspector}
|
||||||
toggleScreensharing={toggleScreensharing}
|
/>
|
||||||
isScreensharing={isScreensharing}
|
);
|
||||||
localScreenshareFeed={localScreenshareFeed}
|
} else {
|
||||||
screenshareFeeds={screenshareFeeds}
|
return (
|
||||||
simpleGrid={simpleGrid}
|
<InCallView
|
||||||
setShowInspector={onChangeShowInspector}
|
groupCall={groupCall}
|
||||||
showInspector={showInspector}
|
client={client}
|
||||||
roomId={roomId}
|
roomName={groupCall.room.name}
|
||||||
/>
|
microphoneMuted={microphoneMuted}
|
||||||
);
|
localVideoMuted={localVideoMuted}
|
||||||
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
|
userMediaFeeds={userMediaFeeds}
|
||||||
|
activeSpeaker={activeSpeaker}
|
||||||
|
onLeave={onLeave}
|
||||||
|
toggleScreensharing={toggleScreensharing}
|
||||||
|
isScreensharing={isScreensharing}
|
||||||
|
localScreenshareFeed={localScreenshareFeed}
|
||||||
|
screenshareFeeds={screenshareFeeds}
|
||||||
|
setShowInspector={onChangeShowInspector}
|
||||||
|
showInspector={showInspector}
|
||||||
|
roomId={roomId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (state === GroupCallState.Entering) {
|
} else if (state === GroupCallState.Entering) {
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
@@ -107,6 +139,7 @@ export function GroupCallView({
|
|||||||
return (
|
return (
|
||||||
<LobbyView
|
<LobbyView
|
||||||
client={client}
|
client={client}
|
||||||
|
groupCall={groupCall}
|
||||||
hasLocalParticipant={hasLocalParticipant}
|
hasLocalParticipant={hasLocalParticipant}
|
||||||
roomName={groupCall.room.name}
|
roomName={groupCall.room.name}
|
||||||
state={state}
|
state={state}
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import styles from "./InCallView.module.css";
|
import styles from "./InCallView.module.css";
|
||||||
import {
|
import {
|
||||||
@@ -7,19 +23,15 @@ import {
|
|||||||
ScreenshareButton,
|
ScreenshareButton,
|
||||||
} from "../button";
|
} from "../button";
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import VideoGrid, {
|
import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
|
||||||
useVideoGridLayout,
|
import { VideoTileContainer } from "../video-grid/VideoTileContainer";
|
||||||
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
|
|
||||||
import { VideoTileContainer } from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoTileContainer";
|
|
||||||
import SimpleVideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/SimpleVideoGrid";
|
|
||||||
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
|
|
||||||
import { getAvatarUrl } from "../matrix-utils";
|
import { getAvatarUrl } from "../matrix-utils";
|
||||||
import { GroupCallInspector } from "./GroupCallInspector";
|
import { GroupCallInspector } from "./GroupCallInspector";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { useRageshakeRequestModal } from "../settings/rageshake";
|
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||||
import { usePreventScroll } from "@react-aria/overlays";
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
@@ -44,7 +56,6 @@ export function InCallView({
|
|||||||
toggleScreensharing,
|
toggleScreensharing,
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
simpleGrid,
|
|
||||||
setShowInspector,
|
setShowInspector,
|
||||||
showInspector,
|
showInspector,
|
||||||
roomId,
|
roomId,
|
||||||
@@ -149,8 +160,6 @@ export function InCallView({
|
|||||||
<div className={styles.centerMessage}>
|
<div className={styles.centerMessage}>
|
||||||
<p>Waiting for other participants...</p>
|
<p>Waiting for other participants...</p>
|
||||||
</div>
|
</div>
|
||||||
) : simpleGrid ? (
|
|
||||||
<SimpleVideoGrid items={items} />
|
|
||||||
) : (
|
) : (
|
||||||
<VideoGrid
|
<VideoGrid
|
||||||
items={items}
|
items={items}
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import styles from "./LobbyView.module.css";
|
import styles from "./LobbyView.module.css";
|
||||||
import { Button, CopyButton, MicButton, VideoButton } from "../button";
|
import { Button, CopyButton } from "../button";
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
|
import { useCallFeed } from "../video-grid/useCallFeed";
|
||||||
import { useMediaStream } from "matrix-react-sdk/src/hooks/useMediaStream";
|
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
import { Avatar } from "../Avatar";
|
|
||||||
import { useProfile } from "../profile/useProfile";
|
|
||||||
import useMeasure from "react-use-measure";
|
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
import { VideoPreview } from "./VideoPreview";
|
||||||
|
import { AudioPreview } from "./AudioPreview";
|
||||||
|
|
||||||
export function LobbyView({
|
export function LobbyView({
|
||||||
client,
|
client,
|
||||||
|
groupCall,
|
||||||
roomName,
|
roomName,
|
||||||
state,
|
state,
|
||||||
onInitLocalCallFeed,
|
onInitLocalCallFeed,
|
||||||
@@ -32,11 +45,14 @@ export function LobbyView({
|
|||||||
roomId,
|
roomId,
|
||||||
}) {
|
}) {
|
||||||
const { stream } = useCallFeed(localCallFeed);
|
const { stream } = useCallFeed(localCallFeed);
|
||||||
const { audioOutput } = useMediaHandler();
|
const {
|
||||||
const videoRef = useMediaStream(stream, audioOutput, true);
|
audioInput,
|
||||||
const { displayName, avatarUrl } = useProfile(client);
|
audioInputs,
|
||||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
setAudioInput,
|
||||||
const avatarSize = (previewBounds.height - 66) / 2;
|
audioOutput,
|
||||||
|
audioOutputs,
|
||||||
|
setAudioOutput,
|
||||||
|
} = useMediaHandler();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onInitLocalCallFeed();
|
onInitLocalCallFeed();
|
||||||
@@ -64,53 +80,31 @@ export function LobbyView({
|
|||||||
</Header>
|
</Header>
|
||||||
<div className={styles.joinRoom}>
|
<div className={styles.joinRoom}>
|
||||||
<div className={styles.joinRoomContent}>
|
<div className={styles.joinRoomContent}>
|
||||||
<div className={styles.preview} ref={previewRef}>
|
{groupCall.isPtt ? (
|
||||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
<AudioPreview
|
||||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
roomName={roomName}
|
||||||
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
state={state}
|
||||||
Webcam/microphone permissions needed to join the call.
|
audioInput={audioInput}
|
||||||
</Body>
|
audioInputs={audioInputs}
|
||||||
)}
|
setAudioInput={setAudioInput}
|
||||||
{state === GroupCallState.InitializingLocalCallFeed && (
|
audioOutput={audioOutput}
|
||||||
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
audioOutputs={audioOutputs}
|
||||||
Accept webcam/microphone permissions to join the call.
|
setAudioOutput={setAudioOutput}
|
||||||
</Body>
|
/>
|
||||||
)}
|
) : (
|
||||||
{state === GroupCallState.LocalCallFeedInitialized && (
|
<VideoPreview
|
||||||
<>
|
state={state}
|
||||||
{localVideoMuted && (
|
client={client}
|
||||||
<div className={styles.avatarContainer}>
|
microphoneMuted={microphoneMuted}
|
||||||
<Avatar
|
localVideoMuted={localVideoMuted}
|
||||||
style={{
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
width: avatarSize,
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
height: avatarSize,
|
setShowInspector={setShowInspector}
|
||||||
borderRadius: avatarSize,
|
showInspector={showInspector}
|
||||||
fontSize: Math.round(avatarSize / 2),
|
stream={stream}
|
||||||
}}
|
audioOutput={audioOutput}
|
||||||
src={avatarUrl}
|
/>
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.previewButtons}>
|
|
||||||
<MicButton
|
|
||||||
muted={microphoneMuted}
|
|
||||||
onPress={toggleMicrophoneMuted}
|
|
||||||
/>
|
|
||||||
<VideoButton
|
|
||||||
muted={localVideoMuted}
|
|
||||||
onPress={toggleLocalVideoMuted}
|
|
||||||
/>
|
|
||||||
<OverflowMenu
|
|
||||||
roomId={roomId}
|
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
client={client}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
ref={joinCallButtonRef}
|
ref={joinCallButtonRef}
|
||||||
className={styles.copyButton}
|
className={styles.copyButton}
|
||||||
|
|||||||
@@ -46,58 +46,6 @@ limitations under the License.
|
|||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
|
||||||
position: relative;
|
|
||||||
min-height: 280px;
|
|
||||||
height: 50vh;
|
|
||||||
border-radius: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--bgColor3);
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview video {
|
|
||||||
width: calc(100% + 1px);
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
background-color: black;
|
|
||||||
/* transform scale doesn't perfectly match width, so make -1.01 border issues */
|
|
||||||
transform: scaleX(-1.01);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatarContainer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 66px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--bgColor3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.webcamPermissions {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButtons {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 66px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(23, 25, 28, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.joinCallButton {
|
.joinCallButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -118,17 +66,3 @@ limitations under the License.
|
|||||||
.copyButton:last-child {
|
.copyButton:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.previewButtons > * {
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButtons > :last-child {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.preview {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Menu } from "../Menu";
|
import { Menu } from "../Menu";
|
||||||
|
|||||||
25
src/room/PTTButton.module.css
Normal file
25
src/room/PTTButton.module.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.pttButton {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 232px;
|
||||||
|
max-width: 232px;
|
||||||
|
border-radius: 116px;
|
||||||
|
color: ##fff;
|
||||||
|
border: 6px solid #0dbd8b;
|
||||||
|
background-color: #21262c;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.talking {
|
||||||
|
background-color: #0dbd8b;
|
||||||
|
box-shadow: 0px 0px 0px 17px rgba(13, 189, 139, 0.2),
|
||||||
|
0px 0px 0px 34px rgba(13, 189, 139, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #ff5b55;
|
||||||
|
border-color: #ff5b55;
|
||||||
|
box-shadow: 0px 0px 0px 17px rgba(255, 91, 85, 0.2),
|
||||||
|
0px 0px 0px 34px rgba(255, 91, 85, 0.2);
|
||||||
|
}
|
||||||
164
src/room/PTTButton.tsx
Normal file
164
src/room/PTTButton.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useEffect, useState, createRef } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import styles from "./PTTButton.module.css";
|
||||||
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||||
|
import { Avatar } from "../Avatar";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showTalkOverError: boolean;
|
||||||
|
activeSpeakerUserId: string;
|
||||||
|
activeSpeakerDisplayName: string;
|
||||||
|
activeSpeakerAvatarUrl: string;
|
||||||
|
activeSpeakerIsLocalUser: boolean;
|
||||||
|
size: number;
|
||||||
|
startTalking: () => void;
|
||||||
|
stopTalking: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isHeld: boolean;
|
||||||
|
// If the button is being pressed by touch, the ID of that touch
|
||||||
|
activeTouchID: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PTTButton: React.FC<Props> = ({
|
||||||
|
showTalkOverError,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
activeSpeakerDisplayName,
|
||||||
|
activeSpeakerAvatarUrl,
|
||||||
|
activeSpeakerIsLocalUser,
|
||||||
|
size,
|
||||||
|
startTalking,
|
||||||
|
stopTalking,
|
||||||
|
}) => {
|
||||||
|
const buttonRef = createRef<HTMLButtonElement>();
|
||||||
|
|
||||||
|
const [{ isHeld, activeTouchID }, setState] = useState<State>({
|
||||||
|
isHeld: false,
|
||||||
|
activeTouchID: null,
|
||||||
|
});
|
||||||
|
const onWindowMouseUp = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (isHeld) stopTalking();
|
||||||
|
setState({ isHeld: false, activeTouchID: null });
|
||||||
|
},
|
||||||
|
[isHeld, setState, stopTalking]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onWindowTouchEnd = useCallback(
|
||||||
|
(e: TouchEvent) => {
|
||||||
|
// ignore any ended touches that weren't the one pressing the
|
||||||
|
// button (bafflingly the TouchList isn't an iterable so we
|
||||||
|
// have to do this a really old-school way).
|
||||||
|
let touchFound = false;
|
||||||
|
for (let i = 0; i < e.changedTouches.length; ++i) {
|
||||||
|
if (e.changedTouches.item(i).identifier === activeTouchID) {
|
||||||
|
touchFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!touchFound) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
if (isHeld) stopTalking();
|
||||||
|
setState({ isHeld: false, activeTouchID: null });
|
||||||
|
},
|
||||||
|
[isHeld, activeTouchID, setState, stopTalking]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onButtonMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setState({ isHeld: true, activeTouchID: null });
|
||||||
|
startTalking();
|
||||||
|
},
|
||||||
|
[setState, startTalking]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onButtonTouchStart = useCallback(
|
||||||
|
(e: TouchEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isHeld) return;
|
||||||
|
|
||||||
|
setState({
|
||||||
|
isHeld: true,
|
||||||
|
activeTouchID: e.changedTouches.item(0).identifier,
|
||||||
|
});
|
||||||
|
startTalking();
|
||||||
|
},
|
||||||
|
[isHeld, setState, startTalking]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentButtonElement = buttonRef.current;
|
||||||
|
|
||||||
|
// These listeners go on the window so even if the user's cursor / finger
|
||||||
|
// leaves the button while holding it, the button stays pushed until
|
||||||
|
// they stop clicking / tapping.
|
||||||
|
window.addEventListener("mouseup", onWindowMouseUp);
|
||||||
|
window.addEventListener("touchend", onWindowTouchEnd);
|
||||||
|
// This is a native DOM listener too because we want to preventDefault in it
|
||||||
|
// to stop also getting a click event, so we need it to be non-passive.
|
||||||
|
currentButtonElement.addEventListener("touchstart", onButtonTouchStart, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mouseup", onWindowMouseUp);
|
||||||
|
window.removeEventListener("touchend", onWindowTouchEnd);
|
||||||
|
currentButtonElement.removeEventListener(
|
||||||
|
"touchstart",
|
||||||
|
onButtonTouchStart
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [onWindowMouseUp, onWindowTouchEnd, onButtonTouchStart, buttonRef]);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames(styles.pttButton, {
|
||||||
|
[styles.talking]: activeSpeakerUserId,
|
||||||
|
[styles.error]: showTalkOverError,
|
||||||
|
})}
|
||||||
|
onMouseDown={onButtonMouseDown}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{activeSpeakerIsLocalUser || !activeSpeakerUserId ? (
|
||||||
|
<MicIcon
|
||||||
|
className={styles.micIcon}
|
||||||
|
width={size / 3}
|
||||||
|
height={size / 3}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
key={activeSpeakerUserId}
|
||||||
|
style={{
|
||||||
|
width: size - 12,
|
||||||
|
height: size - 12,
|
||||||
|
borderRadius: size - 12,
|
||||||
|
fontSize: Math.round((size - 12) / 2),
|
||||||
|
}}
|
||||||
|
src={activeSpeakerAvatarUrl}
|
||||||
|
fallback={activeSpeakerDisplayName.slice(0, 1).toUpperCase()}
|
||||||
|
className={styles.avatar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
112
src/room/PTTCallView.module.css
Normal file
112
src/room/PTTCallView.module.css
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
.pttCallView {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.pttCallView {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants > p {
|
||||||
|
color: #a9b2bc;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facepile {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.talkingInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speakerIcon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pttButtonContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionTip {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer > * {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer > :last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.participants {
|
||||||
|
margin-bottom: 67px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.talkingInfo {
|
||||||
|
margin-bottom: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionTip {
|
||||||
|
margin-top: 42px;
|
||||||
|
margin-bottom: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pttButtonContainer {
|
||||||
|
flex: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
flex: auto;
|
||||||
|
order: 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
248
src/room/PTTCallView.tsx
Normal file
248
src/room/PTTCallView.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
||||||
|
import useMeasure from "react-use-measure";
|
||||||
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
|
import { GroupCall, MatrixClient, RoomMember } from "matrix-js-sdk";
|
||||||
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
|
||||||
|
import { useModalTriggerState } from "../Modal";
|
||||||
|
import { SettingsModal } from "../settings/SettingsModal";
|
||||||
|
import { InviteModal } from "./InviteModal";
|
||||||
|
import { HangupButton, InviteButton, SettingsButton } from "../button";
|
||||||
|
import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header";
|
||||||
|
import styles from "./PTTCallView.module.css";
|
||||||
|
import { Facepile } from "../Facepile";
|
||||||
|
import { PTTButton } from "./PTTButton";
|
||||||
|
import { PTTFeed } from "./PTTFeed";
|
||||||
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
import { usePTT } from "./usePTT";
|
||||||
|
import { Timer } from "./Timer";
|
||||||
|
import { Toggle } from "../input/Toggle";
|
||||||
|
import { getAvatarUrl } from "../matrix-utils";
|
||||||
|
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
||||||
|
import { usePTTSounds } from "../sound/usePttSounds";
|
||||||
|
import { PTTClips } from "../sound/PTTClips";
|
||||||
|
|
||||||
|
function getPromptText(
|
||||||
|
showTalkOverError: boolean,
|
||||||
|
pttButtonHeld: boolean,
|
||||||
|
activeSpeakerIsLocalUser: boolean,
|
||||||
|
talkOverEnabled: boolean,
|
||||||
|
activeSpeakerUserId: string,
|
||||||
|
activeSpeakerDisplayName: string
|
||||||
|
): string {
|
||||||
|
const isTouchScreen = Boolean(window.ontouchstart !== undefined);
|
||||||
|
|
||||||
|
if (showTalkOverError) {
|
||||||
|
return "You can't talk at the same time";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pttButtonHeld && activeSpeakerIsLocalUser) {
|
||||||
|
if (isTouchScreen) {
|
||||||
|
return "Release to stop";
|
||||||
|
} else {
|
||||||
|
return "Release spacebar key to stop";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (talkOverEnabled && activeSpeakerUserId && !activeSpeakerIsLocalUser) {
|
||||||
|
if (isTouchScreen) {
|
||||||
|
return `Press and hold to talk over ${activeSpeakerDisplayName}`;
|
||||||
|
} else {
|
||||||
|
return `Press and hold spacebar to talk over ${activeSpeakerDisplayName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTouchScreen) {
|
||||||
|
return "Press and hold to talk";
|
||||||
|
} else {
|
||||||
|
return "Press and hold spacebar to talk";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
client: MatrixClient;
|
||||||
|
roomId: string;
|
||||||
|
roomName: string;
|
||||||
|
groupCall: GroupCall;
|
||||||
|
participants: RoomMember[];
|
||||||
|
userMediaFeeds: CallFeed[];
|
||||||
|
onLeave: () => void;
|
||||||
|
setShowInspector: (boolean) => void;
|
||||||
|
showInspector: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PTTCallView: React.FC<Props> = ({
|
||||||
|
client,
|
||||||
|
roomId,
|
||||||
|
roomName,
|
||||||
|
groupCall,
|
||||||
|
participants,
|
||||||
|
userMediaFeeds,
|
||||||
|
onLeave,
|
||||||
|
setShowInspector,
|
||||||
|
showInspector,
|
||||||
|
}) => {
|
||||||
|
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||||
|
useModalTriggerState();
|
||||||
|
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
||||||
|
useModalTriggerState();
|
||||||
|
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
|
const facepileSize = bounds.width < 800 ? "sm" : "md";
|
||||||
|
const pttButtonSize = 232;
|
||||||
|
const pttBorderWidth = 6;
|
||||||
|
|
||||||
|
const { audioOutput } = useMediaHandler();
|
||||||
|
|
||||||
|
const {
|
||||||
|
startTalkingLocalRef,
|
||||||
|
startTalkingRemoteRef,
|
||||||
|
blockedRef,
|
||||||
|
endTalkingRef,
|
||||||
|
playClip,
|
||||||
|
} = usePTTSounds();
|
||||||
|
|
||||||
|
const {
|
||||||
|
pttButtonHeld,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
setTalkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
startTalking,
|
||||||
|
stopTalking,
|
||||||
|
transmitBlocked,
|
||||||
|
} = usePTT(client, groupCall, userMediaFeeds, playClip);
|
||||||
|
|
||||||
|
const showTalkOverError = pttButtonHeld && transmitBlocked;
|
||||||
|
|
||||||
|
const activeSpeakerIsLocalUser =
|
||||||
|
activeSpeakerUserId && client.getUserId() === activeSpeakerUserId;
|
||||||
|
const activeSpeakerUser = activeSpeakerUserId
|
||||||
|
? client.getUser(activeSpeakerUserId)
|
||||||
|
: null;
|
||||||
|
const activeSpeakerAvatarUrl = activeSpeakerUser
|
||||||
|
? getAvatarUrl(
|
||||||
|
client,
|
||||||
|
activeSpeakerUser.avatarUrl,
|
||||||
|
pttButtonSize - pttBorderWidth * 2
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
const activeSpeakerDisplayName = activeSpeakerUser
|
||||||
|
? activeSpeakerUser.displayName
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.pttCallView} ref={containerRef}>
|
||||||
|
<PTTClips
|
||||||
|
startTalkingLocalRef={startTalkingLocalRef}
|
||||||
|
startTalkingRemoteRef={startTalkingRemoteRef}
|
||||||
|
endTalkingRef={endTalkingRef}
|
||||||
|
blockedRef={blockedRef}
|
||||||
|
/>
|
||||||
|
<Header className={styles.header}>
|
||||||
|
<LeftNav>
|
||||||
|
<RoomSetupHeaderInfo roomName={roomName} onPress={onLeave} />
|
||||||
|
</LeftNav>
|
||||||
|
<RightNav />
|
||||||
|
</Header>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<div className={styles.participants}>
|
||||||
|
<p>{`${participants.length} ${
|
||||||
|
participants.length > 1 ? "people" : "person"
|
||||||
|
} connected`}</p>
|
||||||
|
<Facepile
|
||||||
|
size={facepileSize}
|
||||||
|
max={8}
|
||||||
|
className={styles.facepile}
|
||||||
|
client={client}
|
||||||
|
participants={participants}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<SettingsButton onPress={() => settingsModalState.open()} />
|
||||||
|
<HangupButton onPress={onLeave} />
|
||||||
|
<InviteButton onPress={() => inviteModalState.open()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pttButtonContainer}>
|
||||||
|
{activeSpeakerUserId ? (
|
||||||
|
<div className={styles.talkingInfo}>
|
||||||
|
<h2>
|
||||||
|
{!activeSpeakerIsLocalUser && (
|
||||||
|
<AudioIcon className={styles.speakerIcon} />
|
||||||
|
)}
|
||||||
|
{activeSpeakerIsLocalUser
|
||||||
|
? "Talking..."
|
||||||
|
: `${activeSpeakerDisplayName} is talking...`}
|
||||||
|
</h2>
|
||||||
|
<Timer value={activeSpeakerUserId} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.talkingInfo} />
|
||||||
|
)}
|
||||||
|
<PTTButton
|
||||||
|
showTalkOverError={showTalkOverError}
|
||||||
|
activeSpeakerUserId={activeSpeakerUserId}
|
||||||
|
activeSpeakerDisplayName={activeSpeakerDisplayName}
|
||||||
|
activeSpeakerAvatarUrl={activeSpeakerAvatarUrl}
|
||||||
|
activeSpeakerIsLocalUser={activeSpeakerIsLocalUser}
|
||||||
|
size={pttButtonSize}
|
||||||
|
startTalking={startTalking}
|
||||||
|
stopTalking={stopTalking}
|
||||||
|
/>
|
||||||
|
<p className={styles.actionTip}>
|
||||||
|
{getPromptText(
|
||||||
|
showTalkOverError,
|
||||||
|
pttButtonHeld,
|
||||||
|
activeSpeakerIsLocalUser,
|
||||||
|
talkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
activeSpeakerDisplayName
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{userMediaFeeds.map((callFeed) => (
|
||||||
|
<PTTFeed
|
||||||
|
key={callFeed.userId}
|
||||||
|
callFeed={callFeed}
|
||||||
|
audioOutputDevice={audioOutput}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{isAdmin && (
|
||||||
|
<Toggle
|
||||||
|
isSelected={talkOverEnabled}
|
||||||
|
onChange={setTalkOverEnabled}
|
||||||
|
label="Talk over speaker"
|
||||||
|
id="talkOverEnabled"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{settingsModalState.isOpen && (
|
||||||
|
<SettingsModal
|
||||||
|
{...settingsModalProps}
|
||||||
|
setShowInspector={setShowInspector}
|
||||||
|
showInspector={showInspector}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{inviteModalState.isOpen && (
|
||||||
|
<InviteModal roomId={roomId} {...inviteModalProps} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
26
src/room/PTTFeed.jsx
Normal file
26
src/room/PTTFeed.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
||||||
|
import { useCallFeed } from "../video-grid/useCallFeed";
|
||||||
|
import { useMediaStream } from "../video-grid/useMediaStream";
|
||||||
|
import styles from "./PTTFeed.module.css";
|
||||||
|
|
||||||
|
export function PTTFeed({ callFeed, audioOutputDevice }) {
|
||||||
|
const { isLocal, stream } = useCallFeed(callFeed);
|
||||||
|
const mediaRef = useMediaStream(stream, audioOutputDevice, isLocal);
|
||||||
|
return <audio ref={mediaRef} className={styles.audioFeed} playsInline />;
|
||||||
|
}
|
||||||
3
src/room/PTTFeed.module.css
Normal file
3
src/room/PTTFeed.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.audioFeed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -1,8 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { FieldRow, ErrorMessage } from "../input/Input";
|
import { FieldRow, ErrorMessage } from "../input/Input";
|
||||||
import { useSubmitRageshake } from "../settings/rageshake";
|
import { useSubmitRageshake } from "../settings/submit-rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
|
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import styles from "./RoomAuthView.module.css";
|
import styles from "./RoomAuthView.module.css";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export function RoomPage() {
|
|||||||
|
|
||||||
const { roomId: maybeRoomId } = useParams();
|
const { roomId: maybeRoomId } = useParams();
|
||||||
const { hash, search } = useLocation();
|
const { hash, search } = useLocation();
|
||||||
const [simpleGrid, viaServers] = useMemo(() => {
|
const [viaServers] = useMemo(() => {
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
return [params.has("simple"), params.getAll("via")];
|
return [params.getAll("via")];
|
||||||
}, [search]);
|
}, [search]);
|
||||||
const roomId = (maybeRoomId || hash || "").toLowerCase();
|
const roomId = (maybeRoomId || hash || "").toLowerCase();
|
||||||
|
|
||||||
@@ -56,7 +56,6 @@ export function RoomPage() {
|
|||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
simpleGrid={simpleGrid}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</GroupCallLoader>
|
</GroupCallLoader>
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useLocation, useHistory } from "react-router-dom";
|
import { useLocation, useHistory } from "react-router-dom";
|
||||||
import { defaultHomeserverHost } from "../matrix-utils";
|
import { defaultHomeserverHost } from "../matrix-utils";
|
||||||
|
|||||||
53
src/room/Timer.jsx
Normal file
53
src/room/Timer.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function leftPad(value) {
|
||||||
|
return value < 10 ? "0" + value : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(msElapsed) {
|
||||||
|
const secondsElapsed = msElapsed / 1000;
|
||||||
|
const hours = Math.floor(secondsElapsed / 3600);
|
||||||
|
const minutes = Math.floor(secondsElapsed / 60) - hours * 60;
|
||||||
|
const seconds = Math.floor(secondsElapsed - hours * 3600 - minutes * 60);
|
||||||
|
return `${leftPad(hours)}:${leftPad(minutes)}:${leftPad(seconds)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Timer({ value }) {
|
||||||
|
const [timestamp, setTimestamp] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const startTimeMs = performance.now();
|
||||||
|
|
||||||
|
let animationFrame;
|
||||||
|
|
||||||
|
function onUpdate(curTimeMs) {
|
||||||
|
const msElapsed = curTimeMs - startTimeMs;
|
||||||
|
setTimestamp(formatTime(msElapsed));
|
||||||
|
animationFrame = requestAnimationFrame(onUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(startTimeMs);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrame);
|
||||||
|
};
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return <p>{timestamp}</p>;
|
||||||
|
}
|
||||||
96
src/room/VideoPreview.jsx
Normal file
96
src/room/VideoPreview.jsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
||||||
|
import { MicButton, VideoButton } from "../button";
|
||||||
|
import { useMediaStream } from "../video-grid/useMediaStream";
|
||||||
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
|
import { Avatar } from "../Avatar";
|
||||||
|
import { useProfile } from "../profile/useProfile";
|
||||||
|
import useMeasure from "react-use-measure";
|
||||||
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import styles from "./VideoPreview.module.css";
|
||||||
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
|
export function VideoPreview({
|
||||||
|
client,
|
||||||
|
state,
|
||||||
|
roomId,
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
toggleLocalVideoMuted,
|
||||||
|
toggleMicrophoneMuted,
|
||||||
|
setShowInspector,
|
||||||
|
showInspector,
|
||||||
|
audioOutput,
|
||||||
|
stream,
|
||||||
|
}) {
|
||||||
|
const videoRef = useMediaStream(stream, audioOutput, true);
|
||||||
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
|
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
|
const avatarSize = (previewBounds.height - 66) / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.preview} ref={previewRef}>
|
||||||
|
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||||
|
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
||||||
|
Webcam/microphone permissions needed to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
|
||||||
|
Accept webcam/microphone permissions to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||||
|
<>
|
||||||
|
{localVideoMuted && (
|
||||||
|
<div className={styles.avatarContainer}>
|
||||||
|
<Avatar
|
||||||
|
style={{
|
||||||
|
width: avatarSize,
|
||||||
|
height: avatarSize,
|
||||||
|
borderRadius: avatarSize,
|
||||||
|
fontSize: Math.round(avatarSize / 2),
|
||||||
|
}}
|
||||||
|
src={avatarUrl}
|
||||||
|
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.previewButtons}>
|
||||||
|
<MicButton
|
||||||
|
muted={microphoneMuted}
|
||||||
|
onPress={toggleMicrophoneMuted}
|
||||||
|
/>
|
||||||
|
<VideoButton
|
||||||
|
muted={localVideoMuted}
|
||||||
|
onPress={toggleLocalVideoMuted}
|
||||||
|
/>
|
||||||
|
<OverflowMenu
|
||||||
|
roomId={roomId}
|
||||||
|
setShowInspector={setShowInspector}
|
||||||
|
showInspector={showInspector}
|
||||||
|
client={client}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
src/room/VideoPreview.module.css
Normal file
65
src/room/VideoPreview.module.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.preview {
|
||||||
|
position: relative;
|
||||||
|
min-height: 280px;
|
||||||
|
height: 50vh;
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bgColor3);
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview video {
|
||||||
|
width: calc(100% + 1px);
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
background-color: black;
|
||||||
|
/* transform scale doesn't perfectly match width, so make -1.01 border issues */
|
||||||
|
transform: scaleX(-1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 66px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bgColor3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webcamPermissions {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewButtons {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 66px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(23, 25, 28, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewButtons > * {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewButtons > :last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
.preview {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
268
src/room/useGroupCall.js
Normal file
268
src/room/useGroupCall.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { useCallback, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
GroupCallEvent,
|
||||||
|
GroupCallState,
|
||||||
|
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { usePageUnload } from "./usePageUnload";
|
||||||
|
|
||||||
|
export function useGroupCall(groupCall) {
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
state,
|
||||||
|
calls,
|
||||||
|
localCallFeed,
|
||||||
|
activeSpeaker,
|
||||||
|
userMediaFeeds,
|
||||||
|
error,
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
isScreensharing,
|
||||||
|
screenshareFeeds,
|
||||||
|
localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId,
|
||||||
|
participants,
|
||||||
|
hasLocalParticipant,
|
||||||
|
requestingScreenshare,
|
||||||
|
},
|
||||||
|
setState,
|
||||||
|
] = useState({
|
||||||
|
state: GroupCallState.LocalCallFeedUninitialized,
|
||||||
|
calls: [],
|
||||||
|
userMediaFeeds: [],
|
||||||
|
microphoneMuted: false,
|
||||||
|
localVideoMuted: false,
|
||||||
|
screenshareFeeds: [],
|
||||||
|
isScreensharing: false,
|
||||||
|
requestingScreenshare: false,
|
||||||
|
participants: [],
|
||||||
|
hasLocalParticipant: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateState = (state) =>
|
||||||
|
setState((prevState) => ({ ...prevState, ...state }));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onGroupCallStateChanged() {
|
||||||
|
updateState({
|
||||||
|
state: groupCall.state,
|
||||||
|
calls: [...groupCall.calls],
|
||||||
|
localCallFeed: groupCall.localCallFeed,
|
||||||
|
activeSpeaker: groupCall.activeSpeaker,
|
||||||
|
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||||
|
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||||
|
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||||
|
isScreensharing: groupCall.isScreensharing(),
|
||||||
|
localScreenshareFeed: groupCall.localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||||
|
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||||
|
participants: [...groupCall.participants],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUserMediaFeedsChanged(userMediaFeeds) {
|
||||||
|
updateState({
|
||||||
|
userMediaFeeds: [...userMediaFeeds],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScreenshareFeedsChanged(screenshareFeeds) {
|
||||||
|
updateState({
|
||||||
|
screenshareFeeds: [...screenshareFeeds],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onActiveSpeakerChanged(activeSpeaker) {
|
||||||
|
updateState({
|
||||||
|
activeSpeaker: activeSpeaker,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLocalMuteStateChanged(microphoneMuted, localVideoMuted) {
|
||||||
|
updateState({
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLocalScreenshareStateChanged(
|
||||||
|
isScreensharing,
|
||||||
|
localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId
|
||||||
|
) {
|
||||||
|
updateState({
|
||||||
|
isScreensharing,
|
||||||
|
localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCallsChanged(calls) {
|
||||||
|
updateState({
|
||||||
|
calls: [...calls],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onParticipantsChanged(participants) {
|
||||||
|
updateState({
|
||||||
|
participants: [...participants],
|
||||||
|
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
|
||||||
|
groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
|
||||||
|
groupCall.on(
|
||||||
|
GroupCallEvent.ScreenshareFeedsChanged,
|
||||||
|
onScreenshareFeedsChanged
|
||||||
|
);
|
||||||
|
groupCall.on(GroupCallEvent.ActiveSpeakerChanged, onActiveSpeakerChanged);
|
||||||
|
groupCall.on(GroupCallEvent.LocalMuteStateChanged, onLocalMuteStateChanged);
|
||||||
|
groupCall.on(
|
||||||
|
GroupCallEvent.LocalScreenshareStateChanged,
|
||||||
|
onLocalScreenshareStateChanged
|
||||||
|
);
|
||||||
|
groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged);
|
||||||
|
groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged);
|
||||||
|
|
||||||
|
updateState({
|
||||||
|
error: null,
|
||||||
|
state: groupCall.state,
|
||||||
|
calls: [...groupCall.calls],
|
||||||
|
localCallFeed: groupCall.localCallFeed,
|
||||||
|
activeSpeaker: groupCall.activeSpeaker,
|
||||||
|
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||||
|
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||||
|
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||||
|
isScreensharing: groupCall.isScreensharing(),
|
||||||
|
localScreenshareFeed: groupCall.localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||||
|
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||||
|
participants: [...groupCall.participants],
|
||||||
|
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.GroupCallStateChanged,
|
||||||
|
onGroupCallStateChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.UserMediaFeedsChanged,
|
||||||
|
onUserMediaFeedsChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.ScreenshareFeedsChanged,
|
||||||
|
onScreenshareFeedsChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.ActiveSpeakerChanged,
|
||||||
|
onActiveSpeakerChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.LocalMuteStateChanged,
|
||||||
|
onLocalMuteStateChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.LocalScreenshareStateChanged,
|
||||||
|
onLocalScreenshareStateChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(GroupCallEvent.CallsChanged, onCallsChanged);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.ParticipantsChanged,
|
||||||
|
onParticipantsChanged
|
||||||
|
);
|
||||||
|
groupCall.leave();
|
||||||
|
};
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
usePageUnload(() => {
|
||||||
|
groupCall.leave();
|
||||||
|
});
|
||||||
|
|
||||||
|
const initLocalCallFeed = useCallback(
|
||||||
|
() => groupCall.initLocalCallFeed(),
|
||||||
|
[groupCall]
|
||||||
|
);
|
||||||
|
|
||||||
|
const enter = useCallback(() => {
|
||||||
|
if (
|
||||||
|
groupCall.state !== GroupCallState.LocalCallFeedUninitialized &&
|
||||||
|
groupCall.state !== GroupCallState.LocalCallFeedInitialized
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupCall.enter().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
updateState({ error });
|
||||||
|
});
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
const leave = useCallback(() => groupCall.leave(), [groupCall]);
|
||||||
|
|
||||||
|
const toggleLocalVideoMuted = useCallback(() => {
|
||||||
|
groupCall.setLocalVideoMuted(!groupCall.isLocalVideoMuted());
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
const toggleMicrophoneMuted = useCallback(() => {
|
||||||
|
groupCall.setMicrophoneMuted(!groupCall.isMicrophoneMuted());
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
const toggleScreensharing = useCallback(() => {
|
||||||
|
updateState({ requestingScreenshare: true });
|
||||||
|
|
||||||
|
groupCall.setScreensharingEnabled(!groupCall.isScreensharing()).then(() => {
|
||||||
|
updateState({ requestingScreenshare: false });
|
||||||
|
});
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.RTCPeerConnection === undefined) {
|
||||||
|
const error = new Error(
|
||||||
|
"WebRTC is not supported or is being blocked in this browser."
|
||||||
|
);
|
||||||
|
console.error(error);
|
||||||
|
updateState({ error });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
calls,
|
||||||
|
localCallFeed,
|
||||||
|
activeSpeaker,
|
||||||
|
userMediaFeeds,
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
error,
|
||||||
|
initLocalCallFeed,
|
||||||
|
enter,
|
||||||
|
leave,
|
||||||
|
toggleLocalVideoMuted,
|
||||||
|
toggleMicrophoneMuted,
|
||||||
|
toggleScreensharing,
|
||||||
|
requestingScreenshare,
|
||||||
|
isScreensharing,
|
||||||
|
screenshareFeeds,
|
||||||
|
localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId,
|
||||||
|
participants,
|
||||||
|
hasLocalParticipant,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||||
|
|
||||||
@@ -86,7 +102,7 @@ export function useLoadGroupCall(client, roomId, viaServers, createIfNotFound) {
|
|||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
setState((prevState) => ({ ...prevState, loading: false, error }))
|
setState((prevState) => ({ ...prevState, loading: false, error }))
|
||||||
);
|
);
|
||||||
}, [client, roomId, state.reloadId]);
|
}, [client, roomId, state.reloadId, createIfNotFound, viaServers]);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
278
src/room/usePTT.ts
Normal file
278
src/room/usePTT.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { useCallback, useEffect, useState } from "react";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { PlayClipFunction, PTTClipID } from "../sound/usePttSounds";
|
||||||
|
|
||||||
|
// Works out who the active speaker should be given what feeds are active and
|
||||||
|
// the power level of each user.
|
||||||
|
function getActiveSpeakerFeed(
|
||||||
|
feeds: CallFeed[],
|
||||||
|
groupCall: GroupCall
|
||||||
|
): CallFeed | null {
|
||||||
|
const activeSpeakerFeeds = feeds.filter((f) => !f.isAudioMuted());
|
||||||
|
|
||||||
|
let activeSpeakerFeed = null;
|
||||||
|
let highestPowerLevel = null;
|
||||||
|
for (const feed of activeSpeakerFeeds) {
|
||||||
|
const member = groupCall.room.getMember(feed.userId);
|
||||||
|
if (highestPowerLevel === null || member.powerLevel > highestPowerLevel) {
|
||||||
|
highestPowerLevel = member.powerLevel;
|
||||||
|
activeSpeakerFeed = feed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeSpeakerFeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PTTState {
|
||||||
|
pttButtonHeld: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
talkOverEnabled: boolean;
|
||||||
|
setTalkOverEnabled: (boolean) => void;
|
||||||
|
activeSpeakerUserId: string;
|
||||||
|
startTalking: () => void;
|
||||||
|
stopTalking: () => void;
|
||||||
|
transmitBlocked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePTT = (
|
||||||
|
client: MatrixClient,
|
||||||
|
groupCall: GroupCall,
|
||||||
|
userMediaFeeds: CallFeed[],
|
||||||
|
playClip: PlayClipFunction
|
||||||
|
): PTTState => {
|
||||||
|
// Used to serialise all the mute calls so they don't race. It has
|
||||||
|
// its own state as its always set separately from anything else.
|
||||||
|
const [mutePromise, setMutePromise] = useState(
|
||||||
|
Promise.resolve<boolean | void>(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrapper to serialise all the mute operations on the promise
|
||||||
|
const setMicMuteWrapper = useCallback(
|
||||||
|
(muted: boolean) => {
|
||||||
|
setMutePromise(
|
||||||
|
mutePromise.then(() => {
|
||||||
|
return groupCall.setMicrophoneMuted(muted).catch((e) => {
|
||||||
|
logger.error("Failed to unmute microphone", e);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[groupCall, mutePromise]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
pttButtonHeld,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
transmitBlocked,
|
||||||
|
},
|
||||||
|
setState,
|
||||||
|
] = useState(() => {
|
||||||
|
const roomMember = groupCall.room.getMember(client.getUserId());
|
||||||
|
|
||||||
|
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAdmin: roomMember.powerLevel >= 100,
|
||||||
|
talkOverEnabled: false,
|
||||||
|
pttButtonHeld: false,
|
||||||
|
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
|
||||||
|
transmitBlocked: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const onMuteStateChanged = useCallback(() => {
|
||||||
|
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
|
||||||
|
|
||||||
|
let blocked = false;
|
||||||
|
if (activeSpeakerUserId === null && activeSpeakerFeed !== null) {
|
||||||
|
if (activeSpeakerFeed.userId === client.getUserId()) {
|
||||||
|
playClip(PTTClipID.START_TALKING_LOCAL);
|
||||||
|
} else {
|
||||||
|
playClip(PTTClipID.START_TALKING_REMOTE);
|
||||||
|
}
|
||||||
|
} else if (activeSpeakerUserId !== null && activeSpeakerFeed === null) {
|
||||||
|
playClip(PTTClipID.END_TALKING);
|
||||||
|
} else if (
|
||||||
|
pttButtonHeld &&
|
||||||
|
activeSpeakerUserId === client.getUserId() &&
|
||||||
|
activeSpeakerFeed?.userId !== client.getUserId()
|
||||||
|
) {
|
||||||
|
// We were talking but we've been cut off: mute our own mic
|
||||||
|
// (this is the easier way of cutting other speakers off if an
|
||||||
|
// admin barges in: we could also mute the non-admin speaker
|
||||||
|
// on all receivers, but we'd have to make sure we unmuted them
|
||||||
|
// correctly.)
|
||||||
|
setMicMuteWrapper(true);
|
||||||
|
blocked = true;
|
||||||
|
playClip(PTTClipID.BLOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
activeSpeakerUserId: activeSpeakerFeed
|
||||||
|
? activeSpeakerFeed.userId
|
||||||
|
: null,
|
||||||
|
transmitBlocked: blocked,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
playClip,
|
||||||
|
groupCall,
|
||||||
|
pttButtonHeld,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
client,
|
||||||
|
userMediaFeeds,
|
||||||
|
setMicMuteWrapper,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
for (const callFeed of userMediaFeeds) {
|
||||||
|
callFeed.addListener(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
|
||||||
|
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const callFeed of userMediaFeeds) {
|
||||||
|
callFeed.removeListener(
|
||||||
|
CallFeedEvent.MuteStateChanged,
|
||||||
|
onMuteStateChanged
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [userMediaFeeds, onMuteStateChanged, groupCall]);
|
||||||
|
|
||||||
|
const startTalking = useCallback(async () => {
|
||||||
|
if (pttButtonHeld) return;
|
||||||
|
|
||||||
|
let blocked = false;
|
||||||
|
if (activeSpeakerUserId && !(isAdmin && talkOverEnabled)) {
|
||||||
|
playClip(PTTClipID.BLOCKED);
|
||||||
|
blocked = true;
|
||||||
|
}
|
||||||
|
// setstate before doing the async call to mute / unmute the mic
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
pttButtonHeld: true,
|
||||||
|
transmitBlocked: blocked,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!blocked && groupCall.isMicrophoneMuted()) {
|
||||||
|
setMicMuteWrapper(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
pttButtonHeld,
|
||||||
|
groupCall,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
setState,
|
||||||
|
playClip,
|
||||||
|
setMicMuteWrapper,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stopTalking = useCallback(async () => {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
pttButtonHeld: false,
|
||||||
|
transmitBlocked: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setMicMuteWrapper(true);
|
||||||
|
}, [setMicMuteWrapper]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKeyDown(event: KeyboardEvent): void {
|
||||||
|
if (event.code === "Space") {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (pttButtonHeld) return;
|
||||||
|
|
||||||
|
startTalking();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyUp(event: KeyboardEvent): void {
|
||||||
|
if (event.code === "Space") {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
stopTalking();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBlur(): void {
|
||||||
|
// TODO: We will need to disable this for a global PTT hotkey to work
|
||||||
|
if (!groupCall.isMicrophoneMuted()) {
|
||||||
|
setMicMuteWrapper(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prevState) => ({ ...prevState, pttButtonHeld: false }));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
window.addEventListener("keyup", onKeyUp);
|
||||||
|
window.addEventListener("blur", onBlur);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
window.removeEventListener("keyup", onKeyUp);
|
||||||
|
window.removeEventListener("blur", onBlur);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
groupCall,
|
||||||
|
startTalking,
|
||||||
|
stopTalking,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
pttButtonHeld,
|
||||||
|
setMicMuteWrapper,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const setTalkOverEnabled = useCallback((talkOverEnabled) => {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
talkOverEnabled,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pttButtonHeld,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
setTalkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
startTalking,
|
||||||
|
stopTalking,
|
||||||
|
transmitBlocked,
|
||||||
|
};
|
||||||
|
};
|
||||||
70
src/room/usePageUnload.js
Normal file
70
src/room/usePageUnload.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/9039885
|
||||||
|
function isIOS() {
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
"iPad Simulator",
|
||||||
|
"iPhone Simulator",
|
||||||
|
"iPod Simulator",
|
||||||
|
"iPad",
|
||||||
|
"iPhone",
|
||||||
|
"iPod",
|
||||||
|
].includes(navigator.platform) ||
|
||||||
|
// iPad on iOS 13 detection
|
||||||
|
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePageUnload(callback) {
|
||||||
|
useEffect(() => {
|
||||||
|
let pageVisibilityTimeout;
|
||||||
|
|
||||||
|
function onBeforeUnload(event) {
|
||||||
|
if (event.type === "visibilitychange") {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
clearTimeout(pageVisibilityTimeout);
|
||||||
|
} else {
|
||||||
|
// Wait 5 seconds before closing the page to avoid accidentally leaving
|
||||||
|
// TODO: Make this configurable?
|
||||||
|
pageVisibilityTimeout = setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
|
||||||
|
if (isIOS()) {
|
||||||
|
window.addEventListener("pagehide", onBeforeUnload);
|
||||||
|
document.addEventListener("visibilitychange", onBeforeUnload);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", onBeforeUnload);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pagehide", onBeforeUnload);
|
||||||
|
document.removeEventListener("visibilitychange", onBeforeUnload);
|
||||||
|
window.removeEventListener("beforeunload", onBeforeUnload);
|
||||||
|
clearTimeout(pageVisibilityTimeout);
|
||||||
|
};
|
||||||
|
}, [callback]);
|
||||||
|
}
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { useEffect } from "react";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import styles from "./SettingsModal.module.css";
|
import styles from "./SettingsModal.module.css";
|
||||||
@@ -10,7 +26,7 @@ import { Item } from "@react-stately/collections";
|
|||||||
import { useMediaHandler } from "./useMediaHandler";
|
import { useMediaHandler } from "./useMediaHandler";
|
||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useDownloadDebugLog } from "./rageshake";
|
import { useDownloadDebugLog } from "./submit-rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
|
export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
|
||||||
|
|||||||
@@ -1,300 +1,535 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
/*
|
||||||
import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
|
Copyright 2017 OpenMarket Ltd
|
||||||
import pako from "pako";
|
Copyright 2018 New Vector Ltd
|
||||||
import { useClient } from "../ClientContext";
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
import { InspectorContext } from "../room/GroupCallInspector";
|
|
||||||
import { useModalTriggerState } from "../Modal";
|
|
||||||
|
|
||||||
export function useSubmitRageshake() {
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
const { client } = useClient();
|
you may not use this file except in compliance with the License.
|
||||||
const [{ json }] = useContext(InspectorContext);
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
const [{ sending, sent, error }, setState] = useState({
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
sending: false,
|
|
||||||
sent: false,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const submitRageshake = useCallback(
|
Unless required by applicable law or agreed to in writing, software
|
||||||
async (opts) => {
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
if (sending) {
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This module contains all the code needed to log the console, persist it to
|
||||||
|
// disk and submit bug reports. Rationale is as follows:
|
||||||
|
// - Monkey-patching the console is preferable to having a log library because
|
||||||
|
// we can catch logs by other libraries more easily, without having to all
|
||||||
|
// depend on the same log framework / pass the logger around.
|
||||||
|
// - We use IndexedDB to persists logs because it has generous disk space
|
||||||
|
// limits compared to local storage. IndexedDB does not work in incognito
|
||||||
|
// mode, in which case this module will not be able to write logs to disk.
|
||||||
|
// However, the logs will still be stored in-memory, so can still be
|
||||||
|
// submitted in a bug report should the user wish to: we can also store more
|
||||||
|
// logs in-memory than in local storage, which does work in incognito mode.
|
||||||
|
// We also need to handle the case where there are 2+ tabs. Each JS runtime
|
||||||
|
// generates a random string which serves as the "ID" for that tab/session.
|
||||||
|
// These IDs are stored along with the log lines.
|
||||||
|
// - Bug reports are sent as a POST over HTTPS: it purposefully does not use
|
||||||
|
// Matrix as bug reports may be made when Matrix is not responsive (which may
|
||||||
|
// be the cause of the bug). We send the most recent N MB of UTF-8 log data,
|
||||||
|
// starting with the most recent, which we know because the "ID"s are
|
||||||
|
// actually timestamps. We then purge the remaining logs. We also do this
|
||||||
|
// purge on startup to prevent logs from accumulating.
|
||||||
|
|
||||||
|
// the frequency with which we flush to indexeddb
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
const FLUSH_RATE_MS = 30 * 1000;
|
||||||
|
|
||||||
|
// the length of log data we keep in indexeddb (and include in the reports)
|
||||||
|
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
|
||||||
|
|
||||||
|
// A class which monkey-patches the global console and stores log lines.
|
||||||
|
export class ConsoleLogger {
|
||||||
|
logs = "";
|
||||||
|
|
||||||
|
monkeyPatch(consoleObj) {
|
||||||
|
// Monkey-patch console logging
|
||||||
|
const consoleFunctionsToLevels = {
|
||||||
|
log: "I",
|
||||||
|
info: "I",
|
||||||
|
warn: "W",
|
||||||
|
error: "E",
|
||||||
|
};
|
||||||
|
Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
|
||||||
|
const level = consoleFunctionsToLevels[fnName];
|
||||||
|
const originalFn = consoleObj[fnName].bind(consoleObj);
|
||||||
|
consoleObj[fnName] = (...args) => {
|
||||||
|
this.log(level, ...args);
|
||||||
|
originalFn(...args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(level, ...args) {
|
||||||
|
// We don't know what locale the user may be running so use ISO strings
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
|
||||||
|
// Convert objects and errors to helpful things
|
||||||
|
args = args.map((arg) => {
|
||||||
|
if (arg instanceof DOMException) {
|
||||||
|
return arg.message + ` (${arg.name} | ${arg.code})`;
|
||||||
|
} else if (arg instanceof Error) {
|
||||||
|
return arg.message + (arg.stack ? `\n${arg.stack}` : "");
|
||||||
|
} else if (typeof arg === "object") {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(arg);
|
||||||
|
} catch (e) {
|
||||||
|
// In development, it can be useful to log complex cyclic
|
||||||
|
// objects to the console for inspection. This is fine for
|
||||||
|
// the console, but default `stringify` can't handle that.
|
||||||
|
// We workaround this by using a special replacer function
|
||||||
|
// to only log values of the root object and avoid cycles.
|
||||||
|
return JSON.stringify(arg, (key, value) => {
|
||||||
|
if (key && typeof value === "object") {
|
||||||
|
return "<object>";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Some browsers support string formatting which we're not doing here
|
||||||
|
// so the lines are a little more ugly but easy to implement / quick to
|
||||||
|
// run.
|
||||||
|
// Example line:
|
||||||
|
// 2017-01-18T11:23:53.214Z W Failed to set badge count
|
||||||
|
let line = `${ts} ${level} ${args.join(" ")}\n`;
|
||||||
|
// Do some cleanup
|
||||||
|
line = line.replace(/token=[a-zA-Z0-9-]+/gm, "token=xxxxx");
|
||||||
|
// Using + really is the quickest way in JS
|
||||||
|
// http://jsperf.com/concat-vs-plus-vs-join
|
||||||
|
this.logs += line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve log lines to flush to disk.
|
||||||
|
* @param {boolean} keepLogs True to not delete logs after flushing.
|
||||||
|
* @return {string} \n delimited log lines to flush.
|
||||||
|
*/
|
||||||
|
flush(keepLogs) {
|
||||||
|
// The ConsoleLogger doesn't care how these end up on disk, it just
|
||||||
|
// flushes them to the caller.
|
||||||
|
if (keepLogs) {
|
||||||
|
return this.logs;
|
||||||
|
}
|
||||||
|
const logsToFlush = this.logs;
|
||||||
|
this.logs = "";
|
||||||
|
return logsToFlush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A class which stores log lines in an IndexedDB instance.
|
||||||
|
export class IndexedDBLogStore {
|
||||||
|
index = 0;
|
||||||
|
db = null;
|
||||||
|
flushPromise = null;
|
||||||
|
flushAgainPromise = null;
|
||||||
|
|
||||||
|
constructor(indexedDB, logger) {
|
||||||
|
this.indexedDB = indexedDB;
|
||||||
|
this.logger = logger;
|
||||||
|
this.id = "instance-" + Math.random() + Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} Resolves when the store is ready.
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
const req = this.indexedDB.open("logs");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
req.onsuccess = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
this.db = event.target.result;
|
||||||
|
// Periodically flush logs to local storage / indexeddb
|
||||||
|
setInterval(this.flush.bind(this), FLUSH_RATE_MS);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
req.onerror = (event) => {
|
||||||
|
const err =
|
||||||
|
// @ts-ignore
|
||||||
|
"Failed to open log database: " + event.target.error.name;
|
||||||
|
logger.error(err);
|
||||||
|
reject(new Error(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// First time: Setup the object store
|
||||||
|
req.onupgradeneeded = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const db = event.target.result;
|
||||||
|
const logObjStore = db.createObjectStore("logs", {
|
||||||
|
keyPath: ["id", "index"],
|
||||||
|
});
|
||||||
|
// Keys in the database look like: [ "instance-148938490", 0 ]
|
||||||
|
// Later on we need to query everything based on an instance id.
|
||||||
|
// In order to do this, we need to set up indexes "id".
|
||||||
|
logObjStore.createIndex("id", "id", { unique: false });
|
||||||
|
|
||||||
|
logObjStore.add(
|
||||||
|
this.generateLogEntry(new Date() + " ::: Log database was created.")
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastModifiedStore = db.createObjectStore("logslastmod", {
|
||||||
|
keyPath: "id",
|
||||||
|
});
|
||||||
|
lastModifiedStore.add(this.generateLastModifiedTime());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush logs to disk.
|
||||||
|
*
|
||||||
|
* There are guards to protect against race conditions in order to ensure
|
||||||
|
* that all previous flushes have completed before the most recent flush.
|
||||||
|
* Consider without guards:
|
||||||
|
* - A calls flush() periodically.
|
||||||
|
* - B calls flush() and wants to send logs immediately afterwards.
|
||||||
|
* - If B doesn't wait for A's flush to complete, B will be missing the
|
||||||
|
* contents of A's flush.
|
||||||
|
* To protect against this, we set 'flushPromise' when a flush is ongoing.
|
||||||
|
* Subsequent calls to flush() during this period will chain another flush,
|
||||||
|
* then keep returning that same chained flush.
|
||||||
|
*
|
||||||
|
* This guarantees that we will always eventually do a flush when flush() is
|
||||||
|
* called.
|
||||||
|
*
|
||||||
|
* @return {Promise} Resolved when the logs have been flushed.
|
||||||
|
*/
|
||||||
|
flush() {
|
||||||
|
// check if a flush() operation is ongoing
|
||||||
|
if (this.flushPromise) {
|
||||||
|
if (this.flushAgainPromise) {
|
||||||
|
// this is the 3rd+ time we've called flush() : return the same promise.
|
||||||
|
return this.flushAgainPromise;
|
||||||
|
}
|
||||||
|
// queue up a flush to occur immediately after the pending one completes.
|
||||||
|
this.flushAgainPromise = this.flushPromise
|
||||||
|
.then(() => {
|
||||||
|
return this.flush();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.flushAgainPromise = null;
|
||||||
|
});
|
||||||
|
return this.flushAgainPromise;
|
||||||
|
}
|
||||||
|
// there is no flush promise or there was but it has finished, so do
|
||||||
|
// a brand new one, destroying the chain which may have been built up.
|
||||||
|
this.flushPromise = new Promise((resolve, reject) => {
|
||||||
|
if (!this.db) {
|
||||||
|
// not connected yet or user rejected access for us to r/w to the db.
|
||||||
|
reject(new Error("No connected database"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const lines = this.logger.flush();
|
||||||
try {
|
if (lines.length === 0) {
|
||||||
setState({ sending: true, sent: false, error: null });
|
resolve();
|
||||||
|
return;
|
||||||
let userAgent = "UNKNOWN";
|
|
||||||
if (window.navigator && window.navigator.userAgent) {
|
|
||||||
userAgent = window.navigator.userAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
let touchInput = "UNKNOWN";
|
|
||||||
try {
|
|
||||||
// MDN claims broad support across browsers
|
|
||||||
touchInput = String(window.matchMedia("(pointer: coarse)").matches);
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
const body = new FormData();
|
|
||||||
body.append(
|
|
||||||
"text",
|
|
||||||
opts.description || "User did not supply any additional text."
|
|
||||||
);
|
|
||||||
body.append("app", "matrix-video-chat");
|
|
||||||
body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
|
|
||||||
body.append("user_agent", userAgent);
|
|
||||||
body.append("installed_pwa", false);
|
|
||||||
body.append("touch_input", touchInput);
|
|
||||||
|
|
||||||
if (client) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (opts.roomId) {
|
|
||||||
body.append("room_id", opts.roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.isCryptoEnabled()) {
|
|
||||||
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
|
||||||
if (client.getDeviceCurve25519Key) {
|
|
||||||
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
|
|
||||||
}
|
|
||||||
body.append("device_keys", keys.join(", "));
|
|
||||||
body.append("cross_signing_key", client.getCrossSigningId());
|
|
||||||
|
|
||||||
// add cross-signing status information
|
|
||||||
const crossSigning = client.crypto.crossSigningInfo;
|
|
||||||
const secretStorage = client.crypto.secretStorage;
|
|
||||||
|
|
||||||
body.append(
|
|
||||||
"cross_signing_ready",
|
|
||||||
String(await client.isCrossSigningReady())
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"cross_signing_supported_by_hs",
|
|
||||||
String(
|
|
||||||
await client.doesServerSupportUnstableFeature(
|
|
||||||
"org.matrix.e2e_cross_signing"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
body.append("cross_signing_key", crossSigning.getId());
|
|
||||||
body.append(
|
|
||||||
"cross_signing_privkey_in_secret_storage",
|
|
||||||
String(
|
|
||||||
!!(await crossSigning.isStoredInSecretStorage(secretStorage))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const pkCache = client.getCrossSigningCacheCallbacks();
|
|
||||||
body.append(
|
|
||||||
"cross_signing_master_privkey_cached",
|
|
||||||
String(
|
|
||||||
!!(pkCache && (await pkCache.getCrossSigningKeyCache("master")))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"cross_signing_self_signing_privkey_cached",
|
|
||||||
String(
|
|
||||||
!!(
|
|
||||||
pkCache &&
|
|
||||||
(await pkCache.getCrossSigningKeyCache("self_signing"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"cross_signing_user_signing_privkey_cached",
|
|
||||||
String(
|
|
||||||
!!(
|
|
||||||
pkCache &&
|
|
||||||
(await pkCache.getCrossSigningKeyCache("user_signing"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
body.append(
|
|
||||||
"secret_storage_ready",
|
|
||||||
String(await client.isSecretStorageReady())
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"secret_storage_key_in_account",
|
|
||||||
String(!!(await secretStorage.hasKey()))
|
|
||||||
);
|
|
||||||
|
|
||||||
body.append(
|
|
||||||
"session_backup_key_in_secret_storage",
|
|
||||||
String(!!(await client.isKeyBackupKeyStored()))
|
|
||||||
);
|
|
||||||
const sessionBackupKeyFromCache =
|
|
||||||
await client.crypto.getSessionBackupPrivateKey();
|
|
||||||
body.append(
|
|
||||||
"session_backup_key_cached",
|
|
||||||
String(!!sessionBackupKeyFromCache)
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"session_backup_key_well_formed",
|
|
||||||
String(sessionBackupKeyFromCache instanceof Uint8Array)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.label) {
|
|
||||||
body.append("label", opts.label);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add storage persistence/quota information
|
|
||||||
if (navigator.storage && navigator.storage.persisted) {
|
|
||||||
try {
|
|
||||||
body.append(
|
|
||||||
"storageManager_persisted",
|
|
||||||
String(await navigator.storage.persisted())
|
|
||||||
);
|
|
||||||
} catch (e) {}
|
|
||||||
} else if (document.hasStorageAccess) {
|
|
||||||
// Safari
|
|
||||||
try {
|
|
||||||
body.append(
|
|
||||||
"storageManager_persisted",
|
|
||||||
String(await document.hasStorageAccess())
|
|
||||||
);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (navigator.storage && navigator.storage.estimate) {
|
|
||||||
try {
|
|
||||||
const estimate = await navigator.storage.estimate();
|
|
||||||
body.append("storageManager_quota", String(estimate.quota));
|
|
||||||
body.append("storageManager_usage", String(estimate.usage));
|
|
||||||
if (estimate.usageDetails) {
|
|
||||||
Object.keys(estimate.usageDetails).forEach((k) => {
|
|
||||||
body.append(
|
|
||||||
`storageManager_usage_${k}`,
|
|
||||||
String(estimate.usageDetails[k])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.sendLogs) {
|
|
||||||
const logs = await rageshake.getLogsForReport();
|
|
||||||
|
|
||||||
for (const entry of logs) {
|
|
||||||
// encode as UTF-8
|
|
||||||
let buf = new TextEncoder().encode(entry.lines);
|
|
||||||
|
|
||||||
// compress
|
|
||||||
buf = pako.gzip(buf);
|
|
||||||
|
|
||||||
body.append("compressed-log", new Blob([buf]), entry.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json) {
|
|
||||||
body.append(
|
|
||||||
"file",
|
|
||||||
new Blob([JSON.stringify(json)], { type: "text/plain" }),
|
|
||||||
"groupcall.txt"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.rageshakeRequestId) {
|
|
||||||
body.append(
|
|
||||||
"group_call_rageshake_request_id",
|
|
||||||
opts.rageshakeRequestId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetch(
|
|
||||||
import.meta.env.VITE_RAGESHAKE_SUBMIT_URL ||
|
|
||||||
"https://element.io/bugreports/submit",
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
setState({ sending: false, sent: true, error: null });
|
|
||||||
} catch (error) {
|
|
||||||
setState({ sending: false, sent: false, error });
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
},
|
const txn = this.db.transaction(["logs", "logslastmod"], "readwrite");
|
||||||
[client]
|
const objStore = txn.objectStore("logs");
|
||||||
);
|
txn.oncomplete = (event) => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
txn.onerror = (event) => {
|
||||||
|
logger.error("Failed to flush logs : ", event);
|
||||||
|
reject(new Error("Failed to write logs: " + event.target.errorCode));
|
||||||
|
};
|
||||||
|
objStore.add(this.generateLogEntry(lines));
|
||||||
|
const lastModStore = txn.objectStore("logslastmod");
|
||||||
|
lastModStore.put(this.generateLastModifiedTime());
|
||||||
|
}).then(() => {
|
||||||
|
this.flushPromise = null;
|
||||||
|
});
|
||||||
|
return this.flushPromise;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
/**
|
||||||
submitRageshake,
|
* Consume the most recent logs and return them. Older logs which are not
|
||||||
sending,
|
* returned are deleted at the same time, so this can be called at startup
|
||||||
sent,
|
* to do house-keeping to keep the logs from growing too large.
|
||||||
error,
|
*
|
||||||
};
|
* @return {Promise<Object[]>} Resolves to an array of objects. The array is
|
||||||
}
|
* sorted in time (oldest first) based on when the log file was created (the
|
||||||
|
* log ID). The objects have said log ID in an "id" field and "lines" which
|
||||||
|
* is a big string with all the new-line delimited logs.
|
||||||
|
*/
|
||||||
|
async consume() {
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
export function useDownloadDebugLog() {
|
// Returns: a string representing the concatenated logs for this ID.
|
||||||
const [{ json }] = useContext(InspectorContext);
|
// Stops adding log fragments when the size exceeds maxSize
|
||||||
|
function fetchLogs(id, maxSize) {
|
||||||
|
const objectStore = db
|
||||||
|
.transaction("logs", "readonly")
|
||||||
|
.objectStore("logs");
|
||||||
|
|
||||||
const downloadDebugLog = useCallback(() => {
|
return new Promise((resolve, reject) => {
|
||||||
const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
|
const query = objectStore
|
||||||
const url = URL.createObjectURL(blob);
|
.index("id")
|
||||||
const el = document.createElement("a");
|
.openCursor(IDBKeyRange.only(id), "prev");
|
||||||
el.href = url;
|
let lines = "";
|
||||||
el.download = "groupcall.json";
|
query.onerror = (event) => {
|
||||||
el.style.display = "none";
|
reject(new Error("Query failed: " + event.target.errorCode));
|
||||||
document.body.appendChild(el);
|
};
|
||||||
el.click();
|
query.onsuccess = (event) => {
|
||||||
setTimeout(() => {
|
const cursor = event.target.result;
|
||||||
URL.revokeObjectURL(url);
|
if (!cursor) {
|
||||||
el.parentNode.removeChild(el);
|
resolve(lines);
|
||||||
}, 0);
|
return; // end of results
|
||||||
}, [json]);
|
}
|
||||||
|
lines = cursor.value.lines + lines;
|
||||||
return downloadDebugLog;
|
if (lines.length >= maxSize) {
|
||||||
}
|
resolve(lines);
|
||||||
|
} else {
|
||||||
export function useRageshakeRequest() {
|
cursor.continue();
|
||||||
const { client } = useClient();
|
}
|
||||||
|
};
|
||||||
const sendRageshakeRequest = useCallback(
|
|
||||||
(roomId, rageshakeRequestId) => {
|
|
||||||
client.sendEvent(roomId, "org.matrix.rageshake_request", {
|
|
||||||
request_id: rageshakeRequestId,
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
return sendRageshakeRequest;
|
// Returns: A sorted array of log IDs. (newest first)
|
||||||
}
|
function fetchLogIds() {
|
||||||
|
// To gather all the log IDs, query for all records in logslastmod.
|
||||||
|
const o = db
|
||||||
|
.transaction("logslastmod", "readonly")
|
||||||
|
.objectStore("logslastmod");
|
||||||
|
return selectQuery(o, undefined, (cursor) => {
|
||||||
|
return {
|
||||||
|
id: cursor.value.id,
|
||||||
|
ts: cursor.value.ts,
|
||||||
|
};
|
||||||
|
}).then((res) => {
|
||||||
|
// Sort IDs by timestamp (newest first)
|
||||||
|
return res
|
||||||
|
.sort((a, b) => {
|
||||||
|
return b.ts - a.ts;
|
||||||
|
})
|
||||||
|
.map((a) => a.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useRageshakeRequestModal(roomId) {
|
function deleteLogs(id) {
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
return new Promise((resolve, reject) => {
|
||||||
const { client } = useClient();
|
const txn = db.transaction(["logs", "logslastmod"], "readwrite");
|
||||||
const [rageshakeRequestId, setRageshakeRequestId] = useState();
|
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));
|
||||||
|
query.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
o.delete(cursor.primaryKey);
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
txn.oncomplete = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
txn.onerror = (event) => {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
"Failed to delete logs for " +
|
||||||
|
`'${id}' : ${event.target.errorCode}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// delete last modified entries
|
||||||
|
const lastModStore = txn.objectStore("logslastmod");
|
||||||
|
lastModStore.delete(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const allLogIds = await fetchLogIds();
|
||||||
const onEvent = (event) => {
|
let removeLogIds = [];
|
||||||
const type = event.getType();
|
const logs = [];
|
||||||
|
let size = 0;
|
||||||
|
for (let i = 0; i < allLogIds.length; i++) {
|
||||||
|
const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size);
|
||||||
|
|
||||||
if (
|
// always add the log file: fetchLogs will truncate once the maxSize we give it is
|
||||||
type === "org.matrix.rageshake_request" &&
|
// exceeded, so we'll go over the max but only by one fragment's worth.
|
||||||
roomId === event.getRoomId() &&
|
logs.push({
|
||||||
client.getUserId() !== event.getSender()
|
lines: lines,
|
||||||
) {
|
id: allLogIds[i],
|
||||||
setRageshakeRequestId(event.getContent().request_id);
|
});
|
||||||
modalState.open();
|
size += lines.length;
|
||||||
|
|
||||||
|
// If fetchLogs truncated we'll now be at or over the size limit,
|
||||||
|
// in which case we should stop and remove the rest of the log files.
|
||||||
|
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);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (removeLogIds.length > 0) {
|
||||||
|
logger.log("Removing logs: ", removeLogIds);
|
||||||
|
// Don't await this because it's non-fatal if we can't clean up
|
||||||
|
// logs.
|
||||||
|
Promise.all(removeLogIds.map((id) => deleteLogs(id))).then(
|
||||||
|
() => {
|
||||||
|
logger.log(`Removed ${removeLogIds.length} old logs.`);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateLogEntry(lines) {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
lines: lines,
|
||||||
|
index: this.index++,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
client.on("event", onEvent);
|
generateLastModifiedTime() {
|
||||||
|
return {
|
||||||
return () => {
|
id: this.id,
|
||||||
client.removeListener("event", onEvent);
|
ts: Date.now(),
|
||||||
};
|
};
|
||||||
}, [modalState.open, roomId]);
|
}
|
||||||
|
}
|
||||||
return { modalState, modalProps: { ...modalProps, rageshakeRequestId } };
|
|
||||||
|
/**
|
||||||
|
* Helper method to collect results from a Cursor and promiseify it.
|
||||||
|
* @param {ObjectStore|Index} store The store to perform openCursor on.
|
||||||
|
* @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
|
||||||
|
* @param {Function} resultMapper A function which is repeatedly called with a
|
||||||
|
* Cursor.
|
||||||
|
* Return the data you want to keep.
|
||||||
|
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
||||||
|
* resultMapper.
|
||||||
|
*/
|
||||||
|
function selectQuery(store, keyRange, resultMapper) {
|
||||||
|
const query = store.openCursor(keyRange);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const results = [];
|
||||||
|
query.onerror = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
reject(new Error("Query failed: " + event.target.errorCode));
|
||||||
|
};
|
||||||
|
// collect results
|
||||||
|
query.onsuccess = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (!cursor) {
|
||||||
|
resolve(results);
|
||||||
|
return; // end of results
|
||||||
|
}
|
||||||
|
results.push(resultMapper(cursor));
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure rage shaking support for sending bug reports.
|
||||||
|
* Modifies globals.
|
||||||
|
* @param {boolean} setUpPersistence When true (default), the persistence will
|
||||||
|
* be set up immediately for the logs.
|
||||||
|
* @return {Promise} Resolves when set up.
|
||||||
|
*/
|
||||||
|
export function init(setUpPersistence = true) {
|
||||||
|
if (global.mx_rage_initPromise) {
|
||||||
|
return global.mx_rage_initPromise;
|
||||||
|
}
|
||||||
|
global.mx_rage_logger = new ConsoleLogger();
|
||||||
|
global.mx_rage_logger.monkeyPatch(window.console);
|
||||||
|
|
||||||
|
if (setUpPersistence) {
|
||||||
|
return tryInitStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.mx_rage_initPromise = Promise.resolve();
|
||||||
|
return global.mx_rage_initPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to start up the rageshake storage for logs. If not possible (client unsupported)
|
||||||
|
* then this no-ops.
|
||||||
|
* @return {Promise} Resolves when complete.
|
||||||
|
*/
|
||||||
|
export function tryInitStorage() {
|
||||||
|
if (global.mx_rage_initStoragePromise) {
|
||||||
|
return global.mx_rage_initStoragePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("Configuring rageshake persistence...");
|
||||||
|
|
||||||
|
// just *accessing* indexedDB throws an exception in firefox with
|
||||||
|
// indexeddb disabled.
|
||||||
|
let indexedDB;
|
||||||
|
try {
|
||||||
|
indexedDB = window.indexedDB;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (indexedDB) {
|
||||||
|
global.mx_rage_store = new IndexedDBLogStore(
|
||||||
|
indexedDB,
|
||||||
|
global.mx_rage_logger
|
||||||
|
);
|
||||||
|
global.mx_rage_initStoragePromise = global.mx_rage_store.connect();
|
||||||
|
return global.mx_rage_initStoragePromise;
|
||||||
|
}
|
||||||
|
global.mx_rage_initStoragePromise = Promise.resolve();
|
||||||
|
return global.mx_rage_initStoragePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flush() {
|
||||||
|
if (!global.mx_rage_store) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global.mx_rage_store.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old logs.
|
||||||
|
* @return {Promise} Resolves if cleaned logs.
|
||||||
|
*/
|
||||||
|
export async function cleanup() {
|
||||||
|
if (!global.mx_rage_store) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await global.mx_rage_store.consume();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a recent snapshot of the logs, ready for attaching to a bug report
|
||||||
|
*
|
||||||
|
* @return {Array<{lines: string, id, string}>} list of log data
|
||||||
|
*/
|
||||||
|
export async function getLogsForReport() {
|
||||||
|
if (!global.mx_rage_logger) {
|
||||||
|
throw new Error("No console logger, did you forget to call init()?");
|
||||||
|
}
|
||||||
|
// If in incognito mode, store is null, but we still want bug report
|
||||||
|
// sending to work going off the in-memory console logs.
|
||||||
|
if (global.mx_rage_store) {
|
||||||
|
// flush most recent logs
|
||||||
|
await global.mx_rage_store.flush();
|
||||||
|
return await global.mx_rage_store.consume();
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
lines: global.mx_rage_logger.flush(true),
|
||||||
|
id: "-",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
316
src/settings/submit-rageshake.js
Normal file
316
src/settings/submit-rageshake.js
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { useCallback, useContext, useEffect, useState } from "react";
|
||||||
|
import { getLogsForReport } from "./rageshake";
|
||||||
|
import pako from "pako";
|
||||||
|
import { useClient } from "../ClientContext";
|
||||||
|
import { InspectorContext } from "../room/GroupCallInspector";
|
||||||
|
import { useModalTriggerState } from "../Modal";
|
||||||
|
|
||||||
|
export function useSubmitRageshake() {
|
||||||
|
const { client } = useClient();
|
||||||
|
const [{ json }] = useContext(InspectorContext);
|
||||||
|
|
||||||
|
const [{ sending, sent, error }, setState] = useState({
|
||||||
|
sending: false,
|
||||||
|
sent: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitRageshake = useCallback(
|
||||||
|
async (opts) => {
|
||||||
|
if (sending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState({ sending: true, sent: false, error: null });
|
||||||
|
|
||||||
|
let userAgent = "UNKNOWN";
|
||||||
|
if (window.navigator && window.navigator.userAgent) {
|
||||||
|
userAgent = window.navigator.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchInput = "UNKNOWN";
|
||||||
|
try {
|
||||||
|
// MDN claims broad support across browsers
|
||||||
|
touchInput = String(window.matchMedia("(pointer: coarse)").matches);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const body = new FormData();
|
||||||
|
body.append(
|
||||||
|
"text",
|
||||||
|
opts.description || "User did not supply any additional text."
|
||||||
|
);
|
||||||
|
body.append("app", "matrix-video-chat");
|
||||||
|
body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
|
||||||
|
body.append("user_agent", userAgent);
|
||||||
|
body.append("installed_pwa", false);
|
||||||
|
body.append("touch_input", touchInput);
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (opts.roomId) {
|
||||||
|
body.append("room_id", opts.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.isCryptoEnabled()) {
|
||||||
|
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
||||||
|
if (client.getDeviceCurve25519Key) {
|
||||||
|
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
|
||||||
|
}
|
||||||
|
body.append("device_keys", keys.join(", "));
|
||||||
|
body.append("cross_signing_key", client.getCrossSigningId());
|
||||||
|
|
||||||
|
// add cross-signing status information
|
||||||
|
const crossSigning = client.crypto.crossSigningInfo;
|
||||||
|
const secretStorage = client.crypto.secretStorage;
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"cross_signing_ready",
|
||||||
|
String(await client.isCrossSigningReady())
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_supported_by_hs",
|
||||||
|
String(
|
||||||
|
await client.doesServerSupportUnstableFeature(
|
||||||
|
"org.matrix.e2e_cross_signing"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append("cross_signing_key", crossSigning.getId());
|
||||||
|
body.append(
|
||||||
|
"cross_signing_privkey_in_secret_storage",
|
||||||
|
String(
|
||||||
|
!!(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const pkCache = client.getCrossSigningCacheCallbacks();
|
||||||
|
body.append(
|
||||||
|
"cross_signing_master_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(pkCache && (await pkCache.getCrossSigningKeyCache("master")))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_self_signing_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(
|
||||||
|
pkCache &&
|
||||||
|
(await pkCache.getCrossSigningKeyCache("self_signing"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_user_signing_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(
|
||||||
|
pkCache &&
|
||||||
|
(await pkCache.getCrossSigningKeyCache("user_signing"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"secret_storage_ready",
|
||||||
|
String(await client.isSecretStorageReady())
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"secret_storage_key_in_account",
|
||||||
|
String(!!(await secretStorage.hasKey()))
|
||||||
|
);
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_in_secret_storage",
|
||||||
|
String(!!(await client.isKeyBackupKeyStored()))
|
||||||
|
);
|
||||||
|
const sessionBackupKeyFromCache =
|
||||||
|
await client.crypto.getSessionBackupPrivateKey();
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_cached",
|
||||||
|
String(!!sessionBackupKeyFromCache)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_well_formed",
|
||||||
|
String(sessionBackupKeyFromCache instanceof Uint8Array)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.label) {
|
||||||
|
body.append("label", opts.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add storage persistence/quota information
|
||||||
|
if (navigator.storage && navigator.storage.persisted) {
|
||||||
|
try {
|
||||||
|
body.append(
|
||||||
|
"storageManager_persisted",
|
||||||
|
String(await navigator.storage.persisted())
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
} else if (document.hasStorageAccess) {
|
||||||
|
// Safari
|
||||||
|
try {
|
||||||
|
body.append(
|
||||||
|
"storageManager_persisted",
|
||||||
|
String(await document.hasStorageAccess())
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.storage && navigator.storage.estimate) {
|
||||||
|
try {
|
||||||
|
const estimate = await navigator.storage.estimate();
|
||||||
|
body.append("storageManager_quota", String(estimate.quota));
|
||||||
|
body.append("storageManager_usage", String(estimate.usage));
|
||||||
|
if (estimate.usageDetails) {
|
||||||
|
Object.keys(estimate.usageDetails).forEach((k) => {
|
||||||
|
body.append(
|
||||||
|
`storageManager_usage_${k}`,
|
||||||
|
String(estimate.usageDetails[k])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.sendLogs) {
|
||||||
|
const logs = await getLogsForReport();
|
||||||
|
|
||||||
|
for (const entry of logs) {
|
||||||
|
// encode as UTF-8
|
||||||
|
let buf = new TextEncoder().encode(entry.lines);
|
||||||
|
|
||||||
|
// compress
|
||||||
|
buf = pako.gzip(buf);
|
||||||
|
|
||||||
|
body.append("compressed-log", new Blob([buf]), entry.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
body.append(
|
||||||
|
"file",
|
||||||
|
new Blob([JSON.stringify(json)], { type: "text/plain" }),
|
||||||
|
"groupcall.txt"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.rageshakeRequestId) {
|
||||||
|
body.append(
|
||||||
|
"group_call_rageshake_request_id",
|
||||||
|
opts.rageshakeRequestId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch(
|
||||||
|
import.meta.env.VITE_RAGESHAKE_SUBMIT_URL ||
|
||||||
|
"https://element.io/bugreports/submit",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setState({ sending: false, sent: true, error: null });
|
||||||
|
} catch (error) {
|
||||||
|
setState({ sending: false, sent: false, error });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, json, sending]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
submitRageshake,
|
||||||
|
sending,
|
||||||
|
sent,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDownloadDebugLog() {
|
||||||
|
const [{ json }] = useContext(InspectorContext);
|
||||||
|
|
||||||
|
const downloadDebugLog = useCallback(() => {
|
||||||
|
const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const el = document.createElement("a");
|
||||||
|
el.href = url;
|
||||||
|
el.download = "groupcall.json";
|
||||||
|
el.style.display = "none";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.click();
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
}, 0);
|
||||||
|
}, [json]);
|
||||||
|
|
||||||
|
return downloadDebugLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRageshakeRequest() {
|
||||||
|
const { client } = useClient();
|
||||||
|
|
||||||
|
const sendRageshakeRequest = useCallback(
|
||||||
|
(roomId, rageshakeRequestId) => {
|
||||||
|
client.sendEvent(roomId, "org.matrix.rageshake_request", {
|
||||||
|
request_id: rageshakeRequestId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
return sendRageshakeRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRageshakeRequestModal(roomId) {
|
||||||
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
const { client } = useClient();
|
||||||
|
const [rageshakeRequestId, setRageshakeRequestId] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onEvent = (event) => {
|
||||||
|
const type = event.getType();
|
||||||
|
|
||||||
|
if (
|
||||||
|
type === "org.matrix.rageshake_request" &&
|
||||||
|
roomId === event.getRoomId() &&
|
||||||
|
client.getUserId() !== event.getSender()
|
||||||
|
) {
|
||||||
|
setRageshakeRequestId(event.getContent().request_id);
|
||||||
|
modalState.open();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.on("event", onEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
client.removeListener("event", onEvent);
|
||||||
|
};
|
||||||
|
}, [modalState.open, roomId, client, modalState]);
|
||||||
|
|
||||||
|
return { modalState, modalProps: { ...modalProps, rageshakeRequestId } };
|
||||||
|
}
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
|||||||
19
src/sound/PTTClips.module.css
Normal file
19
src/sound/PTTClips.module.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.pttClip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
70
src/sound/PTTClips.tsx
Normal file
70
src/sound/PTTClips.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
||||||
|
|
||||||
|
import startTalkLocalOggUrl from "./start_talk_local.ogg";
|
||||||
|
import startTalkLocalMp3Url from "./start_talk_local.mp3";
|
||||||
|
import startTalkRemoteOggUrl from "./start_talk_remote.ogg";
|
||||||
|
import startTalkRemoteMp3Url from "./start_talk_remote.mp3";
|
||||||
|
import endTalkOggUrl from "./end_talk.ogg";
|
||||||
|
import endTalkMp3Url from "./end_talk.mp3";
|
||||||
|
import blockedOggUrl from "./blocked.ogg";
|
||||||
|
import blockedMp3Url from "./blocked.mp3";
|
||||||
|
import styles from "./PTTClips.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
startTalkingLocalRef: React.RefObject<HTMLAudioElement>;
|
||||||
|
startTalkingRemoteRef: React.RefObject<HTMLAudioElement>;
|
||||||
|
endTalkingRef: React.RefObject<HTMLAudioElement>;
|
||||||
|
blockedRef: React.RefObject<HTMLAudioElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PTTClips: React.FC<Props> = ({
|
||||||
|
startTalkingLocalRef,
|
||||||
|
startTalkingRemoteRef,
|
||||||
|
endTalkingRef,
|
||||||
|
blockedRef,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<audio
|
||||||
|
preload="true"
|
||||||
|
className={styles.pttClip}
|
||||||
|
ref={startTalkingLocalRef}
|
||||||
|
>
|
||||||
|
<source type="audio/ogg" src={startTalkLocalOggUrl} />
|
||||||
|
<source type="audio/mpeg" src={startTalkLocalMp3Url} />
|
||||||
|
</audio>
|
||||||
|
<audio
|
||||||
|
preload="true"
|
||||||
|
className={styles.pttClip}
|
||||||
|
ref={startTalkingRemoteRef}
|
||||||
|
>
|
||||||
|
<source type="audio/ogg" src={startTalkRemoteOggUrl} />
|
||||||
|
<source type="audio/mpeg" src={startTalkRemoteMp3Url} />
|
||||||
|
</audio>
|
||||||
|
<audio preload="true" className={styles.pttClip} ref={endTalkingRef}>
|
||||||
|
<source type="audio/ogg" src={endTalkOggUrl} />
|
||||||
|
<source type="audio/mpeg" src={endTalkMp3Url} />
|
||||||
|
</audio>
|
||||||
|
<audio preload="true" className={styles.pttClip} ref={blockedRef}>
|
||||||
|
<source type="audio/ogg" src={blockedOggUrl} />
|
||||||
|
<source type="audio/mpeg" src={blockedMp3Url} />
|
||||||
|
</audio>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
BIN
src/sound/blocked.mp3
Normal file
BIN
src/sound/blocked.mp3
Normal file
Binary file not shown.
BIN
src/sound/blocked.ogg
Normal file
BIN
src/sound/blocked.ogg
Normal file
Binary file not shown.
BIN
src/sound/end_talk.mp3
Normal file
BIN
src/sound/end_talk.mp3
Normal file
Binary file not shown.
BIN
src/sound/end_talk.ogg
Normal file
BIN
src/sound/end_talk.ogg
Normal file
Binary file not shown.
BIN
src/sound/start_talk_local.mp3
Normal file
BIN
src/sound/start_talk_local.mp3
Normal file
Binary file not shown.
BIN
src/sound/start_talk_local.ogg
Normal file
BIN
src/sound/start_talk_local.ogg
Normal file
Binary file not shown.
BIN
src/sound/start_talk_remote.mp3
Normal file
BIN
src/sound/start_talk_remote.mp3
Normal file
Binary file not shown.
BIN
src/sound/start_talk_remote.ogg
Normal file
BIN
src/sound/start_talk_remote.ogg
Normal file
Binary file not shown.
77
src/sound/usePttSounds.ts
Normal file
77
src/sound/usePttSounds.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
export enum PTTClipID {
|
||||||
|
START_TALKING_LOCAL,
|
||||||
|
START_TALKING_REMOTE,
|
||||||
|
END_TALKING,
|
||||||
|
BLOCKED,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlayClipFunction = (clipID: PTTClipID) => void;
|
||||||
|
|
||||||
|
interface PTTSounds {
|
||||||
|
startTalkingLocalRef: React.RefObject<HTMLAudioElement>;
|
||||||
|
startTalkingRemoteRef: React.RefObject<HTMLAudioElement>;
|
||||||
|
endTalkingRef: React.RefObject<HTMLAudioElement>;
|
||||||
|
blockedRef: React.RefObject<HTMLAudioElement>;
|
||||||
|
playClip: PlayClipFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePTTSounds = (): PTTSounds => {
|
||||||
|
const [startTalkingLocalRef] = useState(React.createRef<HTMLAudioElement>());
|
||||||
|
const [startTalkingRemoteRef] = useState(React.createRef<HTMLAudioElement>());
|
||||||
|
const [endTalkingRef] = useState(React.createRef<HTMLAudioElement>());
|
||||||
|
const [blockedRef] = useState(React.createRef<HTMLAudioElement>());
|
||||||
|
|
||||||
|
const playClip = useCallback(
|
||||||
|
async (clipID: PTTClipID) => {
|
||||||
|
let ref: React.RefObject<HTMLAudioElement>;
|
||||||
|
|
||||||
|
switch (clipID) {
|
||||||
|
case PTTClipID.START_TALKING_LOCAL:
|
||||||
|
ref = startTalkingLocalRef;
|
||||||
|
break;
|
||||||
|
case PTTClipID.START_TALKING_REMOTE:
|
||||||
|
ref = startTalkingRemoteRef;
|
||||||
|
break;
|
||||||
|
case PTTClipID.END_TALKING:
|
||||||
|
ref = endTalkingRef;
|
||||||
|
break;
|
||||||
|
case PTTClipID.BLOCKED:
|
||||||
|
ref = blockedRef;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.currentTime = 0;
|
||||||
|
await ref.current.play();
|
||||||
|
} else {
|
||||||
|
console.log("No media element found");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[startTalkingLocalRef, startTalkingRemoteRef, endTalkingRef, blockedRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
startTalkingLocalRef,
|
||||||
|
startTalkingRemoteRef,
|
||||||
|
endTalkingRef,
|
||||||
|
blockedRef,
|
||||||
|
playClip,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { useTabList, useTab, useTabPanel } from "@react-aria/tabs";
|
import { useTabList, useTab, useTabPanel } from "@react-aria/tabs";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { TabContainer, TabItem } from "./Tabs";
|
import { TabContainer, TabItem } from "./Tabs";
|
||||||
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 React from "react";
|
import React from "react";
|
||||||
import { Headline, Title, Subtitle, Body, Caption, Micro } from "./Typography";
|
import { Headline, Title, Subtitle, Body, Caption, Micro } from "./Typography";
|
||||||
|
|
||||||
|
|||||||
6
src/useShouldShowPtt.js
Normal file
6
src/useShouldShowPtt.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
export function useShouldShowPtt() {
|
||||||
|
const { hash } = useLocation();
|
||||||
|
return hash.startsWith("#ptt");
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user