182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
/*
|
|
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.
|
|
*/
|
|
|
|
import React, { useCallback, useEffect, 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 { Avatar } from "../Avatar";
|
|
|
|
interface Props {
|
|
showTalkOverError: boolean;
|
|
activeSpeakerUserId: string;
|
|
activeSpeakerDisplayName: string;
|
|
activeSpeakerAvatarUrl: string;
|
|
activeSpeakerIsLocalUser: boolean;
|
|
activeSpeakerVolume: number;
|
|
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;
|
|
}
|
|
|
|
export const PTTButton: React.FC<Props> = ({
|
|
showTalkOverError,
|
|
activeSpeakerUserId,
|
|
activeSpeakerDisplayName,
|
|
activeSpeakerAvatarUrl,
|
|
activeSpeakerIsLocalUser,
|
|
activeSpeakerVolume,
|
|
size,
|
|
startTalking,
|
|
stopTalking,
|
|
}) => {
|
|
const buttonRef = createRef<HTMLButtonElement>();
|
|
|
|
const [{ isHeld, activeTouchID }, setState] = useState<State>({
|
|
isHeld: false,
|
|
activeTouchID: null,
|
|
});
|
|
const onWindowMouseUp = useCallback(
|
|
(e) => {
|
|
if (isHeld) stopTalking();
|
|
setState({ isHeld: false, activeTouchID: null });
|
|
},
|
|
[isHeld, setState, 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 (isHeld) stopTalking();
|
|
setState({ isHeld: false, activeTouchID: null });
|
|
},
|
|
[isHeld, activeTouchID, setState, stopTalking]
|
|
);
|
|
|
|
const onButtonMouseDown = useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
e.preventDefault();
|
|
setState({ isHeld: true, activeTouchID: null });
|
|
startTalking();
|
|
},
|
|
[setState, startTalking]
|
|
);
|
|
|
|
const onButtonTouchStart = useCallback(
|
|
(e: TouchEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (isHeld) return;
|
|
|
|
setState({
|
|
isHeld: true,
|
|
activeTouchID: e.changedTouches.item(0).identifier,
|
|
});
|
|
startTalking();
|
|
},
|
|
[isHeld, setState, startTalking]
|
|
);
|
|
|
|
useEffect(() => {
|
|
const currentButtonElement = buttonRef.current;
|
|
|
|
// 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,
|
|
});
|
|
|
|
return () => {
|
|
window.removeEventListener("mouseup", onWindowMouseUp);
|
|
window.removeEventListener("touchend", onWindowTouchEnd);
|
|
currentButtonElement.removeEventListener(
|
|
"touchstart",
|
|
onButtonTouchStart
|
|
);
|
|
};
|
|
}, [onWindowMouseUp, onWindowTouchEnd, onButtonTouchStart, buttonRef]);
|
|
|
|
const { shadow } = useSpring({
|
|
shadow: (Math.max(activeSpeakerVolume, -70) + 70) * 0.6,
|
|
config: {
|
|
clamp: true,
|
|
tension: 300,
|
|
},
|
|
});
|
|
const shadowColor = showTalkOverError
|
|
? "var(--alert-20)"
|
|
: "var(--primaryColor-20)";
|
|
|
|
return (
|
|
<animated.button
|
|
className={classNames(styles.pttButton, {
|
|
[styles.talking]: activeSpeakerUserId,
|
|
[styles.error]: showTalkOverError,
|
|
})}
|
|
style={{
|
|
boxShadow: shadow.to(
|
|
(s) =>
|
|
`0px 0px 0px ${s}px ${shadowColor}, 0px 0px 0px ${
|
|
2 * s
|
|
}px ${shadowColor}`
|
|
),
|
|
}}
|
|
onMouseDown={onButtonMouseDown}
|
|
ref={buttonRef}
|
|
>
|
|
{activeSpeakerIsLocalUser || !activeSpeakerUserId ? (
|
|
<MicIcon
|
|
className={styles.micIcon}
|
|
width={size / 3}
|
|
height={size / 3}
|
|
/>
|
|
) : (
|
|
<Avatar
|
|
key={activeSpeakerUserId}
|
|
size={size - 12}
|
|
src={activeSpeakerAvatarUrl}
|
|
fallback={activeSpeakerDisplayName.slice(0, 1).toUpperCase()}
|
|
className={styles.avatar}
|
|
/>
|
|
)}
|
|
</animated.button>
|
|
);
|
|
};
|