From 2e945780dec63d1b19e65559f87bcdb1ba8ac3cf Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 14 Jun 2022 16:53:56 -0400 Subject: [PATCH] Make the 'waiting for network' state work with spacebar --- src/room/PTTButton.tsx | 147 +++++++++++++++++++++------------------ src/room/PTTCallView.tsx | 9 +-- src/room/usePTT.ts | 56 +-------------- src/useEvents.ts | 34 +++++++++ 4 files changed, 117 insertions(+), 129 deletions(-) create mode 100644 src/useEvents.ts diff --git a/src/room/PTTButton.tsx b/src/room/PTTButton.tsx index aed48908..440b548d 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; @@ -38,6 +40,7 @@ interface Props { } export const PTTButton: React.FC = ({ + enabled, showTalkOverError, activeSpeakerUserId, activeSpeakerDisplayName, @@ -53,94 +56,104 @@ export const PTTButton: React.FC = ({ }) => { const buttonRef = createRef(); - const [held, setHeld] = useState(false); const [activeTouchId, setActiveTouchId] = useState(null); const hold = useCallback(() => { - setHeld(true); // This update is delayed so the user only sees it if latency is significant enqueueNetworkWaiting(true, 100); - }, [setHeld, enqueueNetworkWaiting]); + startTalking(); + }, [enqueueNetworkWaiting, startTalking]); const unhold = useCallback(() => { - setHeld(false); setNetworkWaiting(false); - }, [setHeld, setNetworkWaiting]); - - const onWindowMouseUp = useCallback( - (e) => { - if (held) stopTalking(); - unhold(); - }, - [held, unhold, 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 (held) stopTalking(); - unhold(); - setActiveTouchId(null); - }, - [held, activeTouchId, unhold, stopTalking] - ); + stopTalking(); + }, [setNetworkWaiting, stopTalking]); const onButtonMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); hold(); - startTalking(); }, - [hold, 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; + + e.preventDefault(); + unhold(); + setActiveTouchId(null); + }, + [unhold, activeTouchId, setActiveTouchId] + ) + ); + + // 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(); - if (!held) { hold(); setActiveTouchId(e.changedTouches.item(0).identifier); - startTalking(); - } - }, - [held, hold, startTalking] + }, + [hold, setActiveTouchId] + ), + { passive: false } ); - useEffect(() => { - const currentButtonElement = buttonRef.current; + useEventTarget( + window, + "keydown", + useCallback( + (e: KeyboardEvent) => { + if (e.code === "Space") { + if (!enabled) return; + 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(); + } + }, + [enabled, hold] + ) + ); + useEventTarget( + window, + "keyup", + useCallback( + (e: KeyboardEvent) => { + if (e.code === "Space") { + e.preventDefault(); - return () => { - window.removeEventListener("mouseup", onWindowMouseUp); - window.removeEventListener("touchend", onWindowTouchEnd); - currentButtonElement.removeEventListener( - "touchstart", - onButtonTouchStart - ); - }; - }, [onWindowMouseUp, onWindowTouchEnd, onButtonTouchStart, buttonRef]); + 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, diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index 652136e6..e065c143 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -134,13 +134,7 @@ export const PTTCallView: React.FC = ({ stopTalking, transmitBlocked, connected, - } = usePTT( - client, - groupCall, - userMediaFeeds, - playClip, - !feedbackModalState.isOpen - ); + } = usePTT(client, groupCall, userMediaFeeds, playClip); const [talkingExpected, enqueueTalkingExpected, setTalkingExpected] = useDelayedState(false); @@ -227,6 +221,7 @@ export const PTTCallView: React.FC = ({
)} { // 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 00000000..b08f9ff4 --- /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