refactor(core-html5): custom hooks for voice data
This commit is contained in:
parent
b700000133
commit
17b734e642
@ -664,8 +664,12 @@ select
|
|||||||
"user_voice"."talking",
|
"user_voice"."talking",
|
||||||
"user_voice"."startTime",
|
"user_voice"."startTime",
|
||||||
"user_voice"."endTime",
|
"user_voice"."endTime",
|
||||||
"user_voice"."voiceActivityAt"
|
"user_voice"."voiceActivityAt",
|
||||||
|
"user"."color",
|
||||||
|
"user"."name",
|
||||||
|
"user"."speechLocale"
|
||||||
FROM "user_voice"
|
FROM "user_voice"
|
||||||
|
JOIN "user" ON "user"."userId" = "user_voice"."userId"
|
||||||
WHERE "voiceActivityAt" is not null
|
WHERE "voiceActivityAt" is not null
|
||||||
AND --filter recent activities to avoid receiving all history every time it starts the streming
|
AND --filter recent activities to avoid receiving all history every time it starts the streming
|
||||||
("voiceActivityAt" > current_timestamp - '10 seconds'::interval
|
("voiceActivityAt" > current_timestamp - '10 seconds'::interval
|
||||||
|
@ -16,6 +16,9 @@ select_permissions:
|
|||||||
- talking
|
- talking
|
||||||
- userId
|
- userId
|
||||||
- voiceActivityAt
|
- voiceActivityAt
|
||||||
|
- color
|
||||||
|
- name
|
||||||
|
- speechLocale
|
||||||
filter:
|
filter:
|
||||||
meetingId:
|
meetingId:
|
||||||
_eq: X-Hasura-MeetingId
|
_eq: X-Hasura-MeetingId
|
||||||
|
@ -7,6 +7,7 @@ import Auth from '/imports/ui/services/auth';
|
|||||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||||
import useSettings from '/imports/ui/services/settings/hooks/useSettings';
|
import useSettings from '/imports/ui/services/settings/hooks/useSettings';
|
||||||
import { SETTINGS } from '/imports/ui/services/settings/enums';
|
import { SETTINGS } from '/imports/ui/services/settings/enums';
|
||||||
|
import useWhoIsUnmuted from '/imports/ui/core/hooks/useWhoIsUnmuted';
|
||||||
|
|
||||||
const ReactionsButtonContainer = ({ ...props }) => {
|
const ReactionsButtonContainer = ({ ...props }) => {
|
||||||
const layoutContextDispatch = layoutDispatch();
|
const layoutContextDispatch = layoutDispatch();
|
||||||
@ -23,13 +24,14 @@ const ReactionsButtonContainer = ({ ...props }) => {
|
|||||||
voice: user.voice,
|
voice: user.voice,
|
||||||
reactionEmoji: user.reactionEmoji,
|
reactionEmoji: user.reactionEmoji,
|
||||||
}));
|
}));
|
||||||
|
const { data: unmutedUsers } = useWhoIsUnmuted();
|
||||||
|
|
||||||
const currentUser = {
|
const currentUser = {
|
||||||
userId: Auth.userID,
|
userId: Auth.userID,
|
||||||
emoji: currentUserData?.emoji,
|
emoji: currentUserData?.emoji,
|
||||||
raiseHand: currentUserData?.raiseHand,
|
raiseHand: currentUserData?.raiseHand,
|
||||||
away: currentUserData?.away,
|
away: currentUserData?.away,
|
||||||
muted: currentUserData?.voice?.muted || false,
|
muted: !unmutedUsers.has(Auth.userID),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { autoCloseReactionsBar } = useSettings(SETTINGS.APPLICATION);
|
const { autoCloseReactionsBar } = useSettings(SETTINGS.APPLICATION);
|
||||||
|
@ -18,6 +18,8 @@ import MutedAlert from '/imports/ui/components/muted-alert/component';
|
|||||||
import MuteToggle from './buttons/muteToggle';
|
import MuteToggle from './buttons/muteToggle';
|
||||||
import ListenOnly from './buttons/listenOnly';
|
import ListenOnly from './buttons/listenOnly';
|
||||||
import LiveSelection from './buttons/LiveSelection';
|
import LiveSelection from './buttons/LiveSelection';
|
||||||
|
import useWhoIsTalking from '/imports/ui/core/hooks/useWhoIsTalking';
|
||||||
|
import useWhoIsUnmuted from '/imports/ui/core/hooks/useWhoIsUnmuted';
|
||||||
|
|
||||||
const AUDIO_INPUT = 'audioinput';
|
const AUDIO_INPUT = 'audioinput';
|
||||||
const AUDIO_OUTPUT = 'audiooutput';
|
const AUDIO_OUTPUT = 'audiooutput';
|
||||||
@ -230,13 +232,16 @@ const InputStreamLiveSelectorContainer: React.FC = () => {
|
|||||||
locked: u?.locked ?? false,
|
locked: u?.locked ?? false,
|
||||||
away: u?.away,
|
away: u?.away,
|
||||||
voice: {
|
voice: {
|
||||||
muted: u?.voice?.muted ?? false,
|
|
||||||
listenOnly: u?.voice?.listenOnly ?? false,
|
listenOnly: u?.voice?.listenOnly ?? false,
|
||||||
talking: u?.voice?.talking ?? false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: talkingUsers } = useWhoIsTalking();
|
||||||
|
const { data: unmutedUsers } = useWhoIsUnmuted();
|
||||||
|
const talking = Boolean(currentUser?.userId && talkingUsers[currentUser.userId]);
|
||||||
|
const muted = Boolean(currentUser?.userId && !unmutedUsers.has(currentUser.userId));
|
||||||
|
|
||||||
const { data: currentMeeting } = useMeeting((m: Partial<Meeting>) => {
|
const { data: currentMeeting } = useMeeting((m: Partial<Meeting>) => {
|
||||||
return {
|
return {
|
||||||
lockSettings: m?.lockSettings,
|
lockSettings: m?.lockSettings,
|
||||||
@ -264,8 +269,8 @@ const InputStreamLiveSelectorContainer: React.FC = () => {
|
|||||||
isAudioLocked={(!currentUser?.isModerator && currentUser?.locked
|
isAudioLocked={(!currentUser?.isModerator && currentUser?.locked
|
||||||
&& currentMeeting?.lockSettings?.disableMic) ?? false}
|
&& currentMeeting?.lockSettings?.disableMic) ?? false}
|
||||||
listenOnly={currentUser?.voice?.listenOnly ?? false}
|
listenOnly={currentUser?.voice?.listenOnly ?? false}
|
||||||
muted={currentUser?.voice?.muted ?? false}
|
muted={muted}
|
||||||
talking={currentUser?.voice?.talking ?? false}
|
talking={talking}
|
||||||
inAudio={!!currentUser?.voice ?? false}
|
inAudio={!!currentUser?.voice ?? false}
|
||||||
showMute={(!!currentUser?.voice && !currentMeeting?.lockSettings?.disableMic) ?? false}
|
showMute={(!!currentUser?.voice && !currentMeeting?.lockSettings?.disableMic) ?? false}
|
||||||
isConnected={isConnected}
|
isConnected={isConnected}
|
||||||
|
@ -3,16 +3,15 @@ import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
|||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
import AudioManager from '/imports/ui/services/audio-manager';
|
import AudioManager from '/imports/ui/services/audio-manager';
|
||||||
import useToggleVoice from './useToggleVoice';
|
import useToggleVoice from './useToggleVoice';
|
||||||
|
import useWhoIsUnmuted from '/imports/ui/core/hooks/useWhoIsUnmuted';
|
||||||
|
|
||||||
const useMuteMicrophone = () => {
|
const useMuteMicrophone = () => {
|
||||||
const { data: currentUser } = useCurrentUser((u) => ({
|
const { data: currentUser } = useCurrentUser((u) => ({
|
||||||
userId: u.userId,
|
userId: u.userId,
|
||||||
voice: {
|
|
||||||
muted: u.voice?.muted,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
const toggleVoice = useToggleVoice();
|
const toggleVoice = useToggleVoice();
|
||||||
const muted = !!currentUser?.voice?.muted;
|
const { data: unmutedUsers } = useWhoIsUnmuted();
|
||||||
|
const muted = currentUser?.userId && !unmutedUsers.has(currentUser?.userId);
|
||||||
const userId = currentUser?.userId ?? '';
|
const userId = currentUser?.userId ?? '';
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { USER_SET_MUTED } from '../mutations';
|
import { USER_SET_MUTED } from '../mutations';
|
||||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
|
|
||||||
const useToggleVoice = () => {
|
const useToggleVoice = () => {
|
||||||
const [userSetMuted] = useMutation(USER_SET_MUTED);
|
const [userSetMuted] = useMutation(USER_SET_MUTED);
|
||||||
const { data: currentUserData } = useCurrentUser((u) => ({
|
|
||||||
voice: {
|
|
||||||
muted: u.voice?.muted,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const toggleVoice = async (userId: string, muted: boolean) => {
|
const toggleVoice = async (userId: string, muted: boolean) => {
|
||||||
try {
|
try {
|
||||||
@ -20,7 +14,7 @@ const useToggleVoice = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return useCallback(toggleVoice, [currentUserData?.voice?.muted]);
|
return useCallback(toggleVoice, [userSetMuted]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useToggleVoice;
|
export default useToggleVoice;
|
||||||
|
@ -4,7 +4,6 @@ interface VoiceUsers {
|
|||||||
joined: string | null;
|
joined: string | null;
|
||||||
listenOnly: string | null;
|
listenOnly: string | null;
|
||||||
muted: string | null;
|
muted: string | null;
|
||||||
talking: string | null;
|
|
||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,8 +16,6 @@ export const VOICE_USERS_SUBSCRIPTION = gql`
|
|||||||
user_voice {
|
user_voice {
|
||||||
joined
|
joined
|
||||||
listenOnly
|
listenOnly
|
||||||
muted
|
|
||||||
talking
|
|
||||||
userId
|
userId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import { SETTINGS } from '../../services/settings/enums';
|
|||||||
import { useStorageKey } from '../../services/storage/hooks';
|
import { useStorageKey } from '../../services/storage/hooks';
|
||||||
import { BREAKOUT_COUNT } from './queries';
|
import { BREAKOUT_COUNT } from './queries';
|
||||||
import useMeeting from '../../core/hooks/useMeeting';
|
import useMeeting from '../../core/hooks/useMeeting';
|
||||||
|
import useWhoIsUnmuted from '../../core/hooks/useWhoIsUnmuted';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
joinedAudio: {
|
joinedAudio: {
|
||||||
@ -173,7 +174,9 @@ const AudioContainer = (props) => {
|
|||||||
const { hasBreakoutRooms: hadBreakoutRooms } = prevProps || {};
|
const { hasBreakoutRooms: hadBreakoutRooms } = prevProps || {};
|
||||||
const userIsReturningFromBreakoutRoom = hadBreakoutRooms && !hasBreakoutRooms;
|
const userIsReturningFromBreakoutRoom = hadBreakoutRooms && !hasBreakoutRooms;
|
||||||
|
|
||||||
const { data: currentUserMuted } = useCurrentUser((u) => u?.voice?.muted ?? false);
|
const { data: currentUser } = useCurrentUser((u) => ({ userId: u.userId }));
|
||||||
|
const { data: unmutedUsers } = useWhoIsUnmuted();
|
||||||
|
const currentUserMuted = currentUser?.userId && !unmutedUsers.has(currentUser.userId);
|
||||||
|
|
||||||
const joinAudio = () => {
|
const joinAudio = () => {
|
||||||
if (Service.isConnected()) return;
|
if (Service.isConnected()) return;
|
||||||
|
@ -4,20 +4,16 @@ import {
|
|||||||
IsBreakoutSubscriptionData,
|
IsBreakoutSubscriptionData,
|
||||||
MEETING_ISBREAKOUT_SUBSCRIPTION,
|
MEETING_ISBREAKOUT_SUBSCRIPTION,
|
||||||
} from './queries';
|
} from './queries';
|
||||||
import { UserVoice } from '/imports/ui/Types/userVoice';
|
|
||||||
import { uniqueId } from '/imports/utils/string-utils';
|
import { uniqueId } from '/imports/utils/string-utils';
|
||||||
import Styled from './styles';
|
import Styled from './styles';
|
||||||
import { User } from '/imports/ui/Types/user';
|
import { User } from '/imports/ui/Types/user';
|
||||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||||
import { muteUser } from './service';
|
import { muteUser } from './service';
|
||||||
import useToggleVoice from '../../../audio/audio-graphql/hooks/useToggleVoice';
|
import useToggleVoice from '../../../audio/audio-graphql/hooks/useToggleVoice';
|
||||||
import TALKING_INDICATOR_SUBSCRIPTION from '/imports/ui/core/graphql/queries/userVoiceSubscription';
|
|
||||||
import { setTalkingIndicatorList } from '/imports/ui/core/hooks/useTalkingIndicator';
|
import { setTalkingIndicatorList } from '/imports/ui/core/hooks/useTalkingIndicator';
|
||||||
import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedSubscription';
|
import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedSubscription';
|
||||||
|
import useVoiceActivity from '/imports/ui/core/hooks/useVoiceActivity';
|
||||||
interface TalkingIndicatorSubscriptionData {
|
import { VoiceActivityResponse } from '/imports/ui/core/graphql/queries/whoIsTalking';
|
||||||
user_voice: Array<Partial<UserVoice>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TALKING_INDICATORS_MAX = 8;
|
const TALKING_INDICATORS_MAX = 8;
|
||||||
|
|
||||||
@ -49,7 +45,7 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface TalkingIndicatorProps {
|
interface TalkingIndicatorProps {
|
||||||
talkingUsers: Array<Partial<UserVoice>>;
|
talkingUsers: Array<VoiceActivityResponse['user_voice_activity_stream'][number]>;
|
||||||
isBreakout: boolean;
|
isBreakout: boolean;
|
||||||
moreThanMaxIndicators: boolean;
|
moreThanMaxIndicators: boolean;
|
||||||
isModerator: boolean;
|
isModerator: boolean;
|
||||||
@ -70,14 +66,15 @@ const TalkingIndicator: React.FC<TalkingIndicatorProps> = ({
|
|||||||
setTalkingIndicatorList([]);
|
setTalkingIndicatorList([]);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const talkingElements = useMemo(() => talkingUsers.map((talkingUser: Partial<UserVoice>) => {
|
const talkingElements = useMemo(() => talkingUsers.map((talkingUser) => {
|
||||||
const {
|
const {
|
||||||
talking,
|
talking,
|
||||||
muted,
|
muted,
|
||||||
user: { color, speechLocale } = {} as Partial<User>,
|
color,
|
||||||
|
speechLocale,
|
||||||
|
name,
|
||||||
} = talkingUser;
|
} = talkingUser;
|
||||||
|
|
||||||
const name = talkingUser.user?.name;
|
|
||||||
const ariaLabel = intl.formatMessage(talking
|
const ariaLabel = intl.formatMessage(talking
|
||||||
? intlMessages.isTalking : intlMessages.wasTalking, {
|
? intlMessages.isTalking : intlMessages.wasTalking, {
|
||||||
0: name,
|
0: name,
|
||||||
@ -182,19 +179,6 @@ const TalkingIndicatorContainer: React.FC = (() => {
|
|||||||
isModerator: u?.isModerator,
|
isModerator: u?.isModerator,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const {
|
|
||||||
data: talkingIndicatorData,
|
|
||||||
loading: talkingIndicatorLoading,
|
|
||||||
error: talkingIndicatorError,
|
|
||||||
} = useDeduplicatedSubscription<TalkingIndicatorSubscriptionData>(
|
|
||||||
TALKING_INDICATOR_SUBSCRIPTION,
|
|
||||||
{
|
|
||||||
variables: {
|
|
||||||
limit: TALKING_INDICATORS_MAX,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: isBreakoutData,
|
data: isBreakoutData,
|
||||||
loading: isBreakoutLoading,
|
loading: isBreakoutLoading,
|
||||||
@ -202,19 +186,31 @@ const TalkingIndicatorContainer: React.FC = (() => {
|
|||||||
} = useDeduplicatedSubscription<IsBreakoutSubscriptionData>(MEETING_ISBREAKOUT_SUBSCRIPTION);
|
} = useDeduplicatedSubscription<IsBreakoutSubscriptionData>(MEETING_ISBREAKOUT_SUBSCRIPTION);
|
||||||
|
|
||||||
const toggleVoice = useToggleVoice();
|
const toggleVoice = useToggleVoice();
|
||||||
|
const {
|
||||||
|
data: voiceActivity,
|
||||||
|
loading: voiceActivityLoading,
|
||||||
|
error: voiceActivityError,
|
||||||
|
} = useVoiceActivity();
|
||||||
|
const talkingUsers = useMemo(() => Object.values(voiceActivity)
|
||||||
|
.filter((v) => v.showTalkingIndicator)
|
||||||
|
.sort((v1, v2) => {
|
||||||
|
if (!v1.startTime && !v2.startTime) return 0;
|
||||||
|
if (!v1.startTime) return 1;
|
||||||
|
if (!v2.startTime) return -1;
|
||||||
|
return v2.startTime - v1.startTime;
|
||||||
|
}).slice(0, TALKING_INDICATORS_MAX), [voiceActivity]);
|
||||||
|
|
||||||
if (talkingIndicatorLoading || isBreakoutLoading) return null;
|
if (voiceActivityLoading || isBreakoutLoading) return null;
|
||||||
|
|
||||||
if (talkingIndicatorError || isBreakoutError) {
|
if (voiceActivityError || isBreakoutError) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
error:
|
error:
|
||||||
{ JSON.stringify(talkingIndicatorError || isBreakoutError) }
|
{ JSON.stringify(voiceActivityError || isBreakoutError) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const talkingUsers = talkingIndicatorData?.user_voice ?? [];
|
|
||||||
const isBreakout = isBreakoutData?.meeting[0]?.isBreakout ?? false;
|
const isBreakout = isBreakoutData?.meeting[0]?.isBreakout ?? false;
|
||||||
setTalkingIndicatorList(talkingUsers);
|
setTalkingIndicatorList(talkingUsers);
|
||||||
return (
|
return (
|
||||||
|
@ -17,6 +17,8 @@ import normalizeEmojiName from './service';
|
|||||||
import { convertRemToPixels } from '/imports/utils/dom-utils';
|
import { convertRemToPixels } from '/imports/utils/dom-utils';
|
||||||
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
|
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
|
||||||
import { useIsReactionsEnabled } from '/imports/ui/services/features';
|
import { useIsReactionsEnabled } from '/imports/ui/services/features';
|
||||||
|
import useWhoIsTalking from '/imports/ui/core/hooks/useWhoIsTalking';
|
||||||
|
import useWhoIsUnmuted from '/imports/ui/core/hooks/useWhoIsUnmuted';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
moderator: {
|
moderator: {
|
||||||
@ -100,7 +102,13 @@ const UserListItem: React.FC<UserListItemProps> = ({ user, lockSettings }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const voiceUser = user.voice;
|
const { data: talkingUsers } = useWhoIsTalking();
|
||||||
|
const { data: unmutedUsers } = useWhoIsUnmuted();
|
||||||
|
const voiceUser = {
|
||||||
|
...user.voice,
|
||||||
|
talking: talkingUsers[user.userId],
|
||||||
|
muted: !unmutedUsers.has(user.userId),
|
||||||
|
};
|
||||||
const subs = [];
|
const subs = [];
|
||||||
|
|
||||||
const LABEL = window.meetingClientSettings.public.user.label;
|
const LABEL = window.meetingClientSettings.public.user.label;
|
||||||
|
@ -9,6 +9,8 @@ import useSettings from '/imports/ui/services/settings/hooks/useSettings';
|
|||||||
import { SETTINGS } from '/imports/ui/services/settings/enums';
|
import { SETTINGS } from '/imports/ui/services/settings/enums';
|
||||||
import { useStorageKey } from '/imports/ui/services/storage/hooks';
|
import { useStorageKey } from '/imports/ui/services/storage/hooks';
|
||||||
import useVoiceUsers from '/imports/ui/components/audio/audio-graphql/hooks/useVoiceUsers';
|
import useVoiceUsers from '/imports/ui/components/audio/audio-graphql/hooks/useVoiceUsers';
|
||||||
|
import useWhoIsTalking from '/imports/ui/core/hooks/useWhoIsTalking';
|
||||||
|
import useWhoIsUnmuted from '/imports/ui/core/hooks/useWhoIsUnmuted';
|
||||||
|
|
||||||
interface VideoListItemContainerProps {
|
interface VideoListItemContainerProps {
|
||||||
numOfStreams: number;
|
numOfStreams: number;
|
||||||
@ -55,13 +57,18 @@ const VideoListItemContainer: React.FC<VideoListItemContainerProps> = (props) =>
|
|||||||
|
|
||||||
const disabledCams = useStorageKey('disabledCams') || [];
|
const disabledCams = useStorageKey('disabledCams') || [];
|
||||||
const voiceUsers = useVoiceUsers((v) => ({
|
const voiceUsers = useVoiceUsers((v) => ({
|
||||||
muted: v.muted,
|
|
||||||
listenOnly: v.listenOnly,
|
listenOnly: v.listenOnly,
|
||||||
talking: v.talking,
|
|
||||||
joined: v.joined,
|
joined: v.joined,
|
||||||
userId: v.userId,
|
userId: v.userId,
|
||||||
}));
|
}));
|
||||||
const voiceUser = voiceUsers.data?.find((v) => v.userId === userId);
|
const { data: talkingUsers } = useWhoIsTalking();
|
||||||
|
const { data: unmutedUsers } = useWhoIsUnmuted();
|
||||||
|
const voiceUser = voiceUsers.data?.find((v) => v.userId === userId) ?? {};
|
||||||
|
const voiceData = {
|
||||||
|
...voiceUser,
|
||||||
|
talking: talkingUsers[userId],
|
||||||
|
muted: !unmutedUsers.has(userId),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VideoListItem
|
<VideoListItem
|
||||||
@ -83,7 +90,7 @@ const VideoListItemContainer: React.FC<VideoListItemContainerProps> = (props) =>
|
|||||||
onVirtualBgDrop={onVirtualBgDrop}
|
onVirtualBgDrop={onVirtualBgDrop}
|
||||||
settingsSelfViewDisable={settingsSelfViewDisable}
|
settingsSelfViewDisable={settingsSelfViewDisable}
|
||||||
stream={stream}
|
stream={stream}
|
||||||
voiceUser={voiceUser}
|
voiceUser={voiceData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,6 @@ const TALKING_INDICATOR_SUBSCRIPTION = gql`
|
|||||||
) {
|
) {
|
||||||
callerName
|
callerName
|
||||||
spoke
|
spoke
|
||||||
talking
|
|
||||||
floor
|
floor
|
||||||
startTime
|
startTime
|
||||||
muted
|
muted
|
||||||
|
@ -39,8 +39,6 @@ subscription UserListSubscription($offset: Int!, $limit: Int!) {
|
|||||||
voice {
|
voice {
|
||||||
joined
|
joined
|
||||||
listenOnly
|
listenOnly
|
||||||
talking
|
|
||||||
muted
|
|
||||||
voiceUserId
|
voiceUserId
|
||||||
}
|
}
|
||||||
cameras {
|
cameras {
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export interface VoiceActivityResponse {
|
||||||
|
user_voice_activity_stream: Array<{
|
||||||
|
startTime: number | undefined;
|
||||||
|
endTime: number | undefined;
|
||||||
|
muted: boolean;
|
||||||
|
talking: boolean;
|
||||||
|
userId: string;
|
||||||
|
color: string;
|
||||||
|
name: string;
|
||||||
|
speechLocale: string | undefined;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VOICE_ACTIVITY = gql`
|
||||||
|
subscription UserVoiceActivity {
|
||||||
|
user_voice_activity_stream(
|
||||||
|
cursor: { initial_value: { voiceActivityAt: "2020-01-01" } },
|
||||||
|
batch_size: 10
|
||||||
|
) {
|
||||||
|
muted
|
||||||
|
startTime
|
||||||
|
endTime
|
||||||
|
talking
|
||||||
|
userId
|
||||||
|
color
|
||||||
|
name
|
||||||
|
speechLocale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default VOICE_ACTIVITY;
|
@ -0,0 +1,79 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import logger from '/imports/startup/client/logger';
|
||||||
|
import VOICE_ACTIVITY, { VoiceActivityResponse } from '/imports/ui/core/graphql/queries/whoIsTalking';
|
||||||
|
import useDeduplicatedSubscription from './useDeduplicatedSubscription';
|
||||||
|
|
||||||
|
type VoiceItem = VoiceActivityResponse['user_voice_activity_stream'][number] & {
|
||||||
|
showTalkingIndicator: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TALKING_INDICATOR_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
const useVoiceActivity = () => {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
} = useDeduplicatedSubscription<VoiceActivityResponse>(VOICE_ACTIVITY);
|
||||||
|
const [record, setRecord] = useState<Record<string, VoiceItem>>({});
|
||||||
|
const timeoutRegistry = useRef<Record<string, NodeJS.Timeout>>({});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error({
|
||||||
|
logCode: 'voice_activity_sub_error',
|
||||||
|
extraInfo: {
|
||||||
|
errorName: error.name,
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
}, 'useVoiceActivity hook failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const voiceActivity: Record<string, VoiceItem> = { ...record };
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
data.user_voice_activity_stream.forEach((voice) => {
|
||||||
|
const {
|
||||||
|
userId, talking, endTime, muted,
|
||||||
|
} = voice;
|
||||||
|
|
||||||
|
if (muted) {
|
||||||
|
delete voiceActivity[userId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
voiceActivity[userId] = Object.assign(
|
||||||
|
voiceActivity[userId] || {},
|
||||||
|
voice,
|
||||||
|
{ showTalkingIndicator: talking || voiceActivity[userId]?.showTalkingIndicator },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (talking && timeoutRegistry.current[userId]) {
|
||||||
|
clearTimeout(timeoutRegistry.current[userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endTime) {
|
||||||
|
timeoutRegistry.current[userId] = setTimeout(() => {
|
||||||
|
setRecord((prevRecord) => ({
|
||||||
|
...prevRecord,
|
||||||
|
[userId]: Object.assign(
|
||||||
|
prevRecord[userId] || {},
|
||||||
|
{ showTalkingIndicator: false },
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}, TALKING_INDICATOR_TIMEOUT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setRecord(voiceActivity);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
data: record,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useVoiceActivity;
|
27
bigbluebutton-html5/imports/ui/core/hooks/useWhoIsTalking.ts
Normal file
27
bigbluebutton-html5/imports/ui/core/hooks/useWhoIsTalking.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import useVoiceActivity from './useVoiceActivity';
|
||||||
|
|
||||||
|
const useWhoIsTalking = () => {
|
||||||
|
const {
|
||||||
|
data: voiceActivity,
|
||||||
|
loading,
|
||||||
|
} = useVoiceActivity();
|
||||||
|
const [record, setRecord] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const talkingUsers: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
Object.keys(voiceActivity).forEach((userId) => {
|
||||||
|
talkingUsers[userId] = voiceActivity[userId]?.talking;
|
||||||
|
});
|
||||||
|
|
||||||
|
setRecord(talkingUsers);
|
||||||
|
}, [voiceActivity]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: record,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWhoIsTalking;
|
21
bigbluebutton-html5/imports/ui/core/hooks/useWhoIsUnmuted.ts
Normal file
21
bigbluebutton-html5/imports/ui/core/hooks/useWhoIsUnmuted.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import useVoiceActivity from './useVoiceActivity';
|
||||||
|
|
||||||
|
const useWhoIsUnmuted = () => {
|
||||||
|
const {
|
||||||
|
data: voiceActivity,
|
||||||
|
loading,
|
||||||
|
} = useVoiceActivity();
|
||||||
|
|
||||||
|
const record = useMemo(() => {
|
||||||
|
const mutedUsers: Set<string> = new Set(Object.keys(voiceActivity));
|
||||||
|
return mutedUsers;
|
||||||
|
}, [voiceActivity]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: record,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWhoIsUnmuted;
|
Loading…
Reference in New Issue
Block a user