Backups: Handle groupV2 notifications
This commit is contained in:
parent
4c4ab306eb
commit
5df8924197
27 changed files with 4563 additions and 301 deletions
|
@ -4159,15 +4159,15 @@
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-add--many--other": {
|
"icu:GroupV2--pending-add--many--other": {
|
||||||
"messageformat": "{memberName} invited {count, plural, one {#} other {#}} people to the group.",
|
"messageformat": "{memberName} invited {count, plural, one {# person} other {# people}} to the group.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-add--many--you": {
|
"icu:GroupV2--pending-add--many--you": {
|
||||||
"messageformat": "You invited {count, number} people to the group.",
|
"messageformat": "You invited {count, plural, one {# person} other {# people}} to the group.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-add--many--unknown": {
|
"icu:GroupV2--pending-add--many--unknown": {
|
||||||
"messageformat": "{count, plural, one {#} other {#}} people were invited to the group.",
|
"messageformat": "{count, plural, one {# person was} other {# people were}} invited to the group.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--decline--other": {
|
"icu:GroupV2--pending-remove--decline--other": {
|
||||||
|
@ -4207,15 +4207,15 @@
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke--many--other": {
|
"icu:GroupV2--pending-remove--revoke--many--other": {
|
||||||
"messageformat": "{memberName} revoked invitations to the group for {count, number} people.",
|
"messageformat": "{memberName} revoked {count, plural, one {an invitation to the group for 1 person} other {invitations to the group for # people}}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke--many--you": {
|
"icu:GroupV2--pending-remove--revoke--many--you": {
|
||||||
"messageformat": "You revoked invitations to the group for {count, number} people.",
|
"messageformat": "You revoked {count, plural, one {an invitation to the group for 1 person} other {invitations to the group for # people}}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke--many--unknown": {
|
"icu:GroupV2--pending-remove--revoke--many--unknown": {
|
||||||
"messageformat": "An admin revoked invitations to the group for {count, number} people.",
|
"messageformat": "An admin revoked {count, plural, one {an invitation to the group for 1 person} other {invitations to the group for # people}}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke-invite-from--one--other": {
|
"icu:GroupV2--pending-remove--revoke-invite-from--one--other": {
|
||||||
|
@ -4243,27 +4243,27 @@
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke-invite-from--many--other": {
|
"icu:GroupV2--pending-remove--revoke-invite-from--many--other": {
|
||||||
"messageformat": "{adminName} revoked invitations to the group for {count, plural, one {# person} other {# people}} invited by {memberName}.",
|
"messageformat": "{adminName} revoked {count, plural, one {an invitation to the group for 1 person} other {invitations to the group for # people}} invited by {memberName}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke-invite-from--many--you": {
|
"icu:GroupV2--pending-remove--revoke-invite-from--many--you": {
|
||||||
"messageformat": "You revoked invitations to the group for {count, number} people invited by {memberName}.",
|
"messageformat": "You revoked {count, plural, one {an invitation to the group for 1 person} other {invitations to the group for # people}} invited by {memberName}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke-invite-from--many--unknown": {
|
"icu:GroupV2--pending-remove--revoke-invite-from--many--unknown": {
|
||||||
"messageformat": "An admin revoked invitations to the group for {count, number} people invited by {memberName}.",
|
"messageformat": "An admin revoked {count, plural, one {an invitation to the group for 1 person} other {invitations to the group for # people}} invited by {memberName}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke-invite-from-you--many--other": {
|
"icu:GroupV2--pending-remove--revoke-invite-from-you--many--other": {
|
||||||
"messageformat": "{adminName} revoked the invitations to the group you sent to {count, number} people.",
|
"messageformat": "{adminName} revoked the {count, plural, one {invitation to the group you sent to 1 person} other {invitations to the group you sent to # people}}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke-invite-from-you--many--you": {
|
"icu:GroupV2--pending-remove--revoke-invite-from-you--many--you": {
|
||||||
"messageformat": "You rescinded your invitation to {count, number} people.",
|
"messageformat": "You rescinded your invitation to {count, plural, one {# person} other {# people}}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--pending-remove--revoke-invite-from-you--many--unknown": {
|
"icu:GroupV2--pending-remove--revoke-invite-from-you--many--unknown": {
|
||||||
"messageformat": "An admin revoked the invitations to the group you sent to {count, number} people.",
|
"messageformat": "An admin revoked the {count, plural, one {invitation to the group you sent to 1 person} other {invitations to the group you sent to # people}}.",
|
||||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||||
},
|
},
|
||||||
"icu:GroupV2--admin-approval-add-one--you": {
|
"icu:GroupV2--admin-approval-add-one--you": {
|
||||||
|
@ -4454,6 +4454,10 @@
|
||||||
"messageformat": "This member will need to accept an invite to join this group again, and will not receive group messages until they accept:",
|
"messageformat": "This member will need to accept an invite to join this group again, and will not receive group messages until they accept:",
|
||||||
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
||||||
},
|
},
|
||||||
|
"icu:GroupV1--Migration--info--invited--count": {
|
||||||
|
"messageformat": "{count, plural, one {# member} other {# members}} will need to accept an invite to join this group again, and will not receive group messages until they accept.",
|
||||||
|
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
||||||
|
},
|
||||||
"icu:GroupV1--Migration--info--removed--before--many": {
|
"icu:GroupV1--Migration--info--removed--before--many": {
|
||||||
"messageformat": "These members are not capable of joining New Groups, and will be removed from the group:",
|
"messageformat": "These members are not capable of joining New Groups, and will be removed from the group:",
|
||||||
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
||||||
|
@ -4462,6 +4466,10 @@
|
||||||
"messageformat": "This member is not capable of joining New Groups, and will be removed from the group:",
|
"messageformat": "This member is not capable of joining New Groups, and will be removed from the group:",
|
||||||
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
||||||
},
|
},
|
||||||
|
"icu:GroupV1--Migration--info--removed--before--count": {
|
||||||
|
"messageformat": "{count, plural, one {# member is} other {# members are}} not capable of joining New Groups, and will be removed from the group.",
|
||||||
|
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
||||||
|
},
|
||||||
"icu:GroupV1--Migration--info--removed--after--many": {
|
"icu:GroupV1--Migration--info--removed--after--many": {
|
||||||
"messageformat": "These members were not capable of joining New Groups, and were removed from the group:",
|
"messageformat": "These members were not capable of joining New Groups, and were removed from the group:",
|
||||||
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
||||||
|
@ -4470,6 +4478,10 @@
|
||||||
"messageformat": "This member was not capable of joining New Groups, and was removed from the group:",
|
"messageformat": "This member was not capable of joining New Groups, and was removed from the group:",
|
||||||
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
||||||
},
|
},
|
||||||
|
"icu:GroupV1--Migration--info--removed--after--count": {
|
||||||
|
"messageformat": "{count, plural, one {# member was not capable of joining New Groups, and was} other {# members were not capable of joining New Groups, and were}} removed from the group.",
|
||||||
|
"description": "Shown on Learn More popup after or Migration popup before GV1 migration"
|
||||||
|
},
|
||||||
"icu:GroupV1--Migration--invited--you": {
|
"icu:GroupV1--Migration--invited--you": {
|
||||||
"messageformat": "You couldn't be added to the New Group and have been invited to join.",
|
"messageformat": "You couldn't be added to the New Group and have been invited to join.",
|
||||||
"description": "Shown in timeline when a group is upgraded and you were invited instead of added"
|
"description": "Shown in timeline when a group is upgraded and you were invited instead of added"
|
||||||
|
@ -4479,7 +4491,7 @@
|
||||||
"description": "Shown in timeline when a group is upgraded and one person was invited, instead of added"
|
"description": "Shown in timeline when a group is upgraded and one person was invited, instead of added"
|
||||||
},
|
},
|
||||||
"icu:GroupV1--Migration--invited--many": {
|
"icu:GroupV1--Migration--invited--many": {
|
||||||
"messageformat": "{count, number} members couldn’t be added to the New Group and have been invited to join.",
|
"messageformat": "{count, plural, one {# member couldn’t be added to the New Group and has been invited to join} other {# members couldn’t be added to the New Group and have been invited to join}}.",
|
||||||
"description": "Shown in timeline when a group is upgraded and some people were invited, instead of added"
|
"description": "Shown in timeline when a group is upgraded and some people were invited, instead of added"
|
||||||
},
|
},
|
||||||
"icu:GroupV1--Migration--removed--one": {
|
"icu:GroupV1--Migration--removed--one": {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
max-height: 100%;
|
max-height: 80vh;
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -262,7 +262,7 @@ export class ConversationController {
|
||||||
getOrCreate(
|
getOrCreate(
|
||||||
identifier: string | null,
|
identifier: string | null,
|
||||||
type: ConversationAttributesTypeType,
|
type: ConversationAttributesTypeType,
|
||||||
additionalInitialProps = {}
|
additionalInitialProps: Partial<ConversationAttributesType> = {}
|
||||||
): ConversationModel {
|
): ConversationModel {
|
||||||
if (typeof identifier !== 'string') {
|
if (typeof identifier !== 'string') {
|
||||||
throw new TypeError("'id' must be a string");
|
throw new TypeError("'id' must be a string");
|
||||||
|
@ -358,7 +358,7 @@ export class ConversationController {
|
||||||
async getOrCreateAndWait(
|
async getOrCreateAndWait(
|
||||||
id: string | null,
|
id: string | null,
|
||||||
type: ConversationAttributesTypeType,
|
type: ConversationAttributesTypeType,
|
||||||
additionalInitialProps = {}
|
additionalInitialProps: Partial<ConversationAttributesType> = {}
|
||||||
): Promise<ConversationModel> {
|
): Promise<ConversationModel> {
|
||||||
await this.load();
|
await this.load();
|
||||||
const conversation = this.getOrCreate(id, type, additionalInitialProps);
|
const conversation = this.getOrCreate(id, type, additionalInitialProps);
|
||||||
|
|
|
@ -36,11 +36,13 @@ const contact3: ConversationType = getDefaultConversation({
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
areWeInvited: Boolean(overrideProps.areWeInvited),
|
areWeInvited: Boolean(overrideProps.areWeInvited),
|
||||||
droppedMembers: overrideProps.droppedMembers || [contact3, contact1],
|
droppedMembers: overrideProps.droppedMembers,
|
||||||
|
droppedMemberCount: overrideProps.droppedMemberCount || 0,
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
hasMigrated: Boolean(overrideProps.hasMigrated),
|
hasMigrated: Boolean(overrideProps.hasMigrated),
|
||||||
i18n,
|
i18n,
|
||||||
invitedMembers: overrideProps.invitedMembers || [contact2],
|
invitedMembers: overrideProps.invitedMembers,
|
||||||
|
invitedMemberCount: overrideProps.invitedMemberCount || 0,
|
||||||
onMigrate: action('onMigrate'),
|
onMigrate: action('onMigrate'),
|
||||||
onClose: action('onClose'),
|
onClose: action('onClose'),
|
||||||
theme: ThemeType.light,
|
theme: ThemeType.light,
|
||||||
|
@ -75,23 +77,41 @@ export function MigratedYouAreInvited(): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotYetMigratedMultipleDroppedAndInvitedMembers(): JSX.Element {
|
export function MigratedMultipleDroppedAndInvitedMember(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<GroupV1MigrationDialog
|
<GroupV1MigrationDialog
|
||||||
{...createProps({
|
{...createProps({
|
||||||
droppedMembers: [contact3, contact1, contact2],
|
hasMigrated: true,
|
||||||
invitedMembers: [contact2, contact3, contact1],
|
droppedMembers: [contact1],
|
||||||
|
droppedMemberCount: 1,
|
||||||
|
invitedMembers: [contact2],
|
||||||
|
invitedMemberCount: 1,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotYetMigratedNoMembers(): JSX.Element {
|
export function MigratedMultipleDroppedAndInvitedMembers(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<GroupV1MigrationDialog
|
<GroupV1MigrationDialog
|
||||||
{...createProps({
|
{...createProps({
|
||||||
droppedMembers: [],
|
hasMigrated: true,
|
||||||
invitedMembers: [],
|
droppedMembers: [contact3, contact1, contact2],
|
||||||
|
droppedMemberCount: 3,
|
||||||
|
invitedMembers: [contact2, contact3, contact1],
|
||||||
|
invitedMemberCount: 3,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MigratedNoMembers(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupV1MigrationDialog
|
||||||
|
{...createProps({
|
||||||
|
hasMigrated: true,
|
||||||
|
droppedMemberCount: 0,
|
||||||
|
invitedMemberCount: 0,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -101,7 +121,65 @@ export function NotYetMigratedJustDroppedMember(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<GroupV1MigrationDialog
|
<GroupV1MigrationDialog
|
||||||
{...createProps({
|
{...createProps({
|
||||||
invitedMembers: [],
|
droppedMembers: [contact1],
|
||||||
|
droppedMemberCount: 1,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotYetMigratedJustDroppedMembers(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupV1MigrationDialog
|
||||||
|
{...createProps({
|
||||||
|
droppedMembers: [contact1, contact2],
|
||||||
|
droppedMemberCount: 2,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotYetMigratedDropped1(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupV1MigrationDialog
|
||||||
|
{...createProps({
|
||||||
|
droppedMemberCount: 1,
|
||||||
|
invitedMemberCount: 0,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotYetMigratedDropped2(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupV1MigrationDialog
|
||||||
|
{...createProps({
|
||||||
|
droppedMemberCount: 2,
|
||||||
|
invitedMemberCount: 0,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MigratedJustCountIs1(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupV1MigrationDialog
|
||||||
|
{...createProps({
|
||||||
|
hasMigrated: true,
|
||||||
|
droppedMemberCount: 1,
|
||||||
|
invitedMemberCount: 1,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MigratedJustCountIs2(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupV1MigrationDialog
|
||||||
|
{...createProps({
|
||||||
|
hasMigrated: true,
|
||||||
|
droppedMemberCount: 2,
|
||||||
|
invitedMemberCount: 2,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,9 +11,11 @@ import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
|
||||||
export type DataPropsType = {
|
export type DataPropsType = {
|
||||||
readonly areWeInvited: boolean;
|
readonly areWeInvited: boolean;
|
||||||
readonly droppedMembers: Array<ConversationType>;
|
readonly droppedMembers?: Array<ConversationType>;
|
||||||
|
readonly droppedMemberCount: number;
|
||||||
readonly hasMigrated: boolean;
|
readonly hasMigrated: boolean;
|
||||||
readonly invitedMembers: Array<ConversationType>;
|
readonly invitedMembers?: Array<ConversationType>;
|
||||||
|
readonly invitedMemberCount: number;
|
||||||
readonly getPreferredBadge: PreferredBadgeSelectorType;
|
readonly getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
readonly theme: ThemeType;
|
readonly theme: ThemeType;
|
||||||
|
@ -30,10 +32,12 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
|
||||||
React.memo(function GroupV1MigrationDialogInner({
|
React.memo(function GroupV1MigrationDialogInner({
|
||||||
areWeInvited,
|
areWeInvited,
|
||||||
droppedMembers,
|
droppedMembers,
|
||||||
|
droppedMemberCount,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
hasMigrated,
|
hasMigrated,
|
||||||
i18n,
|
i18n,
|
||||||
invitedMembers,
|
invitedMembers,
|
||||||
|
invitedMemberCount,
|
||||||
theme,
|
theme,
|
||||||
onClose,
|
onClose,
|
||||||
onMigrate,
|
onMigrate,
|
||||||
|
@ -88,6 +92,7 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
members: invitedMembers,
|
members: invitedMembers,
|
||||||
|
count: invitedMemberCount,
|
||||||
hasMigrated,
|
hasMigrated,
|
||||||
kind: 'invited',
|
kind: 'invited',
|
||||||
theme,
|
theme,
|
||||||
|
@ -96,6 +101,7 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
members: droppedMembers,
|
members: droppedMembers,
|
||||||
|
count: droppedMemberCount,
|
||||||
hasMigrated,
|
hasMigrated,
|
||||||
kind: 'dropped',
|
kind: 'dropped',
|
||||||
theme,
|
theme,
|
||||||
|
@ -110,21 +116,50 @@ function renderMembers({
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
members,
|
members,
|
||||||
|
count,
|
||||||
hasMigrated,
|
hasMigrated,
|
||||||
kind,
|
kind,
|
||||||
theme,
|
theme,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
members: Array<ConversationType>;
|
members?: Array<ConversationType>;
|
||||||
|
count: number;
|
||||||
hasMigrated: boolean;
|
hasMigrated: boolean;
|
||||||
kind: 'invited' | 'dropped';
|
kind: 'invited' | 'dropped';
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
}>): React.ReactNode {
|
}>): React.ReactNode {
|
||||||
if (!members.length) {
|
if (count === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!members) {
|
||||||
|
if (kind === 'invited') {
|
||||||
|
return (
|
||||||
|
<GroupDialog.Paragraph>
|
||||||
|
{i18n('icu:GroupV1--Migration--info--invited--count', { count })}
|
||||||
|
</GroupDialog.Paragraph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (hasMigrated) {
|
||||||
|
return (
|
||||||
|
<GroupDialog.Paragraph>
|
||||||
|
{i18n('icu:GroupV1--Migration--info--removed--after--count', {
|
||||||
|
count,
|
||||||
|
})}
|
||||||
|
</GroupDialog.Paragraph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GroupDialog.Paragraph>
|
||||||
|
{i18n('icu:GroupV1--Migration--info--removed--before--count', {
|
||||||
|
count,
|
||||||
|
})}
|
||||||
|
</GroupDialog.Paragraph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let text: string;
|
let text: string;
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'invited':
|
case 'invited':
|
||||||
|
@ -137,13 +172,13 @@ function renderMembers({
|
||||||
if (hasMigrated) {
|
if (hasMigrated) {
|
||||||
text =
|
text =
|
||||||
members.length === 1
|
members.length === 1
|
||||||
? i18n('icu:GroupV1--Migration--info--removed--before--one')
|
? i18n('icu:GroupV1--Migration--info--removed--after--one')
|
||||||
: i18n('icu:GroupV1--Migration--info--removed--before--many');
|
: i18n('icu:GroupV1--Migration--info--removed--after--many');
|
||||||
} else {
|
} else {
|
||||||
text =
|
text =
|
||||||
members.length === 1
|
members.length === 1
|
||||||
? i18n('icu:GroupV1--Migration--info--removed--after--one')
|
? i18n('icu:GroupV1--Migration--info--removed--before--one')
|
||||||
: i18n('icu:GroupV1--Migration--info--removed--after--many');
|
: i18n('icu:GroupV1--Migration--info--removed--before--many');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -33,9 +33,11 @@ export default {
|
||||||
areWeInvited: false,
|
areWeInvited: false,
|
||||||
conversationId: '123',
|
conversationId: '123',
|
||||||
droppedMembers: [contact1],
|
droppedMembers: [contact1],
|
||||||
|
droppedMemberCount: 1,
|
||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
i18n,
|
i18n,
|
||||||
invitedMembers: [contact2],
|
invitedMembers: [contact2],
|
||||||
|
invitedMemberCount: 1,
|
||||||
theme: ThemeType.light,
|
theme: ThemeType.light,
|
||||||
},
|
},
|
||||||
} satisfies Meta<PropsType>;
|
} satisfies Meta<PropsType>;
|
||||||
|
@ -55,7 +57,9 @@ export function MultipleDroppedAndInvitedMembers(args: PropsType): JSX.Element {
|
||||||
<GroupV1Migration
|
<GroupV1Migration
|
||||||
{...args}
|
{...args}
|
||||||
invitedMembers={[contact1, contact2]}
|
invitedMembers={[contact1, contact2]}
|
||||||
|
invitedMemberCount={3}
|
||||||
droppedMembers={[contact1, contact2]}
|
droppedMembers={[contact1, contact2]}
|
||||||
|
droppedMemberCount={3}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -65,7 +69,9 @@ export function JustInvitedMembers(args: PropsType): JSX.Element {
|
||||||
<GroupV1Migration
|
<GroupV1Migration
|
||||||
{...args}
|
{...args}
|
||||||
invitedMembers={[contact1, contact1, contact2, contact2]}
|
invitedMembers={[contact1, contact1, contact2, contact2]}
|
||||||
|
invitedMemberCount={4}
|
||||||
droppedMembers={[]}
|
droppedMembers={[]}
|
||||||
|
droppedMemberCount={0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -75,11 +81,45 @@ export function JustDroppedMembers(args: PropsType): JSX.Element {
|
||||||
<GroupV1Migration
|
<GroupV1Migration
|
||||||
{...args}
|
{...args}
|
||||||
invitedMembers={[]}
|
invitedMembers={[]}
|
||||||
|
invitedMemberCount={0}
|
||||||
droppedMembers={[contact1, contact1, contact2, contact2]}
|
droppedMembers={[contact1, contact1, contact2, contact2]}
|
||||||
|
droppedMemberCount={4}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoDroppedOrInvitedMembers(args: PropsType): JSX.Element {
|
export function NoDroppedOrInvitedMembers(args: PropsType): JSX.Element {
|
||||||
return <GroupV1Migration {...args} invitedMembers={[]} droppedMembers={[]} />;
|
return (
|
||||||
|
<GroupV1Migration
|
||||||
|
{...args}
|
||||||
|
invitedMembers={[]}
|
||||||
|
invitedMemberCount={0}
|
||||||
|
droppedMembers={[]}
|
||||||
|
droppedMemberCount={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NoArraysCountIsZero(args: PropsType): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupV1Migration
|
||||||
|
{...args}
|
||||||
|
invitedMembers={undefined}
|
||||||
|
invitedMemberCount={0}
|
||||||
|
droppedMembers={undefined}
|
||||||
|
droppedMemberCount={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NoArraysWithCount(args: PropsType): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupV1Migration
|
||||||
|
{...args}
|
||||||
|
invitedMembers={undefined}
|
||||||
|
invitedMemberCount={4}
|
||||||
|
droppedMembers={undefined}
|
||||||
|
droppedMemberCount={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,10 @@ import * as log from '../../logging/log';
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
areWeInvited: boolean;
|
areWeInvited: boolean;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
droppedMembers: Array<ConversationType>;
|
droppedMembers?: Array<ConversationType>;
|
||||||
invitedMembers: Array<ConversationType>;
|
invitedMembers?: Array<ConversationType>;
|
||||||
|
droppedMemberCount: number;
|
||||||
|
invitedMemberCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsHousekeepingType = {
|
export type PropsHousekeepingType = {
|
||||||
|
@ -32,9 +34,11 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
|
||||||
const {
|
const {
|
||||||
areWeInvited,
|
areWeInvited,
|
||||||
droppedMembers,
|
droppedMembers,
|
||||||
|
droppedMemberCount,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
invitedMembers,
|
invitedMembers,
|
||||||
|
invitedMemberCount,
|
||||||
theme,
|
theme,
|
||||||
} = props;
|
} = props;
|
||||||
const [showingDialog, setShowingDialog] = React.useState(false);
|
const [showingDialog, setShowingDialog] = React.useState(false);
|
||||||
|
@ -55,12 +59,23 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
|
||||||
<>
|
<>
|
||||||
<p>{i18n('icu:GroupV1--Migration--was-upgraded')}</p>
|
<p>{i18n('icu:GroupV1--Migration--was-upgraded')}</p>
|
||||||
<p>
|
<p>
|
||||||
|
{' '}
|
||||||
{areWeInvited ? (
|
{areWeInvited ? (
|
||||||
i18n('icu:GroupV1--Migration--invited--you')
|
i18n('icu:GroupV1--Migration--invited--you')
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{renderUsers(invitedMembers, i18n, 'invited')}
|
{renderUsers({
|
||||||
{renderUsers(droppedMembers, i18n, 'removed')}
|
members: invitedMembers,
|
||||||
|
count: invitedMemberCount,
|
||||||
|
i18n,
|
||||||
|
kind: 'invited',
|
||||||
|
})}
|
||||||
|
{renderUsers({
|
||||||
|
members: droppedMembers,
|
||||||
|
count: droppedMemberCount,
|
||||||
|
i18n,
|
||||||
|
kind: 'removed',
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
@ -80,10 +95,12 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
|
||||||
<GroupV1MigrationDialog
|
<GroupV1MigrationDialog
|
||||||
areWeInvited={areWeInvited}
|
areWeInvited={areWeInvited}
|
||||||
droppedMembers={droppedMembers}
|
droppedMembers={droppedMembers}
|
||||||
|
droppedMemberCount={droppedMemberCount}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
hasMigrated
|
hasMigrated
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
invitedMembers={invitedMembers}
|
invitedMembers={invitedMembers}
|
||||||
|
invitedMemberCount={invitedMemberCount}
|
||||||
onMigrate={() => log.warn('GroupV1Migration: Modal called migrate()')}
|
onMigrate={() => log.warn('GroupV1Migration: Modal called migrate()')}
|
||||||
onClose={dismissDialog}
|
onClose={dismissDialog}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
@ -93,16 +110,22 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUsers(
|
function renderUsers({
|
||||||
members: Array<ConversationType>,
|
members,
|
||||||
i18n: LocalizerType,
|
count,
|
||||||
kind: 'invited' | 'removed'
|
i18n,
|
||||||
): React.ReactElement | null {
|
kind,
|
||||||
if (!members || members.length === 0) {
|
}: {
|
||||||
|
members?: Array<ConversationType>;
|
||||||
|
count: number;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
kind: 'invited' | 'removed';
|
||||||
|
}): React.ReactElement | null {
|
||||||
|
if (count === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (members.length === 1) {
|
if (members && count === 1) {
|
||||||
const contact = <ContactName title={members[0].title} />;
|
const contact = <ContactName title={members[0].title} />;
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
|
@ -124,18 +147,16 @@ function renderUsers(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = members.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
{kind === 'invited' && members.length > 1 && (
|
{kind === 'invited' && (
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:GroupV1--Migration--invited--many"
|
id="icu:GroupV1--Migration--invited--many"
|
||||||
components={{ count }}
|
components={{ count }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{kind === 'removed' && members.length > 1 && (
|
{kind === 'removed' && (
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:GroupV1--Migration--removed--many"
|
id="icu:GroupV1--Migration--removed--many"
|
||||||
|
|
|
@ -14,16 +14,31 @@ import type { SmartContactRendererType } from '../../groupChange';
|
||||||
import type { PropsType } from './GroupV2Change';
|
import type { PropsType } from './GroupV2Change';
|
||||||
import { GroupV2Change } from './GroupV2Change';
|
import { GroupV2Change } from './GroupV2Change';
|
||||||
|
|
||||||
|
// Note: this should be kept up to date with backup_groupv2_notifications_test.ts, to
|
||||||
|
// maintain the comprehensive set of GroupV2 notifications we need to handle
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const OUR_ACI = generateAci();
|
const OUR_ACI = generateAci();
|
||||||
const OUR_PNI = generatePni();
|
const OUR_PNI = generatePni();
|
||||||
const CONTACT_A = generateAci();
|
const CONTACT_A = generateAci();
|
||||||
|
const CONTACT_A_PNI = generatePni();
|
||||||
const CONTACT_B = generateAci();
|
const CONTACT_B = generateAci();
|
||||||
const CONTACT_C = generateAci();
|
const CONTACT_C = generateAci();
|
||||||
const ADMIN_A = generateAci();
|
const ADMIN_A = generateAci();
|
||||||
const INVITEE_A = generateAci();
|
const INVITEE_A = generateAci();
|
||||||
|
|
||||||
|
const contactMap = {
|
||||||
|
[OUR_ACI]: 'YOU',
|
||||||
|
[OUR_PNI]: 'YOU',
|
||||||
|
[CONTACT_A]: 'CONTACT_A',
|
||||||
|
[CONTACT_A_PNI]: 'CONTACT_A',
|
||||||
|
[CONTACT_B]: 'CONTACT_B',
|
||||||
|
[CONTACT_C]: 'CONTACT_C',
|
||||||
|
[ADMIN_A]: 'ADMIN_A',
|
||||||
|
[INVITEE_A]: 'INVITEE_A',
|
||||||
|
};
|
||||||
|
|
||||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||||
const RoleEnum = Proto.Member.Role;
|
const RoleEnum = Proto.Member.Role;
|
||||||
|
|
||||||
|
@ -31,10 +46,17 @@ const renderContact: SmartContactRendererType<JSX.Element> = (
|
||||||
conversationId: string
|
conversationId: string
|
||||||
) => (
|
) => (
|
||||||
<React.Fragment key={conversationId}>
|
<React.Fragment key={conversationId}>
|
||||||
{`Conversation(${conversationId})`}
|
{contactMap[conversationId] || 'UNKNOWN'}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function checkServiceIdEquivalence(
|
||||||
|
left: ServiceIdString | undefined,
|
||||||
|
right: ServiceIdString | undefined
|
||||||
|
): boolean {
|
||||||
|
return Boolean(left && right && contactMap[left] === contactMap[right]);
|
||||||
|
}
|
||||||
|
|
||||||
const renderChange = (
|
const renderChange = (
|
||||||
change: GroupV2ChangeType,
|
change: GroupV2ChangeType,
|
||||||
{
|
{
|
||||||
|
@ -57,6 +79,7 @@ const renderChange = (
|
||||||
blockGroupLinkRequests={action('blockGroupLinkRequests')}
|
blockGroupLinkRequests={action('blockGroupLinkRequests')}
|
||||||
conversationId="some-conversation-id"
|
conversationId="some-conversation-id"
|
||||||
change={change}
|
change={change}
|
||||||
|
checkServiceIdEquivalence={checkServiceIdEquivalence}
|
||||||
groupBannedMemberships={groupBannedMemberships}
|
groupBannedMemberships={groupBannedMemberships}
|
||||||
groupMemberships={groupMemberships}
|
groupMemberships={groupMemberships}
|
||||||
groupName={groupName}
|
groupName={groupName}
|
||||||
|
@ -603,6 +626,24 @@ export function MemberAddFromInvited(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_PNI,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add-from-invite',
|
||||||
|
aci: OUR_ACI,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A_PNI,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'member-add-from-invite',
|
||||||
|
aci: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -923,6 +964,15 @@ export function PendingAddMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ACI,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-many',
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
from: CONTACT_A,
|
from: CONTACT_A,
|
||||||
details: [
|
details: [
|
||||||
|
@ -932,11 +982,28 @@ export function PendingAddMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-many',
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-add-many',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
type: 'pending-add-many',
|
type: 'pending-add-many',
|
||||||
count: 5,
|
count: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
@ -1100,6 +1167,16 @@ export function PendingRemoveMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ACI,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
inviter: OUR_ACI,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
from: ADMIN_A,
|
from: ADMIN_A,
|
||||||
details: [
|
details: [
|
||||||
|
@ -1110,11 +1187,30 @@ export function PendingRemoveMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
inviter: OUR_ACI,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 5,
|
||||||
|
inviter: OUR_ACI,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
type: 'pending-remove-many',
|
type: 'pending-remove-many',
|
||||||
count: 5,
|
count: 1,
|
||||||
inviter: OUR_ACI,
|
inviter: OUR_ACI,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1129,6 +1225,16 @@ export function PendingRemoveMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ACI,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
inviter: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
from: ADMIN_A,
|
from: ADMIN_A,
|
||||||
details: [
|
details: [
|
||||||
|
@ -1139,6 +1245,16 @@ export function PendingRemoveMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: ADMIN_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
inviter: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
|
@ -1148,6 +1264,15 @@ export function PendingRemoveMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
inviter: CONTACT_A,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
from: OUR_ACI,
|
from: OUR_ACI,
|
||||||
details: [
|
details: [
|
||||||
|
@ -1157,6 +1282,15 @@ export function PendingRemoveMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: OUR_ACI,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
|
||||||
{renderChange({
|
{renderChange({
|
||||||
from: CONTACT_A,
|
from: CONTACT_A,
|
||||||
|
@ -1167,6 +1301,15 @@ export function PendingRemoveMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
|
@ -1175,6 +1318,14 @@ export function PendingRemoveMany(): JSX.Element {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
|
{renderChange({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1183,6 +1334,7 @@ export function AdminApprovalAdd(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderChange({
|
{renderChange({
|
||||||
|
from: OUR_ACI,
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
type: 'admin-approval-add-one',
|
type: 'admin-approval-add-one',
|
||||||
|
@ -1191,6 +1343,7 @@ export function AdminApprovalAdd(): JSX.Element {
|
||||||
],
|
],
|
||||||
})}
|
})}
|
||||||
{renderChange({
|
{renderChange({
|
||||||
|
from: CONTACT_A,
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
type: 'admin-approval-add-one',
|
type: 'admin-approval-add-one',
|
||||||
|
@ -1332,6 +1485,21 @@ export function AdminApprovalBounce(): JSX.Element {
|
||||||
|
|
||||||
{ groupBannedMemberships: [CONTACT_A] }
|
{ groupBannedMemberships: [CONTACT_A] }
|
||||||
)}
|
)}
|
||||||
|
Open request
|
||||||
|
{renderChange(
|
||||||
|
{
|
||||||
|
from: CONTACT_A,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
type: 'admin-approval-bounce',
|
||||||
|
aci: CONTACT_A,
|
||||||
|
times: 4,
|
||||||
|
isApprovalPending: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ groupBannedMemberships: [] }
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,16 +29,16 @@ import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
areWeAdmin: boolean;
|
areWeAdmin: boolean;
|
||||||
|
change: GroupV2ChangeType;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
groupBannedMemberships?: ReadonlyArray<ServiceIdString>;
|
||||||
groupMemberships?: ReadonlyArray<{
|
groupMemberships?: ReadonlyArray<{
|
||||||
aci: AciString;
|
aci: AciString;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
}>;
|
}>;
|
||||||
groupBannedMemberships?: ReadonlyArray<ServiceIdString>;
|
|
||||||
groupName?: string;
|
groupName?: string;
|
||||||
ourAci: AciString | undefined;
|
ourAci: AciString | undefined;
|
||||||
ourPni: PniString | undefined;
|
ourPni: PniString | undefined;
|
||||||
change: GroupV2ChangeType;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsActionsType = {
|
export type PropsActionsType = {
|
||||||
|
@ -49,6 +49,10 @@ export type PropsActionsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsHousekeepingType = {
|
export type PropsHousekeepingType = {
|
||||||
|
checkServiceIdEquivalence(
|
||||||
|
left: ServiceIdString | undefined,
|
||||||
|
right: ServiceIdString | undefined
|
||||||
|
): boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
renderContact: SmartContactRendererType<JSX.Element>;
|
renderContact: SmartContactRendererType<JSX.Element>;
|
||||||
};
|
};
|
||||||
|
@ -293,6 +297,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
|
||||||
areWeAdmin,
|
areWeAdmin,
|
||||||
blockGroupLinkRequests,
|
blockGroupLinkRequests,
|
||||||
change,
|
change,
|
||||||
|
checkServiceIdEquivalence,
|
||||||
conversationId,
|
conversationId,
|
||||||
groupBannedMemberships,
|
groupBannedMemberships,
|
||||||
groupMemberships,
|
groupMemberships,
|
||||||
|
@ -306,6 +311,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderChange<JSX.Element>(change, {
|
{renderChange<JSX.Element>(change, {
|
||||||
|
checkServiceIdEquivalence,
|
||||||
i18n,
|
i18n,
|
||||||
ourAci,
|
ourAci,
|
||||||
ourPni,
|
ourPni,
|
||||||
|
|
|
@ -361,6 +361,7 @@ const renderItem = ({
|
||||||
isNextItemCallingNotification={false}
|
isNextItemCallingNotification={false}
|
||||||
theme={ThemeType.light}
|
theme={ThemeType.light}
|
||||||
platform="darwin"
|
platform="darwin"
|
||||||
|
checkServiceIdEquivalence={() => false}
|
||||||
containerElementRef={containerElementRef}
|
containerElementRef={containerElementRef}
|
||||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||||
conversationId=""
|
conversationId=""
|
||||||
|
|
|
@ -69,6 +69,7 @@ const getDefaultProps = () => ({
|
||||||
toggleSelectMessage: action('toggleSelectMessage'),
|
toggleSelectMessage: action('toggleSelectMessage'),
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
checkForAccount: action('checkForAccount'),
|
checkForAccount: action('checkForAccount'),
|
||||||
|
checkServiceIdEquivalence: () => false,
|
||||||
clearTargetedMessage: action('clearTargetedMessage'),
|
clearTargetedMessage: action('clearTargetedMessage'),
|
||||||
setMessageToEdit: action('setMessageToEdit'),
|
setMessageToEdit: action('setMessageToEdit'),
|
||||||
setQuoteByMessageId: action('setQuoteByMessageId'),
|
setQuoteByMessageId: action('setQuoteByMessageId'),
|
||||||
|
|
|
@ -61,6 +61,7 @@ import {
|
||||||
type MessageRequestResponseNotificationData,
|
type MessageRequestResponseNotificationData,
|
||||||
} from './MessageRequestResponseNotification';
|
} from './MessageRequestResponseNotification';
|
||||||
import type { MessageRequestState } from './MessageRequestActionsConfirmation';
|
import type { MessageRequestState } from './MessageRequestActionsConfirmation';
|
||||||
|
import type { ServiceIdString } from '../../types/ServiceId';
|
||||||
|
|
||||||
type CallHistoryType = {
|
type CallHistoryType = {
|
||||||
type: 'callHistory';
|
type: 'callHistory';
|
||||||
|
@ -172,6 +173,10 @@ export type TimelineItemType = (
|
||||||
) & { timestamp: number };
|
) & { timestamp: number };
|
||||||
|
|
||||||
type PropsLocalType = {
|
type PropsLocalType = {
|
||||||
|
checkServiceIdEquivalence(
|
||||||
|
left: ServiceIdString | undefined,
|
||||||
|
right: ServiceIdString | undefined
|
||||||
|
): boolean;
|
||||||
containerElementRef: RefObject<HTMLElement>;
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
item?: TimelineItemType;
|
item?: TimelineItemType;
|
||||||
|
|
|
@ -37,6 +37,10 @@ export type RenderOptionsType<T extends string | JSX.Element> = {
|
||||||
ourAci: AciString | undefined;
|
ourAci: AciString | undefined;
|
||||||
ourPni: PniString | undefined;
|
ourPni: PniString | undefined;
|
||||||
renderContact: SmartContactRendererType<T>;
|
renderContact: SmartContactRendererType<T>;
|
||||||
|
checkServiceIdEquivalence(
|
||||||
|
left: ServiceIdString | undefined,
|
||||||
|
right: ServiceIdString | undefined
|
||||||
|
): boolean;
|
||||||
renderIntl: StringRendererType<T>;
|
renderIntl: StringRendererType<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,6 +87,7 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
options: RenderOptionsType<T>
|
options: RenderOptionsType<T>
|
||||||
): string | T | ReadonlyArray<string | T> {
|
): string | T | ReadonlyArray<string | T> {
|
||||||
const {
|
const {
|
||||||
|
checkServiceIdEquivalence,
|
||||||
from,
|
from,
|
||||||
i18n: localizer,
|
i18n: localizer,
|
||||||
ourAci,
|
ourAci,
|
||||||
|
@ -243,11 +248,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
return i18n('icu:GroupV2--access-invite-link--enabled--you');
|
return i18n('icu:GroupV2--access-invite-link--enabled--you');
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--access-invite-link--enabled--other', {
|
||||||
'icu:GroupV2--access-invite-link--enabled--other',
|
adminName: renderContact(from),
|
||||||
|
});
|
||||||
{ adminName: renderContact(from) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return i18n('icu:GroupV2--access-invite-link--enabled--unknown');
|
return i18n('icu:GroupV2--access-invite-link--enabled--unknown');
|
||||||
}
|
}
|
||||||
|
@ -256,11 +259,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
return i18n('icu:GroupV2--access-invite-link--disabled--you');
|
return i18n('icu:GroupV2--access-invite-link--disabled--you');
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--access-invite-link--disabled--other', {
|
||||||
'icu:GroupV2--access-invite-link--disabled--other',
|
adminName: renderContact(from),
|
||||||
|
});
|
||||||
{ adminName: renderContact(from) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return i18n('icu:GroupV2--access-invite-link--disabled--unknown');
|
return i18n('icu:GroupV2--access-invite-link--disabled--unknown');
|
||||||
}
|
}
|
||||||
|
@ -304,7 +305,7 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
const weAreJoiner = isOurServiceId(aci);
|
const weAreJoiner = isOurServiceId(aci);
|
||||||
const weAreInviter = isOurServiceId(inviter);
|
const weAreInviter = isOurServiceId(inviter);
|
||||||
|
|
||||||
if (!from || from !== aci) {
|
if (!from || !checkServiceIdEquivalence(from, aci)) {
|
||||||
if (weAreJoiner) {
|
if (weAreJoiner) {
|
||||||
// They can't be the same, no fromYou check here
|
// They can't be the same, no fromYou check here
|
||||||
if (from) {
|
if (from) {
|
||||||
|
@ -350,13 +351,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
inviterName: renderContact(inviter),
|
inviterName: renderContact(inviter),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return i18n(
|
return i18n('icu:GroupV2--member-add--from-invite--other-no-from', {
|
||||||
'icu:GroupV2--member-add--from-invite--other-no-from',
|
|
||||||
|
|
||||||
{
|
|
||||||
inviteeName: renderContact(aci),
|
inviteeName: renderContact(aci),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (detail.type === 'member-add-from-link') {
|
if (detail.type === 'member-add-from-link') {
|
||||||
const { aci } = detail;
|
const { aci } = detail;
|
||||||
|
@ -383,11 +380,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
|
|
||||||
if (weAreJoiner) {
|
if (weAreJoiner) {
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--member-add-from-admin-approval--you--other', {
|
||||||
'icu:GroupV2--member-add-from-admin-approval--you--other',
|
adminName: renderContact(from),
|
||||||
|
});
|
||||||
{ adminName: renderContact(from) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: this shouldn't happen, because we only capture 'add-from-admin-approval'
|
// Note: this shouldn't happen, because we only capture 'add-from-admin-approval'
|
||||||
|
@ -399,31 +394,23 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--member-add-from-admin-approval--other--you', {
|
||||||
'icu:GroupV2--member-add-from-admin-approval--other--you',
|
joinerName: renderContact(aci),
|
||||||
|
});
|
||||||
{ joinerName: renderContact(aci) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--member-add-from-admin-approval--other--other', {
|
||||||
'icu:GroupV2--member-add-from-admin-approval--other--other',
|
|
||||||
|
|
||||||
{
|
|
||||||
adminName: renderContact(from),
|
adminName: renderContact(from),
|
||||||
joinerName: renderContact(aci),
|
joinerName: renderContact(aci),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: this shouldn't happen, because we only capture 'add-from-admin-approval'
|
// Note: this shouldn't happen, because we only capture 'add-from-admin-approval'
|
||||||
// status from group change events, which always have a sender.
|
// status from group change events, which always have a sender.
|
||||||
log.warn('member-add-from-admin-approval change type; we have no from');
|
log.warn('member-add-from-admin-approval change type; we have no from');
|
||||||
return i18n(
|
return i18n('icu:GroupV2--member-add-from-admin-approval--other--unknown', {
|
||||||
'icu:GroupV2--member-add-from-admin-approval--other--unknown',
|
joinerName: renderContact(aci),
|
||||||
|
});
|
||||||
{ joinerName: renderContact(aci) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (detail.type === 'member-remove') {
|
if (detail.type === 'member-remove') {
|
||||||
const { aci } = detail;
|
const { aci } = detail;
|
||||||
|
@ -446,7 +433,7 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
memberName: renderContact(aci),
|
memberName: renderContact(aci),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (from && from === aci) {
|
if (from && fromYou) {
|
||||||
return i18n('icu:GroupV2--member-remove--other--self', {
|
return i18n('icu:GroupV2--member-remove--other--self', {
|
||||||
memberName: renderContact(from),
|
memberName: renderContact(from),
|
||||||
});
|
});
|
||||||
|
@ -468,11 +455,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
if (newPrivilege === RoleEnum.ADMINISTRATOR) {
|
if (newPrivilege === RoleEnum.ADMINISTRATOR) {
|
||||||
if (weAreMember) {
|
if (weAreMember) {
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--member-privilege--promote--you--other', {
|
||||||
'icu:GroupV2--member-privilege--promote--you--other',
|
adminName: renderContact(from),
|
||||||
|
});
|
||||||
{ adminName: renderContact(from) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return i18n('icu:GroupV2--member-privilege--promote--you--unknown');
|
return i18n('icu:GroupV2--member-privilege--promote--you--unknown');
|
||||||
|
@ -509,20 +494,14 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--member-privilege--demote--other--other', {
|
||||||
'icu:GroupV2--member-privilege--demote--other--other',
|
|
||||||
|
|
||||||
{
|
|
||||||
adminName: renderContact(from),
|
adminName: renderContact(from),
|
||||||
memberName: renderContact(aci),
|
memberName: renderContact(aci),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
return i18n('icu:GroupV2--member-privilege--demote--other--unknown', {
|
||||||
}
|
memberName: renderContact(aci),
|
||||||
return i18n(
|
});
|
||||||
'icu:GroupV2--member-privilege--demote--other--unknown',
|
|
||||||
|
|
||||||
{ memberName: renderContact(aci) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
log.warn(
|
log.warn(
|
||||||
`member-privilege change type, privilege ${newPrivilege} is unknown`
|
`member-privilege change type, privilege ${newPrivilege} is unknown`
|
||||||
|
@ -586,14 +565,12 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--you',
|
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--you',
|
||||||
|
|
||||||
{ inviteeName: renderContact(serviceId) }
|
{ inviteeName: renderContact(serviceId) }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--other',
|
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--other',
|
||||||
|
|
||||||
{
|
{
|
||||||
adminName: renderContact(from),
|
adminName: renderContact(from),
|
||||||
inviteeName: renderContact(serviceId),
|
inviteeName: renderContact(serviceId),
|
||||||
|
@ -602,7 +579,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
}
|
}
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
|
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
|
||||||
|
|
||||||
{ inviteeName: renderContact(serviceId) }
|
{ inviteeName: renderContact(serviceId) }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -619,30 +595,24 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
}
|
}
|
||||||
if (inviter && sentByInviter) {
|
if (inviter && sentByInviter) {
|
||||||
if (weAreInvited) {
|
if (weAreInvited) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--pending-remove--revoke-own--to-you', {
|
||||||
'icu:GroupV2--pending-remove--revoke-own--to-you',
|
inviterName: renderContact(inviter),
|
||||||
|
});
|
||||||
{ inviterName: renderContact(inviter) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return i18n(
|
return i18n('icu:GroupV2--pending-remove--revoke-own--unknown', {
|
||||||
'icu:GroupV2--pending-remove--revoke-own--unknown',
|
inviterName: renderContact(inviter),
|
||||||
|
});
|
||||||
{ inviterName: renderContact(inviter) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (inviter) {
|
if (inviter) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from--one--you',
|
'icu:GroupV2--pending-remove--revoke-invite-from--one--you',
|
||||||
|
|
||||||
{ memberName: renderContact(inviter) }
|
{ memberName: renderContact(inviter) }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from--one--other',
|
'icu:GroupV2--pending-remove--revoke-invite-from--one--other',
|
||||||
|
|
||||||
{
|
{
|
||||||
adminName: renderContact(from),
|
adminName: renderContact(from),
|
||||||
memberName: renderContact(inviter),
|
memberName: renderContact(inviter),
|
||||||
|
@ -651,7 +621,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
}
|
}
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from--one--unknown',
|
'icu:GroupV2--pending-remove--revoke-invite-from--one--unknown',
|
||||||
|
|
||||||
{ memberName: renderContact(inviter) }
|
{ memberName: renderContact(inviter) }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -673,14 +642,12 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--you',
|
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--you',
|
||||||
|
|
||||||
{ count }
|
{ count }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--other',
|
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--other',
|
||||||
|
|
||||||
{
|
{
|
||||||
adminName: renderContact(from),
|
adminName: renderContact(from),
|
||||||
count,
|
count,
|
||||||
|
@ -689,7 +656,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
}
|
}
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--unknown',
|
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--unknown',
|
||||||
|
|
||||||
{ count }
|
{ count }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -697,7 +663,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from--many--you',
|
'icu:GroupV2--pending-remove--revoke-invite-from--many--you',
|
||||||
|
|
||||||
{
|
{
|
||||||
count,
|
count,
|
||||||
memberName: renderContact(inviter),
|
memberName: renderContact(inviter),
|
||||||
|
@ -707,7 +672,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from--many--other',
|
'icu:GroupV2--pending-remove--revoke-invite-from--many--other',
|
||||||
|
|
||||||
{
|
{
|
||||||
adminName: renderContact(from),
|
adminName: renderContact(from),
|
||||||
count,
|
count,
|
||||||
|
@ -717,7 +681,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
}
|
}
|
||||||
return i18n(
|
return i18n(
|
||||||
'icu:GroupV2--pending-remove--revoke-invite-from--many--unknown',
|
'icu:GroupV2--pending-remove--revoke-invite-from--many--unknown',
|
||||||
|
|
||||||
{
|
{
|
||||||
count,
|
count,
|
||||||
memberName: renderContact(inviter),
|
memberName: renderContact(inviter),
|
||||||
|
@ -730,20 +693,14 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--pending-remove--revoke--many--other', {
|
||||||
'icu:GroupV2--pending-remove--revoke--many--other',
|
|
||||||
|
|
||||||
{
|
|
||||||
memberName: renderContact(from),
|
memberName: renderContact(from),
|
||||||
count,
|
count,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
return i18n('icu:GroupV2--pending-remove--revoke--many--unknown', {
|
||||||
}
|
count,
|
||||||
return i18n(
|
});
|
||||||
'icu:GroupV2--pending-remove--revoke--many--unknown',
|
|
||||||
|
|
||||||
{ count }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (detail.type === 'admin-approval-add-one') {
|
if (detail.type === 'admin-approval-add-one') {
|
||||||
const { aci } = detail;
|
const { aci } = detail;
|
||||||
|
@ -768,35 +725,25 @@ function renderChangeDetail<T extends string | JSX.Element>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--admin-approval-remove-one--other--you', {
|
||||||
'icu:GroupV2--admin-approval-remove-one--other--you',
|
joinerName: renderContact(aci),
|
||||||
|
});
|
||||||
{ joinerName: renderContact(aci) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (from && from === aci) {
|
if (from && fromYou) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--admin-approval-remove-one--other--own', {
|
||||||
'icu:GroupV2--admin-approval-remove-one--other--own',
|
joinerName: renderContact(aci),
|
||||||
|
});
|
||||||
{ joinerName: renderContact(aci) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (from) {
|
if (from) {
|
||||||
return i18n(
|
return i18n('icu:GroupV2--admin-approval-remove-one--other--other', {
|
||||||
'icu:GroupV2--admin-approval-remove-one--other--other',
|
|
||||||
|
|
||||||
{
|
|
||||||
adminName: renderContact(from),
|
adminName: renderContact(from),
|
||||||
joinerName: renderContact(aci),
|
joinerName: renderContact(aci),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return i18n(
|
return i18n('icu:GroupV2--admin-approval-remove-one--other--unknown', {
|
||||||
'icu:GroupV2--admin-approval-remove-one--other--unknown',
|
joinerName: renderContact(aci),
|
||||||
|
});
|
||||||
{ joinerName: renderContact(aci) }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (detail.type === 'admin-approval-bounce') {
|
if (detail.type === 'admin-approval-bounce') {
|
||||||
const { aci, times, isApprovalPending } = detail;
|
const { aci, times, isApprovalPending } = detail;
|
||||||
|
|
|
@ -2429,12 +2429,15 @@ export async function initiateMigrationToGroupV2(
|
||||||
groupChangeMessages.push({
|
groupChangeMessages.push({
|
||||||
...generateBasicMessage(),
|
...generateBasicMessage(),
|
||||||
type: 'group-v1-migration',
|
type: 'group-v1-migration',
|
||||||
invitedGV2Members: pendingMembersV2.map(
|
groupMigration: {
|
||||||
|
areWeInvited: false,
|
||||||
|
droppedMemberIds: droppedGV2MemberIds,
|
||||||
|
invitedMembers: pendingMembersV2.map(
|
||||||
({ serviceId: uuid, ...rest }) => {
|
({ serviceId: uuid, ...rest }) => {
|
||||||
return { ...rest, uuid };
|
return { ...rest, uuid };
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
droppedGV2MemberIds,
|
},
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
seenStatus: SeenStatus.Seen,
|
seenStatus: SeenStatus.Seen,
|
||||||
});
|
});
|
||||||
|
|
54
ts/model-types.d.ts
vendored
54
ts/model-types.d.ts
vendored
|
@ -65,8 +65,12 @@ export type CustomError = Error & {
|
||||||
|
|
||||||
export type GroupMigrationType = {
|
export type GroupMigrationType = {
|
||||||
areWeInvited: boolean;
|
areWeInvited: boolean;
|
||||||
droppedMemberIds: Array<string>;
|
droppedMemberIds?: Array<string>;
|
||||||
invitedMembers: Array<LegacyMigrationPendingMemberType>;
|
invitedMembers?: Array<LegacyMigrationPendingMemberType>;
|
||||||
|
|
||||||
|
// We don't generate data like this; these were added to support import/export
|
||||||
|
droppedMemberCount?: number;
|
||||||
|
invitedMemberCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QuotedAttachment = {
|
export type QuotedAttachment = {
|
||||||
|
@ -131,6 +135,28 @@ export type EditHistoryType = {
|
||||||
received_at_ms?: number;
|
received_at_ms?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MessageType =
|
||||||
|
| 'call-history'
|
||||||
|
| 'change-number-notification'
|
||||||
|
| 'chat-session-refreshed'
|
||||||
|
| 'conversation-merge'
|
||||||
|
| 'delivery-issue'
|
||||||
|
| 'group-v1-migration'
|
||||||
|
| 'group-v2-change'
|
||||||
|
| 'group'
|
||||||
|
| 'incoming'
|
||||||
|
| 'keychange'
|
||||||
|
| 'outgoing'
|
||||||
|
| 'phone-number-discovery'
|
||||||
|
| 'profile-change'
|
||||||
|
| 'story'
|
||||||
|
| 'timer-notification'
|
||||||
|
| 'universal-timer-notification'
|
||||||
|
| 'contact-removed-notification'
|
||||||
|
| 'title-transition-notification'
|
||||||
|
| 'verified-change'
|
||||||
|
| 'message-request-response-event';
|
||||||
|
|
||||||
export type MessageAttributesType = {
|
export type MessageAttributesType = {
|
||||||
bodyAttachment?: AttachmentType;
|
bodyAttachment?: AttachmentType;
|
||||||
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
bodyRanges?: ReadonlyArray<RawBodyRange>;
|
||||||
|
@ -180,27 +206,7 @@ export type MessageAttributesType = {
|
||||||
verifiedChanged?: string;
|
verifiedChanged?: string;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: MessageType;
|
||||||
| 'call-history'
|
|
||||||
| 'change-number-notification'
|
|
||||||
| 'chat-session-refreshed'
|
|
||||||
| 'conversation-merge'
|
|
||||||
| 'delivery-issue'
|
|
||||||
| 'group-v1-migration'
|
|
||||||
| 'group-v2-change'
|
|
||||||
| 'group'
|
|
||||||
| 'incoming'
|
|
||||||
| 'keychange'
|
|
||||||
| 'outgoing'
|
|
||||||
| 'phone-number-discovery'
|
|
||||||
| 'profile-change'
|
|
||||||
| 'story'
|
|
||||||
| 'timer-notification'
|
|
||||||
| 'universal-timer-notification'
|
|
||||||
| 'contact-removed-notification'
|
|
||||||
| 'title-transition-notification'
|
|
||||||
| 'verified-change'
|
|
||||||
| 'message-request-response-event';
|
|
||||||
body?: string;
|
body?: string;
|
||||||
attachments?: Array<AttachmentType>;
|
attachments?: Array<AttachmentType>;
|
||||||
preview?: Array<LinkPreviewType>;
|
preview?: Array<LinkPreviewType>;
|
||||||
|
@ -437,7 +443,7 @@ export type ConversationAttributesType = {
|
||||||
};
|
};
|
||||||
announcementsOnly?: boolean;
|
announcementsOnly?: boolean;
|
||||||
avatar?: ContactAvatarType | null;
|
avatar?: ContactAvatarType | null;
|
||||||
avatars?: Array<AvatarDataType>;
|
avatars?: ReadonlyArray<Readonly<AvatarDataType>>;
|
||||||
description?: string;
|
description?: string;
|
||||||
expireTimer?: DurationInSeconds;
|
expireTimer?: DurationInSeconds;
|
||||||
membersV2?: Array<GroupV2MemberType>;
|
membersV2?: Array<GroupV2MemberType>;
|
||||||
|
|
|
@ -4516,9 +4516,10 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ourConversationId =
|
const ourConversation =
|
||||||
window.ConversationController.getOurConversationId();
|
window.ConversationController.getOurConversationOrThrow();
|
||||||
source = source || ourConversationId;
|
source = source || ourConversation.id;
|
||||||
|
const sourceServiceId = ourConversation.get('serviceId');
|
||||||
|
|
||||||
this.set({ expireTimer });
|
this.set({ expireTimer });
|
||||||
|
|
||||||
|
@ -4535,7 +4536,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const isFromSyncOperation =
|
const isFromSyncOperation =
|
||||||
reason === 'group sync' || reason === 'contact sync';
|
reason === 'group sync' || reason === 'contact sync';
|
||||||
const isFromMe =
|
const isFromMe =
|
||||||
window.ConversationController.get(source)?.id === ourConversationId;
|
window.ConversationController.get(source) === ourConversation;
|
||||||
const isNoteToSelf = isMe(this.attributes);
|
const isNoteToSelf = isMe(this.attributes);
|
||||||
const shouldBeRead =
|
const shouldBeRead =
|
||||||
(isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
|
(isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
|
||||||
|
@ -4547,6 +4548,7 @@ export class ConversationModel extends window.Backbone
|
||||||
expirationTimerUpdate: {
|
expirationTimerUpdate: {
|
||||||
expireTimer,
|
expireTimer,
|
||||||
source,
|
source,
|
||||||
|
sourceServiceId,
|
||||||
fromSync,
|
fromSync,
|
||||||
fromGroupUpdate,
|
fromGroupUpdate,
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,17 +2,21 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
import { Aci, Pni } from '@signalapp/libsignal-client';
|
import { Aci, Pni, ServiceId } from '@signalapp/libsignal-client';
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import pTimeout from 'p-timeout';
|
import pTimeout from 'p-timeout';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
import { Backups } from '../../protobuf';
|
import { Backups, SignalService } from '../../protobuf';
|
||||||
import Data from '../../sql/Client';
|
import Data from '../../sql/Client';
|
||||||
import type { PageMessagesCursorType } from '../../sql/Interface';
|
import type { PageMessagesCursorType } from '../../sql/Interface';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { StorySendMode, MY_STORY_ID } from '../../types/Stories';
|
import { StorySendMode, MY_STORY_ID } from '../../types/Stories';
|
||||||
import type { ServiceIdString } from '../../types/ServiceId';
|
import {
|
||||||
|
isPniString,
|
||||||
|
type AciString,
|
||||||
|
type ServiceIdString,
|
||||||
|
} from '../../types/ServiceId';
|
||||||
import type { RawBodyRange } from '../../types/BodyRange';
|
import type { RawBodyRange } from '../../types/BodyRange';
|
||||||
import { LONG_ATTACHMENT_LIMIT } from '../../types/Message';
|
import { LONG_ATTACHMENT_LIMIT } from '../../types/Message';
|
||||||
import type {
|
import type {
|
||||||
|
@ -25,6 +29,7 @@ import { drop } from '../../util/drop';
|
||||||
import { explodePromise } from '../../util/explodePromise';
|
import { explodePromise } from '../../util/explodePromise';
|
||||||
import {
|
import {
|
||||||
isDirectConversation,
|
isDirectConversation,
|
||||||
|
isGroup,
|
||||||
isGroupV2,
|
isGroupV2,
|
||||||
isMe,
|
isMe,
|
||||||
} from '../../util/whatTypeOfConversation';
|
} from '../../util/whatTypeOfConversation';
|
||||||
|
@ -42,11 +47,41 @@ import {
|
||||||
parsePhoneNumberSharingMode,
|
parsePhoneNumberSharingMode,
|
||||||
} from '../../util/phoneNumberSharingMode';
|
} from '../../util/phoneNumberSharingMode';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { isNormalBubble } from '../../state/selectors/message';
|
import {
|
||||||
|
isCallHistory,
|
||||||
|
isChatSessionRefreshed,
|
||||||
|
isContactRemovedNotification,
|
||||||
|
isConversationMerge,
|
||||||
|
isDeliveryIssue,
|
||||||
|
isEndSession,
|
||||||
|
isExpirationTimerUpdate,
|
||||||
|
isGiftBadge,
|
||||||
|
isGroupUpdate,
|
||||||
|
isGroupV1Migration,
|
||||||
|
isGroupV2Change,
|
||||||
|
isKeyChange,
|
||||||
|
isNormalBubble,
|
||||||
|
isPhoneNumberDiscovery,
|
||||||
|
isProfileChange,
|
||||||
|
isUniversalTimerNotification,
|
||||||
|
isUnsupportedMessage,
|
||||||
|
isVerifiedChange,
|
||||||
|
} from '../../state/selectors/message';
|
||||||
import * as Bytes from '../../Bytes';
|
import * as Bytes from '../../Bytes';
|
||||||
import { canBeSynced as canPreferredReactionEmojiBeSynced } from '../../reactions/preferredReactionEmoji';
|
import { canBeSynced as canPreferredReactionEmojiBeSynced } from '../../reactions/preferredReactionEmoji';
|
||||||
import { SendStatus } from '../../messages/MessageSendState';
|
import { SendStatus } from '../../messages/MessageSendState';
|
||||||
import { BACKUP_VERSION } from './constants';
|
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 { isAciString } from '../../util/isAciString';
|
||||||
|
import type { AboutMe } from './types';
|
||||||
|
import { messageHasPaymentEvent } from '../../messages/helpers';
|
||||||
|
import {
|
||||||
|
numberToAddressType,
|
||||||
|
numberToPhoneType,
|
||||||
|
} from '../../types/EmbeddedContact';
|
||||||
|
|
||||||
const MAX_CONCURRENCY = 10;
|
const MAX_CONCURRENCY = 10;
|
||||||
|
|
||||||
|
@ -224,6 +259,18 @@ export class BackupExportStream extends Readable {
|
||||||
|
|
||||||
let cursor: PageMessagesCursorType | undefined;
|
let cursor: PageMessagesCursorType | undefined;
|
||||||
|
|
||||||
|
const callHistory = getCallsHistoryForRedux();
|
||||||
|
const callHistoryByCallId = makeLookup(callHistory, 'callId');
|
||||||
|
|
||||||
|
const me = window.ConversationController.getOurConversationOrThrow();
|
||||||
|
const serviceId = me.get('serviceId');
|
||||||
|
const aci = isAciString(serviceId) ? serviceId : undefined;
|
||||||
|
strictAssert(aci, 'We must have our own ACI');
|
||||||
|
const aboutMe = {
|
||||||
|
aci,
|
||||||
|
pni: me.get('pni'),
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!cursor?.done) {
|
while (!cursor?.done) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
@ -232,7 +279,7 @@ export class BackupExportStream extends Readable {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const items = await pMap(
|
const items = await pMap(
|
||||||
messages,
|
messages,
|
||||||
message => this.toChatItem(message),
|
message => this.toChatItem(message, { aboutMe, callHistoryByCallId }),
|
||||||
{ concurrency: MAX_CONCURRENCY }
|
{ concurrency: MAX_CONCURRENCY }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -511,21 +558,22 @@ export class BackupExportStream extends Readable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async toChatItem(
|
private async toChatItem(
|
||||||
message: MessageAttributesType
|
message: MessageAttributesType,
|
||||||
): Promise<Backups.IChatItem | undefined> {
|
options: {
|
||||||
if (!isNormalBubble(message)) {
|
aboutMe: AboutMe;
|
||||||
return undefined;
|
callHistoryByCallId: Record<string, CallHistoryDetails>;
|
||||||
}
|
}
|
||||||
|
): Promise<Backups.IChatItem | undefined> {
|
||||||
const chatId = this.getRecipientId({ id: message.conversationId });
|
const chatId = this.getRecipientId({ id: message.conversationId });
|
||||||
if (chatId === undefined) {
|
if (chatId === undefined) {
|
||||||
log.warn('backups: message chat not found');
|
log.warn('backups: message chat not found');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let authorId: Long;
|
let authorId: Long | undefined;
|
||||||
|
|
||||||
const isOutgoing = message.type === 'outgoing';
|
const isOutgoing = message.type === 'outgoing';
|
||||||
|
const isIncoming = message.type === 'incoming';
|
||||||
|
|
||||||
if (isOutgoing) {
|
if (isOutgoing) {
|
||||||
const ourAci = window.storage.user.getCheckedAci();
|
const ourAci = window.storage.user.getCheckedAci();
|
||||||
|
@ -544,8 +592,9 @@ export class BackupExportStream extends Readable {
|
||||||
serviceId: message.sourceServiceId,
|
serviceId: message.sourceServiceId,
|
||||||
e164: message.source,
|
e164: message.source,
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
return undefined;
|
if (isOutgoing || isIncoming) {
|
||||||
|
strictAssert(authorId, 'Incoming/outgoing messages require an author');
|
||||||
}
|
}
|
||||||
|
|
||||||
let expireStartDate: Long | undefined;
|
let expireStartDate: Long | undefined;
|
||||||
|
@ -570,7 +619,14 @@ export class BackupExportStream extends Readable {
|
||||||
expiresInMs,
|
expiresInMs,
|
||||||
revisions: [],
|
revisions: [],
|
||||||
sms: false,
|
sms: false,
|
||||||
standardMessage: {
|
};
|
||||||
|
|
||||||
|
if (!isNormalBubble(message)) {
|
||||||
|
return this.toChatItemFromNonBubble(result, message, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (DESKTOP-6964): put incoming/outgoing fields below onto non-bubble messages
|
||||||
|
result.standardMessage = {
|
||||||
quote: await this.toQuote(message.quote),
|
quote: await this.toQuote(message.quote),
|
||||||
text: {
|
text: {
|
||||||
// Note that we store full text on the message model so we have to
|
// Note that we store full text on the message model so we have to
|
||||||
|
@ -599,7 +655,6 @@ export class BackupExportStream extends Readable {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isOutgoing) {
|
if (isOutgoing) {
|
||||||
|
@ -667,6 +722,795 @@ export class BackupExportStream extends Readable {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(indutny): convert to bytes
|
||||||
|
private aciToBytes(aci: AciString | string): Uint8Array {
|
||||||
|
return Aci.parseFromServiceIdString(aci).getRawUuidBytes();
|
||||||
|
}
|
||||||
|
private serviceIdToBytes(serviceId: ServiceIdString): Uint8Array {
|
||||||
|
return ServiceId.parseFromServiceIdString(serviceId).getRawUuidBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async toChatItemFromNonBubble(
|
||||||
|
chatItem: Backups.IChatItem,
|
||||||
|
message: MessageAttributesType,
|
||||||
|
options: {
|
||||||
|
aboutMe: AboutMe;
|
||||||
|
callHistoryByCallId: Record<string, CallHistoryDetails>;
|
||||||
|
}
|
||||||
|
): Promise<Backups.IChatItem | undefined> {
|
||||||
|
const { contact, sticker } = message;
|
||||||
|
|
||||||
|
if (contact && contact[0]) {
|
||||||
|
const contactMessage = new Backups.ContactMessage();
|
||||||
|
|
||||||
|
// TODO (DESKTOP-6845): properly handle avatarUrlPath
|
||||||
|
|
||||||
|
contactMessage.contact = contact.map(contactDetails => ({
|
||||||
|
...contactDetails,
|
||||||
|
number: contactDetails.number?.map(number => ({
|
||||||
|
...number,
|
||||||
|
type: numberToPhoneType(number.type),
|
||||||
|
})),
|
||||||
|
email: contactDetails.email?.map(email => ({
|
||||||
|
...email,
|
||||||
|
type: numberToPhoneType(email.type),
|
||||||
|
})),
|
||||||
|
address: contactDetails.address?.map(address => ({
|
||||||
|
...address,
|
||||||
|
type: numberToAddressType(address.type),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// TODO (DESKTOP-6964): add reactions
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
chatItem.contactMessage = contactMessage;
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isErased) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
chatItem.remoteDeletedMessage = new Backups.RemoteDeletedMessage();
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sticker) {
|
||||||
|
const stickerMessage = new Backups.StickerMessage();
|
||||||
|
|
||||||
|
const stickerProto = new Backups.Sticker();
|
||||||
|
stickerProto.emoji = sticker.emoji;
|
||||||
|
stickerProto.packId = Bytes.fromHex(sticker.packId);
|
||||||
|
stickerProto.packKey = Bytes.fromBase64(sticker.packKey);
|
||||||
|
stickerProto.stickerId = sticker.stickerId;
|
||||||
|
// TODO (DESKTOP-6845): properly handle data FilePointer
|
||||||
|
|
||||||
|
// TODO (DESKTOP-6964): add reactions
|
||||||
|
|
||||||
|
stickerMessage.sticker = stickerProto;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
chatItem.stickerMessage = stickerMessage;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.toChatItemUpdate(chatItem, message, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toChatItemUpdate(
|
||||||
|
chatItem: Backups.IChatItem,
|
||||||
|
message: MessageAttributesType,
|
||||||
|
options: {
|
||||||
|
aboutMe: AboutMe;
|
||||||
|
callHistoryByCallId: Record<string, CallHistoryDetails>;
|
||||||
|
}
|
||||||
|
): Promise<Backups.IChatItem | undefined> {
|
||||||
|
const logId = `toChatItemUpdate(${getMessageIdForLogging(message)})`;
|
||||||
|
|
||||||
|
const updateMessage = new Backups.ChatUpdateMessage();
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
chatItem.updateMessage = updateMessage;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpirationTimerUpdate(message)) {
|
||||||
|
const expiresInSeconds = message.expirationTimerUpdate?.expireTimer;
|
||||||
|
const expiresInMs = (expiresInSeconds ?? 0) * 1000;
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(
|
||||||
|
message.conversationId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conversation && isGroup(conversation.attributes)) {
|
||||||
|
const groupChatUpdate = new Backups.GroupChangeChatUpdate();
|
||||||
|
|
||||||
|
const timerUpdate = new Backups.GroupExpirationTimerUpdate();
|
||||||
|
timerUpdate.expiresInMs = expiresInMs;
|
||||||
|
|
||||||
|
const sourceServiceId = message.expirationTimerUpdate?.sourceServiceId;
|
||||||
|
if (sourceServiceId && Aci.parseFromServiceIdString(sourceServiceId)) {
|
||||||
|
timerUpdate.updaterAci = uuidToBytes(sourceServiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerUpdate = new Backups.GroupChangeChatUpdate.Update();
|
||||||
|
|
||||||
|
innerUpdate.groupExpirationTimerUpdate = timerUpdate;
|
||||||
|
|
||||||
|
groupChatUpdate.updates = [innerUpdate];
|
||||||
|
|
||||||
|
updateMessage.groupChange = groupChatUpdate;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source =
|
||||||
|
message.expirationTimerUpdate?.sourceServiceId ||
|
||||||
|
message.expirationTimerUpdate?.source;
|
||||||
|
if (source && !chatItem.authorId) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
chatItem.authorId = this.getOrPushPrivateRecipient({
|
||||||
|
id: source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const expirationTimerChange = new Backups.ExpirationTimerChatUpdate();
|
||||||
|
expirationTimerChange.expiresInMs = expiresInMs;
|
||||||
|
|
||||||
|
updateMessage.expirationTimerChange = expirationTimerChange;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGroupV2Change(message)) {
|
||||||
|
updateMessage.groupChange = await this.toGroupV2Update(message, options);
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKeyChange(message)) {
|
||||||
|
const simpleUpdate = new Backups.SimpleChatUpdate();
|
||||||
|
simpleUpdate.type = Backups.SimpleChatUpdate.Type.IDENTITY_UPDATE;
|
||||||
|
|
||||||
|
updateMessage.simpleUpdate = simpleUpdate;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProfileChange(message)) {
|
||||||
|
const profileChange = new Backups.ProfileChangeChatUpdate();
|
||||||
|
if (!message.profileChange) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newName, oldName } = message.profileChange;
|
||||||
|
profileChange.newName = newName;
|
||||||
|
profileChange.previousName = oldName;
|
||||||
|
|
||||||
|
updateMessage.profileChange = profileChange;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVerifiedChange(message)) {
|
||||||
|
// TODO (DESKTOP-6964)): it can't be this simple if we show this in groups, right?
|
||||||
|
|
||||||
|
const simpleUpdate = new Backups.SimpleChatUpdate();
|
||||||
|
simpleUpdate.type = Backups.SimpleChatUpdate.Type.IDENTITY_VERIFIED;
|
||||||
|
|
||||||
|
updateMessage.simpleUpdate = simpleUpdate;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConversationMerge(message)) {
|
||||||
|
const threadMerge = new Backups.ThreadMergeChatUpdate();
|
||||||
|
const e164 = message.conversationMerge?.renderInfo.e164;
|
||||||
|
if (!e164) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
threadMerge.previousE164 = Long.fromString(e164);
|
||||||
|
|
||||||
|
updateMessage.threadMerge = threadMerge;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPhoneNumberDiscovery(message)) {
|
||||||
|
// TODO (DESKTOP-6964): need to add to protos
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUniversalTimerNotification(message)) {
|
||||||
|
// TODO (DESKTOP-6964): need to add to protos
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isContactRemovedNotification(message)) {
|
||||||
|
// TODO (DESKTOP-6964): this doesn't appear to be in the protos at all
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageHasPaymentEvent(message)) {
|
||||||
|
// TODO (DESKTOP-6964): are these enough?
|
||||||
|
// SimpleChatUpdate
|
||||||
|
// PAYMENTS_ACTIVATED
|
||||||
|
// PAYMENT_ACTIVATION_REQUEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGiftBadge(message)) {
|
||||||
|
// TODO (DESKTOP-6964)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGroupUpdate(message)) {
|
||||||
|
// TODO (DESKTOP-6964)
|
||||||
|
// these old-school message types are no longer generated but we probably
|
||||||
|
// still want to render them
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGroupV1Migration(message)) {
|
||||||
|
const { groupMigration } = message;
|
||||||
|
|
||||||
|
const groupChatUpdate = new Backups.GroupChangeChatUpdate();
|
||||||
|
|
||||||
|
groupChatUpdate.updates = [];
|
||||||
|
|
||||||
|
const areWeInvited = groupMigration?.areWeInvited ?? false;
|
||||||
|
const droppedMemberCount =
|
||||||
|
groupMigration?.droppedMemberCount ??
|
||||||
|
groupMigration?.droppedMemberIds?.length ??
|
||||||
|
message.droppedGV2MemberIds?.length ??
|
||||||
|
0;
|
||||||
|
const invitedMemberCount =
|
||||||
|
groupMigration?.invitedMemberCount ??
|
||||||
|
groupMigration?.invitedMembers?.length ??
|
||||||
|
message.invitedGV2Members?.length ??
|
||||||
|
0;
|
||||||
|
|
||||||
|
let addedItem = false;
|
||||||
|
if (areWeInvited) {
|
||||||
|
const container = new Backups.GroupChangeChatUpdate.Update();
|
||||||
|
container.groupV2MigrationSelfInvitedUpdate =
|
||||||
|
new Backups.GroupV2MigrationSelfInvitedUpdate();
|
||||||
|
groupChatUpdate.updates.push(container);
|
||||||
|
addedItem = true;
|
||||||
|
}
|
||||||
|
if (droppedMemberCount > 0) {
|
||||||
|
const container = new Backups.GroupChangeChatUpdate.Update();
|
||||||
|
const update = new Backups.GroupV2MigrationDroppedMembersUpdate();
|
||||||
|
update.droppedMembersCount = droppedMemberCount;
|
||||||
|
container.groupV2MigrationDroppedMembersUpdate = update;
|
||||||
|
groupChatUpdate.updates.push(container);
|
||||||
|
addedItem = true;
|
||||||
|
}
|
||||||
|
if (invitedMemberCount > 0) {
|
||||||
|
const container = new Backups.GroupChangeChatUpdate.Update();
|
||||||
|
const update = new Backups.GroupV2MigrationInvitedMembersUpdate();
|
||||||
|
update.invitedMembersCount = invitedMemberCount;
|
||||||
|
container.groupV2MigrationInvitedMembersUpdate = update;
|
||||||
|
groupChatUpdate.updates.push(container);
|
||||||
|
addedItem = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addedItem) {
|
||||||
|
const container = new Backups.GroupChangeChatUpdate.Update();
|
||||||
|
container.groupV2MigrationUpdate = new Backups.GroupV2MigrationUpdate();
|
||||||
|
groupChatUpdate.updates.push(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMessage.groupChange = groupChatUpdate;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeliveryIssue(message)) {
|
||||||
|
// TODO (DESKTOP-6964)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEndSession(message)) {
|
||||||
|
const simpleUpdate = new Backups.SimpleChatUpdate();
|
||||||
|
simpleUpdate.type = Backups.SimpleChatUpdate.Type.END_SESSION;
|
||||||
|
|
||||||
|
updateMessage.simpleUpdate = simpleUpdate;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChatSessionRefreshed(message)) {
|
||||||
|
const simpleUpdate = new Backups.SimpleChatUpdate();
|
||||||
|
simpleUpdate.type = Backups.SimpleChatUpdate.Type.CHAT_SESSION_REFRESH;
|
||||||
|
|
||||||
|
updateMessage.simpleUpdate = simpleUpdate;
|
||||||
|
|
||||||
|
return chatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUnsupportedMessage(message)) {
|
||||||
|
// TODO (DESKTOP-6964): need to add to protos
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: Message was not a bubble, but didn't understand type`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toGroupV2Update(
|
||||||
|
message: MessageAttributesType,
|
||||||
|
options: {
|
||||||
|
aboutMe: AboutMe;
|
||||||
|
}
|
||||||
|
): Promise<Backups.GroupChangeChatUpdate | undefined> {
|
||||||
|
const logId = `toGroupV2Update(${getMessageIdForLogging(message)})`;
|
||||||
|
|
||||||
|
const { groupV2Change } = message;
|
||||||
|
const { aboutMe } = options;
|
||||||
|
if (!isGroupV2Change(message) || !groupV2Change) {
|
||||||
|
throw new Error(`${logId}: Message was not a groupv2 change`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { from, details } = groupV2Change;
|
||||||
|
const updates: Array<Backups.GroupChangeChatUpdate.Update> = [];
|
||||||
|
|
||||||
|
details.forEach(detail => {
|
||||||
|
const update = new Backups.GroupChangeChatUpdate.Update();
|
||||||
|
const { type } = detail;
|
||||||
|
|
||||||
|
if (type === 'create') {
|
||||||
|
const innerUpdate = new Backups.GroupCreationUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
update.groupCreationUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'access-attributes') {
|
||||||
|
const innerUpdate =
|
||||||
|
new Backups.GroupAttributesAccessLevelChangeUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.accessLevel = detail.newPrivilege;
|
||||||
|
|
||||||
|
update.groupAttributesAccessLevelChangeUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'access-members') {
|
||||||
|
const innerUpdate =
|
||||||
|
new Backups.GroupMembershipAccessLevelChangeUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.accessLevel = detail.newPrivilege;
|
||||||
|
|
||||||
|
update.groupMembershipAccessLevelChangeUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'access-invite-link') {
|
||||||
|
const innerUpdate = new Backups.GroupInviteLinkAdminApprovalUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.linkRequiresAdminApproval =
|
||||||
|
detail.newPrivilege ===
|
||||||
|
SignalService.AccessControl.AccessRequired.ADMINISTRATOR;
|
||||||
|
|
||||||
|
update.groupInviteLinkAdminApprovalUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'announcements-only') {
|
||||||
|
const innerUpdate = new Backups.GroupAnnouncementOnlyChangeUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.isAnnouncementOnly = detail.announcementsOnly;
|
||||||
|
|
||||||
|
update.groupAnnouncementOnlyChangeUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'avatar') {
|
||||||
|
const innerUpdate = new Backups.GroupAvatarUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.wasRemoved = detail.removed;
|
||||||
|
|
||||||
|
update.groupAvatarUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'title') {
|
||||||
|
const innerUpdate = new Backups.GroupNameUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.newGroupName = detail.newTitle;
|
||||||
|
|
||||||
|
update.groupNameUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'group-link-add') {
|
||||||
|
const innerUpdate = new Backups.GroupInviteLinkEnabledUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.linkRequiresAdminApproval =
|
||||||
|
detail.privilege ===
|
||||||
|
SignalService.AccessControl.AccessRequired.ADMINISTRATOR;
|
||||||
|
|
||||||
|
update.groupInviteLinkEnabledUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'group-link-reset') {
|
||||||
|
const innerUpdate = new Backups.GroupInviteLinkResetUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
update.groupInviteLinkResetUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'group-link-remove') {
|
||||||
|
const innerUpdate = new Backups.GroupInviteLinkDisabledUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
update.groupInviteLinkDisabledUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'member-add') {
|
||||||
|
if (from && from === detail.aci) {
|
||||||
|
const innerUpdate = new Backups.GroupMemberJoinedUpdate();
|
||||||
|
innerUpdate.newMemberAci = this.serviceIdToBytes(from);
|
||||||
|
|
||||||
|
update.groupMemberJoinedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerUpdate = new Backups.GroupMemberAddedUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.newMemberAci = this.aciToBytes(detail.aci);
|
||||||
|
|
||||||
|
update.groupMemberAddedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'member-add-from-invite') {
|
||||||
|
if (from && checkServiceIdEquivalence(from, detail.aci)) {
|
||||||
|
const innerUpdate = new Backups.GroupInvitationAcceptedUpdate();
|
||||||
|
innerUpdate.newMemberAci = this.aciToBytes(detail.aci);
|
||||||
|
if (detail.inviter) {
|
||||||
|
innerUpdate.inviterAci = this.aciToBytes(detail.inviter);
|
||||||
|
}
|
||||||
|
update.groupInvitationAcceptedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerUpdate = new Backups.GroupMemberAddedUpdate();
|
||||||
|
innerUpdate.newMemberAci = this.aciToBytes(detail.aci);
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
if (detail.inviter) {
|
||||||
|
innerUpdate.inviterAci = this.aciToBytes(detail.inviter);
|
||||||
|
}
|
||||||
|
innerUpdate.hadOpenInvitation = true;
|
||||||
|
|
||||||
|
update.groupMemberAddedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'member-add-from-link') {
|
||||||
|
const innerUpdate = new Backups.GroupMemberJoinedByLinkUpdate();
|
||||||
|
innerUpdate.newMemberAci = this.aciToBytes(detail.aci);
|
||||||
|
|
||||||
|
update.groupMemberJoinedByLinkUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'member-add-from-admin-approval') {
|
||||||
|
const innerUpdate = new Backups.GroupJoinRequestApprovalUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
innerUpdate.requestorAci = this.aciToBytes(detail.aci);
|
||||||
|
innerUpdate.wasApproved = true;
|
||||||
|
|
||||||
|
update.groupJoinRequestApprovalUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'member-privilege') {
|
||||||
|
const innerUpdate = new Backups.GroupAdminStatusUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
innerUpdate.memberAci = this.aciToBytes(detail.aci);
|
||||||
|
innerUpdate.wasAdminStatusGranted =
|
||||||
|
detail.newPrivilege === SignalService.Member.Role.ADMINISTRATOR;
|
||||||
|
|
||||||
|
update.groupAdminStatusUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'member-remove') {
|
||||||
|
if (from && from === detail.aci) {
|
||||||
|
const innerUpdate = new Backups.GroupMemberLeftUpdate();
|
||||||
|
innerUpdate.aci = this.serviceIdToBytes(from);
|
||||||
|
|
||||||
|
update.groupMemberLeftUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerUpdate = new Backups.GroupMemberRemovedUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.removerAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.removedAci = this.aciToBytes(detail.aci);
|
||||||
|
|
||||||
|
update.groupMemberRemovedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'pending-add-one') {
|
||||||
|
if (
|
||||||
|
(aboutMe.aci && detail.serviceId === aboutMe.aci) ||
|
||||||
|
(aboutMe.pni && detail.serviceId === aboutMe.pni)
|
||||||
|
) {
|
||||||
|
const innerUpdate = new Backups.SelfInvitedToGroupUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.inviterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
update.selfInvitedToGroupUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
from &&
|
||||||
|
((aboutMe.aci && from === aboutMe.aci) ||
|
||||||
|
(aboutMe.pni && from === aboutMe.pni))
|
||||||
|
) {
|
||||||
|
const innerUpdate = new Backups.SelfInvitedOtherUserToGroupUpdate();
|
||||||
|
innerUpdate.inviteeServiceId = this.serviceIdToBytes(
|
||||||
|
detail.serviceId
|
||||||
|
);
|
||||||
|
|
||||||
|
update.selfInvitedOtherUserToGroupUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerUpdate = new Backups.GroupUnknownInviteeUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.inviterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.inviteeCount = 1;
|
||||||
|
|
||||||
|
update.groupUnknownInviteeUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'pending-add-many') {
|
||||||
|
const innerUpdate = new Backups.GroupUnknownInviteeUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.inviterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.inviteeCount = detail.count;
|
||||||
|
|
||||||
|
update.groupUnknownInviteeUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'pending-remove-one') {
|
||||||
|
if (from && detail.serviceId && from === detail.serviceId) {
|
||||||
|
const innerUpdate = new Backups.GroupInvitationDeclinedUpdate();
|
||||||
|
if (detail.inviter) {
|
||||||
|
innerUpdate.inviterAci = this.aciToBytes(detail.inviter);
|
||||||
|
}
|
||||||
|
if (isAciString(detail.serviceId)) {
|
||||||
|
innerUpdate.inviteeAci = this.aciToBytes(detail.serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
update.groupInvitationDeclinedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(aboutMe.aci && detail.serviceId === aboutMe.aci) ||
|
||||||
|
(aboutMe.pni && detail.serviceId === aboutMe.pni)
|
||||||
|
) {
|
||||||
|
const innerUpdate = new Backups.GroupSelfInvitationRevokedUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.revokerAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
update.groupSelfInvitationRevokedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerUpdate = new Backups.GroupInvitationRevokedUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
innerUpdate.invitees = [
|
||||||
|
{
|
||||||
|
inviteeAci: isAciString(detail.serviceId)
|
||||||
|
? this.aciToBytes(detail.serviceId)
|
||||||
|
: undefined,
|
||||||
|
inviteePni: isPniString(detail.serviceId)
|
||||||
|
? this.serviceIdToBytes(detail.serviceId)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
update.groupInvitationRevokedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'pending-remove-many') {
|
||||||
|
const innerUpdate = new Backups.GroupInvitationRevokedUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
innerUpdate.invitees = [];
|
||||||
|
for (let i = 0, max = detail.count; i < max; i += 1) {
|
||||||
|
// Yes, we're adding totally empty invitees. This is okay.
|
||||||
|
innerUpdate.invitees.push({});
|
||||||
|
}
|
||||||
|
|
||||||
|
update.groupInvitationRevokedUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'admin-approval-add-one') {
|
||||||
|
const innerUpdate = new Backups.GroupJoinRequestUpdate();
|
||||||
|
innerUpdate.requestorAci = this.aciToBytes(detail.aci);
|
||||||
|
|
||||||
|
update.groupJoinRequestUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'admin-approval-remove-one') {
|
||||||
|
if (from && detail.aci && from === detail.aci) {
|
||||||
|
const innerUpdate = new Backups.GroupJoinRequestCanceledUpdate();
|
||||||
|
innerUpdate.requestorAci = this.aciToBytes(detail.aci);
|
||||||
|
|
||||||
|
update.groupJoinRequestCanceledUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerUpdate = new Backups.GroupJoinRequestApprovalUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
innerUpdate.requestorAci = this.aciToBytes(detail.aci);
|
||||||
|
innerUpdate.wasApproved = false;
|
||||||
|
|
||||||
|
update.groupJoinRequestApprovalUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'admin-approval-bounce') {
|
||||||
|
// We can't express all we need in GroupSequenceOfRequestsAndCancelsUpdate, so we
|
||||||
|
// add an additional groupJoinRequestUpdate to express that there
|
||||||
|
// is an approval pending.
|
||||||
|
if (detail.isApprovalPending) {
|
||||||
|
const innerUpdate = new Backups.GroupJoinRequestUpdate();
|
||||||
|
innerUpdate.requestorAci = this.aciToBytes(detail.aci);
|
||||||
|
|
||||||
|
// We need to create another update since the items we put in Update are oneof
|
||||||
|
const secondUpdate = new Backups.GroupChangeChatUpdate.Update();
|
||||||
|
secondUpdate.groupJoinRequestUpdate = innerUpdate;
|
||||||
|
updates.push(secondUpdate);
|
||||||
|
|
||||||
|
// not returning because we really do want both of these
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerUpdate =
|
||||||
|
new Backups.GroupSequenceOfRequestsAndCancelsUpdate();
|
||||||
|
innerUpdate.requestorAci = this.aciToBytes(detail.aci);
|
||||||
|
innerUpdate.count = detail.times;
|
||||||
|
|
||||||
|
update.groupSequenceOfRequestsAndCancelsUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'description') {
|
||||||
|
const innerUpdate = new Backups.GroupDescriptionUpdate();
|
||||||
|
innerUpdate.newDescription = detail.removed
|
||||||
|
? undefined
|
||||||
|
: detail.description;
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
update.groupDescriptionUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else if (type === 'summary') {
|
||||||
|
const innerUpdate = new Backups.GenericGroupUpdate();
|
||||||
|
if (from) {
|
||||||
|
innerUpdate.updaterAci = this.serviceIdToBytes(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
update.genericGroupUpdate = innerUpdate;
|
||||||
|
updates.push(update);
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
throw new Error(`${logId}: No updates generated from message`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupUpdate = new Backups.GroupChangeChatUpdate();
|
||||||
|
groupUpdate.updates = updates;
|
||||||
|
|
||||||
|
return groupUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
private async toQuote(
|
private async toQuote(
|
||||||
quote?: QuotedMessageType
|
quote?: QuotedMessageType
|
||||||
): Promise<Backups.IQuote | null> {
|
): Promise<Backups.IQuote | null> {
|
||||||
|
@ -720,9 +1564,7 @@ export class BackupExportStream extends Readable {
|
||||||
|
|
||||||
...('mentionAci' in range
|
...('mentionAci' in range
|
||||||
? {
|
? {
|
||||||
mentionAci: Aci.parseFromServiceIdString(
|
mentionAci: this.aciToBytes(range.mentionAci),
|
||||||
range.mentionAci
|
|
||||||
).getRawUuidBytes(),
|
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
// Numeric values are compatible between backup and message protos
|
// Numeric values are compatible between backup and message protos
|
||||||
|
@ -731,3 +1573,13 @@ export class BackupExportStream extends Readable {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkServiceIdEquivalence(
|
||||||
|
left: ServiceIdString | undefined,
|
||||||
|
right: ServiceIdString | undefined
|
||||||
|
) {
|
||||||
|
const leftConvo = window.ConversationController.get(left);
|
||||||
|
const rightConvo = window.ConversationController.get(right);
|
||||||
|
|
||||||
|
return leftConvo && rightConvo && leftConvo === rightConvo;
|
||||||
|
}
|
||||||
|
|
|
@ -5,11 +5,13 @@ import { Aci, Pni } from '@signalapp/libsignal-client';
|
||||||
import { v4 as generateUuid } from 'uuid';
|
import { v4 as generateUuid } from 'uuid';
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import { Writable } from 'stream';
|
import { Writable } from 'stream';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
import { Backups } from '../../protobuf';
|
import { Backups, SignalService } from '../../protobuf';
|
||||||
import Data from '../../sql/Client';
|
import Data from '../../sql/Client';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { StorySendMode } from '../../types/Stories';
|
import { StorySendMode } from '../../types/Stories';
|
||||||
|
import type { ServiceIdString } from '../../types/ServiceId';
|
||||||
import { fromAciObject, fromPniObject } from '../../types/ServiceId';
|
import { fromAciObject, fromPniObject } from '../../types/ServiceId';
|
||||||
import * as Errors from '../../types/errors';
|
import * as Errors from '../../types/errors';
|
||||||
import type {
|
import type {
|
||||||
|
@ -36,6 +38,9 @@ import type { SendStateByConversationId } from '../../messages/MessageSendState'
|
||||||
import { SeenStatus } from '../../MessageSeenStatus';
|
import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
import * as Bytes from '../../Bytes';
|
import * as Bytes from '../../Bytes';
|
||||||
import { BACKUP_VERSION } from './constants';
|
import { BACKUP_VERSION } from './constants';
|
||||||
|
import type { AboutMe } from './types';
|
||||||
|
import type { GroupV2ChangeDetailType } from '../../groups';
|
||||||
|
import { isNotNil } from '../../util/isNotNil';
|
||||||
|
|
||||||
const MAX_CONCURRENCY = 10;
|
const MAX_CONCURRENCY = 10;
|
||||||
|
|
||||||
|
@ -44,6 +49,11 @@ type ConversationOpType = Readonly<{
|
||||||
attributes: ConversationAttributesType;
|
attributes: ConversationAttributesType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type ChatItemParseResult = {
|
||||||
|
message: Partial<MessageAttributesType>;
|
||||||
|
additionalMessages: Array<Partial<MessageAttributesType>>;
|
||||||
|
};
|
||||||
|
|
||||||
async function processConversationOpBatch(
|
async function processConversationOpBatch(
|
||||||
batch: ReadonlyArray<ConversationOpType>
|
batch: ReadonlyArray<ConversationOpType>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -67,6 +77,7 @@ async function processConversationOpBatch(
|
||||||
export class BackupImportStream extends Writable {
|
export class BackupImportStream extends Writable {
|
||||||
private parsedBackupInfo = false;
|
private parsedBackupInfo = false;
|
||||||
private logId = 'BackupImportStream(unknown)';
|
private logId = 'BackupImportStream(unknown)';
|
||||||
|
private aboutMe: AboutMe | undefined;
|
||||||
|
|
||||||
private readonly recipientIdToConvo = new Map<
|
private readonly recipientIdToConvo = new Map<
|
||||||
number,
|
number,
|
||||||
|
@ -126,7 +137,19 @@ export class BackupImportStream extends Writable {
|
||||||
} else {
|
} else {
|
||||||
const frame = Backups.Frame.decode(data);
|
const frame = Backups.Frame.decode(data);
|
||||||
|
|
||||||
await this.processFrame(frame);
|
await this.processFrame(frame, { aboutMe: this.aboutMe });
|
||||||
|
|
||||||
|
if (!this.aboutMe && this.ourConversation) {
|
||||||
|
const { serviceId, pni } = this.ourConversation;
|
||||||
|
strictAssert(
|
||||||
|
isAciString(serviceId),
|
||||||
|
'ourConversation serviceId must be ACI'
|
||||||
|
);
|
||||||
|
this.aboutMe = {
|
||||||
|
aci: serviceId,
|
||||||
|
pni,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
done();
|
done();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -181,7 +204,12 @@ export class BackupImportStream extends Writable {
|
||||||
this.saveMessageBatcher.unregister();
|
this.saveMessageBatcher.unregister();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processFrame(frame: Backups.Frame): Promise<void> {
|
private async processFrame(
|
||||||
|
frame: Backups.Frame,
|
||||||
|
options: { aboutMe?: AboutMe }
|
||||||
|
): Promise<void> {
|
||||||
|
const { aboutMe } = options;
|
||||||
|
|
||||||
if (frame.account) {
|
if (frame.account) {
|
||||||
await this.fromAccount(frame.account);
|
await this.fromAccount(frame.account);
|
||||||
|
|
||||||
|
@ -215,7 +243,13 @@ export class BackupImportStream extends Writable {
|
||||||
} else if (frame.chat) {
|
} else if (frame.chat) {
|
||||||
await this.fromChat(frame.chat);
|
await this.fromChat(frame.chat);
|
||||||
} else if (frame.chatItem) {
|
} else if (frame.chatItem) {
|
||||||
await this.fromChatItem(frame.chatItem);
|
if (!aboutMe) {
|
||||||
|
throw new Error(
|
||||||
|
'processFrame: Processing a chatItem frame, but no aboutMe data!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fromChatItem(frame.chatItem, { aboutMe });
|
||||||
} else {
|
} else {
|
||||||
log.warn(`${this.logId}: unsupported frame item ${frame.item}`);
|
log.warn(`${this.logId}: unsupported frame item ${frame.item}`);
|
||||||
}
|
}
|
||||||
|
@ -494,30 +528,46 @@ export class BackupImportStream extends Writable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fromChatItem(item: Backups.IChatItem): Promise<void> {
|
private async fromChatItem(
|
||||||
strictAssert(this.ourConversation != null, 'AccountData missing');
|
item: Backups.IChatItem,
|
||||||
|
options: { aboutMe: AboutMe }
|
||||||
|
): Promise<void> {
|
||||||
|
const { aboutMe } = options;
|
||||||
|
|
||||||
strictAssert(item.chatId != null, 'chatItem must have a chatId');
|
const timestamp = item?.dateSent?.toNumber();
|
||||||
strictAssert(item.authorId != null, 'chatItem must have a authorId');
|
const logId = `fromChatItem(${timestamp})`;
|
||||||
strictAssert(item.dateSent != null, 'chatItem must have a dateSent');
|
|
||||||
|
strictAssert(this.ourConversation != null, `${logId}: AccountData missing`);
|
||||||
|
|
||||||
|
strictAssert(item.chatId != null, `${logId}: must have a chatId`);
|
||||||
|
strictAssert(item.dateSent != null, `${logId}: must have a dateSent`);
|
||||||
|
strictAssert(timestamp, `${logId}: must have a timestamp`);
|
||||||
|
|
||||||
const chatConvo = this.chatIdToConvo.get(item.chatId.toNumber());
|
const chatConvo = this.chatIdToConvo.get(item.chatId.toNumber());
|
||||||
strictAssert(chatConvo !== undefined, 'chat conversation not found');
|
strictAssert(
|
||||||
|
chatConvo !== undefined,
|
||||||
|
`${logId}: chat conversation not found`
|
||||||
|
);
|
||||||
|
|
||||||
const authorConvo = this.recipientIdToConvo.get(item.authorId.toNumber());
|
const authorConvo = item.authorId
|
||||||
strictAssert(authorConvo !== undefined, 'author conversation not found');
|
? this.recipientIdToConvo.get(item.authorId.toNumber())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const isOutgoing = this.ourConversation.id === authorConvo.id;
|
const isOutgoing =
|
||||||
|
authorConvo && this.ourConversation.id === authorConvo?.id;
|
||||||
|
const isIncoming =
|
||||||
|
authorConvo && this.ourConversation.id !== authorConvo?.id;
|
||||||
|
const isDirectionLess = !isOutgoing && !isIncoming;
|
||||||
|
|
||||||
let attributes: MessageAttributesType = {
|
let attributes: MessageAttributesType = {
|
||||||
id: generateUuid(),
|
id: generateUuid(),
|
||||||
canReplyToStory: false,
|
canReplyToStory: false,
|
||||||
conversationId: chatConvo.id,
|
conversationId: chatConvo.id,
|
||||||
received_at: incrementMessageCounter(),
|
received_at: incrementMessageCounter(),
|
||||||
sent_at: item.dateSent.toNumber(),
|
sent_at: timestamp,
|
||||||
source: authorConvo.e164,
|
source: authorConvo?.e164,
|
||||||
sourceServiceId: authorConvo.serviceId,
|
sourceServiceId: authorConvo?.serviceId,
|
||||||
timestamp: item.dateSent.toNumber(),
|
timestamp,
|
||||||
type: isOutgoing ? 'outgoing' : 'incoming',
|
type: isOutgoing ? 'outgoing' : 'incoming',
|
||||||
unidentifiedDeliveryReceived: false,
|
unidentifiedDeliveryReceived: false,
|
||||||
expirationStartTimestamp: item.expireStartDate
|
expirationStartTimestamp: item.expireStartDate
|
||||||
|
@ -527,10 +577,14 @@ export class BackupImportStream extends Writable {
|
||||||
? DurationInSeconds.fromMillis(item.expiresInMs.toNumber())
|
? DurationInSeconds.fromMillis(item.expiresInMs.toNumber())
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
const additionalMessages: Array<MessageAttributesType> = [];
|
||||||
|
|
||||||
if (isOutgoing) {
|
const { outgoing, incoming, directionless } = item;
|
||||||
const { outgoing } = item;
|
if (outgoing) {
|
||||||
strictAssert(outgoing, 'outgoing message must have outgoing field');
|
strictAssert(
|
||||||
|
isOutgoing,
|
||||||
|
`${logId}: outgoing message must have outgoing field`
|
||||||
|
);
|
||||||
|
|
||||||
const sendStateByConversationId: SendStateByConversationId = {};
|
const sendStateByConversationId: SendStateByConversationId = {};
|
||||||
|
|
||||||
|
@ -583,9 +637,12 @@ export class BackupImportStream extends Writable {
|
||||||
|
|
||||||
attributes.sendStateByConversationId = sendStateByConversationId;
|
attributes.sendStateByConversationId = sendStateByConversationId;
|
||||||
chatConvo.active_at = attributes.sent_at;
|
chatConvo.active_at = attributes.sent_at;
|
||||||
} else {
|
}
|
||||||
const { incoming } = item;
|
if (incoming) {
|
||||||
strictAssert(incoming, 'incoming message must have incoming field');
|
strictAssert(
|
||||||
|
isIncoming,
|
||||||
|
`${logId}: message with incoming field must be incoming`
|
||||||
|
);
|
||||||
attributes.received_at_ms =
|
attributes.received_at_ms =
|
||||||
incoming.dateReceived?.toNumber() ?? Date.now();
|
incoming.dateReceived?.toNumber() ?? Date.now();
|
||||||
|
|
||||||
|
@ -600,25 +657,63 @@ export class BackupImportStream extends Writable {
|
||||||
|
|
||||||
chatConvo.active_at = attributes.received_at_ms;
|
chatConvo.active_at = attributes.received_at_ms;
|
||||||
}
|
}
|
||||||
|
if (directionless) {
|
||||||
|
strictAssert(
|
||||||
|
isDirectionLess,
|
||||||
|
`${logId}: directionless message must not be incoming/outgoing`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (item.standardMessage) {
|
if (item.standardMessage) {
|
||||||
|
// TODO (DESKTOP-6964): add revisions to editHistory
|
||||||
|
|
||||||
attributes = {
|
attributes = {
|
||||||
...attributes,
|
...attributes,
|
||||||
...this.fromStandardMessage(item.standardMessage),
|
...this.fromStandardMessage(item.standardMessage),
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
const result = await this.fromNonBubbleChatItem(item, {
|
||||||
|
aboutMe,
|
||||||
|
author: authorConvo,
|
||||||
|
conversation: chatConvo,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(`${logId}: fromNonBubbleChat item returned nothing!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes = {
|
||||||
|
...attributes,
|
||||||
|
...result.message,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sentAt = attributes.sent_at;
|
||||||
|
(result.additionalMessages || []).forEach(additional => {
|
||||||
|
sentAt -= 1;
|
||||||
|
additionalMessages.push({
|
||||||
|
...attributes,
|
||||||
|
sent_at: sentAt,
|
||||||
|
...additional,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
assertDev(
|
assertDev(
|
||||||
isAciString(this.ourConversation.serviceId),
|
isAciString(this.ourConversation.serviceId),
|
||||||
'Our conversation must have ACI'
|
`${logId}: Our conversation must have ACI`
|
||||||
);
|
);
|
||||||
this.saveMessage(attributes);
|
this.saveMessage(attributes);
|
||||||
|
additionalMessages.forEach(additional => this.saveMessage(additional));
|
||||||
|
|
||||||
|
// TODO (DESKTOP-6964): We'll want to increment for more types here - stickers, etc.
|
||||||
|
if (item.standardMessage) {
|
||||||
if (isOutgoing) {
|
if (isOutgoing) {
|
||||||
chatConvo.sentMessageCount = (chatConvo.sentMessageCount ?? 0) + 1;
|
chatConvo.sentMessageCount = (chatConvo.sentMessageCount ?? 0) + 1;
|
||||||
} else {
|
} else {
|
||||||
chatConvo.messageCount = (chatConvo.messageCount ?? 0) + 1;
|
chatConvo.messageCount = (chatConvo.messageCount ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.updateConversation(chatConvo);
|
this.updateConversation(chatConvo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -658,4 +753,676 @@ export class BackupImportStream extends Writable {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fromNonBubbleChatItem(
|
||||||
|
chatItem: Backups.IChatItem,
|
||||||
|
options: {
|
||||||
|
aboutMe: AboutMe;
|
||||||
|
author?: ConversationAttributesType;
|
||||||
|
conversation: ConversationAttributesType;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
): Promise<ChatItemParseResult | undefined> {
|
||||||
|
const { timestamp } = options;
|
||||||
|
const logId = `fromChatItemToNonBubble(${timestamp})`;
|
||||||
|
|
||||||
|
if (chatItem.standardMessage) {
|
||||||
|
throw new Error(`${logId}: Got chat item with standardMessage set!`);
|
||||||
|
}
|
||||||
|
if (chatItem.contactMessage) {
|
||||||
|
// TODO (DESKTOP-6964)
|
||||||
|
} else if (chatItem.remoteDeletedMessage) {
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
isErased: true,
|
||||||
|
},
|
||||||
|
additionalMessages: [],
|
||||||
|
};
|
||||||
|
} else if (chatItem.stickerMessage) {
|
||||||
|
// TODO (DESKTOP-6964)
|
||||||
|
} else if (chatItem.updateMessage) {
|
||||||
|
return this.fromChatItemUpdateMessage(chatItem.updateMessage, options);
|
||||||
|
} else {
|
||||||
|
throw new Error(`${logId}: Message was missing all five message types`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fromChatItemUpdateMessage(
|
||||||
|
updateMessage: Backups.IChatUpdateMessage,
|
||||||
|
options: {
|
||||||
|
aboutMe: AboutMe;
|
||||||
|
author?: ConversationAttributesType;
|
||||||
|
conversation: ConversationAttributesType;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
): Promise<ChatItemParseResult | undefined> {
|
||||||
|
const { aboutMe, author } = options;
|
||||||
|
|
||||||
|
if (updateMessage.groupChange) {
|
||||||
|
return this.fromGroupUpdateMessage(updateMessage.groupChange, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateMessage.expirationTimerChange) {
|
||||||
|
const { expiresInMs } = updateMessage.expirationTimerChange;
|
||||||
|
|
||||||
|
const sourceServiceId = author?.serviceId ?? aboutMe.aci;
|
||||||
|
const expireTimer = isNumber(expiresInMs)
|
||||||
|
? DurationInSeconds.fromMillis(expiresInMs)
|
||||||
|
: DurationInSeconds.fromSeconds(0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: 'timer-notification',
|
||||||
|
sourceServiceId,
|
||||||
|
flags: SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
|
expirationTimerUpdate: {
|
||||||
|
expireTimer,
|
||||||
|
sourceServiceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalMessages: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (DESKTOP-6964): check these fields
|
||||||
|
// updateMessage.simpleUpdate
|
||||||
|
// updateMessage.profileChange
|
||||||
|
// updateMessage.threadMerge
|
||||||
|
// updateMessage.sessionSwitchover
|
||||||
|
// updateMessage.callingMessage
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fromGroupUpdateMessage(
|
||||||
|
groupChange: Backups.IGroupChangeChatUpdate,
|
||||||
|
options: {
|
||||||
|
aboutMe: AboutMe;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
): Promise<ChatItemParseResult | undefined> {
|
||||||
|
const { updates } = groupChange;
|
||||||
|
const { aboutMe, timestamp } = options;
|
||||||
|
const logId = `fromGroupUpdateMessage${timestamp}`;
|
||||||
|
|
||||||
|
const details: Array<GroupV2ChangeDetailType> = [];
|
||||||
|
let from: ServiceIdString | undefined;
|
||||||
|
const additionalMessages: Array<Partial<MessageAttributesType>> = [];
|
||||||
|
let migrationMessage: Partial<MessageAttributesType> | undefined;
|
||||||
|
function getDefaultMigrationMessage() {
|
||||||
|
return {
|
||||||
|
type: 'group-v1-migration' as const,
|
||||||
|
groupMigration: {
|
||||||
|
areWeInvited: false,
|
||||||
|
droppedMemberCount: 0,
|
||||||
|
invitedMemberCount: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let openApprovalServiceId: ServiceIdString | undefined;
|
||||||
|
let openBounceServiceId: ServiceIdString | undefined;
|
||||||
|
|
||||||
|
updates?.forEach(update => {
|
||||||
|
if (update.genericGroupUpdate) {
|
||||||
|
const { updaterAci } = update.genericGroupUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'summary',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupCreationUpdate) {
|
||||||
|
const { updaterAci } = update.groupCreationUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'create',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupNameUpdate) {
|
||||||
|
const { updaterAci, newGroupName } = update.groupNameUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'title',
|
||||||
|
newTitle: dropNull(newGroupName),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupAvatarUpdate) {
|
||||||
|
const { updaterAci, wasRemoved } = update.groupAvatarUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'avatar',
|
||||||
|
removed: Boolean(dropNull(wasRemoved)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupDescriptionUpdate) {
|
||||||
|
const { updaterAci, newDescription } = update.groupDescriptionUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
const description = dropNull(newDescription);
|
||||||
|
details.push({
|
||||||
|
type: 'description',
|
||||||
|
description,
|
||||||
|
removed:
|
||||||
|
description === undefined || description.length === 0
|
||||||
|
? true
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupMembershipAccessLevelChangeUpdate) {
|
||||||
|
const { updaterAci, accessLevel } =
|
||||||
|
update.groupMembershipAccessLevelChangeUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'access-members',
|
||||||
|
newPrivilege:
|
||||||
|
dropNull(accessLevel) ??
|
||||||
|
SignalService.AccessControl.AccessRequired.UNKNOWN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupAttributesAccessLevelChangeUpdate) {
|
||||||
|
const { updaterAci, accessLevel } =
|
||||||
|
update.groupAttributesAccessLevelChangeUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'access-attributes',
|
||||||
|
newPrivilege:
|
||||||
|
dropNull(accessLevel) ??
|
||||||
|
SignalService.AccessControl.AccessRequired.UNKNOWN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupAnnouncementOnlyChangeUpdate) {
|
||||||
|
const { updaterAci, isAnnouncementOnly } =
|
||||||
|
update.groupAnnouncementOnlyChangeUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'announcements-only',
|
||||||
|
announcementsOnly: Boolean(dropNull(isAnnouncementOnly)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupAdminStatusUpdate) {
|
||||||
|
const { updaterAci, memberAci, wasAdminStatusGranted } =
|
||||||
|
update.groupAdminStatusUpdate;
|
||||||
|
if (updaterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
if (!memberAci) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: We can't render this without a target member!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'member-privilege',
|
||||||
|
aci: fromAciObject(Aci.fromUuidBytes(memberAci)),
|
||||||
|
newPrivilege: wasAdminStatusGranted
|
||||||
|
? SignalService.Member.Role.ADMINISTRATOR
|
||||||
|
: SignalService.Member.Role.DEFAULT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupMemberLeftUpdate) {
|
||||||
|
const { aci } = update.groupMemberLeftUpdate;
|
||||||
|
if (!aci || Bytes.isEmpty(aci)) {
|
||||||
|
throw new Error(`${logId}: groupMemberLeftUpdate had missing aci!`);
|
||||||
|
}
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(aci));
|
||||||
|
details.push({
|
||||||
|
type: 'member-remove',
|
||||||
|
aci: fromAciObject(Aci.fromUuidBytes(aci)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupMemberRemovedUpdate) {
|
||||||
|
const { removerAci, removedAci } = update.groupMemberRemovedUpdate;
|
||||||
|
if (removerAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(removerAci));
|
||||||
|
}
|
||||||
|
if (!removedAci || Bytes.isEmpty(removedAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupMemberRemovedUpdate had missing removedAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'member-remove',
|
||||||
|
aci: fromAciObject(Aci.fromUuidBytes(removedAci)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.selfInvitedToGroupUpdate) {
|
||||||
|
const { inviterAci } = update.selfInvitedToGroupUpdate;
|
||||||
|
if (inviterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(inviterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'pending-add-one',
|
||||||
|
serviceId: aboutMe.aci,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.selfInvitedOtherUserToGroupUpdate) {
|
||||||
|
const { inviteeServiceId } = update.selfInvitedOtherUserToGroupUpdate;
|
||||||
|
from = aboutMe.aci;
|
||||||
|
if (!inviteeServiceId || Bytes.isEmpty(inviteeServiceId)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: selfInvitedOtherUserToGroupUpdate had missing inviteeServiceId!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'pending-add-one',
|
||||||
|
serviceId: fromAciObject(Aci.fromUuidBytes(inviteeServiceId)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupUnknownInviteeUpdate) {
|
||||||
|
const { inviterAci, inviteeCount } = update.groupUnknownInviteeUpdate;
|
||||||
|
if (inviterAci) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(inviterAci));
|
||||||
|
}
|
||||||
|
if (!isNumber(inviteeCount)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupUnknownInviteeUpdate had non-number inviteeCount`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'pending-add-many',
|
||||||
|
count: inviteeCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupInvitationAcceptedUpdate) {
|
||||||
|
const { inviterAci, newMemberAci } =
|
||||||
|
update.groupInvitationAcceptedUpdate;
|
||||||
|
if (!newMemberAci || Bytes.isEmpty(newMemberAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupInvitationAcceptedUpdate had missing newMemberAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(newMemberAci));
|
||||||
|
const inviter =
|
||||||
|
inviterAci && Bytes.isNotEmpty(inviterAci)
|
||||||
|
? fromAciObject(Aci.fromUuidBytes(inviterAci))
|
||||||
|
: undefined;
|
||||||
|
details.push({
|
||||||
|
type: 'member-add-from-invite',
|
||||||
|
aci: fromAciObject(Aci.fromUuidBytes(newMemberAci)),
|
||||||
|
inviter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupInvitationDeclinedUpdate) {
|
||||||
|
const { inviterAci, inviteeAci } = update.groupInvitationDeclinedUpdate;
|
||||||
|
if (!inviteeAci || Bytes.isEmpty(inviteeAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupInvitationDeclinedUpdate had missing inviteeAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(inviteeAci));
|
||||||
|
details.push({
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
inviter: Bytes.isNotEmpty(inviterAci)
|
||||||
|
? fromAciObject(Aci.fromUuidBytes(inviterAci))
|
||||||
|
: undefined,
|
||||||
|
serviceId: from,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupMemberJoinedUpdate) {
|
||||||
|
const { newMemberAci } = update.groupMemberJoinedUpdate;
|
||||||
|
if (!newMemberAci || Bytes.isEmpty(newMemberAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupMemberJoinedUpdate had missing newMemberAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(newMemberAci));
|
||||||
|
details.push({
|
||||||
|
type: 'member-add',
|
||||||
|
aci: fromAciObject(Aci.fromUuidBytes(newMemberAci)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupMemberAddedUpdate) {
|
||||||
|
const { hadOpenInvitation, inviterAci, newMemberAci, updaterAci } =
|
||||||
|
update.groupMemberAddedUpdate;
|
||||||
|
if (Bytes.isNotEmpty(updaterAci)) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
if (!newMemberAci || Bytes.isEmpty(newMemberAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupMemberAddedUpdate had missing newMemberAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (hadOpenInvitation || Bytes.isNotEmpty(inviterAci)) {
|
||||||
|
const inviter =
|
||||||
|
inviterAci && Bytes.isNotEmpty(inviterAci)
|
||||||
|
? fromAciObject(Aci.fromUuidBytes(inviterAci))
|
||||||
|
: undefined;
|
||||||
|
details.push({
|
||||||
|
type: 'member-add-from-invite',
|
||||||
|
aci: fromAciObject(Aci.fromUuidBytes(newMemberAci)),
|
||||||
|
inviter,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
details.push({
|
||||||
|
type: 'member-add',
|
||||||
|
aci: fromAciObject(Aci.fromUuidBytes(newMemberAci)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (update.groupSelfInvitationRevokedUpdate) {
|
||||||
|
const { revokerAci } = update.groupSelfInvitationRevokedUpdate;
|
||||||
|
if (Bytes.isNotEmpty(revokerAci)) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(revokerAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
serviceId: aboutMe.aci,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupInvitationRevokedUpdate) {
|
||||||
|
const { updaterAci, invitees } = update.groupInvitationRevokedUpdate;
|
||||||
|
if (Bytes.isNotEmpty(updaterAci)) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
if (!invitees || invitees.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupInvitationRevokedUpdate had missing invitees list!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invitees.length === 1) {
|
||||||
|
const { inviteeAci, inviteePni } = invitees[0];
|
||||||
|
let serviceId: ServiceIdString | undefined = Bytes.isNotEmpty(
|
||||||
|
inviteeAci
|
||||||
|
)
|
||||||
|
? fromAciObject(Aci.fromUuidBytes(inviteeAci))
|
||||||
|
: undefined;
|
||||||
|
if (!serviceId) {
|
||||||
|
serviceId = Bytes.isNotEmpty(inviteePni)
|
||||||
|
? fromPniObject(Pni.fromUuidBytes(inviteePni))
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
if (serviceId) {
|
||||||
|
details.push({
|
||||||
|
type: 'pending-remove-one',
|
||||||
|
serviceId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
details.push({
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
details.push({
|
||||||
|
type: 'pending-remove-many',
|
||||||
|
count: invitees.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (update.groupJoinRequestUpdate) {
|
||||||
|
const { requestorAci } = update.groupJoinRequestUpdate;
|
||||||
|
if (!requestorAci || Bytes.isEmpty(requestorAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupInvitationRevokedUpdate was missing requestorAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(requestorAci));
|
||||||
|
openApprovalServiceId = from;
|
||||||
|
details.push({
|
||||||
|
type: 'admin-approval-add-one',
|
||||||
|
aci: from,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupJoinRequestApprovalUpdate) {
|
||||||
|
const { updaterAci, requestorAci, wasApproved } =
|
||||||
|
update.groupJoinRequestApprovalUpdate;
|
||||||
|
if (!requestorAci || Bytes.isEmpty(requestorAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupJoinRequestApprovalUpdate was missing requestorAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Bytes.isNotEmpty(updaterAci)) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
|
||||||
|
const aci = fromAciObject(Aci.fromUuidBytes(requestorAci));
|
||||||
|
if (wasApproved) {
|
||||||
|
details.push({
|
||||||
|
type: 'member-add-from-admin-approval',
|
||||||
|
aci,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
details.push({
|
||||||
|
type: 'admin-approval-remove-one',
|
||||||
|
aci,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (update.groupJoinRequestCanceledUpdate) {
|
||||||
|
const { requestorAci } = update.groupJoinRequestCanceledUpdate;
|
||||||
|
if (!requestorAci || Bytes.isEmpty(requestorAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupJoinRequestCanceledUpdate was missing requestorAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(requestorAci));
|
||||||
|
details.push({
|
||||||
|
type: 'admin-approval-remove-one',
|
||||||
|
aci: from,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupInviteLinkResetUpdate) {
|
||||||
|
const { updaterAci } = update.groupInviteLinkResetUpdate;
|
||||||
|
if (Bytes.isNotEmpty(updaterAci)) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'group-link-reset',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupInviteLinkEnabledUpdate) {
|
||||||
|
const { updaterAci, linkRequiresAdminApproval } =
|
||||||
|
update.groupInviteLinkEnabledUpdate;
|
||||||
|
if (Bytes.isNotEmpty(updaterAci)) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'group-link-add',
|
||||||
|
privilege: linkRequiresAdminApproval
|
||||||
|
? SignalService.AccessControl.AccessRequired.ADMINISTRATOR
|
||||||
|
: SignalService.AccessControl.AccessRequired.ANY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupInviteLinkAdminApprovalUpdate) {
|
||||||
|
const { updaterAci, linkRequiresAdminApproval } =
|
||||||
|
update.groupInviteLinkAdminApprovalUpdate;
|
||||||
|
if (Bytes.isNotEmpty(updaterAci)) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'access-invite-link',
|
||||||
|
newPrivilege: linkRequiresAdminApproval
|
||||||
|
? SignalService.AccessControl.AccessRequired.ADMINISTRATOR
|
||||||
|
: SignalService.AccessControl.AccessRequired.ANY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupInviteLinkDisabledUpdate) {
|
||||||
|
const { updaterAci } = update.groupInviteLinkDisabledUpdate;
|
||||||
|
if (Bytes.isNotEmpty(updaterAci)) {
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
}
|
||||||
|
details.push({
|
||||||
|
type: 'group-link-remove',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupMemberJoinedByLinkUpdate) {
|
||||||
|
const { newMemberAci } = update.groupMemberJoinedByLinkUpdate;
|
||||||
|
if (!newMemberAci || Bytes.isEmpty(newMemberAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupMemberJoinedByLinkUpdate was missing newMemberAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
from = fromAciObject(Aci.fromUuidBytes(newMemberAci));
|
||||||
|
details.push({
|
||||||
|
type: 'member-add-from-link',
|
||||||
|
aci: from,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupV2MigrationUpdate) {
|
||||||
|
migrationMessage = migrationMessage || getDefaultMigrationMessage();
|
||||||
|
}
|
||||||
|
if (update.groupV2MigrationSelfInvitedUpdate) {
|
||||||
|
migrationMessage = migrationMessage || getDefaultMigrationMessage();
|
||||||
|
const { groupMigration } = migrationMessage;
|
||||||
|
if (!groupMigration) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: migrationMessage had no groupMigration processing groupV2MigrationSelfInvitedUpdate!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
groupMigration.areWeInvited = true;
|
||||||
|
}
|
||||||
|
if (update.groupV2MigrationInvitedMembersUpdate) {
|
||||||
|
migrationMessage = migrationMessage || getDefaultMigrationMessage();
|
||||||
|
const { groupMigration } = migrationMessage;
|
||||||
|
if (!groupMigration) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: migrationMessage had no groupMigration processing groupV2MigrationInvitedMembersUpdate!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { invitedMembersCount } =
|
||||||
|
update.groupV2MigrationInvitedMembersUpdate;
|
||||||
|
if (!isNumber(invitedMembersCount)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupV2MigrationInvitedMembersUpdate had a non-number invitedMembersCount!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
groupMigration.invitedMemberCount = invitedMembersCount;
|
||||||
|
}
|
||||||
|
if (update.groupV2MigrationDroppedMembersUpdate) {
|
||||||
|
migrationMessage = migrationMessage || getDefaultMigrationMessage();
|
||||||
|
const { groupMigration } = migrationMessage;
|
||||||
|
if (!groupMigration) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: migrationMessage had no groupMigration processing groupV2MigrationDroppedMembersUpdate!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { droppedMembersCount } =
|
||||||
|
update.groupV2MigrationDroppedMembersUpdate;
|
||||||
|
if (!isNumber(droppedMembersCount)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupV2MigrationDroppedMembersUpdate had a non-number droppedMembersCount!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
groupMigration.droppedMemberCount = droppedMembersCount;
|
||||||
|
}
|
||||||
|
if (update.groupSequenceOfRequestsAndCancelsUpdate) {
|
||||||
|
const { count, requestorAci } =
|
||||||
|
update.groupSequenceOfRequestsAndCancelsUpdate;
|
||||||
|
if (!requestorAci || Bytes.isEmpty(requestorAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupSequenceOfRequestsAndCancelsUpdate was missing requestorAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isNumber(count)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupSequenceOfRequestsAndCancelsUpdate had a non-number count!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const aci = fromAciObject(Aci.fromUuidBytes(requestorAci));
|
||||||
|
openBounceServiceId = aci;
|
||||||
|
from = aci;
|
||||||
|
details.push({
|
||||||
|
type: 'admin-approval-bounce',
|
||||||
|
aci,
|
||||||
|
times: count,
|
||||||
|
// This will be set later if we find an open approval request for this aci
|
||||||
|
isApprovalPending: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (update.groupExpirationTimerUpdate) {
|
||||||
|
const { updaterAci, expiresInMs } = update.groupExpirationTimerUpdate;
|
||||||
|
if (!updaterAci || Bytes.isEmpty(updaterAci)) {
|
||||||
|
throw new Error(
|
||||||
|
`${logId}: groupExpirationTimerUpdate was missing updaterAci!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const sourceServiceId = fromAciObject(Aci.fromUuidBytes(updaterAci));
|
||||||
|
const expireTimer = isNumber(expiresInMs)
|
||||||
|
? DurationInSeconds.fromMillis(expiresInMs)
|
||||||
|
: undefined;
|
||||||
|
additionalMessages.push({
|
||||||
|
type: 'timer-notification',
|
||||||
|
sourceServiceId,
|
||||||
|
flags: SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||||
|
expirationTimerUpdate: {
|
||||||
|
expireTimer,
|
||||||
|
sourceServiceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let finalDetails = details;
|
||||||
|
if (
|
||||||
|
openApprovalServiceId &&
|
||||||
|
openBounceServiceId &&
|
||||||
|
openApprovalServiceId === openBounceServiceId
|
||||||
|
) {
|
||||||
|
finalDetails = details
|
||||||
|
.map(item => {
|
||||||
|
const approvalMatch =
|
||||||
|
item.type === 'admin-approval-add-one' &&
|
||||||
|
item.aci === openApprovalServiceId;
|
||||||
|
if (approvalMatch) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounceMatch =
|
||||||
|
item.type === 'admin-approval-bounce' &&
|
||||||
|
item.aci === openApprovalServiceId;
|
||||||
|
if (bounceMatch) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
isApprovalPending: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (migrationMessage) {
|
||||||
|
additionalMessages.push(migrationMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalDetails.length === 0 && additionalMessages.length > 0) {
|
||||||
|
return {
|
||||||
|
message: additionalMessages[0],
|
||||||
|
additionalMessages: additionalMessages.slice(1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalDetails.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: 'group-v2-change',
|
||||||
|
groupV2Change: {
|
||||||
|
from,
|
||||||
|
details: finalDetails,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalMessages,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
9
ts/services/backups/types.d.ts
vendored
Normal file
9
ts/services/backups/types.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { AciString, PniString } from '../../types/ServiceId';
|
||||||
|
|
||||||
|
export type AboutMe = {
|
||||||
|
aci: AciString;
|
||||||
|
pni?: PniString;
|
||||||
|
};
|
|
@ -862,23 +862,23 @@ export const getCachedSelectorForConversation = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type GetConversationByIdType = (id?: string) => ConversationType;
|
export type GetConversationByAnyIdSelectorType = (
|
||||||
export const getConversationSelector = createSelector(
|
id?: string
|
||||||
getCachedSelectorForConversation,
|
) => ConversationType | undefined;
|
||||||
|
export const getConversationByAnyIdSelector = createSelector(
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
getConversationsByServiceId,
|
getConversationsByServiceId,
|
||||||
getConversationsByE164,
|
getConversationsByE164,
|
||||||
getConversationsByGroupId,
|
getConversationsByGroupId,
|
||||||
(
|
(
|
||||||
selector: CachedConversationSelectorType,
|
|
||||||
byId: ConversationLookupType,
|
byId: ConversationLookupType,
|
||||||
byServiceId: ConversationLookupType,
|
byServiceId: ConversationLookupType,
|
||||||
byE164: ConversationLookupType,
|
byE164: ConversationLookupType,
|
||||||
byGroupId: ConversationLookupType
|
byGroupId: ConversationLookupType
|
||||||
): GetConversationByIdType => {
|
): GetConversationByAnyIdSelectorType => {
|
||||||
return (id?: string) => {
|
return (id?: string) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return selector(undefined);
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onServiceId = getOwn(
|
const onServiceId = getOwn(
|
||||||
|
@ -886,19 +886,42 @@ export const getConversationSelector = createSelector(
|
||||||
normalizeServiceId(id, 'getConversationSelector')
|
normalizeServiceId(id, 'getConversationSelector')
|
||||||
);
|
);
|
||||||
if (onServiceId) {
|
if (onServiceId) {
|
||||||
return selector(onServiceId);
|
return onServiceId;
|
||||||
}
|
}
|
||||||
const onE164 = getOwn(byE164, id);
|
const onE164 = getOwn(byE164, id);
|
||||||
if (onE164) {
|
if (onE164) {
|
||||||
return selector(onE164);
|
return onE164;
|
||||||
}
|
}
|
||||||
const onGroupId = getOwn(byGroupId, id);
|
const onGroupId = getOwn(byGroupId, id);
|
||||||
if (onGroupId) {
|
if (onGroupId) {
|
||||||
return selector(onGroupId);
|
return onGroupId;
|
||||||
}
|
}
|
||||||
const onId = getOwn(byId, id);
|
const onId = getOwn(byId, id);
|
||||||
if (onId) {
|
if (onId) {
|
||||||
return selector(onId);
|
return onId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type GetConversationByIdType = (id?: string) => ConversationType;
|
||||||
|
export const getConversationSelector = createSelector(
|
||||||
|
getCachedSelectorForConversation,
|
||||||
|
getConversationByAnyIdSelector,
|
||||||
|
(
|
||||||
|
selector: CachedConversationSelectorType,
|
||||||
|
getById: GetConversationByAnyIdSelectorType
|
||||||
|
): GetConversationByIdType => {
|
||||||
|
return (id?: string) => {
|
||||||
|
if (!id) {
|
||||||
|
return selector(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byId = getById(id);
|
||||||
|
if (byId) {
|
||||||
|
return selector(byId);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn(`getConversationSelector: No conversation found for id ${id}`);
|
log.warn(`getConversationSelector: No conversation found for id ${id}`);
|
||||||
|
@ -908,6 +931,24 @@ export const getConversationSelector = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type CheckServiceIdEquivalenceType = (
|
||||||
|
left: ServiceIdString | undefined,
|
||||||
|
right: ServiceIdString | undefined
|
||||||
|
) => boolean;
|
||||||
|
export const getCheckServiceIdEquivalence = createSelector(
|
||||||
|
getConversationByAnyIdSelector,
|
||||||
|
(
|
||||||
|
getById: GetConversationByAnyIdSelectorType
|
||||||
|
): CheckServiceIdEquivalenceType => {
|
||||||
|
return (
|
||||||
|
left: ServiceIdString | undefined,
|
||||||
|
right: ServiceIdString | undefined
|
||||||
|
): boolean => {
|
||||||
|
return Boolean(left && right && getById(left) === getById(right));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const getConversationByIdSelector = createSelector(
|
export const getConversationByIdSelector = createSelector(
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
conversationLookup =>
|
conversationLookup =>
|
||||||
|
|
|
@ -1113,6 +1113,8 @@ function getPropsForGroupV1Migration(
|
||||||
conversationId: message.conversationId,
|
conversationId: message.conversationId,
|
||||||
droppedMembers,
|
droppedMembers,
|
||||||
invitedMembers,
|
invitedMembers,
|
||||||
|
droppedMemberCount: droppedMembers.length,
|
||||||
|
invitedMemberCount: invitedMembers.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1120,19 +1122,30 @@ function getPropsForGroupV1Migration(
|
||||||
areWeInvited,
|
areWeInvited,
|
||||||
droppedMemberIds,
|
droppedMemberIds,
|
||||||
invitedMembers: rawInvitedMembers,
|
invitedMembers: rawInvitedMembers,
|
||||||
|
droppedMemberCount: rawDroppedMemberCount,
|
||||||
|
invitedMemberCount: rawInvitedMemberCount,
|
||||||
} = migration;
|
} = migration;
|
||||||
const invitedMembers = rawInvitedMembers.map(item =>
|
const droppedMembers = droppedMemberIds
|
||||||
conversationSelector(item.uuid)
|
? droppedMemberIds.map(conversationId =>
|
||||||
);
|
|
||||||
const droppedMembers = droppedMemberIds.map(conversationId =>
|
|
||||||
conversationSelector(conversationId)
|
conversationSelector(conversationId)
|
||||||
);
|
)
|
||||||
|
: undefined;
|
||||||
|
const invitedMembers = rawInvitedMembers
|
||||||
|
? rawInvitedMembers.map(item => conversationSelector(item.uuid))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const droppedMemberCount =
|
||||||
|
rawDroppedMemberCount ?? droppedMemberIds?.length ?? 0;
|
||||||
|
const invitedMemberCount =
|
||||||
|
rawInvitedMemberCount ?? invitedMembers?.length ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
areWeInvited,
|
areWeInvited,
|
||||||
conversationId: message.conversationId,
|
conversationId: message.conversationId,
|
||||||
droppedMembers,
|
droppedMembers,
|
||||||
invitedMembers,
|
invitedMembers,
|
||||||
|
droppedMemberCount,
|
||||||
|
invitedMemberCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,9 @@ export const SmartGroupV1MigrationDialog = memo(
|
||||||
hasMigrated={hasMigrated}
|
hasMigrated={hasMigrated}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
droppedMembers={droppedMembers}
|
droppedMembers={droppedMembers}
|
||||||
|
droppedMemberCount={droppedMembers.length}
|
||||||
invitedMembers={invitedMembers}
|
invitedMembers={invitedMembers}
|
||||||
|
invitedMemberCount={invitedMembers.length}
|
||||||
onMigrate={handleMigrate}
|
onMigrate={handleMigrate}
|
||||||
onClose={closeGV2MigrationDialog}
|
onClose={closeGV2MigrationDialog}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,7 +21,10 @@ import {
|
||||||
getTheme,
|
getTheme,
|
||||||
getPlatform,
|
getPlatform,
|
||||||
} from '../selectors/user';
|
} from '../selectors/user';
|
||||||
import { getTargetedMessage } from '../selectors/conversations';
|
import {
|
||||||
|
getTargetedMessage,
|
||||||
|
getCheckServiceIdEquivalence,
|
||||||
|
} from '../selectors/conversations';
|
||||||
import { useTimelineItem } from '../selectors/timeline';
|
import { useTimelineItem } from '../selectors/timeline';
|
||||||
import {
|
import {
|
||||||
areMessagesInSameGroup,
|
areMessagesInSameGroup,
|
||||||
|
@ -86,6 +89,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
||||||
const isTargeted = Boolean(
|
const isTargeted = Boolean(
|
||||||
targetedMessage && messageId === targetedMessage.id
|
targetedMessage && messageId === targetedMessage.id
|
||||||
);
|
);
|
||||||
|
const checkServiceIdEquivalence = useSelector(getCheckServiceIdEquivalence);
|
||||||
|
|
||||||
const isNextItemCallingNotification = nextItem?.type === 'callHistory';
|
const isNextItemCallingNotification = nextItem?.type === 'callHistory';
|
||||||
|
|
||||||
|
@ -176,6 +180,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
||||||
<TimelineItem
|
<TimelineItem
|
||||||
item={item}
|
item={item}
|
||||||
id={messageId}
|
id={messageId}
|
||||||
|
checkServiceIdEquivalence={checkServiceIdEquivalence}
|
||||||
containerElementRef={containerElementRef}
|
containerElementRef={containerElementRef}
|
||||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
|
|
2242
ts/test-electron/backup/backup_groupv2_notifications_test.ts
Normal file
2242
ts/test-electron/backup/backup_groupv2_notifications_test.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { chmodSync, rmdirSync, writeFileSync, mkdtempSync } from 'fs';
|
import { chmodSync, rmSync, writeFileSync, mkdtempSync } from 'fs';
|
||||||
import { pathExists, readJsonSync } from 'fs-extra';
|
import { pathExists, readJsonSync } from 'fs-extra';
|
||||||
|
|
||||||
import { v4 as generateGuid } from 'uuid';
|
import { v4 as generateGuid } from 'uuid';
|
||||||
|
@ -25,7 +25,7 @@ describe('base_config', () => {
|
||||||
try {
|
try {
|
||||||
chmodSync(targetDir, 0o755);
|
chmodSync(targetDir, 0o755);
|
||||||
chmodSync(targetPath, 0o755);
|
chmodSync(targetPath, 0o755);
|
||||||
rmdirSync(targetDir, { recursive: true });
|
rmSync(targetDir, { recursive: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
assert.strictEqual(err.code, 'ENOENT');
|
assert.strictEqual(err.code, 'ENOENT');
|
||||||
}
|
}
|
||||||
|
@ -167,7 +167,7 @@ describe('base_config', () => {
|
||||||
throwOnFilesystemErrors: true,
|
throwOnFilesystemErrors: true,
|
||||||
});
|
});
|
||||||
config.set('foo', 123);
|
config.set('foo', 123);
|
||||||
rmdirSync(targetDir, { recursive: true });
|
rmSync(targetDir, { recursive: true });
|
||||||
|
|
||||||
assert.throws(() => config.set('foo', 456));
|
assert.throws(() => config.set('foo', 456));
|
||||||
assert.strictEqual(config.get('foo'), 123);
|
assert.strictEqual(config.get('foo'), 123);
|
||||||
|
@ -185,7 +185,7 @@ describe('base_config', () => {
|
||||||
throwOnFilesystemErrors: false,
|
throwOnFilesystemErrors: false,
|
||||||
});
|
});
|
||||||
config.set('foo', 123);
|
config.set('foo', 123);
|
||||||
rmdirSync(targetDir, { recursive: true });
|
rmSync(targetDir, { recursive: true });
|
||||||
|
|
||||||
config.set('bar', 456);
|
config.set('bar', 456);
|
||||||
|
|
||||||
|
|
|
@ -2221,7 +2221,7 @@ export default class MessageReceiver
|
||||||
const { sourceServiceId: sourceAci } = envelope;
|
const { sourceServiceId: sourceAci } = envelope;
|
||||||
strictAssert(
|
strictAssert(
|
||||||
isAciString(sourceAci),
|
isAciString(sourceAci),
|
||||||
'MessageReceiver.handleEditMesage: received message from PNI'
|
'MessageReceiver.handleStoryMessage: received message from PNI'
|
||||||
);
|
);
|
||||||
|
|
||||||
const attachments: Array<ProcessedAttachment> = [];
|
const attachments: Array<ProcessedAttachment> = [];
|
||||||
|
|
|
@ -143,6 +143,12 @@ export function getNotificationDataForMessage(
|
||||||
);
|
);
|
||||||
|
|
||||||
const changes = GroupChange.renderChange<string>(change, {
|
const changes = GroupChange.renderChange<string>(change, {
|
||||||
|
checkServiceIdEquivalence: (left, right) => {
|
||||||
|
return (
|
||||||
|
window.ConversationController.get(left) ===
|
||||||
|
window.ConversationController.get(right)
|
||||||
|
);
|
||||||
|
},
|
||||||
i18n: window.i18n,
|
i18n: window.i18n,
|
||||||
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
ourAci: window.textsecure.storage.user.getCheckedAci(),
|
||||||
ourPni: window.textsecure.storage.user.getCheckedPni(),
|
ourPni: window.textsecure.storage.user.getCheckedPni(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue