Peek call links and group calls from Calls Tab
This commit is contained in:
parent
da1425265d
commit
fc9c5488c5
13 changed files with 762 additions and 71 deletions
|
@ -7076,6 +7076,22 @@
|
|||
"messageformat": "Call link",
|
||||
"description": "On the Calls Tab, the subtitle text for a Call Link entry."
|
||||
},
|
||||
"icu:CallsList__ItemCallInfo--Active": {
|
||||
"messageformat": "Active",
|
||||
"description": "On the Calls Tab, the subtitle text for an active call."
|
||||
},
|
||||
"icu:CallsList__LeaveCallDialogTitle": {
|
||||
"messageformat": "Leave the current call?",
|
||||
"description": "On the Calls Tab, when trying to join a different call when you're already in another one, this is the title of the confirmation dialog to leave the other call."
|
||||
},
|
||||
"icu:CallsList__LeaveCallDialogBody": {
|
||||
"messageformat": "You must leave the current call before joining a new call.",
|
||||
"description": "On the Calls Tab, when trying to join a different call when you're already in another one, this is the body of the confirmation dialog to leave the other call."
|
||||
},
|
||||
"icu:CallsList__LeaveCallDialogButton--leave": {
|
||||
"messageformat": "Leave call",
|
||||
"description": "On the Calls Tab, when trying to join a different call when you're already in another one, this is the button to confirm leaving the other call."
|
||||
},
|
||||
"icu:CallsNewCall__EmptyState--noQuery": {
|
||||
"messageformat": "No recent conversations.",
|
||||
"description": "Calls Tab > New Call > Conversations List > When no results found > With no search query"
|
||||
|
@ -7084,6 +7100,14 @@
|
|||
"messageformat": "No results for “{query}”",
|
||||
"description": "Calls Tab > New Call > Conversations List > When no results found > With a search query"
|
||||
},
|
||||
"icu:CallsNewCallButton--return": {
|
||||
"messageformat": "Return",
|
||||
"description": "Calls Tab button label for returning to the call you're currently in"
|
||||
},
|
||||
"icu:CallsNewCallButtonTooltip--in-another-call": {
|
||||
"messageformat": "You must leave the current call before joining a new call",
|
||||
"description": "Calls Tab button tooltip for the join call button when you are already in a different call."
|
||||
},
|
||||
"icu:CallHistory__Description--Default": {
|
||||
"messageformat": "{direction, select, Outgoing {Outgoing} other {Incoming}} {type, select, Audio {voice} Video {video} Group {group} other {}} call",
|
||||
"description": "Call History > Short description of call > When call was not missed or declined (generally accepted)"
|
||||
|
|
|
@ -884,3 +884,82 @@ $rtl-icon-map: (
|
|||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tooltip {
|
||||
& {
|
||||
@include font-body-2;
|
||||
|
||||
@include light-theme {
|
||||
background-color: $color-gray-04;
|
||||
color: $color-black;
|
||||
outline: 1px solid $color-gray-20;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-80;
|
||||
color: $color-gray-15;
|
||||
outline: 1px solid $color-gray-62;
|
||||
}
|
||||
|
||||
padding-block: 5px;
|
||||
padding-inline: 12px;
|
||||
border-radius: 6px;
|
||||
filter: drop-shadow(0px 4px 3px $color-black-alpha-16);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
& .module-tooltip-arrow::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-style: solid;
|
||||
border-width: 7px;
|
||||
}
|
||||
|
||||
&[data-placement='bottom'] .module-tooltip-arrow::before {
|
||||
@include light-theme {
|
||||
border-color: transparent transparent $color-gray-20 transparent;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
border-color: transparent transparent $color-gray-62 transparent;
|
||||
}
|
||||
|
||||
margin-top: -14px;
|
||||
/* stylelint-disable-next-line liberty/use-logical-spec */
|
||||
margin-left: -7px;
|
||||
}
|
||||
|
||||
&[data-placement='bottom'] .module-tooltip-arrow::after {
|
||||
@include light-theme {
|
||||
border-bottom-color: $color-gray-04;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
border-bottom-color: $color-gray-80;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement='top'] .module-tooltip-arrow::before {
|
||||
@include light-theme {
|
||||
border-color: $color-gray-20 transparent transparent transparent;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
border-color: $color-gray-62 transparent transparent transparent;
|
||||
}
|
||||
|
||||
margin-top: 0;
|
||||
/* stylelint-disable-next-line liberty/use-logical-spec */
|
||||
margin-left: -7px;
|
||||
}
|
||||
|
||||
&[data-placement='top'] .module-tooltip-arrow::after {
|
||||
@include light-theme {
|
||||
border-top-color: $color-gray-04;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
border-top-color: $color-gray-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,6 +104,7 @@
|
|||
@include button-reset;
|
||||
flex-shrink: 0;
|
||||
padding: 4px;
|
||||
margin-inline-end: 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:not(.CallsList__ToggleFilterByMissed--pressed):hover {
|
||||
|
@ -322,6 +323,66 @@
|
|||
}
|
||||
}
|
||||
|
||||
.CallsNewCall__ItemActionButton--join-call {
|
||||
$background: $color-accent-green;
|
||||
|
||||
@include font-body-2-bold;
|
||||
@include rounded-corners;
|
||||
|
||||
display: flex;
|
||||
width: auto;
|
||||
height: 26px;
|
||||
padding-block: 4px;
|
||||
padding-inline: 10px;
|
||||
align-items: center;
|
||||
background-color: $background;
|
||||
color: $color-white;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
&:before {
|
||||
$icon-size: 16px;
|
||||
|
||||
@include color-svg(
|
||||
'../images/icons/v3/video/video-compact-fill.svg',
|
||||
$color-white
|
||||
);
|
||||
content: '';
|
||||
display: block;
|
||||
height: $icon-size;
|
||||
margin-inline-end: 4px;
|
||||
min-width: $icon-size;
|
||||
width: $icon-size;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
&:hover {
|
||||
@include any-theme {
|
||||
background-color: darken($background, 16%);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include keyboard-mode {
|
||||
background-color: darken($background, 16%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CallsNewCall__ItemActionButton--join-call-disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.CallsNewCall__ItemActionButtonTooltip {
|
||||
@include tooltip;
|
||||
max-width: 212px;
|
||||
}
|
||||
|
||||
.CallsNewCall__ItemIcon {
|
||||
display: block;
|
||||
width: 20px;
|
||||
|
|
|
@ -200,6 +200,7 @@ import { deriveStorageServiceKey } from './Crypto';
|
|||
import { getThemeType } from './util/getThemeType';
|
||||
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
||||
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
|
||||
import { CallMode } from './types/Calling';
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
|
@ -2923,6 +2924,7 @@ export async function startApp(): Promise<void> {
|
|||
conversationId
|
||||
);
|
||||
window.reduxActions.calling.peekNotConnectedGroupCall({
|
||||
callMode: CallMode.Group,
|
||||
conversationId,
|
||||
});
|
||||
if (callId != null) {
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
GroupCallStatus,
|
||||
isSameCallHistoryGroup,
|
||||
} from '../types/CallDisposition';
|
||||
import { formatDateTimeShort } from '../util/timestamp';
|
||||
import { formatDateTimeShort, isMoreRecentThan } from '../util/timestamp';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import * as log from '../logging/log';
|
||||
import { refMerger } from '../util/refMerger';
|
||||
|
@ -39,16 +39,32 @@ import { UserText } from './UserText';
|
|||
import { I18n } from './I18n';
|
||||
import { NavSidebarSearchHeader } from './NavSidebar';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||
import { formatCallHistoryGroup } from '../util/callDisposition';
|
||||
import {
|
||||
formatCallHistoryGroup,
|
||||
getCallIdFromEra,
|
||||
} from '../util/callDisposition';
|
||||
import { CallsNewCallButton } from './CallsNewCall';
|
||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
import { Theme } from '../util/theme';
|
||||
import type { CallingConversationType } from '../types/Calling';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import type { CallLinkType } from '../types/CallLink';
|
||||
import {
|
||||
callLinkToConversation,
|
||||
getPlaceholderCallLinkConversation,
|
||||
} from '../util/callLinks';
|
||||
import type { CallStateType } from '../state/selectors/calling';
|
||||
import {
|
||||
isGroupOrAdhocCallMode,
|
||||
isGroupOrAdhocCallState,
|
||||
} from '../util/isGroupOrAdhocCall';
|
||||
import { isAnybodyInGroupCall } from '../state/ducks/callingHelpers';
|
||||
import type {
|
||||
ActiveCallStateType,
|
||||
PeekNotConnectedGroupCallType,
|
||||
} from '../state/ducks/calling';
|
||||
import { DAY, MINUTE, SECOND } from '../util/durations';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
|
||||
function Timestamp({
|
||||
i18n,
|
||||
|
@ -109,7 +125,7 @@ const defaultPendingState: SearchState = {
|
|||
};
|
||||
|
||||
type CallsListProps = Readonly<{
|
||||
hasActiveCall: boolean;
|
||||
activeCall: ActiveCallStateType | undefined;
|
||||
getCallHistoryGroupsCount: (
|
||||
options: CallHistoryFilterOptions
|
||||
) => Promise<number>;
|
||||
|
@ -118,8 +134,11 @@ type CallsListProps = Readonly<{
|
|||
pagination: CallHistoryPagination
|
||||
) => Promise<Array<CallHistoryGroup>>;
|
||||
callHistoryEdition: number;
|
||||
getAdhocCall: (roomId: string) => CallStateType | undefined;
|
||||
getCall: (id: string) => CallStateType | undefined;
|
||||
getCallLink: (id: string) => CallLinkType | undefined;
|
||||
getConversation: (id: string) => ConversationType | void;
|
||||
hangUpActiveCall: (reason: string) => void;
|
||||
i18n: LocalizerType;
|
||||
selectedCallHistoryGroup: CallHistoryGroup | null;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
|
@ -128,10 +147,17 @@ type CallsListProps = Readonly<{
|
|||
conversationId: string,
|
||||
selectedCallHistoryGroup: CallHistoryGroup
|
||||
) => void;
|
||||
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
|
||||
startCallLinkLobbyByRoomId: (roomId: string) => void;
|
||||
togglePip: () => void;
|
||||
}>;
|
||||
|
||||
const CALL_LIST_ITEM_ROW_HEIGHT = 62;
|
||||
const INACTIVE_CALL_LINKS_TO_PEEK = 10;
|
||||
const INACTIVE_CALL_LINK_AGE_THRESHOLD = 10 * DAY;
|
||||
const INACTIVE_CALL_LINK_PEEK_INTERVAL = 5 * MINUTE;
|
||||
const PEEK_BATCH_COUNT = 10;
|
||||
const PEEK_QUEUE_INTERVAL = 30 * SECOND;
|
||||
|
||||
function rowHeight() {
|
||||
return CALL_LIST_ITEM_ROW_HEIGHT;
|
||||
|
@ -145,35 +171,320 @@ function isSameOptions(
|
|||
}
|
||||
|
||||
export function CallsList({
|
||||
hasActiveCall,
|
||||
activeCall,
|
||||
getCallHistoryGroupsCount,
|
||||
getCallHistoryGroups,
|
||||
callHistoryEdition,
|
||||
getAdhocCall,
|
||||
getCall,
|
||||
getCallLink,
|
||||
getConversation,
|
||||
hangUpActiveCall,
|
||||
i18n,
|
||||
selectedCallHistoryGroup,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
onSelectCallHistoryGroup,
|
||||
peekNotConnectedGroupCall,
|
||||
startCallLinkLobbyByRoomId,
|
||||
togglePip,
|
||||
}: CallsListProps): JSX.Element {
|
||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||
const listRef = useRef<List>(null);
|
||||
const [queryInput, setQueryInput] = useState('');
|
||||
const [status, setStatus] = useState(CallHistoryFilterStatus.All);
|
||||
const [searchState, setSearchState] = useState(defaultInitState);
|
||||
const [isLeaveCallDialogVisible, setIsLeaveCallDialogVisible] =
|
||||
useState(false);
|
||||
|
||||
const prevOptionsRef = useRef<CallHistoryFilterOptions | null>(null);
|
||||
|
||||
const getCallHistoryGroupsCountRef = useRef(getCallHistoryGroupsCount);
|
||||
const getCallHistoryGroupsRef = useRef(getCallHistoryGroups);
|
||||
|
||||
const searchStateItemsRef = useRef<ReadonlyArray<CallHistoryGroup> | null>(
|
||||
null
|
||||
);
|
||||
const peekQueueRef = useRef<Set<string>>(new Set());
|
||||
const peekQueueArgsRef = useRef<Map<string, PeekNotConnectedGroupCallType>>(
|
||||
new Map()
|
||||
);
|
||||
const inactiveCallLinksPeekedAtRef = useRef<Map<string, number>>(new Map());
|
||||
|
||||
const peekQueueTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
function clearPeekQueueTimer() {
|
||||
if (peekQueueTimerRef.current != null) {
|
||||
clearInterval(peekQueueTimerRef.current);
|
||||
peekQueueTimerRef.current = null;
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearPeekQueueTimer();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getCallHistoryGroupsCountRef.current = getCallHistoryGroupsCount;
|
||||
getCallHistoryGroupsRef.current = getCallHistoryGroups;
|
||||
}, [getCallHistoryGroupsCount, getCallHistoryGroups]);
|
||||
|
||||
const getConversationForItem = useCallback(
|
||||
(item: CallHistoryGroup | null): CallingConversationType | null => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAdhoc = item?.type === CallType.Adhoc;
|
||||
if (isAdhoc) {
|
||||
const callLink = isAdhoc ? getCallLink(item.peerId) : null;
|
||||
if (callLink) {
|
||||
return callLinkToConversation(callLink, i18n);
|
||||
}
|
||||
return getPlaceholderCallLinkConversation(item.peerId, i18n);
|
||||
}
|
||||
|
||||
return getConversation(item.peerId) ?? null;
|
||||
},
|
||||
[getCallLink, getConversation, i18n]
|
||||
);
|
||||
|
||||
const getCallByPeerId = useCallback(
|
||||
({
|
||||
mode,
|
||||
peerId,
|
||||
}: {
|
||||
mode: CallMode | undefined;
|
||||
peerId: string | undefined;
|
||||
}): CallStateType | undefined => {
|
||||
if (!peerId || !mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === CallMode.Adhoc) {
|
||||
return getAdhocCall(peerId);
|
||||
}
|
||||
|
||||
const conversation = getConversation(peerId);
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getCall(conversation.id);
|
||||
},
|
||||
[getAdhocCall, getCall, getConversation]
|
||||
);
|
||||
|
||||
const getIsCallActive = useCallback(
|
||||
({
|
||||
callHistoryGroup,
|
||||
}: {
|
||||
callHistoryGroup: CallHistoryGroup | null;
|
||||
}): boolean => {
|
||||
if (!callHistoryGroup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { mode, peerId } = callHistoryGroup;
|
||||
const call = getCallByPeerId({ mode, peerId });
|
||||
if (!call) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isGroupOrAdhocCallState(call)) {
|
||||
if (!isAnybodyInGroupCall(call.peekInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode === CallMode.Group) {
|
||||
const eraId = call.peekInfo?.eraId;
|
||||
if (!eraId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const callId = getCallIdFromEra(eraId);
|
||||
return callHistoryGroup.children.some(
|
||||
groupItem => groupItem.callId === callId
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Direct is not supported currently
|
||||
return false;
|
||||
},
|
||||
[getCallByPeerId]
|
||||
);
|
||||
|
||||
const getIsInCall = useCallback(
|
||||
({
|
||||
activeCallConversationId,
|
||||
callHistoryGroup,
|
||||
conversation,
|
||||
isActive,
|
||||
}: {
|
||||
activeCallConversationId: string | undefined;
|
||||
callHistoryGroup: CallHistoryGroup | null;
|
||||
conversation: CallingConversationType | null;
|
||||
isActive: boolean;
|
||||
}): boolean => {
|
||||
if (!callHistoryGroup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { mode, peerId } = callHistoryGroup;
|
||||
|
||||
if (mode === CallMode.Adhoc) {
|
||||
return peerId === activeCallConversationId;
|
||||
}
|
||||
|
||||
// Not supported currently
|
||||
if (mode === CallMode.Direct) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Group
|
||||
return Boolean(
|
||||
isActive &&
|
||||
conversation &&
|
||||
conversation?.id === activeCallConversationId
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// If the call is already enqueued then this is a no op.
|
||||
const maybeEnqueueCallPeek = useCallback((item: CallHistoryGroup): void => {
|
||||
const { mode: callMode, peerId } = item;
|
||||
const queue = peekQueueRef.current;
|
||||
if (queue.has(peerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGroupOrAdhocCallMode(callMode)) {
|
||||
peekQueueArgsRef.current.set(peerId, {
|
||||
callMode,
|
||||
conversationId: peerId,
|
||||
});
|
||||
queue.add(peerId);
|
||||
} else {
|
||||
log.error(`Trying to peek unsupported call mode ${callMode}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Get the oldest inserted peerIds by iterating the Set in insertion order.
|
||||
const getPeerIdsToPeek = useCallback((): ReadonlyArray<string> => {
|
||||
const peerIds: Array<string> = [];
|
||||
for (const peerId of peekQueueRef.current) {
|
||||
peerIds.push(peerId);
|
||||
if (peerIds.length === PEEK_BATCH_COUNT) {
|
||||
return peerIds;
|
||||
}
|
||||
}
|
||||
|
||||
return peerIds;
|
||||
}, []);
|
||||
|
||||
const doCallPeeks = useCallback((): void => {
|
||||
const peerIds = getPeerIdsToPeek();
|
||||
for (const peerId of peerIds) {
|
||||
const peekArgs = peekQueueArgsRef.current.get(peerId);
|
||||
if (peekArgs) {
|
||||
inactiveCallLinksPeekedAtRef.current.set(peerId, new Date().getTime());
|
||||
peekNotConnectedGroupCall(peekArgs);
|
||||
}
|
||||
|
||||
peekQueueRef.current.delete(peerId);
|
||||
peekQueueArgsRef.current.delete(peerId);
|
||||
}
|
||||
}, [getPeerIdsToPeek, peekNotConnectedGroupCall]);
|
||||
|
||||
const enqueueCallPeeks = useCallback(
|
||||
(callItems: ReadonlyArray<CallHistoryGroup>, isFirstRun: boolean): void => {
|
||||
let peekCount = 0;
|
||||
let inactiveCallLinksToPeek = 0;
|
||||
for (const item of callItems) {
|
||||
const { mode } = item;
|
||||
if (isGroupOrAdhocCallMode(mode)) {
|
||||
const isActive = getIsCallActive({ callHistoryGroup: item });
|
||||
|
||||
if (isActive) {
|
||||
// Don't peek if you're already in the call.
|
||||
const activeCallConversationId = activeCall?.conversationId;
|
||||
if (activeCallConversationId) {
|
||||
const conversation = getConversationForItem(item);
|
||||
const isInCall = getIsInCall({
|
||||
activeCallConversationId,
|
||||
callHistoryGroup: item,
|
||||
conversation,
|
||||
isActive,
|
||||
});
|
||||
if (isInCall) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
maybeEnqueueCallPeek(item);
|
||||
peekCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
mode === CallMode.Adhoc &&
|
||||
isFirstRun &&
|
||||
inactiveCallLinksToPeek < INACTIVE_CALL_LINKS_TO_PEEK &&
|
||||
isMoreRecentThan(item.timestamp, INACTIVE_CALL_LINK_AGE_THRESHOLD)
|
||||
) {
|
||||
const peekedAt = inactiveCallLinksPeekedAtRef.current.get(
|
||||
item.peerId
|
||||
);
|
||||
if (
|
||||
peekedAt &&
|
||||
isMoreRecentThan(peekedAt, INACTIVE_CALL_LINK_PEEK_INTERVAL)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
maybeEnqueueCallPeek(item);
|
||||
inactiveCallLinksToPeek += 1;
|
||||
peekCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (peekCount === 0) {
|
||||
return;
|
||||
}
|
||||
log.info(`Found ${peekCount} calls to peek.`);
|
||||
|
||||
if (peekQueueTimerRef.current != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Starting background call peek.');
|
||||
peekQueueTimerRef.current = setInterval(() => {
|
||||
if (searchStateItemsRef.current) {
|
||||
enqueueCallPeeks(searchStateItemsRef.current, false);
|
||||
}
|
||||
|
||||
if (peekQueueRef.current.size > 0) {
|
||||
doCallPeeks();
|
||||
}
|
||||
}, PEEK_QUEUE_INTERVAL);
|
||||
|
||||
doCallPeeks();
|
||||
},
|
||||
[
|
||||
activeCall?.conversationId,
|
||||
doCallPeeks,
|
||||
getConversationForItem,
|
||||
getIsCallActive,
|
||||
getIsInCall,
|
||||
maybeEnqueueCallPeek,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
|
@ -219,6 +530,11 @@ export function CallsList({
|
|||
return;
|
||||
}
|
||||
|
||||
if (results) {
|
||||
enqueueCallPeeks(results.items, true);
|
||||
searchStateItemsRef.current = results.items;
|
||||
}
|
||||
|
||||
// Only commit the new search state once the results are ready
|
||||
setSearchState({
|
||||
state: results == null ? 'rejected' : 'fulfilled',
|
||||
|
@ -246,7 +562,7 @@ export function CallsList({
|
|||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [queryInput, status, callHistoryEdition]);
|
||||
}, [queryInput, status, callHistoryEdition, enqueueCallPeeks]);
|
||||
|
||||
const loadMoreRows = useCallback(
|
||||
async (props: IndexRange) => {
|
||||
|
@ -279,6 +595,8 @@ export function CallsList({
|
|||
return;
|
||||
}
|
||||
|
||||
enqueueCallPeeks(groups, false);
|
||||
|
||||
setSearchState(prevSearchState => {
|
||||
strictAssert(
|
||||
prevSearchState.results != null,
|
||||
|
@ -286,6 +604,7 @@ export function CallsList({
|
|||
);
|
||||
const newItems = prevSearchState.results.items.slice();
|
||||
newItems.splice(startIndex, stopIndex, ...groups);
|
||||
searchStateItemsRef.current = newItems;
|
||||
return {
|
||||
...prevSearchState,
|
||||
results: {
|
||||
|
@ -298,27 +617,7 @@ export function CallsList({
|
|||
log.error('CallsList#loadMoreRows error fetching', error);
|
||||
}
|
||||
},
|
||||
[searchState]
|
||||
);
|
||||
|
||||
const getConversationForItem = useCallback(
|
||||
(item: CallHistoryGroup | null): CallingConversationType | null => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAdhoc = item?.type === CallType.Adhoc;
|
||||
if (isAdhoc) {
|
||||
const callLink = isAdhoc ? getCallLink(item.peerId) : null;
|
||||
if (callLink) {
|
||||
return callLinkToConversation(callLink, i18n);
|
||||
}
|
||||
return getPlaceholderCallLinkConversation(item.peerId, i18n);
|
||||
}
|
||||
|
||||
return getConversation(item.peerId) ?? null;
|
||||
},
|
||||
[getCallLink, getConversation, i18n]
|
||||
[enqueueCallPeeks, searchState]
|
||||
);
|
||||
|
||||
const isRowLoaded = useCallback(
|
||||
|
@ -332,10 +631,23 @@ export function CallsList({
|
|||
({ key, index, style }: ListRowProps) => {
|
||||
const item = searchState.results?.items.at(index) ?? null;
|
||||
const conversation = getConversationForItem(item);
|
||||
const activeCallConversationId = activeCall?.conversationId;
|
||||
|
||||
const isActive = getIsCallActive({
|
||||
callHistoryGroup: item,
|
||||
});
|
||||
const isInCall = getIsInCall({
|
||||
activeCallConversationId,
|
||||
callHistoryGroup: item,
|
||||
conversation,
|
||||
isActive,
|
||||
});
|
||||
|
||||
const isAdhoc = item?.type === CallType.Adhoc;
|
||||
const isNewCallVisible = Boolean(
|
||||
const isCallButtonVisible = Boolean(
|
||||
!isAdhoc || (isAdhoc && getCallLink(item.peerId))
|
||||
);
|
||||
const isActiveVisible = Boolean(isCallButtonVisible && item && isActive);
|
||||
|
||||
if (
|
||||
searchState.state === 'pending' ||
|
||||
|
@ -410,12 +722,20 @@ export function CallsList({
|
|||
/>
|
||||
}
|
||||
trailing={
|
||||
isNewCallVisible ? (
|
||||
isCallButtonVisible ? (
|
||||
<CallsNewCallButton
|
||||
callType={item.type}
|
||||
hasActiveCall={hasActiveCall}
|
||||
isActive={isActiveVisible}
|
||||
isInCall={isInCall}
|
||||
isEnabled={isInCall || !activeCall}
|
||||
onClick={() => {
|
||||
if (isAdhoc) {
|
||||
if (isInCall) {
|
||||
togglePip();
|
||||
} else if (activeCall) {
|
||||
if (isActiveVisible) {
|
||||
setIsLeaveCallDialogVisible(true);
|
||||
}
|
||||
} else if (isAdhoc) {
|
||||
startCallLinkLobbyByRoomId(item.peerId);
|
||||
} else if (conversation) {
|
||||
if (item.type === CallType.Audio) {
|
||||
|
@ -425,6 +745,7 @@ export function CallsList({
|
|||
}
|
||||
}
|
||||
}}
|
||||
i18n={i18n}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
|
@ -441,7 +762,11 @@ export function CallsList({
|
|||
<span className="CallsList__ItemCallInfo">
|
||||
{item.children.length > 1 ? `(${item.children.length}) ` : ''}
|
||||
{statusText} ·{' '}
|
||||
<Timestamp i18n={i18n} timestamp={item.timestamp} />
|
||||
{isActiveVisible ? (
|
||||
i18n('icu:CallsList__ItemCallInfo--Active')
|
||||
) : (
|
||||
<Timestamp i18n={i18n} timestamp={item.timestamp} />
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
onClick={() => {
|
||||
|
@ -459,15 +784,18 @@ export function CallsList({
|
|||
);
|
||||
},
|
||||
[
|
||||
hasActiveCall,
|
||||
activeCall,
|
||||
searchState,
|
||||
getCallLink,
|
||||
getConversationForItem,
|
||||
getIsCallActive,
|
||||
getIsInCall,
|
||||
selectedCallHistoryGroup,
|
||||
onSelectCallHistoryGroup,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
startCallLinkLobbyByRoomId,
|
||||
togglePip,
|
||||
i18n,
|
||||
]
|
||||
);
|
||||
|
@ -498,6 +826,31 @@ export function CallsList({
|
|||
|
||||
return (
|
||||
<>
|
||||
{isLeaveCallDialogVisible && (
|
||||
<ConfirmationDialog
|
||||
dialogName="GroupCallRemoteParticipant.blockInfo"
|
||||
cancelText={i18n('icu:cancel')}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setIsLeaveCallDialogVisible(false);
|
||||
}}
|
||||
title={i18n('icu:CallsList__LeaveCallDialogTitle')}
|
||||
actions={[
|
||||
{
|
||||
text: i18n('icu:CallsList__LeaveCallDialogButton--leave'),
|
||||
style: 'affirmative',
|
||||
action: () => {
|
||||
hangUpActiveCall(
|
||||
'Calls Tab leave active call to join different call'
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n('icu:CallsList__LeaveCallDialogBody')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
<NavSidebarSearchHeader>
|
||||
<SearchInput
|
||||
i18n={i18n}
|
||||
|
|
|
@ -6,6 +6,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||
import { partition } from 'lodash';
|
||||
import type { ListRowProps } from 'react-virtualized';
|
||||
import { List } from 'react-virtualized';
|
||||
import classNames from 'classnames';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { SearchInput } from './SearchInput';
|
||||
|
@ -18,6 +19,7 @@ import { Avatar, AvatarSize } from './Avatar';
|
|||
import { I18n } from './I18n';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||
import { CallType } from '../types/CallDisposition';
|
||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
|
||||
type CallsNewCallProps = Readonly<{
|
||||
hasActiveCall: boolean;
|
||||
|
@ -35,33 +37,69 @@ type Row =
|
|||
|
||||
export function CallsNewCallButton({
|
||||
callType,
|
||||
hasActiveCall,
|
||||
isEnabled,
|
||||
isActive,
|
||||
isInCall,
|
||||
i18n,
|
||||
onClick,
|
||||
}: {
|
||||
callType: CallType;
|
||||
hasActiveCall: boolean;
|
||||
isActive: boolean;
|
||||
isEnabled: boolean;
|
||||
isInCall: boolean;
|
||||
i18n: LocalizerType;
|
||||
onClick: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
let innerContent: React.ReactNode | string;
|
||||
let tooltipContent = '';
|
||||
if (callType === CallType.Audio) {
|
||||
innerContent = (
|
||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Phone" />
|
||||
);
|
||||
} else if (isActive) {
|
||||
innerContent = isInCall
|
||||
? i18n('icu:CallsNewCallButton--return')
|
||||
: i18n('icu:joinOngoingCall');
|
||||
if (!isEnabled) {
|
||||
tooltipContent = i18n('icu:CallsNewCallButtonTooltip--in-another-call');
|
||||
}
|
||||
} else {
|
||||
innerContent = (
|
||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Video" />
|
||||
);
|
||||
}
|
||||
|
||||
const buttonContent = (
|
||||
<button
|
||||
type="button"
|
||||
className="CallsNewCall__ItemActionButton"
|
||||
aria-disabled={hasActiveCall}
|
||||
className={classNames(
|
||||
'CallsNewCall__ItemActionButton',
|
||||
isActive ? 'CallsNewCall__ItemActionButton--join-call' : undefined,
|
||||
isEnabled
|
||||
? undefined
|
||||
: 'CallsNewCall__ItemActionButton--join-call-disabled'
|
||||
)}
|
||||
aria-label={tooltipContent}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
if (!hasActiveCall) {
|
||||
onClick();
|
||||
}
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{callType === CallType.Audio && (
|
||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Phone" />
|
||||
)}
|
||||
{callType !== CallType.Audio && (
|
||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Video" />
|
||||
)}
|
||||
{innerContent}
|
||||
</button>
|
||||
);
|
||||
|
||||
return tooltipContent === '' ? (
|
||||
buttonContent
|
||||
) : (
|
||||
<Tooltip
|
||||
className="CallsNewCall__ItemActionButtonTooltip"
|
||||
content={tooltipContent}
|
||||
direction={TooltipPlacement.Top}
|
||||
>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function CallsNewCall({
|
||||
|
@ -173,6 +211,8 @@ export function CallsNewCall({
|
|||
);
|
||||
}
|
||||
|
||||
const isNewCallEnabled = !hasActiveCall;
|
||||
|
||||
return (
|
||||
<div key={key} style={style}>
|
||||
<ListTile
|
||||
|
@ -195,19 +235,29 @@ export function CallsNewCall({
|
|||
{item.conversation.type === 'direct' && (
|
||||
<CallsNewCallButton
|
||||
callType={CallType.Audio}
|
||||
hasActiveCall={hasActiveCall}
|
||||
isActive={false}
|
||||
isEnabled={isNewCallEnabled}
|
||||
isInCall={false}
|
||||
onClick={() => {
|
||||
onOutgoingAudioCallInConversation(item.conversation.id);
|
||||
if (isNewCallEnabled) {
|
||||
onOutgoingAudioCallInConversation(item.conversation.id);
|
||||
}
|
||||
}}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
<CallsNewCallButton
|
||||
// It's okay if this is a group
|
||||
callType={CallType.Video}
|
||||
hasActiveCall={hasActiveCall}
|
||||
isActive={false}
|
||||
isEnabled={isNewCallEnabled}
|
||||
isInCall={false}
|
||||
onClick={() => {
|
||||
onOutgoingVideoCallInConversation(item.conversation.id);
|
||||
if (isNewCallEnabled) {
|
||||
onOutgoingVideoCallInConversation(item.conversation.id);
|
||||
}
|
||||
}}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -13,12 +13,16 @@ import type {
|
|||
} from '../types/CallDisposition';
|
||||
import { CallsNewCall } from './CallsNewCall';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
import type { ActiveCallStateType } from '../state/ducks/calling';
|
||||
import type {
|
||||
ActiveCallStateType,
|
||||
PeekNotConnectedGroupCallType,
|
||||
} from '../state/ducks/calling';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
import type { CallLinkType } from '../types/CallLink';
|
||||
import type { CallStateType } from '../state/selectors/calling';
|
||||
|
||||
enum CallsTabSidebarView {
|
||||
CallsListView,
|
||||
|
@ -37,8 +41,11 @@ type CallsTabProps = Readonly<{
|
|||
pagination: CallHistoryPagination
|
||||
) => Promise<Array<CallHistoryGroup>>;
|
||||
callHistoryEdition: number;
|
||||
getAdhocCall: (roomId: string) => CallStateType | undefined;
|
||||
getCall: (id: string) => CallStateType | undefined;
|
||||
getCallLink: (id: string) => CallLinkType | undefined;
|
||||
getConversation: (id: string) => ConversationType | void;
|
||||
hangUpActiveCall: (reason: string) => void;
|
||||
hasFailedStorySends: boolean;
|
||||
hasPendingUpdate: boolean;
|
||||
i18n: LocalizerType;
|
||||
|
@ -48,6 +55,7 @@ type CallsTabProps = Readonly<{
|
|||
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
|
||||
preferredLeftPaneWidth: number;
|
||||
renderConversationDetails: (
|
||||
conversationId: string,
|
||||
|
@ -59,6 +67,7 @@ type CallsTabProps = Readonly<{
|
|||
regionCode: string | undefined;
|
||||
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
|
||||
startCallLinkLobbyByRoomId: (roomId: string) => void;
|
||||
togglePip: () => void;
|
||||
}>;
|
||||
|
||||
export function CallsTab({
|
||||
|
@ -68,8 +77,11 @@ export function CallsTab({
|
|||
getCallHistoryGroupsCount,
|
||||
getCallHistoryGroups,
|
||||
callHistoryEdition,
|
||||
getAdhocCall,
|
||||
getCall,
|
||||
getCallLink,
|
||||
getConversation,
|
||||
hangUpActiveCall,
|
||||
hasFailedStorySends,
|
||||
hasPendingUpdate,
|
||||
i18n,
|
||||
|
@ -79,12 +91,14 @@ export function CallsTab({
|
|||
onToggleNavTabsCollapse,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
peekNotConnectedGroupCall,
|
||||
preferredLeftPaneWidth,
|
||||
renderConversationDetails,
|
||||
renderToastManager,
|
||||
regionCode,
|
||||
savePreferredLeftPaneWidth,
|
||||
startCallLinkLobbyByRoomId,
|
||||
togglePip,
|
||||
}: CallsTabProps): JSX.Element {
|
||||
const [sidebarView, setSidebarView] = useState(
|
||||
CallsTabSidebarView.CallsListView
|
||||
|
@ -231,12 +245,15 @@ export function CallsTab({
|
|||
{sidebarView === CallsTabSidebarView.CallsListView && (
|
||||
<CallsList
|
||||
key={CallsTabSidebarView.CallsListView}
|
||||
hasActiveCall={activeCall != null}
|
||||
activeCall={activeCall}
|
||||
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
|
||||
getCallHistoryGroups={getCallHistoryGroups}
|
||||
callHistoryEdition={callHistoryEdition}
|
||||
getAdhocCall={getAdhocCall}
|
||||
getCall={getCall}
|
||||
getCallLink={getCallLink}
|
||||
getConversation={getConversation}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
i18n={i18n}
|
||||
selectedCallHistoryGroup={selected?.callHistoryGroup ?? null}
|
||||
onSelectCallHistoryGroup={handleSelectCallHistoryGroup}
|
||||
|
@ -246,7 +263,9 @@ export function CallsTab({
|
|||
onOutgoingVideoCallInConversation={
|
||||
handleOutgoingVideoCallInConversation
|
||||
}
|
||||
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
|
||||
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
|
||||
togglePip={togglePip}
|
||||
/>
|
||||
)}
|
||||
{sidebarView === CallsTabSidebarView.NewCallView && (
|
||||
|
|
|
@ -2369,6 +2369,7 @@ export class CallingClass {
|
|||
|
||||
if (update === RingUpdate.Requested) {
|
||||
this.reduxInterface?.peekNotConnectedGroupCall({
|
||||
callMode: CallMode.Group,
|
||||
conversationId: conversation.id,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -285,7 +285,8 @@ type SendGroupCallReactionLocalCopyType = ReadonlyDeep<{
|
|||
timestamp: number;
|
||||
}>;
|
||||
|
||||
type PeekNotConnectedGroupCallType = ReadonlyDeep<{
|
||||
export type PeekNotConnectedGroupCallType = ReadonlyDeep<{
|
||||
callMode: CallMode.Group | CallMode.Adhoc;
|
||||
conversationId: string;
|
||||
}>;
|
||||
|
||||
|
@ -1368,6 +1369,48 @@ function handleCallLinkUpdate(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* When starting a lobby and there's an active call, if we're already in call then
|
||||
* focus it (toggle pip), otherwise show an error.
|
||||
* @returns {boolean} `true` if there was an active call and we handled it.
|
||||
*/
|
||||
function handleActiveCallOnStartLobby({
|
||||
conversationId,
|
||||
state,
|
||||
dispatch,
|
||||
}: {
|
||||
conversationId: string;
|
||||
state: RootStateType;
|
||||
dispatch: ThunkDispatch<
|
||||
RootStateType,
|
||||
unknown,
|
||||
ShowErrorModalActionType | TogglePipActionType
|
||||
>;
|
||||
}): boolean {
|
||||
const { activeCallState } = state.calling;
|
||||
if (!activeCallState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeCallState.conversationId === conversationId) {
|
||||
dispatch({
|
||||
type: TOGGLE_PIP,
|
||||
});
|
||||
} else {
|
||||
const i18n = getIntl(state);
|
||||
dispatch({
|
||||
type: SHOW_ERROR_MODAL,
|
||||
payload: {
|
||||
title: i18n('icu:calling__cant-join'),
|
||||
description: i18n('icu:calling__dialog-already-in-call'),
|
||||
buttonVariant: ButtonVariant.Primary,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function hangUpActiveCall(
|
||||
reason: string
|
||||
): ThunkAction<void, RootStateType, unknown, HangUpActionType> {
|
||||
|
@ -1553,10 +1596,10 @@ function peekNotConnectedGroupCall(
|
|||
payload: PeekNotConnectedGroupCallType
|
||||
): ThunkAction<void, RootStateType, unknown, PeekGroupCallFulfilledActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const { conversationId } = payload;
|
||||
const { callMode, conversationId } = payload;
|
||||
doGroupCallPeek({
|
||||
conversationId,
|
||||
callMode: CallMode.Group,
|
||||
callMode,
|
||||
dispatch,
|
||||
getState,
|
||||
});
|
||||
|
@ -1868,27 +1911,22 @@ const _startCallLinkLobby = async ({
|
|||
dispatch: ThunkDispatch<
|
||||
RootStateType,
|
||||
unknown,
|
||||
StartCallLinkLobbyActionType | ShowErrorModalActionType
|
||||
| StartCallLinkLobbyActionType
|
||||
| ShowErrorModalActionType
|
||||
| TogglePipActionType
|
||||
>;
|
||||
getState: () => RootStateType;
|
||||
}) => {
|
||||
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||
const state = getState();
|
||||
|
||||
if (state.calling.activeCallState) {
|
||||
const i18n = getIntl(getState());
|
||||
dispatch({
|
||||
type: SHOW_ERROR_MODAL,
|
||||
payload: {
|
||||
title: i18n('icu:calling__cant-join'),
|
||||
description: i18n('icu:calling__dialog-already-in-call'),
|
||||
buttonVariant: ButtonVariant.Primary,
|
||||
},
|
||||
});
|
||||
if (
|
||||
handleActiveCallOnStartLobby({ conversationId: roomId, state, dispatch })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||
|
||||
const readResult = await calling.readCallLink({ callLinkRootKey });
|
||||
const { callLinkState } = readResult;
|
||||
if (!callLinkState) {
|
||||
|
@ -1919,7 +1957,6 @@ const _startCallLinkLobby = async ({
|
|||
return;
|
||||
}
|
||||
|
||||
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||
try {
|
||||
const callLinkExists = await dataInterface.callLinkExists(roomId);
|
||||
if (callLinkExists) {
|
||||
|
@ -1983,7 +2020,7 @@ function startCallingLobby({
|
|||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
StartCallingLobbyActionType
|
||||
StartCallingLobbyActionType | TogglePipActionType
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
|
|
@ -46,3 +46,12 @@ export const isAnybodyElseInGroupCall = (
|
|||
peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>,
|
||||
ourAci: AciString
|
||||
): boolean => Boolean(peekInfo?.acis.some(id => id !== ourAci));
|
||||
|
||||
export const isAnybodyInGroupCall = (
|
||||
peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>
|
||||
): boolean => {
|
||||
if (!peekInfo?.acis) {
|
||||
return false;
|
||||
}
|
||||
return peekInfo.acis.length > 0;
|
||||
};
|
||||
|
|
|
@ -25,7 +25,12 @@ import type { ConversationType } from '../ducks/conversations';
|
|||
import { SmartConversationDetails } from './ConversationDetails';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import { useCallingActions } from '../ducks/calling';
|
||||
import { getActiveCallState, getCallLinkSelector } from '../selectors/calling';
|
||||
import {
|
||||
getActiveCallState,
|
||||
getAdhocCallSelector,
|
||||
getCallSelector,
|
||||
getCallLinkSelector,
|
||||
} from '../selectors/calling';
|
||||
import { useCallHistoryActions } from '../ducks/callHistory';
|
||||
import { getCallHistoryEdition } from '../selectors/callHistory';
|
||||
import { getHasPendingUpdate } from '../selectors/updates';
|
||||
|
@ -97,6 +102,8 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
const allConversations = useSelector(getAllConversations);
|
||||
const regionCode = useSelector(getRegionCode);
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
const getAdhocCall = useSelector(getAdhocCallSelector);
|
||||
const getCall = useSelector(getCallSelector);
|
||||
const getCallLink = useSelector(getCallLinkSelector);
|
||||
|
||||
const activeCall = useSelector(getActiveCallState);
|
||||
|
@ -107,9 +114,12 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
|
||||
|
||||
const {
|
||||
hangUpActiveCall,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
peekNotConnectedGroupCall,
|
||||
startCallLinkLobbyByRoomId,
|
||||
togglePip,
|
||||
} = useCallingActions();
|
||||
const {
|
||||
clearAllCallHistory: clearCallHistory,
|
||||
|
@ -169,8 +179,11 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
getConversation={getConversation}
|
||||
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
|
||||
getCallHistoryGroups={getCallHistoryGroups}
|
||||
getAdhocCall={getAdhocCall}
|
||||
getCall={getCall}
|
||||
getCallLink={getCallLink}
|
||||
callHistoryEdition={callHistoryEdition}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
hasFailedStorySends={hasFailedStorySends}
|
||||
hasPendingUpdate={hasPendingUpdate}
|
||||
i18n={i18n}
|
||||
|
@ -180,12 +193,14 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
onToggleNavTabsCollapse={toggleNavTabsCollapse}
|
||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
|
||||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
renderConversationDetails={renderConversationDetails}
|
||||
renderToastManager={renderToastManager}
|
||||
regionCode={regionCode}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
|
||||
togglePip={togglePip}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1498,6 +1498,7 @@ describe('calling duck', () => {
|
|||
const dispatch = sinon.spy();
|
||||
|
||||
await peekNotConnectedGroupCall({
|
||||
callMode: CallMode.Group,
|
||||
conversationId: 'fake-group-call-conversation-id',
|
||||
})(
|
||||
dispatch,
|
||||
|
|
|
@ -3906,5 +3906,45 @@
|
|||
"line": " const imageDataCache = React.useRef<CallingImageDataCache>(new Map());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-05-06T20:18:59.647Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallsList.tsx",
|
||||
"line": " const searchStateItemsRef = useRef<ReadonlyArray<CallHistoryGroup> | null>(",
|
||||
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||
"updated": "2024-05-16T02:10:00.652Z",
|
||||
"reasonDetail": "<optional>"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallsList.tsx",
|
||||
"line": " const peekQueueRef = useRef<Set<string>>(new Set());",
|
||||
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||
"updated": "2024-05-16T02:10:00.652Z",
|
||||
"reasonDetail": "<optional>"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallsList.tsx",
|
||||
"line": " const peekQueueArgsRef = useRef<Map<string, PeekNotConnectedGroupCallType>>(",
|
||||
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||
"updated": "2024-05-16T02:10:00.652Z",
|
||||
"reasonDetail": "<optional>"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallsList.tsx",
|
||||
"line": " const inactiveCallLinksPeekedAtRef = useRef<Map<string, number>>(new Map());",
|
||||
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||
"updated": "2024-05-16T02:10:00.652Z",
|
||||
"reasonDetail": "<optional>"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallsList.tsx",
|
||||
"line": " const peekQueueTimerRef = useRef<NodeJS.Timeout | null>(null);",
|
||||
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||
"updated": "2024-05-16T02:10:00.652Z",
|
||||
"reasonDetail": "<optional>"
|
||||
}
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue