Migration: Use pendingMember roles, better 'you were invited'

This commit is contained in:
Scott Nonnenberg 2020-12-01 15:45:39 -08:00 committed by GitHub
parent bb5036364e
commit b3c161f484
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 166 additions and 78 deletions

View file

@ -4010,6 +4010,10 @@
"message": "All message history and media will be kept from before the upgrade.", "message": "All message history and media will be kept from before the upgrade.",
"description": "Shown on Migration popup before GV1 migration" "description": "Shown on Migration popup before GV1 migration"
}, },
"GroupV1--Migration--info--invited--you": {
"message": "You will need to accept an invite to join this group again, and will not receive group messages until you accept.",
"description": "Shown on Learn More popup after GV1 migration"
},
"GroupV1--Migration--info--invited--many": { "GroupV1--Migration--info--invited--many": {
"message": "These members will need to accept an invite to join this group again, and will not receive group messages until they accept:", "message": "These 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" "description": "Shown on Learn More popup after or Migration popup before GV1 migration"
@ -4034,6 +4038,10 @@
"message": "This member was not capable of joining New Groups, and was removed from the group:", "message": "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"
}, },
"GroupV1--Migration--invited--you": {
"message": "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"
},
"GroupV1--Migration--invited--one": { "GroupV1--Migration--invited--one": {
"message": "$contact$ couldnt be added to the New Group and has been invited to join.", "message": "$contact$ couldnt be added to the New Group and has been invited to join.",
"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",

View file

@ -36,6 +36,10 @@ function booleanOr(value: boolean | undefined, defaultValue: boolean): boolean {
} }
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
areWeInvited: boolean(
'areWeInvited',
booleanOr(overrideProps.areWeInvited, false)
),
droppedMembers: overrideProps.droppedMembers || [contact1], droppedMembers: overrideProps.droppedMembers || [contact1],
hasMigrated: boolean( hasMigrated: boolean(
'hasMigrated', 'hasMigrated',
@ -63,6 +67,17 @@ stories.add('Migrated, basic', () => {
); );
}); });
stories.add('Migrated, you are invited', () => {
return (
<GroupV1MigrationDialog
{...createProps({
hasMigrated: true,
areWeInvited: true,
})}
/>
);
});
stories.add('Not yet migrated, multiple dropped and invited members', () => { stories.add('Not yet migrated, multiple dropped and invited members', () => {
return ( return (
<GroupV1MigrationDialog <GroupV1MigrationDialog

View file

@ -16,6 +16,7 @@ export type ActionSpec = {
type CallbackType = () => unknown; type CallbackType = () => unknown;
export type DataPropsType = { export type DataPropsType = {
readonly areWeInvited: boolean;
readonly droppedMembers: Array<ConversationType>; readonly droppedMembers: Array<ConversationType>;
readonly hasMigrated: boolean; readonly hasMigrated: boolean;
readonly invitedMembers: Array<ConversationType>; readonly invitedMembers: Array<ConversationType>;
@ -37,6 +38,7 @@ function focusRef(el: HTMLElement | null) {
export const GroupV1MigrationDialog = React.memo((props: PropsType) => { export const GroupV1MigrationDialog = React.memo((props: PropsType) => {
const { const {
areWeInvited,
droppedMembers, droppedMembers,
hasMigrated, hasMigrated,
i18n, i18n,
@ -76,12 +78,23 @@ export const GroupV1MigrationDialog = React.memo((props: PropsType) => {
{keepHistory} {keepHistory}
</div> </div>
</div> </div>
{renderMembers( {areWeInvited ? (
invitedMembers, <div className="module-group-v2-migration-dialog__item">
'GroupV1--Migration--info--invited', <div className="module-group-v2-migration-dialog__item__bullet" />
i18n <div className="module-group-v2-migration-dialog__item__content">
{i18n('GroupV1--Migration--info--invited--you')}
</div>
</div>
) : (
<>
{renderMembers(
invitedMembers,
'GroupV1--Migration--info--invited',
i18n
)}
{renderMembers(droppedMembers, droppedMembersKey, i18n)}
</>
)} )}
{renderMembers(droppedMembers, droppedMembersKey, i18n)}
</div> </div>
{renderButtons(hasMigrated, onClose, migrate, i18n)} {renderButtons(hasMigrated, onClose, migrate, i18n)}
</div> </div>

View file

@ -4,6 +4,8 @@
/* eslint-disable-next-line max-classes-per-file */ /* eslint-disable-next-line max-classes-per-file */
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { isBoolean } from 'lodash';
import { boolean } from '@storybook/addon-knobs';
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
@ -30,6 +32,10 @@ const contact2 = {
}; };
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
areWeInvited: boolean(
'areWeInvited',
isBoolean(overrideProps.areWeInvited) ? overrideProps.areWeInvited : false
),
droppedMembers: overrideProps.droppedMembers || [contact1], droppedMembers: overrideProps.droppedMembers || [contact1],
i18n, i18n,
invitedMembers: overrideProps.invitedMembers || [contact2], invitedMembers: overrideProps.invitedMembers || [contact2],
@ -37,6 +43,14 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
const stories = storiesOf('Components/Conversation/GroupV1Migration', module); const stories = storiesOf('Components/Conversation/GroupV1Migration', module);
stories.add('You were invited', () => (
<GroupV1Migration
{...createProps({
areWeInvited: true,
})}
/>
));
stories.add('Single dropped and single invited member', () => ( stories.add('Single dropped and single invited member', () => (
<GroupV1Migration {...createProps()} /> <GroupV1Migration {...createProps()} />
)); ));

View file

@ -11,6 +11,7 @@ import { ModalHost } from '../ModalHost';
import { GroupV1MigrationDialog } from '../GroupV1MigrationDialog'; import { GroupV1MigrationDialog } from '../GroupV1MigrationDialog';
export type PropsDataType = { export type PropsDataType = {
areWeInvited: boolean;
droppedMembers: Array<ConversationType>; droppedMembers: Array<ConversationType>;
invitedMembers: Array<ConversationType>; invitedMembers: Array<ConversationType>;
}; };
@ -22,7 +23,7 @@ export type PropsHousekeepingType = {
export type PropsType = PropsDataType & PropsHousekeepingType; export type PropsType = PropsDataType & PropsHousekeepingType;
export function GroupV1Migration(props: PropsType): React.ReactElement { export function GroupV1Migration(props: PropsType): React.ReactElement {
const { droppedMembers, i18n, invitedMembers } = props; const { areWeInvited, droppedMembers, i18n, invitedMembers } = props;
const [showingDialog, setShowingDialog] = React.useState(false); const [showingDialog, setShowingDialog] = React.useState(false);
const showDialog = React.useCallback(() => { const showDialog = React.useCallback(() => {
@ -39,8 +40,16 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
<div className="module-group-v1-migration--text"> <div className="module-group-v1-migration--text">
{i18n('GroupV1--Migration--was-upgraded')} {i18n('GroupV1--Migration--was-upgraded')}
</div> </div>
{renderUsers(invitedMembers, i18n, 'GroupV1--Migration--invited')} {areWeInvited ? (
{renderUsers(droppedMembers, i18n, 'GroupV1--Migration--removed')} <div className="module-group-v1-migration--text">
{i18n('GroupV1--Migration--invited--you')}
</div>
) : (
<>
{renderUsers(invitedMembers, i18n, 'GroupV1--Migration--invited')}
{renderUsers(droppedMembers, i18n, 'GroupV1--Migration--removed')}
</>
)}
<button <button
type="button" type="button"
className="module-group-v1-migration--button" className="module-group-v1-migration--button"
@ -51,6 +60,7 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
{showingDialog ? ( {showingDialog ? (
<ModalHost onClose={dismissDialog}> <ModalHost onClose={dismissDialog}>
<GroupV1MigrationDialog <GroupV1MigrationDialog
areWeInvited
droppedMembers={droppedMembers} droppedMembers={droppedMembers}
hasMigrated hasMigrated
i18n={i18n} i18n={i18n}

View file

@ -365,7 +365,7 @@ async function buildGroupProto({
const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid); const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid);
member.userId = uuidCipherTextBuffer; member.userId = uuidCipherTextBuffer;
member.role = MEMBER_ROLE_ENUM.DEFAULT; member.role = item.role || MEMBER_ROLE_ENUM.DEFAULT;
pendingMember.member = member; pendingMember.member = member;
pendingMember.timestamp = item.timestamp; pendingMember.timestamp = item.timestamp;
@ -726,8 +726,6 @@ export async function isGroupEligibleToMigrate(
export async function getGroupMigrationMembers( export async function getGroupMigrationMembers(
conversation: ConversationModel conversation: ConversationModel
): Promise<{ ): Promise<{
areWeInvited: boolean;
areWeMember: boolean;
droppedGV2MemberIds: Array<string>; droppedGV2MemberIds: Array<string>;
membersV2: Array<GroupV2MemberType>; membersV2: Array<GroupV2MemberType>;
pendingMembersV2: Array<GroupV2PendingMemberType>; pendingMembersV2: Array<GroupV2PendingMemberType>;
@ -871,13 +869,19 @@ export async function getGroupMigrationMembers(
conversationId, conversationId,
timestamp: now, timestamp: now,
addedByUserId: ourConversationId, addedByUserId: ourConversationId,
role: MEMBER_ROLE_ENUM.ADMINISTRATOR,
}; };
}) })
); );
if (!areWeMember) {
throw new Error(`getGroupMigrationMembers/${logId}: We are not a member!`);
}
if (areWeInvited) {
throw new Error(`getGroupMigrationMembers/${logId}: We are invited!`);
}
return { return {
areWeInvited,
areWeMember,
droppedGV2MemberIds, droppedGV2MemberIds,
membersV2, membersV2,
pendingMembersV2, pendingMembersV2,
@ -929,25 +933,12 @@ export async function initiateMigrationToGroupV2(
} }
const { const {
areWeMember,
areWeInvited,
membersV2, membersV2,
pendingMembersV2, pendingMembersV2,
droppedGV2MemberIds, droppedGV2MemberIds,
previousGroupV1Members, previousGroupV1Members,
} = await getGroupMigrationMembers(conversation); } = await getGroupMigrationMembers(conversation);
if (!areWeMember) {
throw new Error(
`initiateMigrationToGroupV2/${logId}: After members migration, we are not a member!`
);
}
if (areWeInvited) {
throw new Error(
`initiateMigrationToGroupV2/${logId}: After members migration, we are invited!`
);
}
const rawSizeLimit = window.Signal.RemoteConfig.getValue( const rawSizeLimit = window.Signal.RemoteConfig.getValue(
'global.groupsv2.groupSizeHardLimit' 'global.groupsv2.groupSizeHardLimit'
); );
@ -1308,31 +1299,35 @@ export async function respondToGroupV2Migration({
...(newAttributes.membersV2 || []).map(item => item.conversationId), ...(newAttributes.membersV2 || []).map(item => item.conversationId),
...(newAttributes.pendingMembersV2 || []).map(item => item.conversationId), ...(newAttributes.pendingMembersV2 || []).map(item => item.conversationId),
]; ];
const droppedGV2MemberIds: Array<string> = difference( const droppedMemberIds: Array<string> = difference(
previousGroupV1MembersIds, previousGroupV1MembersIds,
combinedConversationIds combinedConversationIds
).filter(id => id && id !== ourConversationId); ).filter(id => id && id !== ourConversationId);
const invitedGV2Members = (newAttributes.pendingMembersV2 || []).filter( const invitedMembers = (newAttributes.pendingMembersV2 || []).filter(
item => item.conversationId !== ourConversationId item => item.conversationId !== ourConversationId
); );
// Generate notifications into the timeline
const groupChangeMessages: Array<MessageAttributesType> = [];
groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v1-migration',
invitedGV2Members,
droppedGV2MemberIds,
});
const areWeInvited = (newAttributes.pendingMembersV2 || []).some( const areWeInvited = (newAttributes.pendingMembersV2 || []).some(
item => item.conversationId === ourConversationId item => item.conversationId === ourConversationId
); );
const areWeMember = (newAttributes.membersV2 || []).some( const areWeMember = (newAttributes.membersV2 || []).some(
item => item.conversationId === ourConversationId item => item.conversationId === ourConversationId
); );
// Generate notifications into the timeline
const groupChangeMessages: Array<MessageAttributesType> = [];
groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v1-migration',
groupMigration: {
areWeInvited,
invitedMembers,
droppedMemberIds,
},
});
if (!areWeInvited && !areWeMember) { if (!areWeInvited && !areWeMember) {
// Add a message to the timeline saying the user was removed // Add a message to the timeline saying the user was removed. This shouldn't happen.
groupChangeMessages.push({ groupChangeMessages.push({
...generateBasicMessage(), ...generateBasicMessage(),
type: 'group-v2-change', type: 'group-v2-change',
@ -1345,20 +1340,6 @@ export async function respondToGroupV2Migration({
], ],
}, },
}); });
} else if (areWeInvited && !areWeMember && ourConversationId) {
// Add a message to the timeline saying we were invited to the group
groupChangeMessages.push({
...generateBasicMessage(),
type: 'group-v2-change',
groupV2Change: {
details: [
{
type: 'pending-add-one' as const,
conversationId: ourConversationId,
},
],
},
});
} }
// This buffer ensures that all migration-related messages are sorted above // This buffer ensures that all migration-related messages are sorted above
@ -2609,6 +2590,7 @@ async function applyGroupChange({
conversationId: conversation.id, conversationId: conversation.id,
addedByUserId: added.addedByUserId, addedByUserId: added.addedByUserId,
timestamp: added.timestamp, timestamp: added.timestamp,
role: added.member.role || MEMBER_ROLE_ENUM.DEFAULT,
}; };
if (added.member && added.member.profileKey) { if (added.member && added.member.profileKey) {
@ -2659,6 +2641,8 @@ async function applyGroupChange({
} }
); );
const previousRecord = pendingMembers[conversation.id];
if (pendingMembers[conversation.id]) { if (pendingMembers[conversation.id]) {
delete pendingMembers[conversation.id]; delete pendingMembers[conversation.id];
} else { } else {
@ -2677,7 +2661,7 @@ async function applyGroupChange({
members[conversation.id] = { members[conversation.id] = {
conversationId: conversation.id, conversationId: conversation.id,
joinedAtVersion: version, joinedAtVersion: version,
role: MEMBER_ROLE_ENUM.DEFAULT, role: previousRecord.role || MEMBER_ROLE_ENUM.DEFAULT,
}; };
newProfileKeys.push({ newProfileKeys.push({
@ -2843,6 +2827,7 @@ async function applyGroupState({
}): Promise<ConversationAttributesType> { }): Promise<ConversationAttributesType> {
const logId = idForLogging(group); const logId = idForLogging(group);
const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired;
const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role;
const version = groupState.version || 0; const version = groupState.version || 0;
const result = { ...group }; const result = { ...group };
@ -2912,17 +2897,12 @@ async function applyGroupState({
} }
} }
if ( if (!isValidRole(member.role)) {
!member.role || throw new Error('applyGroupState: Member had invalid role');
member.role === window.textsecure.protobuf.Member.Role.UNKNOWN
) {
throw new Error(
'applyGroupState: Received false or UNKNOWN member.role'
);
} }
return { return {
role: member.role, role: member.role || MEMBER_ROLE_ENUM.DEFAULT,
joinedAtVersion: member.joinedAtVersion || version, joinedAtVersion: member.joinedAtVersion || version,
conversationId: conversation.id, conversationId: conversation.id,
}; };
@ -2947,7 +2927,9 @@ async function applyGroupState({
} }
); );
} else { } else {
throw new Error('Pending member did not have an associated userId'); throw new Error(
'applyGroupState: Pending member did not have an associated userId'
);
} }
if (member.addedByUserId) { if (member.addedByUserId) {
@ -2956,13 +2938,20 @@ async function applyGroupState({
'private' 'private'
); );
} else { } else {
throw new Error('Pending member did not have an addedByUserID'); throw new Error(
'applyGroupState: Pending member did not have an addedByUserID'
);
}
if (!isValidRole(member.member.role)) {
throw new Error('applyGroupState: Pending member had invalid role');
} }
return { return {
addedByUserId: invitedBy.id, addedByUserId: invitedBy.id,
conversationId: pending.id, conversationId: pending.id,
timestamp: member.timestamp, timestamp: member.timestamp,
role: member.member.role || MEMBER_ROLE_ENUM.DEFAULT,
}; };
} }
); );
@ -2971,7 +2960,7 @@ async function applyGroupState({
return result; return result;
} }
function isValidRole(role?: number): boolean { function isValidRole(role?: number): role is number {
const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role;
return ( return (

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

@ -13,7 +13,11 @@ import {
LastMessageStatus, LastMessageStatus,
} from './state/ducks/conversations'; } from './state/ducks/conversations';
import { SendOptionsType } from './textsecure/SendMessage'; import { SendOptionsType } from './textsecure/SendMessage';
import { SyncMessageClass } from './textsecure.d'; import {
AccessRequiredEnum,
MemberRoleEnum,
SyncMessageClass,
} from './textsecure.d';
import { UserMessage } from './types/Message'; import { UserMessage } from './types/Message';
import { MessageModel } from './models/messages'; import { MessageModel } from './models/messages';
import { ConversationModel } from './models/conversations'; import { ConversationModel } from './models/conversations';
@ -46,6 +50,12 @@ export interface CustomError extends Error {
number?: string; number?: string;
} }
export type GroupMigrationType = {
areWeInvited: boolean;
droppedMemberIds: Array<string>;
invitedMembers: Array<GroupV2PendingMemberType>;
};
export type MessageAttributesType = { export type MessageAttributesType = {
bodyPending: boolean; bodyPending: boolean;
bodyRanges: BodyRangesType; bodyRanges: BodyRangesType;
@ -57,11 +67,11 @@ export type MessageAttributesType = {
deletedForEveryoneTimestamp?: number; deletedForEveryoneTimestamp?: number;
delivered: number; delivered: number;
delivered_to: Array<string | null>; delivered_to: Array<string | null>;
droppedGV2MemberIds?: Array<string>;
errors: Array<CustomError> | null; errors: Array<CustomError> | null;
expirationStartTimestamp: number | null; expirationStartTimestamp: number | null;
expireTimer: number; expireTimer: number;
expires_at: number; expires_at: number;
groupMigration?: GroupMigrationType;
group_update: { group_update: {
avatarUpdated: boolean; avatarUpdated: boolean;
joined: Array<string>; joined: Array<string>;
@ -74,7 +84,6 @@ export type MessageAttributesType = {
isErased: boolean; isErased: boolean;
isTapToViewInvalid: boolean; isTapToViewInvalid: boolean;
isViewOnce: boolean; isViewOnce: boolean;
invitedGV2Members?: Array<GroupV2PendingMemberType>;
key_changed: string; key_changed: string;
local: boolean; local: boolean;
logger: unknown; logger: unknown;
@ -139,6 +148,10 @@ export type MessageAttributesType = {
unread: number; unread: number;
timestamp: number; timestamp: number;
// Backwards-compatibility with prerelease data schema
invitedGV2Members?: Array<GroupV2PendingMemberType>;
droppedGV2MemberIds?: Array<string>;
}; };
export type ConversationAttributesTypeType = 'private' | 'group'; export type ConversationAttributesTypeType = 'private' | 'group';
@ -215,8 +228,8 @@ export type ConversationAttributesType = {
// GroupV2 other fields // GroupV2 other fields
accessControl?: { accessControl?: {
attributes: number; attributes: AccessRequiredEnum;
members: number; members: AccessRequiredEnum;
}; };
avatar?: { avatar?: {
url: string; url: string;
@ -232,13 +245,14 @@ export type ConversationAttributesType = {
export type GroupV2MemberType = { export type GroupV2MemberType = {
conversationId: string; conversationId: string;
role: number; role: MemberRoleEnum;
joinedAtVersion: number; joinedAtVersion: number;
}; };
export type GroupV2PendingMemberType = { export type GroupV2PendingMemberType = {
addedByUserId?: string; addedByUserId?: string;
conversationId: string; conversationId: string;
timestamp: number; timestamp: number;
role: MemberRoleEnum;
}; };
export type VerificationOptions = { export type VerificationOptions = {

View file

@ -515,17 +515,40 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
getPropsForGroupV1Migration(): GroupV1MigrationPropsType { getPropsForGroupV1Migration(): GroupV1MigrationPropsType {
const invitedGV2Members = this.get('invitedGV2Members') || []; const migration = this.get('groupMigration');
const droppedGV2MemberIds = this.get('droppedGV2MemberIds') || []; if (!migration) {
// Backwards-compatibility with data schema in early betas
const invitedGV2Members = this.get('invitedGV2Members') || [];
const droppedGV2MemberIds = this.get('droppedGV2MemberIds') || [];
const invitedMembers = invitedGV2Members.map(item => const invitedMembers = invitedGV2Members.map(item =>
this.findAndFormatContact(item.conversationId)
);
const droppedMembers = droppedGV2MemberIds.map(conversationId =>
this.findAndFormatContact(conversationId)
);
return {
areWeInvited: false,
droppedMembers,
invitedMembers,
};
}
const {
areWeInvited,
droppedMemberIds,
invitedMembers: rawInvitedMembers,
} = migration;
const invitedMembers = rawInvitedMembers.map(item =>
this.findAndFormatContact(item.conversationId) this.findAndFormatContact(item.conversationId)
); );
const droppedMembers = droppedGV2MemberIds.map(conversationId => const droppedMembers = droppedMemberIds.map(conversationId =>
this.findAndFormatContact(conversationId) this.findAndFormatContact(conversationId)
); );
return { return {
areWeInvited,
droppedMembers, droppedMembers,
invitedMembers, invitedMembers,
}; };

6
ts/textsecure.d.ts vendored
View file

@ -261,7 +261,7 @@ export declare class MemberClass {
// Note: only role and presentation are required when creating a group // Note: only role and presentation are required when creating a group
} }
type MemberRoleEnum = number; export type MemberRoleEnum = number;
// Note: we need to use namespaces to express nested classes in Typescript // Note: we need to use namespaces to express nested classes in Typescript
export declare namespace MemberClass { export declare namespace MemberClass {
@ -283,8 +283,6 @@ export declare class PendingMemberClass {
timestamp?: ProtoBigNumberType; timestamp?: ProtoBigNumberType;
} }
type AccessRequiredEnum = number;
export declare class AccessControlClass { export declare class AccessControlClass {
static decode: ( static decode: (
data: ArrayBuffer | ByteBufferClass, data: ArrayBuffer | ByteBufferClass,
@ -295,6 +293,8 @@ export declare class AccessControlClass {
members?: AccessRequiredEnum; members?: AccessRequiredEnum;
} }
export type AccessRequiredEnum = number;
// Note: we need to use namespaces to express nested classes in Typescript // Note: we need to use namespaces to express nested classes in Typescript
export declare namespace AccessControlClass { export declare namespace AccessControlClass {
class AccessRequired { class AccessRequired {

View file

@ -1201,7 +1201,8 @@ Whisper.ConversationView = Whisper.View.extend({
}); });
}; };
// Grab the dropped/invited user set // Note: this call will throw if, after generating member lists, we are no longer a
// member or are in the pending member list.
const { const {
droppedGV2MemberIds, droppedGV2MemberIds,
pendingMembersV2, pendingMembersV2,
@ -1219,6 +1220,7 @@ Whisper.ConversationView = Whisper.View.extend({
JSX: window.Signal.State.Roots.createGroupV1MigrationModal( JSX: window.Signal.State.Roots.createGroupV1MigrationModal(
window.reduxStore, window.reduxStore,
{ {
areWeInvited: false,
droppedMemberIds: droppedGV2MemberIds, droppedMemberIds: droppedGV2MemberIds,
hasMigrated: false, hasMigrated: false,
invitedMemberIds, invitedMemberIds,