Moves startGV2Migration to redux

This commit is contained in:
Josh Perez 2022-12-08 01:41:37 -05:00 committed by GitHub
parent 452e0b7b31
commit 7ea38bb1a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 171 additions and 125 deletions

View file

@ -105,7 +105,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
),
title: '',
// GroupV1 Disabled Actions
onStartGroupMigration: action('onStartGroupMigration'),
showGV2MigrationDialog: action('showGV2MigrationDialog'),
// GroupV2
announcementsOnly: boolean(
'announcementsOnly',

View file

@ -162,7 +162,7 @@ export type Props = Pick<
| 'clearShowPickerHint'
> &
MessageRequestActionsProps &
Pick<GroupV1DisabledActionsPropsType, 'onStartGroupMigration'> &
Pick<GroupV1DisabledActionsPropsType, 'showGV2MigrationDialog'> &
Pick<GroupV2PendingApprovalActionsPropsType, 'onCancelJoinRequest'> &
OwnProps;
@ -244,7 +244,7 @@ export function CompositionArea({
title,
// GroupV1 Disabled Actions
isGroupV1AndDisabled,
onStartGroupMigration,
showGV2MigrationDialog,
// GroupV2
announcementsOnly,
areWeAdmin,
@ -561,8 +561,9 @@ export function CompositionArea({
if (!left && isGroupV1AndDisabled) {
return (
<GroupV1DisabledActions
conversationId={conversationId}
i18n={i18n}
onStartGroupMigration={onStartGroupMigration}
showGV2MigrationDialog={showGV2MigrationDialog}
/>
);
}

View file

@ -35,6 +35,7 @@ const contact3: ConversationType = getDefaultConversation({
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
areWeInvited: Boolean(overrideProps.areWeInvited),
conversationId: '123',
droppedMembers: overrideProps.droppedMembers || [contact3, contact1],
getPreferredBadge: () => undefined,
hasMigrated: Boolean(overrideProps.hasMigrated),

View file

@ -8,39 +8,62 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import { GroupDialog } from './GroupDialog';
import { sortByTitle } from '../util/sortByTitle';
type CallbackType = () => unknown;
export type DataPropsType = {
conversationId: string;
readonly areWeInvited: boolean;
readonly droppedMembers: Array<ConversationType>;
readonly hasMigrated: boolean;
readonly invitedMembers: Array<ConversationType>;
readonly migrate: CallbackType;
readonly onClose: CallbackType;
};
export type HousekeepingPropsType = {
readonly getPreferredBadge: PreferredBadgeSelectorType;
readonly i18n: LocalizerType;
readonly theme: ThemeType;
};
export type PropsType = DataPropsType & HousekeepingPropsType;
type ActionsPropsType =
| {
initiateMigrationToGroupV2: (conversationId: string) => unknown;
closeGV2MigrationDialog: () => unknown;
}
| {
readonly migrate: () => unknown;
readonly onClose: () => unknown;
};
export type PropsType = DataPropsType & ActionsPropsType;
export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
React.memo(function GroupV1MigrationDialogInner(props: PropsType) {
const {
areWeInvited,
conversationId,
droppedMembers,
getPreferredBadge,
hasMigrated,
i18n,
invitedMembers,
migrate,
onClose,
theme,
} = props;
let migrateHandler;
if ('migrate' in props) {
migrateHandler = props.migrate;
} else if ('initiateMigrationToGroupV2' in props) {
migrateHandler = () => props.initiateMigrationToGroupV2(conversationId);
} else {
throw new Error(
'GroupV1MigrationDialog: No conversationId or migration function'
);
}
let closeHandler;
if ('onClose' in props) {
closeHandler = props.onClose;
} else if ('closeGV2MigrationDialog' in props) {
closeHandler = props.closeGV2MigrationDialog;
} else {
throw new Error('GroupV1MigrationDialog: No close function provided');
}
const title = hasMigrated
? i18n('GroupV1--Migration--info--title')
: i18n('GroupV1--Migration--migrate--title');
@ -60,13 +83,13 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
};
if (hasMigrated) {
primaryButtonText = i18n('Confirmation--confirm');
onClickPrimaryButton = onClose;
onClickPrimaryButton = closeHandler;
} else {
primaryButtonText = i18n('GroupV1--Migration--migrate');
onClickPrimaryButton = migrate;
onClickPrimaryButton = migrateHandler;
secondaryButtonProps = {
secondaryButtonText: i18n('cancel'),
onClickSecondaryButton: onClose,
onClickSecondaryButton: closeHandler,
};
}
@ -74,7 +97,7 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
<GroupDialog
i18n={i18n}
onClickPrimaryButton={onClickPrimaryButton}
onClose={onClose}
onClose={closeHandler}
primaryButtonText={primaryButtonText}
title={title}
{...secondaryButtonProps}

View file

@ -12,8 +12,9 @@ import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const createProps = (): GroupV1DisabledActionsPropsType => ({
conversationId: '123',
i18n,
onStartGroupMigration: action('onStartGroupMigration'),
showGV2MigrationDialog: action('showGV2MigrationDialog'),
});
export default {

View file

@ -6,13 +6,15 @@ import { Intl } from '../Intl';
import type { LocalizerType } from '../../types/Util';
export type PropsType = {
conversationId: string;
i18n: LocalizerType;
onStartGroupMigration: () => unknown;
showGV2MigrationDialog: (id: string) => unknown;
};
export function GroupV1DisabledActions({
conversationId,
i18n,
onStartGroupMigration,
showGV2MigrationDialog,
}: PropsType): JSX.Element {
return (
<div className="module-group-v1-disabled-actions">
@ -37,7 +39,7 @@ export function GroupV1DisabledActions({
<div className="module-group-v1-disabled-actions__buttons">
<button
type="button"
onClick={onStartGroupMigration}
onClick={() => showGV2MigrationDialog(conversationId)}
tabIndex={0}
className="module-group-v1-disabled-actions__buttons__button"
>

View file

@ -31,6 +31,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
'areWeInvited',
isBoolean(overrideProps.areWeInvited) ? overrideProps.areWeInvited : false
),
conversationId: '123',
droppedMembers: overrideProps.droppedMembers || [contact1],
getPreferredBadge: () => undefined,
i18n,

View file

@ -15,6 +15,7 @@ import * as log from '../../logging/log';
export type PropsDataType = {
areWeInvited: boolean;
conversationId: string;
droppedMembers: Array<ConversationType>;
invitedMembers: Array<ConversationType>;
};
@ -30,6 +31,7 @@ export type PropsType = PropsDataType & PropsHousekeepingType;
export function GroupV1Migration(props: PropsType): React.ReactElement {
const {
areWeInvited,
conversationId,
droppedMembers,
getPreferredBadge,
i18n,
@ -86,6 +88,7 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
{showingDialog ? (
<GroupV1MigrationDialog
areWeInvited={areWeInvited}
conversationId={conversationId}
droppedMembers={droppedMembers}
getPreferredBadge={getPreferredBadge}
hasMigrated

View file

@ -28,7 +28,6 @@ import { createChatColorPicker } from './state/roots/createChatColorPicker';
import { createConversationDetails } from './state/roots/createConversationDetails';
import { createApp } from './state/roots/createApp';
import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement';
import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal';
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import { createMessageDetail } from './state/roots/createMessageDetail';
import { createConversationNotificationsSettings } from './state/roots/createConversationNotificationsSettings';
@ -410,7 +409,6 @@ export const setup = (options: {
createChatColorPicker,
createConversationDetails,
createGroupLinkManagement,
createGroupV1MigrationModal,
createGroupV2JoinModal,
createGroupV2Permissions,
createMessageDetail,

View file

@ -107,6 +107,11 @@ import { denyPendingApprovalRequest } from '../../util/denyPendingApprovalReques
import { SignalService as Proto } from '../../protobuf';
import { addReportSpamJob } from '../../jobs/helpers/addReportSpamJob';
import { reportSpamJobQueue } from '../../jobs/reportSpamJobQueue';
import {
modifyGroupV2,
buildPromotePendingAdminApprovalMemberChange,
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
} from '../../groups';
// State
@ -876,6 +881,7 @@ export const actions = {
doubleCheckMissingQuoteReference,
generateNewGroupLink,
loadRecentMediaItems,
initiateMigrationToGroupV2,
messageChanged,
messageDeleted,
messageExpanded,
@ -2142,7 +2148,7 @@ function approvePendingMembershipFromGroupV2(
isGroupV2(conversation.attributes) &&
isMemberRequestingToJoin(conversation.attributes, uuid)
) {
await window.Signal.Groups.modifyGroupV2({
await modifyGroupV2({
conversation,
usingCredentialsFrom: [pendingMember],
createGroupChange: async () => {
@ -2157,12 +2163,10 @@ function approvePendingMembershipFromGroupV2(
return undefined;
}
return window.Signal.Groups.buildPromotePendingAdminApprovalMemberChange(
{
group: conversation.attributes,
uuid,
}
);
return buildPromotePendingAdminApprovalMemberChange({
group: conversation.attributes,
uuid,
});
},
name: 'approvePendingMembershipFromGroupV2',
});
@ -2362,6 +2366,26 @@ function deleteConversation(conversationId: string): NoopActionType {
};
}
function initiateMigrationToGroupV2(conversationId: string): NoopActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error(
'deleteConversation: Expected a conversation to be found. Doing nothing'
);
}
longRunningTaskWrapper({
idForLogging: conversation.idForLogging(),
name: 'initiateMigrationToGroupV2',
task: () => doInitiateMigrationToGroupV2(conversation),
});
return {
type: 'NOOP',
payload: null,
};
}
function loadRecentMediaItems(
conversationId: string,
limit: number

View file

@ -3,15 +3,19 @@
import type { ThunkAction } from 'redux-thunk';
import type { ExplodePromiseResultType } from '../../util/explodePromise';
import type { GroupV2PendingMemberType } from '../../model-types.d';
import type { PropsForMessage } from '../selectors/message';
import type { RecipientsByConversation } from './stories';
import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import type { StateType as RootStateType } from '../reducer';
import type { UUIDStringType } from '../../types/UUID';
import * as SingleServePromise from '../../services/singleServePromise';
import { getMessageById } from '../../messages/getMessageById';
import { getMessagePropsSelector } from '../selectors/message';
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
import { useBoundActions } from '../../hooks/useBoundActions';
import type { RecipientsByConversation } from './stories';
import { isGroupV1 } from '../../util/whatTypeOfConversation';
import { getGroupMigrationMembers } from '../../groups';
// State
@ -24,9 +28,19 @@ export type SafetyNumberChangedBlockingDataType = Readonly<{
source?: SafetyNumberChangeSource;
}>;
type MigrateToGV2PropsType = {
areWeInvited: boolean;
conversationId: string;
droppedMemberIds: ReadonlyArray<string>;
hasMigrated: boolean;
invitedMemberIds: ReadonlyArray<string>;
};
export type GlobalModalsStateType = Readonly<{
addUserToAnotherGroupModalContactId?: string;
contactModalState?: ContactModalStateType;
forwardMessageProps?: ForwardMessagePropsType;
gv2MigrationProps?: MigrateToGV2PropsType;
isProfileEditorVisible: boolean;
isSignalConnectionsVisible: boolean;
isStoriesSettingsVisible: boolean;
@ -34,7 +48,6 @@ export type GlobalModalsStateType = Readonly<{
profileEditorHasError: boolean;
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
safetyNumberModalContactId?: string;
addUserToAnotherGroupModalContactId?: string;
userNotFoundModalState?: UserNotFoundModalStateType;
}>;
@ -60,6 +73,8 @@ const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL';
export const SHOW_SEND_ANYWAY_DIALOG = 'globalModals/SHOW_SEND_ANYWAY_DIALOG';
const HIDE_SEND_ANYWAY_DIALOG = 'globalModals/HIDE_SEND_ANYWAY_DIALOG';
const SHOW_GV2_MIGRATION_DIALOG = 'globalModals/SHOW_GV2_MIGRATION_DIALOG';
const CLOSE_GV2_MIGRATION_DIALOG = 'globalModals/CLOSE_GV2_MIGRATION_DIALOG';
export type ContactModalStateType = {
contactId: string;
@ -137,6 +152,15 @@ type HideStoriesSettingsActionType = {
type: typeof HIDE_STORIES_SETTINGS;
};
type StartMigrationToGV2ActionType = {
type: typeof SHOW_GV2_MIGRATION_DIALOG;
payload: MigrateToGV2PropsType;
};
type CloseGV2MigrationDialogActionType = {
type: typeof CLOSE_GV2_MIGRATION_DIALOG;
};
export type ShowSendAnywayDialogActionType = {
type: typeof SHOW_SEND_ANYWAY_DIALOG;
payload: SafetyNumberChangedBlockingDataType & {
@ -149,6 +173,8 @@ type HideSendAnywayDialogActiontype = {
};
export type GlobalModalsActionType =
| StartMigrationToGV2ActionType
| CloseGV2MigrationDialogActionType
| HideContactModalActionType
| ShowContactModalActionType
| HideWhatsNewModalActionType
@ -185,6 +211,8 @@ export const actions = {
toggleSafetyNumberModal,
toggleAddUserToAnotherGroupModal,
toggleSignalConnectionsModal,
showGV2MigrationDialog,
closeGV2MigrationDialog,
};
export const useGlobalModalActions = (): typeof actions =>
@ -244,6 +272,57 @@ function showStoriesSettings(): ShowStoriesSettingsActionType {
return { type: SHOW_STORIES_SETTINGS };
}
function showGV2MigrationDialog(
conversationId: string
): ThunkAction<void, RootStateType, unknown, StartMigrationToGV2ActionType> {
return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error(
'showGV2MigrationDialog: Expected a conversation to be found. Doing nothing'
);
}
const idForLogging = conversation.idForLogging();
if (!isGroupV1(conversation.attributes)) {
throw new Error(
`showGV2MigrationDialog/${idForLogging}: Cannot start, not a GroupV1 group`
);
}
// Note: this call will throw if, after generating member lists, we are no longer a
// member or are in the pending member list.
const { droppedGV2MemberIds, pendingMembersV2 } =
await longRunningTaskWrapper({
idForLogging,
name: 'getGroupMigrationMembers',
task: () => getGroupMigrationMembers(conversation),
});
const invitedMemberIds = pendingMembersV2.map(
(item: GroupV2PendingMemberType) => item.uuid
);
dispatch({
type: SHOW_GV2_MIGRATION_DIALOG,
payload: {
areWeInvited: false,
conversationId,
droppedMemberIds: droppedGV2MemberIds,
hasMigrated: false,
invitedMemberIds,
},
});
};
}
function closeGV2MigrationDialog(): CloseGV2MigrationDialogActionType {
return {
type: CLOSE_GV2_MIGRATION_DIALOG,
};
}
function toggleForwardMessageModal(
messageId?: string
): ThunkAction<

View file

@ -1,21 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Provider } from 'react-redux';
import type { Store } from 'redux';
import type { PropsType } from '../smart/GroupV1MigrationDialog';
import { SmartGroupV1MigrationDialog } from '../smart/GroupV1MigrationDialog';
export const createGroupV1MigrationModal = (
store: Store,
props: PropsType
): React.ReactElement => {
return (
<Provider store={store}>
<SmartGroupV1MigrationDialog {...props} />
</Provider>
);
};

View file

@ -1121,6 +1121,7 @@ function getPropsForGroupV1Migration(
return {
areWeInvited: false,
conversationId: message.conversationId,
droppedMembers,
invitedMembers,
};
@ -1140,6 +1141,7 @@ function getPropsForGroupV1Migration(
return {
areWeInvited,
conversationId: message.conversationId,
droppedMembers,
invitedMembers,
};

View file

@ -30,7 +30,6 @@ export type PropsType = {
| 'onPickSticker'
| 'onSelectMediaQuality'
| 'onSendMessage'
| 'onStartGroupMigration'
| 'onTextTooLong'
| 'openConversation'
>;

View file

@ -3,7 +3,7 @@
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsType as GroupV1MigrationDialogPropsType } from '../../components/GroupV1MigrationDialog';
import type { DataPropsType as GroupV1MigrationDialogPropsType } from '../../components/GroupV1MigrationDialog';
import { GroupV1MigrationDialog } from '../../components/GroupV1MigrationDialog';
import type { ConversationType } from '../ducks/conversations';
import type { StateType } from '../reducer';

View file

@ -16,21 +16,14 @@ import * as Errors from '../types/errors';
import type { DraftBodyRangesType } from '../types/Util';
import type { MIMEType } from '../types/MIME';
import type { ConversationModel } from '../models/conversations';
import type {
GroupV2PendingMemberType,
MessageAttributesType,
} from '../model-types.d';
import type { MessageAttributesType } from '../model-types.d';
import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem';
import { getMessageById } from '../messages/getMessageById';
import { getContactId } from '../messages/helpers';
import { strictAssert } from '../util/assert';
import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend';
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
import {
isDirectConversation,
isGroup,
isGroupV1,
} from '../util/whatTypeOfConversation';
import { isDirectConversation, isGroup } from '../util/whatTypeOfConversation';
import { findAndFormatContact } from '../util/findAndFormatContact';
import { getPreferredBadgeSelector } from '../state/selectors/badges';
import {
@ -195,7 +188,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
private contactModalView?: Backbone.View;
private conversationView?: Backbone.View;
private lightboxView?: ReactWrapperView;
private migrationDialog?: Backbone.View;
private stickerPreviewModalView?: Backbone.View;
// Panel support
@ -450,7 +442,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
getQuotedMessage: () => this.model.get('quotedMessageId'),
clearQuotedMessage: () => this.setQuoteMessage(undefined),
onStartGroupMigration: () => this.startMigrationToGV2(),
onCancelJoinRequest: async () => {
await window.showConfirmationDialog({
dialogName: 'GroupV2CancelRequestToJoin',
@ -686,62 +677,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.model.loadAndScroll(messageId);
}
async startMigrationToGV2(): Promise<void> {
const logId = this.model.idForLogging();
if (!isGroupV1(this.model.attributes)) {
throw new Error(
`startMigrationToGV2/${logId}: Cannot start, not a GroupV1 group`
);
}
const onClose = () => {
if (this.migrationDialog) {
this.migrationDialog.remove();
this.migrationDialog = undefined;
}
};
onClose();
const migrate = () => {
onClose();
longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'initiateMigrationToGroupV2',
task: () => window.Signal.Groups.initiateMigrationToGroupV2(this.model),
});
};
// Note: this call will throw if, after generating member lists, we are no longer a
// member or are in the pending member list.
const { droppedGV2MemberIds, pendingMembersV2 } =
await longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'getGroupMigrationMembers',
task: () => window.Signal.Groups.getGroupMigrationMembers(this.model),
});
const invitedMemberIds = pendingMembersV2.map(
(item: GroupV2PendingMemberType) => item.uuid
);
this.migrationDialog = new ReactWrapperView({
className: 'group-v1-migration-wrapper',
JSX: window.Signal.State.Roots.createGroupV1MigrationModal(
window.reduxStore,
{
areWeInvited: false,
droppedMemberIds: droppedGV2MemberIds,
hasMigrated: false,
invitedMemberIds,
migrate,
onClose,
}
),
});
}
unload(reason: string): void {
log.info(
'unloading conversation',

2
ts/window.d.ts vendored
View file

@ -40,7 +40,6 @@ import type { createApp } from './state/roots/createApp';
import type { createChatColorPicker } from './state/roots/createChatColorPicker';
import type { createConversationDetails } from './state/roots/createConversationDetails';
import type { createGroupLinkManagement } from './state/roots/createGroupLinkManagement';
import type { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal';
import type { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import type { createGroupV2Permissions } from './state/roots/createGroupV2Permissions';
import type { createMessageDetail } from './state/roots/createMessageDetail';
@ -172,7 +171,6 @@ export type SignalCoreType = {
createChatColorPicker: typeof createChatColorPicker;
createConversationDetails: typeof createConversationDetails;
createGroupLinkManagement: typeof createGroupLinkManagement;
createGroupV1MigrationModal: typeof createGroupV1MigrationModal;
createGroupV2JoinModal: typeof createGroupV2JoinModal;
createGroupV2Permissions: typeof createGroupV2Permissions;
createMessageDetail: typeof createMessageDetail;