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..440b548 100644 --- a/src/room/PTTButton.tsx +++ b/src/room/PTTButton.tsx @@ -14,15 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useEffect, useState, createRef } from "react"; +import React, { useCallback, useState, createRef } from "react"; import classNames from "classnames"; import { useSpring, animated } from "@react-spring/web"; import styles from "./PTTButton.module.css"; import { ReactComponent as MicIcon } from "../icons/Mic.svg"; +import { useEventTarget } from "../useEvents"; import { Avatar } from "../Avatar"; interface Props { + enabled: boolean; showTalkOverError: boolean; activeSpeakerUserId: string; activeSpeakerDisplayName: string; @@ -32,15 +34,13 @@ 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 = ({ + enabled, showTalkOverError, activeSpeakerUserId, activeSpeakerDisplayName, @@ -50,89 +50,110 @@ export const PTTButton: React.FC = ({ size, startTalking, stopTalking, + networkWaiting, + enqueueNetworkWaiting, + setNetworkWaiting, }) => { const buttonRef = createRef(); - const [{ isHeld, activeTouchID }, setState] = useState({ - isHeld: false, - activeTouchID: null, - }); - const onWindowMouseUp = useCallback( - (e) => { - if (isHeld) stopTalking(); - setState({ isHeld: false, activeTouchID: null }); - }, - [isHeld, setState, stopTalking] - ); + const [activeTouchId, setActiveTouchId] = useState(null); - 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 hold = useCallback(() => { + // This update is delayed so the user only sees it if latency is significant + enqueueNetworkWaiting(true, 100); + startTalking(); + }, [enqueueNetworkWaiting, startTalking]); + const unhold = useCallback(() => { + setNetworkWaiting(false); + stopTalking(); + }, [setNetworkWaiting, stopTalking]); const onButtonMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); - setState({ isHeld: true, activeTouchID: null }); - startTalking(); + hold(); }, - [setState, startTalking] + [hold] ); - const onButtonTouchStart = useCallback( - (e: TouchEvent) => { - e.preventDefault(); + // 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. + useEventTarget(window, "mouseup", unhold); + useEventTarget( + window, + "touchend", + 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; - if (isHeld) return; - - setState({ - isHeld: true, - activeTouchID: e.changedTouches.item(0).identifier, - }); - startTalking(); - }, - [isHeld, setState, startTalking] + e.preventDefault(); + unhold(); + setActiveTouchId(null); + }, + [unhold, activeTouchId, setActiveTouchId] + ) ); - useEffect(() => { - const currentButtonElement = buttonRef.current; + // 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. + useEventTarget( + buttonRef.current, + "touchstart", + useCallback( + (e: TouchEvent) => { + e.preventDefault(); - // 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, - }); + hold(); + setActiveTouchId(e.changedTouches.item(0).identifier); + }, + [hold, setActiveTouchId] + ), + { passive: false } + ); - return () => { - window.removeEventListener("mouseup", onWindowMouseUp); - window.removeEventListener("touchend", onWindowTouchEnd); - currentButtonElement.removeEventListener( - "touchstart", - onButtonTouchStart - ); - }; - }, [onWindowMouseUp, onWindowTouchEnd, onButtonTouchStart, buttonRef]); + useEventTarget( + window, + "keydown", + useCallback( + (e: KeyboardEvent) => { + if (e.code === "Space") { + if (!enabled) return; + e.preventDefault(); + + hold(); + } + }, + [enabled, hold] + ) + ); + useEventTarget( + window, + "keyup", + useCallback( + (e: KeyboardEvent) => { + if (e.code === "Space") { + e.preventDefault(); + + unhold(); + } + }, + [unhold] + ) + ); + + // TODO: We will need to disable this for a global PTT hotkey to work + useEventTarget(window, "blur", unhold); const { shadow } = useSpring({ shadow: (Math.max(activeSpeakerVolume, -70) + 70) * 0.6, @@ -143,12 +164,15 @@ export const PTTButton: React.FC = ({ }); const shadowColor = showTalkOverError ? "var(--alert-20)" + : networkWaiting + ? "var(--tertiary-content-20)" : "var(--accent-20)"; return ( = ({ stopTalking, transmitBlocked, connected, - } = usePTT( - client, - groupCall, - userMediaFeeds, - playClip, - !feedbackModalState.isOpen - ); + } = usePTT(client, groupCall, userMediaFeeds, playClip); + const [talkingExpected, enqueueTalkingExpected, setTalkingExpected] = + useDelayedState(false); const showTalkOverError = pttButtonHeld && transmitBlocked; + const networkWaiting = + talkingExpected && !activeSpeakerUserId && !showTalkOverError; - const activeSpeakerIsLocalUser = - activeSpeakerUserId && client.getUserId() === activeSpeakerUserId; + const activeSpeakerIsLocalUser = activeSpeakerUserId === client.getUserId(); const activeSpeakerUser = activeSpeakerUserId ? client.getUser(activeSpeakerUserId) : null; @@ -148,6 +151,10 @@ export const PTTCallView: React.FC = ({ ? activeSpeakerUser.displayName : ""; + useEffect(() => { + setTalkingExpected(activeSpeakerIsLocalUser); + }, [activeSpeakerIsLocalUser, setTalkingExpected]); + return (
= ({
)} = ({ size={pttButtonSize} startTalking={startTalking} stopTalking={stopTalking} + networkWaiting={networkWaiting} + enqueueNetworkWaiting={enqueueTalkingExpected} + setNetworkWaiting={setTalkingExpected} />

{getPromptText( + networkWaiting, showTalkOverError, pttButtonHeld, activeSpeakerIsLocalUser, diff --git a/src/room/usePTT.ts b/src/room/usePTT.ts index 7710655..096e662 100644 --- a/src/room/usePTT.ts +++ b/src/room/usePTT.ts @@ -80,8 +80,7 @@ export const usePTT = ( client: MatrixClient, groupCall: GroupCall, userMediaFeeds: CallFeed[], - playClip: PlayClipFunction, - enablePTTButton: boolean + 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. @@ -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/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 ( <> -