Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f9efb3563 | ||
|
|
69cfa1db6d | ||
|
|
977016fbb2 | ||
|
|
fb3d9e2a16 | ||
|
|
8da492d00d | ||
|
|
9676014120 | ||
|
|
7d87b8d1e5 | ||
|
|
ecb139721b | ||
|
|
aa45261b0d | ||
|
|
017ec13981 | ||
|
|
880a2ca127 | ||
|
|
5282ab5f12 | ||
|
|
582e6637dc | ||
|
|
65804cd962 | ||
|
|
0411e1cac8 | ||
|
|
bab5c9aa42 | ||
|
|
d680a36cab | ||
|
|
25bde3560b | ||
|
|
ddac2ba5ef | ||
|
|
cd55098921 | ||
|
|
f1bdad0d7f | ||
|
|
9fac2c95e5 | ||
|
|
486d0abd30 | ||
|
|
d9bd48b9a6 | ||
|
|
64e30c89e3 | ||
|
|
1860eaae7a | ||
|
|
771424cbf0 | ||
|
|
925a909ec1 | ||
|
|
f07ee54e05 | ||
|
|
7ee2f630db | ||
|
|
626fdb9f79 | ||
|
|
2cf40ff0b8 | ||
|
|
9edc1acc90 | ||
|
|
641e6c53b6 | ||
|
|
14fbddf780 | ||
|
|
2a69b72bed | ||
|
|
e21094b525 | ||
|
|
da3d038547 | ||
|
|
c6b90803f8 | ||
|
|
9444f43c72 | ||
|
|
26251e1e60 | ||
|
|
5b3183cbd3 | ||
|
|
e9b963080c | ||
|
|
1164e6f1e7 | ||
|
|
21c7bb979e | ||
|
|
1ff9073a1a | ||
|
|
7ed2f9bd9a | ||
|
|
2cdbeb6f12 | ||
|
|
7bd95621f1 | ||
|
|
a05501a909 | ||
|
|
e6960a1e15 | ||
|
|
c057713004 | ||
|
|
35e2135e3c | ||
|
|
af74228f8e | ||
|
|
9a44790450 | ||
|
|
5c4bab2a8a | ||
|
|
94380b64bd | ||
|
|
cbfd03f9c6 | ||
|
|
edf58f1d7d | ||
|
|
17fed7cd9c | ||
|
|
266861bdad | ||
|
|
426e1a433b | ||
|
|
3b8dfcec51 | ||
|
|
6f892edd5e | ||
|
|
126bfec339 | ||
|
|
59938cd46b | ||
|
|
a445bcd0b9 | ||
|
|
2acb6825e9 | ||
|
|
7d44a1e979 | ||
|
|
aa1fabf857 | ||
|
|
c714a0608c | ||
|
|
92d15e110a | ||
|
|
1367ff9914 | ||
|
|
7a2d64c0ef | ||
|
|
60b5f7cab2 | ||
|
|
d81c52e9bb | ||
|
|
c54f1bd7a3 | ||
|
|
24f721e414 | ||
|
|
3e19843bf7 | ||
|
|
183eea9f24 | ||
|
|
548ea7220b | ||
|
|
8cd45b64a1 | ||
|
|
c33d97a2ed | ||
|
|
7926a1f9b9 | ||
|
|
c7da1177ab | ||
|
|
1e5539f165 | ||
|
|
d019add257 | ||
|
|
cc8ce7a05c | ||
|
|
6913fddcd3 | ||
|
|
c13040f0b0 | ||
|
|
b3285974f9 | ||
|
|
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 | ||
|
|
7a9ff98550 | ||
|
|
3d54047f87 | ||
|
|
dc75c1cfb4 | ||
|
|
e2aee0be81 | ||
|
|
44486aa62d | ||
|
|
a0e4de73cc | ||
|
|
38f9a79bd3 | ||
|
|
fc1aaf02bf |
21
.env
21
.env
@@ -14,12 +14,15 @@
|
|||||||
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
|
||||||
|
|
||||||
# VITE_CUSTOM_THEME=true
|
# VITE_CUSTOM_THEME=true
|
||||||
# VITE_PRIMARY_COLOR=#0dbd8b
|
# VITE_THEME_ACCENT=#0dbd8b
|
||||||
# VITE_BG_COLOR_1=#ffffff
|
# VITE_THEME_ACCENT_20=#0dbd8b33
|
||||||
# VITE_BG_COLOR_2=#f0f1f4
|
# VITE_THEME_ALERT=#ff5b55
|
||||||
# VITE_BG_COLOR_3=#dbdfe4
|
# VITE_THEME_ALERT_20=#ff5b5533
|
||||||
# VITE_BG_COLOR_4=#d1d3d7
|
# VITE_THEME_LINKS=#0086e6
|
||||||
# VITE_INPUT_BORDER_COLOR=#e7e7e7
|
# VITE_THEME_PRIMARY_CONTENT=#ffffff
|
||||||
# VITE_INPUT_BORDER_COLOR_FOCUSED=#238cf5
|
# VITE_THEME_SECONDARY_CONTENT=#a9b2bc
|
||||||
# VITE_TEXT_COLOR_1=#17191c
|
# VITE_THEME_TERTIARY_CONTENT=#8e99a4
|
||||||
# VITE_TEXT_COLOR_2=#61708b
|
# VITE_THEME_QUATERNARY_CONTENT=#6f7882
|
||||||
|
# VITE_THEME_QUINARY_CONTENT=#394049
|
||||||
|
# VITE_THEME_SYSTEM=#21262c
|
||||||
|
# VITE_THEME_BACKGROUND=#15191e
|
||||||
|
|||||||
38
.eslintrc.js
Normal file
38
.eslintrc.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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: {
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* @vector-im/element-call-reviewers
|
||||||
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 }}
|
||||||
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 @@
|
|||||||
|
{}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ RUN element-call/scripts/dockerbuild.sh
|
|||||||
FROM nginxinc/nginx-unprivileged:alpine
|
FROM nginxinc/nginx-unprivileged:alpine
|
||||||
|
|
||||||
COPY --from=builder /src/element-call/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
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
25
package.json
25
package.json
@@ -5,10 +5,15 @@
|
|||||||
"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 0 src",
|
||||||
|
"lint:types": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||||
"@react-aria/button": "^3.3.4",
|
"@react-aria/button": "^3.3.4",
|
||||||
"@react-aria/dialog": "^3.1.4",
|
"@react-aria/dialog": "^3.1.4",
|
||||||
"@react-aria/focus": "^3.5.0",
|
"@react-aria/focus": "^3.5.0",
|
||||||
@@ -26,12 +31,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",
|
||||||
|
"@types/grecaptcha": "^3.0.4",
|
||||||
"@use-gesture/react": "^10.2.11",
|
"@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",
|
||||||
"lodash-move": "^1.1.1",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#aa0d3bd1f5a006d151f826e6b8c5f286abb6e960",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
|
||||||
"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",
|
||||||
@@ -48,10 +53,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"
|
||||||
|
|||||||
24
src/@types/global.d.ts
vendored
Normal file
24
src/@types/global.d.ts
vendored
Normal file
@@ -0,0 +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 "matrix-js-sdk/src/@types/global";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
// TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
|
||||||
|
OLM_OPTIONS: Record<string, string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
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,58 +0,0 @@
|
|||||||
import React, { useMemo } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import styles from "./Avatar.module.css";
|
|
||||||
|
|
||||||
const backgroundColors = [
|
|
||||||
"#5C56F5",
|
|
||||||
"#03B381",
|
|
||||||
"#368BD6",
|
|
||||||
"#AC3BA8",
|
|
||||||
"#E64F7A",
|
|
||||||
"#FF812D",
|
|
||||||
"#2DC2C5",
|
|
||||||
"#74D12C",
|
|
||||||
];
|
|
||||||
|
|
||||||
function hashStringToArrIndex(str, arrLength) {
|
|
||||||
let sum = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
sum += str.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sum % arrLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Avatar({
|
|
||||||
bgKey,
|
|
||||||
src,
|
|
||||||
fallback,
|
|
||||||
size,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
...rest
|
|
||||||
}) {
|
|
||||||
const backgroundColor = useMemo(() => {
|
|
||||||
const index = hashStringToArrIndex(
|
|
||||||
bgKey || fallback || src || "",
|
|
||||||
backgroundColors.length
|
|
||||||
);
|
|
||||||
return backgroundColors[index];
|
|
||||||
}, [bgKey, src, fallback]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.avatar, styles[size || "md"], className)}
|
|
||||||
style={{ backgroundColor, ...style }}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{src ? (
|
|
||||||
<img src={src} />
|
|
||||||
) : typeof fallback === "string" ? (
|
|
||||||
<span>{fallback}</span>
|
|
||||||
) : (
|
|
||||||
fallback
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
.avatar {
|
.avatar {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: #ffffff;
|
color: var(--primary-content);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar svg * {
|
.avatar svg * {
|
||||||
fill: #ffffff;
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar span {
|
.avatar span {
|
||||||
|
|||||||
115
src/Avatar.tsx
Normal file
115
src/Avatar.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React, { useMemo, CSSProperties } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import { getAvatarUrl } from "./matrix-utils";
|
||||||
|
import { useClient } from "./ClientContext";
|
||||||
|
import styles from "./Avatar.module.css";
|
||||||
|
|
||||||
|
const backgroundColors = [
|
||||||
|
"#5C56F5",
|
||||||
|
"#03B381",
|
||||||
|
"#368BD6",
|
||||||
|
"#AC3BA8",
|
||||||
|
"#E64F7A",
|
||||||
|
"#FF812D",
|
||||||
|
"#2DC2C5",
|
||||||
|
"#74D12C",
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum Size {
|
||||||
|
XS = "xs",
|
||||||
|
SM = "sm",
|
||||||
|
MD = "md",
|
||||||
|
LG = "lg",
|
||||||
|
XL = "xl",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sizes = new Map([
|
||||||
|
[Size.XS, 22],
|
||||||
|
[Size.SM, 32],
|
||||||
|
[Size.MD, 36],
|
||||||
|
[Size.LG, 42],
|
||||||
|
[Size.XL, 90],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function hashStringToArrIndex(str: string, arrLength: number) {
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
sum += str.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum % arrLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
|
||||||
|
src?.startsWith("mxc://") ? client && getAvatarUrl(client, src, size) : src;
|
||||||
|
|
||||||
|
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
bgKey?: string;
|
||||||
|
src: string;
|
||||||
|
fallback: string;
|
||||||
|
size?: Size | number;
|
||||||
|
className: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Avatar: React.FC<Props> = ({
|
||||||
|
bgKey,
|
||||||
|
src,
|
||||||
|
fallback,
|
||||||
|
size = Size.MD,
|
||||||
|
className,
|
||||||
|
style = {},
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { client } = useClient();
|
||||||
|
|
||||||
|
const [sizeClass, sizePx, sizeStyle] = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(Size).includes(size as Size)
|
||||||
|
? [styles[size as string], sizes.get(size as Size), {}]
|
||||||
|
: [
|
||||||
|
null,
|
||||||
|
size as number,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size,
|
||||||
|
fontSize: Math.round((size as number) / 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[size]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedSrc = useMemo(
|
||||||
|
() => resolveAvatarSrc(client, src, sizePx),
|
||||||
|
[client, src, sizePx]
|
||||||
|
);
|
||||||
|
|
||||||
|
const backgroundColor = useMemo(() => {
|
||||||
|
const index = hashStringToArrIndex(
|
||||||
|
bgKey || fallback || src || "",
|
||||||
|
backgroundColors.length
|
||||||
|
);
|
||||||
|
return backgroundColors[index];
|
||||||
|
}, [bgKey, src, fallback]);
|
||||||
|
|
||||||
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.avatar, sizeClass, className)}
|
||||||
|
style={{ backgroundColor, ...sizeStyle, ...style }}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{resolvedSrc ? (
|
||||||
|
<img src={resolvedSrc} />
|
||||||
|
) : typeof fallback === "string" ? (
|
||||||
|
<span>{fallback}</span>
|
||||||
|
) : (
|
||||||
|
fallback
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 New Vector Ltd
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
|
FC,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
@@ -23,17 +24,59 @@ import React, {
|
|||||||
useContext,
|
useContext,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import { ErrorView } from "./FullScreenView";
|
import { ErrorView } from "./FullScreenView";
|
||||||
import { initClient, defaultHomeserver } from "./matrix-utils";
|
import { initClient, defaultHomeserver } from "./matrix-utils";
|
||||||
|
|
||||||
const ClientContext = createContext();
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
matrixclient: MatrixClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ClientProvider({ children }) {
|
export interface Session {
|
||||||
|
user_id: string;
|
||||||
|
device_id: string;
|
||||||
|
access_token: string;
|
||||||
|
passwordlessUser: boolean;
|
||||||
|
tempPassword?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSession = (): Session => {
|
||||||
|
const data = localStorage.getItem("matrix-auth-store");
|
||||||
|
if (data) return JSON.parse(data);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const saveSession = (session: Session) =>
|
||||||
|
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
||||||
|
const clearSession = () => localStorage.removeItem("matrix-auth-store");
|
||||||
|
|
||||||
|
interface ClientState {
|
||||||
|
loading: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isPasswordlessUser: boolean;
|
||||||
|
client: MatrixClient;
|
||||||
|
userName: string;
|
||||||
|
changePassword: (password: string) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
setClient: (client: MatrixClient, session: Session) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientContext = createContext<ClientState>(null);
|
||||||
|
|
||||||
|
type ClientProviderState = Omit<
|
||||||
|
ClientState,
|
||||||
|
"changePassword" | "logout" | "setClient"
|
||||||
|
> & { error?: Error };
|
||||||
|
|
||||||
|
export const ClientProvider: FC = ({ children }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [
|
const [
|
||||||
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
||||||
setState,
|
setState,
|
||||||
] = useState({
|
] = useState<ClientProviderState>({
|
||||||
loading: true,
|
loading: true,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
@@ -43,18 +86,16 @@ export function ClientProvider({ children }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function restore() {
|
const restore = async (): Promise<
|
||||||
|
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
||||||
|
> => {
|
||||||
try {
|
try {
|
||||||
const authStore = localStorage.getItem("matrix-auth-store");
|
const session = loadSession();
|
||||||
|
|
||||||
if (authStore) {
|
if (session) {
|
||||||
const {
|
/* eslint-disable camelcase */
|
||||||
user_id,
|
const { user_id, device_id, access_token, passwordlessUser } =
|
||||||
device_id,
|
session;
|
||||||
access_token,
|
|
||||||
passwordlessUser,
|
|
||||||
tempPassword,
|
|
||||||
} = JSON.parse(authStore);
|
|
||||||
|
|
||||||
const client = await initClient({
|
const client = await initClient({
|
||||||
baseUrl: defaultHomeserver,
|
baseUrl: defaultHomeserver,
|
||||||
@@ -62,37 +103,26 @@ export function ClientProvider({ children }) {
|
|||||||
userId: user_id,
|
userId: user_id,
|
||||||
deviceId: device_id,
|
deviceId: device_id,
|
||||||
});
|
});
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
localStorage.setItem(
|
return { client, isPasswordlessUser: passwordlessUser };
|
||||||
"matrix-auth-store",
|
|
||||||
JSON.stringify({
|
|
||||||
user_id,
|
|
||||||
device_id,
|
|
||||||
access_token,
|
|
||||||
|
|
||||||
passwordlessUser,
|
|
||||||
tempPassword,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return { client, passwordlessUser };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { client: undefined };
|
return { client: undefined, isPasswordlessUser: false };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
localStorage.removeItem("matrix-auth-store");
|
clearSession();
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
restore()
|
restore()
|
||||||
.then(({ client, passwordlessUser }) => {
|
.then(({ client, isPasswordlessUser }) => {
|
||||||
setState({
|
setState({
|
||||||
client,
|
client,
|
||||||
loading: false,
|
loading: false,
|
||||||
isAuthenticated: !!client,
|
isAuthenticated: Boolean(client),
|
||||||
isPasswordlessUser: !!passwordlessUser,
|
isPasswordlessUser,
|
||||||
userName: client?.getUserIdLocalpart(),
|
userName: client?.getUserIdLocalpart(),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -108,31 +138,23 @@ export function ClientProvider({ children }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const changePassword = useCallback(
|
const changePassword = useCallback(
|
||||||
async (password) => {
|
async (password: string) => {
|
||||||
const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse(
|
const { tempPassword, ...session } = loadSession();
|
||||||
localStorage.getItem("matrix-auth-store")
|
|
||||||
);
|
|
||||||
|
|
||||||
await client.setPassword(
|
await client.setPassword(
|
||||||
{
|
{
|
||||||
type: "m.login.password",
|
type: "m.login.password",
|
||||||
identifier: {
|
identifier: {
|
||||||
type: "m.id.user",
|
type: "m.id.user",
|
||||||
user: existingSession.user_id,
|
user: session.user_id,
|
||||||
},
|
},
|
||||||
user: existingSession.user_id,
|
user: session.user_id,
|
||||||
password: tempPassword,
|
password: tempPassword,
|
||||||
},
|
},
|
||||||
password
|
password
|
||||||
);
|
);
|
||||||
|
|
||||||
localStorage.setItem(
|
saveSession({ ...session, passwordlessUser: false });
|
||||||
"matrix-auth-store",
|
|
||||||
JSON.stringify({
|
|
||||||
...existingSession,
|
|
||||||
passwordlessUser: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
client,
|
client,
|
||||||
@@ -146,23 +168,23 @@ export function ClientProvider({ children }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setClient = useCallback(
|
const setClient = useCallback(
|
||||||
(newClient, session) => {
|
(newClient: MatrixClient, session: Session) => {
|
||||||
if (client && client !== newClient) {
|
if (client && client !== newClient) {
|
||||||
client.stopClient();
|
client.stopClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newClient) {
|
if (newClient) {
|
||||||
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
saveSession(session);
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
client: newClient,
|
client: newClient,
|
||||||
loading: false,
|
loading: false,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
isPasswordlessUser: !!session.passwordlessUser,
|
isPasswordlessUser: session.passwordlessUser,
|
||||||
userName: newClient.getUserIdLocalpart(),
|
userName: newClient.getUserIdLocalpart(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("matrix-auth-store");
|
clearSession();
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
client: undefined,
|
client: undefined,
|
||||||
@@ -177,29 +199,23 @@ export function ClientProvider({ children }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
localStorage.removeItem("matrix-auth-store");
|
clearSession();
|
||||||
window.location = "/";
|
history.push("/");
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (client) {
|
if (client) {
|
||||||
const loadTime = Date.now();
|
const loadTime = Date.now();
|
||||||
|
|
||||||
const onToDeviceEvent = (event) => {
|
const onToDeviceEvent = (event: MatrixEvent) => {
|
||||||
if (event.getType() !== "org.matrix.call_duplicate_session") {
|
if (event.getType() !== "org.matrix.call_duplicate_session") return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
|
|
||||||
if (content.session_id === client.getSessionId()) {
|
if (content.session_id === client.getSessionId()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.timestamp > loadTime) {
|
if (content.timestamp > loadTime) {
|
||||||
if (client) {
|
client?.stopClient();
|
||||||
client.stopClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -210,7 +226,7 @@ export function ClientProvider({ children }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
client.on("toDeviceEvent", onToDeviceEvent);
|
client.on(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
||||||
|
|
||||||
client.sendToDevice("org.matrix.call_duplicate_session", {
|
client.sendToDevice("org.matrix.call_duplicate_session", {
|
||||||
[client.getUserId()]: {
|
[client.getUserId()]: {
|
||||||
@@ -219,12 +235,12 @@ export function ClientProvider({ children }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
client.removeListener("toDeviceEvent", onToDeviceEvent);
|
client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
const context = useMemo(
|
const context = useMemo<ClientState>(
|
||||||
() => ({
|
() => ({
|
||||||
loading,
|
loading,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
@@ -258,8 +274,6 @@ export function ClientProvider({ children }) {
|
|||||||
return (
|
return (
|
||||||
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useClient() {
|
export const useClient = () => useContext(ClientContext);
|
||||||
return useContext(ClientContext);
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,59 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "./Facepile.module.css";
|
import styles from "./Facepile.module.css";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Avatar } from "./Avatar";
|
import { Avatar, sizes } from "./Avatar";
|
||||||
import { getAvatarUrl } from "./matrix-utils";
|
|
||||||
|
const overlapMap = {
|
||||||
|
xs: 2,
|
||||||
|
sm: 4,
|
||||||
|
md: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Facepile({
|
||||||
|
className,
|
||||||
|
client,
|
||||||
|
participants,
|
||||||
|
max,
|
||||||
|
size,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const _size = sizes.get(size);
|
||||||
|
const _overlap = overlapMap[size];
|
||||||
|
|
||||||
export function Facepile({ className, client, participants, ...rest }) {
|
|
||||||
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}
|
||||||
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(--system);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facepile.md .avatar {
|
||||||
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,12 +57,13 @@ export function HeaderLogo({ className }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoomHeaderInfo({ roomName }) {
|
export function RoomHeaderInfo({ roomName, avatarUrl }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.roomAvatar}>
|
<div className={styles.roomAvatar}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="md"
|
size="md"
|
||||||
|
src={avatarUrl}
|
||||||
bgKey={roomName}
|
bgKey={roomName}
|
||||||
fallback={roomName.slice(0, 1).toUpperCase()}
|
fallback={roomName.slice(0, 1).toUpperCase()}
|
||||||
/>
|
/>
|
||||||
@@ -73,13 +74,13 @@ export function RoomHeaderInfo({ roomName }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoomSetupHeaderInfo({ roomName, ...rest }) {
|
export function RoomSetupHeaderInfo({ roomName, avatarUrl, ...rest }) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const { buttonProps } = useButton(rest, ref);
|
const { buttonProps } = useButton(rest, ref);
|
||||||
return (
|
return (
|
||||||
<button className={styles.backButton} ref={ref} {...buttonProps}>
|
<button className={styles.backButton} ref={ref} {...buttonProps}>
|
||||||
<ArrowLeftIcon width={16} height={16} />
|
<ArrowLeftIcon width={16} height={16} />
|
||||||
<RoomHeaderInfo roomName={roomName} />
|
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/IndexedDBWorker.js
Normal file
5
src/IndexedDBWorker.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker";
|
||||||
|
|
||||||
|
const remoteWorker = new IndexedDBStoreWorker(self.postMessage);
|
||||||
|
|
||||||
|
self.onmessage = remoteWorker.onMessage;
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid var(--inputBorderColor);
|
border: 1px solid var(--quinary-content);
|
||||||
background-color: var(--bgColor1);
|
background-color: var(--background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -28,6 +28,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.option.disabled {
|
.option.disabled {
|
||||||
color: var(--textColor2);
|
color: var(--quaternary-content);
|
||||||
background-color: var(--bgColor3);
|
background-color: var(--bgColor3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
.menuItem.focused,
|
.menuItem.focused,
|
||||||
.menuItem:hover {
|
.menuItem:hover {
|
||||||
background-color: var(--bgColor4);
|
background-color: var(--quinary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menuItem.focused:first-child,
|
.menuItem.focused:first-child,
|
||||||
@@ -39,3 +39,12 @@
|
|||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkIcon * {
|
||||||
|
stroke: var(--primary-content);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 135px;
|
max-width: 135px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.userButton svg * {
|
.userButton svg * {
|
||||||
fill: var(--textColor1);
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.authLinks a {
|
.authLinks a {
|
||||||
color: #0dbd8b;
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 New Vector Ltd
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,9 +14,18 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useRef, useState, useMemo } from "react";
|
import React, {
|
||||||
|
FC,
|
||||||
|
FormEvent,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||||
|
|
||||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||||
|
import { useClient } from "../ClientContext";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
||||||
@@ -24,27 +33,30 @@ import styles from "./LoginPage.module.css";
|
|||||||
import { useInteractiveLogin } from "./useInteractiveLogin";
|
import { useInteractiveLogin } from "./useInteractiveLogin";
|
||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function LoginPage() {
|
export const LoginPage: FC = () => {
|
||||||
usePageTitle("Login");
|
usePageTitle("Login");
|
||||||
|
|
||||||
const [_, login] = useInteractiveLogin();
|
const { setClient } = useClient();
|
||||||
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
const login = useInteractiveLogin();
|
||||||
const usernameRef = useRef();
|
const homeserver = defaultHomeserver; // TODO: Make this configurable
|
||||||
const passwordRef = useRef();
|
const usernameRef = useRef<HTMLInputElement>();
|
||||||
|
const passwordRef = useRef<HTMLInputElement>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState<Error>();
|
||||||
|
|
||||||
// TODO: Handle hitting login page with authenticated client
|
// TODO: Handle hitting login page with authenticated client
|
||||||
|
|
||||||
const onSubmitLoginForm = useCallback(
|
const onSubmitLoginForm = useCallback(
|
||||||
(e) => {
|
(e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
login(homeserver, usernameRef.current.value, passwordRef.current.value)
|
||||||
.then(() => {
|
.then(([client, session]) => {
|
||||||
|
setClient(client, session);
|
||||||
|
|
||||||
if (location.state && location.state.from) {
|
if (location.state && location.state.from) {
|
||||||
history.push(location.state.from);
|
history.push(location.state.from);
|
||||||
} else {
|
} else {
|
||||||
@@ -56,13 +68,13 @@ export function LoginPage() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[login, location, history, homeserver]
|
[login, location, history, homeserver, setClient]
|
||||||
);
|
);
|
||||||
|
|
||||||
const homeserverHost = useMemo(() => {
|
const homeserverHost = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
return new URL(homeserver).host;
|
return new URL(homeserver).host;
|
||||||
} catch (_error) {
|
} catch (error) {
|
||||||
return defaultHomeserverHost;
|
return defaultHomeserverHost;
|
||||||
}
|
}
|
||||||
}, [homeserver]);
|
}, [homeserver]);
|
||||||
@@ -121,4 +133,4 @@ export function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 New Vector Ltd
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,8 +14,18 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, {
|
||||||
|
FC,
|
||||||
|
FormEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { captureException } from "@sentry/react";
|
||||||
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
@@ -28,67 +38,68 @@ import { useRecaptcha } from "./useRecaptcha";
|
|||||||
import { Caption, Link } from "../typography/Typography";
|
import { Caption, Link } from "../typography/Typography";
|
||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function RegisterPage() {
|
export const RegisterPage: FC = () => {
|
||||||
usePageTitle("Register");
|
usePageTitle("Register");
|
||||||
|
|
||||||
const { loading, isAuthenticated, isPasswordlessUser, client } = useClient();
|
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
|
||||||
const confirmPasswordRef = useRef();
|
useClient();
|
||||||
|
const confirmPasswordRef = useRef<HTMLInputElement>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [registering, setRegistering] = useState(false);
|
const [registering, setRegistering] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState<Error>();
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
const [passwordConfirmation, setPasswordConfirmation] = useState("");
|
||||||
const [{ privacyPolicyUrl, recaptchaKey }, register] =
|
const [privacyPolicyUrl, recaptchaKey, register] =
|
||||||
useInteractiveRegistration();
|
useInteractiveRegistration();
|
||||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||||
|
|
||||||
const onSubmitRegisterForm = useCallback(
|
const onSubmitRegisterForm = useCallback(
|
||||||
(e) => {
|
(e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target as HTMLFormElement);
|
||||||
const userName = data.get("userName");
|
const userName = data.get("userName") as string;
|
||||||
const password = data.get("password");
|
const password = data.get("password") as string;
|
||||||
const passwordConfirmation = data.get("passwordConfirmation");
|
const passwordConfirmation = data.get("passwordConfirmation") as string;
|
||||||
|
|
||||||
if (password !== passwordConfirmation) {
|
if (password !== passwordConfirmation) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
const submit = async () => {
|
||||||
setRegistering(true);
|
setRegistering(true);
|
||||||
|
|
||||||
let roomIds;
|
|
||||||
|
|
||||||
if (client && isPasswordlessUser) {
|
|
||||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
|
||||||
roomIds = Array.from(groupCalls).map(
|
|
||||||
(groupCall) => groupCall.room.roomId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const recaptchaResponse = await execute();
|
const recaptchaResponse = await execute();
|
||||||
const newClient = await register(
|
const [newClient, session] = await register(
|
||||||
userName,
|
userName,
|
||||||
password,
|
password,
|
||||||
userName,
|
userName,
|
||||||
recaptchaResponse
|
recaptchaResponse
|
||||||
);
|
);
|
||||||
|
|
||||||
if (roomIds) {
|
if (client && isPasswordlessUser) {
|
||||||
for (const roomId of roomIds) {
|
// Migrate the user's rooms
|
||||||
|
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
|
||||||
|
const roomId = groupCall.room.roomId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await newClient.joinRoom(roomId);
|
await newClient.joinRoom(roomId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Couldn't join room ${roomId}`, error);
|
if (error.errcode === "M_LIMIT_EXCEEDED") {
|
||||||
|
await sleep(error.data.retry_after_ms);
|
||||||
|
await newClient.joinRoom(roomId);
|
||||||
|
} else {
|
||||||
|
captureException(error);
|
||||||
|
console.error(`Couldn't join room ${roomId}`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
setClient(newClient, session);
|
||||||
|
};
|
||||||
|
|
||||||
submit()
|
submit()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (location.state && location.state.from) {
|
if (location.state?.from) {
|
||||||
history.push(location.state.from);
|
history.push(location.state.from);
|
||||||
} else {
|
} else {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
@@ -100,18 +111,23 @@ export function RegisterPage() {
|
|||||||
reset();
|
reset();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[register, location, history, isPasswordlessUser, reset, execute, client]
|
[
|
||||||
|
register,
|
||||||
|
location,
|
||||||
|
history,
|
||||||
|
isPasswordlessUser,
|
||||||
|
reset,
|
||||||
|
execute,
|
||||||
|
client,
|
||||||
|
setClient,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!confirmPasswordRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
||||||
confirmPasswordRef.current.setCustomValidity("Passwords must match");
|
confirmPasswordRef.current?.setCustomValidity("Passwords must match");
|
||||||
} else {
|
} else {
|
||||||
confirmPasswordRef.current.setCustomValidity("");
|
confirmPasswordRef.current?.setCustomValidity("");
|
||||||
}
|
}
|
||||||
}, [password, passwordConfirmation]);
|
}, [password, passwordConfirmation]);
|
||||||
|
|
||||||
@@ -119,7 +135,7 @@ export function RegisterPage() {
|
|||||||
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
}, [history, isAuthenticated, isPasswordlessUser, registering]);
|
}, [loading, history, isAuthenticated, isPasswordlessUser, registering]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
@@ -207,4 +223,4 @@ export function RegisterPage() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
@@ -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,45 +0,0 @@
|
|||||||
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
|
||||||
import { useState, useCallback } from "react";
|
|
||||||
import { useClient } from "../ClientContext";
|
|
||||||
import { initClient, defaultHomeserver } from "../matrix-utils";
|
|
||||||
|
|
||||||
export function useInteractiveLogin() {
|
|
||||||
const { setClient } = useClient();
|
|
||||||
const [state, setState] = useState({ loading: false });
|
|
||||||
|
|
||||||
const auth = useCallback(async (homeserver, username, password) => {
|
|
||||||
const authClient = matrix.createClient(homeserver);
|
|
||||||
|
|
||||||
const interactiveAuth = new InteractiveAuth({
|
|
||||||
matrixClient: authClient,
|
|
||||||
busyChanged(loading) {
|
|
||||||
setState((prev) => ({ ...prev, loading }));
|
|
||||||
},
|
|
||||||
async doRequest(_auth, _background) {
|
|
||||||
return authClient.login("m.login.password", {
|
|
||||||
identifier: {
|
|
||||||
type: "m.id.user",
|
|
||||||
user: username,
|
|
||||||
},
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { user_id, access_token, device_id } =
|
|
||||||
await interactiveAuth.attemptAuth();
|
|
||||||
|
|
||||||
const client = await initClient({
|
|
||||||
baseUrl: defaultHomeserver,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
setClient(client, { user_id, access_token, device_id });
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [state, auth];
|
|
||||||
}
|
|
||||||
69
src/auth/useInteractiveLogin.ts
Normal file
69
src/auth/useInteractiveLogin.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
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 } from "react";
|
||||||
|
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||||
|
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { initClient, defaultHomeserver } from "../matrix-utils";
|
||||||
|
import { Session } from "../ClientContext";
|
||||||
|
|
||||||
|
export const useInteractiveLogin = () =>
|
||||||
|
useCallback<
|
||||||
|
(
|
||||||
|
homeserver: string,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
) => Promise<[MatrixClient, Session]>
|
||||||
|
>(async (homeserver: string, username: string, password: string) => {
|
||||||
|
const authClient = createClient(homeserver);
|
||||||
|
|
||||||
|
const interactiveAuth = new InteractiveAuth({
|
||||||
|
matrixClient: authClient,
|
||||||
|
doRequest: () =>
|
||||||
|
authClient.login("m.login.password", {
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: username,
|
||||||
|
},
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
stateUpdated: null,
|
||||||
|
requestEmailToken: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// XXX: This claims to return an IAuthData which contains none of these
|
||||||
|
// things - the js-sdk types may be wrong?
|
||||||
|
/* eslint-disable camelcase,@typescript-eslint/no-explicit-any */
|
||||||
|
const { user_id, access_token, device_id } =
|
||||||
|
(await interactiveAuth.attemptAuth()) as any;
|
||||||
|
const session = {
|
||||||
|
user_id,
|
||||||
|
access_token,
|
||||||
|
device_id,
|
||||||
|
passwordlessUser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = await initClient({
|
||||||
|
baseUrl: defaultHomeserver,
|
||||||
|
accessToken: access_token,
|
||||||
|
userId: user_id,
|
||||||
|
deviceId: device_id,
|
||||||
|
});
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
return [client, session];
|
||||||
|
}, []);
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
|
||||||
import { useClient } from "../ClientContext";
|
|
||||||
import { initClient, defaultHomeserver } from "../matrix-utils";
|
|
||||||
|
|
||||||
export function useInteractiveRegistration() {
|
|
||||||
const { setClient } = useClient();
|
|
||||||
const [state, setState] = useState({ privacyPolicyUrl: "#", loading: false });
|
|
||||||
|
|
||||||
const authClientRef = useRef();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
authClientRef.current = matrix.createClient(defaultHomeserver);
|
|
||||||
|
|
||||||
authClientRef.current.registerRequest({}).catch((error) => {
|
|
||||||
const privacyPolicyUrl =
|
|
||||||
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url;
|
|
||||||
|
|
||||||
const recaptchaKey = error.data?.params["m.login.recaptcha"]?.public_key;
|
|
||||||
|
|
||||||
if (privacyPolicyUrl || recaptchaKey) {
|
|
||||||
setState((prev) => ({ ...prev, privacyPolicyUrl, recaptchaKey }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const register = useCallback(
|
|
||||||
async (
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
displayName,
|
|
||||||
recaptchaResponse,
|
|
||||||
passwordlessUser
|
|
||||||
) => {
|
|
||||||
const interactiveAuth = new InteractiveAuth({
|
|
||||||
matrixClient: authClientRef.current,
|
|
||||||
busyChanged(loading) {
|
|
||||||
setState((prev) => ({ ...prev, loading }));
|
|
||||||
},
|
|
||||||
async doRequest(auth, _background) {
|
|
||||||
return authClientRef.current.registerRequest({
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
auth: auth || undefined,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
stateUpdated(nextStage, status) {
|
|
||||||
if (status.error) {
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStage === "m.login.terms") {
|
|
||||||
interactiveAuth.submitAuthDict({
|
|
||||||
type: "m.login.terms",
|
|
||||||
});
|
|
||||||
} else if (nextStage === "m.login.recaptcha") {
|
|
||||||
interactiveAuth.submitAuthDict({
|
|
||||||
type: "m.login.recaptcha",
|
|
||||||
response: recaptchaResponse,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { user_id, access_token, device_id } =
|
|
||||||
await interactiveAuth.attemptAuth();
|
|
||||||
|
|
||||||
const client = await initClient({
|
|
||||||
baseUrl: defaultHomeserver,
|
|
||||||
accessToken: access_token,
|
|
||||||
userId: user_id,
|
|
||||||
deviceId: device_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.setDisplayName(displayName);
|
|
||||||
|
|
||||||
const session = { user_id, device_id, access_token, passwordlessUser };
|
|
||||||
|
|
||||||
if (passwordlessUser) {
|
|
||||||
session.tempPassword = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
setClient(client, session);
|
|
||||||
|
|
||||||
const user = client.getUser(client.getUserId());
|
|
||||||
|
|
||||||
user.setRawDisplayName(displayName);
|
|
||||||
user.setDisplayName(displayName);
|
|
||||||
|
|
||||||
return client;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return [state, register];
|
|
||||||
}
|
|
||||||
124
src/auth/useInteractiveRegistration.ts
Normal file
124
src/auth/useInteractiveRegistration.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
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, useCallback, useRef } from "react";
|
||||||
|
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||||
|
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { initClient, defaultHomeserver } from "../matrix-utils";
|
||||||
|
import { Session } from "../ClientContext";
|
||||||
|
|
||||||
|
export const useInteractiveRegistration = (): [
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName: string,
|
||||||
|
recaptchaResponse: string,
|
||||||
|
passwordlessUser?: boolean
|
||||||
|
) => Promise<[MatrixClient, Session]>
|
||||||
|
] => {
|
||||||
|
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string>();
|
||||||
|
const [recaptchaKey, setRecaptchaKey] = useState<string>();
|
||||||
|
|
||||||
|
const authClient = useRef<MatrixClient>();
|
||||||
|
if (!authClient.current) {
|
||||||
|
authClient.current = createClient(defaultHomeserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authClient.current.registerRequest({}).catch((error) => {
|
||||||
|
setPrivacyPolicyUrl(
|
||||||
|
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
|
||||||
|
);
|
||||||
|
setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const register = useCallback(
|
||||||
|
async (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName: string,
|
||||||
|
recaptchaResponse: string,
|
||||||
|
passwordlessUser?: boolean
|
||||||
|
): Promise<[MatrixClient, Session]> => {
|
||||||
|
const interactiveAuth = new InteractiveAuth({
|
||||||
|
matrixClient: authClient.current,
|
||||||
|
doRequest: (auth) =>
|
||||||
|
authClient.current.registerRequest({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
auth: auth || undefined,
|
||||||
|
}),
|
||||||
|
stateUpdated: (nextStage, status) => {
|
||||||
|
if (status.error) {
|
||||||
|
throw new Error(status.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextStage === "m.login.terms") {
|
||||||
|
interactiveAuth.submitAuthDict({
|
||||||
|
type: "m.login.terms",
|
||||||
|
});
|
||||||
|
} else if (nextStage === "m.login.recaptcha") {
|
||||||
|
interactiveAuth.submitAuthDict({
|
||||||
|
type: "m.login.recaptcha",
|
||||||
|
response: recaptchaResponse,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestEmailToken: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// XXX: This claims to return an IAuthData which contains none of these
|
||||||
|
// things - the js-sdk types may be wrong?
|
||||||
|
/* eslint-disable camelcase,@typescript-eslint/no-explicit-any */
|
||||||
|
const { user_id, access_token, device_id } =
|
||||||
|
(await interactiveAuth.attemptAuth()) as any;
|
||||||
|
|
||||||
|
const client = await initClient({
|
||||||
|
baseUrl: defaultHomeserver,
|
||||||
|
accessToken: access_token,
|
||||||
|
userId: user_id,
|
||||||
|
deviceId: device_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.setDisplayName(displayName);
|
||||||
|
|
||||||
|
const session: Session = {
|
||||||
|
user_id,
|
||||||
|
device_id,
|
||||||
|
access_token,
|
||||||
|
passwordlessUser,
|
||||||
|
};
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
if (passwordlessUser) {
|
||||||
|
session.tempPassword = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = client.getUser(client.getUserId());
|
||||||
|
user.setRawDisplayName(displayName);
|
||||||
|
user.setDisplayName(displayName);
|
||||||
|
|
||||||
|
return [client, session];
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [privacyPolicyUrl, recaptchaKey, register];
|
||||||
|
};
|
||||||
@@ -1,49 +1,62 @@
|
|||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
/*
|
||||||
|
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, useCallback, useRef, useState } from "react";
|
import { useEffect, useCallback, useRef, useState } from "react";
|
||||||
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
mxOnRecaptchaLoaded: () => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const RECAPTCHA_SCRIPT_URL =
|
const RECAPTCHA_SCRIPT_URL =
|
||||||
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
|
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
|
||||||
|
|
||||||
export function useRecaptcha(sitekey) {
|
interface RecaptchaPromiseRef {
|
||||||
|
resolve: (response: string) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRecaptcha = (sitekey: string) => {
|
||||||
const [recaptchaId] = useState(() => randomString(16));
|
const [recaptchaId] = useState(() => randomString(16));
|
||||||
const promiseRef = useRef();
|
const promiseRef = useRef<RecaptchaPromiseRef>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sitekey) {
|
if (!sitekey) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRecaptchaLoaded = () => {
|
const onRecaptchaLoaded = () => {
|
||||||
if (!document.getElementById(recaptchaId)) {
|
if (!document.getElementById(recaptchaId)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.grecaptcha.render(recaptchaId, {
|
window.grecaptcha.render(recaptchaId, {
|
||||||
sitekey,
|
sitekey,
|
||||||
size: "invisible",
|
size: "invisible",
|
||||||
callback: (response) => {
|
callback: (response: string) => promiseRef.current?.resolve(response),
|
||||||
if (promiseRef.current) {
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
promiseRef.current.resolve(response);
|
"error-callback": () => promiseRef.current?.reject(new Error()),
|
||||||
}
|
|
||||||
},
|
|
||||||
"error-callback": (error) => {
|
|
||||||
if (promiseRef.current) {
|
|
||||||
promiseRef.current.reject(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (typeof window.grecaptcha?.render === "function") {
|
||||||
typeof window.grecaptcha !== "undefined" &&
|
|
||||||
typeof window.grecaptcha.render === "function"
|
|
||||||
) {
|
|
||||||
onRecaptchaLoaded();
|
onRecaptchaLoaded();
|
||||||
} else {
|
} else {
|
||||||
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
|
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
|
||||||
|
|
||||||
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
|
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
|
||||||
const scriptTag = document.createElement("script");
|
const scriptTag = document.createElement("script") as HTMLScriptElement;
|
||||||
scriptTag.src = RECAPTCHA_SCRIPT_URL;
|
scriptTag.src = RECAPTCHA_SCRIPT_URL;
|
||||||
scriptTag.async = true;
|
scriptTag.async = true;
|
||||||
document.body.appendChild(scriptTag);
|
document.body.appendChild(scriptTag);
|
||||||
@@ -64,7 +77,7 @@ export function useRecaptcha(sitekey) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const observer = new MutationObserver((mutationsList) => {
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
for (const item of mutationsList) {
|
for (const item of mutationsList) {
|
||||||
if (item.target?.style?.visibility !== "visible") {
|
if ((item.target as HTMLElement)?.style?.visibility !== "visible") {
|
||||||
reject(new Error("Recaptcha dismissed"));
|
reject(new Error("Recaptcha dismissed"));
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
return;
|
return;
|
||||||
@@ -85,7 +98,7 @@ export function useRecaptcha(sitekey) {
|
|||||||
|
|
||||||
window.grecaptcha.execute();
|
window.grecaptcha.execute();
|
||||||
|
|
||||||
const iframe = document.querySelector(
|
const iframe = document.querySelector<HTMLIFrameElement>(
|
||||||
'iframe[src*="recaptcha/api2/bframe"]'
|
'iframe[src*="recaptcha/api2/bframe"]'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,13 +108,11 @@ export function useRecaptcha(sitekey) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [recaptchaId, sitekey]);
|
}, [sitekey]);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
if (window.grecaptcha) {
|
window.grecaptcha?.reset();
|
||||||
window.grecaptcha.reset();
|
}, []);
|
||||||
}
|
|
||||||
}, [recaptchaId]);
|
|
||||||
|
|
||||||
return { execute, reset, recaptchaId };
|
return { execute, reset, recaptchaId };
|
||||||
}
|
};
|
||||||
@@ -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,9 @@ 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 { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.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";
|
||||||
@@ -18,8 +37,10 @@ export const variantToClassName = {
|
|||||||
icon: [styles.iconButton],
|
icon: [styles.iconButton],
|
||||||
secondary: [styles.secondary],
|
secondary: [styles.secondary],
|
||||||
copy: [styles.copyButton],
|
copy: [styles.copyButton],
|
||||||
|
secondaryCopy: [styles.secondaryCopy, styles.copyButton],
|
||||||
iconCopy: [styles.iconCopyButton],
|
iconCopy: [styles.iconCopyButton],
|
||||||
secondaryCopy: [styles.copyButton],
|
secondaryHangup: [styles.secondaryHangup],
|
||||||
|
dropdown: [styles.dropdownButton],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sizeToClassName = {
|
export const sizeToClassName = {
|
||||||
@@ -67,13 +88,13 @@ export const Button = forwardRef(
|
|||||||
{
|
{
|
||||||
[styles.on]: on,
|
[styles.on]: on,
|
||||||
[styles.off]: off,
|
[styles.off]: off,
|
||||||
[styles.secondaryCopy]: variant === "secondaryCopy",
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
{...mergeProps(rest, filteredButtonProps)}
|
{...mergeProps(rest, filteredButtonProps)}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
{variant === "dropdown" && <ArrowDownIcon />}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -126,3 +147,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,7 +20,9 @@ limitations under the License.
|
|||||||
.iconButton,
|
.iconButton,
|
||||||
.iconCopyButton,
|
.iconCopyButton,
|
||||||
.secondary,
|
.secondary,
|
||||||
.copyButton {
|
.secondaryHangup,
|
||||||
|
.copyButton,
|
||||||
|
.dropdownButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -34,6 +36,7 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.secondary,
|
.secondary,
|
||||||
|
.secondaryHangup,
|
||||||
.button,
|
.button,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
padding: 7px 15px;
|
padding: 7px 15px;
|
||||||
@@ -43,8 +46,8 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
color: #fff;
|
color: var(--primary-content);
|
||||||
background-color: var(--primaryColor);
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:focus,
|
.button:focus,
|
||||||
@@ -53,6 +56,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;
|
||||||
}
|
}
|
||||||
@@ -62,46 +66,46 @@ limitations under the License.
|
|||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton:hover,
|
.toolbarButton:hover,
|
||||||
.toolbarButtonSecondary:hover {
|
.toolbarButtonSecondary:hover {
|
||||||
background-color: var(--bgColor4);
|
background-color: var(--quinary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton.on,
|
.toolbarButton.on,
|
||||||
.toolbarButton.off {
|
.toolbarButton.off {
|
||||||
background-color: #ffffff;
|
background-color: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButtonSecondary.on {
|
.toolbarButtonSecondary.on {
|
||||||
background-color: #0dbd8b;
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton:not(.stroke) svg * {
|
.iconButton:not(.stroke) svg * {
|
||||||
fill: #ffffff;
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton:not(.stroke):hover svg * {
|
.iconButton:not(.stroke):hover svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton.on:not(.stroke) svg * {
|
.iconButton.on:not(.stroke) svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton.on.stroke svg * {
|
.iconButton.on.stroke svg * {
|
||||||
stroke: #0dbd8b;
|
stroke: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hangupButton,
|
.hangupButton,
|
||||||
.hangupButton:hover {
|
.hangupButton:hover {
|
||||||
background-color: #ff5b55;
|
background-color: var(--alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton.on svg * {
|
.toolbarButton.on svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton.off svg * {
|
.toolbarButton.off svg * {
|
||||||
@@ -109,19 +113,25 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButtonSecondary.on svg * {
|
.toolbarButtonSecondary.on svg * {
|
||||||
fill: #ffffff;
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary,
|
.secondary,
|
||||||
.copyButton {
|
.copyButton {
|
||||||
color: #0dbd8b;
|
color: var(--accent);
|
||||||
border: 2px solid #0dbd8b;
|
border: 2px solid var(--accent);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryHangup {
|
||||||
|
color: var(--alert);
|
||||||
|
border: 2px solid var(--alert);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyButton.secondaryCopy {
|
.copyButton.secondaryCopy {
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
border-color: var(--textColor1);
|
border-color: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyButton {
|
.copyButton {
|
||||||
@@ -144,12 +154,12 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.copyButton:not(.on) svg * {
|
.copyButton:not(.on) svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyButton.on {
|
.copyButton.on {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: #0dbd8b;
|
background-color: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,21 +168,40 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.copyButton.secondaryCopy:not(.on) svg * {
|
.copyButton.secondaryCopy:not(.on) svg * {
|
||||||
fill: var(--textColor1);
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconCopyButton svg * {
|
.iconCopyButton svg * {
|
||||||
fill: var(--textColor3);
|
fill: var(--tertiary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconCopyButton:hover svg * {
|
.iconCopyButton:hover svg * {
|
||||||
fill: #0dbd8b;
|
fill: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconCopyButton.on svg *,
|
.iconCopyButton.on svg *,
|
||||||
.iconCopyButton.on:hover svg * {
|
.iconCopyButton.on:hover svg * {
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: #0dbd8b;
|
stroke: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownButton {
|
||||||
|
color: var(--primary-content);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownButton:hover,
|
||||||
|
.dropdownButton.on {
|
||||||
|
background-color: var(--quinary-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownButton svg {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownButton svg * {
|
||||||
|
fill: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg {
|
.lg {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
.callTile {
|
.callTile {
|
||||||
height: 95px;
|
height: 95px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
src/home/CallTypeDropdown.module.css
Normal file
3
src/home/CallTypeDropdown.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
69
src/home/CallTypeDropdown.tsx
Normal file
69
src/home/CallTypeDropdown.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
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, { FC } from "react";
|
||||||
|
import { Item } from "@react-stately/collections";
|
||||||
|
|
||||||
|
import { Headline } from "../typography/Typography";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
||||||
|
import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||||
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||||
|
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||||
|
import styles from "./CallTypeDropdown.module.css";
|
||||||
|
import commonStyles from "./common.module.css";
|
||||||
|
import menuStyles from "../Menu.module.css";
|
||||||
|
import { Menu } from "../Menu";
|
||||||
|
|
||||||
|
export enum CallType {
|
||||||
|
Video = "video",
|
||||||
|
Radio = "radio",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
callType: CallType;
|
||||||
|
setCallType: (value: CallType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
|
||||||
|
return (
|
||||||
|
<PopoverMenuTrigger placement="bottom">
|
||||||
|
<Button variant="dropdown" className={commonStyles.headline}>
|
||||||
|
<Headline className={styles.label}>
|
||||||
|
{callType === CallType.Video ? "Video call" : "Walkie-talkie call"}
|
||||||
|
</Headline>
|
||||||
|
</Button>
|
||||||
|
{(props) => (
|
||||||
|
<Menu {...props} label="Call type menu" onAction={setCallType}>
|
||||||
|
<Item key={CallType.Video} textValue="Video call">
|
||||||
|
<VideoIcon />
|
||||||
|
<span>Video call</span>
|
||||||
|
{callType === CallType.Video && (
|
||||||
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
|
)}
|
||||||
|
</Item>
|
||||||
|
<Item key={CallType.Radio} textValue="Walkie-talkie call">
|
||||||
|
<MicIcon />
|
||||||
|
<span>Walkie-talkie call</span>
|
||||||
|
{callType === CallType.Radio && (
|
||||||
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
|
)}
|
||||||
|
</Item>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</PopoverMenuTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,5 +1,21 @@
|
|||||||
|
/*
|
||||||
|
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, roomAliasLocalpartFromRoomName } from "../matrix-utils";
|
||||||
import { useGroupCallRooms } from "./useGroupCallRooms";
|
import { useGroupCallRooms } from "./useGroupCallRooms";
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||||
import commonStyles from "./common.module.css";
|
import commonStyles from "./common.module.css";
|
||||||
@@ -11,10 +27,12 @@ import { UserMenuContainer } from "../UserMenuContainer";
|
|||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Headline, Title } from "../typography/Typography";
|
import { Title } from "../typography/Typography";
|
||||||
import { Form } from "../form/Form";
|
import { Form } from "../form/Form";
|
||||||
|
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
||||||
|
|
||||||
export function RegisteredView({ client }) {
|
export function RegisteredView({ client }) {
|
||||||
|
const [callType, setCallType] = useState(CallType.Video);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -23,12 +41,13 @@ export function RegisteredView({ client }) {
|
|||||||
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 = callType === CallType.Radio;
|
||||||
|
|
||||||
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}`);
|
||||||
@@ -37,7 +56,7 @@ export function RegisteredView({ client }) {
|
|||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
if (error.errcode === "M_ROOM_IN_USE") {
|
if (error.errcode === "M_ROOM_IN_USE") {
|
||||||
setExistingRoomId(roomAliasFromRoomName(roomName));
|
setExistingRoomId(roomAliasLocalpartFromRoomName(roomName));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
modalState.open();
|
modalState.open();
|
||||||
@@ -49,7 +68,7 @@ export function RegisteredView({ client }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[client]
|
[client, callType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentRooms = useGroupCallRooms(client);
|
const recentRooms = useGroupCallRooms(client);
|
||||||
@@ -60,6 +79,9 @@ export function RegisteredView({ client }) {
|
|||||||
history.push(`/${existingRoomId}`);
|
history.push(`/${existingRoomId}`);
|
||||||
}, [history, existingRoomId]);
|
}, [history, existingRoomId]);
|
||||||
|
|
||||||
|
const callNameLabel =
|
||||||
|
callType === CallType.Video ? "Video call name" : "Walkie-talkie call name";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -73,20 +95,19 @@ export function RegisteredView({ client }) {
|
|||||||
<div className={commonStyles.container}>
|
<div className={commonStyles.container}>
|
||||||
<main className={commonStyles.main}>
|
<main className={commonStyles.main}>
|
||||||
<HeaderLogo className={commonStyles.logo} />
|
<HeaderLogo className={commonStyles.logo} />
|
||||||
<Headline className={commonStyles.headline}>
|
<CallTypeDropdown callType={callType} setCallType={setCallType} />
|
||||||
Enter a call name
|
|
||||||
</Headline>
|
|
||||||
<Form className={styles.form} onSubmit={onSubmit}>
|
<Form className={styles.form} onSubmit={onSubmit}>
|
||||||
<FieldRow className={styles.fieldRow}>
|
<FieldRow className={styles.fieldRow}>
|
||||||
<InputField
|
<InputField
|
||||||
id="callName"
|
id="callName"
|
||||||
name="callName"
|
name="callName"
|
||||||
label="Call name"
|
label={callNameLabel}
|
||||||
placeholder="Call name"
|
placeholder={callNameLabel}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fieldRow {
|
.fieldRow {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldRow:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +1,110 @@
|
|||||||
|
/*
|
||||||
|
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 { useClient } from "../ClientContext";
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { createRoom, roomAliasFromRoomName } from "../matrix-utils";
|
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
|
||||||
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
import { useInteractiveRegistration } from "../auth/useInteractiveRegistration";
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||||
import { useRecaptcha } from "../auth/useRecaptcha";
|
import { useRecaptcha } from "../auth/useRecaptcha";
|
||||||
import { Body, Caption, Link, Headline } from "../typography/Typography";
|
import { Body, Caption, Link, Headline } from "../typography/Typography";
|
||||||
import { Form } from "../form/Form";
|
import { Form } from "../form/Form";
|
||||||
|
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
||||||
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";
|
||||||
|
|
||||||
export function UnauthenticatedView() {
|
export function UnauthenticatedView() {
|
||||||
|
const { setClient } = useClient();
|
||||||
|
const [callType, setCallType] = useState(CallType.Video);
|
||||||
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] =
|
||||||
useInteractiveRegistration();
|
useInteractiveRegistration();
|
||||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||||
|
|
||||||
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
const [onFinished, setOnFinished] = useState();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
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 displayName = data.get("displayName");
|
const displayName = data.get("displayName");
|
||||||
|
const ptt = callType === CallType.Radio;
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const recaptchaResponse = await execute();
|
const recaptchaResponse = await execute();
|
||||||
const userName = generateRandomName();
|
const userName = generateRandomName();
|
||||||
const client = await register(
|
const [client, session] = await register(
|
||||||
userName,
|
userName,
|
||||||
randomString(16),
|
randomString(16),
|
||||||
displayName,
|
displayName,
|
||||||
recaptchaResponse,
|
recaptchaResponse,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
const roomIdOrAlias = await createRoom(client, roomName);
|
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
let roomIdOrAlias;
|
||||||
history.push(`/room/${roomIdOrAlias}`);
|
try {
|
||||||
|
roomIdOrAlias = await createRoom(client, roomName, ptt);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.errcode === "M_ROOM_IN_USE") {
|
||||||
|
setOnFinished(() => () => {
|
||||||
|
setClient(client, session);
|
||||||
|
const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName);
|
||||||
|
const [, serverName] = client.getUserId().split(":");
|
||||||
|
history.push(`/room/#${aliasLocalpart}:${serverName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
modalState.open();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only consider the registration successful if we managed to create the room, too
|
||||||
|
setClient(client, session);
|
||||||
|
history.push(`/room/${roomIdOrAlias}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
if (error.errcode === "M_ROOM_IN_USE") {
|
console.error(error);
|
||||||
setExistingRoomId(roomAliasFromRoomName(roomName));
|
setLoading(false);
|
||||||
setLoading(false);
|
setError(error);
|
||||||
setError(undefined);
|
reset();
|
||||||
modalState.open();
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
|
||||||
setLoading(false);
|
|
||||||
setError(error);
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[register, reset, execute]
|
[register, reset, execute, history, callType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const callNameLabel =
|
||||||
const [existingRoomId, setExistingRoomId] = useState();
|
callType === CallType.Video ? "Video call name" : "Walkie-talkie call name";
|
||||||
const history = useHistory();
|
|
||||||
const onJoinExistingRoom = useCallback(() => {
|
|
||||||
history.push(`/${existingRoomId}`);
|
|
||||||
}, [history, existingRoomId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -85,16 +119,14 @@ export function UnauthenticatedView() {
|
|||||||
<div className={commonStyles.container}>
|
<div className={commonStyles.container}>
|
||||||
<main className={commonStyles.main}>
|
<main className={commonStyles.main}>
|
||||||
<HeaderLogo className={commonStyles.logo} />
|
<HeaderLogo className={commonStyles.logo} />
|
||||||
<Headline className={commonStyles.headline}>
|
<CallTypeDropdown callType={callType} setCallType={setCallType} />
|
||||||
Enter a call name
|
|
||||||
</Headline>
|
|
||||||
<Form className={styles.form} onSubmit={onSubmit}>
|
<Form className={styles.form} onSubmit={onSubmit}>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="callName"
|
id="callName"
|
||||||
name="callName"
|
name="callName"
|
||||||
label="Call name"
|
label={callNameLabel}
|
||||||
placeholder="Call name"
|
placeholder={callNameLabel}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -141,7 +173,7 @@ export function UnauthenticatedView() {
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{modalState.isOpen && (
|
{modalState.isOpen && (
|
||||||
<JoinExistingCallModal onJoin={onJoinExistingRoom} {...modalProps} />
|
<JoinExistingCallModal onJoin={onFinished} {...modalProps} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = {};
|
||||||
@@ -63,7 +79,7 @@ export function useGroupCallRooms(client) {
|
|||||||
return {
|
return {
|
||||||
roomId: room.getCanonicalAlias() || room.roomId,
|
roomId: room.getCanonicalAlias() || room.roomId,
|
||||||
roomName: room.name,
|
roomName: room.name,
|
||||||
avatarUrl: null,
|
avatarUrl: room.getMxcAvatarUrl(),
|
||||||
room,
|
room,
|
||||||
groupCall,
|
groupCall,
|
||||||
participants: [...groupCall.participants],
|
participants: [...groupCall.participants],
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 New Vector Ltd
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -25,19 +25,19 @@ limitations under the License.
|
|||||||
:root {
|
:root {
|
||||||
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
|
--inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f,
|
||||||
U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF;
|
U+25c2-2664, U+2666-2763, U+2765-2b05, U+2b07-2b1b, U+2b1d-10FFFF;
|
||||||
--primaryColor: #0dbd8b;
|
--accent: #0dbd8b;
|
||||||
--bgColor1: #15191e;
|
--accent-20: #0dbd8b33;
|
||||||
--bgColor2: #21262c;
|
--alert: #ff5b55;
|
||||||
--bgColor3: #444;
|
--alert-20: #ff5b5533;
|
||||||
--bgColor4: #394049;
|
--links: #0086e6;
|
||||||
--bgColor5: #8d97a5;
|
--primary-content: #ffffff;
|
||||||
--textColor1: #fff;
|
--secondary-content: #a9b2bc;
|
||||||
--textColor2: #6f7882;
|
--tertiary-content: #8e99a4;
|
||||||
--textColor3: #8e99a4;
|
--quaternary-content: #6f7882;
|
||||||
--textColor4: #a9b2bc;
|
--quinary-content: #394049;
|
||||||
--inputBorderColor: #394049;
|
--system: #21262c;
|
||||||
--inputBorderColorFocused: #0086e6;
|
--background: #15191e;
|
||||||
--linkColor: #0086e6;
|
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -121,8 +121,9 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--bgColor1);
|
background-color: var(--background);
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
|
color-scheme: dark;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
@@ -181,7 +182,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--primaryColor);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +194,8 @@ a:active {
|
|||||||
hr {
|
hr {
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--bgColor4);
|
border-top: 1px solid var(--quinary-content);
|
||||||
color: var(--textColor2);
|
color: var(--quaternary-content);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 11px;
|
bottom: 11px;
|
||||||
right: -4px;
|
right: -4px;
|
||||||
background-color: var(--bgColor4);
|
background-color: var(--quinary-content);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -37,5 +37,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.removeButton {
|
.removeButton {
|
||||||
color: #0dbd8b;
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
.inputField {
|
.inputField {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: border-color 0.25s;
|
transition: border-color 0.25s;
|
||||||
border: 1px solid var(--inputBorderColor);
|
border: 1px solid var(--quinary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input,
|
.inputField input,
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 12px 9px 10px 9px;
|
padding: 12px 9px 10px 9px;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
background-color: var(--bgColor1);
|
background-color: var(--background);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
.inputField.disabled input,
|
.inputField.disabled input,
|
||||||
.inputField.disabled textarea,
|
.inputField.disabled textarea,
|
||||||
.inputField.disabled span {
|
.inputField.disabled span {
|
||||||
color: var(--textColor2);
|
color: var(--quaternary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField span {
|
.inputField span {
|
||||||
@@ -65,13 +65,13 @@
|
|||||||
.inputField input:placeholder-shown:focus::placeholder,
|
.inputField input:placeholder-shown:focus::placeholder,
|
||||||
.inputField textarea:placeholder-shown:focus::placeholder {
|
.inputField textarea:placeholder-shown:focus::placeholder {
|
||||||
transition: color 0.25s ease-in 0.1s;
|
transition: color 0.25s ease-in 0.1s;
|
||||||
color: var(--textColor2);
|
color: var(--quaternary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField label {
|
.inputField label {
|
||||||
transition: font-size 0.25s ease-out 0.1s, color 0.25s ease-out 0.1s,
|
transition: font-size 0.25s ease-out 0.1s, color 0.25s ease-out 0.1s,
|
||||||
top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s;
|
top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s;
|
||||||
color: var(--textColor3);
|
color: var(--tertiary-content);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inputField:focus-within {
|
.inputField:focus-within {
|
||||||
border-color: var(--inputBorderColorFocused);
|
border-color: var(--links);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField input:focus,
|
.inputField input:focus,
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
.inputField textarea:focus + label,
|
.inputField textarea:focus + label,
|
||||||
.inputField textarea:not(:placeholder-shown) + label,
|
.inputField textarea:not(:placeholder-shown) + label,
|
||||||
.inputField.prefix textarea + label {
|
.inputField.prefix textarea + label {
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
|
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
|
||||||
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
|
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
|
|
||||||
.inputField input:focus + label,
|
.inputField input:focus + label,
|
||||||
.inputField textarea:focus + label {
|
.inputField textarea:focus + label {
|
||||||
color: var(--inputBorderColorFocused);
|
color: var(--links);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkboxField {
|
.checkboxField {
|
||||||
@@ -154,12 +154,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.checkbox svg * {
|
.checkbox svg * {
|
||||||
stroke: #fff;
|
stroke: var(--primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkboxField input[type="checkbox"]:checked + label > .checkbox {
|
.checkboxField input[type="checkbox"]:checked + label > .checkbox {
|
||||||
background: var(--primaryColor);
|
background: var(--accent);
|
||||||
border-color: var(--primaryColor);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkboxField input[type="checkbox"]:checked + label > .checkbox svg {
|
.checkboxField input[type="checkbox"]:checked + label > .checkbox svg {
|
||||||
@@ -167,12 +167,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.checkboxField:focus-within .checkbox {
|
.checkboxField:focus-within .checkbox {
|
||||||
border: 1.5px solid var(--inputBorderColorFocused) !important;
|
border: 1.5px solid var(--links) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorMessage {
|
.errorMessage {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #ff5b55;
|
color: var(--alert);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
background-color: var(--bgColor1);
|
background-color: var(--background);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--inputBorderColor);
|
border: 1px solid var(--quinary-content);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: var(--textColor1);
|
color: var(--primary-content);
|
||||||
height: 40px;
|
height: 40px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
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: var(--quaternary-content);
|
||||||
|
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: var(--background);
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 10px 8px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--quaternary-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .button {
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .ball {
|
||||||
|
left: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .label {
|
||||||
|
color: var(--primary-content);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 New Vector Ltd
|
Copyright 2021-2022 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,13 +14,20 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// We need to import this somewhere, once, so that the correct 'request'
|
||||||
|
// function gets set. It needs to be not in the same file as we use
|
||||||
|
// createClient, or the typescript transpiler gets confused about
|
||||||
|
// dependency references.
|
||||||
|
import "matrix-js-sdk/src/browser-index";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { createBrowserHistory } from "history";
|
import { createBrowserHistory } from "history";
|
||||||
import "./index.css";
|
|
||||||
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 "./index.css";
|
||||||
|
import App from "./App";
|
||||||
import { ErrorView } from "./FullScreenView";
|
import { ErrorView } from "./FullScreenView";
|
||||||
import { init as initRageshake } from "./settings/rageshake";
|
import { init as initRageshake } from "./settings/rageshake";
|
||||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||||
@@ -31,29 +38,50 @@ console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
|||||||
|
|
||||||
if (import.meta.env.VITE_CUSTOM_THEME) {
|
if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||||
const style = document.documentElement.style;
|
const style = document.documentElement.style;
|
||||||
style.setProperty("--primaryColor", import.meta.env.VITE_PRIMARY_COLOR);
|
style.setProperty("--accent", import.meta.env.VITE_THEME_ACCENT as string);
|
||||||
style.setProperty("--bgColor1", import.meta.env.VITE_BG_COLOR_1);
|
|
||||||
style.setProperty("--bgColor2", import.meta.env.VITE_BG_COLOR_2);
|
|
||||||
style.setProperty("--bgColor3", import.meta.env.VITE_BG_COLOR_3);
|
|
||||||
style.setProperty("--bgColor4", import.meta.env.VITE_BG_COLOR_4);
|
|
||||||
style.setProperty("--bgColor5", import.meta.env.VITE_BG_COLOR_5);
|
|
||||||
style.setProperty("--textColor1", import.meta.env.VITE_TEXT_COLOR_1);
|
|
||||||
style.setProperty("--textColor2", import.meta.env.VITE_TEXT_COLOR_2);
|
|
||||||
style.setProperty("--textColor4", import.meta.env.VITE_TEXT_COLOR_4);
|
|
||||||
style.setProperty(
|
style.setProperty(
|
||||||
"--inputBorderColor",
|
"--accent-20",
|
||||||
import.meta.env.VITE_INPUT_BORDER_COLOR
|
import.meta.env.VITE_THEME_ACCENT_20 as string
|
||||||
|
);
|
||||||
|
style.setProperty("--alert", import.meta.env.VITE_THEME_ALERT as string);
|
||||||
|
style.setProperty(
|
||||||
|
"--alert-20",
|
||||||
|
import.meta.env.VITE_THEME_ALERT_20 as string
|
||||||
|
);
|
||||||
|
style.setProperty("--links", import.meta.env.VITE_THEME_LINKS as string);
|
||||||
|
style.setProperty(
|
||||||
|
"--primary-content",
|
||||||
|
import.meta.env.VITE_THEME_PRIMARY_CONTENT as string
|
||||||
);
|
);
|
||||||
style.setProperty(
|
style.setProperty(
|
||||||
"--inputBorderColorFocused",
|
"--secondary-content",
|
||||||
import.meta.env.VITE_INPUT_BORDER_COLOR_FOCUSED
|
import.meta.env.VITE_THEME_SECONDARY_CONTENT as string
|
||||||
|
);
|
||||||
|
style.setProperty(
|
||||||
|
"--tertiary-content",
|
||||||
|
import.meta.env.VITE_THEME_TERTIARY_CONTENT as string
|
||||||
|
);
|
||||||
|
style.setProperty(
|
||||||
|
"--quaternary-content",
|
||||||
|
import.meta.env.VITE_THEME_QUATERNARY_CONTENT as string
|
||||||
|
);
|
||||||
|
style.setProperty(
|
||||||
|
"--quinary-content",
|
||||||
|
import.meta.env.VITE_THEME_QUINARY_CONTENT as string
|
||||||
|
);
|
||||||
|
style.setProperty("--system", import.meta.env.VITE_THEME_SYSTEM as string);
|
||||||
|
style.setProperty(
|
||||||
|
"--background",
|
||||||
|
import.meta.env.VITE_THEME_BACKGROUND as string
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
dsn: import.meta.env.VITE_SENTRY_DSN as string,
|
||||||
|
environment:
|
||||||
|
(import.meta.env.VITE_SENTRY_ENVIRONMENT as string) ?? "production",
|
||||||
integrations: [
|
integrations: [
|
||||||
new Integrations.BrowserTracing({
|
new Integrations.BrowserTracing({
|
||||||
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
|
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import matrix from "matrix-js-sdk/src/browser-index";
|
|
||||||
import {
|
|
||||||
GroupCallIntent,
|
|
||||||
GroupCallType,
|
|
||||||
} from "matrix-js-sdk/src/browser-index";
|
|
||||||
|
|
||||||
export const defaultHomeserver =
|
|
||||||
import.meta.env.VITE_DEFAULT_HOMESERVER ||
|
|
||||||
`${window.location.protocol}//${window.location.host}`;
|
|
||||||
|
|
||||||
export const defaultHomeserverHost = new URL(defaultHomeserver).host;
|
|
||||||
|
|
||||||
function waitForSync(client) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const onSync = (state, _old, data) => {
|
|
||||||
if (state === "PREPARED") {
|
|
||||||
resolve();
|
|
||||||
client.removeListener("sync", onSync);
|
|
||||||
} else if (state === "ERROR") {
|
|
||||||
reject(data?.error);
|
|
||||||
client.removeListener("sync", onSync);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
client.on("sync", onSync);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initClient(clientOptions) {
|
|
||||||
const client = matrix.createClient({
|
|
||||||
...clientOptions,
|
|
||||||
useAuthorizationHeader: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.startClient({
|
|
||||||
// dirty hack to reduce chance of gappy syncs
|
|
||||||
// should be fixed by spotting gaps and backpaginating
|
|
||||||
initialSyncLimit: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForSync(client);
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function roomAliasFromRoomName(roomName) {
|
|
||||||
return roomName
|
|
||||||
.trim()
|
|
||||||
.replace(/\s/g, "-")
|
|
||||||
.replace(/[^\w-]/g, "")
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function roomNameFromRoomId(roomId) {
|
|
||||||
return roomId
|
|
||||||
.match(/([^:]+):.*$/)[1]
|
|
||||||
.substring(1)
|
|
||||||
.split("-")
|
|
||||||
.map((part) =>
|
|
||||||
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part
|
|
||||||
)
|
|
||||||
.join(" ")
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLocalRoomId(roomId) {
|
|
||||||
if (!roomId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = roomId.match(/[^:]+:(.*)$/);
|
|
||||||
|
|
||||||
if (parts.length < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts[1] === defaultHomeserverHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRoom(client, name) {
|
|
||||||
const { room_id, room_alias } = await client.createRoom({
|
|
||||||
visibility: "private",
|
|
||||||
preset: "public_chat",
|
|
||||||
name,
|
|
||||||
room_alias_name: roomAliasFromRoomName(name),
|
|
||||||
power_level_content_override: {
|
|
||||||
invite: 100,
|
|
||||||
kick: 100,
|
|
||||||
ban: 100,
|
|
||||||
redact: 50,
|
|
||||||
state_default: 0,
|
|
||||||
events_default: 0,
|
|
||||||
users_default: 0,
|
|
||||||
events: {
|
|
||||||
"m.room.power_levels": 100,
|
|
||||||
"m.room.history_visibility": 100,
|
|
||||||
"m.room.tombstone": 100,
|
|
||||||
"m.room.encryption": 100,
|
|
||||||
"m.room.name": 50,
|
|
||||||
"m.room.message": 0,
|
|
||||||
"m.room.encrypted": 50,
|
|
||||||
"m.sticker": 50,
|
|
||||||
"org.matrix.msc3401.call.member": 0,
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
[client.getUserId()]: 100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.createGroupCall(
|
|
||||||
room_id,
|
|
||||||
GroupCallType.Video,
|
|
||||||
GroupCallIntent.Prompt
|
|
||||||
);
|
|
||||||
|
|
||||||
return room_alias || room_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRoomUrl(roomId) {
|
|
||||||
if (roomId.startsWith("#")) {
|
|
||||||
const [localPart, host] = roomId.replace("#", "").split(":");
|
|
||||||
|
|
||||||
if (host !== defaultHomeserverHost) {
|
|
||||||
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
|
||||||
} else {
|
|
||||||
return `${window.location.protocol}//${window.location.host}/${localPart}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAvatarUrl(client, mxcUrl, avatarSize = 96) {
|
|
||||||
const width = Math.floor(avatarSize * window.devicePixelRatio);
|
|
||||||
const height = Math.floor(avatarSize * window.devicePixelRatio);
|
|
||||||
return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, "crop");
|
|
||||||
}
|
|
||||||
224
src/matrix-utils.ts
Normal file
224
src/matrix-utils.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import Olm from "@matrix-org/olm";
|
||||||
|
import olmWasmPath from "@matrix-org/olm/olm.wasm?url";
|
||||||
|
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
|
||||||
|
import { WebStorageSessionStore } from "matrix-js-sdk/src/store/session/webstorage";
|
||||||
|
import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
||||||
|
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||||
|
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
|
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
import {
|
||||||
|
GroupCallIntent,
|
||||||
|
GroupCallType,
|
||||||
|
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||||
|
|
||||||
|
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||||
|
|
||||||
|
export const defaultHomeserver =
|
||||||
|
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
|
||||||
|
`${window.location.protocol}//${window.location.host}`;
|
||||||
|
|
||||||
|
export const defaultHomeserverHost = new URL(defaultHomeserver).host;
|
||||||
|
|
||||||
|
function waitForSync(client: MatrixClient) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const onSync = (
|
||||||
|
state: SyncState,
|
||||||
|
_old: SyncState,
|
||||||
|
data: ISyncStateData
|
||||||
|
) => {
|
||||||
|
if (state === "PREPARED") {
|
||||||
|
resolve();
|
||||||
|
client.removeListener(ClientEvent.Sync, onSync);
|
||||||
|
} else if (state === "ERROR") {
|
||||||
|
reject(data?.error);
|
||||||
|
client.removeListener(ClientEvent.Sync, onSync);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
client.on(ClientEvent.Sync, onSync);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initClient(
|
||||||
|
clientOptions: ICreateClientOpts
|
||||||
|
): Promise<MatrixClient> {
|
||||||
|
// TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
|
||||||
|
window.OLM_OPTIONS = {};
|
||||||
|
await Olm.init({ locateFile: () => olmWasmPath });
|
||||||
|
|
||||||
|
let indexedDB: IDBFactory;
|
||||||
|
|
||||||
|
try {
|
||||||
|
indexedDB = window.indexedDB;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const storeOpts = {} as ICreateClientOpts;
|
||||||
|
|
||||||
|
if (indexedDB && localStorage && !import.meta.env.DEV) {
|
||||||
|
storeOpts.store = new IndexedDBStore({
|
||||||
|
indexedDB: window.indexedDB,
|
||||||
|
localStorage: window.localStorage,
|
||||||
|
dbName: "element-call-sync",
|
||||||
|
workerFactory: () => new IndexedDBWorker(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage) {
|
||||||
|
storeOpts.sessionStore = new WebStorageSessionStore(localStorage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexedDB) {
|
||||||
|
storeOpts.cryptoStore = new IndexedDBCryptoStore(
|
||||||
|
indexedDB,
|
||||||
|
"matrix-js-sdk:crypto"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
...storeOpts,
|
||||||
|
...clientOptions,
|
||||||
|
useAuthorizationHeader: true,
|
||||||
|
// Use a relatively low timeout for API calls: this is a realtime application
|
||||||
|
// so we don't want API calls taking ages, we'd rather they just fail.
|
||||||
|
localTimeoutMs: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.store.startup();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Error starting matrix client store. Falling back to memory store.",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
client.store = new MemoryStore({ localStorage });
|
||||||
|
await client.store.startup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.initCrypto) {
|
||||||
|
await client.initCrypto();
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.startClient({
|
||||||
|
// dirty hack to reduce chance of gappy syncs
|
||||||
|
// should be fixed by spotting gaps and backpaginating
|
||||||
|
initialSyncLimit: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForSync(client);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roomAliasLocalpartFromRoomName(roomName: string): string {
|
||||||
|
return roomName
|
||||||
|
.trim()
|
||||||
|
.replace(/\s/g, "-")
|
||||||
|
.replace(/[^\w-]/g, "")
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fullAliasFromRoomName(
|
||||||
|
roomName: string,
|
||||||
|
client: MatrixClient
|
||||||
|
): string {
|
||||||
|
return `#${roomAliasLocalpartFromRoomName(roomName)}:${client.getDomain()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roomNameFromRoomId(roomId: string): string {
|
||||||
|
return roomId
|
||||||
|
.match(/([^:]+):.*$/)[1]
|
||||||
|
.substring(1)
|
||||||
|
.split("-")
|
||||||
|
.map((part) =>
|
||||||
|
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part
|
||||||
|
)
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLocalRoomId(roomId: string): boolean {
|
||||||
|
if (!roomId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = roomId.match(/[^:]+:(.*)$/);
|
||||||
|
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[1] === defaultHomeserverHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRoom(
|
||||||
|
client: MatrixClient,
|
||||||
|
name: string,
|
||||||
|
isPtt = false
|
||||||
|
): Promise<string> {
|
||||||
|
const createRoomResult = await client.createRoom({
|
||||||
|
visibility: Visibility.Private,
|
||||||
|
preset: Preset.PublicChat,
|
||||||
|
name,
|
||||||
|
room_alias_name: roomAliasLocalpartFromRoomName(name),
|
||||||
|
power_level_content_override: {
|
||||||
|
invite: 100,
|
||||||
|
kick: 100,
|
||||||
|
ban: 100,
|
||||||
|
redact: 50,
|
||||||
|
state_default: 0,
|
||||||
|
events_default: 0,
|
||||||
|
users_default: 0,
|
||||||
|
events: {
|
||||||
|
"m.room.power_levels": 100,
|
||||||
|
"m.room.history_visibility": 100,
|
||||||
|
"m.room.tombstone": 100,
|
||||||
|
"m.room.encryption": 100,
|
||||||
|
"m.room.name": 50,
|
||||||
|
"m.room.message": 0,
|
||||||
|
"m.room.encrypted": 50,
|
||||||
|
"m.sticker": 50,
|
||||||
|
"org.matrix.msc3401.call.member": 0,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
[client.getUserId()]: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Creating ${isPtt ? "PTT" : "video"} group call room`);
|
||||||
|
|
||||||
|
await client.createGroupCall(
|
||||||
|
createRoomResult.room_id,
|
||||||
|
isPtt ? GroupCallType.Voice : GroupCallType.Video,
|
||||||
|
isPtt,
|
||||||
|
GroupCallIntent.Prompt
|
||||||
|
);
|
||||||
|
|
||||||
|
return fullAliasFromRoomName(name, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRoomUrl(roomId: string): string {
|
||||||
|
if (roomId.startsWith("#")) {
|
||||||
|
const [localPart, host] = roomId.replace("#", "").split(":");
|
||||||
|
|
||||||
|
if (host !== defaultHomeserverHost) {
|
||||||
|
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||||
|
} else {
|
||||||
|
return `${window.location.protocol}//${window.location.host}/${localPart}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvatarUrl(
|
||||||
|
client: MatrixClient,
|
||||||
|
mxcUrl: string,
|
||||||
|
avatarSize = 96
|
||||||
|
): string {
|
||||||
|
const width = Math.floor(avatarSize * window.devicePixelRatio);
|
||||||
|
const height = Math.floor(avatarSize * window.devicePixelRatio);
|
||||||
|
return mxcUrl && client.mxcUrlToHttp(mxcUrl, width, height, "crop");
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 194px;
|
width: 194px;
|
||||||
background: var(--bgColor2);
|
background: var(--system);
|
||||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,5 +1,20 @@
|
|||||||
|
/*
|
||||||
|
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";
|
|
||||||
|
|
||||||
export function useProfile(client) {
|
export function useProfile(client) {
|
||||||
const [{ loading, displayName, avatarUrl, error, success }, setState] =
|
const [{ loading, displayName, avatarUrl, error, success }, setState] =
|
||||||
@@ -10,7 +25,7 @@ export function useProfile(client) {
|
|||||||
success: false,
|
success: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
displayName: user?.rawDisplayName,
|
displayName: user?.rawDisplayName,
|
||||||
avatarUrl: user && client && getAvatarUrl(client, user.avatarUrl),
|
avatarUrl: user?.avatarUrl,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -21,7 +36,7 @@ export function useProfile(client) {
|
|||||||
success: false,
|
success: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl: getAvatarUrl(client, avatarUrl),
|
avatarUrl,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -68,11 +83,7 @@ export function useProfile(client) {
|
|||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl: removeAvatar
|
avatarUrl: removeAvatar ? null : mxcAvatarUrl ?? prev.avatarUrl,
|
||||||
? null
|
|
||||||
: mxcAvatarUrl
|
|
||||||
? getAvatarUrl(client, mxcAvatarUrl)
|
|
||||||
: prev.avatarUrl,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
success: true,
|
success: true,
|
||||||
}));
|
}));
|
||||||
|
|||||||
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} - Walkie-talkie 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,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 } 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";
|
||||||
|
|||||||
@@ -1,10 +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 React from "react";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
||||||
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg";
|
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg";
|
||||||
import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
|
import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
|
||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||||
import styles from "./GridLayoutMenu.module.css";
|
import menuStyles from "../Menu.module.css";
|
||||||
import { Menu } from "../Menu";
|
import { Menu } from "../Menu";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
import { Tooltip, TooltipTrigger } from "../Tooltip";
|
import { Tooltip, TooltipTrigger } from "../Tooltip";
|
||||||
@@ -23,13 +39,15 @@ export function GridLayoutMenu({ layout, setLayout }) {
|
|||||||
<Item key="freedom" textValue="Freedom">
|
<Item key="freedom" textValue="Freedom">
|
||||||
<FreedomIcon />
|
<FreedomIcon />
|
||||||
<span>Freedom</span>
|
<span>Freedom</span>
|
||||||
{layout === "freedom" && <CheckIcon className={styles.checkIcon} />}
|
{layout === "freedom" && (
|
||||||
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
|
)}
|
||||||
</Item>
|
</Item>
|
||||||
<Item key="spotlight" textValue="Spotlight">
|
<Item key="spotlight" textValue="Spotlight">
|
||||||
<SpotlightIcon />
|
<SpotlightIcon />
|
||||||
<span>Spotlight</span>
|
<span>Spotlight</span>
|
||||||
{layout === "spotlight" && (
|
{layout === "spotlight" && (
|
||||||
<CheckIcon className={styles.checkIcon} />
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
)}
|
)}
|
||||||
</Item>
|
</Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
.checkIcon {
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkIcon * {
|
|
||||||
stroke: 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 { Resizable } from "re-resizable";
|
import { Resizable } from "re-resizable";
|
||||||
import React, {
|
import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.inspector {
|
.inspector {
|
||||||
background-color: var(--bgColor2);
|
background-color: var(--system);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollContainer {
|
.scrollContainer {
|
||||||
@@ -20,6 +20,6 @@
|
|||||||
|
|
||||||
.sequenceDiagramViewer :global(.messageText) {
|
.sequenceDiagramViewer :global(.messageText) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
fill: var(--textColor1) !important;
|
fill: var(--primary-content) !important;
|
||||||
stroke: var(--textColor1) !important;
|
stroke: var(--primary-content) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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 { 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";
|
||||||
@@ -5,7 +21,9 @@ 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 { useRoomAvatar } from "./useRoomAvatar";
|
||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
|
|
||||||
@@ -15,19 +33,6 @@ export function GroupCallView({
|
|||||||
roomId,
|
roomId,
|
||||||
groupCall,
|
groupCall,
|
||||||
}) {
|
}) {
|
||||||
const [showInspector, setShowInspector] = useState(
|
|
||||||
() => !!localStorage.getItem("matrix-group-call-inspector")
|
|
||||||
);
|
|
||||||
const onChangeShowInspector = useCallback((show) => {
|
|
||||||
setShowInspector(show);
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
localStorage.setItem("matrix-group-call-inspector", "true");
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem("matrix-group-call-inspector");
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
error,
|
error,
|
||||||
@@ -47,8 +52,11 @@ export function GroupCallView({
|
|||||||
localScreenshareFeed,
|
localScreenshareFeed,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
hasLocalParticipant,
|
hasLocalParticipant,
|
||||||
|
participants,
|
||||||
} = useGroupCall(groupCall);
|
} = useGroupCall(groupCall);
|
||||||
|
|
||||||
|
const avatarUrl = useRoomAvatar(groupCall.room);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.groupCall = groupCall;
|
window.groupCall = groupCall;
|
||||||
}, [groupCall]);
|
}, [groupCall]);
|
||||||
@@ -72,27 +80,41 @@ 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}
|
avatarUrl={avatarUrl}
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
groupCall={groupCall}
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
participants={participants}
|
||||||
userMediaFeeds={userMediaFeeds}
|
userMediaFeeds={userMediaFeeds}
|
||||||
activeSpeaker={activeSpeaker}
|
onLeave={onLeave}
|
||||||
onLeave={onLeave}
|
/>
|
||||||
toggleScreensharing={toggleScreensharing}
|
);
|
||||||
isScreensharing={isScreensharing}
|
} else {
|
||||||
localScreenshareFeed={localScreenshareFeed}
|
return (
|
||||||
screenshareFeeds={screenshareFeeds}
|
<InCallView
|
||||||
setShowInspector={onChangeShowInspector}
|
groupCall={groupCall}
|
||||||
showInspector={showInspector}
|
client={client}
|
||||||
roomId={roomId}
|
roomName={groupCall.room.name}
|
||||||
/>
|
avatarUrl={avatarUrl}
|
||||||
);
|
microphoneMuted={microphoneMuted}
|
||||||
|
localVideoMuted={localVideoMuted}
|
||||||
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
|
userMediaFeeds={userMediaFeeds}
|
||||||
|
activeSpeaker={activeSpeaker}
|
||||||
|
onLeave={onLeave}
|
||||||
|
toggleScreensharing={toggleScreensharing}
|
||||||
|
isScreensharing={isScreensharing}
|
||||||
|
localScreenshareFeed={localScreenshareFeed}
|
||||||
|
screenshareFeeds={screenshareFeeds}
|
||||||
|
roomId={roomId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (state === GroupCallState.Entering) {
|
} else if (state === GroupCallState.Entering) {
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
@@ -105,8 +127,10 @@ 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}
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
state={state}
|
state={state}
|
||||||
onInitLocalCallFeed={initLocalCallFeed}
|
onInitLocalCallFeed={initLocalCallFeed}
|
||||||
localCallFeed={localCallFeed}
|
localCallFeed={localCallFeed}
|
||||||
@@ -115,8 +139,6 @@ export function GroupCallView({
|
|||||||
localVideoMuted={localVideoMuted}
|
localVideoMuted={localVideoMuted}
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
setShowInspector={onChangeShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
import React, { useCallback, useMemo } from "react";
|
/*
|
||||||
|
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, useRef } from "react";
|
||||||
import styles from "./InCallView.module.css";
|
import styles from "./InCallView.module.css";
|
||||||
import {
|
import {
|
||||||
HangupButton,
|
HangupButton,
|
||||||
@@ -9,7 +25,6 @@ import {
|
|||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
|
import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
|
||||||
import { VideoTileContainer } from "../video-grid/VideoTileContainer";
|
import { VideoTileContainer } from "../video-grid/VideoTileContainer";
|
||||||
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";
|
||||||
@@ -19,6 +34,8 @@ 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";
|
||||||
|
import { useShowInspector } from "../settings/useSetting";
|
||||||
|
import { useModalTriggerState } from "../Modal";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
@@ -30,6 +47,7 @@ export function InCallView({
|
|||||||
client,
|
client,
|
||||||
groupCall,
|
groupCall,
|
||||||
roomName,
|
roomName,
|
||||||
|
avatarUrl,
|
||||||
microphoneMuted,
|
microphoneMuted,
|
||||||
localVideoMuted,
|
localVideoMuted,
|
||||||
toggleLocalVideoMuted,
|
toggleLocalVideoMuted,
|
||||||
@@ -40,14 +58,19 @@ export function InCallView({
|
|||||||
toggleScreensharing,
|
toggleScreensharing,
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
roomId,
|
roomId,
|
||||||
}) {
|
}) {
|
||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
||||||
|
|
||||||
const { audioOutput } = useMediaHandler();
|
const { audioOutput } = useMediaHandler();
|
||||||
|
const [showInspector] = useShowInspector();
|
||||||
|
|
||||||
|
const audioContext = useRef();
|
||||||
|
if (!audioContext.current) audioContext.current = new AudioContext();
|
||||||
|
|
||||||
|
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
|
||||||
|
useModalTriggerState();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const participants = [];
|
const participants = [];
|
||||||
@@ -84,23 +107,6 @@ export function InCallView({
|
|||||||
return participants;
|
return participants;
|
||||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
|
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
|
||||||
|
|
||||||
const onFocusTile = useCallback(
|
|
||||||
(tiles, focusedTile) => {
|
|
||||||
if (layout === "freedom") {
|
|
||||||
return tiles.map((tile) => {
|
|
||||||
if (tile === focusedTile) {
|
|
||||||
return { ...tile, focused: !tile.focused };
|
|
||||||
}
|
|
||||||
|
|
||||||
return tile;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return tiles;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[layout, setLayout]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderAvatar = useCallback(
|
const renderAvatar = useCallback(
|
||||||
(roomMember, width, height) => {
|
(roomMember, width, height) => {
|
||||||
const avatarUrl = roomMember.user?.avatarUrl;
|
const avatarUrl = roomMember.user?.avatarUrl;
|
||||||
@@ -109,13 +115,8 @@ export function InCallView({
|
|||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
key={roomMember.userId}
|
key={roomMember.userId}
|
||||||
style={{
|
size={size}
|
||||||
width: size,
|
src={avatarUrl}
|
||||||
height: size,
|
|
||||||
borderRadius: size,
|
|
||||||
fontSize: Math.round(size / 2),
|
|
||||||
}}
|
|
||||||
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)}
|
|
||||||
fallback={roomMember.name.slice(0, 1).toUpperCase()}
|
fallback={roomMember.name.slice(0, 1).toUpperCase()}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
/>
|
/>
|
||||||
@@ -133,7 +134,7 @@ export function InCallView({
|
|||||||
<div className={styles.inRoom}>
|
<div className={styles.inRoom}>
|
||||||
<Header>
|
<Header>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<RoomHeaderInfo roomName={roomName} />
|
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||||
@@ -145,12 +146,7 @@ export function InCallView({
|
|||||||
<p>Waiting for other participants...</p>
|
<p>Waiting for other participants...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<VideoGrid
|
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
|
||||||
items={items}
|
|
||||||
layout={layout}
|
|
||||||
onFocusTile={onFocusTile}
|
|
||||||
disableAnimations={isSafari}
|
|
||||||
>
|
|
||||||
{({ item, ...rest }) => (
|
{({ item, ...rest }) => (
|
||||||
<VideoTileContainer
|
<VideoTileContainer
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -158,6 +154,7 @@ export function InCallView({
|
|||||||
getAvatar={renderAvatar}
|
getAvatar={renderAvatar}
|
||||||
showName={items.length > 2 || item.focused}
|
showName={items.length > 2 || item.focused}
|
||||||
audioOutputDevice={audioOutput}
|
audioOutputDevice={audioOutput}
|
||||||
|
audioContext={audioContext.current}
|
||||||
disableSpeakingIndicator={items.length < 3}
|
disableSpeakingIndicator={items.length < 3}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
@@ -176,10 +173,11 @@ export function InCallView({
|
|||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
inCall
|
inCall
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
client={client}
|
client={client}
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
|
showInvite={true}
|
||||||
|
feedbackModalState={feedbackModalState}
|
||||||
|
feedbackModalProps={feedbackModalProps}
|
||||||
/>
|
/>
|
||||||
<HangupButton onPress={onLeave} />
|
<HangupButton onPress={onLeave} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,24 +1,38 @@
|
|||||||
|
/*
|
||||||
|
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 "../video-grid/useCallFeed";
|
import { useCallFeed } from "../video-grid/useCallFeed";
|
||||||
import { useMediaStream } from "../video-grid/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,
|
||||||
|
avatarUrl,
|
||||||
state,
|
state,
|
||||||
onInitLocalCallFeed,
|
onInitLocalCallFeed,
|
||||||
onEnter,
|
onEnter,
|
||||||
@@ -27,16 +41,17 @@ export function LobbyView({
|
|||||||
localVideoMuted,
|
localVideoMuted,
|
||||||
toggleLocalVideoMuted,
|
toggleLocalVideoMuted,
|
||||||
toggleMicrophoneMuted,
|
toggleMicrophoneMuted,
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
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();
|
||||||
@@ -56,7 +71,7 @@ export function LobbyView({
|
|||||||
<div className={styles.room}>
|
<div className={styles.room}>
|
||||||
<Header>
|
<Header>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<RoomHeaderInfo roomName={roomName} />
|
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
<UserMenuContainer />
|
<UserMenuContainer />
|
||||||
@@ -64,53 +79,30 @@ 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}>
|
roomId={roomId}
|
||||||
<Avatar
|
microphoneMuted={microphoneMuted}
|
||||||
style={{
|
localVideoMuted={localVideoMuted}
|
||||||
width: avatarSize,
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
height: avatarSize,
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
borderRadius: avatarSize,
|
stream={stream}
|
||||||
fontSize: Math.round(avatarSize / 2),
|
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";
|
||||||
@@ -15,17 +31,16 @@ import { FeedbackModal } from "./FeedbackModal";
|
|||||||
|
|
||||||
export function OverflowMenu({
|
export function OverflowMenu({
|
||||||
roomId,
|
roomId,
|
||||||
setShowInspector,
|
|
||||||
showInspector,
|
|
||||||
inCall,
|
inCall,
|
||||||
groupCall,
|
groupCall,
|
||||||
|
showInvite,
|
||||||
|
feedbackModalState,
|
||||||
|
feedbackModalProps,
|
||||||
}) {
|
}) {
|
||||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||||
useModalTriggerState();
|
useModalTriggerState();
|
||||||
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
||||||
useModalTriggerState();
|
useModalTriggerState();
|
||||||
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
|
|
||||||
useModalTriggerState();
|
|
||||||
|
|
||||||
// TODO: On closing modal, focus should be restored to the trigger button
|
// TODO: On closing modal, focus should be restored to the trigger button
|
||||||
// https://github.com/adobe/react-spectrum/issues/2444
|
// https://github.com/adobe/react-spectrum/issues/2444
|
||||||
@@ -54,10 +69,12 @@ export function OverflowMenu({
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="More menu" onAction={onAction}>
|
<Menu {...props} label="More menu" onAction={onAction}>
|
||||||
<Item key="invite" textValue="Invite people">
|
{showInvite && (
|
||||||
<AddUserIcon />
|
<Item key="invite" textValue="Invite people">
|
||||||
<span>Invite people</span>
|
<AddUserIcon />
|
||||||
</Item>
|
<span>Invite people</span>
|
||||||
|
</Item>
|
||||||
|
)}
|
||||||
<Item key="settings" textValue="Settings">
|
<Item key="settings" textValue="Settings">
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
@@ -69,13 +86,7 @@ export function OverflowMenu({
|
|||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
</PopoverMenuTrigger>
|
</PopoverMenuTrigger>
|
||||||
{settingsModalState.isOpen && (
|
{settingsModalState.isOpen && <SettingsModal {...settingsModalProps} />}
|
||||||
<SettingsModal
|
|
||||||
{...settingsModalProps}
|
|
||||||
setShowInspector={setShowInspector}
|
|
||||||
showInspector={showInspector}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{inviteModalState.isOpen && (
|
{inviteModalState.isOpen && (
|
||||||
<InviteModal roomId={roomId} {...inviteModalProps} />
|
<InviteModal roomId={roomId} {...inviteModalProps} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
23
src/room/PTTButton.module.css
Normal file
23
src/room/PTTButton.module.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.pttButton {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 232px;
|
||||||
|
max-width: 232px;
|
||||||
|
border-radius: 116px;
|
||||||
|
color: var(--primary-content);
|
||||||
|
border: 6px solid var(--accent);
|
||||||
|
background-color: #21262c;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.talking {
|
||||||
|
background-color: var(--accent);
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: var(--alert);
|
||||||
|
border-color: var(--alert);
|
||||||
|
}
|
||||||
182
src/room/PTTButton.tsx
Normal file
182
src/room/PTTButton.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
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 { useSpring, animated } from "@react-spring/web";
|
||||||
|
|
||||||
|
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;
|
||||||
|
activeSpeakerVolume: number;
|
||||||
|
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,
|
||||||
|
activeSpeakerVolume,
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const { shadow } = useSpring({
|
||||||
|
shadow: (Math.max(activeSpeakerVolume, -70) + 70) * 0.6,
|
||||||
|
config: {
|
||||||
|
clamp: true,
|
||||||
|
tension: 300,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const shadowColor = showTalkOverError
|
||||||
|
? "var(--alert-20)"
|
||||||
|
: "var(--accent-20)";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<animated.button
|
||||||
|
className={classNames(styles.pttButton, {
|
||||||
|
[styles.talking]: activeSpeakerUserId,
|
||||||
|
[styles.error]: showTalkOverError,
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
boxShadow: shadow.to(
|
||||||
|
(s) =>
|
||||||
|
`0px 0px 0px ${s}px ${shadowColor}, 0px 0px 0px ${
|
||||||
|
2 * s
|
||||||
|
}px ${shadowColor}`
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
onMouseDown={onButtonMouseDown}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{activeSpeakerIsLocalUser || !activeSpeakerUserId ? (
|
||||||
|
<MicIcon
|
||||||
|
className={styles.micIcon}
|
||||||
|
width={size / 3}
|
||||||
|
height={size / 3}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
key={activeSpeakerUserId}
|
||||||
|
size={size - 12}
|
||||||
|
src={activeSpeakerAvatarUrl}
|
||||||
|
fallback={activeSpeakerDisplayName.slice(0, 1).toUpperCase()}
|
||||||
|
className={styles.avatar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</animated.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: var(--secondary-content);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
264
src/room/PTTCallView.tsx
Normal file
264
src/room/PTTCallView.tsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
/*
|
||||||
|
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 { InviteModal } from "./InviteModal";
|
||||||
|
import { HangupButton, InviteButton } 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 { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
||||||
|
import { usePTTSounds } from "../sound/usePttSounds";
|
||||||
|
import { PTTClips } from "../sound/PTTClips";
|
||||||
|
import { GroupCallInspector } from "./GroupCallInspector";
|
||||||
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
|
|
||||||
|
function getPromptText(
|
||||||
|
showTalkOverError: boolean,
|
||||||
|
pttButtonHeld: boolean,
|
||||||
|
activeSpeakerIsLocalUser: boolean,
|
||||||
|
talkOverEnabled: boolean,
|
||||||
|
activeSpeakerUserId: string,
|
||||||
|
activeSpeakerDisplayName: string,
|
||||||
|
connected: boolean
|
||||||
|
): string {
|
||||||
|
if (!connected) return "Connection Lost";
|
||||||
|
|
||||||
|
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;
|
||||||
|
avatarUrl: string;
|
||||||
|
groupCall: GroupCall;
|
||||||
|
participants: RoomMember[];
|
||||||
|
userMediaFeeds: CallFeed[];
|
||||||
|
onLeave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PTTCallView: React.FC<Props> = ({
|
||||||
|
client,
|
||||||
|
roomId,
|
||||||
|
roomName,
|
||||||
|
avatarUrl,
|
||||||
|
groupCall,
|
||||||
|
participants,
|
||||||
|
userMediaFeeds,
|
||||||
|
onLeave,
|
||||||
|
}) => {
|
||||||
|
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||||
|
useModalTriggerState();
|
||||||
|
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
|
||||||
|
useModalTriggerState();
|
||||||
|
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
|
const facepileSize = bounds.width < 800 ? "sm" : "md";
|
||||||
|
const pttButtonSize = 232;
|
||||||
|
|
||||||
|
const { audioOutput } = useMediaHandler();
|
||||||
|
|
||||||
|
const {
|
||||||
|
startTalkingLocalRef,
|
||||||
|
startTalkingRemoteRef,
|
||||||
|
blockedRef,
|
||||||
|
endTalkingRef,
|
||||||
|
playClip,
|
||||||
|
} = usePTTSounds();
|
||||||
|
|
||||||
|
const {
|
||||||
|
pttButtonHeld,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
setTalkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
activeSpeakerVolume,
|
||||||
|
startTalking,
|
||||||
|
stopTalking,
|
||||||
|
transmitBlocked,
|
||||||
|
connected,
|
||||||
|
} = usePTT(
|
||||||
|
client,
|
||||||
|
groupCall,
|
||||||
|
userMediaFeeds,
|
||||||
|
playClip,
|
||||||
|
!feedbackModalState.isOpen
|
||||||
|
);
|
||||||
|
|
||||||
|
const showTalkOverError = pttButtonHeld && transmitBlocked;
|
||||||
|
|
||||||
|
const activeSpeakerIsLocalUser =
|
||||||
|
activeSpeakerUserId && client.getUserId() === activeSpeakerUserId;
|
||||||
|
const activeSpeakerUser = activeSpeakerUserId
|
||||||
|
? client.getUser(activeSpeakerUserId)
|
||||||
|
: null;
|
||||||
|
const activeSpeakerAvatarUrl = activeSpeakerUser?.avatarUrl;
|
||||||
|
const activeSpeakerDisplayName = activeSpeakerUser
|
||||||
|
? activeSpeakerUser.displayName
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.pttCallView} ref={containerRef}>
|
||||||
|
<PTTClips
|
||||||
|
startTalkingLocalRef={startTalkingLocalRef}
|
||||||
|
startTalkingRemoteRef={startTalkingRemoteRef}
|
||||||
|
endTalkingRef={endTalkingRef}
|
||||||
|
blockedRef={blockedRef}
|
||||||
|
/>
|
||||||
|
<GroupCallInspector
|
||||||
|
client={client}
|
||||||
|
groupCall={groupCall}
|
||||||
|
// Never shown in PTT mode, but must be present to collect call state
|
||||||
|
// https://github.com/vector-im/element-call/issues/328
|
||||||
|
show={false}
|
||||||
|
/>
|
||||||
|
<Header className={styles.header}>
|
||||||
|
<LeftNav>
|
||||||
|
<RoomSetupHeaderInfo
|
||||||
|
roomName={roomName}
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
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}>
|
||||||
|
<OverflowMenu
|
||||||
|
inCall
|
||||||
|
roomId={roomId}
|
||||||
|
client={client}
|
||||||
|
groupCall={groupCall}
|
||||||
|
showInvite={false}
|
||||||
|
feedbackModalState={feedbackModalState}
|
||||||
|
feedbackModalProps={feedbackModalProps}
|
||||||
|
/>
|
||||||
|
<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}
|
||||||
|
activeSpeakerVolume={activeSpeakerVolume}
|
||||||
|
size={pttButtonSize}
|
||||||
|
startTalking={startTalking}
|
||||||
|
stopTalking={stopTalking}
|
||||||
|
/>
|
||||||
|
<p className={styles.actionTip}>
|
||||||
|
{getPromptText(
|
||||||
|
showTalkOverError,
|
||||||
|
pttButtonHeld,
|
||||||
|
activeSpeakerIsLocalUser,
|
||||||
|
talkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
activeSpeakerDisplayName,
|
||||||
|
connected
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{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,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 { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
|
/*
|
||||||
|
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 { useClient } from "../ClientContext";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Body, Caption, Link, Headline } from "../typography/Typography";
|
import { Body, Caption, Link, Headline } from "../typography/Typography";
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||||
@@ -13,11 +30,13 @@ import { UserMenuContainer } from "../UserMenuContainer";
|
|||||||
import { generateRandomName } from "../auth/generateRandomName";
|
import { generateRandomName } from "../auth/generateRandomName";
|
||||||
|
|
||||||
export function RoomAuthView() {
|
export function RoomAuthView() {
|
||||||
|
const { setClient } = useClient();
|
||||||
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] =
|
||||||
useInteractiveRegistration();
|
useInteractiveRegistration();
|
||||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -29,13 +48,14 @@ export function RoomAuthView() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const recaptchaResponse = await execute();
|
const recaptchaResponse = await execute();
|
||||||
const userName = generateRandomName();
|
const userName = generateRandomName();
|
||||||
await register(
|
const [client, session] = await register(
|
||||||
userName,
|
userName,
|
||||||
randomString(16),
|
randomString(16),
|
||||||
displayName,
|
displayName,
|
||||||
recaptchaResponse,
|
recaptchaResponse,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
setClient(client, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
93
src/room/VideoPreview.jsx
Normal file
93
src/room/VideoPreview.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
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";
|
||||||
|
import { useModalTriggerState } from "../Modal";
|
||||||
|
|
||||||
|
export function VideoPreview({
|
||||||
|
client,
|
||||||
|
state,
|
||||||
|
roomId,
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
toggleLocalVideoMuted,
|
||||||
|
toggleMicrophoneMuted,
|
||||||
|
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;
|
||||||
|
|
||||||
|
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
|
||||||
|
useModalTriggerState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.preview} ref={previewRef}>
|
||||||
|
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||||
|
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
|
||||||
|
Camera/microphone permissions needed to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||||
|
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
|
||||||
|
Accept camera/microphone permissions to join the call.
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||||
|
<>
|
||||||
|
{localVideoMuted && (
|
||||||
|
<div className={styles.avatarContainer}>
|
||||||
|
<Avatar
|
||||||
|
size={avatarSize}
|
||||||
|
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}
|
||||||
|
client={client}
|
||||||
|
feedbackModalState={feedbackModalState}
|
||||||
|
feedbackModalProps={feedbackModalProps}
|
||||||
|
/>
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cameraPermissions {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,74 @@
|
|||||||
|
/*
|
||||||
|
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 { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
GroupCallEvent,
|
GroupCallEvent,
|
||||||
GroupCallState,
|
GroupCallState,
|
||||||
|
GroupCall,
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import { usePageUnload } from "./usePageUnload";
|
import { usePageUnload } from "./usePageUnload";
|
||||||
|
|
||||||
export function useGroupCall(groupCall) {
|
export interface UseGroupCallType {
|
||||||
|
state: GroupCallState;
|
||||||
|
calls: MatrixCall[];
|
||||||
|
localCallFeed: CallFeed;
|
||||||
|
activeSpeaker: string;
|
||||||
|
userMediaFeeds: CallFeed[];
|
||||||
|
microphoneMuted: boolean;
|
||||||
|
localVideoMuted: boolean;
|
||||||
|
error: Error;
|
||||||
|
initLocalCallFeed: () => void;
|
||||||
|
enter: () => void;
|
||||||
|
leave: () => void;
|
||||||
|
toggleLocalVideoMuted: () => void;
|
||||||
|
toggleMicrophoneMuted: () => void;
|
||||||
|
toggleScreensharing: () => void;
|
||||||
|
requestingScreenshare: boolean;
|
||||||
|
isScreensharing: boolean;
|
||||||
|
screenshareFeeds: CallFeed[];
|
||||||
|
localScreenshareFeed: CallFeed;
|
||||||
|
localDesktopCapturerSourceId: string;
|
||||||
|
participants: RoomMember[];
|
||||||
|
hasLocalParticipant: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
state: GroupCallState;
|
||||||
|
calls: MatrixCall[];
|
||||||
|
localCallFeed: CallFeed;
|
||||||
|
activeSpeaker: string;
|
||||||
|
userMediaFeeds: CallFeed[];
|
||||||
|
error: Error;
|
||||||
|
microphoneMuted: boolean;
|
||||||
|
localVideoMuted: boolean;
|
||||||
|
screenshareFeeds: CallFeed[];
|
||||||
|
localScreenshareFeed: CallFeed;
|
||||||
|
localDesktopCapturerSourceId: string;
|
||||||
|
isScreensharing: boolean;
|
||||||
|
requestingScreenshare: boolean;
|
||||||
|
participants: RoomMember[];
|
||||||
|
hasLocalParticipant: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGroupCall(groupCall: GroupCall): UseGroupCallType {
|
||||||
const [
|
const [
|
||||||
{
|
{
|
||||||
state,
|
state,
|
||||||
@@ -25,20 +88,25 @@ export function useGroupCall(groupCall) {
|
|||||||
requestingScreenshare,
|
requestingScreenshare,
|
||||||
},
|
},
|
||||||
setState,
|
setState,
|
||||||
] = useState({
|
] = useState<State>({
|
||||||
state: GroupCallState.LocalCallFeedUninitialized,
|
state: GroupCallState.LocalCallFeedUninitialized,
|
||||||
calls: [],
|
calls: [],
|
||||||
|
localCallFeed: null,
|
||||||
|
activeSpeaker: null,
|
||||||
userMediaFeeds: [],
|
userMediaFeeds: [],
|
||||||
|
error: null,
|
||||||
microphoneMuted: false,
|
microphoneMuted: false,
|
||||||
localVideoMuted: false,
|
localVideoMuted: false,
|
||||||
screenshareFeeds: [],
|
|
||||||
isScreensharing: false,
|
isScreensharing: false,
|
||||||
|
screenshareFeeds: [],
|
||||||
|
localScreenshareFeed: null,
|
||||||
|
localDesktopCapturerSourceId: null,
|
||||||
requestingScreenshare: false,
|
requestingScreenshare: false,
|
||||||
participants: [],
|
participants: [],
|
||||||
hasLocalParticipant: false,
|
hasLocalParticipant: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateState = (state) =>
|
const updateState = (state: Partial<State>) =>
|
||||||
setState((prevState) => ({ ...prevState, ...state }));
|
setState((prevState) => ({ ...prevState, ...state }));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,25 +127,28 @@ export function useGroupCall(groupCall) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUserMediaFeedsChanged(userMediaFeeds) {
|
function onUserMediaFeedsChanged(userMediaFeeds: CallFeed[]): void {
|
||||||
updateState({
|
updateState({
|
||||||
userMediaFeeds: [...userMediaFeeds],
|
userMediaFeeds: [...userMediaFeeds],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScreenshareFeedsChanged(screenshareFeeds) {
|
function onScreenshareFeedsChanged(screenshareFeeds: CallFeed[]): void {
|
||||||
updateState({
|
updateState({
|
||||||
screenshareFeeds: [...screenshareFeeds],
|
screenshareFeeds: [...screenshareFeeds],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onActiveSpeakerChanged(activeSpeaker) {
|
function onActiveSpeakerChanged(activeSpeaker: string): void {
|
||||||
updateState({
|
updateState({
|
||||||
activeSpeaker: activeSpeaker,
|
activeSpeaker: activeSpeaker,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLocalMuteStateChanged(microphoneMuted, localVideoMuted) {
|
function onLocalMuteStateChanged(
|
||||||
|
microphoneMuted: boolean,
|
||||||
|
localVideoMuted: boolean
|
||||||
|
): void {
|
||||||
updateState({
|
updateState({
|
||||||
microphoneMuted,
|
microphoneMuted,
|
||||||
localVideoMuted,
|
localVideoMuted,
|
||||||
@@ -85,10 +156,10 @@ export function useGroupCall(groupCall) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onLocalScreenshareStateChanged(
|
function onLocalScreenshareStateChanged(
|
||||||
isScreensharing,
|
isScreensharing: boolean,
|
||||||
localScreenshareFeed,
|
localScreenshareFeed: CallFeed,
|
||||||
localDesktopCapturerSourceId
|
localDesktopCapturerSourceId: string
|
||||||
) {
|
): void {
|
||||||
updateState({
|
updateState({
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
localScreenshareFeed,
|
localScreenshareFeed,
|
||||||
@@ -96,13 +167,13 @@ export function useGroupCall(groupCall) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCallsChanged(calls) {
|
function onCallsChanged(calls: MatrixCall[]): void {
|
||||||
updateState({
|
updateState({
|
||||||
calls: [...calls],
|
calls: [...calls],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onParticipantsChanged(participants) {
|
function onParticipantsChanged(participants: RoomMember[]): void {
|
||||||
updateState({
|
updateState({
|
||||||
participants: [...participants],
|
participants: [...participants],
|
||||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
hasLocalParticipant: groupCall.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;
|
||||||
}
|
}
|
||||||
|
|||||||
341
src/room/usePTT.ts
Normal file
341
src/room/usePTT.ts
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
/*
|
||||||
|
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, ClientEvent } 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 { SyncState } from "matrix-js-sdk/src/sync";
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
// make sure the feeds are in a deterministic order so every client picks
|
||||||
|
// the same one as the active speaker. The custom sort function sorts
|
||||||
|
// by user ID, so needs a collator of some kind to compare. We make a
|
||||||
|
// specific one to help ensure every client sorts the same way
|
||||||
|
// although of course user IDs shouldn't contain accented characters etc.
|
||||||
|
// anyway).
|
||||||
|
const collator = new Intl.Collator("en", {
|
||||||
|
sensitivity: "variant",
|
||||||
|
usage: "sort",
|
||||||
|
ignorePunctuation: false,
|
||||||
|
});
|
||||||
|
activeSpeakerFeeds.sort((a: CallFeed, b: CallFeed): number =>
|
||||||
|
collator.compare(a.userId, b.userId)
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
activeSpeakerVolume: number;
|
||||||
|
startTalking: () => void;
|
||||||
|
stopTalking: () => void;
|
||||||
|
transmitBlocked: boolean;
|
||||||
|
// connected is actually an indication of whether we're connected to the HS
|
||||||
|
// (ie. the client's syncing state) rather than media connection, since
|
||||||
|
// it's peer to peer so we can't really say which peer is 'disconnected' if
|
||||||
|
// there's only one other person in the call and they've lost Internet.
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePTT = (
|
||||||
|
client: MatrixClient,
|
||||||
|
groupCall: GroupCall,
|
||||||
|
userMediaFeeds: CallFeed[],
|
||||||
|
playClip: PlayClipFunction,
|
||||||
|
enablePTTButton: boolean
|
||||||
|
): 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,
|
||||||
|
activeSpeakerVolume,
|
||||||
|
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,
|
||||||
|
activeSpeakerVolume: -Infinity,
|
||||||
|
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) => ({
|
||||||
|
...prevState,
|
||||||
|
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
|
||||||
|
transmitBlocked: blocked,
|
||||||
|
}));
|
||||||
|
}, [
|
||||||
|
playClip,
|
||||||
|
groupCall,
|
||||||
|
pttButtonHeld,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
client,
|
||||||
|
userMediaFeeds,
|
||||||
|
setMicMuteWrapper,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
for (const callFeed of userMediaFeeds) {
|
||||||
|
callFeed.on(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
|
||||||
|
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const callFeed of userMediaFeeds) {
|
||||||
|
callFeed.off(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [userMediaFeeds, onMuteStateChanged, groupCall]);
|
||||||
|
|
||||||
|
const onVolumeChanged = useCallback((volume: number) => {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
activeSpeakerVolume: volume,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
|
||||||
|
activeSpeakerFeed?.on(CallFeedEvent.VolumeChanged, onVolumeChanged);
|
||||||
|
return () => {
|
||||||
|
activeSpeakerFeed?.off(CallFeedEvent.VolumeChanged, onVolumeChanged);
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
activeSpeakerVolume: -Infinity,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}, [activeSpeakerUserId, onVolumeChanged, userMediaFeeds, 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]);
|
||||||
|
|
||||||
|
// separate state for connected: we set it separately from other things
|
||||||
|
// in the client sync callback
|
||||||
|
const [connected, setConnected] = useState(true);
|
||||||
|
|
||||||
|
const onClientSync = useCallback(
|
||||||
|
(syncState: SyncState) => {
|
||||||
|
setConnected(syncState !== SyncState.Error);
|
||||||
|
},
|
||||||
|
[setConnected]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKeyDown(event: KeyboardEvent): void {
|
||||||
|
if (event.code === "Space") {
|
||||||
|
if (!enablePTTButton) return;
|
||||||
|
|
||||||
|
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,
|
||||||
|
enablePTTButton,
|
||||||
|
setMicMuteWrapper,
|
||||||
|
client,
|
||||||
|
onClientSync,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.on(ClientEvent.Sync, onClientSync);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
client.removeListener(ClientEvent.Sync, onClientSync);
|
||||||
|
};
|
||||||
|
}, [client, onClientSync]);
|
||||||
|
|
||||||
|
const setTalkOverEnabled = useCallback((talkOverEnabled) => {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
talkOverEnabled,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pttButtonHeld,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
setTalkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
activeSpeakerVolume,
|
||||||
|
startTalking,
|
||||||
|
stopTalking,
|
||||||
|
transmitBlocked,
|
||||||
|
connected,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
|
|
||||||
// https://stackoverflow.com/a/9039885
|
// https://stackoverflow.com/a/9039885
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user