Search call links in calls tab

This commit is contained in:
ayumi-signal 2024-05-17 16:22:51 -07:00 committed by GitHub
parent fc9c5488c5
commit dea641bae4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 384 additions and 112 deletions

View file

@ -3619,48 +3619,66 @@ function getCallHistoryGroupDataSync(
): unknown { ): unknown {
return db.transaction(() => { return db.transaction(() => {
const { limit, offset } = pagination; const { limit, offset } = pagination;
const { status, conversationIds } = filter; const { status, conversationIds, callLinkRoomIds } = filter;
// TODO: DESKTOP-6827 Search Calls Tab for adhoc calls
if (conversationIds != null) {
strictAssert(conversationIds.length > 0, "can't filter by empty array");
const isUsingTempTable = conversationIds != null || callLinkRoomIds != null;
if (isUsingTempTable) {
const [createTempTable] = sql` const [createTempTable] = sql`
CREATE TEMP TABLE temp_callHistory_filtered_conversations ( CREATE TEMP TABLE temp_callHistory_filtered_peers (
id TEXT, conversationId TEXT,
serviceId TEXT, serviceId TEXT,
groupId TEXT groupId TEXT,
callLinkRoomId TEXT
); );
`; `;
db.exec(createTempTable); db.exec(createTempTable);
if (conversationIds != null) {
strictAssert(conversationIds.length > 0, "can't filter by empty array");
batchMultiVarQuery(db, conversationIds, ids => { batchMultiVarQuery(db, conversationIds, ids => {
const idList = sqlJoin(ids.map(id => sqlFragment`${id}`)); const idList = sqlJoin(ids.map(id => sqlFragment`${id}`));
const [insertQuery, insertParams] = sql` const [insertQuery, insertParams] = sql`
INSERT INTO temp_callHistory_filtered_conversations INSERT INTO temp_callHistory_filtered_peers
(id, serviceId, groupId) (conversationId, serviceId, groupId)
SELECT id, serviceId, groupId SELECT id, serviceId, groupId
FROM conversations FROM conversations
WHERE conversations.id IN (${idList}); WHERE conversations.id IN (${idList});
`; `;
db.prepare(insertQuery).run(insertParams); db.prepare(insertQuery).run(insertParams);
}); });
}
if (callLinkRoomIds != null) {
strictAssert(callLinkRoomIds.length > 0, "can't filter by empty array");
batchMultiVarQuery(db, callLinkRoomIds, ids => {
const idList = sqlJoin(ids.map(id => sqlFragment`(${id})`));
const [insertQuery, insertParams] = sql`
INSERT INTO temp_callHistory_filtered_peers
(callLinkRoomId)
VALUES ${idList};
`;
db.prepare(insertQuery).run(insertParams);
});
}
} }
const innerJoin = // peerId can be a conversation id (legacy), a serviceId, groupId, or call link roomId
conversationIds != null const innerJoin = isUsingTempTable
? // peerId can be a conversation id (legacy), a serviceId, or a groupId ? sqlFragment`
sqlFragment` INNER JOIN temp_callHistory_filtered_peers ON (
INNER JOIN temp_callHistory_filtered_conversations ON ( temp_callHistory_filtered_peers.conversationId IS c.peerId
temp_callHistory_filtered_conversations.id IS c.peerId OR temp_callHistory_filtered_peers.serviceId IS c.peerId
OR temp_callHistory_filtered_conversations.serviceId IS c.peerId OR temp_callHistory_filtered_peers.groupId IS c.peerId
OR temp_callHistory_filtered_conversations.groupId IS c.peerId OR temp_callHistory_filtered_peers.callLinkRoomId IS c.peerId
) )
` `
: sqlFragment``; : sqlFragment``;
const filterClause = const filterClause =
status === CallHistoryFilterStatus.All status === CallHistoryFilterStatus.All
@ -3795,9 +3813,9 @@ function getCallHistoryGroupDataSync(
? db.prepare(query).pluck(true).get(params) ? db.prepare(query).pluck(true).get(params)
: db.prepare(query).all(params); : db.prepare(query).all(params);
if (conversationIds != null) { if (isUsingTempTable) {
const [dropTempTableQuery] = sql` const [dropTempTableQuery] = sql`
DROP TABLE temp_callHistory_filtered_conversations; DROP TABLE temp_callHistory_filtered_peers;
`; `;
db.exec(dropTempTableQuery); db.exec(dropTempTableQuery);
@ -3819,6 +3837,11 @@ async function getCallHistoryGroupsCount(
limit: 0, limit: 0,
offset: 0, offset: 0,
}); });
if (result == null) {
return 0;
}
return countSchema.parse(result); return countSchema.parse(result);
} }
@ -5975,6 +5998,7 @@ async function removeAll(): Promise<void> {
DELETE FROM attachment_downloads; DELETE FROM attachment_downloads;
DELETE FROM badgeImageFiles; DELETE FROM badgeImageFiles;
DELETE FROM badges; DELETE FROM badges;
DELETE FROM callLinks;
DELETE FROM callsHistory; DELETE FROM callsHistory;
DELETE FROM conversations; DELETE FROM conversations;
DELETE FROM emojis; DELETE FROM emojis;

View file

@ -31,7 +31,7 @@ import type {
PresentedSource, PresentedSource,
PresentableSource, PresentableSource,
} from '../../types/Calling'; } from '../../types/Calling';
import type { CallLinkStateType } from '../../types/CallLink'; import type { CallLinkStateType, CallLinkType } from '../../types/CallLink';
import { import {
CALLING_REACTIONS_LIFETIME, CALLING_REACTIONS_LIFETIME,
MAX_CALLING_REACTIONS, MAX_CALLING_REACTIONS,
@ -174,15 +174,8 @@ export type AdhocCallsType = {
[roomId: string]: GroupCallStateType; [roomId: string]: GroupCallStateType;
}; };
export type CallLinksByRoomIdStateType = ReadonlyDeep<
CallLinkStateType & {
rootKey: string;
adminKey: string | null;
}
>;
export type CallLinksByRoomIdType = ReadonlyDeep<{ export type CallLinksByRoomIdType = ReadonlyDeep<{
[roomId: string]: CallLinksByRoomIdStateType; [roomId: string]: CallLinkType;
}>; }>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep // eslint-disable-next-line local-rules/type-alias-readonlydeep
@ -244,8 +237,7 @@ type GroupCallStateChangeActionPayloadType =
}; };
type HandleCallLinkUpdateActionPayloadType = ReadonlyDeep<{ type HandleCallLinkUpdateActionPayloadType = ReadonlyDeep<{
roomId: string; callLink: CallLinkType;
callLinkDetails: CallLinksByRoomIdStateType;
}>; }>;
type HangUpActionPayloadType = ReadonlyDeep<{ type HangUpActionPayloadType = ReadonlyDeep<{
@ -384,6 +376,7 @@ type StartCallLinkLobbyPayloadType = {
peekInfo?: GroupCallPeekInfoType; peekInfo?: GroupCallPeekInfoType;
remoteParticipants: Array<GroupCallParticipantInfoType>; remoteParticipants: Array<GroupCallParticipantInfoType>;
callLinkState: CallLinkStateType; callLinkState: CallLinkStateType;
callLinkRoomId: string;
callLinkRootKey: string; callLinkRootKey: string;
}; };
@ -1333,10 +1326,11 @@ function handleCallLinkUpdate(
'revoked', 'revoked',
]); ]);
const callLinkDetails: CallLinksByRoomIdStateType = { const callLink: CallLinkType = {
...CALL_LINK_DEFAULT_STATE, ...CALL_LINK_DEFAULT_STATE,
...existingCallLinkState, ...existingCallLinkState,
...freshCallLinkState, ...freshCallLinkState,
roomId,
rootKey, rootKey,
adminKey, adminKey,
}; };
@ -1352,19 +1346,13 @@ function handleCallLinkUpdate(
log.info(`${logId}: Updated existing call link state`); log.info(`${logId}: Updated existing call link state`);
} }
} else { } else {
await dataInterface.insertCallLink({ await dataInterface.insertCallLink(callLink);
roomId,
...callLinkDetails,
});
log.info(`${logId}: Saved new call link`); log.info(`${logId}: Saved new call link`);
} }
dispatch({ dispatch({
type: HANDLE_CALL_LINK_UPDATE, type: HANDLE_CALL_LINK_UPDATE,
payload: { payload: { callLink },
roomId,
callLinkDetails,
},
}); });
}; };
} }
@ -2006,6 +1994,7 @@ const _startCallLinkLobby = async ({
payload: { payload: {
...callLobbyData, ...callLobbyData,
callLinkState, callLinkState,
callLinkRoomId: roomId,
callLinkRootKey: rootKey, callLinkRootKey: rootKey,
conversationId: roomId, conversationId: roomId,
isConversationTooBigToRing: false, isConversationTooBigToRing: false,
@ -2415,6 +2404,9 @@ export function reducer(
...callLinks, ...callLinks,
[conversationId]: { [conversationId]: {
...action.payload.callLinkState, ...action.payload.callLinkState,
roomId:
callLinks[conversationId]?.roomId ??
action.payload.callLinkRoomId,
rootKey: rootKey:
callLinks[conversationId]?.rootKey ?? callLinks[conversationId]?.rootKey ??
action.payload.callLinkRootKey, action.payload.callLinkRootKey,
@ -3355,13 +3347,14 @@ export function reducer(
if (action.type === HANDLE_CALL_LINK_UPDATE) { if (action.type === HANDLE_CALL_LINK_UPDATE) {
const { callLinks } = state; const { callLinks } = state;
const { roomId, callLinkDetails } = action.payload; const { callLink } = action.payload;
const { roomId } = callLink;
return { return {
...state, ...state,
callLinks: { callLinks: {
...callLinks, ...callLinks,
[roomId]: callLinkDetails, [roomId]: callLink,
}, },
}; };
} }

View file

@ -79,17 +79,13 @@ export type CallLinkSelectorType = (roomId: string) => CallLinkType | undefined;
export const getCallLinkSelector = createSelector( export const getCallLinkSelector = createSelector(
getCallLinksByRoomId, getCallLinksByRoomId,
(callLinksByRoomId: CallLinksByRoomIdType): CallLinkSelectorType => (callLinksByRoomId: CallLinksByRoomIdType): CallLinkSelectorType =>
(roomId: string): CallLinkType | undefined => { (roomId: string): CallLinkType | undefined =>
const callLinkState = getOwn(callLinksByRoomId, roomId); getOwn(callLinksByRoomId, roomId)
if (!callLinkState) { );
return;
}
return { export const getAllCallLinks = createSelector(
roomId, getCallLinksByRoomId,
...callLinkState, (lookup): Array<CallLinkType> => Object.values(lookup)
};
}
); );
export type CallSelectorType = ( export type CallSelectorType = (

View file

@ -28,6 +28,7 @@ import { useCallingActions } from '../ducks/calling';
import { import {
getActiveCallState, getActiveCallState,
getAdhocCallSelector, getAdhocCallSelector,
getAllCallLinks,
getCallSelector, getCallSelector,
getCallLinkSelector, getCallLinkSelector,
} from '../selectors/calling'; } from '../selectors/calling';
@ -36,41 +37,67 @@ import { getCallHistoryEdition } from '../selectors/callHistory';
import { getHasPendingUpdate } from '../selectors/updates'; import { getHasPendingUpdate } from '../selectors/updates';
import { getHasAnyFailedStorySends } from '../selectors/stories'; import { getHasAnyFailedStorySends } from '../selectors/stories';
import { getOtherTabsUnreadStats } from '../selectors/nav'; import { getOtherTabsUnreadStats } from '../selectors/nav';
import type { CallLinkType } from '../../types/CallLink';
import { filterCallLinks } from '../../util/filterCallLinks';
function getCallHistoryFilter( function getCallHistoryFilter({
allConversations: Array<ConversationType>, allCallLinks,
regionCode: string | undefined, allConversations,
options: CallHistoryFilterOptions regionCode,
): CallHistoryFilter | null { options,
}: {
allConversations: Array<ConversationType>;
allCallLinks: Array<CallLinkType>;
regionCode: string | undefined;
options: CallHistoryFilterOptions;
}): CallHistoryFilter | null {
const { status } = options;
const query = options.query.normalize().trim(); const query = options.query.normalize().trim();
if (query !== '') { if (query === '') {
const currentConversations = allConversations.filter(conversation => {
return conversation.removalStage == null;
});
const filteredConversations = filterAndSortConversations(
currentConversations,
query,
regionCode
);
// If there are no matching conversations, then no calls will match.
if (filteredConversations.length === 0) {
return null;
}
return { return {
status: options.status, status,
conversationIds: filteredConversations.map(conversation => { callLinkRoomIds: null,
return conversation.id; conversationIds: null,
}),
}; };
} }
let callLinkRoomIds = null;
let conversationIds = null;
const currentConversations = allConversations.filter(conversation => {
return conversation.removalStage == null;
});
const filteredConversations = filterAndSortConversations(
currentConversations,
query,
regionCode
);
if (filteredConversations.length > 0) {
conversationIds = filteredConversations.map(conversation => {
return conversation.id;
});
}
const filteredCallLinks = filterCallLinks(allCallLinks, query);
if (filteredCallLinks.length > 0) {
callLinkRoomIds = filteredCallLinks.map(callLink => {
return callLink.roomId;
});
}
// If the search query resulted in no matching call links or conversations, then
// no calls will match.
if (callLinkRoomIds == null && conversationIds == null) {
return null;
}
return { return {
status: options.status, status,
conversationIds: null, callLinkRoomIds,
conversationIds,
}; };
} }
@ -99,6 +126,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
const { savePreferredLeftPaneWidth, toggleNavTabsCollapse } = const { savePreferredLeftPaneWidth, toggleNavTabsCollapse } =
useItemsActions(); useItemsActions();
const allCallLinks = useSelector(getAllCallLinks);
const allConversations = useSelector(getAllConversations); const allConversations = useSelector(getAllConversations);
const regionCode = useSelector(getRegionCode); const regionCode = useSelector(getRegionCode);
const getConversation = useSelector(getConversationSelector); const getConversation = useSelector(getConversationSelector);
@ -129,11 +157,12 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
const getCallHistoryGroupsCount = useCallback( const getCallHistoryGroupsCount = useCallback(
async (options: CallHistoryFilterOptions) => { async (options: CallHistoryFilterOptions) => {
const callHistoryFilter = getCallHistoryFilter( const callHistoryFilter = getCallHistoryFilter({
allCallLinks,
allConversations, allConversations,
regionCode, regionCode,
options options,
); });
if (callHistoryFilter == null) { if (callHistoryFilter == null) {
return 0; return 0;
} }
@ -142,7 +171,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
); );
return count; return count;
}, },
[allConversations, regionCode] [allCallLinks, allConversations, regionCode]
); );
const getCallHistoryGroups = useCallback( const getCallHistoryGroups = useCallback(
@ -150,11 +179,12 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
options: CallHistoryFilterOptions, options: CallHistoryFilterOptions,
pagination: CallHistoryPagination pagination: CallHistoryPagination
) => { ) => {
const callHistoryFilter = getCallHistoryFilter( const callHistoryFilter = getCallHistoryFilter({
allCallLinks,
allConversations, allConversations,
regionCode, regionCode,
options options,
); });
if (callHistoryFilter == null) { if (callHistoryFilter == null) {
return []; return [];
} }
@ -164,7 +194,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
); );
return results; return results;
}, },
[allConversations, regionCode] [allCallLinks, allConversations, regionCode]
); );
useEffect(() => { useEffect(() => {

View file

@ -11,15 +11,15 @@ export const FAKE_CALL_LINK: CallLinkType = {
name: 'Fun Link', name: 'Fun Link',
restrictions: CallLinkRestrictions.None, restrictions: CallLinkRestrictions.None,
revoked: false, revoked: false,
roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4', roomId: 'd517b48dd118bee24068d4938886c8abe192706d84936d52594a9157189d2759',
rootKey: 'bpmc-mrgn-hntf-mffd-mndd-xbxk-zmgq-qszg', rootKey: 'dxbb-xfqz-xkgp-nmrx-bpqn-ptkb-spdt-pdgt',
}; };
// Please set expiration // Please set expiration
export const FAKE_CALL_LINK_WITH_ADMIN_KEY: CallLinkType = { export const FAKE_CALL_LINK_WITH_ADMIN_KEY: CallLinkType = {
adminKey: 'xXPI77e6MoVHYREW8iKYmQ==', adminKey: 'xXPI77e6MoVHYREW8iKYmQ==',
expiration: Date.now() + MONTH, // set me expiration: Date.now() + MONTH, // set me
name: 'Fun Link', name: 'Admin Link',
restrictions: CallLinkRestrictions.None, restrictions: CallLinkRestrictions.None,
revoked: false, revoked: false,
roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4', roomId: 'c097eb04cc278d6bc7ed9fb2ddeac00dc9646ae6ddb38513dad9a8a4fe3c38f4',

View file

@ -23,11 +23,16 @@ import {
} from '../../types/CallDisposition'; } from '../../types/CallDisposition';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import type { ConversationAttributesType } from '../../model-types'; import type { ConversationAttributesType } from '../../model-types';
import {
FAKE_CALL_LINK,
FAKE_CALL_LINK_WITH_ADMIN_KEY,
} from '../../test-both/helpers/fakeCallLink';
const { const {
removeAll, removeAll,
getCallHistoryGroups, getCallHistoryGroups,
getCallHistoryGroupsCount, getCallHistoryGroupsCount,
insertCallLink,
saveCallHistory, saveCallHistory,
saveConversation, saveConversation,
} = dataInterface; } = dataInterface;
@ -90,7 +95,11 @@ describe('sql/getCallHistoryGroups', () => {
await saveCallHistory(call2); await saveCallHistory(call2);
const groups = await getCallHistoryGroups( const groups = await getCallHistoryGroups(
{ status: CallHistoryFilterStatus.All, conversationIds: null }, {
status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: null,
},
{ offset: 0, limit: 0 } { offset: 0, limit: 0 }
); );
@ -121,7 +130,11 @@ describe('sql/getCallHistoryGroups', () => {
await saveCallHistory(call2); await saveCallHistory(call2);
const groups = await getCallHistoryGroups( const groups = await getCallHistoryGroups(
{ status: CallHistoryFilterStatus.All, conversationIds: null }, {
status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: null,
},
{ offset: 0, limit: 0 } { offset: 0, limit: 0 }
); );
@ -156,7 +169,11 @@ describe('sql/getCallHistoryGroups', () => {
await saveCallHistory(call4); await saveCallHistory(call4);
const groups = await getCallHistoryGroups( const groups = await getCallHistoryGroups(
{ status: CallHistoryFilterStatus.All, conversationIds: null }, {
status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: null,
},
{ offset: 0, limit: 0 } { offset: 0, limit: 0 }
); );
@ -218,6 +235,7 @@ describe('sql/getCallHistoryGroups', () => {
const groups = await getCallHistoryGroups( const groups = await getCallHistoryGroups(
{ {
status: CallHistoryFilterStatus.All, status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: [conversation1.id], conversationIds: [conversation1.id],
}, },
{ offset: 0, limit: 0 } { offset: 0, limit: 0 }
@ -230,6 +248,7 @@ describe('sql/getCallHistoryGroups', () => {
const groups = await getCallHistoryGroups( const groups = await getCallHistoryGroups(
{ {
status: CallHistoryFilterStatus.All, status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: [conversation2.id], conversationIds: [conversation2.id],
}, },
{ offset: 0, limit: 0 } { offset: 0, limit: 0 }
@ -268,6 +287,7 @@ describe('sql/getCallHistoryGroups', () => {
const groups = await getCallHistoryGroups( const groups = await getCallHistoryGroups(
{ {
status: CallHistoryFilterStatus.All, status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: [conversation.id], conversationIds: [conversation.id],
}, },
{ offset: 0, limit: 0 } { offset: 0, limit: 0 }
@ -310,7 +330,11 @@ describe('sql/getCallHistoryGroups', () => {
await saveCallHistory(call2); await saveCallHistory(call2);
const groups = await getCallHistoryGroups( const groups = await getCallHistoryGroups(
{ status: CallHistoryFilterStatus.Missed, conversationIds: null }, {
status: CallHistoryFilterStatus.Missed,
callLinkRoomIds: null,
conversationIds: null,
},
{ offset: 0, limit: 0 } { offset: 0, limit: 0 }
); );
@ -328,7 +352,7 @@ describe('sql/getCallHistoryGroups', () => {
ringerId: null, ringerId: null,
mode: CallMode.Adhoc, mode: CallMode.Adhoc,
type: CallType.Adhoc, type: CallType.Adhoc,
direction: CallDirection.Outgoing, direction: CallDirection.Incoming,
timestamp, timestamp,
status: AdhocCallStatus.Joined, status: AdhocCallStatus.Joined,
}; };
@ -341,12 +365,127 @@ describe('sql/getCallHistoryGroups', () => {
await saveCallHistory(call2); await saveCallHistory(call2);
const groups = await getCallHistoryGroups( const groups = await getCallHistoryGroups(
{ status: CallHistoryFilterStatus.All, conversationIds: null }, {
status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: null,
},
{ offset: 0, limit: 0 } { offset: 0, limit: 0 }
); );
assert.deepEqual(groups, [toAdhocGroup(call2)]); assert.deepEqual(groups, [toAdhocGroup(call2)]);
}); });
it('should search call links', async () => {
const now = Date.now();
const { roomId: roomId1 } = FAKE_CALL_LINK;
const { roomId: roomId2 } = FAKE_CALL_LINK_WITH_ADMIN_KEY;
await insertCallLink(FAKE_CALL_LINK);
await insertCallLink(FAKE_CALL_LINK_WITH_ADMIN_KEY);
const conversation1Uuid = generateAci();
const conversation2GroupId = 'groupId:2';
const conversation1: ConversationAttributesType = {
type: 'private',
version: 0,
id: 'id:1',
serviceId: conversation1Uuid,
};
const conversation2: ConversationAttributesType = {
type: 'group',
version: 2,
id: 'id:2',
groupId: conversation2GroupId,
};
await saveConversation(conversation1);
await saveConversation(conversation2);
function toAdhocCall(callId: string, roomId: string, timestamp: number) {
return {
callId,
peerId: roomId,
ringerId: null,
mode: CallMode.Adhoc,
type: CallType.Adhoc,
direction: CallDirection.Outgoing,
timestamp,
status: AdhocCallStatus.Joined,
};
}
function toConversationCall(
callId: string,
timestamp: number,
mode: CallMode,
peerId: string | ServiceIdString
) {
return {
callId,
peerId,
ringerId: null,
mode,
type: CallType.Video,
direction: CallDirection.Incoming,
timestamp,
status: DirectCallStatus.Accepted,
};
}
const call1 = toAdhocCall('1', roomId1, now - 30);
const call2 = toAdhocCall('2', roomId2, now - 20);
const call3 = toConversationCall(
'3',
now - 10,
CallMode.Direct,
conversation1Uuid
);
const call4 = toConversationCall(
'4',
now,
CallMode.Group,
conversation2GroupId
);
await saveCallHistory(call1);
await saveCallHistory(call2);
await saveCallHistory(call3);
await saveCallHistory(call4);
{
const groups = await getCallHistoryGroups(
{
status: CallHistoryFilterStatus.All,
callLinkRoomIds: [roomId1],
conversationIds: null,
},
{ offset: 0, limit: 0 }
);
assert.deepEqual(groups, [toAdhocGroup(call1)], 'just call link');
}
{
const groups = await getCallHistoryGroups(
{
status: CallHistoryFilterStatus.All,
callLinkRoomIds: [roomId2],
conversationIds: [conversation2.id],
},
{ offset: 0, limit: 0 }
);
assert.deepEqual(
groups,
[toGroup([call4]), toAdhocGroup(call2)],
'call link and conversation'
);
}
});
}); });
describe('sql/getCallHistoryGroupsCount', () => { describe('sql/getCallHistoryGroupsCount', () => {
@ -383,6 +522,7 @@ describe('sql/getCallHistoryGroupsCount', () => {
const result = await getCallHistoryGroupsCount({ const result = await getCallHistoryGroupsCount({
status: CallHistoryFilterStatus.All, status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: null, conversationIds: null,
}); });
@ -419,6 +559,7 @@ describe('sql/getCallHistoryGroupsCount', () => {
const result = await getCallHistoryGroupsCount({ const result = await getCallHistoryGroupsCount({
status: CallHistoryFilterStatus.All, status: CallHistoryFilterStatus.All,
callLinkRoomIds: null,
conversationIds: null, conversationIds: null,
}); });

View file

@ -1363,12 +1363,12 @@ describe('calling duck', () => {
sinon.assert.calledWith(dispatch, { sinon.assert.calledWith(dispatch, {
type: 'calling/HANDLE_CALL_LINK_UPDATE', type: 'calling/HANDLE_CALL_LINK_UPDATE',
payload: { payload: {
roomId, callLink: {
callLinkDetails: {
name, name,
restrictions, restrictions,
expiration, expiration,
revoked, revoked,
roomId,
rootKey, rootKey,
adminKey, adminKey,
}, },
@ -1383,12 +1383,12 @@ describe('calling duck', () => {
sinon.assert.calledWith(dispatch, { sinon.assert.calledWith(dispatch, {
type: 'calling/HANDLE_CALL_LINK_UPDATE', type: 'calling/HANDLE_CALL_LINK_UPDATE',
payload: { payload: {
roomId, callLink: {
callLinkDetails: {
name, name,
restrictions, restrictions,
expiration, expiration,
revoked, revoked,
roomId,
rootKey, rootKey,
adminKey: 'banana', adminKey: 'banana',
}, },
@ -1451,6 +1451,7 @@ describe('calling duck', () => {
payload: { payload: {
...callLobbyData, ...callLobbyData,
callLinkState, callLinkState,
callLinkRoomId: roomId,
callLinkRootKey: rootKey, callLinkRootKey: rootKey,
conversationId: roomId, conversationId: roomId,
isConversationTooBigToRing: false, isConversationTooBigToRing: false,

View file

@ -133,6 +133,7 @@ export type CallHistoryFilterOptions = Readonly<{
export type CallHistoryFilter = Readonly<{ export type CallHistoryFilter = Readonly<{
status: CallHistoryFilterStatus; status: CallHistoryFilterStatus;
callLinkRoomIds: ReadonlyArray<string> | null;
conversationIds: ReadonlyArray<string> | null; conversationIds: ReadonlyArray<string> | null;
}>; }>;

View file

@ -0,0 +1,86 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type Fuse from 'fuse.js';
import { fuseGetFnRemoveDiacritics, getCachedFuseIndex } from './fuse';
import { removeDiacritics } from './removeDiacritics';
import type { CallLinkType } from '../types/CallLink';
// Based on parameters in filterAndSortConversations
const FUSE_OPTIONS: Fuse.IFuseOptions<CallLinkType> = {
threshold: 0.2,
includeScore: true,
useExtendedSearch: true,
shouldSort: true,
distance: 200,
keys: [
{
name: 'name',
weight: 1,
},
],
getFn: (item, path) => {
if (
(path === 'name' || (path.length === 1 && path[0] === 'name')) &&
item.name === ''
) {
return removeDiacritics(
window.i18n('icu:calling__call-link-default-title')
);
}
return fuseGetFnRemoveDiacritics(item, path);
},
};
function searchCallLinks(
callLinks: ReadonlyArray<CallLinkType>,
searchTerm: string
): ReadonlyArray<Pick<Fuse.FuseResult<CallLinkType>, 'item' | 'score'>> {
// TODO: DESKTOP-6974
// Escape the search term
const extendedSearchTerm = removeDiacritics(searchTerm);
const index = getCachedFuseIndex(callLinks, FUSE_OPTIONS);
return index.search(extendedSearchTerm);
}
function startsWithLetter(title: string) {
return /^\p{Letter}/u.test(title);
}
function sortAlphabetically(a: CallLinkType, b: CallLinkType) {
const aStartsWithLetter = startsWithLetter(a.name);
const bStartsWithLetter = startsWithLetter(b.name);
if (aStartsWithLetter && !bStartsWithLetter) {
return -1;
}
if (!aStartsWithLetter && bStartsWithLetter) {
return 1;
}
return a.name.localeCompare(b.name);
}
export function filterCallLinks(
callLinks: ReadonlyArray<CallLinkType>,
searchTerm: string
): Array<CallLinkType> {
if (searchTerm.length) {
return searchCallLinks(callLinks, searchTerm)
.slice()
.sort((a, b) => {
const score = (a.score ?? 0) - (b.score ?? 0);
if (score !== 0) {
return score;
}
return sortAlphabetically(a.item, b.item);
})
.map(result => result.item);
}
return callLinks.concat().sort((a, b) => {
return sortAlphabetically(a, b);
});
}