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",
|
"messageformat": "Call link",
|
||||||
"description": "On the Calls Tab, the subtitle text for a Call Link entry."
|
"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": {
|
"icu:CallsNewCall__EmptyState--noQuery": {
|
||||||
"messageformat": "No recent conversations.",
|
"messageformat": "No recent conversations.",
|
||||||
"description": "Calls Tab > New Call > Conversations List > When no results found > With no search query"
|
"description": "Calls Tab > New Call > Conversations List > When no results found > With no search query"
|
||||||
|
@ -7084,6 +7100,14 @@
|
||||||
"messageformat": "No results for “{query}”",
|
"messageformat": "No results for “{query}”",
|
||||||
"description": "Calls Tab > New Call > Conversations List > When no results found > With a search 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": {
|
"icu:CallHistory__Description--Default": {
|
||||||
"messageformat": "{direction, select, Outgoing {Outgoing} other {Incoming}} {type, select, Audio {voice} Video {video} Group {group} other {}} call",
|
"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)"
|
"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;
|
-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;
|
@include button-reset;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:not(.CallsList__ToggleFilterByMissed--pressed):hover {
|
&: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 {
|
.CallsNewCall__ItemIcon {
|
||||||
display: block;
|
display: block;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
|
|
@ -200,6 +200,7 @@ import { deriveStorageServiceKey } from './Crypto';
|
||||||
import { getThemeType } from './util/getThemeType';
|
import { getThemeType } from './util/getThemeType';
|
||||||
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
||||||
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
|
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
|
||||||
|
import { CallMode } from './types/Calling';
|
||||||
|
|
||||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||||
|
@ -2923,6 +2924,7 @@ export async function startApp(): Promise<void> {
|
||||||
conversationId
|
conversationId
|
||||||
);
|
);
|
||||||
window.reduxActions.calling.peekNotConnectedGroupCall({
|
window.reduxActions.calling.peekNotConnectedGroupCall({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId,
|
conversationId,
|
||||||
});
|
});
|
||||||
if (callId != null) {
|
if (callId != null) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import {
|
||||||
GroupCallStatus,
|
GroupCallStatus,
|
||||||
isSameCallHistoryGroup,
|
isSameCallHistoryGroup,
|
||||||
} from '../types/CallDisposition';
|
} from '../types/CallDisposition';
|
||||||
import { formatDateTimeShort } from '../util/timestamp';
|
import { formatDateTimeShort, isMoreRecentThan } from '../util/timestamp';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { refMerger } from '../util/refMerger';
|
import { refMerger } from '../util/refMerger';
|
||||||
|
@ -39,16 +39,32 @@ import { UserText } from './UserText';
|
||||||
import { I18n } from './I18n';
|
import { I18n } from './I18n';
|
||||||
import { NavSidebarSearchHeader } from './NavSidebar';
|
import { NavSidebarSearchHeader } from './NavSidebar';
|
||||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||||
import { formatCallHistoryGroup } from '../util/callDisposition';
|
import {
|
||||||
|
formatCallHistoryGroup,
|
||||||
|
getCallIdFromEra,
|
||||||
|
} from '../util/callDisposition';
|
||||||
import { CallsNewCallButton } from './CallsNewCall';
|
import { CallsNewCallButton } from './CallsNewCall';
|
||||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
import type { CallingConversationType } from '../types/Calling';
|
import type { CallingConversationType } from '../types/Calling';
|
||||||
|
import { CallMode } from '../types/Calling';
|
||||||
import type { CallLinkType } from '../types/CallLink';
|
import type { CallLinkType } from '../types/CallLink';
|
||||||
import {
|
import {
|
||||||
callLinkToConversation,
|
callLinkToConversation,
|
||||||
getPlaceholderCallLinkConversation,
|
getPlaceholderCallLinkConversation,
|
||||||
} from '../util/callLinks';
|
} 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({
|
function Timestamp({
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -109,7 +125,7 @@ const defaultPendingState: SearchState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type CallsListProps = Readonly<{
|
type CallsListProps = Readonly<{
|
||||||
hasActiveCall: boolean;
|
activeCall: ActiveCallStateType | undefined;
|
||||||
getCallHistoryGroupsCount: (
|
getCallHistoryGroupsCount: (
|
||||||
options: CallHistoryFilterOptions
|
options: CallHistoryFilterOptions
|
||||||
) => Promise<number>;
|
) => Promise<number>;
|
||||||
|
@ -118,8 +134,11 @@ type CallsListProps = Readonly<{
|
||||||
pagination: CallHistoryPagination
|
pagination: CallHistoryPagination
|
||||||
) => Promise<Array<CallHistoryGroup>>;
|
) => Promise<Array<CallHistoryGroup>>;
|
||||||
callHistoryEdition: number;
|
callHistoryEdition: number;
|
||||||
|
getAdhocCall: (roomId: string) => CallStateType | undefined;
|
||||||
|
getCall: (id: string) => CallStateType | undefined;
|
||||||
getCallLink: (id: string) => CallLinkType | undefined;
|
getCallLink: (id: string) => CallLinkType | undefined;
|
||||||
getConversation: (id: string) => ConversationType | void;
|
getConversation: (id: string) => ConversationType | void;
|
||||||
|
hangUpActiveCall: (reason: string) => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
selectedCallHistoryGroup: CallHistoryGroup | null;
|
selectedCallHistoryGroup: CallHistoryGroup | null;
|
||||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||||
|
@ -128,10 +147,17 @@ type CallsListProps = Readonly<{
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
selectedCallHistoryGroup: CallHistoryGroup
|
selectedCallHistoryGroup: CallHistoryGroup
|
||||||
) => void;
|
) => void;
|
||||||
|
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
|
||||||
startCallLinkLobbyByRoomId: (roomId: string) => void;
|
startCallLinkLobbyByRoomId: (roomId: string) => void;
|
||||||
|
togglePip: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const CALL_LIST_ITEM_ROW_HEIGHT = 62;
|
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() {
|
function rowHeight() {
|
||||||
return CALL_LIST_ITEM_ROW_HEIGHT;
|
return CALL_LIST_ITEM_ROW_HEIGHT;
|
||||||
|
@ -145,35 +171,320 @@ function isSameOptions(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CallsList({
|
export function CallsList({
|
||||||
hasActiveCall,
|
activeCall,
|
||||||
getCallHistoryGroupsCount,
|
getCallHistoryGroupsCount,
|
||||||
getCallHistoryGroups,
|
getCallHistoryGroups,
|
||||||
callHistoryEdition,
|
callHistoryEdition,
|
||||||
|
getAdhocCall,
|
||||||
|
getCall,
|
||||||
getCallLink,
|
getCallLink,
|
||||||
getConversation,
|
getConversation,
|
||||||
|
hangUpActiveCall,
|
||||||
i18n,
|
i18n,
|
||||||
selectedCallHistoryGroup,
|
selectedCallHistoryGroup,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
onSelectCallHistoryGroup,
|
onSelectCallHistoryGroup,
|
||||||
|
peekNotConnectedGroupCall,
|
||||||
startCallLinkLobbyByRoomId,
|
startCallLinkLobbyByRoomId,
|
||||||
|
togglePip,
|
||||||
}: CallsListProps): JSX.Element {
|
}: CallsListProps): JSX.Element {
|
||||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||||
const listRef = useRef<List>(null);
|
const listRef = useRef<List>(null);
|
||||||
const [queryInput, setQueryInput] = useState('');
|
const [queryInput, setQueryInput] = useState('');
|
||||||
const [status, setStatus] = useState(CallHistoryFilterStatus.All);
|
const [status, setStatus] = useState(CallHistoryFilterStatus.All);
|
||||||
const [searchState, setSearchState] = useState(defaultInitState);
|
const [searchState, setSearchState] = useState(defaultInitState);
|
||||||
|
const [isLeaveCallDialogVisible, setIsLeaveCallDialogVisible] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const prevOptionsRef = useRef<CallHistoryFilterOptions | null>(null);
|
const prevOptionsRef = useRef<CallHistoryFilterOptions | null>(null);
|
||||||
|
|
||||||
const getCallHistoryGroupsCountRef = useRef(getCallHistoryGroupsCount);
|
const getCallHistoryGroupsCountRef = useRef(getCallHistoryGroupsCount);
|
||||||
const getCallHistoryGroupsRef = useRef(getCallHistoryGroups);
|
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(() => {
|
useEffect(() => {
|
||||||
getCallHistoryGroupsCountRef.current = getCallHistoryGroupsCount;
|
getCallHistoryGroupsCountRef.current = getCallHistoryGroupsCount;
|
||||||
getCallHistoryGroupsRef.current = getCallHistoryGroups;
|
getCallHistoryGroupsRef.current = getCallHistoryGroups;
|
||||||
}, [getCallHistoryGroupsCount, 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(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
@ -219,6 +530,11 @@ export function CallsList({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (results) {
|
||||||
|
enqueueCallPeeks(results.items, true);
|
||||||
|
searchStateItemsRef.current = results.items;
|
||||||
|
}
|
||||||
|
|
||||||
// Only commit the new search state once the results are ready
|
// Only commit the new search state once the results are ready
|
||||||
setSearchState({
|
setSearchState({
|
||||||
state: results == null ? 'rejected' : 'fulfilled',
|
state: results == null ? 'rejected' : 'fulfilled',
|
||||||
|
@ -246,7 +562,7 @@ export function CallsList({
|
||||||
return () => {
|
return () => {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
};
|
};
|
||||||
}, [queryInput, status, callHistoryEdition]);
|
}, [queryInput, status, callHistoryEdition, enqueueCallPeeks]);
|
||||||
|
|
||||||
const loadMoreRows = useCallback(
|
const loadMoreRows = useCallback(
|
||||||
async (props: IndexRange) => {
|
async (props: IndexRange) => {
|
||||||
|
@ -279,6 +595,8 @@ export function CallsList({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enqueueCallPeeks(groups, false);
|
||||||
|
|
||||||
setSearchState(prevSearchState => {
|
setSearchState(prevSearchState => {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
prevSearchState.results != null,
|
prevSearchState.results != null,
|
||||||
|
@ -286,6 +604,7 @@ export function CallsList({
|
||||||
);
|
);
|
||||||
const newItems = prevSearchState.results.items.slice();
|
const newItems = prevSearchState.results.items.slice();
|
||||||
newItems.splice(startIndex, stopIndex, ...groups);
|
newItems.splice(startIndex, stopIndex, ...groups);
|
||||||
|
searchStateItemsRef.current = newItems;
|
||||||
return {
|
return {
|
||||||
...prevSearchState,
|
...prevSearchState,
|
||||||
results: {
|
results: {
|
||||||
|
@ -298,27 +617,7 @@ export function CallsList({
|
||||||
log.error('CallsList#loadMoreRows error fetching', error);
|
log.error('CallsList#loadMoreRows error fetching', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[searchState]
|
[enqueueCallPeeks, 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]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRowLoaded = useCallback(
|
const isRowLoaded = useCallback(
|
||||||
|
@ -332,10 +631,23 @@ export function CallsList({
|
||||||
({ key, index, style }: ListRowProps) => {
|
({ key, index, style }: ListRowProps) => {
|
||||||
const item = searchState.results?.items.at(index) ?? null;
|
const item = searchState.results?.items.at(index) ?? null;
|
||||||
const conversation = getConversationForItem(item);
|
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 isAdhoc = item?.type === CallType.Adhoc;
|
||||||
const isNewCallVisible = Boolean(
|
const isCallButtonVisible = Boolean(
|
||||||
!isAdhoc || (isAdhoc && getCallLink(item.peerId))
|
!isAdhoc || (isAdhoc && getCallLink(item.peerId))
|
||||||
);
|
);
|
||||||
|
const isActiveVisible = Boolean(isCallButtonVisible && item && isActive);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
searchState.state === 'pending' ||
|
searchState.state === 'pending' ||
|
||||||
|
@ -410,12 +722,20 @@ export function CallsList({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
trailing={
|
trailing={
|
||||||
isNewCallVisible ? (
|
isCallButtonVisible ? (
|
||||||
<CallsNewCallButton
|
<CallsNewCallButton
|
||||||
callType={item.type}
|
callType={item.type}
|
||||||
hasActiveCall={hasActiveCall}
|
isActive={isActiveVisible}
|
||||||
|
isInCall={isInCall}
|
||||||
|
isEnabled={isInCall || !activeCall}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isAdhoc) {
|
if (isInCall) {
|
||||||
|
togglePip();
|
||||||
|
} else if (activeCall) {
|
||||||
|
if (isActiveVisible) {
|
||||||
|
setIsLeaveCallDialogVisible(true);
|
||||||
|
}
|
||||||
|
} else if (isAdhoc) {
|
||||||
startCallLinkLobbyByRoomId(item.peerId);
|
startCallLinkLobbyByRoomId(item.peerId);
|
||||||
} else if (conversation) {
|
} else if (conversation) {
|
||||||
if (item.type === CallType.Audio) {
|
if (item.type === CallType.Audio) {
|
||||||
|
@ -425,6 +745,7 @@ export function CallsList({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
|
@ -441,7 +762,11 @@ export function CallsList({
|
||||||
<span className="CallsList__ItemCallInfo">
|
<span className="CallsList__ItemCallInfo">
|
||||||
{item.children.length > 1 ? `(${item.children.length}) ` : ''}
|
{item.children.length > 1 ? `(${item.children.length}) ` : ''}
|
||||||
{statusText} ·{' '}
|
{statusText} ·{' '}
|
||||||
<Timestamp i18n={i18n} timestamp={item.timestamp} />
|
{isActiveVisible ? (
|
||||||
|
i18n('icu:CallsList__ItemCallInfo--Active')
|
||||||
|
) : (
|
||||||
|
<Timestamp i18n={i18n} timestamp={item.timestamp} />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -459,15 +784,18 @@ export function CallsList({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
hasActiveCall,
|
activeCall,
|
||||||
searchState,
|
searchState,
|
||||||
getCallLink,
|
getCallLink,
|
||||||
getConversationForItem,
|
getConversationForItem,
|
||||||
|
getIsCallActive,
|
||||||
|
getIsInCall,
|
||||||
selectedCallHistoryGroup,
|
selectedCallHistoryGroup,
|
||||||
onSelectCallHistoryGroup,
|
onSelectCallHistoryGroup,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
startCallLinkLobbyByRoomId,
|
startCallLinkLobbyByRoomId,
|
||||||
|
togglePip,
|
||||||
i18n,
|
i18n,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -498,6 +826,31 @@ export function CallsList({
|
||||||
|
|
||||||
return (
|
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>
|
<NavSidebarSearchHeader>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { partition } from 'lodash';
|
import { partition } from 'lodash';
|
||||||
import type { ListRowProps } from 'react-virtualized';
|
import type { ListRowProps } from 'react-virtualized';
|
||||||
import { List } from 'react-virtualized';
|
import { List } from 'react-virtualized';
|
||||||
|
import classNames from 'classnames';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../types/I18N';
|
import type { LocalizerType } from '../types/I18N';
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
|
@ -18,6 +19,7 @@ import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { I18n } from './I18n';
|
import { I18n } from './I18n';
|
||||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||||
import { CallType } from '../types/CallDisposition';
|
import { CallType } from '../types/CallDisposition';
|
||||||
|
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||||
|
|
||||||
type CallsNewCallProps = Readonly<{
|
type CallsNewCallProps = Readonly<{
|
||||||
hasActiveCall: boolean;
|
hasActiveCall: boolean;
|
||||||
|
@ -35,33 +37,69 @@ type Row =
|
||||||
|
|
||||||
export function CallsNewCallButton({
|
export function CallsNewCallButton({
|
||||||
callType,
|
callType,
|
||||||
hasActiveCall,
|
isEnabled,
|
||||||
|
isActive,
|
||||||
|
isInCall,
|
||||||
|
i18n,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
callType: CallType;
|
callType: CallType;
|
||||||
hasActiveCall: boolean;
|
isActive: boolean;
|
||||||
|
isEnabled: boolean;
|
||||||
|
isInCall: boolean;
|
||||||
|
i18n: LocalizerType;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}): JSX.Element {
|
}): 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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="CallsNewCall__ItemActionButton"
|
className={classNames(
|
||||||
aria-disabled={hasActiveCall}
|
'CallsNewCall__ItemActionButton',
|
||||||
|
isActive ? 'CallsNewCall__ItemActionButton--join-call' : undefined,
|
||||||
|
isEnabled
|
||||||
|
? undefined
|
||||||
|
: 'CallsNewCall__ItemActionButton--join-call-disabled'
|
||||||
|
)}
|
||||||
|
aria-label={tooltipContent}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!hasActiveCall) {
|
onClick();
|
||||||
onClick();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{callType === CallType.Audio && (
|
{innerContent}
|
||||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Phone" />
|
|
||||||
)}
|
|
||||||
{callType !== CallType.Audio && (
|
|
||||||
<span className="CallsNewCall__ItemIcon CallsNewCall__ItemIcon--Video" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return tooltipContent === '' ? (
|
||||||
|
buttonContent
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
className="CallsNewCall__ItemActionButtonTooltip"
|
||||||
|
content={tooltipContent}
|
||||||
|
direction={TooltipPlacement.Top}
|
||||||
|
>
|
||||||
|
{buttonContent}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CallsNewCall({
|
export function CallsNewCall({
|
||||||
|
@ -173,6 +211,8 @@ export function CallsNewCall({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNewCallEnabled = !hasActiveCall;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} style={style}>
|
<div key={key} style={style}>
|
||||||
<ListTile
|
<ListTile
|
||||||
|
@ -195,19 +235,29 @@ export function CallsNewCall({
|
||||||
{item.conversation.type === 'direct' && (
|
{item.conversation.type === 'direct' && (
|
||||||
<CallsNewCallButton
|
<CallsNewCallButton
|
||||||
callType={CallType.Audio}
|
callType={CallType.Audio}
|
||||||
hasActiveCall={hasActiveCall}
|
isActive={false}
|
||||||
|
isEnabled={isNewCallEnabled}
|
||||||
|
isInCall={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onOutgoingAudioCallInConversation(item.conversation.id);
|
if (isNewCallEnabled) {
|
||||||
|
onOutgoingAudioCallInConversation(item.conversation.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CallsNewCallButton
|
<CallsNewCallButton
|
||||||
// It's okay if this is a group
|
// It's okay if this is a group
|
||||||
callType={CallType.Video}
|
callType={CallType.Video}
|
||||||
hasActiveCall={hasActiveCall}
|
isActive={false}
|
||||||
|
isEnabled={isNewCallEnabled}
|
||||||
|
isInCall={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onOutgoingVideoCallInConversation(item.conversation.id);
|
if (isNewCallEnabled) {
|
||||||
|
onOutgoingVideoCallInConversation(item.conversation.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,16 @@ import type {
|
||||||
} from '../types/CallDisposition';
|
} from '../types/CallDisposition';
|
||||||
import { CallsNewCall } from './CallsNewCall';
|
import { CallsNewCall } from './CallsNewCall';
|
||||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
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 { ContextMenu } from './ContextMenu';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import type { UnreadStats } from '../util/countUnreadStats';
|
import type { UnreadStats } from '../util/countUnreadStats';
|
||||||
import type { WidthBreakpoint } from './_util';
|
import type { WidthBreakpoint } from './_util';
|
||||||
import type { CallLinkType } from '../types/CallLink';
|
import type { CallLinkType } from '../types/CallLink';
|
||||||
|
import type { CallStateType } from '../state/selectors/calling';
|
||||||
|
|
||||||
enum CallsTabSidebarView {
|
enum CallsTabSidebarView {
|
||||||
CallsListView,
|
CallsListView,
|
||||||
|
@ -37,8 +41,11 @@ type CallsTabProps = Readonly<{
|
||||||
pagination: CallHistoryPagination
|
pagination: CallHistoryPagination
|
||||||
) => Promise<Array<CallHistoryGroup>>;
|
) => Promise<Array<CallHistoryGroup>>;
|
||||||
callHistoryEdition: number;
|
callHistoryEdition: number;
|
||||||
|
getAdhocCall: (roomId: string) => CallStateType | undefined;
|
||||||
|
getCall: (id: string) => CallStateType | undefined;
|
||||||
getCallLink: (id: string) => CallLinkType | undefined;
|
getCallLink: (id: string) => CallLinkType | undefined;
|
||||||
getConversation: (id: string) => ConversationType | void;
|
getConversation: (id: string) => ConversationType | void;
|
||||||
|
hangUpActiveCall: (reason: string) => void;
|
||||||
hasFailedStorySends: boolean;
|
hasFailedStorySends: boolean;
|
||||||
hasPendingUpdate: boolean;
|
hasPendingUpdate: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -48,6 +55,7 @@ type CallsTabProps = Readonly<{
|
||||||
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
||||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||||
|
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
|
||||||
preferredLeftPaneWidth: number;
|
preferredLeftPaneWidth: number;
|
||||||
renderConversationDetails: (
|
renderConversationDetails: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
@ -59,6 +67,7 @@ type CallsTabProps = Readonly<{
|
||||||
regionCode: string | undefined;
|
regionCode: string | undefined;
|
||||||
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
|
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
|
||||||
startCallLinkLobbyByRoomId: (roomId: string) => void;
|
startCallLinkLobbyByRoomId: (roomId: string) => void;
|
||||||
|
togglePip: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function CallsTab({
|
export function CallsTab({
|
||||||
|
@ -68,8 +77,11 @@ export function CallsTab({
|
||||||
getCallHistoryGroupsCount,
|
getCallHistoryGroupsCount,
|
||||||
getCallHistoryGroups,
|
getCallHistoryGroups,
|
||||||
callHistoryEdition,
|
callHistoryEdition,
|
||||||
|
getAdhocCall,
|
||||||
|
getCall,
|
||||||
getCallLink,
|
getCallLink,
|
||||||
getConversation,
|
getConversation,
|
||||||
|
hangUpActiveCall,
|
||||||
hasFailedStorySends,
|
hasFailedStorySends,
|
||||||
hasPendingUpdate,
|
hasPendingUpdate,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -79,12 +91,14 @@ export function CallsTab({
|
||||||
onToggleNavTabsCollapse,
|
onToggleNavTabsCollapse,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
|
peekNotConnectedGroupCall,
|
||||||
preferredLeftPaneWidth,
|
preferredLeftPaneWidth,
|
||||||
renderConversationDetails,
|
renderConversationDetails,
|
||||||
renderToastManager,
|
renderToastManager,
|
||||||
regionCode,
|
regionCode,
|
||||||
savePreferredLeftPaneWidth,
|
savePreferredLeftPaneWidth,
|
||||||
startCallLinkLobbyByRoomId,
|
startCallLinkLobbyByRoomId,
|
||||||
|
togglePip,
|
||||||
}: CallsTabProps): JSX.Element {
|
}: CallsTabProps): JSX.Element {
|
||||||
const [sidebarView, setSidebarView] = useState(
|
const [sidebarView, setSidebarView] = useState(
|
||||||
CallsTabSidebarView.CallsListView
|
CallsTabSidebarView.CallsListView
|
||||||
|
@ -231,12 +245,15 @@ export function CallsTab({
|
||||||
{sidebarView === CallsTabSidebarView.CallsListView && (
|
{sidebarView === CallsTabSidebarView.CallsListView && (
|
||||||
<CallsList
|
<CallsList
|
||||||
key={CallsTabSidebarView.CallsListView}
|
key={CallsTabSidebarView.CallsListView}
|
||||||
hasActiveCall={activeCall != null}
|
activeCall={activeCall}
|
||||||
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
|
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
|
||||||
getCallHistoryGroups={getCallHistoryGroups}
|
getCallHistoryGroups={getCallHistoryGroups}
|
||||||
callHistoryEdition={callHistoryEdition}
|
callHistoryEdition={callHistoryEdition}
|
||||||
|
getAdhocCall={getAdhocCall}
|
||||||
|
getCall={getCall}
|
||||||
getCallLink={getCallLink}
|
getCallLink={getCallLink}
|
||||||
getConversation={getConversation}
|
getConversation={getConversation}
|
||||||
|
hangUpActiveCall={hangUpActiveCall}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
selectedCallHistoryGroup={selected?.callHistoryGroup ?? null}
|
selectedCallHistoryGroup={selected?.callHistoryGroup ?? null}
|
||||||
onSelectCallHistoryGroup={handleSelectCallHistoryGroup}
|
onSelectCallHistoryGroup={handleSelectCallHistoryGroup}
|
||||||
|
@ -246,7 +263,9 @@ export function CallsTab({
|
||||||
onOutgoingVideoCallInConversation={
|
onOutgoingVideoCallInConversation={
|
||||||
handleOutgoingVideoCallInConversation
|
handleOutgoingVideoCallInConversation
|
||||||
}
|
}
|
||||||
|
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
|
||||||
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
|
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
|
||||||
|
togglePip={togglePip}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{sidebarView === CallsTabSidebarView.NewCallView && (
|
{sidebarView === CallsTabSidebarView.NewCallView && (
|
||||||
|
|
|
@ -2369,6 +2369,7 @@ export class CallingClass {
|
||||||
|
|
||||||
if (update === RingUpdate.Requested) {
|
if (update === RingUpdate.Requested) {
|
||||||
this.reduxInterface?.peekNotConnectedGroupCall({
|
this.reduxInterface?.peekNotConnectedGroupCall({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,7 +285,8 @@ type SendGroupCallReactionLocalCopyType = ReadonlyDeep<{
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type PeekNotConnectedGroupCallType = ReadonlyDeep<{
|
export type PeekNotConnectedGroupCallType = ReadonlyDeep<{
|
||||||
|
callMode: CallMode.Group | CallMode.Adhoc;
|
||||||
conversationId: string;
|
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(
|
function hangUpActiveCall(
|
||||||
reason: string
|
reason: string
|
||||||
): ThunkAction<void, RootStateType, unknown, HangUpActionType> {
|
): ThunkAction<void, RootStateType, unknown, HangUpActionType> {
|
||||||
|
@ -1553,10 +1596,10 @@ function peekNotConnectedGroupCall(
|
||||||
payload: PeekNotConnectedGroupCallType
|
payload: PeekNotConnectedGroupCallType
|
||||||
): ThunkAction<void, RootStateType, unknown, PeekGroupCallFulfilledActionType> {
|
): ThunkAction<void, RootStateType, unknown, PeekGroupCallFulfilledActionType> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { conversationId } = payload;
|
const { callMode, conversationId } = payload;
|
||||||
doGroupCallPeek({
|
doGroupCallPeek({
|
||||||
conversationId,
|
conversationId,
|
||||||
callMode: CallMode.Group,
|
callMode,
|
||||||
dispatch,
|
dispatch,
|
||||||
getState,
|
getState,
|
||||||
});
|
});
|
||||||
|
@ -1868,27 +1911,22 @@ const _startCallLinkLobby = async ({
|
||||||
dispatch: ThunkDispatch<
|
dispatch: ThunkDispatch<
|
||||||
RootStateType,
|
RootStateType,
|
||||||
unknown,
|
unknown,
|
||||||
StartCallLinkLobbyActionType | ShowErrorModalActionType
|
| StartCallLinkLobbyActionType
|
||||||
|
| ShowErrorModalActionType
|
||||||
|
| TogglePipActionType
|
||||||
>;
|
>;
|
||||||
getState: () => RootStateType;
|
getState: () => RootStateType;
|
||||||
}) => {
|
}) => {
|
||||||
|
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||||
|
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
if (state.calling.activeCallState) {
|
if (
|
||||||
const i18n = getIntl(getState());
|
handleActiveCallOnStartLobby({ conversationId: roomId, state, dispatch })
|
||||||
dispatch({
|
) {
|
||||||
type: SHOW_ERROR_MODAL,
|
|
||||||
payload: {
|
|
||||||
title: i18n('icu:calling__cant-join'),
|
|
||||||
description: i18n('icu:calling__dialog-already-in-call'),
|
|
||||||
buttonVariant: ButtonVariant.Primary,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
|
||||||
|
|
||||||
const readResult = await calling.readCallLink({ callLinkRootKey });
|
const readResult = await calling.readCallLink({ callLinkRootKey });
|
||||||
const { callLinkState } = readResult;
|
const { callLinkState } = readResult;
|
||||||
if (!callLinkState) {
|
if (!callLinkState) {
|
||||||
|
@ -1919,7 +1957,6 @@ const _startCallLinkLobby = async ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
|
||||||
try {
|
try {
|
||||||
const callLinkExists = await dataInterface.callLinkExists(roomId);
|
const callLinkExists = await dataInterface.callLinkExists(roomId);
|
||||||
if (callLinkExists) {
|
if (callLinkExists) {
|
||||||
|
@ -1983,7 +2020,7 @@ function startCallingLobby({
|
||||||
void,
|
void,
|
||||||
RootStateType,
|
RootStateType,
|
||||||
unknown,
|
unknown,
|
||||||
StartCallingLobbyActionType
|
StartCallingLobbyActionType | TogglePipActionType
|
||||||
> {
|
> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
|
@ -46,3 +46,12 @@ export const isAnybodyElseInGroupCall = (
|
||||||
peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>,
|
peekInfo: undefined | Readonly<Pick<GroupCallPeekInfoType, 'acis'>>,
|
||||||
ourAci: AciString
|
ourAci: AciString
|
||||||
): boolean => Boolean(peekInfo?.acis.some(id => id !== ourAci));
|
): 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 { SmartConversationDetails } from './ConversationDetails';
|
||||||
import { SmartToastManager } from './ToastManager';
|
import { SmartToastManager } from './ToastManager';
|
||||||
import { useCallingActions } from '../ducks/calling';
|
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 { useCallHistoryActions } from '../ducks/callHistory';
|
||||||
import { getCallHistoryEdition } from '../selectors/callHistory';
|
import { getCallHistoryEdition } from '../selectors/callHistory';
|
||||||
import { getHasPendingUpdate } from '../selectors/updates';
|
import { getHasPendingUpdate } from '../selectors/updates';
|
||||||
|
@ -97,6 +102,8 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
||||||
const allConversations = useSelector(getAllConversations);
|
const allConversations = useSelector(getAllConversations);
|
||||||
const regionCode = useSelector(getRegionCode);
|
const regionCode = useSelector(getRegionCode);
|
||||||
const getConversation = useSelector(getConversationSelector);
|
const getConversation = useSelector(getConversationSelector);
|
||||||
|
const getAdhocCall = useSelector(getAdhocCallSelector);
|
||||||
|
const getCall = useSelector(getCallSelector);
|
||||||
const getCallLink = useSelector(getCallLinkSelector);
|
const getCallLink = useSelector(getCallLinkSelector);
|
||||||
|
|
||||||
const activeCall = useSelector(getActiveCallState);
|
const activeCall = useSelector(getActiveCallState);
|
||||||
|
@ -107,9 +114,12 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
||||||
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
|
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
hangUpActiveCall,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
|
peekNotConnectedGroupCall,
|
||||||
startCallLinkLobbyByRoomId,
|
startCallLinkLobbyByRoomId,
|
||||||
|
togglePip,
|
||||||
} = useCallingActions();
|
} = useCallingActions();
|
||||||
const {
|
const {
|
||||||
clearAllCallHistory: clearCallHistory,
|
clearAllCallHistory: clearCallHistory,
|
||||||
|
@ -169,8 +179,11 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
||||||
getConversation={getConversation}
|
getConversation={getConversation}
|
||||||
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
|
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
|
||||||
getCallHistoryGroups={getCallHistoryGroups}
|
getCallHistoryGroups={getCallHistoryGroups}
|
||||||
|
getAdhocCall={getAdhocCall}
|
||||||
|
getCall={getCall}
|
||||||
getCallLink={getCallLink}
|
getCallLink={getCallLink}
|
||||||
callHistoryEdition={callHistoryEdition}
|
callHistoryEdition={callHistoryEdition}
|
||||||
|
hangUpActiveCall={hangUpActiveCall}
|
||||||
hasFailedStorySends={hasFailedStorySends}
|
hasFailedStorySends={hasFailedStorySends}
|
||||||
hasPendingUpdate={hasPendingUpdate}
|
hasPendingUpdate={hasPendingUpdate}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
@ -180,12 +193,14 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
||||||
onToggleNavTabsCollapse={toggleNavTabsCollapse}
|
onToggleNavTabsCollapse={toggleNavTabsCollapse}
|
||||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||||
|
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
|
||||||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||||
renderConversationDetails={renderConversationDetails}
|
renderConversationDetails={renderConversationDetails}
|
||||||
renderToastManager={renderToastManager}
|
renderToastManager={renderToastManager}
|
||||||
regionCode={regionCode}
|
regionCode={regionCode}
|
||||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||||
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
|
startCallLinkLobbyByRoomId={startCallLinkLobbyByRoomId}
|
||||||
|
togglePip={togglePip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1498,6 +1498,7 @@ describe('calling duck', () => {
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
|
|
||||||
await peekNotConnectedGroupCall({
|
await peekNotConnectedGroupCall({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
})(
|
})(
|
||||||
dispatch,
|
dispatch,
|
||||||
|
|
|
@ -3906,5 +3906,45 @@
|
||||||
"line": " const imageDataCache = React.useRef<CallingImageDataCache>(new Map());",
|
"line": " const imageDataCache = React.useRef<CallingImageDataCache>(new Map());",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2024-05-06T20:18:59.647Z"
|
"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