2022-05-04 16:09:48 +00:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2022-07-05 10:06:32 +00:00
|
|
|
import React, { useCallback, useState, useRef } from "react";
|
2022-04-29 00:44:50 +00:00
|
|
|
import classNames from "classnames";
|
2022-05-31 22:01:34 +00:00
|
|
|
import { useSpring, animated } from "@react-spring/web";
|
2022-05-06 10:32:09 +00:00
|
|
|
|
2022-04-23 01:05:48 +00:00
|
|
|
import styles from "./PTTButton.module.css";
|
|
|
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
2022-06-14 20:53:56 +00:00
|
|
|
import { useEventTarget } from "../useEvents";
|
2022-04-23 01:05:48 +00:00
|
|
|
import { Avatar } from "../Avatar";
|
|
|
|
|
2022-05-06 10:32:09 +00:00
|
|
|
interface Props {
|
2022-06-14 20:53:56 +00:00
|
|
|
enabled: boolean;
|
2022-05-06 10:32:09 +00:00
|
|
|
showTalkOverError: boolean;
|
|
|
|
activeSpeakerUserId: string;
|
|
|
|
activeSpeakerDisplayName: string;
|
|
|
|
activeSpeakerAvatarUrl: string;
|
|
|
|
activeSpeakerIsLocalUser: boolean;
|
2022-05-31 22:01:34 +00:00
|
|
|
activeSpeakerVolume: number;
|
2022-05-06 10:32:09 +00:00
|
|
|
size: number;
|
|
|
|
startTalking: () => void;
|
|
|
|
stopTalking: () => void;
|
2022-06-14 16:00:26 +00:00
|
|
|
networkWaiting: boolean;
|
|
|
|
enqueueNetworkWaiting: (value: boolean, delay: number) => void;
|
|
|
|
setNetworkWaiting: (value: boolean) => void;
|
2022-05-13 17:28:48 +00:00
|
|
|
}
|
|
|
|
|
2022-05-06 10:32:09 +00:00
|
|
|
export const PTTButton: React.FC<Props> = ({
|
2022-06-14 20:53:56 +00:00
|
|
|
enabled,
|
2022-04-29 00:44:50 +00:00
|
|
|
showTalkOverError,
|
|
|
|
activeSpeakerUserId,
|
|
|
|
activeSpeakerDisplayName,
|
|
|
|
activeSpeakerAvatarUrl,
|
|
|
|
activeSpeakerIsLocalUser,
|
2022-05-31 22:01:34 +00:00
|
|
|
activeSpeakerVolume,
|
2022-04-29 00:44:50 +00:00
|
|
|
size,
|
2022-04-29 17:56:17 +00:00
|
|
|
startTalking,
|
|
|
|
stopTalking,
|
2022-06-14 16:00:26 +00:00
|
|
|
networkWaiting,
|
|
|
|
enqueueNetworkWaiting,
|
|
|
|
setNetworkWaiting,
|
2022-05-06 10:32:09 +00:00
|
|
|
}) => {
|
2022-07-05 10:06:32 +00:00
|
|
|
const buttonRef = useRef<HTMLButtonElement>();
|
2022-05-13 17:28:48 +00:00
|
|
|
|
2022-06-14 16:00:26 +00:00
|
|
|
const [activeTouchId, setActiveTouchId] = useState<number | null>(null);
|
2022-07-08 14:52:32 +00:00
|
|
|
const [buttonHeld, setButtonHeld] = useState(false);
|
2022-06-14 16:00:26 +00:00
|
|
|
|
|
|
|
const hold = useCallback(() => {
|
2022-06-14 16:10:17 +00:00
|
|
|
// This update is delayed so the user only sees it if latency is significant
|
2022-07-08 14:52:32 +00:00
|
|
|
if (buttonHeld) return;
|
|
|
|
setButtonHeld(true);
|
2022-06-14 16:00:26 +00:00
|
|
|
enqueueNetworkWaiting(true, 100);
|
2022-06-14 20:53:56 +00:00
|
|
|
startTalking();
|
2022-07-08 14:52:32 +00:00
|
|
|
}, [enqueueNetworkWaiting, startTalking, buttonHeld]);
|
2022-06-14 16:00:26 +00:00
|
|
|
const unhold = useCallback(() => {
|
2022-07-08 14:52:32 +00:00
|
|
|
setButtonHeld(false);
|
2022-06-14 16:00:26 +00:00
|
|
|
setNetworkWaiting(false);
|
2022-06-14 20:53:56 +00:00
|
|
|
stopTalking();
|
|
|
|
}, [setNetworkWaiting, stopTalking]);
|
2022-05-04 15:52:45 +00:00
|
|
|
|
2022-05-12 11:07:04 +00:00
|
|
|
const onButtonMouseDown = useCallback(
|
|
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
|
|
e.preventDefault();
|
2022-06-14 16:00:26 +00:00
|
|
|
hold();
|
2022-05-12 11:07:04 +00:00
|
|
|
},
|
2022-06-14 20:53:56 +00:00
|
|
|
[hold]
|
2022-05-12 11:07:04 +00:00
|
|
|
);
|
|
|
|
|
2022-06-14 20:53:56 +00:00
|
|
|
// 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();
|
2022-05-13 17:28:48 +00:00
|
|
|
|
2022-06-14 16:00:26 +00:00
|
|
|
hold();
|
|
|
|
setActiveTouchId(e.changedTouches.item(0).identifier);
|
2022-06-14 20:53:56 +00:00
|
|
|
},
|
|
|
|
[hold, setActiveTouchId]
|
|
|
|
),
|
|
|
|
{ passive: false }
|
|
|
|
);
|
|
|
|
|
|
|
|
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]
|
|
|
|
)
|
2022-05-12 11:07:04 +00:00
|
|
|
);
|
2022-05-04 15:52:45 +00:00
|
|
|
|
2022-06-14 20:53:56 +00:00
|
|
|
// TODO: We will need to disable this for a global PTT hotkey to work
|
|
|
|
useEventTarget(window, "blur", unhold);
|
2022-05-31 22:01:34 +00:00
|
|
|
|
|
|
|
const { shadow } = useSpring({
|
2022-06-01 14:41:49 +00:00
|
|
|
shadow: (Math.max(activeSpeakerVolume, -70) + 70) * 0.6,
|
2022-05-31 22:01:34 +00:00
|
|
|
config: {
|
|
|
|
clamp: true,
|
|
|
|
tension: 300,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const shadowColor = showTalkOverError
|
2022-06-01 14:41:12 +00:00
|
|
|
? "var(--alert-20)"
|
2022-06-14 16:00:26 +00:00
|
|
|
: networkWaiting
|
|
|
|
? "var(--tertiary-content-20)"
|
2022-06-01 15:48:17 +00:00
|
|
|
: "var(--accent-20)";
|
2022-05-31 22:01:34 +00:00
|
|
|
|
2022-04-23 01:05:48 +00:00
|
|
|
return (
|
2022-05-31 22:01:34 +00:00
|
|
|
<animated.button
|
2022-04-23 01:05:48 +00:00
|
|
|
className={classNames(styles.pttButton, {
|
2022-04-29 00:44:50 +00:00
|
|
|
[styles.talking]: activeSpeakerUserId,
|
2022-06-14 16:00:26 +00:00
|
|
|
[styles.networkWaiting]: networkWaiting,
|
2022-04-29 00:44:50 +00:00
|
|
|
[styles.error]: showTalkOverError,
|
2022-04-23 01:05:48 +00:00
|
|
|
})}
|
2022-05-31 22:01:34 +00:00
|
|
|
style={{
|
|
|
|
boxShadow: shadow.to(
|
|
|
|
(s) =>
|
|
|
|
`0px 0px 0px ${s}px ${shadowColor}, 0px 0px 0px ${
|
|
|
|
2 * s
|
|
|
|
}px ${shadowColor}`
|
|
|
|
),
|
|
|
|
}}
|
2022-05-04 15:52:45 +00:00
|
|
|
onMouseDown={onButtonMouseDown}
|
2022-05-13 17:28:48 +00:00
|
|
|
ref={buttonRef}
|
2022-04-23 01:05:48 +00:00
|
|
|
>
|
2022-04-29 00:44:50 +00:00
|
|
|
{activeSpeakerIsLocalUser || !activeSpeakerUserId ? (
|
2022-04-23 01:05:48 +00:00
|
|
|
<MicIcon
|
2022-04-29 00:44:50 +00:00
|
|
|
className={styles.micIcon}
|
2022-04-23 01:05:48 +00:00
|
|
|
width={size / 3}
|
|
|
|
height={size / 3}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<Avatar
|
2022-04-29 00:44:50 +00:00
|
|
|
key={activeSpeakerUserId}
|
2022-05-18 23:00:59 +00:00
|
|
|
size={size - 12}
|
2022-04-29 00:44:50 +00:00
|
|
|
src={activeSpeakerAvatarUrl}
|
|
|
|
fallback={activeSpeakerDisplayName.slice(0, 1).toUpperCase()}
|
2022-04-23 01:05:48 +00:00
|
|
|
className={styles.avatar}
|
|
|
|
/>
|
|
|
|
)}
|
2022-05-31 22:01:34 +00:00
|
|
|
</animated.button>
|
2022-04-23 01:05:48 +00:00
|
|
|
);
|
2022-05-06 10:32:09 +00:00
|
|
|
};
|