Add a 'waiting for network' state to walkie-talkie mode
This commit is contained in:
		
					parent
					
						
							
								939398b277
							
						
					
				
			
			
				commit
				
					
						276532e2e1
					
				
			
		
					 7 changed files with 108 additions and 28 deletions
				
			
		
							
								
								
									
										1
									
								
								.env
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								.env
									
										
									
									
									
								
							| 
						 | 
					@ -22,6 +22,7 @@
 | 
				
			||||||
# VITE_THEME_PRIMARY_CONTENT=#ffffff
 | 
					# VITE_THEME_PRIMARY_CONTENT=#ffffff
 | 
				
			||||||
# VITE_THEME_SECONDARY_CONTENT=#a9b2bc
 | 
					# VITE_THEME_SECONDARY_CONTENT=#a9b2bc
 | 
				
			||||||
# VITE_THEME_TERTIARY_CONTENT=#8e99a4
 | 
					# VITE_THEME_TERTIARY_CONTENT=#8e99a4
 | 
				
			||||||
 | 
					# VITE_THEME_TERTIARY_CONTENT_20=#8e99a433
 | 
				
			||||||
# VITE_THEME_QUATERNARY_CONTENT=#6f7882
 | 
					# VITE_THEME_QUATERNARY_CONTENT=#6f7882
 | 
				
			||||||
# VITE_THEME_QUINARY_CONTENT=#394049
 | 
					# VITE_THEME_QUINARY_CONTENT=#394049
 | 
				
			||||||
# VITE_THEME_SYSTEM=#21262c
 | 
					# VITE_THEME_SYSTEM=#21262c
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,7 @@ limitations under the License.
 | 
				
			||||||
  --primary-content: #ffffff;
 | 
					  --primary-content: #ffffff;
 | 
				
			||||||
  --secondary-content: #a9b2bc;
 | 
					  --secondary-content: #a9b2bc;
 | 
				
			||||||
  --tertiary-content: #8e99a4;
 | 
					  --tertiary-content: #8e99a4;
 | 
				
			||||||
 | 
					  --tertiary-content-20: #8e99a433;
 | 
				
			||||||
  --quaternary-content: #6f7882;
 | 
					  --quaternary-content: #6f7882;
 | 
				
			||||||
  --quinary-content: #394049;
 | 
					  --quinary-content: #394049;
 | 
				
			||||||
  --system: #21262c;
 | 
					  --system: #21262c;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,6 +61,10 @@ if (import.meta.env.VITE_CUSTOM_THEME) {
 | 
				
			||||||
    "--tertiary-content",
 | 
					    "--tertiary-content",
 | 
				
			||||||
    import.meta.env.VITE_THEME_TERTIARY_CONTENT as string
 | 
					    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(
 | 
					  style.setProperty(
 | 
				
			||||||
    "--quaternary-content",
 | 
					    "--quaternary-content",
 | 
				
			||||||
    import.meta.env.VITE_THEME_QUATERNARY_CONTENT as string
 | 
					    import.meta.env.VITE_THEME_QUATERNARY_CONTENT as string
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,12 @@
 | 
				
			||||||
  cursor: unset;
 | 
					  cursor: unset;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.networkWaiting {
 | 
				
			||||||
 | 
					  background-color: var(--tertiary-content);
 | 
				
			||||||
 | 
					  border-color: var(--tertiary-content);
 | 
				
			||||||
 | 
					  cursor: unset;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.error {
 | 
					.error {
 | 
				
			||||||
  background-color: var(--alert);
 | 
					  background-color: var(--alert);
 | 
				
			||||||
  border-color: var(--alert);
 | 
					  border-color: var(--alert);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,12 +32,9 @@ interface Props {
 | 
				
			||||||
  size: number;
 | 
					  size: number;
 | 
				
			||||||
  startTalking: () => void;
 | 
					  startTalking: () => void;
 | 
				
			||||||
  stopTalking: () => void;
 | 
					  stopTalking: () => void;
 | 
				
			||||||
}
 | 
					  networkWaiting: boolean;
 | 
				
			||||||
 | 
					  enqueueNetworkWaiting: (value: boolean, delay: number) => void;
 | 
				
			||||||
interface State {
 | 
					  setNetworkWaiting: (value: boolean) => void;
 | 
				
			||||||
  isHeld: boolean;
 | 
					 | 
				
			||||||
  // If the button is being pressed by touch, the ID of that touch
 | 
					 | 
				
			||||||
  activeTouchID: number | null;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const PTTButton: React.FC<Props> = ({
 | 
					export const PTTButton: React.FC<Props> = ({
 | 
				
			||||||
| 
						 | 
					@ -50,19 +47,30 @@ export const PTTButton: React.FC<Props> = ({
 | 
				
			||||||
  size,
 | 
					  size,
 | 
				
			||||||
  startTalking,
 | 
					  startTalking,
 | 
				
			||||||
  stopTalking,
 | 
					  stopTalking,
 | 
				
			||||||
 | 
					  networkWaiting,
 | 
				
			||||||
 | 
					  enqueueNetworkWaiting,
 | 
				
			||||||
 | 
					  setNetworkWaiting,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const buttonRef = createRef<HTMLButtonElement>();
 | 
					  const buttonRef = createRef<HTMLButtonElement>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [{ isHeld, activeTouchID }, setState] = useState<State>({
 | 
					  const [held, setHeld] = useState(false);
 | 
				
			||||||
    isHeld: false,
 | 
					  const [activeTouchId, setActiveTouchId] = useState<number | null>(null);
 | 
				
			||||||
    activeTouchID: null,
 | 
					
 | 
				
			||||||
  });
 | 
					  const hold = useCallback(() => {
 | 
				
			||||||
 | 
					    setHeld(true);
 | 
				
			||||||
 | 
					    enqueueNetworkWaiting(true, 100);
 | 
				
			||||||
 | 
					  }, [setHeld, enqueueNetworkWaiting]);
 | 
				
			||||||
 | 
					  const unhold = useCallback(() => {
 | 
				
			||||||
 | 
					    setHeld(false);
 | 
				
			||||||
 | 
					    setNetworkWaiting(false);
 | 
				
			||||||
 | 
					  }, [setHeld, setNetworkWaiting]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onWindowMouseUp = useCallback(
 | 
					  const onWindowMouseUp = useCallback(
 | 
				
			||||||
    (e) => {
 | 
					    (e) => {
 | 
				
			||||||
      if (isHeld) stopTalking();
 | 
					      if (held) stopTalking();
 | 
				
			||||||
      setState({ isHeld: false, activeTouchID: null });
 | 
					      unhold();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [isHeld, setState, stopTalking]
 | 
					    [held, unhold, stopTalking]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onWindowTouchEnd = useCallback(
 | 
					  const onWindowTouchEnd = useCallback(
 | 
				
			||||||
| 
						 | 
					@ -72,7 +80,7 @@ export const PTTButton: React.FC<Props> = ({
 | 
				
			||||||
      // have to do this a really old-school way).
 | 
					      // have to do this a really old-school way).
 | 
				
			||||||
      let touchFound = false;
 | 
					      let touchFound = false;
 | 
				
			||||||
      for (let i = 0; i < e.changedTouches.length; ++i) {
 | 
					      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;
 | 
					          touchFound = true;
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -80,34 +88,33 @@ export const PTTButton: React.FC<Props> = ({
 | 
				
			||||||
      if (!touchFound) return;
 | 
					      if (!touchFound) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      if (isHeld) stopTalking();
 | 
					      if (held) stopTalking();
 | 
				
			||||||
      setState({ isHeld: false, activeTouchID: null });
 | 
					      unhold();
 | 
				
			||||||
 | 
					      setActiveTouchId(null);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [isHeld, activeTouchID, setState, stopTalking]
 | 
					    [held, activeTouchId, unhold, stopTalking]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onButtonMouseDown = useCallback(
 | 
					  const onButtonMouseDown = useCallback(
 | 
				
			||||||
    (e: React.MouseEvent<HTMLButtonElement>) => {
 | 
					    (e: React.MouseEvent<HTMLButtonElement>) => {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      setState({ isHeld: true, activeTouchID: null });
 | 
					      hold();
 | 
				
			||||||
      startTalking();
 | 
					      startTalking();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [setState, startTalking]
 | 
					    [hold, startTalking]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const onButtonTouchStart = useCallback(
 | 
					  const onButtonTouchStart = useCallback(
 | 
				
			||||||
    (e: TouchEvent) => {
 | 
					    (e: TouchEvent) => {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isHeld) return;
 | 
					      if (!held) {
 | 
				
			||||||
 | 
					        hold();
 | 
				
			||||||
      setState({
 | 
					        setActiveTouchId(e.changedTouches.item(0).identifier);
 | 
				
			||||||
        isHeld: true,
 | 
					        startTalking();
 | 
				
			||||||
        activeTouchID: e.changedTouches.item(0).identifier,
 | 
					      }
 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      startTalking();
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [isHeld, setState, startTalking]
 | 
					    [held, hold, startTalking]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
| 
						 | 
					@ -143,12 +150,15 @@ export const PTTButton: React.FC<Props> = ({
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const shadowColor = showTalkOverError
 | 
					  const shadowColor = showTalkOverError
 | 
				
			||||||
    ? "var(--alert-20)"
 | 
					    ? "var(--alert-20)"
 | 
				
			||||||
 | 
					    : networkWaiting
 | 
				
			||||||
 | 
					    ? "var(--tertiary-content-20)"
 | 
				
			||||||
    : "var(--accent-20)";
 | 
					    : "var(--accent-20)";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <animated.button
 | 
					    <animated.button
 | 
				
			||||||
      className={classNames(styles.pttButton, {
 | 
					      className={classNames(styles.pttButton, {
 | 
				
			||||||
        [styles.talking]: activeSpeakerUserId,
 | 
					        [styles.talking]: activeSpeakerUserId,
 | 
				
			||||||
 | 
					        [styles.networkWaiting]: networkWaiting,
 | 
				
			||||||
        [styles.error]: showTalkOverError,
 | 
					        [styles.error]: showTalkOverError,
 | 
				
			||||||
      })}
 | 
					      })}
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@ import { ResizeObserver } from "@juggle/resize-observer";
 | 
				
			||||||
import { GroupCall, MatrixClient, RoomMember } from "matrix-js-sdk";
 | 
					import { GroupCall, MatrixClient, RoomMember } from "matrix-js-sdk";
 | 
				
			||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
					import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useDelayedState } from "../useDelayedState";
 | 
				
			||||||
import { useModalTriggerState } from "../Modal";
 | 
					import { useModalTriggerState } from "../Modal";
 | 
				
			||||||
import { InviteModal } from "./InviteModal";
 | 
					import { InviteModal } from "./InviteModal";
 | 
				
			||||||
import { HangupButton, InviteButton } from "../button";
 | 
					import { HangupButton, InviteButton } from "../button";
 | 
				
			||||||
| 
						 | 
					@ -39,6 +40,7 @@ import { GroupCallInspector } from "./GroupCallInspector";
 | 
				
			||||||
import { OverflowMenu } from "./OverflowMenu";
 | 
					import { OverflowMenu } from "./OverflowMenu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getPromptText(
 | 
					function getPromptText(
 | 
				
			||||||
 | 
					  networkWaiting: boolean,
 | 
				
			||||||
  showTalkOverError: boolean,
 | 
					  showTalkOverError: boolean,
 | 
				
			||||||
  pttButtonHeld: boolean,
 | 
					  pttButtonHeld: boolean,
 | 
				
			||||||
  activeSpeakerIsLocalUser: boolean,
 | 
					  activeSpeakerIsLocalUser: boolean,
 | 
				
			||||||
| 
						 | 
					@ -47,10 +49,14 @@ function getPromptText(
 | 
				
			||||||
  activeSpeakerDisplayName: string,
 | 
					  activeSpeakerDisplayName: string,
 | 
				
			||||||
  connected: boolean
 | 
					  connected: boolean
 | 
				
			||||||
): string {
 | 
					): string {
 | 
				
			||||||
  if (!connected) return "Connection Lost";
 | 
					  if (!connected) return "Connection lost";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const isTouchScreen = Boolean(window.ontouchstart !== undefined);
 | 
					  const isTouchScreen = Boolean(window.ontouchstart !== undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (networkWaiting) {
 | 
				
			||||||
 | 
					    return "Waiting for network";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (showTalkOverError) {
 | 
					  if (showTalkOverError) {
 | 
				
			||||||
    return "You can't talk at the same time";
 | 
					    return "You can't talk at the same time";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -136,7 +142,11 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
    !feedbackModalState.isOpen
 | 
					    !feedbackModalState.isOpen
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [talkingExpected, enqueueTalkingExpected, setTalkingExpected] =
 | 
				
			||||||
 | 
					    useDelayedState(false);
 | 
				
			||||||
  const showTalkOverError = pttButtonHeld && transmitBlocked;
 | 
					  const showTalkOverError = pttButtonHeld && transmitBlocked;
 | 
				
			||||||
 | 
					  const networkWaiting =
 | 
				
			||||||
 | 
					    talkingExpected && !activeSpeakerUserId && !showTalkOverError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const activeSpeakerIsLocalUser =
 | 
					  const activeSpeakerIsLocalUser =
 | 
				
			||||||
    activeSpeakerUserId && client.getUserId() === activeSpeakerUserId;
 | 
					    activeSpeakerUserId && client.getUserId() === activeSpeakerUserId;
 | 
				
			||||||
| 
						 | 
					@ -226,9 +236,13 @@ export const PTTCallView: React.FC<Props> = ({
 | 
				
			||||||
            size={pttButtonSize}
 | 
					            size={pttButtonSize}
 | 
				
			||||||
            startTalking={startTalking}
 | 
					            startTalking={startTalking}
 | 
				
			||||||
            stopTalking={stopTalking}
 | 
					            stopTalking={stopTalking}
 | 
				
			||||||
 | 
					            networkWaiting={networkWaiting}
 | 
				
			||||||
 | 
					            enqueueNetworkWaiting={enqueueTalkingExpected}
 | 
				
			||||||
 | 
					            setNetworkWaiting={setTalkingExpected}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <p className={styles.actionTip}>
 | 
					          <p className={styles.actionTip}>
 | 
				
			||||||
            {getPromptText(
 | 
					            {getPromptText(
 | 
				
			||||||
 | 
					              networkWaiting,
 | 
				
			||||||
              showTalkOverError,
 | 
					              showTalkOverError,
 | 
				
			||||||
              pttButtonHeld,
 | 
					              pttButtonHeld,
 | 
				
			||||||
              activeSpeakerIsLocalUser,
 | 
					              activeSpeakerIsLocalUser,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										44
									
								
								src/useDelayedState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/useDelayedState.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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 = <T>(
 | 
				
			||||||
 | 
					  initial?: T
 | 
				
			||||||
 | 
					): [T, (value: T, delay: number) => void, (value: T) => void] => {
 | 
				
			||||||
 | 
					  const [state, setState] = useState<T>(initial);
 | 
				
			||||||
 | 
					  const timers = useRef<Set<ReturnType<typeof setTimeout>>>();
 | 
				
			||||||
 | 
					  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];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue