Compare commits

..

96 Commits

Author SHA1 Message Date
David Baker
c13040f0b0 Merge pull request #327 from vector-im/dbkr/end_talk_sound
Add sound when speaker stops speaking
2022-05-16 20:37:38 +01:00
David Baker
24a1091954 Merge pull request #325 from vector-im/dbkr/fix_mute_races
Mute local mic if blocked and fix races on mute / unmute
2022-05-16 14:12:15 +01:00
David Baker
9fd7329554 Add sound when speaker stops speaking
And also a slightly nicer blocked sound (ok, I couldn't let it go).
2022-05-13 21:00:14 +01:00
David Baker
2a19a9964d Merge pull request #326 from vector-im/dbkr/ptt_button_touch_fixes
Fixes for touch interface on push-to-talk button
2022-05-13 20:45:22 +01:00
David Baker
3fc9c1b74a Import createref separately 2022-05-13 20:43:20 +01:00
David Baker
f6f0c20b08 Chain promises correctly 2022-05-13 20:39:21 +01:00
David Baker
26a1c165d9 Merge pull request #322 from vector-im/dbkr/blocked_sound_on_timeout
Play the blocked sound on time limit
2022-05-13 19:26:46 +01:00
David Baker
2af87fa8b8 Fixes for touch interface on push-to-talk button
* Avoid also getting a 'mousedown' event by making the event listener
   non-passive so the preventDefault() works
 * Remember the touch that pressed the button so we only un-press
   when that touch ends, otherwise the button gets released if the
   user taps the screen anywhere else.
2022-05-13 18:28:48 +01:00
David Baker
d34c8d08a4 Add comment 2022-05-13 18:09:45 +01:00
David Baker
0f687fb8b8 Fix races on mute / unmute
By serialising everything on a promise chain
2022-05-13 17:58:59 +01:00
David Baker
603dd3786a Play the blocked sound on time limit
Play the 'blocked' sound effect whenever the user is till holding
the button but can't speak anymore, whether they've been cut off
by someone else or have reached their time limit.
2022-05-12 14:13:03 +01:00
David Baker
9fbe4278c2 Merge pull request #321 from vector-im/dbkr/ptt_mobile_touch_prompt
Fix hold-to-speak and prompt text on mobile
2022-05-12 12:16:48 +01:00
David Baker
b222b4f708 Fix hold-to-speak and prompt text on mobile 2022-05-12 12:07:04 +01:00
David Baker
abc2449b07 Merge pull request #320 from vector-im/dbkr/pttcallview_types
More types on PTTCallView
2022-05-12 10:07:59 +01:00
David Baker
e6459de0d9 Merge pull request #319 from vector-im/dbkr/pttsounds
Add push-to-talk sound effects
2022-05-12 10:07:50 +01:00
David Baker
323505fbb4 Put back listeners commented for testing 2022-05-12 10:04:14 +01:00
David Baker
2b06c6f2e6 More types on PTTCallView
Give it a props type
2022-05-11 17:44:26 +01:00
David Baker
5a56e46f7b Prettier 2022-05-11 16:50:41 +01:00
David Baker
abe9ece38f Add push-to-talk sound effects
Fixes https://github.com/vector-im/element-call/issues/296
2022-05-11 16:28:08 +01:00
David Baker
cb8d837370 Fetch redirects file from github 2022-05-11 13:20:35 +01:00
David Baker
500a19d655 Merge pull request #318 from vector-im/dbkr/netlify_redirect
Add redirects to netlify deployment
2022-05-11 13:11:09 +01:00
David Baker
0d3daf5fa3 move config files into config/ 2022-05-11 12:11:49 +01:00
David Baker
66aede01dc Merge pull request #316 from vector-im/dbkr/typescript_round_1
Initial round of typescripting
2022-05-11 11:50:07 +01:00
David Baker
6d7be57dcf More comments 2022-05-11 11:42:17 +01:00
David Baker
5b913205af Add redirects to netlify deployment 2022-05-11 11:39:48 +01:00
David Baker
fd93d89b26 Merge pull request #317 from dbkr/main
Add GHA to deploy main branch to Netlify
2022-05-11 11:13:18 +01:00
David Baker
abdfcd879d Hopefully add sentry to cd builds 2022-05-10 21:24:20 +01:00
Robert Long
b231424f96 Reference vite/svgr types 2022-05-10 13:04:04 -07:00
David Baker
b2418d5384 Put manual deployment updating back
the netlify action's deployment updating doesn't seem to work
2022-05-10 20:48:30 +01:00
David Baker
f2232a0740 Enable production deploys
and also remove the manual deployment updating as it looks like the netlify action supports it anyway
2022-05-10 19:55:05 +01:00
David Baker
04c6d990bd Make dir name match 2022-05-10 19:39:27 +01:00
David Baker
455bb09108 Try bumping token permissions 2022-05-10 19:35:40 +01:00
David Baker
d8fe617535 Try this path 2022-05-10 19:20:15 +01:00
David Baker
970568fd17 yaml indenting 2022-05-10 19:19:22 +01:00
David Baker
f6677889e0 Hopefully deploy main branch to netlify 2022-05-10 19:11:45 +01:00
David Baker
04780ab7aa Seems we can turn noUnusedLocals on after all 2022-05-10 18:12:10 +01:00
David Baker
b7df8019f0 Import request typings
to tell tsc about _Request which has abort()
2022-05-10 18:09:17 +01:00
David Baker
0a9115248d Include more js-sdk types 2022-05-10 17:45:54 +01:00
David Baker
27d492e9e2 Pin js-sdk commit & add olm for types 2022-05-10 17:41:50 +01:00
David Baker
bc22d36ef8 Use the merged js-sdk 2022-05-10 17:22:20 +01:00
David Baker
cf9625f33e Final type fixes
Revert previous type import hack and type a couple more bits
2022-05-10 17:18:26 +01:00
David Baker
446fd9c7c0 Make tsc happy with the js-sdk 2022-05-07 19:02:54 +01:00
David Baker
adc7892d8c Enable type checking & disable lint warning 2022-05-06 22:55:47 +01:00
David Baker
f805f4ead6 Remove some unnecessary tsconfig bits 2022-05-06 22:44:03 +01:00
David Baker
00ffa1b6cd Add types from merge & fix import order 2022-05-06 22:43:22 +01:00
David Baker
055fbe786d Merge remote-tracking branch 'origin/main' into dbkr/typescript_round_1 2022-05-06 21:41:14 +01:00
David Baker
7a561bd034 Merge pull request #315 from vector-im/dbkr/fix_lint_errors
Fix lint errors
2022-05-06 21:35:55 +01:00
David Baker
5fb1f556d5 Prettier 2022-05-06 21:34:58 +01:00
David Baker
f4ba315cef Add more hook dependencies 2022-05-06 21:33:49 +01:00
David Baker
9ba12da544 Merge branch 'main' into dbkr/fix_lint_errors 2022-05-06 21:31:35 +01:00
David Baker
657096fd9a Merge pull request #314 from vector-im/dbkr/vscode_prettier
Set formatter to prettier
2022-05-06 21:29:00 +01:00
David Baker
9374900ce0 Merge pull request #313 from vector-im/dbkr/fix_time_limit
Return to normal state when time limit reached
2022-05-06 21:28:40 +01:00
David Baker
7e5610eb36 Prettier 2022-05-06 21:27:07 +01:00
David Baker
1253638861 Store unmuteError in main state 2022-05-06 21:23:29 +01:00
David Baker
83feb28909 Merge pull request #312 from vector-im/dbkr/license_headers
Add all the license headers
2022-05-06 21:19:44 +01:00
David Baker
5422cb76f1 Merge branch 'main' into dbkr/license_headers 2022-05-06 21:18:45 +01:00
David Baker
a6eb52ae76 Merge pull request #311 from vector-im/dbkr/fix_pt_button_behaviour
Fix mouseup/down behaviour of PTT button
2022-05-06 21:17:50 +01:00
David Baker
4488947eed Initial round of typescripting 2022-05-06 11:32:09 +01:00
David Baker
bf8f164f55 Fix lint errors
Various hooks either missing dependencies or with extra ones.

Two remaining errors are from the recapcta code where I can't
work out if the extra dependency is intentional or not.
2022-05-05 13:15:07 +01:00
David Baker
5487fbc048 Set formatter to prettier
Although this will only work with the extension installed
2022-05-05 12:31:09 +01:00
David Baker
a70dbb130f Run prettier 2022-05-05 12:26:30 +01:00
David Baker
7edf544d73 Return to normal state when time limit reached 2022-05-05 12:22:51 +01:00
David Baker
ad3bde9920 Undo unintentionally commented line 2022-05-04 17:36:35 +01:00
David Baker
85a98b3706 Remove onWindowBlur
we already do this in usePTT
2022-05-04 17:35:43 +01:00
David Baker
85e3f3761a Add all the license headers 2022-05-04 17:09:48 +01:00
David Baker
f0b116714b Fix mouseup/down behaviour of PTT button
Handle mouseup events anywhere so the button releases if you move
the cursor out of the button & release. Likewsie for window losing
focus.
2022-05-04 16:52:45 +01:00
David Baker
dbef06269b Merge pull request #310 from vector-im/ptt
Add feature-flagged support for Radio/PTT Mode
2022-05-04 11:40:26 +01:00
David Baker
894815268a Merge remote-tracking branch 'origin/main' into ptt 2022-05-04 11:37:52 +01:00
David Baker
8ecec0bc7e 3 more warnings in the PTT stuff 2022-05-04 11:35:33 +01:00
David Baker
66839e02f6 Add ESLint support too 2022-05-04 11:35:15 +01:00
David Baker
bad8f36bf5 Add prettier support
+ CI to check formatting, and fix the couple of instances that
were not in prettier format (case in HTML clour codes).
2022-05-04 11:35:15 +01:00
Robert Long
f5c50230a9 Enable source-maps 2022-05-04 11:34:28 +01:00
David Baker
0136fd3cab Run prettier 2022-05-04 11:24:25 +01:00
Robert Long
2d18953344 Merge pull request #309 from vector-im/dbkr/prettier
Add prettier & ESLint support
2022-05-03 10:36:08 -07:00
Robert Long
d930ab869a Merge pull request #308 from vector-im/dbkr/ptt_enable_flag
Put PTT behind 'feature flag'
2022-05-03 10:34:01 -07:00
Robert Long
dbdb82bd74 Switch to useShouldShowPtt hook 2022-05-03 10:32:06 -07:00
David Baker
61309bacd9 Add ESLint support too 2022-05-03 15:32:16 +01:00
David Baker
b3e88d33a7 Add prettier support
+ CI to check formatting, and fix the couple of instances that
were not in prettier format (case in HTML clour codes).
2022-05-03 14:24:04 +01:00
David Baker
73fda641c8 Switch js-sdk depdendency back to group-call branch 2022-05-03 13:19:57 +01:00
David Baker
be01a4bd81 Commit missed file 2022-05-03 12:05:40 +01:00
David Baker
0814e3c905 Revert unintentional commit 2022-05-03 12:05:22 +01:00
Robert Long
c7dd2e2093 Merge pull request #307 from vector-im/dbkr/fix_toggle
Fix toggle button toggling
2022-05-02 11:30:05 -07:00
Robert Long
cfa525f957 Merge pull request #306 from vector-im/dbkr/button_for_ptt
Wire up pressing the PTT button to unmute as well as spacebar
2022-05-02 11:28:00 -07:00
David Baker
43d579744f Put PTT behind 'feature flag'
AKA does the URL hash start with '#ptt'

This will let us merge PTT back to the main branch
2022-04-29 19:25:00 +01:00
David Baker
48a008093b Fix toggle button toggling
Just use isSelected directly rather than makking the button have its
own state. Also, the isPressed from useToggleButton looks like its
whether the user has the mouse button down on it or not rather than
whether the toggle switch is on, which was making the state wrong.
2022-04-29 19:08:32 +01:00
David Baker
70c099c4b5 Wire up pressing the PTT button to unmute as well as spacebar 2022-04-29 18:56:17 +01:00
Robert Long
363f2340a0 Finish basic ptt implemenation 2022-04-28 17:44:50 -07:00
Robert Long
3a6346aa63 Create a voice group call when using ptt 2022-04-28 11:13:20 -07:00
Robert Long
9ef9680e07 Fix PTT button alignment 2022-04-28 11:13:01 -07:00
Robert Long
e3cec93669 Add basic mobile styling 2022-04-27 17:19:58 -07:00
Robert Long
b6c926d2c8 Additional in-room PTT styling 2022-04-27 16:47:23 -07:00
Robert Long
c430ebb3a3 Finish first pass at PTT lobby UI 2022-04-27 15:18:55 -07:00
Robert Long
ae13814449 Merge pull request #305 from vector-im/enable-source-maps
Enable source-maps
2022-04-27 13:51:40 -07:00
Robert Long
dc75c1cfb4 Enable source-maps 2022-04-27 12:11:59 -07:00
Robert Long
38f9a79bd3 Initial PTT designs 2022-04-22 18:05:48 -07:00
Robert Long
fc1aaf02bf Use dbkr/ptt matrix-js-sdk package 2022-04-22 11:15:39 -07:00
104 changed files with 3556 additions and 232 deletions

41
.eslintrc.js Normal file
View File

@@ -0,0 +1,41 @@
module.exports = {
plugins: [
"matrix-org",
],
extends: [
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"prettier",
],
env: {
browser: true,
node: true,
},
parserOptions: {
"ecmaVersion": "latest",
"sourceType": "module",
},
rules: {
// We break this rule in a few places: dial it back to a warning
// (and run with max warnings) to tolerate the existing code
"react-hooks/exhaustive-deps": ["warn"],
"jsx-a11y/media-has-caption": ["off"],
},
overrides: [
{
files: [
"src/**/*.{ts,tsx}",
],
extends: [
"plugin:matrix-org/typescript",
"plugin:matrix-org/react",
"prettier",
],
},
],
settings: {
react: {
version: "detect",
},
},
};

31
.github/workflows/build.yaml vendored Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
node_modules
dist

1
.prettierrc.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,5 +1,6 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.insertSpaces": true,
"editor.tabSize": 2
}
}

View File

@@ -9,7 +9,7 @@ RUN element-call/scripts/dockerbuild.sh
FROM nginxinc/nginx-unprivileged:alpine
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

4
config/netlify_redirects Normal file
View 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

View File

@@ -5,7 +5,11 @@
"build": "vite build",
"serve": "vite preview",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
"build-storybook": "build-storybook",
"prettier:check": "prettier -c src",
"prettier:format": "prettier -w src",
"lint:js": "eslint --max-warnings 2 src",
"lint:types": "tsc"
},
"dependencies": {
"@juggle/resize-observer": "^3.3.1",
@@ -31,7 +35,7 @@
"color-hash": "^2.0.1",
"events": "^3.3.0",
"lodash-move": "^1.1.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#acef1d7dd0b915368730efabee94deb42b2e4058",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",
"pako": "^2.0.4",
@@ -48,10 +52,24 @@
},
"devDependencies": {
"@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",
"@types/request": "^2.48.8",
"@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0",
"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",
"storybook-builder-vite": "^0.1.12",
"typescript": "^4.6.4",
"vite": "^2.4.2",
"vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^0.4.0"

17
src/@types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "matrix-js-sdk/src/@types/global";

2
src/@types/modules.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />

View File

@@ -1,5 +1,6 @@
import React, { useMemo } from "react";
import classNames from "classnames";
import styles from "./Avatar.module.css";
const backgroundColors = [
@@ -13,7 +14,7 @@ const backgroundColors = [
"#74D12C",
];
function hashStringToArrIndex(str, arrLength) {
function hashStringToArrIndex(str: string, arrLength: number) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
@@ -23,7 +24,16 @@ function hashStringToArrIndex(str, arrLength) {
return sum % arrLength;
}
export function Avatar({
interface Props extends React.HTMLAttributes<HTMLDivElement> {
bgKey?: string;
src: string;
fallback: string;
size?: number;
className: string;
style: React.CSSProperties;
}
export const Avatar: React.FC<Props> = ({
bgKey,
src,
fallback,
@@ -31,7 +41,7 @@ export function Avatar({
className,
style,
...rest
}) {
}) => {
const backgroundColor = useMemo(() => {
const index = hashStringToArrIndex(
bgKey || fallback || src || "",
@@ -40,6 +50,7 @@ export function Avatar({
return backgroundColors[index];
}, [bgKey, src, fallback]);
/* eslint-disable jsx-a11y/alt-text */
return (
<div
className={classNames(styles.avatar, styles[size || "md"], className)}
@@ -55,4 +66,4 @@ export function Avatar({
)}
</div>
);
}
};

View File

@@ -4,35 +4,63 @@ import classNames from "classnames";
import { Avatar } from "./Avatar";
import { getAvatarUrl } from "./matrix-utils";
export function Facepile({ className, client, participants, ...rest }) {
const overlapMap = {
xs: 2,
sm: 4,
md: 8,
};
const sizeMap = {
xs: 24,
sm: 32,
md: 36,
};
export function Facepile({
className,
client,
participants,
max,
size,
...rest
}) {
const _size = sizeMap[size];
const _overlap = overlapMap[size];
return (
<div
className={classNames(styles.facepile, className)}
className={classNames(styles.facepile, styles[size], className)}
title={participants.map((member) => member.name).join(", ")}
style={{ width: participants.length * (_size - _overlap) + _overlap }}
{...rest}
>
{participants.slice(0, 3).map((member, i) => {
{participants.slice(0, max).map((member, i) => {
const avatarUrl = member.user?.avatarUrl;
return (
<Avatar
key={member.userId}
size="xs"
src={avatarUrl && getAvatarUrl(client, avatarUrl, 22)}
size={size}
src={avatarUrl && getAvatarUrl(client, avatarUrl, _size)}
fallback={member.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
style={{ left: i * 22 }}
style={{ left: i * (_size - _overlap) }}
/>
);
})}
{participants.length > 3 && (
{participants.length > max && (
<Avatar
key="additional"
size="xs"
fallback={`+${participants.length - 3}`}
size={size}
fallback={`+${participants.length - max}`}
className={styles.avatar}
style={{ left: 3 * 22 }}
style={{ left: max * (_size - _overlap) }}
/>
)}
</div>
);
}
Facepile.defaultProps = {
max: 3,
size: "xs",
};

View File

@@ -1,11 +1,26 @@
.facepile {
width: 100%;
height: 24px;
position: relative;
}
.facepile.xs {
height: 24px;
}
.facepile.sm {
height: 32px;
}
.facepile.md {
height: 36px;
}
.facepile .avatar {
position: absolute;
top: 0;
border: 1px solid var(--bgColor2);
}
.facepile.md .avatar {
border-width: 2px;
}

View File

@@ -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 {
uniqueNamesGenerator,
adjectives,
colors,
animals,
Config,
} from "unique-names-generator";
const elements = [
@@ -126,7 +143,7 @@ const elements = [
"oganesson",
];
export function generateRandomName(config) {
export function generateRandomName(config: Config): string {
return uniqueNamesGenerator({
dictionaries: [colors, adjectives, animals, elements],
style: "lowerCase",

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
import { useState, useCallback } from "react";
import { useClient } from "../ClientContext";
@@ -7,39 +23,42 @@ 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 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 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 { user_id, access_token, device_id } =
await interactiveAuth.attemptAuth();
const client = await initClient({
baseUrl: defaultHomeserver,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
const client = await initClient({
baseUrl: defaultHomeserver,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
setClient(client, { user_id, access_token, device_id });
setClient(client, { user_id, access_token, device_id });
return client;
}, []);
return client;
},
[setClient]
);
return [state, auth];
}

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
import { useState, useEffect, useCallback, useRef } from "react";
import { useClient } from "../ClientContext";
@@ -89,7 +105,7 @@ export function useInteractiveRegistration() {
return client;
},
[]
[setClient]
);
return [state, register];

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useEffect, useCallback, useRef, useState } from "react";

View File

@@ -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 classNames from "classnames";
import styles from "./Button.module.css";
@@ -7,6 +23,8 @@ import { ReactComponent as VideoIcon } from "../icons/Video.svg";
import { ReactComponent as DisableVideoIcon } from "../icons/DisableVideo.svg";
import { ReactComponent as HangupIcon } from "../icons/Hangup.svg";
import { ReactComponent as ScreenshareIcon } from "../icons/Screenshare.svg";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { useButton } from "@react-aria/button";
import { mergeProps, useObjectRef } from "@react-aria/utils";
import { TooltipTrigger } from "../Tooltip";
@@ -20,6 +38,7 @@ export const variantToClassName = {
copy: [styles.copyButton],
iconCopy: [styles.iconCopyButton],
secondaryCopy: [styles.copyButton],
secondaryHangup: [styles.secondaryHangup],
};
export const sizeToClassName = {
@@ -126,3 +145,25 @@ export function HangupButton({ className, ...rest }) {
</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>
);
}

View File

@@ -20,6 +20,7 @@ limitations under the License.
.iconButton,
.iconCopyButton,
.secondary,
.secondaryHangup,
.copyButton {
position: relative;
display: flex;
@@ -34,6 +35,7 @@ limitations under the License.
}
.secondary,
.secondaryHangup,
.button,
.copyButton {
padding: 7px 15px;
@@ -53,6 +55,7 @@ limitations under the License.
.iconButton:focus,
.iconCopyButton:focus,
.secondary:focus,
.secondaryHangup:focus,
.copyButton:focus {
outline: auto;
}
@@ -119,6 +122,12 @@ limitations under the License.
background-color: transparent;
}
.secondaryHangup {
color: #ff5b55;
border: 2px solid #ff5b55;
background-color: transparent;
}
.copyButton.secondaryCopy {
color: var(--textColor1);
border-color: var(--textColor1);

View File

@@ -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 useClipboard from "react-use-clipboard";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";

View File

@@ -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 { Link } from "react-router-dom";
import classNames from "classnames";

View File

@@ -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 "./CopyButton";
export * from "./LinkButton";

View File

@@ -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 React, { forwardRef } from "react";
import styles from "./Form.module.css";

View File

@@ -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 { Link } from "react-router-dom";
import { CopyButton } from "../button";

View File

@@ -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 { Modal, ModalContent } from "../Modal";
import { Button } from "../button";

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState, useCallback } from "react";
import { createRoom, roomAliasFromRoomName } from "../matrix-utils";
import { useGroupCallRooms } from "./useGroupCallRooms";
@@ -13,22 +29,25 @@ import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { useHistory } from "react-router-dom";
import { Headline, Title } from "../typography/Typography";
import { Form } from "../form/Form";
import { useShouldShowPtt } from "../useShouldShowPtt";
export function RegisteredView({ client }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const history = useHistory();
const shouldShowPtt = useShouldShowPtt();
const onSubmit = useCallback(
(e) => {
e.preventDefault();
const data = new FormData(e.target);
const roomName = data.get("callName");
const ptt = data.get("ptt") !== null;
async function submit() {
setError(undefined);
setLoading(true);
const roomIdOrAlias = await createRoom(client, roomName);
const roomIdOrAlias = await createRoom(client, roomName, ptt);
if (roomIdOrAlias) {
history.push(`/room/${roomIdOrAlias}`);
@@ -87,6 +106,7 @@ export function RegisteredView({ client }) {
required
autoComplete="off"
/>
<Button
type="submit"
size="lg"
@@ -96,6 +116,16 @@ export function RegisteredView({ client }) {
{loading ? "Loading..." : "Go"}
</Button>
</FieldRow>
{shouldShowPtt && (
<FieldRow className={styles.fieldRow}>
<InputField
id="ptt"
name="ptt"
label="Push to Talk"
type="checkbox"
/>
</FieldRow>
)}
{error && (
<FieldRow className={styles.fieldRow}>
<ErrorMessage>{error.message}</ErrorMessage>

View File

@@ -7,6 +7,10 @@
}
.fieldRow {
margin-bottom: 24px;
}
.fieldRow:last-child {
margin-bottom: 0;
}

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from "react";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { UserMenuContainer } from "../UserMenuContainer";
@@ -15,8 +31,10 @@ import { Form } from "../form/Form";
import styles from "./UnauthenticatedView.module.css";
import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName";
import { useShouldShowPtt } from "../useShouldShowPtt";
export function UnauthenticatedView() {
const shouldShowPtt = useShouldShowPtt();
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [{ privacyPolicyUrl, recaptchaKey }, register] =
@@ -28,6 +46,7 @@ export function UnauthenticatedView() {
const data = new FormData(e.target);
const roomName = data.get("callName");
const displayName = data.get("displayName");
const ptt = data.get("ptt") !== null;
async function submit() {
setError(undefined);
@@ -41,7 +60,7 @@ export function UnauthenticatedView() {
recaptchaResponse,
true
);
const roomIdOrAlias = await createRoom(client, roomName);
const roomIdOrAlias = await createRoom(client, roomName, ptt);
if (roomIdOrAlias) {
history.push(`/room/${roomIdOrAlias}`);
@@ -111,6 +130,16 @@ export function UnauthenticatedView() {
autoComplete="off"
/>
</FieldRow>
{shouldShowPtt && (
<FieldRow>
<InputField
id="ptt"
name="ptt"
label="Push to Talk"
type="checkbox"
/>
</FieldRow>
)}
<Caption>
By clicking "Go", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>

View File

@@ -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";
const tsCache = {};
@@ -81,7 +97,7 @@ export function useGroupCallRooms(client) {
client.removeListener("GroupCall.incoming", updateRooms);
client.removeListener("GroupCall.participants", updateRooms);
};
}, []);
}, [client]);
return rooms;
}

View File

@@ -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 React, { useEffect } from "react";
import { useCallback } from "react";

View File

@@ -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 classNames from "classnames";
import styles from "./Input.module.css";

View File

@@ -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 { HiddenSelect, useSelect } from "@react-aria/select";
import { useButton } from "@react-aria/button";

57
src/input/Toggle.jsx Normal file
View 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>
);
}

View File

@@ -0,0 +1,46 @@
.toggle {
align-items: center;
margin-bottom: 20px;
}
.button {
position: relative;
padding: 0;
transition: background-color 0.2s ease-out 0.1s;
width: 44px;
height: 24px;
border: none;
border-radius: 21px;
background-color: #6f7882;
cursor: pointer;
margin-right: 8px;
}
.ball {
transition: left 0.15s ease-out 0.1s;
position: absolute;
width: 20px;
height: 20px;
border-radius: 21px;
background-color: #15191e;
left: 2px;
top: 2px;
}
.label {
padding: 10px 8px;
line-height: 24px;
color: #6f7882;
}
.toggle.on .button {
background-color: #0dbd8b;
}
.toggle.on .ball {
left: 22px;
}
.toggle.on .label {
color: #ffffff;
}

View File

@@ -54,6 +54,7 @@ const history = createBrowserHistory();
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? "production",
integrations: [
new Integrations.BrowserTracing({
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),

View File

@@ -76,7 +76,7 @@ export function isLocalRoomId(roomId) {
return parts[1] === defaultHomeserverHost;
}
export async function createRoom(client, name) {
export async function createRoom(client, name, isPtt = false) {
const { room_id, room_alias } = await client.createRoom({
visibility: "private",
preset: "public_chat",
@@ -107,9 +107,12 @@ export async function createRoom(client, name) {
},
});
console.log({ isPtt });
await client.createGroupCall(
room_id,
GroupCallType.Video,
isPtt ? GroupCallType.Voice : GroupCallType.Video,
isPtt,
GroupCallIntent.Prompt
);

View File

@@ -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 { DismissButton, useOverlay } from "@react-aria/overlays";
import { FocusScope } from "@react-aria/focus";

View File

@@ -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 styles from "./PopoverMenu.module.css";
import { useMenuTriggerState } from "@react-stately/menu";

View File

@@ -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 { Button } from "../button";
import { useProfile } from "./useProfile";

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useState, useCallback, useEffect } from "react";
import { getAvatarUrl } from "../matrix-utils";

77
src/room/AudioPreview.jsx Normal file
View File

@@ -0,0 +1,77 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import styles from "./AudioPreview.module.css";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { SelectInput } from "../input/SelectInput";
import { Item } from "@react-stately/collections";
import { Body } from "../typography/Typography";
export function AudioPreview({
state,
roomName,
audioInput,
audioInputs,
setAudioInput,
audioOutput,
audioOutputs,
setAudioOutput,
}) {
return (
<>
<h1>{`${roomName} - Radio Call`}</h1>
<div className={styles.preview}>
{state === GroupCallState.LocalCallFeedUninitialized && (
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
Microphone permissions needed to join the call.
</Body>
)}
{state === GroupCallState.InitializingLocalCallFeed && (
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
Accept microphone permissions to join the call.
</Body>
)}
{state === GroupCallState.LocalCallFeedInitialized && (
<>
<SelectInput
label="Microphone"
selectedKey={audioInput}
onSelectionChange={setAudioInput}
className={styles.inputField}
>
{audioInputs.map(({ deviceId, label }) => (
<Item key={deviceId}>{label}</Item>
))}
</SelectInput>
{audioOutputs.length > 0 && (
<SelectInput
label="Speaker"
selectedKey={audioOutput}
onSelectionChange={setAudioOutput}
className={styles.inputField}
>
{audioOutputs.map(({ deviceId, label }) => (
<Item key={deviceId}>{label}</Item>
))}
</SelectInput>
)}
</>
)}
</div>
</>
);
}

View 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;
}
}

View File

@@ -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 styles from "./CallEndedView.module.css";
import { LinkButton } from "../button";

View File

@@ -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 { Modal, ModalContent } from "../Modal";
import { Button } from "../button";

View File

@@ -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 { Button } from "../button";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";

View File

@@ -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 React, {
useEffect,

View File

@@ -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 { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";

View File

@@ -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 { useHistory } from "react-router-dom";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
@@ -5,6 +21,7 @@ import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView";
import { InCallView } from "./InCallView";
import { PTTCallView } from "./PTTCallView";
import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { useLocationNavigation } from "../useLocationNavigation";
@@ -47,6 +64,7 @@ export function GroupCallView({
localScreenshareFeed,
screenshareFeeds,
hasLocalParticipant,
participants,
} = useGroupCall(groupCall);
useEffect(() => {
@@ -72,27 +90,43 @@ export function GroupCallView({
if (error) {
return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered) {
return (
<InCallView
groupCall={groupCall}
client={client}
roomName={groupCall.room.name}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
toggleMicrophoneMuted={toggleMicrophoneMuted}
userMediaFeeds={userMediaFeeds}
activeSpeaker={activeSpeaker}
onLeave={onLeave}
toggleScreensharing={toggleScreensharing}
isScreensharing={isScreensharing}
localScreenshareFeed={localScreenshareFeed}
screenshareFeeds={screenshareFeeds}
setShowInspector={onChangeShowInspector}
showInspector={showInspector}
roomId={roomId}
/>
);
if (groupCall.isPtt) {
return (
<PTTCallView
client={client}
roomId={roomId}
roomName={groupCall.room.name}
groupCall={groupCall}
participants={participants}
userMediaFeeds={userMediaFeeds}
onLeave={onLeave}
setShowInspector={onChangeShowInspector}
showInspector={showInspector}
/>
);
} else {
return (
<InCallView
groupCall={groupCall}
client={client}
roomName={groupCall.room.name}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
toggleMicrophoneMuted={toggleMicrophoneMuted}
userMediaFeeds={userMediaFeeds}
activeSpeaker={activeSpeaker}
onLeave={onLeave}
toggleScreensharing={toggleScreensharing}
isScreensharing={isScreensharing}
localScreenshareFeed={localScreenshareFeed}
screenshareFeeds={screenshareFeeds}
setShowInspector={onChangeShowInspector}
showInspector={showInspector}
roomId={roomId}
/>
);
}
} else if (state === GroupCallState.Entering) {
return (
<FullScreenView>
@@ -105,6 +139,7 @@ export function GroupCallView({
return (
<LobbyView
client={client}
groupCall={groupCall}
hasLocalParticipant={hasLocalParticipant}
roomName={groupCall.room.name}
state={state}

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useMemo } from "react";
import styles from "./InCallView.module.css";
import {

View File

@@ -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 { Modal, ModalContent } from "../Modal";
import { CopyButton } from "../button";

View File

@@ -1,23 +1,36 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect, useRef } from "react";
import 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 { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { useCallFeed } from "../video-grid/useCallFeed";
import { useMediaStream } from "../video-grid/useMediaStream";
import { getRoomUrl } from "../matrix-utils";
import { OverflowMenu } from "./OverflowMenu";
import { UserMenuContainer } from "../UserMenuContainer";
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 { useMediaHandler } from "../settings/useMediaHandler";
import { VideoPreview } from "./VideoPreview";
import { AudioPreview } from "./AudioPreview";
export function LobbyView({
client,
groupCall,
roomName,
state,
onInitLocalCallFeed,
@@ -32,11 +45,14 @@ export function LobbyView({
roomId,
}) {
const { stream } = useCallFeed(localCallFeed);
const { audioOutput } = useMediaHandler();
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const avatarSize = (previewBounds.height - 66) / 2;
const {
audioInput,
audioInputs,
setAudioInput,
audioOutput,
audioOutputs,
setAudioOutput,
} = useMediaHandler();
useEffect(() => {
onInitLocalCallFeed();
@@ -64,53 +80,31 @@ export function LobbyView({
</Header>
<div className={styles.joinRoom}>
<div className={styles.joinRoomContent}>
<div className={styles.preview} ref={previewRef}>
<video ref={videoRef} muted playsInline disablePictureInPicture />
{state === GroupCallState.LocalCallFeedUninitialized && (
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
Webcam/microphone permissions needed to join the call.
</Body>
)}
{state === GroupCallState.InitializingLocalCallFeed && (
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
Accept webcam/microphone permissions to join the call.
</Body>
)}
{state === GroupCallState.LocalCallFeedInitialized && (
<>
{localVideoMuted && (
<div className={styles.avatarContainer}>
<Avatar
style={{
width: avatarSize,
height: avatarSize,
borderRadius: avatarSize,
fontSize: Math.round(avatarSize / 2),
}}
src={avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>
</div>
)}
<div className={styles.previewButtons}>
<MicButton
muted={microphoneMuted}
onPress={toggleMicrophoneMuted}
/>
<VideoButton
muted={localVideoMuted}
onPress={toggleLocalVideoMuted}
/>
<OverflowMenu
roomId={roomId}
setShowInspector={setShowInspector}
showInspector={showInspector}
client={client}
/>
</div>
</>
)}
</div>
{groupCall.isPtt ? (
<AudioPreview
roomName={roomName}
state={state}
audioInput={audioInput}
audioInputs={audioInputs}
setAudioInput={setAudioInput}
audioOutput={audioOutput}
audioOutputs={audioOutputs}
setAudioOutput={setAudioOutput}
/>
) : (
<VideoPreview
state={state}
client={client}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
toggleMicrophoneMuted={toggleMicrophoneMuted}
setShowInspector={setShowInspector}
showInspector={showInspector}
stream={stream}
audioOutput={audioOutput}
/>
)}
<Button
ref={joinCallButtonRef}
className={styles.copyButton}

View File

@@ -46,58 +46,6 @@ limitations under the License.
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 {
position: absolute;
width: 100%;
@@ -118,17 +66,3 @@ limitations under the License.
.copyButton:last-child {
margin-bottom: 0;
}
.previewButtons > * {
margin-right: 30px;
}
.previewButtons > :last-child {
margin-right: 0px;
}
@media (min-width: 800px) {
.preview {
margin-top: 40px;
}
}

View File

@@ -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 { Button } from "../button";
import { Menu } from "../Menu";

View File

@@ -0,0 +1,25 @@
.pttButton {
width: 100vw;
height: 100vh;
max-height: 232px;
max-width: 232px;
border-radius: 116px;
color: ##fff;
border: 6px solid #0dbd8b;
background-color: #21262c;
position: relative;
padding: 0;
}
.talking {
background-color: #0dbd8b;
box-shadow: 0px 0px 0px 17px rgba(13, 189, 139, 0.2),
0px 0px 0px 34px rgba(13, 189, 139, 0.2);
}
.error {
background-color: #ff5b55;
border-color: #ff5b55;
box-shadow: 0px 0px 0px 17px rgba(255, 91, 85, 0.2),
0px 0px 0px 34px rgba(255, 91, 85, 0.2);
}

164
src/room/PTTButton.tsx Normal file
View File

@@ -0,0 +1,164 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useEffect, useState, createRef } from "react";
import classNames from "classnames";
import styles from "./PTTButton.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { Avatar } from "../Avatar";
interface Props {
showTalkOverError: boolean;
activeSpeakerUserId: string;
activeSpeakerDisplayName: string;
activeSpeakerAvatarUrl: string;
activeSpeakerIsLocalUser: boolean;
size: number;
startTalking: () => void;
stopTalking: () => void;
}
interface State {
isHeld: boolean;
// If the button is being pressed by touch, the ID of that touch
activeTouchID: number | null;
}
export const PTTButton: React.FC<Props> = ({
showTalkOverError,
activeSpeakerUserId,
activeSpeakerDisplayName,
activeSpeakerAvatarUrl,
activeSpeakerIsLocalUser,
size,
startTalking,
stopTalking,
}) => {
const buttonRef = createRef<HTMLButtonElement>();
const [{ isHeld, activeTouchID }, setState] = useState<State>({
isHeld: false,
activeTouchID: null,
});
const onWindowMouseUp = useCallback(
(e) => {
if (isHeld) stopTalking();
setState({ isHeld: false, activeTouchID: null });
},
[isHeld, setState, stopTalking]
);
const onWindowTouchEnd = useCallback(
(e: TouchEvent) => {
// ignore any ended touches that weren't the one pressing the
// button (bafflingly the TouchList isn't an iterable so we
// have to do this a really old-school way).
let touchFound = false;
for (let i = 0; i < e.changedTouches.length; ++i) {
if (e.changedTouches.item(i).identifier === activeTouchID) {
touchFound = true;
break;
}
}
if (!touchFound) return;
e.preventDefault();
if (isHeld) stopTalking();
setState({ isHeld: false, activeTouchID: null });
},
[isHeld, activeTouchID, setState, stopTalking]
);
const onButtonMouseDown = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setState({ isHeld: true, activeTouchID: null });
startTalking();
},
[setState, startTalking]
);
const onButtonTouchStart = useCallback(
(e: TouchEvent) => {
e.preventDefault();
if (isHeld) return;
setState({
isHeld: true,
activeTouchID: e.changedTouches.item(0).identifier,
});
startTalking();
},
[isHeld, setState, startTalking]
);
useEffect(() => {
const currentButtonElement = buttonRef.current;
// These listeners go on the window so even if the user's cursor / finger
// leaves the button while holding it, the button stays pushed until
// they stop clicking / tapping.
window.addEventListener("mouseup", onWindowMouseUp);
window.addEventListener("touchend", onWindowTouchEnd);
// This is a native DOM listener too because we want to preventDefault in it
// to stop also getting a click event, so we need it to be non-passive.
currentButtonElement.addEventListener("touchstart", onButtonTouchStart, {
passive: false,
});
return () => {
window.removeEventListener("mouseup", onWindowMouseUp);
window.removeEventListener("touchend", onWindowTouchEnd);
currentButtonElement.removeEventListener(
"touchstart",
onButtonTouchStart
);
};
}, [onWindowMouseUp, onWindowTouchEnd, onButtonTouchStart, buttonRef]);
return (
<button
className={classNames(styles.pttButton, {
[styles.talking]: activeSpeakerUserId,
[styles.error]: showTalkOverError,
})}
onMouseDown={onButtonMouseDown}
ref={buttonRef}
>
{activeSpeakerIsLocalUser || !activeSpeakerUserId ? (
<MicIcon
className={styles.micIcon}
width={size / 3}
height={size / 3}
/>
) : (
<Avatar
key={activeSpeakerUserId}
style={{
width: size - 12,
height: size - 12,
borderRadius: size - 12,
fontSize: Math.round((size - 12) / 2),
}}
src={activeSpeakerAvatarUrl}
fallback={activeSpeakerDisplayName.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
)}
</button>
);
};

View File

@@ -0,0 +1,112 @@
.pttCallView {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
min-height: 100%;
position: fixed;
height: 100%;
width: 100%;
}
@media (hover: none) {
.pttCallView {
user-select: none;
}
}
.center {
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
}
.participants {
display: flex;
flex-direction: column;
margin: 20px;
}
.participants > p {
color: #a9b2bc;
margin-bottom: 8px;
}
.facepile {
align-self: center;
}
.talkingInfo {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
height: 88px;
}
.speakerIcon {
margin-right: 8px;
}
.pttButtonContainer {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
justify-content: center;
}
.actionTip {
margin-top: 20px;
margin-bottom: 20px;
font-size: 17px;
}
.footer {
position: relative;
display: flex;
justify-content: center;
height: 64px;
margin-bottom: 20px;
}
.footer > * {
margin-right: 30px;
}
.footer > :last-child {
margin-right: 0px;
}
@media (min-width: 800px) {
.participants {
margin-bottom: 67px;
}
.talkingInfo {
margin-bottom: 38px;
}
.center {
margin-top: 48px;
}
.actionTip {
margin-top: 42px;
margin-bottom: 45px;
}
.pttButtonContainer {
flex: 0;
margin-bottom: 0;
justify-content: flex-start;
}
.footer {
flex: auto;
order: 4;
}
}

248
src/room/PTTCallView.tsx Normal file
View File

@@ -0,0 +1,248 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { GroupCall, MatrixClient, RoomMember } from "matrix-js-sdk";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { HangupButton, InviteButton, SettingsButton } from "../button";
import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header";
import styles from "./PTTCallView.module.css";
import { Facepile } from "../Facepile";
import { PTTButton } from "./PTTButton";
import { PTTFeed } from "./PTTFeed";
import { useMediaHandler } from "../settings/useMediaHandler";
import { usePTT } from "./usePTT";
import { Timer } from "./Timer";
import { Toggle } from "../input/Toggle";
import { getAvatarUrl } from "../matrix-utils";
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
import { usePTTSounds } from "../sound/usePttSounds";
import { PTTClips } from "../sound/PTTClips";
function getPromptText(
showTalkOverError: boolean,
pttButtonHeld: boolean,
activeSpeakerIsLocalUser: boolean,
talkOverEnabled: boolean,
activeSpeakerUserId: string,
activeSpeakerDisplayName: string
): string {
const isTouchScreen = Boolean(window.ontouchstart !== undefined);
if (showTalkOverError) {
return "You can't talk at the same time";
}
if (pttButtonHeld && activeSpeakerIsLocalUser) {
if (isTouchScreen) {
return "Release to stop";
} else {
return "Release spacebar key to stop";
}
}
if (talkOverEnabled && activeSpeakerUserId && !activeSpeakerIsLocalUser) {
if (isTouchScreen) {
return `Press and hold to talk over ${activeSpeakerDisplayName}`;
} else {
return `Press and hold spacebar to talk over ${activeSpeakerDisplayName}`;
}
}
if (isTouchScreen) {
return "Press and hold to talk";
} else {
return "Press and hold spacebar to talk";
}
}
interface Props {
client: MatrixClient;
roomId: string;
roomName: string;
groupCall: GroupCall;
participants: RoomMember[];
userMediaFeeds: CallFeed[];
onLeave: () => void;
setShowInspector: (boolean) => void;
showInspector: boolean;
}
export const PTTCallView: React.FC<Props> = ({
client,
roomId,
roomName,
groupCall,
participants,
userMediaFeeds,
onLeave,
setShowInspector,
showInspector,
}) => {
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
const { modalState: settingsModalState, modalProps: settingsModalProps } =
useModalTriggerState();
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
const facepileSize = bounds.width < 800 ? "sm" : "md";
const pttButtonSize = 232;
const pttBorderWidth = 6;
const { audioOutput } = useMediaHandler();
const {
startTalkingLocalRef,
startTalkingRemoteRef,
blockedRef,
endTalkingRef,
playClip,
} = usePTTSounds();
const {
pttButtonHeld,
isAdmin,
talkOverEnabled,
setTalkOverEnabled,
activeSpeakerUserId,
startTalking,
stopTalking,
transmitBlocked,
} = usePTT(client, groupCall, userMediaFeeds, playClip);
const showTalkOverError = pttButtonHeld && transmitBlocked;
const activeSpeakerIsLocalUser =
activeSpeakerUserId && client.getUserId() === activeSpeakerUserId;
const activeSpeakerUser = activeSpeakerUserId
? client.getUser(activeSpeakerUserId)
: null;
const activeSpeakerAvatarUrl = activeSpeakerUser
? getAvatarUrl(
client,
activeSpeakerUser.avatarUrl,
pttButtonSize - pttBorderWidth * 2
)
: null;
const activeSpeakerDisplayName = activeSpeakerUser
? activeSpeakerUser.displayName
: "";
return (
<div className={styles.pttCallView} ref={containerRef}>
<PTTClips
startTalkingLocalRef={startTalkingLocalRef}
startTalkingRemoteRef={startTalkingRemoteRef}
endTalkingRef={endTalkingRef}
blockedRef={blockedRef}
/>
<Header className={styles.header}>
<LeftNav>
<RoomSetupHeaderInfo roomName={roomName} onPress={onLeave} />
</LeftNav>
<RightNav />
</Header>
<div className={styles.center}>
<div className={styles.participants}>
<p>{`${participants.length} ${
participants.length > 1 ? "people" : "person"
} connected`}</p>
<Facepile
size={facepileSize}
max={8}
className={styles.facepile}
client={client}
participants={participants}
/>
</div>
<div className={styles.footer}>
<SettingsButton onPress={() => settingsModalState.open()} />
<HangupButton onPress={onLeave} />
<InviteButton onPress={() => inviteModalState.open()} />
</div>
<div className={styles.pttButtonContainer}>
{activeSpeakerUserId ? (
<div className={styles.talkingInfo}>
<h2>
{!activeSpeakerIsLocalUser && (
<AudioIcon className={styles.speakerIcon} />
)}
{activeSpeakerIsLocalUser
? "Talking..."
: `${activeSpeakerDisplayName} is talking...`}
</h2>
<Timer value={activeSpeakerUserId} />
</div>
) : (
<div className={styles.talkingInfo} />
)}
<PTTButton
showTalkOverError={showTalkOverError}
activeSpeakerUserId={activeSpeakerUserId}
activeSpeakerDisplayName={activeSpeakerDisplayName}
activeSpeakerAvatarUrl={activeSpeakerAvatarUrl}
activeSpeakerIsLocalUser={activeSpeakerIsLocalUser}
size={pttButtonSize}
startTalking={startTalking}
stopTalking={stopTalking}
/>
<p className={styles.actionTip}>
{getPromptText(
showTalkOverError,
pttButtonHeld,
activeSpeakerIsLocalUser,
talkOverEnabled,
activeSpeakerUserId,
activeSpeakerDisplayName
)}
</p>
{userMediaFeeds.map((callFeed) => (
<PTTFeed
key={callFeed.userId}
callFeed={callFeed}
audioOutputDevice={audioOutput}
/>
))}
{isAdmin && (
<Toggle
isSelected={talkOverEnabled}
onChange={setTalkOverEnabled}
label="Talk over speaker"
id="talkOverEnabled"
/>
)}
</div>
</div>
{settingsModalState.isOpen && (
<SettingsModal
{...settingsModalProps}
setShowInspector={setShowInspector}
showInspector={showInspector}
/>
)}
{inviteModalState.isOpen && (
<InviteModal roomId={roomId} {...inviteModalProps} />
)}
</div>
);
};

26
src/room/PTTFeed.jsx Normal file
View 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 />;
}

View File

@@ -0,0 +1,3 @@
.audioFeed {
display: none;
}

View File

@@ -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 { Modal, ModalContent } from "../Modal";
import { Button } from "../button";

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from "react";
import styles from "./RoomAuthView.module.css";
import { Button } from "../button";

View File

@@ -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 { useLocation, useHistory } from "react-router-dom";
import { defaultHomeserverHost } from "../matrix-utils";

53
src/room/Timer.jsx Normal file
View File

@@ -0,0 +1,53 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect, useState } from "react";
function leftPad(value) {
return value < 10 ? "0" + value : value;
}
function formatTime(msElapsed) {
const secondsElapsed = msElapsed / 1000;
const hours = Math.floor(secondsElapsed / 3600);
const minutes = Math.floor(secondsElapsed / 60) - hours * 60;
const seconds = Math.floor(secondsElapsed - hours * 3600 - minutes * 60);
return `${leftPad(hours)}:${leftPad(minutes)}:${leftPad(seconds)}`;
}
export function Timer({ value }) {
const [timestamp, setTimestamp] = useState();
useEffect(() => {
const startTimeMs = performance.now();
let animationFrame;
function onUpdate(curTimeMs) {
const msElapsed = curTimeMs - startTimeMs;
setTimestamp(formatTime(msElapsed));
animationFrame = requestAnimationFrame(onUpdate);
}
onUpdate(startTimeMs);
return () => {
cancelAnimationFrame(animationFrame);
};
}, [value]);
return <p>{timestamp}</p>;
}

96
src/room/VideoPreview.jsx Normal file
View File

@@ -0,0 +1,96 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { MicButton, VideoButton } from "../button";
import { useMediaStream } from "../video-grid/useMediaStream";
import { OverflowMenu } from "./OverflowMenu";
import { Avatar } from "../Avatar";
import { useProfile } from "../profile/useProfile";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import styles from "./VideoPreview.module.css";
import { Body } from "../typography/Typography";
export function VideoPreview({
client,
state,
roomId,
microphoneMuted,
localVideoMuted,
toggleLocalVideoMuted,
toggleMicrophoneMuted,
setShowInspector,
showInspector,
audioOutput,
stream,
}) {
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const avatarSize = (previewBounds.height - 66) / 2;
return (
<div className={styles.preview} ref={previewRef}>
<video ref={videoRef} muted playsInline disablePictureInPicture />
{state === GroupCallState.LocalCallFeedUninitialized && (
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
Webcam/microphone permissions needed to join the call.
</Body>
)}
{state === GroupCallState.InitializingLocalCallFeed && (
<Body fontWeight="semiBold" className={styles.webcamPermissions}>
Accept webcam/microphone permissions to join the call.
</Body>
)}
{state === GroupCallState.LocalCallFeedInitialized && (
<>
{localVideoMuted && (
<div className={styles.avatarContainer}>
<Avatar
style={{
width: avatarSize,
height: avatarSize,
borderRadius: avatarSize,
fontSize: Math.round(avatarSize / 2),
}}
src={avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>
</div>
)}
<div className={styles.previewButtons}>
<MicButton
muted={microphoneMuted}
onPress={toggleMicrophoneMuted}
/>
<VideoButton
muted={localVideoMuted}
onPress={toggleLocalVideoMuted}
/>
<OverflowMenu
roomId={roomId}
setShowInspector={setShowInspector}
showInspector={showInspector}
client={client}
/>
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,65 @@
.preview {
position: relative;
min-height: 280px;
height: 50vh;
border-radius: 24px;
overflow: hidden;
background-color: var(--bgColor3);
margin: 20px;
}
.preview video {
width: calc(100% + 1px);
height: 100%;
object-fit: contain;
background-color: black;
/* transform scale doesn't perfectly match width, so make -1.01 border issues */
transform: scaleX(-1.01);
}
.avatarContainer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 66px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--bgColor3);
}
.webcamPermissions {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
text-align: center;
}
.previewButtons {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 66px;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(23, 25, 28, 0.9);
}
.previewButtons > * {
margin-right: 30px;
}
.previewButtons > :last-child {
margin-right: 0px;
}
@media (min-width: 800px) {
.preview {
margin-top: 40px;
}
}

View File

@@ -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 { useCallback, useEffect, useState } from "react";
import {
GroupCallEvent,

View File

@@ -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 { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
@@ -86,7 +102,7 @@ export function useLoadGroupCall(client, roomId, viaServers, createIfNotFound) {
.catch((error) =>
setState((prevState) => ({ ...prevState, loading: false, error }))
);
}, [client, roomId, state.reloadId]);
}, [client, roomId, state.reloadId, createIfNotFound, viaServers]);
return state;
}

278
src/room/usePTT.ts Normal file
View File

@@ -0,0 +1,278 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useEffect, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
import { logger } from "matrix-js-sdk/src/logger";
import { PlayClipFunction, PTTClipID } from "../sound/usePttSounds";
// Works out who the active speaker should be given what feeds are active and
// the power level of each user.
function getActiveSpeakerFeed(
feeds: CallFeed[],
groupCall: GroupCall
): CallFeed | null {
const activeSpeakerFeeds = feeds.filter((f) => !f.isAudioMuted());
let activeSpeakerFeed = null;
let highestPowerLevel = null;
for (const feed of activeSpeakerFeeds) {
const member = groupCall.room.getMember(feed.userId);
if (highestPowerLevel === null || member.powerLevel > highestPowerLevel) {
highestPowerLevel = member.powerLevel;
activeSpeakerFeed = feed;
}
}
return activeSpeakerFeed;
}
export interface PTTState {
pttButtonHeld: boolean;
isAdmin: boolean;
talkOverEnabled: boolean;
setTalkOverEnabled: (boolean) => void;
activeSpeakerUserId: string;
startTalking: () => void;
stopTalking: () => void;
transmitBlocked: boolean;
}
export const usePTT = (
client: MatrixClient,
groupCall: GroupCall,
userMediaFeeds: CallFeed[],
playClip: PlayClipFunction
): PTTState => {
// Used to serialise all the mute calls so they don't race. It has
// its own state as its always set separately from anything else.
const [mutePromise, setMutePromise] = useState(
Promise.resolve<boolean | void>(false)
);
// Wrapper to serialise all the mute operations on the promise
const setMicMuteWrapper = useCallback(
(muted: boolean) => {
setMutePromise(
mutePromise.then(() => {
return groupCall.setMicrophoneMuted(muted).catch((e) => {
logger.error("Failed to unmute microphone", e);
});
})
);
},
[groupCall, mutePromise]
);
const [
{
pttButtonHeld,
isAdmin,
talkOverEnabled,
activeSpeakerUserId,
transmitBlocked,
},
setState,
] = useState(() => {
const roomMember = groupCall.room.getMember(client.getUserId());
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
return {
isAdmin: roomMember.powerLevel >= 100,
talkOverEnabled: false,
pttButtonHeld: false,
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
transmitBlocked: false,
};
});
const onMuteStateChanged = useCallback(() => {
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
let blocked = false;
if (activeSpeakerUserId === null && activeSpeakerFeed !== null) {
if (activeSpeakerFeed.userId === client.getUserId()) {
playClip(PTTClipID.START_TALKING_LOCAL);
} else {
playClip(PTTClipID.START_TALKING_REMOTE);
}
} else if (activeSpeakerUserId !== null && activeSpeakerFeed === null) {
playClip(PTTClipID.END_TALKING);
} else if (
pttButtonHeld &&
activeSpeakerUserId === client.getUserId() &&
activeSpeakerFeed?.userId !== client.getUserId()
) {
// We were talking but we've been cut off: mute our own mic
// (this is the easier way of cutting other speakers off if an
// admin barges in: we could also mute the non-admin speaker
// on all receivers, but we'd have to make sure we unmuted them
// correctly.)
setMicMuteWrapper(true);
blocked = true;
playClip(PTTClipID.BLOCKED);
}
setState((prevState) => {
return {
...prevState,
activeSpeakerUserId: activeSpeakerFeed
? activeSpeakerFeed.userId
: null,
transmitBlocked: blocked,
};
});
}, [
playClip,
groupCall,
pttButtonHeld,
activeSpeakerUserId,
client,
userMediaFeeds,
setMicMuteWrapper,
]);
useEffect(() => {
for (const callFeed of userMediaFeeds) {
callFeed.addListener(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
}
const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall);
setState((prevState) => ({
...prevState,
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
}));
return () => {
for (const callFeed of userMediaFeeds) {
callFeed.removeListener(
CallFeedEvent.MuteStateChanged,
onMuteStateChanged
);
}
};
}, [userMediaFeeds, onMuteStateChanged, groupCall]);
const startTalking = useCallback(async () => {
if (pttButtonHeld) return;
let blocked = false;
if (activeSpeakerUserId && !(isAdmin && talkOverEnabled)) {
playClip(PTTClipID.BLOCKED);
blocked = true;
}
// setstate before doing the async call to mute / unmute the mic
setState((prevState) => ({
...prevState,
pttButtonHeld: true,
transmitBlocked: blocked,
}));
if (!blocked && groupCall.isMicrophoneMuted()) {
setMicMuteWrapper(false);
}
}, [
pttButtonHeld,
groupCall,
activeSpeakerUserId,
isAdmin,
talkOverEnabled,
setState,
playClip,
setMicMuteWrapper,
]);
const stopTalking = useCallback(async () => {
setState((prevState) => ({
...prevState,
pttButtonHeld: false,
transmitBlocked: false,
}));
setMicMuteWrapper(true);
}, [setMicMuteWrapper]);
useEffect(() => {
function onKeyDown(event: KeyboardEvent): void {
if (event.code === "Space") {
event.preventDefault();
if (pttButtonHeld) return;
startTalking();
}
}
function onKeyUp(event: KeyboardEvent): void {
if (event.code === "Space") {
event.preventDefault();
stopTalking();
}
}
function onBlur(): void {
// TODO: We will need to disable this for a global PTT hotkey to work
if (!groupCall.isMicrophoneMuted()) {
setMicMuteWrapper(true);
}
setState((prevState) => ({ ...prevState, pttButtonHeld: false }));
}
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
window.addEventListener("blur", onBlur);
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keyup", onKeyUp);
window.removeEventListener("blur", onBlur);
};
}, [
groupCall,
startTalking,
stopTalking,
activeSpeakerUserId,
isAdmin,
talkOverEnabled,
pttButtonHeld,
setMicMuteWrapper,
]);
const setTalkOverEnabled = useCallback((talkOverEnabled) => {
setState((prevState) => ({
...prevState,
talkOverEnabled,
}));
}, []);
return {
pttButtonHeld,
isAdmin,
talkOverEnabled,
setTalkOverEnabled,
activeSpeakerUserId,
startTalking,
stopTalking,
transmitBlocked,
};
};

View File

@@ -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";
// https://stackoverflow.com/a/9039885

View File

@@ -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 * as Sentry from "@sentry/react";

View File

@@ -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 { Modal } from "../Modal";
import styles from "./SettingsModal.module.css";

View File

@@ -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 { useCallback, useContext, useEffect, useState } from "react";
import { getLogsForReport } from "./rageshake";
import pako from "pako";
@@ -223,7 +239,7 @@ export function useSubmitRageshake() {
console.error(error);
}
},
[client]
[client, json, sending]
);
return {
@@ -294,7 +310,7 @@ export function useRageshakeRequestModal(roomId) {
return () => {
client.removeListener("event", onEvent);
};
}, [modalState.open, roomId]);
}, [modalState.open, roomId, client, modalState]);
return { modalState, modalProps: { ...modalProps, rageshakeRequestId } };
}

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {
useState,
useEffect,

View File

@@ -0,0 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.pttClip {
display: none;
}

70
src/sound/PTTClips.tsx Normal file
View File

@@ -0,0 +1,70 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import startTalkLocalOggUrl from "./start_talk_local.ogg";
import startTalkLocalMp3Url from "./start_talk_local.mp3";
import startTalkRemoteOggUrl from "./start_talk_remote.ogg";
import startTalkRemoteMp3Url from "./start_talk_remote.mp3";
import endTalkOggUrl from "./end_talk.ogg";
import endTalkMp3Url from "./end_talk.mp3";
import blockedOggUrl from "./blocked.ogg";
import blockedMp3Url from "./blocked.mp3";
import styles from "./PTTClips.module.css";
interface Props {
startTalkingLocalRef: React.RefObject<HTMLAudioElement>;
startTalkingRemoteRef: React.RefObject<HTMLAudioElement>;
endTalkingRef: React.RefObject<HTMLAudioElement>;
blockedRef: React.RefObject<HTMLAudioElement>;
}
export const PTTClips: React.FC<Props> = ({
startTalkingLocalRef,
startTalkingRemoteRef,
endTalkingRef,
blockedRef,
}) => {
return (
<>
<audio
preload="true"
className={styles.pttClip}
ref={startTalkingLocalRef}
>
<source type="audio/ogg" src={startTalkLocalOggUrl} />
<source type="audio/mpeg" src={startTalkLocalMp3Url} />
</audio>
<audio
preload="true"
className={styles.pttClip}
ref={startTalkingRemoteRef}
>
<source type="audio/ogg" src={startTalkRemoteOggUrl} />
<source type="audio/mpeg" src={startTalkRemoteMp3Url} />
</audio>
<audio preload="true" className={styles.pttClip} ref={endTalkingRef}>
<source type="audio/ogg" src={endTalkOggUrl} />
<source type="audio/mpeg" src={endTalkMp3Url} />
</audio>
<audio preload="true" className={styles.pttClip} ref={blockedRef}>
<source type="audio/ogg" src={blockedOggUrl} />
<source type="audio/mpeg" src={blockedMp3Url} />
</audio>
</>
);
};

BIN
src/sound/blocked.mp3 Normal file

Binary file not shown.

BIN
src/sound/blocked.ogg Normal file

Binary file not shown.

BIN
src/sound/end_talk.mp3 Normal file

Binary file not shown.

BIN
src/sound/end_talk.ogg Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

77
src/sound/usePttSounds.ts Normal file
View File

@@ -0,0 +1,77 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useState } from "react";
export enum PTTClipID {
START_TALKING_LOCAL,
START_TALKING_REMOTE,
END_TALKING,
BLOCKED,
}
export type PlayClipFunction = (clipID: PTTClipID) => void;
interface PTTSounds {
startTalkingLocalRef: React.RefObject<HTMLAudioElement>;
startTalkingRemoteRef: React.RefObject<HTMLAudioElement>;
endTalkingRef: React.RefObject<HTMLAudioElement>;
blockedRef: React.RefObject<HTMLAudioElement>;
playClip: PlayClipFunction;
}
export const usePTTSounds = (): PTTSounds => {
const [startTalkingLocalRef] = useState(React.createRef<HTMLAudioElement>());
const [startTalkingRemoteRef] = useState(React.createRef<HTMLAudioElement>());
const [endTalkingRef] = useState(React.createRef<HTMLAudioElement>());
const [blockedRef] = useState(React.createRef<HTMLAudioElement>());
const playClip = useCallback(
async (clipID: PTTClipID) => {
let ref: React.RefObject<HTMLAudioElement>;
switch (clipID) {
case PTTClipID.START_TALKING_LOCAL:
ref = startTalkingLocalRef;
break;
case PTTClipID.START_TALKING_REMOTE:
ref = startTalkingRemoteRef;
break;
case PTTClipID.END_TALKING:
ref = endTalkingRef;
break;
case PTTClipID.BLOCKED:
ref = blockedRef;
break;
}
if (ref.current) {
ref.current.currentTime = 0;
await ref.current.play();
} else {
console.log("No media element found");
}
},
[startTalkingLocalRef, startTalkingRemoteRef, endTalkingRef, blockedRef]
);
return {
startTalkingLocalRef,
startTalkingRemoteRef,
endTalkingRef,
blockedRef,
playClip,
};
};

View File

@@ -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 { useTabList, useTab, useTabPanel } from "@react-aria/tabs";
import { Item } from "@react-stately/collections";

View File

@@ -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 { TabContainer, TabItem } from "./Tabs";
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";

View File

@@ -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 classNames from "classnames";
import { Link as RouterLink } from "react-router-dom";

View File

@@ -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 { Headline, Title, Subtitle, Body, Caption, Micro } from "./Typography";

6
src/useShouldShowPtt.js Normal file
View File

@@ -0,0 +1,6 @@
import { useLocation } from "react-router-dom";
export function useShouldShowPtt() {
const { hash } = useLocation();
return hash.startsWith("#ptt");
}

View File

@@ -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, useRef, useState } from "react";
import { useDrag, useGesture } from "@use-gesture/react";
import { useSprings } from "@react-spring/web";

View File

@@ -4,4 +4,3 @@
flex: 1;
touch-action: none;
}

View File

@@ -1,3 +1,19 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useState } from "react";
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
import { VideoTile } from "./VideoTile";

View File

@@ -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 { animated } from "@react-spring/web";
import classNames from "classnames";

View File

@@ -85,7 +85,7 @@
.videoMutedOverlay {
width: 100%;
height: 100%;
background-color: #21262C;
background-color: #21262c;
}
.presenterLabel {
@@ -93,7 +93,7 @@
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #17191C;
background-color: #17191c;
border-radius: 4px;
display: flex;
justify-content: center;
@@ -110,4 +110,4 @@
width: 25%;
max-width: 360px;
border-radius: 20px;
}
}

View File

@@ -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 { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
import React from "react";
import { useCallFeed } from "./useCallFeed";

View File

@@ -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 { CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";

View File

@@ -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 { useRef, useEffect } from "react";
export function useMediaStream(stream, audioOutputDevice, mute = false) {

Some files were not shown because too many files have changed in this diff Show More