element-call/src/room/PTTButton.tsx

182 lines
5.2 KiB
TypeScript
Raw Normal View History

2022-05-04 17:09:48 +01: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-05-13 20:43:20 +01:00
import React, { useCallback, useEffect, useState, createRef } from "react";
2022-04-28 17:44:50 -07:00
import classNames from "classnames";
import { useSpring, animated } from "@react-spring/web";
2022-05-06 11:32:09 +01:00
2022-04-22 18:05:48 -07:00
import styles from "./PTTButton.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
import { Avatar } from "../Avatar";
2022-05-06 11:32:09 +01:00
interface Props {
showTalkOverError: boolean;
activeSpeakerUserId: string;
activeSpeakerDisplayName: string;
activeSpeakerAvatarUrl: string;
activeSpeakerIsLocalUser: boolean;
activeSpeakerVolume: number;
2022-05-06 11:32:09 +01:00
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;
}
2022-05-06 11:32:09 +01:00
export const PTTButton: React.FC<Props> = ({
2022-04-28 17:44:50 -07:00
showTalkOverError,
activeSpeakerUserId,
activeSpeakerDisplayName,
activeSpeakerAvatarUrl,
activeSpeakerIsLocalUser,
activeSpeakerVolume,
2022-04-28 17:44:50 -07:00
size,
startTalking,
stopTalking,
2022-05-06 11:32:09 +01:00
}) => {
2022-05-13 20:43:20 +01:00
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
2022-06-01 10:41:12 -04:00
? "var(--alert-20)"
: "var(--primaryColor-20)";
2022-04-22 18:05:48 -07:00
return (
<animated.button
2022-04-22 18:05:48 -07:00
className={classNames(styles.pttButton, {
2022-04-28 17:44:50 -07:00
[styles.talking]: activeSpeakerUserId,
[styles.error]: showTalkOverError,
2022-04-22 18:05:48 -07:00
})}
style={{
boxShadow: shadow.to(
(s) =>
`0px 0px 0px ${s}px ${shadowColor}, 0px 0px 0px ${
2 * s
}px ${shadowColor}`
),
}}
onMouseDown={onButtonMouseDown}
ref={buttonRef}
2022-04-22 18:05:48 -07:00
>
2022-04-28 17:44:50 -07:00
{activeSpeakerIsLocalUser || !activeSpeakerUserId ? (
2022-04-22 18:05:48 -07:00
<MicIcon
2022-04-28 17:44:50 -07:00
className={styles.micIcon}
2022-04-22 18:05:48 -07:00
width={size / 3}
height={size / 3}
/>
) : (
<Avatar
2022-04-28 17:44:50 -07:00
key={activeSpeakerUserId}
2022-05-18 19:00:59 -04:00
size={size - 12}
2022-04-28 17:44:50 -07:00
src={activeSpeakerAvatarUrl}
fallback={activeSpeakerDisplayName.slice(0, 1).toUpperCase()}
2022-04-22 18:05:48 -07:00
className={styles.avatar}
/>
)}
</animated.button>
2022-04-22 18:05:48 -07:00
);
2022-05-06 11:32:09 +01:00
};