Backups: Handle groupV2 notifications

This commit is contained in:
Scott Nonnenberg 2024-04-30 06:24:21 -07:00 committed by GitHub
parent 4c4ab306eb
commit 5df8924197
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 4563 additions and 301 deletions

View file

@ -4159,15 +4159,15 @@
"description": "Shown in timeline or conversation preview when v2 group changes"
},
"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"
},
"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"
},
"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"
},
"icu:GroupV2--pending-remove--decline--other": {
@ -4207,15 +4207,15 @@
"description": "Shown in timeline or conversation preview when v2 group changes"
},
"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"
},
"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"
},
"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"
},
"icu:GroupV2--pending-remove--revoke-invite-from--one--other": {
@ -4243,27 +4243,27 @@
"description": "Shown in timeline or conversation preview when v2 group changes"
},
"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"
},
"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"
},
"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"
},
"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"
},
"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"
},
"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"
},
"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:",
"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": {
"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"
@ -4462,6 +4466,10 @@
"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"
},
"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": {
"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"
@ -4470,6 +4478,10 @@
"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"
},
"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": {
"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"
@ -4479,7 +4491,7 @@
"description": "Shown in timeline when a group is upgraded and one person was invited, instead of added"
},
"icu:GroupV1--Migration--invited--many": {
"messageformat": "{count, number} members couldnt be added to the New Group and have been invited to join.",
"messageformat": "{count, plural, one {# member couldnt be added to the New Group and has been invited to join} other {# members couldnt 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"
},
"icu:GroupV1--Migration--removed--one": {

View file

@ -6,7 +6,7 @@
border-radius: 8px;
margin-block: 0;
margin-inline: auto;
max-height: 100%;
max-height: 80vh;
max-width: 360px;
padding: 16px;
position: relative;

View file

@ -262,7 +262,7 @@ export class ConversationController {
getOrCreate(
identifier: string | null,
type: ConversationAttributesTypeType,
additionalInitialProps = {}
additionalInitialProps: Partial<ConversationAttributesType> = {}
): ConversationModel {
if (typeof identifier !== 'string') {
throw new TypeError("'id' must be a string");
@ -358,7 +358,7 @@ export class ConversationController {
async getOrCreateAndWait(
id: string | null,
type: ConversationAttributesTypeType,
additionalInitialProps = {}
additionalInitialProps: Partial<ConversationAttributesType> = {}
): Promise<ConversationModel> {
await this.load();
const conversation = this.getOrCreate(id, type, additionalInitialProps);

View file

@ -36,11 +36,13 @@ const contact3: ConversationType = getDefaultConversation({
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
areWeInvited: Boolean(overrideProps.areWeInvited),
droppedMembers: overrideProps.droppedMembers || [contact3, contact1],
droppedMembers: overrideProps.droppedMembers,
droppedMemberCount: overrideProps.droppedMemberCount || 0,
getPreferredBadge: () => undefined,
hasMigrated: Boolean(overrideProps.hasMigrated),
i18n,
invitedMembers: overrideProps.invitedMembers || [contact2],
invitedMembers: overrideProps.invitedMembers,
invitedMemberCount: overrideProps.invitedMemberCount || 0,
onMigrate: action('onMigrate'),
onClose: action('onClose'),
theme: ThemeType.light,
@ -75,23 +77,41 @@ export function MigratedYouAreInvited(): JSX.Element {
);
}
export function NotYetMigratedMultipleDroppedAndInvitedMembers(): JSX.Element {
export function MigratedMultipleDroppedAndInvitedMember(): JSX.Element {
return (
<GroupV1MigrationDialog
{...createProps({
droppedMembers: [contact3, contact1, contact2],
invitedMembers: [contact2, contact3, contact1],
hasMigrated: true,
droppedMembers: [contact1],
droppedMemberCount: 1,
invitedMembers: [contact2],
invitedMemberCount: 1,
})}
/>
);
}
export function NotYetMigratedNoMembers(): JSX.Element {
export function MigratedMultipleDroppedAndInvitedMembers(): JSX.Element {
return (
<GroupV1MigrationDialog
{...createProps({
droppedMembers: [],
invitedMembers: [],
hasMigrated: true,
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 (
<GroupV1MigrationDialog
{...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,
})}
/>
);

View file

@ -11,9 +11,11 @@ import { missingCaseError } from '../util/missingCaseError';
export type DataPropsType = {
readonly areWeInvited: boolean;
readonly droppedMembers: Array<ConversationType>;
readonly droppedMembers?: Array<ConversationType>;
readonly droppedMemberCount: number;
readonly hasMigrated: boolean;
readonly invitedMembers: Array<ConversationType>;
readonly invitedMembers?: Array<ConversationType>;
readonly invitedMemberCount: number;
readonly getPreferredBadge: PreferredBadgeSelectorType;
readonly i18n: LocalizerType;
readonly theme: ThemeType;
@ -30,10 +32,12 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
React.memo(function GroupV1MigrationDialogInner({
areWeInvited,
droppedMembers,
droppedMemberCount,
getPreferredBadge,
hasMigrated,
i18n,
invitedMembers,
invitedMemberCount,
theme,
onClose,
onMigrate,
@ -88,6 +92,7 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
getPreferredBadge,
i18n,
members: invitedMembers,
count: invitedMemberCount,
hasMigrated,
kind: 'invited',
theme,
@ -96,6 +101,7 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
getPreferredBadge,
i18n,
members: droppedMembers,
count: droppedMemberCount,
hasMigrated,
kind: 'dropped',
theme,
@ -110,21 +116,50 @@ function renderMembers({
getPreferredBadge,
i18n,
members,
count,
hasMigrated,
kind,
theme,
}: Readonly<{
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
members: Array<ConversationType>;
members?: Array<ConversationType>;
count: number;
hasMigrated: boolean;
kind: 'invited' | 'dropped';
theme: ThemeType;
}>): React.ReactNode {
if (!members.length) {
if (count === 0) {
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;
switch (kind) {
case 'invited':
@ -137,13 +172,13 @@ function renderMembers({
if (hasMigrated) {
text =
members.length === 1
? i18n('icu:GroupV1--Migration--info--removed--before--one')
: i18n('icu:GroupV1--Migration--info--removed--before--many');
? i18n('icu:GroupV1--Migration--info--removed--after--one')
: i18n('icu:GroupV1--Migration--info--removed--after--many');
} else {
text =
members.length === 1
? i18n('icu:GroupV1--Migration--info--removed--after--one')
: i18n('icu:GroupV1--Migration--info--removed--after--many');
? i18n('icu:GroupV1--Migration--info--removed--before--one')
: i18n('icu:GroupV1--Migration--info--removed--before--many');
}
break;
default:

View file

@ -33,9 +33,11 @@ export default {
areWeInvited: false,
conversationId: '123',
droppedMembers: [contact1],
droppedMemberCount: 1,
getPreferredBadge: () => undefined,
i18n,
invitedMembers: [contact2],
invitedMemberCount: 1,
theme: ThemeType.light,
},
} satisfies Meta<PropsType>;
@ -55,7 +57,9 @@ export function MultipleDroppedAndInvitedMembers(args: PropsType): JSX.Element {
<GroupV1Migration
{...args}
invitedMembers={[contact1, contact2]}
invitedMemberCount={3}
droppedMembers={[contact1, contact2]}
droppedMemberCount={3}
/>
);
}
@ -65,7 +69,9 @@ export function JustInvitedMembers(args: PropsType): JSX.Element {
<GroupV1Migration
{...args}
invitedMembers={[contact1, contact1, contact2, contact2]}
invitedMemberCount={4}
droppedMembers={[]}
droppedMemberCount={0}
/>
);
}
@ -75,11 +81,45 @@ export function JustDroppedMembers(args: PropsType): JSX.Element {
<GroupV1Migration
{...args}
invitedMembers={[]}
invitedMemberCount={0}
droppedMembers={[contact1, contact1, contact2, contact2]}
droppedMemberCount={4}
/>
);
}
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}
/>
);
}

View file

@ -16,8 +16,10 @@ import * as log from '../../logging/log';
export type PropsDataType = {
areWeInvited: boolean;
conversationId: string;
droppedMembers: Array<ConversationType>;
invitedMembers: Array<ConversationType>;
droppedMembers?: Array<ConversationType>;
invitedMembers?: Array<ConversationType>;
droppedMemberCount: number;
invitedMemberCount: number;
};
export type PropsHousekeepingType = {
@ -32,9 +34,11 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
const {
areWeInvited,
droppedMembers,
droppedMemberCount,
getPreferredBadge,
i18n,
invitedMembers,
invitedMemberCount,
theme,
} = props;
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>
{' '}
{areWeInvited ? (
i18n('icu:GroupV1--Migration--invited--you')
) : (
<>
{renderUsers(invitedMembers, i18n, 'invited')}
{renderUsers(droppedMembers, i18n, 'removed')}
{renderUsers({
members: invitedMembers,
count: invitedMemberCount,
i18n,
kind: 'invited',
})}
{renderUsers({
members: droppedMembers,
count: droppedMemberCount,
i18n,
kind: 'removed',
})}
</>
)}
</p>
@ -80,10 +95,12 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
<GroupV1MigrationDialog
areWeInvited={areWeInvited}
droppedMembers={droppedMembers}
droppedMemberCount={droppedMemberCount}
getPreferredBadge={getPreferredBadge}
hasMigrated
i18n={i18n}
invitedMembers={invitedMembers}
invitedMemberCount={invitedMemberCount}
onMigrate={() => log.warn('GroupV1Migration: Modal called migrate()')}
onClose={dismissDialog}
theme={theme}
@ -93,16 +110,22 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
);
}
function renderUsers(
members: Array<ConversationType>,
i18n: LocalizerType,
kind: 'invited' | 'removed'
): React.ReactElement | null {
if (!members || members.length === 0) {
function renderUsers({
members,
count,
i18n,
kind,
}: {
members?: Array<ConversationType>;
count: number;
i18n: LocalizerType;
kind: 'invited' | 'removed';
}): React.ReactElement | null {
if (count === 0) {
return null;
}
if (members.length === 1) {
if (members && count === 1) {
const contact = <ContactName title={members[0].title} />;
return (
<p>
@ -124,18 +147,16 @@ function renderUsers(
);
}
const count = members.length;
return (
<p>
{kind === 'invited' && members.length > 1 && (
{kind === 'invited' && (
<Intl
i18n={i18n}
id="icu:GroupV1--Migration--invited--many"
components={{ count }}
/>
)}
{kind === 'removed' && members.length > 1 && (
{kind === 'removed' && (
<Intl
i18n={i18n}
id="icu:GroupV1--Migration--removed--many"

View file

@ -14,16 +14,31 @@ import type { SmartContactRendererType } from '../../groupChange';
import type { PropsType } 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 OUR_ACI = generateAci();
const OUR_PNI = generatePni();
const CONTACT_A = generateAci();
const CONTACT_A_PNI = generatePni();
const CONTACT_B = generateAci();
const CONTACT_C = generateAci();
const ADMIN_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 RoleEnum = Proto.Member.Role;
@ -31,10 +46,17 @@ const renderContact: SmartContactRendererType<JSX.Element> = (
conversationId: string
) => (
<React.Fragment key={conversationId}>
{`Conversation(${conversationId})`}
{contactMap[conversationId] || 'UNKNOWN'}
</React.Fragment>
);
function checkServiceIdEquivalence(
left: ServiceIdString | undefined,
right: ServiceIdString | undefined
): boolean {
return Boolean(left && right && contactMap[left] === contactMap[right]);
}
const renderChange = (
change: GroupV2ChangeType,
{
@ -57,6 +79,7 @@ const renderChange = (
blockGroupLinkRequests={action('blockGroupLinkRequests')}
conversationId="some-conversation-id"
change={change}
checkServiceIdEquivalence={checkServiceIdEquivalence}
groupBannedMemberships={groupBannedMemberships}
groupMemberships={groupMemberships}
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({
from: CONTACT_A,
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({
details: [
{
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({
from: ADMIN_A,
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({
details: [
{
type: 'pending-remove-many',
count: 5,
count: 1,
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({
from: ADMIN_A,
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({
details: [
{
@ -1148,6 +1264,15 @@ export function PendingRemoveMany(): JSX.Element {
},
],
})}
{renderChange({
details: [
{
type: 'pending-remove-many',
count: 1,
inviter: CONTACT_A,
},
],
})}
{renderChange({
from: OUR_ACI,
details: [
@ -1157,6 +1282,15 @@ export function PendingRemoveMany(): JSX.Element {
},
],
})}
{renderChange({
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
count: 1,
},
],
})}
{renderChange({
from: CONTACT_A,
@ -1167,6 +1301,15 @@ export function PendingRemoveMany(): JSX.Element {
},
],
})}
{renderChange({
from: CONTACT_A,
details: [
{
type: 'pending-remove-many',
count: 1,
},
],
})}
{renderChange({
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 (
<>
{renderChange({
from: OUR_ACI,
details: [
{
type: 'admin-approval-add-one',
@ -1191,6 +1343,7 @@ export function AdminApprovalAdd(): JSX.Element {
],
})}
{renderChange({
from: CONTACT_A,
details: [
{
type: 'admin-approval-add-one',
@ -1332,6 +1485,21 @@ export function AdminApprovalBounce(): JSX.Element {
{ groupBannedMemberships: [CONTACT_A] }
)}
Open request
{renderChange(
{
from: CONTACT_A,
details: [
{
type: 'admin-approval-bounce',
aci: CONTACT_A,
times: 4,
isApprovalPending: true,
},
],
},
{ groupBannedMemberships: [] }
)}
</>
);
}

View file

@ -29,16 +29,16 @@ import { ConfirmationDialog } from '../ConfirmationDialog';
export type PropsDataType = {
areWeAdmin: boolean;
change: GroupV2ChangeType;
conversationId: string;
groupBannedMemberships?: ReadonlyArray<ServiceIdString>;
groupMemberships?: ReadonlyArray<{
aci: AciString;
isAdmin: boolean;
}>;
groupBannedMemberships?: ReadonlyArray<ServiceIdString>;
groupName?: string;
ourAci: AciString | undefined;
ourPni: PniString | undefined;
change: GroupV2ChangeType;
};
export type PropsActionsType = {
@ -49,6 +49,10 @@ export type PropsActionsType = {
};
export type PropsHousekeepingType = {
checkServiceIdEquivalence(
left: ServiceIdString | undefined,
right: ServiceIdString | undefined
): boolean;
i18n: LocalizerType;
renderContact: SmartContactRendererType<JSX.Element>;
};
@ -293,6 +297,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
areWeAdmin,
blockGroupLinkRequests,
change,
checkServiceIdEquivalence,
conversationId,
groupBannedMemberships,
groupMemberships,
@ -306,6 +311,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
return (
<>
{renderChange<JSX.Element>(change, {
checkServiceIdEquivalence,
i18n,
ourAci,
ourPni,

View file

@ -361,6 +361,7 @@ const renderItem = ({
isNextItemCallingNotification={false}
theme={ThemeType.light}
platform="darwin"
checkServiceIdEquivalence={() => false}
containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint}
conversationId=""

View file

@ -69,6 +69,7 @@ const getDefaultProps = () => ({
toggleSelectMessage: action('toggleSelectMessage'),
reactToMessage: action('reactToMessage'),
checkForAccount: action('checkForAccount'),
checkServiceIdEquivalence: () => false,
clearTargetedMessage: action('clearTargetedMessage'),
setMessageToEdit: action('setMessageToEdit'),
setQuoteByMessageId: action('setQuoteByMessageId'),

View file

@ -61,6 +61,7 @@ import {
type MessageRequestResponseNotificationData,
} from './MessageRequestResponseNotification';
import type { MessageRequestState } from './MessageRequestActionsConfirmation';
import type { ServiceIdString } from '../../types/ServiceId';
type CallHistoryType = {
type: 'callHistory';
@ -172,6 +173,10 @@ export type TimelineItemType = (
) & { timestamp: number };
type PropsLocalType = {
checkServiceIdEquivalence(
left: ServiceIdString | undefined,
right: ServiceIdString | undefined
): boolean;
containerElementRef: RefObject<HTMLElement>;
conversationId: string;
item?: TimelineItemType;

View file

@ -37,6 +37,10 @@ export type RenderOptionsType<T extends string | JSX.Element> = {
ourAci: AciString | undefined;
ourPni: PniString | undefined;
renderContact: SmartContactRendererType<T>;
checkServiceIdEquivalence(
left: ServiceIdString | undefined,
right: ServiceIdString | undefined
): boolean;
renderIntl: StringRendererType<T>;
};
@ -83,6 +87,7 @@ function renderChangeDetail<T extends string | JSX.Element>(
options: RenderOptionsType<T>
): string | T | ReadonlyArray<string | T> {
const {
checkServiceIdEquivalence,
from,
i18n: localizer,
ourAci,
@ -243,11 +248,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
return i18n('icu:GroupV2--access-invite-link--enabled--you');
}
if (from) {
return i18n(
'icu:GroupV2--access-invite-link--enabled--other',
{ adminName: renderContact(from) }
);
return i18n('icu:GroupV2--access-invite-link--enabled--other', {
adminName: renderContact(from),
});
}
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');
}
if (from) {
return i18n(
'icu:GroupV2--access-invite-link--disabled--other',
{ adminName: renderContact(from) }
);
return i18n('icu:GroupV2--access-invite-link--disabled--other', {
adminName: renderContact(from),
});
}
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 weAreInviter = isOurServiceId(inviter);
if (!from || from !== aci) {
if (!from || !checkServiceIdEquivalence(from, aci)) {
if (weAreJoiner) {
// They can't be the same, no fromYou check here
if (from) {
@ -350,13 +351,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
inviterName: renderContact(inviter),
});
}
return i18n(
'icu:GroupV2--member-add--from-invite--other-no-from',
{
inviteeName: renderContact(aci),
}
);
return i18n('icu:GroupV2--member-add--from-invite--other-no-from', {
inviteeName: renderContact(aci),
});
}
if (detail.type === 'member-add-from-link') {
const { aci } = detail;
@ -383,11 +380,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
if (weAreJoiner) {
if (from) {
return i18n(
'icu:GroupV2--member-add-from-admin-approval--you--other',
{ adminName: renderContact(from) }
);
return i18n('icu:GroupV2--member-add-from-admin-approval--you--other', {
adminName: renderContact(from),
});
}
// 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) {
return i18n(
'icu:GroupV2--member-add-from-admin-approval--other--you',
{ joinerName: renderContact(aci) }
);
return i18n('icu:GroupV2--member-add-from-admin-approval--other--you', {
joinerName: renderContact(aci),
});
}
if (from) {
return i18n(
'icu:GroupV2--member-add-from-admin-approval--other--other',
{
adminName: renderContact(from),
joinerName: renderContact(aci),
}
);
return i18n('icu:GroupV2--member-add-from-admin-approval--other--other', {
adminName: renderContact(from),
joinerName: renderContact(aci),
});
}
// Note: this shouldn't happen, because we only capture 'add-from-admin-approval'
// status from group change events, which always have a sender.
log.warn('member-add-from-admin-approval change type; we have no from');
return i18n(
'icu:GroupV2--member-add-from-admin-approval--other--unknown',
{ joinerName: renderContact(aci) }
);
return i18n('icu:GroupV2--member-add-from-admin-approval--other--unknown', {
joinerName: renderContact(aci),
});
}
if (detail.type === 'member-remove') {
const { aci } = detail;
@ -446,7 +433,7 @@ function renderChangeDetail<T extends string | JSX.Element>(
memberName: renderContact(aci),
});
}
if (from && from === aci) {
if (from && fromYou) {
return i18n('icu:GroupV2--member-remove--other--self', {
memberName: renderContact(from),
});
@ -468,11 +455,9 @@ function renderChangeDetail<T extends string | JSX.Element>(
if (newPrivilege === RoleEnum.ADMINISTRATOR) {
if (weAreMember) {
if (from) {
return i18n(
'icu:GroupV2--member-privilege--promote--you--other',
{ adminName: renderContact(from) }
);
return i18n('icu:GroupV2--member-privilege--promote--you--other', {
adminName: renderContact(from),
});
}
return i18n('icu:GroupV2--member-privilege--promote--you--unknown');
@ -509,20 +494,14 @@ function renderChangeDetail<T extends string | JSX.Element>(
});
}
if (from) {
return i18n(
'icu:GroupV2--member-privilege--demote--other--other',
{
adminName: renderContact(from),
memberName: renderContact(aci),
}
);
return i18n('icu:GroupV2--member-privilege--demote--other--other', {
adminName: renderContact(from),
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(
`member-privilege change type, privilege ${newPrivilege} is unknown`
@ -586,14 +565,12 @@ function renderChangeDetail<T extends string | JSX.Element>(
if (fromYou) {
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--you',
{ inviteeName: renderContact(serviceId) }
);
}
if (from) {
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--other',
{
adminName: renderContact(from),
inviteeName: renderContact(serviceId),
@ -602,7 +579,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
}
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
{ inviteeName: renderContact(serviceId) }
);
}
@ -619,30 +595,24 @@ function renderChangeDetail<T extends string | JSX.Element>(
}
if (inviter && sentByInviter) {
if (weAreInvited) {
return i18n(
'icu:GroupV2--pending-remove--revoke-own--to-you',
{ inviterName: renderContact(inviter) }
);
return i18n('icu:GroupV2--pending-remove--revoke-own--to-you', {
inviterName: renderContact(inviter),
});
}
return i18n(
'icu:GroupV2--pending-remove--revoke-own--unknown',
{ inviterName: renderContact(inviter) }
);
return i18n('icu:GroupV2--pending-remove--revoke-own--unknown', {
inviterName: renderContact(inviter),
});
}
if (inviter) {
if (fromYou) {
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from--one--you',
{ memberName: renderContact(inviter) }
);
}
if (from) {
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from--one--other',
{
adminName: renderContact(from),
memberName: renderContact(inviter),
@ -651,7 +621,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
}
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from--one--unknown',
{ memberName: renderContact(inviter) }
);
}
@ -673,14 +642,12 @@ function renderChangeDetail<T extends string | JSX.Element>(
if (fromYou) {
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--you',
{ count }
);
}
if (from) {
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--other',
{
adminName: renderContact(from),
count,
@ -689,7 +656,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
}
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from-you--many--unknown',
{ count }
);
}
@ -697,7 +663,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
if (fromYou) {
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from--many--you',
{
count,
memberName: renderContact(inviter),
@ -707,7 +672,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
if (from) {
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from--many--other',
{
adminName: renderContact(from),
count,
@ -717,7 +681,6 @@ function renderChangeDetail<T extends string | JSX.Element>(
}
return i18n(
'icu:GroupV2--pending-remove--revoke-invite-from--many--unknown',
{
count,
memberName: renderContact(inviter),
@ -730,20 +693,14 @@ function renderChangeDetail<T extends string | JSX.Element>(
});
}
if (from) {
return i18n(
'icu:GroupV2--pending-remove--revoke--many--other',
{
memberName: renderContact(from),
count,
}
);
return i18n('icu:GroupV2--pending-remove--revoke--many--other', {
memberName: renderContact(from),
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') {
const { aci } = detail;
@ -768,35 +725,25 @@ function renderChangeDetail<T extends string | JSX.Element>(
}
if (fromYou) {
return i18n(
'icu:GroupV2--admin-approval-remove-one--other--you',
{ joinerName: renderContact(aci) }
);
return i18n('icu:GroupV2--admin-approval-remove-one--other--you', {
joinerName: renderContact(aci),
});
}
if (from && from === aci) {
return i18n(
'icu:GroupV2--admin-approval-remove-one--other--own',
{ joinerName: renderContact(aci) }
);
if (from && fromYou) {
return i18n('icu:GroupV2--admin-approval-remove-one--other--own', {
joinerName: renderContact(aci),
});
}
if (from) {
return i18n(
'icu:GroupV2--admin-approval-remove-one--other--other',
{
adminName: renderContact(from),
joinerName: renderContact(aci),
}
);
return i18n('icu:GroupV2--admin-approval-remove-one--other--other', {
adminName: renderContact(from),
joinerName: renderContact(aci),
});
}
return i18n(
'icu:GroupV2--admin-approval-remove-one--other--unknown',
{ joinerName: renderContact(aci) }
);
return i18n('icu:GroupV2--admin-approval-remove-one--other--unknown', {
joinerName: renderContact(aci),
});
}
if (detail.type === 'admin-approval-bounce') {
const { aci, times, isApprovalPending } = detail;

View file

@ -2429,12 +2429,15 @@ export async function initiateMigrationToGroupV2(
groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v1-migration',
invitedGV2Members: pendingMembersV2.map(
({ serviceId: uuid, ...rest }) => {
return { ...rest, uuid };
}
),
droppedGV2MemberIds,
groupMigration: {
areWeInvited: false,
droppedMemberIds: droppedGV2MemberIds,
invitedMembers: pendingMembersV2.map(
({ serviceId: uuid, ...rest }) => {
return { ...rest, uuid };
}
),
},
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
});

54
ts/model-types.d.ts vendored
View file

@ -65,8 +65,12 @@ export type CustomError = Error & {
export type GroupMigrationType = {
areWeInvited: boolean;
droppedMemberIds: Array<string>;
invitedMembers: Array<LegacyMigrationPendingMemberType>;
droppedMemberIds?: Array<string>;
invitedMembers?: Array<LegacyMigrationPendingMemberType>;
// We don't generate data like this; these were added to support import/export
droppedMemberCount?: number;
invitedMemberCount?: number;
};
export type QuotedAttachment = {
@ -131,6 +135,28 @@ export type EditHistoryType = {
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 = {
bodyAttachment?: AttachmentType;
bodyRanges?: ReadonlyArray<RawBodyRange>;
@ -180,27 +206,7 @@ export type MessageAttributesType = {
verifiedChanged?: string;
id: string;
type:
| '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';
type: MessageType;
body?: string;
attachments?: Array<AttachmentType>;
preview?: Array<LinkPreviewType>;
@ -437,7 +443,7 @@ export type ConversationAttributesType = {
};
announcementsOnly?: boolean;
avatar?: ContactAvatarType | null;
avatars?: Array<AvatarDataType>;
avatars?: ReadonlyArray<Readonly<AvatarDataType>>;
description?: string;
expireTimer?: DurationInSeconds;
membersV2?: Array<GroupV2MemberType>;

View file

@ -4516,9 +4516,10 @@ export class ConversationModel extends window.Backbone
}
}
const ourConversationId =
window.ConversationController.getOurConversationId();
source = source || ourConversationId;
const ourConversation =
window.ConversationController.getOurConversationOrThrow();
source = source || ourConversation.id;
const sourceServiceId = ourConversation.get('serviceId');
this.set({ expireTimer });
@ -4535,7 +4536,7 @@ export class ConversationModel extends window.Backbone
const isFromSyncOperation =
reason === 'group sync' || reason === 'contact sync';
const isFromMe =
window.ConversationController.get(source)?.id === ourConversationId;
window.ConversationController.get(source) === ourConversation;
const isNoteToSelf = isMe(this.attributes);
const shouldBeRead =
(isInitialSync && isFromSyncOperation) || isFromMe || isNoteToSelf;
@ -4547,6 +4548,7 @@ export class ConversationModel extends window.Backbone
expirationTimerUpdate: {
expireTimer,
source,
sourceServiceId,
fromSync,
fromGroupUpdate,
},

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,13 @@ import { Aci, Pni } from '@signalapp/libsignal-client';
import { v4 as generateUuid } from 'uuid';
import pMap from 'p-map';
import { Writable } from 'stream';
import { isNumber } from 'lodash';
import { Backups } from '../../protobuf';
import { Backups, SignalService } from '../../protobuf';
import Data from '../../sql/Client';
import * as log from '../../logging/log';
import { StorySendMode } from '../../types/Stories';
import type { ServiceIdString } from '../../types/ServiceId';
import { fromAciObject, fromPniObject } from '../../types/ServiceId';
import * as Errors from '../../types/errors';
import type {
@ -36,6 +38,9 @@ import type { SendStateByConversationId } from '../../messages/MessageSendState'
import { SeenStatus } from '../../MessageSeenStatus';
import * as Bytes from '../../Bytes';
import { BACKUP_VERSION } from './constants';
import type { AboutMe } from './types';
import type { GroupV2ChangeDetailType } from '../../groups';
import { isNotNil } from '../../util/isNotNil';
const MAX_CONCURRENCY = 10;
@ -44,6 +49,11 @@ type ConversationOpType = Readonly<{
attributes: ConversationAttributesType;
}>;
type ChatItemParseResult = {
message: Partial<MessageAttributesType>;
additionalMessages: Array<Partial<MessageAttributesType>>;
};
async function processConversationOpBatch(
batch: ReadonlyArray<ConversationOpType>
): Promise<void> {
@ -67,6 +77,7 @@ async function processConversationOpBatch(
export class BackupImportStream extends Writable {
private parsedBackupInfo = false;
private logId = 'BackupImportStream(unknown)';
private aboutMe: AboutMe | undefined;
private readonly recipientIdToConvo = new Map<
number,
@ -126,7 +137,19 @@ export class BackupImportStream extends Writable {
} else {
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();
} catch (error) {
@ -181,7 +204,12 @@ export class BackupImportStream extends Writable {
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) {
await this.fromAccount(frame.account);
@ -215,7 +243,13 @@ export class BackupImportStream extends Writable {
} else if (frame.chat) {
await this.fromChat(frame.chat);
} 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 {
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> {
strictAssert(this.ourConversation != null, 'AccountData missing');
private async fromChatItem(
item: Backups.IChatItem,
options: { aboutMe: AboutMe }
): Promise<void> {
const { aboutMe } = options;
strictAssert(item.chatId != null, 'chatItem must have a chatId');
strictAssert(item.authorId != null, 'chatItem must have a authorId');
strictAssert(item.dateSent != null, 'chatItem must have a dateSent');
const timestamp = item?.dateSent?.toNumber();
const logId = `fromChatItem(${timestamp})`;
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());
strictAssert(chatConvo !== undefined, 'chat conversation not found');
strictAssert(
chatConvo !== undefined,
`${logId}: chat conversation not found`
);
const authorConvo = this.recipientIdToConvo.get(item.authorId.toNumber());
strictAssert(authorConvo !== undefined, 'author conversation not found');
const authorConvo = item.authorId
? 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 = {
id: generateUuid(),
canReplyToStory: false,
conversationId: chatConvo.id,
received_at: incrementMessageCounter(),
sent_at: item.dateSent.toNumber(),
source: authorConvo.e164,
sourceServiceId: authorConvo.serviceId,
timestamp: item.dateSent.toNumber(),
sent_at: timestamp,
source: authorConvo?.e164,
sourceServiceId: authorConvo?.serviceId,
timestamp,
type: isOutgoing ? 'outgoing' : 'incoming',
unidentifiedDeliveryReceived: false,
expirationStartTimestamp: item.expireStartDate
@ -527,10 +577,14 @@ export class BackupImportStream extends Writable {
? DurationInSeconds.fromMillis(item.expiresInMs.toNumber())
: undefined,
};
const additionalMessages: Array<MessageAttributesType> = [];
if (isOutgoing) {
const { outgoing } = item;
strictAssert(outgoing, 'outgoing message must have outgoing field');
const { outgoing, incoming, directionless } = item;
if (outgoing) {
strictAssert(
isOutgoing,
`${logId}: outgoing message must have outgoing field`
);
const sendStateByConversationId: SendStateByConversationId = {};
@ -583,9 +637,12 @@ export class BackupImportStream extends Writable {
attributes.sendStateByConversationId = sendStateByConversationId;
chatConvo.active_at = attributes.sent_at;
} else {
const { incoming } = item;
strictAssert(incoming, 'incoming message must have incoming field');
}
if (incoming) {
strictAssert(
isIncoming,
`${logId}: message with incoming field must be incoming`
);
attributes.received_at_ms =
incoming.dateReceived?.toNumber() ?? Date.now();
@ -600,24 +657,62 @@ export class BackupImportStream extends Writable {
chatConvo.active_at = attributes.received_at_ms;
}
if (directionless) {
strictAssert(
isDirectionLess,
`${logId}: directionless message must not be incoming/outgoing`
);
}
if (item.standardMessage) {
// TODO (DESKTOP-6964): add revisions to editHistory
attributes = {
...attributes,
...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(
isAciString(this.ourConversation.serviceId),
'Our conversation must have ACI'
`${logId}: Our conversation must have ACI`
);
this.saveMessage(attributes);
additionalMessages.forEach(additional => this.saveMessage(additional));
if (isOutgoing) {
chatConvo.sentMessageCount = (chatConvo.sentMessageCount ?? 0) + 1;
} else {
chatConvo.messageCount = (chatConvo.messageCount ?? 0) + 1;
// TODO (DESKTOP-6964): We'll want to increment for more types here - stickers, etc.
if (item.standardMessage) {
if (isOutgoing) {
chatConvo.sentMessageCount = (chatConvo.sentMessageCount ?? 0) + 1;
} else {
chatConvo.messageCount = (chatConvo.messageCount ?? 0) + 1;
}
}
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
View 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;
};

View file

@ -862,23 +862,23 @@ export const getCachedSelectorForConversation = createSelector(
}
);
export type GetConversationByIdType = (id?: string) => ConversationType;
export const getConversationSelector = createSelector(
getCachedSelectorForConversation,
export type GetConversationByAnyIdSelectorType = (
id?: string
) => ConversationType | undefined;
export const getConversationByAnyIdSelector = createSelector(
getConversationLookup,
getConversationsByServiceId,
getConversationsByE164,
getConversationsByGroupId,
(
selector: CachedConversationSelectorType,
byId: ConversationLookupType,
byServiceId: ConversationLookupType,
byE164: ConversationLookupType,
byGroupId: ConversationLookupType
): GetConversationByIdType => {
): GetConversationByAnyIdSelectorType => {
return (id?: string) => {
if (!id) {
return selector(undefined);
return undefined;
}
const onServiceId = getOwn(
@ -886,19 +886,42 @@ export const getConversationSelector = createSelector(
normalizeServiceId(id, 'getConversationSelector')
);
if (onServiceId) {
return selector(onServiceId);
return onServiceId;
}
const onE164 = getOwn(byE164, id);
if (onE164) {
return selector(onE164);
return onE164;
}
const onGroupId = getOwn(byGroupId, id);
if (onGroupId) {
return selector(onGroupId);
return onGroupId;
}
const onId = getOwn(byId, id);
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}`);
@ -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(
getConversationLookup,
conversationLookup =>

View file

@ -1113,6 +1113,8 @@ function getPropsForGroupV1Migration(
conversationId: message.conversationId,
droppedMembers,
invitedMembers,
droppedMemberCount: droppedMembers.length,
invitedMemberCount: invitedMembers.length,
};
}
@ -1120,19 +1122,30 @@ function getPropsForGroupV1Migration(
areWeInvited,
droppedMemberIds,
invitedMembers: rawInvitedMembers,
droppedMemberCount: rawDroppedMemberCount,
invitedMemberCount: rawInvitedMemberCount,
} = migration;
const invitedMembers = rawInvitedMembers.map(item =>
conversationSelector(item.uuid)
);
const droppedMembers = droppedMemberIds.map(conversationId =>
conversationSelector(conversationId)
);
const droppedMembers = droppedMemberIds
? droppedMemberIds.map(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 {
areWeInvited,
conversationId: message.conversationId,
droppedMembers,
invitedMembers,
droppedMemberCount,
invitedMemberCount,
};
}

View file

@ -72,7 +72,9 @@ export const SmartGroupV1MigrationDialog = memo(
hasMigrated={hasMigrated}
getPreferredBadge={getPreferredBadge}
droppedMembers={droppedMembers}
droppedMemberCount={droppedMembers.length}
invitedMembers={invitedMembers}
invitedMemberCount={invitedMembers.length}
onMigrate={handleMigrate}
onClose={closeGV2MigrationDialog}
/>

View file

@ -21,7 +21,10 @@ import {
getTheme,
getPlatform,
} from '../selectors/user';
import { getTargetedMessage } from '../selectors/conversations';
import {
getTargetedMessage,
getCheckServiceIdEquivalence,
} from '../selectors/conversations';
import { useTimelineItem } from '../selectors/timeline';
import {
areMessagesInSameGroup,
@ -86,6 +89,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
const isTargeted = Boolean(
targetedMessage && messageId === targetedMessage.id
);
const checkServiceIdEquivalence = useSelector(getCheckServiceIdEquivalence);
const isNextItemCallingNotification = nextItem?.type === 'callHistory';
@ -176,6 +180,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
<TimelineItem
item={item}
id={messageId}
checkServiceIdEquivalence={checkServiceIdEquivalence}
containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint}
conversationId={conversationId}

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@
import * as path from 'path';
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 { v4 as generateGuid } from 'uuid';
@ -25,7 +25,7 @@ describe('base_config', () => {
try {
chmodSync(targetDir, 0o755);
chmodSync(targetPath, 0o755);
rmdirSync(targetDir, { recursive: true });
rmSync(targetDir, { recursive: true });
} catch (err) {
assert.strictEqual(err.code, 'ENOENT');
}
@ -167,7 +167,7 @@ describe('base_config', () => {
throwOnFilesystemErrors: true,
});
config.set('foo', 123);
rmdirSync(targetDir, { recursive: true });
rmSync(targetDir, { recursive: true });
assert.throws(() => config.set('foo', 456));
assert.strictEqual(config.get('foo'), 123);
@ -185,7 +185,7 @@ describe('base_config', () => {
throwOnFilesystemErrors: false,
});
config.set('foo', 123);
rmdirSync(targetDir, { recursive: true });
rmSync(targetDir, { recursive: true });
config.set('bar', 456);

View file

@ -2221,7 +2221,7 @@ export default class MessageReceiver
const { sourceServiceId: sourceAci } = envelope;
strictAssert(
isAciString(sourceAci),
'MessageReceiver.handleEditMesage: received message from PNI'
'MessageReceiver.handleStoryMessage: received message from PNI'
);
const attachments: Array<ProcessedAttachment> = [];

View file

@ -143,6 +143,12 @@ export function getNotificationDataForMessage(
);
const changes = GroupChange.renderChange<string>(change, {
checkServiceIdEquivalence: (left, right) => {
return (
window.ConversationController.get(left) ===
window.ConversationController.get(right)
);
},
i18n: window.i18n,
ourAci: window.textsecure.storage.user.getCheckedAci(),
ourPni: window.textsecure.storage.user.getCheckedPni(),