Backup: Support for calls
This commit is contained in:
parent
3a631a587f
commit
63e14a7df6
15 changed files with 1047 additions and 116 deletions
|
@ -276,7 +276,7 @@ message AdHocCall {
|
|||
}
|
||||
|
||||
uint64 callId = 1;
|
||||
// Refers to a `CallLink` recipient.
|
||||
// Refers to a Recipient with the `callLink` field set
|
||||
uint64 recipientId = 2;
|
||||
State state = 3;
|
||||
uint64 callTimestamp = 4;
|
||||
|
@ -703,6 +703,7 @@ message IndividualCall {
|
|||
Direction direction = 3;
|
||||
State state = 4;
|
||||
uint64 startedCallTimestamp = 5;
|
||||
bool read = 6;
|
||||
}
|
||||
|
||||
message GroupCall {
|
||||
|
@ -734,6 +735,7 @@ message GroupCall {
|
|||
uint64 startedCallTimestamp = 5;
|
||||
// The time the call ended. 0 indicates an unknown time.
|
||||
uint64 endedCallTimestamp = 6;
|
||||
bool read = 7;
|
||||
}
|
||||
|
||||
message SimpleChatUpdate {
|
||||
|
|
|
@ -7,6 +7,8 @@ import type { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
|||
import pMap from 'p-map';
|
||||
import pTimeout from 'p-timeout';
|
||||
import { Readable } from 'stream';
|
||||
import { isNumber } from 'lodash';
|
||||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||
|
||||
import { Backups, SignalService } from '../../protobuf';
|
||||
import {
|
||||
|
@ -89,7 +91,18 @@ import { BACKUP_VERSION } from './constants';
|
|||
import { getMessageIdForLogging } from '../../util/idForLogging';
|
||||
import { getCallsHistoryForRedux } from '../callHistoryLoader';
|
||||
import { makeLookup } from '../../util/makeLookup';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import type {
|
||||
CallHistoryDetails,
|
||||
CallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import {
|
||||
CallMode,
|
||||
CallDirection,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
AdhocCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import { isAciString } from '../../util/isAciString';
|
||||
import { hslToRGB } from '../../util/hslToRGB';
|
||||
import type { AboutMe, LocalChatStyle } from './types';
|
||||
|
@ -113,6 +126,10 @@ import { AttachmentBackupManager } from '../../jobs/AttachmentBackupManager';
|
|||
import { getBackupCdnInfo } from './util/mediaId';
|
||||
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||
import { toAdminKeyBytes } from '../../util/callLinks';
|
||||
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
|
||||
import { SeenStatus } from '../../MessageSeenStatus';
|
||||
|
||||
const MAX_CONCURRENCY = 10;
|
||||
|
||||
|
@ -180,6 +197,7 @@ export class BackupExportStream extends Readable {
|
|||
|
||||
private readonly backupTimeMs = getSafeLongFromTimestamp(this.now);
|
||||
private readonly convoIdToRecipientId = new Map<string, number>();
|
||||
private readonly roomIdToRecipientId = new Map<string, number>();
|
||||
private attachmentBackupJobs: Array<CoreAttachmentBackupJobType> = [];
|
||||
private buffers = new Array<Uint8Array>();
|
||||
private nextRecipientId = 0;
|
||||
|
@ -230,6 +248,8 @@ export class BackupExportStream extends Readable {
|
|||
await this.flush();
|
||||
|
||||
const stats = {
|
||||
adHocCalls: 0,
|
||||
callLinks: 0,
|
||||
conversations: 0,
|
||||
chats: 0,
|
||||
distributionLists: 0,
|
||||
|
@ -283,7 +303,7 @@ export class BackupExportStream extends Readable {
|
|||
|
||||
this.pushFrame({
|
||||
recipient: {
|
||||
id: this.getDistributionListRecipientId(),
|
||||
id: Long.fromNumber(this.getNextRecipientId()),
|
||||
distributionList: {
|
||||
distributionId: uuidToBytes(list.id),
|
||||
deletionTimestamp: list.deletedAtTimestamp
|
||||
|
@ -309,6 +329,48 @@ export class BackupExportStream extends Readable {
|
|||
stats.distributionLists += 1;
|
||||
}
|
||||
|
||||
const callLinks = await DataReader.getAllCallLinks();
|
||||
|
||||
for (const link of callLinks) {
|
||||
const {
|
||||
rootKey: rootKeyString,
|
||||
adminKey,
|
||||
name,
|
||||
restrictions,
|
||||
revoked,
|
||||
expiration,
|
||||
} = link;
|
||||
|
||||
if (revoked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const id = this.getNextRecipientId();
|
||||
const rootKey = CallLinkRootKey.parse(rootKeyString);
|
||||
const roomId = getRoomIdFromRootKey(rootKey);
|
||||
|
||||
this.roomIdToRecipientId.set(roomId, id);
|
||||
|
||||
this.pushFrame({
|
||||
recipient: {
|
||||
id: Long.fromNumber(id),
|
||||
callLink: {
|
||||
rootKey: rootKey.bytes,
|
||||
adminKey: adminKey ? toAdminKeyBytes(adminKey) : null,
|
||||
name,
|
||||
restrictions: toCallLinkRestrictionsProto(restrictions),
|
||||
expirationMs: isNumber(expiration)
|
||||
? Long.fromNumber(expiration)
|
||||
: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.flush();
|
||||
stats.callLinks += 1;
|
||||
}
|
||||
|
||||
const stickerPacks = await DataReader.getInstalledStickerPacks();
|
||||
|
||||
for (const { id, key } of stickerPacks) {
|
||||
|
@ -376,6 +438,37 @@ export class BackupExportStream extends Readable {
|
|||
stats.chats += 1;
|
||||
}
|
||||
|
||||
const allCallHistoryItems = await DataReader.getAllCallHistory();
|
||||
|
||||
for (const item of allCallHistoryItems) {
|
||||
const { callId, type, peerId: roomId, status, timestamp } = item;
|
||||
|
||||
if (type !== CallType.Adhoc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recipientId = this.roomIdToRecipientId.get(roomId);
|
||||
if (!recipientId) {
|
||||
log.warn(
|
||||
`backups: Dropping ad-hoc call; recipientId for roomId ${roomId.slice(-2)} not found`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.pushFrame({
|
||||
adHocCall: {
|
||||
callId: Long.fromString(callId),
|
||||
recipientId: Long.fromNumber(recipientId),
|
||||
state: toAdHocCallStateProto(status),
|
||||
callTimestamp: Long.fromNumber(timestamp),
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.flush();
|
||||
stats.adHocCalls += 1;
|
||||
}
|
||||
|
||||
let cursor: PageMessagesCursorType | undefined;
|
||||
|
||||
const callHistory = getCallsHistoryForRedux();
|
||||
|
@ -640,11 +733,11 @@ export class BackupExportStream extends Readable {
|
|||
return result;
|
||||
}
|
||||
|
||||
private getDistributionListRecipientId(): Long {
|
||||
private getNextRecipientId(): number {
|
||||
const recipientId = this.nextRecipientId;
|
||||
this.nextRecipientId += 1;
|
||||
|
||||
return Long.fromNumber(recipientId);
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
private toRecipient(
|
||||
|
@ -1082,7 +1175,7 @@ export class BackupExportStream extends Readable {
|
|||
async toChatItemUpdate(
|
||||
options: NonBubbleOptionsType
|
||||
): Promise<NonBubbleResultType> {
|
||||
const { authorId, message } = options;
|
||||
const { authorId, callHistoryByCallId, message } = options;
|
||||
const logId = `toChatItemUpdate(${getMessageIdForLogging(message)})`;
|
||||
|
||||
const updateMessage = new Backups.ChatUpdateMessage();
|
||||
|
@ -1092,97 +1185,83 @@ export class BackupExportStream extends Readable {
|
|||
};
|
||||
|
||||
if (isCallHistory(message)) {
|
||||
// TODO (DESKTOP-6964)
|
||||
// const callingMessage = new Backups.CallChatUpdate();
|
||||
// const { callId } = message;
|
||||
// if (!callId) {
|
||||
// throw new Error(
|
||||
// `${logId}: Message was callHistory, but missing callId!`
|
||||
// );
|
||||
// }
|
||||
// const callHistory = callHistoryByCallId[callId];
|
||||
// if (!callHistory) {
|
||||
// throw new Error(
|
||||
// `${logId}: Message had callId, but no call history details were found!`
|
||||
// );
|
||||
// }
|
||||
// callingMessage.callId = Long.fromString(callId);
|
||||
// if (callHistory.mode === CallMode.Group) {
|
||||
// const groupCall = new Backups.GroupCallChatUpdate();
|
||||
// const { ringerId } = callHistory;
|
||||
// if (!ringerId) {
|
||||
// throw new Error(
|
||||
// `${logId}: Message had missing ringerId for a group call!`
|
||||
// );
|
||||
// }
|
||||
// groupCall.startedCallAci = this.aciToBytes(ringerId);
|
||||
// groupCall.startedCallTimestamp = Long.fromNumber(callHistory.timestamp);
|
||||
// // Note: we don't store inCallACIs, instead relying on RingRTC in-memory state
|
||||
// callingMessage.groupCall = groupCall;
|
||||
// } else {
|
||||
// const callMessage = new Backups.IndividualCallChatUpdate();
|
||||
// const { direction, type, status } = callHistory;
|
||||
// if (
|
||||
// status === DirectCallStatus.Accepted ||
|
||||
// status === DirectCallStatus.Pending
|
||||
// ) {
|
||||
// if (type === CallType.Audio) {
|
||||
// callMessage.type =
|
||||
// direction === CallDirection.Incoming
|
||||
// ? Backups.IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL
|
||||
// : Backups.IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL;
|
||||
// } else if (type === CallType.Video) {
|
||||
// callMessage.type =
|
||||
// direction === CallDirection.Incoming
|
||||
// ? Backups.IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL
|
||||
// : Backups.IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL;
|
||||
// } else {
|
||||
// throw new Error(
|
||||
// `${logId}: Message direct status '${status}' call had type ${type}`
|
||||
// );
|
||||
// }
|
||||
// } else if (status === DirectCallStatus.Declined) {
|
||||
// if (direction === CallDirection.Incoming) {
|
||||
// // question: do we really not call declined calls things that we decline?
|
||||
// throw new Error(
|
||||
// `${logId}: Message direct call was declined but incoming`
|
||||
// );
|
||||
// }
|
||||
// if (type === CallType.Audio) {
|
||||
// callMessage.type =
|
||||
// Backups.IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_AUDIO_CALL;
|
||||
// } else if (type === CallType.Video) {
|
||||
// callMessage.type =
|
||||
// Backups.IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_VIDEO_CALL;
|
||||
// } else {
|
||||
// throw new Error(
|
||||
// `${logId}: Message direct status '${status}' call had type ${type}`
|
||||
// );
|
||||
// }
|
||||
// } else if (status === DirectCallStatus.Missed) {
|
||||
// if (direction === CallDirection.Outgoing) {
|
||||
// throw new Error(
|
||||
// `${logId}: Message direct call was missed but outgoing`
|
||||
// );
|
||||
// }
|
||||
// if (type === CallType.Audio) {
|
||||
// callMessage.type =
|
||||
// Backups.IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL;
|
||||
// } else if (type === CallType.Video) {
|
||||
// callMessage.type =
|
||||
// Backups.IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL;
|
||||
// } else {
|
||||
// throw new Error(
|
||||
// `${logId}: Message direct status '${status}' call had type ${type}`
|
||||
// );
|
||||
// }
|
||||
// } else {
|
||||
// throw new Error(`${logId}: Message direct call had status ${status}`);
|
||||
// }
|
||||
// callingMessage.callMessage = callMessage;
|
||||
// }
|
||||
// updateMessage.callingMessage = callingMessage;
|
||||
// return chatItem;
|
||||
const conversation = window.ConversationController.get(
|
||||
message.conversationId
|
||||
);
|
||||
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
`${logId}: callHistory message had unknown conversationId!`
|
||||
);
|
||||
}
|
||||
|
||||
const { callId } = message;
|
||||
if (!callId) {
|
||||
throw new Error(`${logId}: callHistory message was missing callId!`);
|
||||
}
|
||||
|
||||
const callHistory = callHistoryByCallId[callId];
|
||||
if (!callHistory) {
|
||||
throw new Error(
|
||||
`${logId}: callHistory message had callId, but no call history details were found!`
|
||||
);
|
||||
}
|
||||
|
||||
if (isGroup(conversation.attributes)) {
|
||||
const groupCall = new Backups.GroupCall();
|
||||
|
||||
strictAssert(
|
||||
callHistory.mode === CallMode.Group,
|
||||
'in group, should be group call'
|
||||
);
|
||||
|
||||
if (callHistory.status === GroupCallStatus.Deleted) {
|
||||
return { kind: NonBubbleResultKind.Drop };
|
||||
}
|
||||
|
||||
const { ringerId } = callHistory;
|
||||
if (ringerId) {
|
||||
const ringerConversation =
|
||||
window.ConversationController.get(ringerId);
|
||||
if (!ringerConversation) {
|
||||
throw new Error(
|
||||
'toChatItemUpdate/callHistory: ringerId conversation not found!'
|
||||
);
|
||||
}
|
||||
|
||||
const recipientId = this.getRecipientId(
|
||||
ringerConversation.attributes
|
||||
);
|
||||
groupCall.ringerRecipientId = recipientId;
|
||||
groupCall.startedCallRecipientId = recipientId;
|
||||
}
|
||||
|
||||
groupCall.callId = Long.fromString(callId);
|
||||
groupCall.state = toGroupCallStateProto(callHistory.status);
|
||||
groupCall.startedCallTimestamp = Long.fromNumber(callHistory.timestamp);
|
||||
groupCall.endedCallTimestamp = Long.fromNumber(0);
|
||||
groupCall.read = message.seenStatus === SeenStatus.Seen;
|
||||
|
||||
updateMessage.groupCall = groupCall;
|
||||
return { kind: NonBubbleResultKind.Directionless, patch };
|
||||
}
|
||||
|
||||
const individualCall = new Backups.IndividualCall();
|
||||
const { direction, type, status, timestamp } = callHistory;
|
||||
|
||||
if (status === GroupCallStatus.Deleted) {
|
||||
return { kind: NonBubbleResultKind.Drop };
|
||||
}
|
||||
|
||||
individualCall.callId = Long.fromString(callId);
|
||||
individualCall.type = toIndividualCallTypeProto(type);
|
||||
individualCall.direction = toIndividualCallDirectionProto(direction);
|
||||
individualCall.state = toIndividualCallStateProto(status);
|
||||
individualCall.startedCallTimestamp = Long.fromNumber(timestamp);
|
||||
individualCall.read = message.seenStatus === SeenStatus.Seen;
|
||||
|
||||
updateMessage.individualCall = individualCall;
|
||||
return { kind: NonBubbleResultKind.Directionless, patch };
|
||||
}
|
||||
|
||||
if (isExpirationTimerUpdate(message)) {
|
||||
|
@ -2484,3 +2563,127 @@ function hslToRGBInt(hue: number, saturation: number): number {
|
|||
// eslint-disable-next-line no-bitwise
|
||||
return ((0xff << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
||||
}
|
||||
|
||||
function toGroupCallStateProto(state: CallStatus): Backups.GroupCall.State {
|
||||
const values = Backups.GroupCall.State;
|
||||
|
||||
if (state === GroupCallStatus.GenericGroupCall) {
|
||||
return values.GENERIC;
|
||||
}
|
||||
if (state === GroupCallStatus.OutgoingRing) {
|
||||
return values.OUTGOING_RING;
|
||||
}
|
||||
if (state === GroupCallStatus.Ringing) {
|
||||
return values.RINGING;
|
||||
}
|
||||
if (state === GroupCallStatus.Joined) {
|
||||
return values.JOINED;
|
||||
}
|
||||
if (state === GroupCallStatus.Accepted) {
|
||||
return values.ACCEPTED;
|
||||
}
|
||||
if (state === GroupCallStatus.Missed) {
|
||||
return values.MISSED;
|
||||
}
|
||||
if (state === GroupCallStatus.MissedNotificationProfile) {
|
||||
return values.MISSED_NOTIFICATION_PROFILE;
|
||||
}
|
||||
if (state === GroupCallStatus.Declined) {
|
||||
return values.DECLINED;
|
||||
}
|
||||
if (state === GroupCallStatus.Deleted) {
|
||||
throw new Error(
|
||||
'groupCallStatusToGroupCallState: Never back up deleted items!'
|
||||
);
|
||||
}
|
||||
|
||||
return values.UNKNOWN_STATE;
|
||||
}
|
||||
|
||||
function toIndividualCallDirectionProto(
|
||||
direction: CallDirection
|
||||
): Backups.IndividualCall.Direction {
|
||||
const values = Backups.IndividualCall.Direction;
|
||||
|
||||
if (direction === CallDirection.Incoming) {
|
||||
return values.INCOMING;
|
||||
}
|
||||
if (direction === CallDirection.Outgoing) {
|
||||
return values.OUTGOING;
|
||||
}
|
||||
|
||||
return values.UNKNOWN_DIRECTION;
|
||||
}
|
||||
|
||||
function toIndividualCallTypeProto(
|
||||
type: CallType
|
||||
): Backups.IndividualCall.Type {
|
||||
const values = Backups.IndividualCall.Type;
|
||||
|
||||
if (type === CallType.Audio) {
|
||||
return values.AUDIO_CALL;
|
||||
}
|
||||
if (type === CallType.Video) {
|
||||
return values.VIDEO_CALL;
|
||||
}
|
||||
|
||||
return values.UNKNOWN_TYPE;
|
||||
}
|
||||
|
||||
function toIndividualCallStateProto(
|
||||
status: CallStatus
|
||||
): Backups.IndividualCall.State {
|
||||
const values = Backups.IndividualCall.State;
|
||||
|
||||
if (status === DirectCallStatus.Accepted) {
|
||||
return values.ACCEPTED;
|
||||
}
|
||||
if (status === DirectCallStatus.Declined) {
|
||||
return values.NOT_ACCEPTED;
|
||||
}
|
||||
if (status === DirectCallStatus.Missed) {
|
||||
return values.MISSED;
|
||||
}
|
||||
if (status === DirectCallStatus.MissedNotificationProfile) {
|
||||
return values.MISSED_NOTIFICATION_PROFILE;
|
||||
}
|
||||
|
||||
if (status === DirectCallStatus.Deleted) {
|
||||
throw new Error(
|
||||
'statusToIndividualCallProtoEnum: Never back up deleted items!'
|
||||
);
|
||||
}
|
||||
|
||||
return values.UNKNOWN_STATE;
|
||||
}
|
||||
|
||||
function toAdHocCallStateProto(status: CallStatus): Backups.AdHocCall.State {
|
||||
const values = Backups.AdHocCall.State;
|
||||
|
||||
if (status === AdhocCallStatus.Generic) {
|
||||
return values.GENERIC;
|
||||
}
|
||||
if (status === AdhocCallStatus.Joined) {
|
||||
return values.GENERIC;
|
||||
}
|
||||
if (status === AdhocCallStatus.Pending) {
|
||||
return values.GENERIC;
|
||||
}
|
||||
|
||||
return values.UNKNOWN_STATE;
|
||||
}
|
||||
|
||||
function toCallLinkRestrictionsProto(
|
||||
restrictions: CallLinkRestrictions
|
||||
): Backups.CallLink.Restrictions {
|
||||
const values = Backups.CallLink.Restrictions;
|
||||
|
||||
if (restrictions === CallLinkRestrictions.None) {
|
||||
return values.NONE;
|
||||
}
|
||||
if (restrictions === CallLinkRestrictions.AdminApproval) {
|
||||
return values.ADMIN_APPROVAL;
|
||||
}
|
||||
|
||||
return values.UNKNOWN;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { v4 as generateUuid } from 'uuid';
|
|||
import pMap from 'p-map';
|
||||
import { Writable } from 'stream';
|
||||
import { isNumber } from 'lodash';
|
||||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||
|
||||
import { Backups, SignalService } from '../../protobuf';
|
||||
import { DataWriter } from '../../sql/Client';
|
||||
|
@ -84,6 +85,20 @@ import { filterAndClean } from '../../types/BodyRange';
|
|||
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../../types/MIME';
|
||||
import { copyFromQuotedMessage } from '../../messages/copyQuote';
|
||||
import { groupAvatarJobQueue } from '../../jobs/groupAvatarJobQueue';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
CallDirection,
|
||||
CallMode,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
|
||||
import { fromAdminKeyBytes } from '../../util/callLinks';
|
||||
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
|
||||
|
||||
const MAX_CONCURRENCY = 10;
|
||||
|
||||
|
@ -231,6 +246,7 @@ export class BackupImportStream extends Writable {
|
|||
number,
|
||||
ConversationAttributesType
|
||||
>();
|
||||
private readonly recipientIdToCallLink = new Map<number, CallLinkType>();
|
||||
private readonly chatIdToConvo = new Map<
|
||||
number,
|
||||
ConversationAttributesType
|
||||
|
@ -382,6 +398,8 @@ export class BackupImportStream extends Writable {
|
|||
if (frame.recipient) {
|
||||
const { recipient } = frame;
|
||||
strictAssert(recipient.id != null, 'Recipient must have an id');
|
||||
const recipientId = recipient.id.toNumber();
|
||||
|
||||
let convo: ConversationAttributesType;
|
||||
if (recipient.contact) {
|
||||
convo = await this.fromContact(recipient.contact);
|
||||
|
@ -402,6 +420,11 @@ export class BackupImportStream extends Writable {
|
|||
} else if (recipient.distributionList) {
|
||||
await this.fromDistributionList(recipient.distributionList);
|
||||
|
||||
// Not a conversation
|
||||
return;
|
||||
} else if (recipient.callLink) {
|
||||
await this.fromCallLink(recipientId, recipient.callLink);
|
||||
|
||||
// Not a conversation
|
||||
return;
|
||||
} else {
|
||||
|
@ -413,7 +436,7 @@ export class BackupImportStream extends Writable {
|
|||
this.saveConversation(convo);
|
||||
}
|
||||
|
||||
this.recipientIdToConvo.set(recipient.id.toNumber(), convo);
|
||||
this.recipientIdToConvo.set(recipientId, convo);
|
||||
} else if (frame.chat) {
|
||||
await this.fromChat(frame.chat);
|
||||
} else if (frame.chatItem) {
|
||||
|
@ -426,6 +449,8 @@ export class BackupImportStream extends Writable {
|
|||
await this.fromChatItem(frame.chatItem, { aboutMe });
|
||||
} else if (frame.stickerPack) {
|
||||
await this.fromStickerPack(frame.stickerPack);
|
||||
} else if (frame.adHocCall) {
|
||||
await this.fromAdHocCall(frame.adHocCall);
|
||||
} else {
|
||||
log.warn(`${this.logId}: unsupported frame item ${frame.item}`);
|
||||
}
|
||||
|
@ -463,6 +488,12 @@ export class BackupImportStream extends Writable {
|
|||
this.saveMessageBatcher.add(attributes);
|
||||
}
|
||||
|
||||
private async saveCallHistory(
|
||||
callHistory: CallHistoryDetails
|
||||
): Promise<void> {
|
||||
await DataWriter.saveCallHistory(callHistory);
|
||||
}
|
||||
|
||||
private async fromAccount({
|
||||
profileKey,
|
||||
username,
|
||||
|
@ -987,6 +1018,38 @@ export class BackupImportStream extends Writable {
|
|||
await DataWriter.createNewStoryDistribution(result);
|
||||
}
|
||||
|
||||
private async fromCallLink(
|
||||
recipientId: number,
|
||||
callLinkProto: Backups.ICallLink
|
||||
): Promise<void> {
|
||||
const {
|
||||
rootKey: rootKeyBytes,
|
||||
adminKey,
|
||||
name,
|
||||
restrictions,
|
||||
expirationMs,
|
||||
} = callLinkProto;
|
||||
|
||||
strictAssert(rootKeyBytes?.length, 'fromCallLink: rootKey is required');
|
||||
strictAssert(name, 'fromCallLink: name is required');
|
||||
|
||||
const rootKey = CallLinkRootKey.fromBytes(Buffer.from(rootKeyBytes));
|
||||
|
||||
const callLink: CallLinkType = {
|
||||
roomId: getRoomIdFromRootKey(rootKey),
|
||||
rootKey: rootKey.toString(),
|
||||
adminKey: adminKey?.length ? fromAdminKeyBytes(adminKey) : null,
|
||||
name,
|
||||
restrictions: fromCallLinkRestrictionsProto(restrictions),
|
||||
revoked: false,
|
||||
expiration: expirationMs?.toNumber() || null,
|
||||
};
|
||||
|
||||
this.recipientIdToCallLink.set(recipientId, callLink);
|
||||
|
||||
await DataWriter.insertCallLink(callLink);
|
||||
}
|
||||
|
||||
private async fromChat(chat: Backups.IChat): Promise<void> {
|
||||
strictAssert(chat.id != null, 'chat must have an id');
|
||||
strictAssert(chat.recipientId != null, 'chat must have a recipientId');
|
||||
|
@ -1329,7 +1392,12 @@ export class BackupImportStream extends Writable {
|
|||
}
|
||||
|
||||
strictAssert(directionless, 'Absent direction state');
|
||||
return { patch: {} };
|
||||
return {
|
||||
patch: {
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async fromStandardMessage(
|
||||
|
@ -1757,7 +1825,7 @@ export class BackupImportStream extends Writable {
|
|||
timestamp: number;
|
||||
}
|
||||
): Promise<ChatItemParseResult | undefined> {
|
||||
const { aboutMe, author } = options;
|
||||
const { aboutMe, author, conversation } = options;
|
||||
|
||||
if (updateMessage.groupChange) {
|
||||
return this.fromGroupUpdateMessage(updateMessage.groupChange, options);
|
||||
|
@ -1870,8 +1938,117 @@ export class BackupImportStream extends Writable {
|
|||
};
|
||||
}
|
||||
|
||||
// TODO (DESKTOP-6964): check these fields
|
||||
// updateMessage.callingMessage
|
||||
if (updateMessage.groupCall) {
|
||||
const { groupId } = conversation;
|
||||
|
||||
if (!isGroup(conversation)) {
|
||||
throw new Error('groupCall: Conversation is not a group!');
|
||||
}
|
||||
if (!groupId) {
|
||||
throw new Error('groupCall: No groupId available on conversation');
|
||||
}
|
||||
|
||||
const {
|
||||
callId: callIdLong,
|
||||
state,
|
||||
ringerRecipientId,
|
||||
startedCallRecipientId,
|
||||
startedCallTimestamp,
|
||||
read,
|
||||
} = updateMessage.groupCall;
|
||||
|
||||
const initiator =
|
||||
ringerRecipientId?.toNumber() || startedCallRecipientId?.toNumber();
|
||||
const ringer = isNumber(initiator)
|
||||
? this.recipientIdToConvo.get(initiator)
|
||||
: undefined;
|
||||
|
||||
if (!callIdLong) {
|
||||
throw new Error('groupCall: callId is required!');
|
||||
}
|
||||
if (!startedCallTimestamp) {
|
||||
throw new Error('groupCall: startedCallTimestamp is required!');
|
||||
}
|
||||
const isRingerMe = ringer?.serviceId === aboutMe.aci;
|
||||
|
||||
const callId = callIdLong.toString();
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
status: fromGroupCallStateProto(state),
|
||||
mode: CallMode.Group,
|
||||
type: CallType.Group,
|
||||
ringerId: ringer?.serviceId ?? null,
|
||||
peerId: groupId,
|
||||
direction: isRingerMe ? CallDirection.Outgoing : CallDirection.Incoming,
|
||||
timestamp: startedCallTimestamp.toNumber(),
|
||||
};
|
||||
|
||||
await this.saveCallHistory(callHistory);
|
||||
|
||||
return {
|
||||
message: {
|
||||
type: 'call-history',
|
||||
callId,
|
||||
sourceServiceId: undefined,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: read ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||
},
|
||||
additionalMessages: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (updateMessage.individualCall) {
|
||||
const {
|
||||
callId: callIdLong,
|
||||
type,
|
||||
direction: protoDirection,
|
||||
state,
|
||||
startedCallTimestamp,
|
||||
read,
|
||||
} = updateMessage.individualCall;
|
||||
|
||||
if (!callIdLong) {
|
||||
throw new Error('individualCall: callId is required!');
|
||||
}
|
||||
if (!startedCallTimestamp) {
|
||||
throw new Error('individualCall: startedCallTimestamp is required!');
|
||||
}
|
||||
|
||||
const peerId = conversation.serviceId || conversation.e164;
|
||||
strictAssert(peerId, 'individualCall: no peerId found for call');
|
||||
|
||||
const callId = callIdLong.toString();
|
||||
const direction = fromIndividualCallDirectionProto(protoDirection);
|
||||
const ringerId =
|
||||
direction === CallDirection.Outgoing
|
||||
? aboutMe.aci
|
||||
: conversation.serviceId;
|
||||
strictAssert(ringerId, 'individualCall: no ringerId found');
|
||||
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
status: fromIndividualCallStateProto(state),
|
||||
mode: CallMode.Direct,
|
||||
type: fromIndividualCallTypeProto(type),
|
||||
ringerId,
|
||||
peerId,
|
||||
direction,
|
||||
timestamp: startedCallTimestamp.toNumber(),
|
||||
};
|
||||
|
||||
await this.saveCallHistory(callHistory);
|
||||
|
||||
return {
|
||||
message: {
|
||||
type: 'call-history',
|
||||
callId,
|
||||
sourceServiceId: undefined,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: read ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||
},
|
||||
additionalMessages: [],
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
@ -2572,6 +2749,44 @@ export class BackupImportStream extends Writable {
|
|||
);
|
||||
}
|
||||
|
||||
private async fromAdHocCall({
|
||||
callId: callIdLong,
|
||||
recipientId: recipientIdLong,
|
||||
state,
|
||||
callTimestamp,
|
||||
}: Backups.IAdHocCall): Promise<void> {
|
||||
strictAssert(callIdLong, 'AdHocCall must have a callId');
|
||||
|
||||
const callId = callIdLong.toString();
|
||||
const logId = `fromAdhocCall(${callId.slice(-2)})`;
|
||||
|
||||
strictAssert(callTimestamp, `${logId}: must have a valid timestamp`);
|
||||
strictAssert(recipientIdLong, 'AdHocCall must have a recipientIdLong');
|
||||
|
||||
const recipientId = recipientIdLong.toNumber();
|
||||
const callLink = this.recipientIdToCallLink.get(recipientId);
|
||||
|
||||
if (!callLink) {
|
||||
log.warn(
|
||||
`${logId}: Dropping ad-hoc call, Call Link for recipientId ${recipientId} not found`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
peerId: callLink.roomId,
|
||||
ringerId: null,
|
||||
mode: CallMode.Adhoc,
|
||||
type: CallType.Adhoc,
|
||||
direction: CallDirection.Unknown,
|
||||
timestamp: callTimestamp.toNumber(),
|
||||
status: fromAdHocCallStateProto(state),
|
||||
};
|
||||
|
||||
await this.saveCallHistory(callHistory);
|
||||
}
|
||||
|
||||
private async fromCustomChatColors(
|
||||
customChatColors:
|
||||
| ReadonlyArray<Backups.ChatStyle.ICustomChatColor>
|
||||
|
@ -2766,3 +2981,133 @@ function rgbIntToHSL(intValue: number): { hue: number; saturation: number } {
|
|||
|
||||
return { hue, saturation };
|
||||
}
|
||||
|
||||
function fromGroupCallStateProto(
|
||||
state: Backups.GroupCall.State | undefined | null
|
||||
): GroupCallStatus {
|
||||
const values = Backups.GroupCall.State;
|
||||
|
||||
if (state == null) {
|
||||
return GroupCallStatus.GenericGroupCall;
|
||||
}
|
||||
|
||||
if (state === values.GENERIC) {
|
||||
return GroupCallStatus.GenericGroupCall;
|
||||
}
|
||||
if (state === values.OUTGOING_RING) {
|
||||
return GroupCallStatus.OutgoingRing;
|
||||
}
|
||||
if (state === values.RINGING) {
|
||||
return GroupCallStatus.Ringing;
|
||||
}
|
||||
if (state === values.JOINED) {
|
||||
return GroupCallStatus.Joined;
|
||||
}
|
||||
if (state === values.ACCEPTED) {
|
||||
return GroupCallStatus.Accepted;
|
||||
}
|
||||
if (state === values.MISSED) {
|
||||
return GroupCallStatus.Missed;
|
||||
}
|
||||
if (state === values.MISSED_NOTIFICATION_PROFILE) {
|
||||
return GroupCallStatus.MissedNotificationProfile;
|
||||
}
|
||||
if (state === values.DECLINED) {
|
||||
return GroupCallStatus.Declined;
|
||||
}
|
||||
|
||||
return GroupCallStatus.GenericGroupCall;
|
||||
}
|
||||
|
||||
function fromIndividualCallDirectionProto(
|
||||
direction: Backups.IndividualCall.Direction | undefined | null
|
||||
): CallDirection {
|
||||
const values = Backups.IndividualCall.Direction;
|
||||
|
||||
if (direction == null) {
|
||||
return CallDirection.Unknown;
|
||||
}
|
||||
if (direction === values.INCOMING) {
|
||||
return CallDirection.Incoming;
|
||||
}
|
||||
if (direction === values.OUTGOING) {
|
||||
return CallDirection.Outgoing;
|
||||
}
|
||||
|
||||
return CallDirection.Unknown;
|
||||
}
|
||||
|
||||
function fromIndividualCallTypeProto(
|
||||
type: Backups.IndividualCall.Type | undefined | null
|
||||
): CallType {
|
||||
const values = Backups.IndividualCall.Type;
|
||||
|
||||
if (type == null) {
|
||||
return CallType.Unknown;
|
||||
}
|
||||
if (type === values.AUDIO_CALL) {
|
||||
return CallType.Audio;
|
||||
}
|
||||
if (type === values.VIDEO_CALL) {
|
||||
return CallType.Video;
|
||||
}
|
||||
|
||||
return CallType.Unknown;
|
||||
}
|
||||
|
||||
function fromIndividualCallStateProto(
|
||||
status: Backups.IndividualCall.State | undefined | null
|
||||
): DirectCallStatus {
|
||||
const values = Backups.IndividualCall.State;
|
||||
|
||||
if (status == null) {
|
||||
return DirectCallStatus.Unknown;
|
||||
}
|
||||
if (status === values.ACCEPTED) {
|
||||
return DirectCallStatus.Accepted;
|
||||
}
|
||||
if (status === values.NOT_ACCEPTED) {
|
||||
return DirectCallStatus.Declined;
|
||||
}
|
||||
if (status === values.MISSED) {
|
||||
return DirectCallStatus.Missed;
|
||||
}
|
||||
if (status === values.MISSED_NOTIFICATION_PROFILE) {
|
||||
return DirectCallStatus.MissedNotificationProfile;
|
||||
}
|
||||
|
||||
return DirectCallStatus.Unknown;
|
||||
}
|
||||
|
||||
function fromAdHocCallStateProto(
|
||||
status: Backups.AdHocCall.State | undefined | null
|
||||
): AdhocCallStatus {
|
||||
const values = Backups.AdHocCall.State;
|
||||
|
||||
if (status == null) {
|
||||
return AdhocCallStatus.Unknown;
|
||||
}
|
||||
if (status === values.GENERIC) {
|
||||
return AdhocCallStatus.Generic;
|
||||
}
|
||||
|
||||
return AdhocCallStatus.Unknown;
|
||||
}
|
||||
|
||||
function fromCallLinkRestrictionsProto(
|
||||
restrictions: Backups.CallLink.Restrictions | undefined | null
|
||||
): CallLinkRestrictions {
|
||||
const values = Backups.CallLink.Restrictions;
|
||||
|
||||
if (restrictions == null) {
|
||||
return CallLinkRestrictions.Unknown;
|
||||
}
|
||||
if (restrictions === values.NONE) {
|
||||
return CallLinkRestrictions.None;
|
||||
}
|
||||
if (restrictions === values.ADMIN_APPROVAL) {
|
||||
return CallLinkRestrictions.AdminApproval;
|
||||
}
|
||||
|
||||
return CallLinkRestrictions.Unknown;
|
||||
}
|
||||
|
|
|
@ -779,6 +779,7 @@ type WritableInterface = {
|
|||
_removeAllMessages: () => void;
|
||||
|
||||
clearCallHistory: (target: CallLogEventTarget) => ReadonlyArray<string>;
|
||||
_removeAllCallHistory: () => void;
|
||||
markCallHistoryDeleted: (callId: string) => void;
|
||||
cleanupCallHistoryMessages: () => void;
|
||||
markCallHistoryRead(callId: string): void;
|
||||
|
@ -796,6 +797,7 @@ type WritableInterface = {
|
|||
beginDeleteAllCallLinks(): void;
|
||||
beginDeleteCallLink(roomId: string): void;
|
||||
finalizeDeleteCallLink(roomId: string): void;
|
||||
_removeAllCallLinks(): void;
|
||||
deleteCallLinkFromSync(roomId: string): void;
|
||||
migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
|
||||
saveEditedMessage: (
|
||||
|
|
|
@ -182,6 +182,7 @@ import {
|
|||
finalizeDeleteCallLink,
|
||||
beginDeleteCallLink,
|
||||
deleteCallLinkFromSync,
|
||||
_removeAllCallLinks,
|
||||
} from './server/callLinks';
|
||||
import {
|
||||
replaceAllEndorsementsForGroup,
|
||||
|
@ -425,6 +426,7 @@ export const DataWriter: ServerWritableInterface = {
|
|||
_removeAllMessages,
|
||||
getUnreadEditedMessagesAndMarkRead,
|
||||
clearCallHistory,
|
||||
_removeAllCallHistory,
|
||||
markCallHistoryDeleted,
|
||||
cleanupCallHistoryMessages,
|
||||
markCallHistoryRead,
|
||||
|
@ -438,6 +440,7 @@ export const DataWriter: ServerWritableInterface = {
|
|||
beginDeleteAllCallLinks,
|
||||
beginDeleteCallLink,
|
||||
finalizeDeleteCallLink,
|
||||
_removeAllCallLinks,
|
||||
deleteCallLinkFromSync,
|
||||
migrateConversationMessages,
|
||||
saveEditedMessage,
|
||||
|
@ -3472,6 +3475,13 @@ function getAllCallHistory(db: ReadableDB): ReadonlyArray<CallHistoryDetails> {
|
|||
return db.prepare(query).all();
|
||||
}
|
||||
|
||||
function _removeAllCallHistory(db: WritableDB): void {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM callsHistory;
|
||||
`;
|
||||
db.prepare(query).run(params);
|
||||
}
|
||||
|
||||
function clearCallHistory(
|
||||
db: WritableDB,
|
||||
target: CallLogEventTarget
|
||||
|
|
|
@ -237,3 +237,10 @@ export function finalizeDeleteCallLink(db: WritableDB, roomId: string): void {
|
|||
`;
|
||||
db.prepare(query).run(params);
|
||||
}
|
||||
|
||||
export function _removeAllCallLinks(db: WritableDB): void {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM callLinks;
|
||||
`;
|
||||
db.prepare(query).run(params);
|
||||
}
|
||||
|
|
|
@ -39,9 +39,9 @@ describe('backup/attachments', () => {
|
|||
let contactA: ConversationModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DataWriter._removeAllMessages();
|
||||
await DataWriter._removeAllConversations();
|
||||
await DataWriter.removeAll();
|
||||
window.storage.reset();
|
||||
window.ConversationController.reset();
|
||||
|
||||
await setupBasics();
|
||||
|
||||
|
@ -69,7 +69,9 @@ describe('backup/attachments', () => {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
await DataWriter.removeAll();
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
asymmetricRoundtripHarness,
|
||||
symmetricRoundtripHarness,
|
||||
} from './helpers';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { SeenStatus } from '../../MessageSeenStatus';
|
||||
|
||||
// Note: this should be kept up to date with GroupV2Change.stories.tsx, to
|
||||
// maintain the comprehensive set of GroupV2 notifications we need to handle
|
||||
|
@ -66,6 +68,8 @@ function createMessage(
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
type: 'group-v2-change',
|
||||
sourceServiceId,
|
||||
};
|
||||
|
@ -73,8 +77,8 @@ function createMessage(
|
|||
|
||||
describe('backup/groupv2/notifications', () => {
|
||||
beforeEach(async () => {
|
||||
await DataWriter._removeAllMessages();
|
||||
await DataWriter._removeAllConversations();
|
||||
await DataWriter.removeAll();
|
||||
window.ConversationController.reset();
|
||||
window.storage.reset();
|
||||
|
||||
await setupBasics();
|
||||
|
@ -112,6 +116,9 @@ describe('backup/groupv2/notifications', () => {
|
|||
|
||||
await loadCallsHistory();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await DataWriter.removeAll();
|
||||
});
|
||||
|
||||
describe('roundtrips given groupv2 notifications with', () => {
|
||||
it('Multiple items', async () => {
|
||||
|
@ -2031,6 +2038,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
};
|
||||
|
||||
|
@ -2047,6 +2056,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
};
|
||||
|
||||
|
@ -2076,6 +2087,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
};
|
||||
|
||||
counter += 1;
|
||||
|
@ -2092,6 +2105,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
};
|
||||
|
||||
const messages: Array<MessageAttributesType> = [
|
||||
|
@ -2124,6 +2139,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: OUR_ACI,
|
||||
};
|
||||
|
||||
|
@ -2140,6 +2157,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: OUR_ACI,
|
||||
};
|
||||
|
||||
|
@ -2156,6 +2175,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: OUR_ACI,
|
||||
};
|
||||
|
||||
|
@ -2188,6 +2209,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: OUR_ACI,
|
||||
};
|
||||
const legacyAfter = {
|
||||
|
@ -2202,6 +2225,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: OUR_ACI,
|
||||
};
|
||||
|
||||
|
@ -2221,6 +2246,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: OUR_ACI,
|
||||
};
|
||||
const allDataAfter = {
|
||||
|
@ -2235,6 +2262,8 @@ describe('backup/groupv2/notifications', () => {
|
|||
received_at: counter,
|
||||
sent_at: counter,
|
||||
timestamp: counter,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: OUR_ACI,
|
||||
};
|
||||
|
||||
|
|
266
ts/test-electron/backup/calling_test.ts
Normal file
266
ts/test-electron/backup/calling_test.ts
Normal file
|
@ -0,0 +1,266 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import assert from 'assert';
|
||||
import { v4 as generateGuid } from 'uuid';
|
||||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||
|
||||
import type { ConversationModel } from '../../models/conversations';
|
||||
import type { MessageAttributesType } from '../../model-types';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
|
||||
import * as Bytes from '../../Bytes';
|
||||
import { getRandomBytes } from '../../Crypto';
|
||||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
import { loadCallsHistory } from '../../services/callHistoryLoader';
|
||||
import { setupBasics, symmetricRoundtripHarness } from './helpers';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
CallDirection,
|
||||
CallMode,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
|
||||
import { fromAdminKeyBytes } from '../../util/callLinks';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { SeenStatus } from '../../MessageSeenStatus';
|
||||
import { deriveGroupID, deriveGroupSecretParams } from '../../util/zkgroup';
|
||||
|
||||
const CONTACT_A = generateAci();
|
||||
const GROUP_MASTER_KEY = getRandomBytes(32);
|
||||
const GROUP_SECRET_PARAMS = deriveGroupSecretParams(GROUP_MASTER_KEY);
|
||||
const GROUP_ID_STRING = Bytes.toBase64(deriveGroupID(GROUP_SECRET_PARAMS));
|
||||
|
||||
describe('backup/calling', () => {
|
||||
let contactA: ConversationModel;
|
||||
let groupA: ConversationModel;
|
||||
let callLink: CallLinkType;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DataWriter.removeAll();
|
||||
window.ConversationController.reset();
|
||||
window.storage.reset();
|
||||
|
||||
await setupBasics();
|
||||
|
||||
contactA = await window.ConversationController.getOrCreateAndWait(
|
||||
CONTACT_A,
|
||||
'private',
|
||||
{ systemGivenName: 'CONTACT_A' }
|
||||
);
|
||||
groupA = await window.ConversationController.getOrCreateAndWait(
|
||||
GROUP_ID_STRING,
|
||||
'group',
|
||||
{
|
||||
groupVersion: 2,
|
||||
masterKey: Bytes.toBase64(GROUP_MASTER_KEY),
|
||||
name: 'Rock Enthusiasts',
|
||||
}
|
||||
);
|
||||
|
||||
const rootKey = CallLinkRootKey.generate();
|
||||
const adminKey = CallLinkRootKey.generateAdminPassKey();
|
||||
callLink = {
|
||||
rootKey: rootKey.toString(),
|
||||
roomId: getRoomIdFromRootKey(rootKey),
|
||||
// TODO: DESKTOP-7511
|
||||
adminKey: fromAdminKeyBytes(Buffer.concat([adminKey, adminKey])),
|
||||
name: "Let's Talk Rocks",
|
||||
restrictions: CallLinkRestrictions.AdminApproval,
|
||||
revoked: false,
|
||||
expiration: null,
|
||||
};
|
||||
|
||||
await DataWriter.insertCallLink(callLink);
|
||||
|
||||
await loadCallsHistory();
|
||||
});
|
||||
after(async () => {
|
||||
await DataWriter.removeAll();
|
||||
});
|
||||
|
||||
describe('Direct calls', () => {
|
||||
it('roundtrips with a missed call', async () => {
|
||||
const now = Date.now();
|
||||
const callId = '11111';
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
peerId: CONTACT_A,
|
||||
ringerId: CONTACT_A,
|
||||
mode: CallMode.Direct,
|
||||
type: CallType.Audio,
|
||||
status: DirectCallStatus.Missed,
|
||||
direction: CallDirection.Incoming,
|
||||
timestamp: now,
|
||||
};
|
||||
await DataWriter.saveCallHistory(callHistory);
|
||||
await loadCallsHistory();
|
||||
|
||||
const messageUnseen: MessageAttributesType = {
|
||||
id: generateGuid(),
|
||||
type: 'call-history',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
timestamp: now,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
conversationId: contactA.id,
|
||||
callId,
|
||||
};
|
||||
const messageSeen: MessageAttributesType = {
|
||||
id: generateGuid(),
|
||||
type: 'call-history',
|
||||
sent_at: now + 1,
|
||||
received_at: now + 1,
|
||||
timestamp: now + 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
conversationId: contactA.id,
|
||||
callId,
|
||||
};
|
||||
await symmetricRoundtripHarness([messageUnseen, messageSeen]);
|
||||
|
||||
const allCallHistory = await DataReader.getAllCallHistory();
|
||||
assert.strictEqual(allCallHistory.length, 1);
|
||||
|
||||
assert.deepEqual(callHistory, allCallHistory[0]);
|
||||
});
|
||||
});
|
||||
describe('Group calls', () => {
|
||||
it('roundtrips with a missed call', async () => {
|
||||
const now = Date.now();
|
||||
const callId = '22222';
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
peerId: GROUP_ID_STRING,
|
||||
ringerId: CONTACT_A,
|
||||
mode: CallMode.Group,
|
||||
type: CallType.Group,
|
||||
status: GroupCallStatus.Declined,
|
||||
direction: CallDirection.Incoming,
|
||||
timestamp: now,
|
||||
};
|
||||
await DataWriter.saveCallHistory(callHistory);
|
||||
await loadCallsHistory();
|
||||
|
||||
const messageUnseen: MessageAttributesType = {
|
||||
id: generateGuid(),
|
||||
type: 'call-history',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
timestamp: now,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
conversationId: groupA.id,
|
||||
callId,
|
||||
};
|
||||
const messageSeen: MessageAttributesType = {
|
||||
id: generateGuid(),
|
||||
type: 'call-history',
|
||||
sent_at: now + 1,
|
||||
received_at: now + 1,
|
||||
timestamp: now + 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
conversationId: groupA.id,
|
||||
callId,
|
||||
};
|
||||
await symmetricRoundtripHarness([messageUnseen, messageSeen]);
|
||||
|
||||
const allCallHistory = await DataReader.getAllCallHistory();
|
||||
assert.strictEqual(allCallHistory.length, 1);
|
||||
|
||||
assert.deepEqual(callHistory, allCallHistory[0]);
|
||||
});
|
||||
});
|
||||
describe('Call Links', () => {
|
||||
it('roundtrips with a link with admin details', async () => {
|
||||
const allCallLinksBefore = await DataReader.getAllCallLinks();
|
||||
assert.strictEqual(allCallLinksBefore.length, 1);
|
||||
|
||||
await symmetricRoundtripHarness([]);
|
||||
|
||||
const allCallLinks = await DataReader.getAllCallLinks();
|
||||
assert.strictEqual(allCallLinks.length, 1);
|
||||
|
||||
assert.deepEqual(callLink, allCallLinks[0]);
|
||||
});
|
||||
it('roundtrips with a link without admin details', async () => {
|
||||
await DataWriter._removeAllCallLinks();
|
||||
|
||||
const rootKey = CallLinkRootKey.generate();
|
||||
const callLinkNoAdmin = {
|
||||
rootKey: rootKey.toString(),
|
||||
roomId: getRoomIdFromRootKey(rootKey),
|
||||
adminKey: null,
|
||||
name: "Let's Talk Rocks #2",
|
||||
restrictions: CallLinkRestrictions.AdminApproval,
|
||||
revoked: false,
|
||||
expiration: null,
|
||||
};
|
||||
await DataWriter.insertCallLink(callLinkNoAdmin);
|
||||
|
||||
const allCallLinksBefore = await DataReader.getAllCallLinks();
|
||||
assert.strictEqual(allCallLinksBefore.length, 1);
|
||||
|
||||
await symmetricRoundtripHarness([]);
|
||||
|
||||
const allCallLinks = await DataReader.getAllCallLinks();
|
||||
assert.strictEqual(allCallLinks.length, 1);
|
||||
|
||||
assert.deepEqual(callLinkNoAdmin, allCallLinks[0]);
|
||||
});
|
||||
});
|
||||
describe('Adhoc calls', () => {
|
||||
it('roundtrips with a joined call', async () => {
|
||||
const now = Date.now();
|
||||
const callId = '333333';
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
peerId: callLink.roomId,
|
||||
ringerId: null,
|
||||
mode: CallMode.Adhoc,
|
||||
type: CallType.Adhoc,
|
||||
status: AdhocCallStatus.Generic,
|
||||
direction: CallDirection.Unknown,
|
||||
timestamp: now,
|
||||
};
|
||||
await DataWriter.saveCallHistory(callHistory);
|
||||
await loadCallsHistory();
|
||||
|
||||
await symmetricRoundtripHarness([]);
|
||||
|
||||
const allCallHistory = await DataReader.getAllCallHistory();
|
||||
assert.strictEqual(allCallHistory.length, 1);
|
||||
|
||||
assert.deepEqual(callHistory, allCallHistory[0]);
|
||||
});
|
||||
|
||||
it('does not roundtrip call with missing call link', async () => {
|
||||
const now = Date.now();
|
||||
const callId = '44444';
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId,
|
||||
peerId: 'nonexistent',
|
||||
ringerId: null,
|
||||
mode: CallMode.Adhoc,
|
||||
type: CallType.Adhoc,
|
||||
status: AdhocCallStatus.Generic,
|
||||
direction: CallDirection.Unknown,
|
||||
timestamp: now,
|
||||
};
|
||||
await DataWriter.saveCallHistory(callHistory);
|
||||
await loadCallsHistory();
|
||||
|
||||
await symmetricRoundtripHarness([]);
|
||||
|
||||
const allCallHistory = await DataReader.getAllCallHistory();
|
||||
assert.strictEqual(allCallHistory.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -199,9 +199,7 @@ export async function asymmetricRoundtripHarness(
|
|||
}
|
||||
|
||||
async function clearData() {
|
||||
await DataWriter._removeAllMessages();
|
||||
await DataWriter._removeAllConversations();
|
||||
await DataWriter.removeAllItems();
|
||||
await DataWriter.removeAll();
|
||||
window.storage.reset();
|
||||
window.ConversationController.reset();
|
||||
|
||||
|
|
|
@ -87,6 +87,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: OUR_ACI,
|
||||
},
|
||||
]);
|
||||
|
@ -101,6 +103,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
},
|
||||
]);
|
||||
|
@ -116,6 +120,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
},
|
||||
]);
|
||||
|
@ -132,6 +138,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
},
|
||||
]);
|
||||
|
@ -148,6 +156,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
},
|
||||
]);
|
||||
|
@ -162,6 +172,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
},
|
||||
]);
|
||||
|
@ -176,6 +188,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
},
|
||||
]);
|
||||
|
@ -190,6 +204,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
},
|
||||
]);
|
||||
|
@ -399,6 +415,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
sourceServiceId: CONTACT_A,
|
||||
sourceDevice: 1,
|
||||
|
@ -419,6 +437,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
changedId: contactA.id,
|
||||
sourceServiceId: CONTACT_A,
|
||||
profileChange: {
|
||||
|
@ -439,6 +459,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
titleTransition: {
|
||||
renderInfo: {
|
||||
|
@ -460,6 +482,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
conversationMerge: {
|
||||
renderInfo: {
|
||||
type: 'private',
|
||||
|
@ -479,6 +503,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
phoneNumberDiscovery: {
|
||||
e164: '+12125551234',
|
||||
|
@ -499,8 +525,8 @@ describe('backup/non-bubble messages', () => {
|
|||
sourceDevice: 1,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
readStatus: ReadStatus.Unread,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
unidentifiedDeliveryReceived: true,
|
||||
supportedVersionAtReceive: 5,
|
||||
requiredProtocolVersion: 6,
|
||||
|
@ -537,6 +563,8 @@ describe('backup/non-bubble messages', () => {
|
|||
},
|
||||
received_at: 1,
|
||||
sent_at: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sourceServiceId: CONTACT_A,
|
||||
timestamp: 1,
|
||||
},
|
||||
|
@ -553,6 +581,8 @@ describe('backup/non-bubble messages', () => {
|
|||
received_at: 1,
|
||||
sourceServiceId: CONTACT_A,
|
||||
sourceDevice: 1,
|
||||
readStatus: ReadStatus.Read,
|
||||
seenStatus: SeenStatus.Seen,
|
||||
sent_at: 1,
|
||||
timestamp: 1,
|
||||
messageRequestResponseEvent: MessageRequestResponseEvent.SPAM,
|
||||
|
|
|
@ -70,6 +70,9 @@ describe('sql/getCallHistoryGroups', () => {
|
|||
beforeEach(async () => {
|
||||
await removeAll();
|
||||
});
|
||||
after(async () => {
|
||||
await removeAll();
|
||||
});
|
||||
|
||||
it('should merge related items in order', async () => {
|
||||
const now = Date.now();
|
||||
|
|
|
@ -10,7 +10,7 @@ import { SignalService as Proto } from '../protobuf';
|
|||
import * as Bytes from '../Bytes';
|
||||
import { UUID_BYTE_SIZE } from './Crypto';
|
||||
|
||||
// These are strings (1) for the database (2) for Storybook.
|
||||
// These are strings (1) for the backup (2) for Storybook.
|
||||
export enum CallMode {
|
||||
Direct = 'Direct',
|
||||
Group = 'Group',
|
||||
|
@ -22,11 +22,15 @@ export enum CallType {
|
|||
Video = 'Video',
|
||||
Group = 'Group',
|
||||
Adhoc = 'Adhoc',
|
||||
// Only used for backup roundtripping
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export enum CallDirection {
|
||||
Incoming = 'Incoming',
|
||||
Outgoing = 'Outgoing',
|
||||
// Only used for backup roundtripping
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export enum CallLogEvent {
|
||||
|
@ -59,6 +63,8 @@ export enum CallStatusValue {
|
|||
Pending = 'Pending',
|
||||
Accepted = 'Accepted',
|
||||
Missed = 'Missed',
|
||||
// TODO: DESKTOP-3483 - not generated locally
|
||||
MissedNotificationProfile = 'MissedNotificationProfile',
|
||||
Declined = 'Declined',
|
||||
Deleted = 'Deleted',
|
||||
GenericGroupCall = 'GenericGroupCall',
|
||||
|
@ -67,14 +73,20 @@ export enum CallStatusValue {
|
|||
Ringing = 'Ringing',
|
||||
Joined = 'Joined',
|
||||
JoinedAdhoc = 'JoinedAdhoc',
|
||||
// Only used for backup roundtripping
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export enum DirectCallStatus {
|
||||
Pending = CallStatusValue.Pending,
|
||||
Accepted = CallStatusValue.Accepted,
|
||||
Missed = CallStatusValue.Missed,
|
||||
// TODO: DESKTOP-3483 - not generated locally
|
||||
MissedNotificationProfile = CallStatusValue.MissedNotificationProfile,
|
||||
Declined = CallStatusValue.Declined,
|
||||
Deleted = CallStatusValue.Deleted,
|
||||
// Only used for backup roundtripping
|
||||
Unknown = CallStatusValue.Unknown,
|
||||
}
|
||||
|
||||
export enum GroupCallStatus {
|
||||
|
@ -84,6 +96,8 @@ export enum GroupCallStatus {
|
|||
Joined = CallStatusValue.Joined,
|
||||
Accepted = CallStatusValue.Accepted,
|
||||
Missed = CallStatusValue.Missed,
|
||||
// TODO: DESKTOP-3483 - not generated locally
|
||||
MissedNotificationProfile = CallStatusValue.MissedNotificationProfile,
|
||||
Declined = CallStatusValue.Declined,
|
||||
Deleted = CallStatusValue.Deleted,
|
||||
}
|
||||
|
@ -93,6 +107,8 @@ export enum AdhocCallStatus {
|
|||
Pending = CallStatusValue.Pending,
|
||||
Joined = CallStatusValue.JoinedAdhoc,
|
||||
Deleted = CallStatusValue.Deleted,
|
||||
// Only used for backup roundtripping
|
||||
Unknown = CallStatusValue.Unknown,
|
||||
}
|
||||
|
||||
export type CallStatus = DirectCallStatus | GroupCallStatus | AdhocCallStatus;
|
||||
|
|
|
@ -284,6 +284,7 @@ export function getCallLogEventForProto(
|
|||
const directionToProto = {
|
||||
[CallDirection.Incoming]: Proto.SyncMessage.CallEvent.Direction.INCOMING,
|
||||
[CallDirection.Outgoing]: Proto.SyncMessage.CallEvent.Direction.OUTGOING,
|
||||
[CallDirection.Unknown]: Proto.SyncMessage.CallEvent.Direction.UNKNOWN,
|
||||
};
|
||||
|
||||
const typeToProto = {
|
||||
|
@ -291,6 +292,7 @@ const typeToProto = {
|
|||
[CallType.Video]: Proto.SyncMessage.CallEvent.Type.VIDEO_CALL,
|
||||
[CallType.Group]: Proto.SyncMessage.CallEvent.Type.GROUP_CALL,
|
||||
[CallType.Adhoc]: Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL,
|
||||
[CallType.Unknown]: Proto.SyncMessage.CallEvent.Type.UNKNOWN,
|
||||
};
|
||||
|
||||
const statusToProto: Record<
|
||||
|
@ -301,6 +303,7 @@ const statusToProto: Record<
|
|||
[CallStatusValue.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED,
|
||||
[CallStatusValue.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE,
|
||||
[CallStatusValue.Missed]: null,
|
||||
[CallStatusValue.MissedNotificationProfile]: null,
|
||||
[CallStatusValue.Pending]: null,
|
||||
[CallStatusValue.GenericGroupCall]: null,
|
||||
[CallStatusValue.GenericAdhocCall]:
|
||||
|
@ -309,6 +312,7 @@ const statusToProto: Record<
|
|||
[CallStatusValue.Ringing]: null,
|
||||
[CallStatusValue.Joined]: null,
|
||||
[CallStatusValue.JoinedAdhoc]: Proto.SyncMessage.CallEvent.Event.ACCEPTED,
|
||||
[CallStatusValue.Unknown]: Proto.SyncMessage.CallEvent.Event.UNKNOWN,
|
||||
};
|
||||
|
||||
function shouldSyncStatus(callStatus: CallStatus) {
|
||||
|
@ -681,12 +685,16 @@ function transitionTimestamp(
|
|||
// We don't care about holding onto timestamps that were from these states
|
||||
if (
|
||||
callHistory.status === DirectCallStatus.Pending ||
|
||||
callHistory.status === DirectCallStatus.Unknown ||
|
||||
callHistory.status === GroupCallStatus.GenericGroupCall ||
|
||||
callHistory.status === GroupCallStatus.OutgoingRing ||
|
||||
callHistory.status === GroupCallStatus.Ringing ||
|
||||
callHistory.status === DirectCallStatus.Missed ||
|
||||
callHistory.status === DirectCallStatus.MissedNotificationProfile ||
|
||||
callHistory.status === GroupCallStatus.Missed ||
|
||||
callHistory.status === AdhocCallStatus.Pending
|
||||
callHistory.status === GroupCallStatus.MissedNotificationProfile ||
|
||||
callHistory.status === AdhocCallStatus.Pending ||
|
||||
callHistory.status === AdhocCallStatus.Unknown
|
||||
) {
|
||||
return latestTimestamp;
|
||||
}
|
||||
|
@ -801,6 +809,7 @@ function transitionGroupCallStatus(
|
|||
}
|
||||
case GroupCallStatus.Ringing:
|
||||
case GroupCallStatus.Missed:
|
||||
case GroupCallStatus.MissedNotificationProfile:
|
||||
case GroupCallStatus.Declined: {
|
||||
return GroupCallStatus.Accepted;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,10 @@ export function getDirectCallNotificationText(
|
|||
callStatus: DirectCallStatus,
|
||||
i18n: LocalizerType
|
||||
): string {
|
||||
if (callStatus === DirectCallStatus.Pending) {
|
||||
if (
|
||||
callStatus === DirectCallStatus.Pending ||
|
||||
callStatus === DirectCallStatus.Unknown
|
||||
) {
|
||||
if (callDirection === CallDirection.Incoming) {
|
||||
return callType === CallType.Video
|
||||
? i18n('icu:incomingVideoCall')
|
||||
|
@ -68,7 +71,10 @@ export function getDirectCallNotificationText(
|
|||
: i18n('icu:missedOrDeclinedOutgoingAudioCall');
|
||||
}
|
||||
|
||||
if (callStatus === DirectCallStatus.Missed) {
|
||||
if (
|
||||
callStatus === DirectCallStatus.Missed ||
|
||||
callStatus === DirectCallStatus.MissedNotificationProfile
|
||||
) {
|
||||
if (callDirection === CallDirection.Incoming) {
|
||||
return callType === CallType.Video
|
||||
? i18n('icu:missedIncomingVideoCall')
|
||||
|
@ -219,5 +225,8 @@ export function getCallingIcon(
|
|||
if (callType === CallType.Group || callType === CallType.Adhoc) {
|
||||
return 'video';
|
||||
}
|
||||
if (callType === CallType.Unknown) {
|
||||
return 'video';
|
||||
}
|
||||
throw missingCaseError(callType);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue