Finish basic ptt implemenation
This commit is contained in:
parent
3a6346aa63
commit
363f2340a0
9 changed files with 350 additions and 45 deletions
36
src/input/Toggle.jsx
Normal file
36
src/input/Toggle.jsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import styles from "./Toggle.module.css";
|
||||||
|
import { useToggleButton } from "@react-aria/button";
|
||||||
|
import { useToggleState } from "@react-stately/toggle";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Field } from "./Input";
|
||||||
|
|
||||||
|
export function Toggle({ id, label, className, ...rest }) {
|
||||||
|
const buttonRef = useRef();
|
||||||
|
const state = useToggleState(rest);
|
||||||
|
const { buttonProps, isPressed } = useToggleButton(rest, state, buttonRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
className={classNames(
|
||||||
|
styles.toggle,
|
||||||
|
{ [styles.on]: isPressed },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
{...buttonProps}
|
||||||
|
ref={buttonRef}
|
||||||
|
id={id}
|
||||||
|
className={classNames(styles.button, {
|
||||||
|
[styles.isPressed]: isPressed,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={styles.ball} />
|
||||||
|
</button>
|
||||||
|
<label className={styles.label} htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
46
src/input/Toggle.module.css
Normal file
46
src/input/Toggle.module.css
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
.toggle {
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
transition: background-color 0.20s ease-out 0.1s;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 21px;
|
||||||
|
background-color: #6F7882;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ball {
|
||||||
|
transition: left 0.15s ease-out 0.1s;
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 21px;
|
||||||
|
background-color: #15191E;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 10px 8px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #6F7882;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .button {
|
||||||
|
background-color: #0DBD8B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .ball {
|
||||||
|
left: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.on .label {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
|
@ -77,18 +77,15 @@ export function GroupCallView({
|
||||||
if (groupCall.isPtt) {
|
if (groupCall.isPtt) {
|
||||||
return (
|
return (
|
||||||
<PTTCallView
|
<PTTCallView
|
||||||
|
client={client}
|
||||||
|
roomId={roomId}
|
||||||
|
roomName={groupCall.room.name}
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
participants={participants}
|
participants={participants}
|
||||||
client={client}
|
|
||||||
roomName={groupCall.room.name}
|
|
||||||
microphoneMuted={microphoneMuted}
|
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
|
||||||
userMediaFeeds={userMediaFeeds}
|
userMediaFeeds={userMediaFeeds}
|
||||||
activeSpeaker={activeSpeaker}
|
|
||||||
onLeave={onLeave}
|
onLeave={onLeave}
|
||||||
setShowInspector={onChangeShowInspector}
|
setShowInspector={onChangeShowInspector}
|
||||||
showInspector={showInspector}
|
showInspector={showInspector}
|
||||||
roomId={roomId}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,41 +1,41 @@
|
||||||
import classNames from "classnames";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
import styles from "./PTTButton.module.css";
|
import styles from "./PTTButton.module.css";
|
||||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
|
|
||||||
export function PTTButton({ client, activeSpeaker }) {
|
export function PTTButton({
|
||||||
const size = 232;
|
showTalkOverError,
|
||||||
|
activeSpeakerUserId,
|
||||||
const isLocal = client.userId === activeSpeaker;
|
activeSpeakerDisplayName,
|
||||||
const avatarUrl = activeSpeaker?.user?.avatarUrl;
|
activeSpeakerAvatarUrl,
|
||||||
|
activeSpeakerIsLocalUser,
|
||||||
|
size,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classNames(styles.pttButton, {
|
className={classNames(styles.pttButton, {
|
||||||
[styles.speaking]: !!activeSpeaker,
|
[styles.talking]: activeSpeakerUserId,
|
||||||
|
[styles.error]: showTalkOverError,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isLocal || !avatarUrl || !activeSpeaker ? (
|
{activeSpeakerIsLocalUser || !activeSpeakerUserId ? (
|
||||||
<MicIcon
|
<MicIcon
|
||||||
classNames={styles.micIcon}
|
className={styles.micIcon}
|
||||||
width={size / 3}
|
width={size / 3}
|
||||||
height={size / 3}
|
height={size / 3}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Avatar
|
<Avatar
|
||||||
key={activeSpeaker.userId}
|
key={activeSpeakerUserId}
|
||||||
style={{
|
style={{
|
||||||
width: size,
|
width: size - 12,
|
||||||
height: size,
|
height: size - 12,
|
||||||
borderRadius: size,
|
borderRadius: size - 12,
|
||||||
fontSize: Math.round(size / 2),
|
fontSize: Math.round((size - 12) / 2),
|
||||||
}}
|
}}
|
||||||
src={
|
src={activeSpeakerAvatarUrl}
|
||||||
activeSpeaker.user.avatarUrl &&
|
fallback={activeSpeakerDisplayName.slice(0, 1).toUpperCase()}
|
||||||
getAvatarUrl(client, activeSpeaker.user.avatarUrl, size)
|
|
||||||
}
|
|
||||||
fallback={activeSpeaker.name.slice(0, 1).toUpperCase()}
|
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,8 +4,20 @@
|
||||||
max-height: 232px;
|
max-height: 232px;
|
||||||
max-width: 232px;
|
max-width: 232px;
|
||||||
border-radius: 116px;
|
border-radius: 116px;
|
||||||
|
|
||||||
color: ##fff;
|
color: ##fff;
|
||||||
border: 6px solid #0dbd8b;
|
border: 6px solid #0dbd8b;
|
||||||
background-color: #21262C;
|
background-color: #21262C;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.talking {
|
||||||
|
background-color: #0dbd8b;
|
||||||
|
box-shadow: 0px 0px 0px 17px rgba(13, 189, 139, 0.2), 0px 0px 0px 34px rgba(13, 189, 139, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #FF5B55;
|
||||||
|
border-color: #FF5B55;
|
||||||
|
box-shadow: 0px 0px 0px 17px rgba(255, 91, 85, 0.2), 0px 0px 0px 34px rgba(255, 91, 85, 0.2);
|
||||||
}
|
}
|
|
@ -2,10 +2,8 @@ import React from "react";
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
import { SettingsModal } from "../settings/SettingsModal";
|
import { SettingsModal } from "../settings/SettingsModal";
|
||||||
import { InviteModal } from "./InviteModal";
|
import { InviteModal } from "./InviteModal";
|
||||||
import { Button, HangupButton, InviteButton, SettingsButton } from "../button";
|
import { HangupButton, InviteButton, SettingsButton } from "../button";
|
||||||
import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header";
|
||||||
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
|
|
||||||
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
|
|
||||||
import styles from "./PTTCallView.module.css";
|
import styles from "./PTTCallView.module.css";
|
||||||
import { Facepile } from "../Facepile";
|
import { Facepile } from "../Facepile";
|
||||||
import { PTTButton } from "./PTTButton";
|
import { PTTButton } from "./PTTButton";
|
||||||
|
@ -13,28 +11,59 @@ import { PTTFeed } from "./PTTFeed";
|
||||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
|
import { usePTT } from "./usePTT";
|
||||||
|
import { Timer } from "./Timer";
|
||||||
|
import { Toggle } from "../input/Toggle";
|
||||||
|
import { getAvatarUrl } from "../matrix-utils";
|
||||||
|
import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
|
||||||
|
|
||||||
export function PTTCallView({
|
export function PTTCallView({
|
||||||
|
client,
|
||||||
|
roomId,
|
||||||
|
roomName,
|
||||||
groupCall,
|
groupCall,
|
||||||
participants,
|
participants,
|
||||||
client,
|
|
||||||
roomName,
|
|
||||||
microphoneMuted,
|
|
||||||
toggleMicrophoneMuted,
|
|
||||||
userMediaFeeds,
|
userMediaFeeds,
|
||||||
activeSpeaker,
|
|
||||||
onLeave,
|
onLeave,
|
||||||
setShowInspector,
|
setShowInspector,
|
||||||
showInspector,
|
showInspector,
|
||||||
roomId,
|
|
||||||
}) {
|
}) {
|
||||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||||
useModalTriggerState();
|
useModalTriggerState();
|
||||||
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
||||||
useModalTriggerState();
|
useModalTriggerState();
|
||||||
const { audioOutput } = useMediaHandler();
|
|
||||||
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
|
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
const facepileSize = bounds.width < 800 ? "sm" : "md";
|
const facepileSize = bounds.width < 800 ? "sm" : "md";
|
||||||
|
const pttButtonSize = 232;
|
||||||
|
const pttBorderWidth = 6;
|
||||||
|
|
||||||
|
const { audioOutput } = useMediaHandler();
|
||||||
|
|
||||||
|
const {
|
||||||
|
pttButtonHeld,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
setTalkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
} = usePTT(client, groupCall, userMediaFeeds);
|
||||||
|
|
||||||
|
const activeSpeakerIsLocalUser =
|
||||||
|
activeSpeakerUserId && client.getUserId() === activeSpeakerUserId;
|
||||||
|
const showTalkOverError =
|
||||||
|
pttButtonHeld && !activeSpeakerIsLocalUser && !talkOverEnabled;
|
||||||
|
const activeSpeakerUser = activeSpeakerUserId
|
||||||
|
? client.getUser(activeSpeakerUserId)
|
||||||
|
: null;
|
||||||
|
const activeSpeakerAvatarUrl = activeSpeakerUser
|
||||||
|
? getAvatarUrl(
|
||||||
|
client,
|
||||||
|
activeSpeakerUser.avatarUrl,
|
||||||
|
pttButtonSize - pttBorderWidth * 2
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
const activeSpeakerDisplayName = activeSpeakerUser
|
||||||
|
? activeSpeakerUser.displayName
|
||||||
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.pttCallView} ref={containerRef}>
|
<div className={styles.pttCallView} ref={containerRef}>
|
||||||
|
@ -64,16 +93,40 @@ export function PTTCallView({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.pttButtonContainer}>
|
<div className={styles.pttButtonContainer}>
|
||||||
|
{activeSpeakerUserId ? (
|
||||||
<div className={styles.talkingInfo}>
|
<div className={styles.talkingInfo}>
|
||||||
<h2>Talking...</h2>
|
<h2>
|
||||||
<p>00:01:24</p>
|
{!activeSpeakerIsLocalUser && (
|
||||||
|
<AudioIcon className={styles.speakerIcon} />
|
||||||
|
)}
|
||||||
|
{activeSpeakerIsLocalUser
|
||||||
|
? "Talking..."
|
||||||
|
: `${activeSpeakerDisplayName} is talking...`}
|
||||||
|
</h2>
|
||||||
|
<Timer value={activeSpeakerUserId} />
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.talkingInfo} />
|
||||||
|
)}
|
||||||
<PTTButton
|
<PTTButton
|
||||||
client={client}
|
showTalkOverError={showTalkOverError}
|
||||||
activeSpeaker={activeSpeaker}
|
activeSpeakerUserId={activeSpeakerUserId}
|
||||||
groupCall={groupCall}
|
activeSpeakerDisplayName={activeSpeakerDisplayName}
|
||||||
|
activeSpeakerAvatarUrl={activeSpeakerAvatarUrl}
|
||||||
|
activeSpeakerIsLocalUser={activeSpeakerIsLocalUser}
|
||||||
|
size={pttButtonSize}
|
||||||
/>
|
/>
|
||||||
<p className={styles.actionTip}>Press and hold spacebar to talk</p>
|
<p className={styles.actionTip}>
|
||||||
|
{showTalkOverError
|
||||||
|
? "You can't talk at the same time"
|
||||||
|
: pttButtonHeld
|
||||||
|
? "Release spacebar key to stop"
|
||||||
|
: talkOverEnabled &&
|
||||||
|
activeSpeakerUserId &&
|
||||||
|
!activeSpeakerIsLocalUser
|
||||||
|
? `Press and hold spacebar to talk over ${activeSpeakerDisplayName}`
|
||||||
|
: "Press and hold spacebar to talk"}
|
||||||
|
</p>
|
||||||
{userMediaFeeds.map((callFeed) => (
|
{userMediaFeeds.map((callFeed) => (
|
||||||
<PTTFeed
|
<PTTFeed
|
||||||
key={callFeed.userId}
|
key={callFeed.userId}
|
||||||
|
@ -81,6 +134,14 @@ export function PTTCallView({
|
||||||
audioOutputDevice={audioOutput}
|
audioOutputDevice={audioOutput}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{isAdmin && (
|
||||||
|
<Toggle
|
||||||
|
isSelected={talkOverEnabled}
|
||||||
|
onChange={setTalkOverEnabled}
|
||||||
|
label="Talk over speaker"
|
||||||
|
id="talkOverEnabled"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,11 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speakerIcon {
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pttButtonContainer {
|
.pttButtonContainer {
|
||||||
|
|
37
src/room/Timer.jsx
Normal file
37
src/room/Timer.jsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function leftPad(value) {
|
||||||
|
return value < 10 ? "0" + value : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(msElapsed) {
|
||||||
|
const secondsElapsed = msElapsed / 1000;
|
||||||
|
const hours = Math.floor(secondsElapsed / 3600);
|
||||||
|
const minutes = Math.floor(secondsElapsed / 60) - hours * 60;
|
||||||
|
const seconds = Math.floor(secondsElapsed - hours * 3600 - minutes * 60);
|
||||||
|
return `${leftPad(hours)}:${leftPad(minutes)}:${leftPad(seconds)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Timer({ value }) {
|
||||||
|
const [timestamp, setTimestamp] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const startTimeMs = performance.now();
|
||||||
|
|
||||||
|
let animationFrame;
|
||||||
|
|
||||||
|
function onUpdate(curTimeMs) {
|
||||||
|
const msElapsed = curTimeMs - startTimeMs;
|
||||||
|
setTimestamp(formatTime(msElapsed));
|
||||||
|
animationFrame = requestAnimationFrame(onUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(startTimeMs);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrame);
|
||||||
|
};
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return <p>{timestamp}</p>;
|
||||||
|
}
|
111
src/room/usePTT.js
Normal file
111
src/room/usePTT.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function usePTT(client, groupCall, userMediaFeeds) {
|
||||||
|
const [
|
||||||
|
{ pttButtonHeld, isAdmin, talkOverEnabled, activeSpeakerUserId },
|
||||||
|
setState,
|
||||||
|
] = useState(() => {
|
||||||
|
const roomMember = groupCall.room.getMember(client.getUserId());
|
||||||
|
|
||||||
|
const activeSpeakerFeed = userMediaFeeds.find((f) => !f.isAudioMuted());
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAdmin: roomMember.powerLevel >= 100,
|
||||||
|
talkOverEnabled: false,
|
||||||
|
pttButtonHeld: false,
|
||||||
|
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onMuteStateChanged(...args) {
|
||||||
|
const activeSpeakerFeed = userMediaFeeds.find((f) => !f.isAudioMuted());
|
||||||
|
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
activeSpeakerUserId: activeSpeakerFeed
|
||||||
|
? activeSpeakerFeed.userId
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const callFeed of userMediaFeeds) {
|
||||||
|
callFeed.addListener("mute_state_changed", onMuteStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSpeakerFeed = userMediaFeeds.find((f) => !f.isAudioMuted());
|
||||||
|
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const callFeed of userMediaFeeds) {
|
||||||
|
callFeed.removeListener("mute_state_changed", onMuteStateChanged);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [userMediaFeeds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKeyDown(event) {
|
||||||
|
if (event.code === "Space") {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!activeSpeakerUserId || isAdmin || talkOverEnabled) {
|
||||||
|
if (groupCall.isMicrophoneMuted()) {
|
||||||
|
groupCall.setMicrophoneMuted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prevState) => ({ ...prevState, pttButtonHeld: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyUp(event) {
|
||||||
|
if (event.code === "Space") {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!groupCall.isMicrophoneMuted()) {
|
||||||
|
groupCall.setMicrophoneMuted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prevState) => ({ ...prevState, pttButtonHeld: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBlur() {
|
||||||
|
// TODO: We will need to disable this for a global PTT hotkey to work
|
||||||
|
if (!groupCall.isMicrophoneMuted()) {
|
||||||
|
groupCall.setMicrophoneMuted(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);
|
||||||
|
};
|
||||||
|
}, [activeSpeakerUserId, isAdmin, talkOverEnabled]);
|
||||||
|
|
||||||
|
const setTalkOverEnabled = useCallback((talkOverEnabled) => {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
talkOverEnabled,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pttButtonHeld,
|
||||||
|
isAdmin,
|
||||||
|
talkOverEnabled,
|
||||||
|
setTalkOverEnabled,
|
||||||
|
activeSpeakerUserId,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue