Raise Hand in Group Calls

This commit is contained in:
ayumi-signal 2023-12-06 13:52:29 -08:00 committed by GitHub
parent 45aeaeefd4
commit d6db3f7943
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1050 additions and 51 deletions

View file

@ -8,6 +8,7 @@ import classNames from 'classnames';
import type { VideoFrameSource } from '@signalapp/ringrtc';
import type {
ActiveCallStateType,
SendGroupCallRaiseHandType,
SendGroupCallReactionType,
SetLocalAudioType,
SetLocalPreviewType,
@ -74,6 +75,7 @@ import { handleOutsideClick } from '../util/handleOutsideClick';
import type { Props as ReactionPickerProps } from './conversation/ReactionPicker';
import type { SmartReactionPicker } from '../state/smart/ReactionPicker';
import { Emoji } from './emoji/Emoji';
import { CallingRaisedHandsList } from './CallingRaisedHandsList';
export type PropsType = {
activeCall: ActiveCallType;
@ -82,12 +84,14 @@ export type PropsType = {
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
hangUpActiveCall: (reason: string) => void;
i18n: LocalizerType;
isGroupCallRaiseHandEnabled: boolean;
isGroupCallReactionsEnabled: boolean;
me: ConversationType;
openSystemPreferencesAction: () => unknown;
renderReactionPicker: (
props: React.ComponentProps<typeof SmartReactionPicker>
) => JSX.Element;
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
setGroupCallVideoRequest: (
_: Array<GroupCallVideoRequest>,
@ -155,12 +159,14 @@ export function CallScreen({
groupMembers,
hangUpActiveCall,
i18n,
isGroupCallRaiseHandEnabled,
isGroupCallReactionsEnabled,
me,
openSystemPreferencesAction,
renderEmojiPicker,
renderReactionPicker,
setGroupCallVideoRequest,
sendGroupCallRaiseHand,
sendGroupCallReaction,
setLocalAudio,
setLocalVideo,
@ -232,6 +238,11 @@ export function CallScreen({
setShowMoreOptions(prevValue => !prevValue);
}, []);
const [showRaisedHandsList, setShowRaisedHandsList] = useState(false);
const toggleRaisedHandsList = useCallback(() => {
setShowRaisedHandsList(prevValue => !prevValue);
}, []);
const [controlsHover, setControlsHover] = useState(false);
const onControlsMouseEnter = useCallback(() => {
@ -460,7 +471,8 @@ export function CallScreen({
});
const isGroupCall = activeCall.callMode === CallMode.Group;
const isMoreOptionsButtonEnabled = isGroupCall && isGroupCallReactionsEnabled;
const isMoreOptionsButtonEnabled =
isGroupCall && (isGroupCallRaiseHandEnabled || isGroupCallReactionsEnabled);
let presentingButtonType: CallingButtonType;
if (presentingSource) {
@ -471,6 +483,110 @@ export function CallScreen({
presentingButtonType = CallingButtonType.PRESENTING_OFF;
}
const raisedHands =
activeCall.callMode === CallMode.Group ? activeCall.raisedHands : undefined;
// This is the value of our hand raised as seen by remote clients. We should prefer
// to use it in UI so the user understands what remote clients see.
const syncedLocalHandRaised = isHandRaised(raisedHands, localDemuxId);
// Don't call setLocalHandRaised because it only sets local state. Instead call
// toggleRaiseHand() which will set ringrtc state and call setLocalHandRaised.
const [localHandRaised, setLocalHandRaised] = useState<boolean>(
syncedLocalHandRaised
);
const previousLocalHandRaised = usePrevious(localHandRaised, localHandRaised);
const toggleRaiseHand = useCallback(
(raise?: boolean) => {
const nextValue = raise ?? !localHandRaised;
if (nextValue === previousLocalHandRaised) {
return;
}
setLocalHandRaised(nextValue);
// It's possible that the ringrtc call can fail due to flaky network connection.
// In that case, local and remote state (localHandRaised and raisedHands) can
// get out of sync. The user might need to manually toggle raise hand to get to
// a coherent state. It would be nice if this returned a Promise (but it doesn't)
sendGroupCallRaiseHand({
conversationId: conversation.id,
raise: nextValue,
});
},
[
localHandRaised,
previousLocalHandRaised,
conversation.id,
sendGroupCallRaiseHand,
]
);
const renderRaisedHandsToast = React.useCallback(
(hands: Array<number>) => {
const names = hands.map(demuxId =>
demuxId === localDemuxId
? i18n('icu:you')
: conversationsByDemuxId.get(demuxId)?.title
);
let message: string;
let buttonOverride: JSX.Element | undefined;
const count = names.length;
switch (count) {
case 0:
return undefined;
case 1:
if (names[0] === i18n('icu:you')) {
message = i18n('icu:CallControls__RaiseHandsToast--you');
buttonOverride = (
<button
className="CallingRaisedHandsToasts__Link"
onClick={() => toggleRaiseHand(false)}
type="button"
>
{i18n('icu:CallControls__RaiseHands--lower')}
</button>
);
} else {
message = i18n('icu:CallControls__RaiseHandsToast--one', {
name: names[0],
});
}
break;
case 2:
message = i18n('icu:CallControls__RaiseHandsToast--two', {
name: names[0],
otherName: names[1],
});
break;
default:
message = i18n('icu:CallControls__RaiseHandsToast--more', {
name: names[0],
otherName: names[1],
overflowCount: names.length - 2,
});
}
return (
<div className="CallingReactionsToast__Content">
<span className="CallingReactionsToast__HandIcon" />
{message}
{buttonOverride || (
<button
className="link CallingRaisedHandsToasts__Link"
onClick={() => setShowRaisedHandsList(true)}
type="button"
>
{i18n('icu:CallControls__RaiseHands--open-queue')}
</button>
)}
</div>
);
},
[i18n, localDemuxId, conversationsByDemuxId, toggleRaiseHand]
);
const raisedHandsCount: number = raisedHands?.size ?? 0;
const callStatus: ReactNode | string = React.useMemo(() => {
if (isRinging) {
return i18n('icu:outgoingCallRinging');
@ -599,6 +715,39 @@ export function CallScreen({
localDemuxId={localDemuxId}
i18n={i18n}
/>
{raisedHands && raisedHandsCount > 0 && (
<>
<button
className="CallingRaisedHandsList__Button"
onClick={toggleRaisedHandsList}
type="button"
>
<span className="CallingRaisedHandsList__ButtonIcon" />
{syncedLocalHandRaised ? (
<>
{i18n('icu:you')}
{raisedHandsCount > 1 && ` + ${String(raisedHandsCount - 1)}`}
</>
) : (
raisedHandsCount
)}
</button>
{showRaisedHandsList && (
<CallingRaisedHandsList
i18n={i18n}
onClose={() => setShowRaisedHandsList(false)}
onLowerMyHand={() => {
toggleRaiseHand(false);
setShowRaisedHandsList(false);
}}
localDemuxId={localDemuxId}
conversationsByDemuxId={conversationsByDemuxId}
raisedHands={raisedHands}
localHandRaised={syncedLocalHandRaised}
/>
)}
</>
)}
<div className="module-ongoing-call__footer">
<div className="module-calling__spacer CallControls__OuterSpacer" />
<div
@ -616,6 +765,8 @@ export function CallScreen({
<CallingButtonToastsContainer
hasLocalAudio={hasLocalAudio}
outgoingRing={undefined}
raisedHands={raisedHands}
renderRaisedHandsToast={renderRaisedHandsToast}
i18n={i18n}
/>
@ -625,18 +776,34 @@ export function CallScreen({
className="CallControls__MoreOptionsMenu"
ref={moreOptionsMenuRef}
>
{renderReactionPicker({
ref: reactionPickerRef,
onClose: () => setShowMoreOptions(false),
onPick: emoji => {
setShowMoreOptions(false);
sendGroupCallReaction({
conversationId: conversation.id,
value: emoji,
});
},
renderEmojiPicker,
})}
{isGroupCallReactionsEnabled &&
renderReactionPicker({
ref: reactionPickerRef,
onClose: () => setShowMoreOptions(false),
onPick: emoji => {
setShowMoreOptions(false);
sendGroupCallReaction({
conversationId: conversation.id,
value: emoji,
});
},
renderEmojiPicker,
})}
{isGroupCallRaiseHandEnabled && (
<button
className="CallControls__MenuItemRaiseHand"
onClick={() => {
setShowMoreOptions(false);
toggleRaiseHand();
}}
type="button"
>
<span className="CallControls__MenuItemRaiseHandIcon" />
{localHandRaised
? i18n('icu:CallControls__MenuItemRaiseHand--lower')
: i18n('icu:CallControls__MenuItemRaiseHand')}
</button>
)}
</div>
</div>
)}
@ -715,6 +882,9 @@ export function CallScreen({
audioLevel={localAudioLevel}
shouldShowSpeaking={isSpeaking}
/>
{syncedLocalHandRaised && (
<div className="CallingStatusIndicator CallingStatusIndicator--HandRaised" />
)}
</div>
) : (
<div className="module-ongoing-call__footer__local-preview" />
@ -875,3 +1045,14 @@ function CallingReactionsToasts(props: CallingReactionsToastsType) {
useReactionsToast(props);
return null;
}
function isHandRaised(
raisedHands: Set<number> | undefined,
demuxId: number | undefined
): boolean {
if (raisedHands === undefined || demuxId === undefined) {
return false;
}
return raisedHands.has(demuxId);
}