From 276532e2e1640e5bc343695a699d9438c44d9123 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 14 Jun 2022 12:00:26 -0400 Subject: [PATCH 1/6] Add a 'waiting for network' state to walkie-talkie mode --- .env | 1 + src/index.css | 1 + src/main.tsx | 4 +++ src/room/PTTButton.module.css | 6 ++++ src/room/PTTButton.tsx | 64 ++++++++++++++++++++--------------- src/room/PTTCallView.tsx | 16 ++++++++- src/useDelayedState.ts | 44 ++++++++++++++++++++++++ 7 files changed, 108 insertions(+), 28 deletions(-) create mode 100644 src/useDelayedState.ts diff --git a/.env b/.env index 1f0fc11..2f62d61 100644 --- a/.env +++ b/.env @@ -22,6 +22,7 @@ # VITE_THEME_PRIMARY_CONTENT=#ffffff # VITE_THEME_SECONDARY_CONTENT=#a9b2bc # VITE_THEME_TERTIARY_CONTENT=#8e99a4 +# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433 # VITE_THEME_QUATERNARY_CONTENT=#6f7882 # VITE_THEME_QUINARY_CONTENT=#394049 # VITE_THEME_SYSTEM=#21262c diff --git a/src/index.css b/src/index.css index da77b1c..7ec1e9c 100644 --- a/src/index.css +++ b/src/index.css @@ -33,6 +33,7 @@ limitations under the License. --primary-content: #ffffff; --secondary-content: #a9b2bc; --tertiary-content: #8e99a4; + --tertiary-content-20: #8e99a433; --quaternary-content: #6f7882; --quinary-content: #394049; --system: #21262c; diff --git a/src/main.tsx b/src/main.tsx index 64d9868..2a64364 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -61,6 +61,10 @@ if (import.meta.env.VITE_CUSTOM_THEME) { "--tertiary-content", import.meta.env.VITE_THEME_TERTIARY_CONTENT as string ); + style.setProperty( + "--tertiary-content-20", + import.meta.env.VITE_THEME_TERTIARY_CONTENT_20 as string + ); style.setProperty( "--quaternary-content", import.meta.env.VITE_THEME_QUATERNARY_CONTENT as string diff --git a/src/room/PTTButton.module.css b/src/room/PTTButton.module.css index fe426a0..8b15d33 100644 --- a/src/room/PTTButton.module.css +++ b/src/room/PTTButton.module.css @@ -17,6 +17,12 @@ cursor: unset; } +.networkWaiting { + background-color: var(--tertiary-content); + border-color: var(--tertiary-content); + cursor: unset; +} + .error { background-color: var(--alert); border-color: var(--alert); diff --git a/src/room/PTTButton.tsx b/src/room/PTTButton.tsx index ef575f6..c52326e 100644 --- a/src/room/PTTButton.tsx +++ b/src/room/PTTButton.tsx @@ -32,12 +32,9 @@ interface Props { 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; + networkWaiting: boolean; + enqueueNetworkWaiting: (value: boolean, delay: number) => void; + setNetworkWaiting: (value: boolean) => void; } export const PTTButton: React.FC = ({ @@ -50,19 +47,30 @@ export const PTTButton: React.FC = ({ size, startTalking, stopTalking, + networkWaiting, + enqueueNetworkWaiting, + setNetworkWaiting, }) => { const buttonRef = createRef(); - const [{ isHeld, activeTouchID }, setState] = useState({ - isHeld: false, - activeTouchID: null, - }); + const [held, setHeld] = useState(false); + const [activeTouchId, setActiveTouchId] = useState(null); + + const hold = useCallback(() => { + setHeld(true); + enqueueNetworkWaiting(true, 100); + }, [setHeld, enqueueNetworkWaiting]); + const unhold = useCallback(() => { + setHeld(false); + setNetworkWaiting(false); + }, [setHeld, setNetworkWaiting]); + const onWindowMouseUp = useCallback( (e) => { - if (isHeld) stopTalking(); - setState({ isHeld: false, activeTouchID: null }); + if (held) stopTalking(); + unhold(); }, - [isHeld, setState, stopTalking] + [held, unhold, stopTalking] ); const onWindowTouchEnd = useCallback( @@ -72,7 +80,7 @@ export const PTTButton: React.FC = ({ // 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) { + if (e.changedTouches.item(i).identifier === activeTouchId) { touchFound = true; break; } @@ -80,34 +88,33 @@ export const PTTButton: React.FC = ({ if (!touchFound) return; e.preventDefault(); - if (isHeld) stopTalking(); - setState({ isHeld: false, activeTouchID: null }); + if (held) stopTalking(); + unhold(); + setActiveTouchId(null); }, - [isHeld, activeTouchID, setState, stopTalking] + [held, activeTouchId, unhold, stopTalking] ); const onButtonMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); - setState({ isHeld: true, activeTouchID: null }); + hold(); startTalking(); }, - [setState, startTalking] + [hold, startTalking] ); const onButtonTouchStart = useCallback( (e: TouchEvent) => { e.preventDefault(); - if (isHeld) return; - - setState({ - isHeld: true, - activeTouchID: e.changedTouches.item(0).identifier, - }); - startTalking(); + if (!held) { + hold(); + setActiveTouchId(e.changedTouches.item(0).identifier); + startTalking(); + } }, - [isHeld, setState, startTalking] + [held, hold, startTalking] ); useEffect(() => { @@ -143,12 +150,15 @@ export const PTTButton: React.FC = ({ }); const shadowColor = showTalkOverError ? "var(--alert-20)" + : networkWaiting + ? "var(--tertiary-content-20)" : "var(--accent-20)"; return ( = ({ !feedbackModalState.isOpen ); + const [talkingExpected, enqueueTalkingExpected, setTalkingExpected] = + useDelayedState(false); const showTalkOverError = pttButtonHeld && transmitBlocked; + const networkWaiting = + talkingExpected && !activeSpeakerUserId && !showTalkOverError; const activeSpeakerIsLocalUser = activeSpeakerUserId && client.getUserId() === activeSpeakerUserId; @@ -226,9 +236,13 @@ export const PTTCallView: React.FC = ({ size={pttButtonSize} startTalking={startTalking} stopTalking={stopTalking} + networkWaiting={networkWaiting} + enqueueNetworkWaiting={enqueueTalkingExpected} + setNetworkWaiting={setTalkingExpected} />

{getPromptText( + networkWaiting, showTalkOverError, pttButtonHeld, activeSpeakerIsLocalUser, diff --git a/src/useDelayedState.ts b/src/useDelayedState.ts new file mode 100644 index 0000000..caff435 --- /dev/null +++ b/src/useDelayedState.ts @@ -0,0 +1,44 @@ +/* +Copyright 2022 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useState, useRef, useEffect } from "react"; + +export const useDelayedState = ( + initial?: T +): [T, (value: T, delay: number) => void, (value: T) => void] => { + const [state, setState] = useState(initial); + const timers = useRef>>(); + if (!timers.current) timers.current = new Set(); + + const setStateDelayed = (value: T, delay: number) => { + const timer = setTimeout(() => { + setState(value); + timers.current.delete(timer); + }, delay); + timers.current.add(timer); + }; + const setStateImmediate = (value: T) => { + // Clear all updates currently in the queue + for (const timer of timers.current) clearTimeout(timer); + timers.current.clear(); + + setState(value); + }; + + useEffect(() => console.log("got", state), [state]); + + return [state, setStateDelayed, setStateImmediate]; +}; From 2eae6243bb37b0d04764c3ce6d653c34cea1383c Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 14 Jun 2022 12:10:17 -0400 Subject: [PATCH 2/6] Add a comment --- src/room/PTTButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/room/PTTButton.tsx b/src/room/PTTButton.tsx index c52326e..aed4890 100644 --- a/src/room/PTTButton.tsx +++ b/src/room/PTTButton.tsx @@ -58,6 +58,7 @@ export const PTTButton: React.FC = ({ const hold = useCallback(() => { setHeld(true); + // This update is delayed so the user only sees it if latency is significant enqueueNetworkWaiting(true, 100); }, [setHeld, enqueueNetworkWaiting]); const unhold = useCallback(() => { From 74ccf7d820ec95badd11e6c1c25545d853399550 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 14 Jun 2022 12:13:59 -0400 Subject: [PATCH 3/6] Clean up useDelayedState --- src/useDelayedState.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/useDelayedState.ts b/src/useDelayedState.ts index caff435..a7e493f 100644 --- a/src/useDelayedState.ts +++ b/src/useDelayedState.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef } from "react"; +// Like useState, except state updates can be enqueued with a configurable delay export const useDelayedState = ( initial?: T ): [T, (value: T, delay: number) => void, (value: T) => void] => { @@ -38,7 +39,5 @@ export const useDelayedState = ( setState(value); }; - useEffect(() => console.log("got", state), [state]); - return [state, setStateDelayed, setStateImmediate]; }; From 1d4ed6609d4cdeda34d03bb881d635b35b73d45a Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 14 Jun 2022 14:15:52 -0400 Subject: [PATCH 4/6] Preload PTT sounds correctly --- src/sound/PTTClips.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sound/PTTClips.tsx b/src/sound/PTTClips.tsx index 90958af..9a72098 100644 --- a/src/sound/PTTClips.tsx +++ b/src/sound/PTTClips.tsx @@ -42,7 +42,7 @@ export const PTTClips: React.FC = ({ return ( <> -

)} { // 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. @@ -258,59 +257,6 @@ export const usePTT = ( [setConnected] ); - useEffect(() => { - function onKeyDown(event: KeyboardEvent): void { - if (event.code === "Space") { - if (!enablePTTButton) return; - - event.preventDefault(); - - if (pttButtonHeld) return; - - startTalking(); - } - } - - function onKeyUp(event: KeyboardEvent): void { - if (event.code === "Space") { - event.preventDefault(); - - stopTalking(); - } - } - - function onBlur(): void { - // TODO: We will need to disable this for a global PTT hotkey to work - if (!groupCall.isMicrophoneMuted()) { - setMicMuteWrapper(true); - } - - setState((prevState) => ({ ...prevState, pttButtonHeld: false })); - } - - window.addEventListener("keydown", onKeyDown); - window.addEventListener("keyup", onKeyUp); - window.addEventListener("blur", onBlur); - - return () => { - window.removeEventListener("keydown", onKeyDown); - window.removeEventListener("keyup", onKeyUp); - window.removeEventListener("blur", onBlur); - }; - }, [ - groupCall, - startTalking, - stopTalking, - activeSpeakerUserId, - isAdmin, - talkOverEnabled, - pttButtonHeld, - enablePTTButton, - setMicMuteWrapper, - client, - onClientSync, - ]); - useEffect(() => { client.on(ClientEvent.Sync, onClientSync); diff --git a/src/useEvents.ts b/src/useEvents.ts new file mode 100644 index 0000000..b08f9ff --- /dev/null +++ b/src/useEvents.ts @@ -0,0 +1,34 @@ +/* +Copyright 2022 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useEffect } from "react"; + +// Shortcut for registering a listener on an EventTarget +export const useEventTarget = ( + target: EventTarget, + eventType: string, + listener: (event: T) => void, + options?: AddEventListenerOptions +) => { + useEffect(() => { + if (target) { + target.addEventListener(eventType, listener, options); + return () => target.removeEventListener(eventType, listener, options); + } + }, [target, eventType, listener, options]); +}; + +// TODO: Have a similar hook for EventEmitters From 22dcb883b3f42044f6ce7365ebb0a58c6cd47deb Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 14 Jun 2022 23:38:40 -0400 Subject: [PATCH 6/6] Fix waiting state not disappearing after the 20 second timeout --- src/room/PTTCallView.tsx | 9 ++++++--- src/useDelayedState.ts | 34 ++++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index e065c14..2dbc07e 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useEffect } from "react"; import useMeasure from "react-use-measure"; import { ResizeObserver } from "@juggle/resize-observer"; import { GroupCall, MatrixClient, RoomMember } from "matrix-js-sdk"; @@ -142,8 +142,7 @@ export const PTTCallView: React.FC = ({ const networkWaiting = talkingExpected && !activeSpeakerUserId && !showTalkOverError; - const activeSpeakerIsLocalUser = - activeSpeakerUserId && client.getUserId() === activeSpeakerUserId; + const activeSpeakerIsLocalUser = activeSpeakerUserId === client.getUserId(); const activeSpeakerUser = activeSpeakerUserId ? client.getUser(activeSpeakerUserId) : null; @@ -152,6 +151,10 @@ export const PTTCallView: React.FC = ({ ? activeSpeakerUser.displayName : ""; + useEffect(() => { + setTalkingExpected(activeSpeakerIsLocalUser); + }, [activeSpeakerIsLocalUser, setTalkingExpected]); + return (
( @@ -24,20 +24,26 @@ export const useDelayedState = ( const timers = useRef>>(); if (!timers.current) timers.current = new Set(); - const setStateDelayed = (value: T, delay: number) => { - const timer = setTimeout(() => { - setState(value); - timers.current.delete(timer); - }, delay); - timers.current.add(timer); - }; - const setStateImmediate = (value: T) => { - // Clear all updates currently in the queue - for (const timer of timers.current) clearTimeout(timer); - timers.current.clear(); + const setStateDelayed = useCallback( + (value: T, delay: number) => { + const timer = setTimeout(() => { + setState(value); + timers.current.delete(timer); + }, delay); + timers.current.add(timer); + }, + [setState, timers] + ); + const setStateImmediate = useCallback( + (value: T) => { + // Clear all updates currently in the queue + for (const timer of timers.current) clearTimeout(timer); + timers.current.clear(); - setState(value); - }; + setState(value); + }, + [setState, timers] + ); return [state, setStateDelayed, setStateImmediate]; };