Moves ConversationDetails to react panels

This commit is contained in:
Josh Perez 2022-12-15 22:12:05 -05:00 committed by GitHub
parent ff3ef0179b
commit d4124abb01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 220 additions and 199 deletions

View file

@ -3,7 +3,6 @@
import React from 'react';
import type { Meta, Story } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import type { Props } from './AddUserToAnotherGroupModal';
import enMessages from '../../_locales/en/messages.json';
@ -30,12 +29,8 @@ export default {
i18n: {
defaultValue: i18n,
},
addMemberToGroup: {
defaultValue: action('addMemberToGroup'),
},
toggleAddUserToAnotherGroupModal: {
defaultValue: action('toggleAddUserToAnotherGroupModal'),
},
addMembersToGroup: { action: true },
toggleAddUserToAnotherGroupModal: { action: true },
},
} as Meta;

View file

@ -32,10 +32,13 @@ type OwnProps = {
type DispatchProps = {
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
addMemberToGroup: (
addMembersToGroup: (
conversationId: string,
contactId: string,
onComplete: () => void
contactIds: Array<string>,
opts: {
onSuccess?: () => unknown;
onFailure?: () => unknown;
}
) => void;
showToast: (toastType: ToastType, parameters?: ReplacementValuesType) => void;
};
@ -47,7 +50,7 @@ export function AddUserToAnotherGroupModal({
theme,
contact,
toggleAddUserToAnotherGroupModal,
addMemberToGroup,
addMembersToGroup,
showToast,
candidateConversations,
regionCode,
@ -203,12 +206,13 @@ export function AddUserToAnotherGroupModal({
showToast(ToastType.AddingUserToGroup, {
contact: contact.title,
});
addMemberToGroup(selectedGroupId, contact.id, () =>
showToast(ToastType.UserAddedToGroup, {
contact: contact.title,
group: selectedGroup.title,
})
);
addMembersToGroup(selectedGroupId, [contact.id], {
onSuccess: () =>
showToast(ToastType.UserAddedToGroup, {
contact: contact.title,
group: selectedGroup.title,
}),
});
toggleAddUserToAnotherGroupModal(undefined);
},
},

View file

@ -37,7 +37,6 @@ const commonProps = {
i18n,
onShowConversationDetails: action('onShowConversationDetails'),
setDisappearingMessages: action('setDisappearingMessages'),
destroyMessages: action('destroyMessages'),
onSearchInConversation: action('onSearchInConversation'),

View file

@ -93,7 +93,6 @@ export type PropsActionsType = {
onOutgoingVideoCallInConversation: (conversationId: string) => void;
onSearchInConversation: () => void;
onShowAllMedia: () => void;
onShowConversationDetails: () => void;
pushPanelForConversation: PushPanelForConversationActionType;
setDisappearingMessages: (
conversationId: string,
@ -352,7 +351,6 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
onMarkUnread,
onMoveToInbox,
onShowAllMedia,
onShowConversationDetails,
pushPanelForConversation,
setDisappearingMessages,
setMuteExpiration,
@ -475,7 +473,13 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
))}
</SubMenu>
{!isGroup || hasGV2AdminEnabled ? (
<MenuItem onClick={onShowConversationDetails}>
<MenuItem
onClick={() =>
pushPanelForConversation(id, {
type: PanelType.ConversationDetails,
})
}
>
{isGroup
? i18n('showConversationDetails')
: i18n('showConversationDetails--direct')}
@ -552,8 +556,13 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
}
private renderHeader(): ReactNode {
const { conversationTitle, groupVersion, onShowConversationDetails, type } =
this.props;
const {
conversationTitle,
id,
groupVersion,
pushPanelForConversation,
type,
} = this.props;
if (conversationTitle !== undefined) {
return (
@ -571,14 +580,16 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
switch (type) {
case 'direct':
onClick = () => {
onShowConversationDetails();
pushPanelForConversation(id, { type: PanelType.ConversationDetails });
};
break;
case 'group': {
const hasGV2AdminEnabled = groupVersion === 2;
onClick = hasGV2AdminEnabled
? () => {
onShowConversationDetails();
pushPanelForConversation(id, {
type: PanelType.ConversationDetails,
});
}
: undefined;
break;

View file

@ -41,8 +41,8 @@ const createProps = (
expireTimer?: DurationInSeconds
): Props => ({
acceptConversation: action('acceptConversation'),
addMembers: async () => {
action('addMembers');
addMembersToGroup: async () => {
action('addMembersToGroup');
},
areWeASubscriber: false,
blockConversation: action('blockConversation'),
@ -57,10 +57,12 @@ const createProps = (
hasActiveCall: false,
hasGroupLink,
getPreferredBadge: () => undefined,
getProfilesForConversation: action('getProfilesForConversation'),
groupsInCommon: [],
i18n,
isAdmin: false,
isGroup: true,
leaveGroup: action('leaveGroup'),
loadRecentMediaItems: action('loadRecentMediaItems'),
memberships: times(32, i => ({
isAdmin: i === 1,
@ -78,7 +80,6 @@ const createProps = (
member: getDefaultConversation(),
})),
setDisappearingMessages: action('setDisappearingMessages'),
showAllMedia: action('showAllMedia'),
showContactModal: action('showContactModal'),
pushPanelForConversation: action('pushPanelForConversation'),
showConversation: action('showConversation'),
@ -86,7 +87,6 @@ const createProps = (
updateGroupAttributes: async () => {
action('updateGroupAttributes')();
},
onLeave: action('onLeave'),
deleteAvatarFromDisk: action('deleteAvatarFromDisk'),
replaceAvatar: action('replaceAvatar'),
saveAvatarToDisk: action('saveAvatarToDisk'),

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Button, ButtonIconType, ButtonVariant } from '../../Button';
import { Tooltip } from '../../Tooltip';
@ -63,7 +63,6 @@ enum ModalState {
}
export type StateProps = {
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
areWeASubscriber: boolean;
badges?: ReadonlyArray<BadgeType>;
canEditGroupInfo: boolean;
@ -81,15 +80,6 @@ export type StateProps = {
memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
showAllMedia: () => void;
updateGroupAttributes: (
_: Readonly<{
avatar?: undefined | Uint8Array;
description?: string;
title?: string;
}>
) => Promise<void>;
onLeave: () => void;
theme: ThemeType;
userAvatarData: Array<AvatarDataType>;
renderChooseGroupMembersModal: (
@ -102,8 +92,18 @@ export type StateProps = {
type ActionProps = {
acceptConversation: (id: string) => void;
addMembersToGroup: (
conversationId: string,
conversationIds: ReadonlyArray<string>,
opts: {
onSuccess?: () => unknown;
onFailure?: () => unknown;
}
) => unknown;
blockConversation: (id: string) => void;
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
getProfilesForConversation: (id: string) => unknown;
leaveGroup: (conversationId: string) => void;
loadRecentMediaItems: (id: string, limit: number) => void;
onOutgoingAudioCallInConversation: (conversationId: string) => unknown;
onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
@ -117,13 +117,25 @@ type ActionProps = {
showConversation: ShowConversationType;
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
toggleSafetyNumberModal: (conversationId: string) => unknown;
updateGroupAttributes: (
conversationId: string,
_: Readonly<{
avatar?: undefined | Uint8Array;
description?: string;
title?: string;
}>,
opts: {
onSuccess?: () => unknown;
onFailure?: () => unknown;
}
) => unknown;
} & Pick<ConversationDetailsMediaListPropsType, 'showLightboxWithMedia'>;
export type Props = StateProps & ActionProps;
export function ConversationDetails({
acceptConversation,
addMembers,
addMembersToGroup,
areWeASubscriber,
badges,
blockConversation,
@ -133,16 +145,17 @@ export function ConversationDetails({
deleteAvatarFromDisk,
hasGroupLink,
getPreferredBadge,
getProfilesForConversation,
groupsInCommon,
hasActiveCall,
i18n,
isAdmin,
isGroup,
leaveGroup,
loadRecentMediaItems,
memberships,
maxGroupSize,
maxRecommendedGroupSize,
onLeave,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
pendingApprovalMemberships,
@ -155,7 +168,6 @@ export function ConversationDetails({
searchInConversation,
setDisappearingMessages,
setMuteExpiration,
showAllMedia,
showContactModal,
showConversation,
showLightboxWithMedia,
@ -177,6 +189,10 @@ export function ConversationDetails({
throw new Error('ConversationDetails rendered without a conversation');
}
useEffect(() => {
getProfilesForConversation(conversation.id);
}, [conversation.id, getProfilesForConversation]);
const invitesCount =
pendingMemberships.length + pendingApprovalMemberships.length;
@ -214,15 +230,17 @@ export function ConversationDetails({
) => {
setEditGroupAttributesRequestState(RequestState.Active);
try {
await updateGroupAttributes(options);
setModalState(ModalState.NothingOpen);
setEditGroupAttributesRequestState(RequestState.Inactive);
} catch (err) {
setEditGroupAttributesRequestState(
RequestState.InactiveWithError
);
}
updateGroupAttributes(conversation.id, options, {
onSuccess: () => {
setModalState(ModalState.NothingOpen);
setEditGroupAttributesRequestState(RequestState.Inactive);
},
onFailure: () => {
setEditGroupAttributesRequestState(
RequestState.InactiveWithError
);
},
});
}}
onClose={() => {
setModalState(ModalState.NothingOpen);
@ -259,13 +277,15 @@ export function ConversationDetails({
makeRequest={async conversationIds => {
setAddGroupMembersRequestState(RequestState.Active);
try {
await addMembers(conversationIds);
setModalState(ModalState.NothingOpen);
setAddGroupMembersRequestState(RequestState.Inactive);
} catch (err) {
setAddGroupMembersRequestState(RequestState.InactiveWithError);
}
addMembersToGroup(conversation.id, conversationIds, {
onSuccess: () => {
setModalState(ModalState.NothingOpen);
setAddGroupMembersRequestState(RequestState.Inactive);
},
onFailure: () => {
setAddGroupMembersRequestState(RequestState.InactiveWithError);
},
});
}}
maxGroupSize={maxGroupSize}
maxRecommendedGroupSize={maxRecommendedGroupSize}
@ -545,7 +565,11 @@ export function ConversationDetails({
conversation={conversation}
i18n={i18n}
loadRecentMediaItems={loadRecentMediaItems}
showAllMedia={showAllMedia}
showAllMedia={() =>
pushPanelForConversation(conversation.id, {
type: PanelType.AllMedia,
})
}
showLightboxWithMedia={showLightboxWithMedia}
/>
@ -570,7 +594,7 @@ export function ConversationDetails({
isBlocked={Boolean(conversation.isBlocked)}
isGroup={isGroup}
left={Boolean(conversation.left)}
onLeave={onLeave}
onLeave={() => leaveGroup(conversation.id)}
/>
)}

View file

@ -2416,43 +2416,6 @@ export class ConversationModel extends window.Backbone
});
}
async addMembersV2(conversationIds: ReadonlyArray<string>): Promise<void> {
await this.modifyGroupV2({
name: 'addMembersV2',
usingCredentialsFrom: conversationIds
.map(id => window.ConversationController.get(id))
.filter(isNotNil),
createGroupChange: () =>
window.Signal.Groups.buildAddMembersChange(
this.attributes,
conversationIds
),
});
}
async updateGroupAttributesV2(
attributes: Readonly<{
avatar?: undefined | Uint8Array;
description?: string;
title?: string;
}>
): Promise<void> {
await this.modifyGroupV2({
name: 'updateGroupAttributesV2',
usingCredentialsFrom: [],
createGroupChange: () =>
window.Signal.Groups.buildUpdateAttributesChange(
{
id: this.id,
publicParams: this.get('publicParams'),
revision: this.get('revision'),
secretParams: this.get('secretParams'),
},
attributes
),
});
}
async leaveGroupV2(): Promise<void> {
if (!isGroupV2(this.attributes)) {
return;

View file

@ -24,7 +24,6 @@ import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
// State
import { createConversationDetails } from './state/roots/createConversationDetails';
import { createApp } from './state/roots/createApp';
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import { createMessageDetail } from './state/roots/createMessageDetail';
@ -395,7 +394,6 @@ export const setup = (options: {
const Roots = {
createApp,
createConversationDetails,
createGroupV2JoinModal,
createMessageDetail,
createSafetyNumberViewer,

View file

@ -114,11 +114,14 @@ import { addReportSpamJob } from '../../jobs/helpers/addReportSpamJob';
import { reportSpamJobQueue } from '../../jobs/reportSpamJobQueue';
import {
modifyGroupV2,
buildAddMembersChange,
buildPromotePendingAdminApprovalMemberChange,
buildUpdateAttributesChange,
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
} from '../../groups';
import { getMessageById } from '../../messages/getMessageById';
import type { PanelRenderType } from '../../types/Panels';
import { isNotNil } from '../../util/isNotNil';
// State
@ -856,7 +859,7 @@ export type ConversationActionType =
export const actions = {
acceptConversation,
addMemberToGroup,
addMembersToGroup,
approvePendingMembershipFromGroupV2,
blockAndReportSpam,
blockConversation,
@ -888,7 +891,9 @@ export const actions = {
discardMessages,
doubleCheckMissingQuoteReference,
generateNewGroupLink,
getProfilesForConversation,
initiateMigrationToGroupV2,
leaveGroup,
loadRecentMediaItems,
messageChanged,
messageDeleted,
@ -942,6 +947,7 @@ export const actions = {
toggleGroupsForStorySend,
toggleHideStories,
updateConversationModelSharedGroups,
updateGroupAttributes,
verifyConversationsStoppingSend,
};
@ -1911,6 +1917,20 @@ function selectMessage(
};
}
function getProfilesForConversation(conversationId: string): NoopActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('getProfilesForConversation: no conversation found');
}
conversation.getProfiles();
return {
type: 'NOOP',
payload: null,
};
}
function conversationStoppedByMissingVerification(payload: {
conversationId: string;
distributionId?: string;
@ -2808,22 +2828,102 @@ function removeMemberFromGroup(
};
}
function addMemberToGroup(
function addMembersToGroup(
conversationId: string,
contactId: string,
onComplete: () => void
contactIds: ReadonlyArray<string>,
{
onSuccess,
onFailure,
}: {
onSuccess?: () => unknown;
onFailure?: () => unknown;
} = {}
): ThunkAction<void, RootStateType, unknown, never> {
return async () => {
const conversationModel = window.ConversationController.get(conversationId);
if (conversationModel) {
const idForLogging = conversationModel.idForLogging();
await longRunningTaskWrapper({
name: 'addMemberToGroup',
idForLogging,
task: () => conversationModel.addMembersV2([contactId]),
});
onComplete();
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('addMembersToGroup: No conversation found');
}
const idForLogging = conversation.idForLogging();
try {
await longRunningTaskWrapper({
name: 'addMembersToGroup',
idForLogging,
task: () =>
modifyGroupV2({
name: 'addMembersToGroup',
conversation,
usingCredentialsFrom: contactIds
.map(id => window.ConversationController.get(id))
.filter(isNotNil),
createGroupChange: async () =>
buildAddMembersChange(conversation.attributes, contactIds),
}),
});
onSuccess?.();
} catch {
onFailure?.();
}
};
}
function updateGroupAttributes(
conversationId: string,
attributes: Readonly<{
avatar?: undefined | Uint8Array;
description?: string;
title?: string;
}>,
{
onSuccess,
onFailure,
}: {
onSuccess?: () => unknown;
onFailure?: () => unknown;
} = {}
): ThunkAction<void, RootStateType, unknown, never> {
return async () => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('updateGroupAttributes: No conversation found');
}
const { id, publicParams, revision, secretParams } =
conversation.attributes;
try {
await modifyGroupV2({
name: 'updateGroupAttributes',
conversation,
usingCredentialsFrom: [],
createGroupChange: async () =>
buildUpdateAttributesChange(
{ id, publicParams, revision, secretParams },
attributes
),
});
onSuccess?.();
} catch {
onFailure?.();
}
};
}
function leaveGroup(
conversationId: string
): ThunkAction<void, RootStateType, unknown, never> {
return async () => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('leaveGroup: No conversation found');
}
await longRunningTaskWrapper({
idForLogging: conversation.idForLogging(),
name: 'leaveGroup',
task: () => conversation.leaveGroupV2(),
});
};
}

View file

@ -1,19 +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 { SmartConversationDetailsProps } from '../smart/ConversationDetails';
import { SmartConversationDetails } from '../smart/ConversationDetails';
export const createConversationDetails = (
store: Store,
props: SmartConversationDetailsProps
): React.ReactElement => (
<Provider store={store}>
<SmartConversationDetails {...props} />
</Provider>
);

View file

@ -35,16 +35,7 @@ import {
} from '../../groups/limits';
export type SmartConversationDetailsProps = {
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
conversationId: string;
showAllMedia: () => void;
updateGroupAttributes: (
_: Readonly<{
avatar?: undefined | Uint8Array;
title?: string;
}>
) => Promise<void>;
onLeave: () => void;
};
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;

View file

@ -36,7 +36,6 @@ export type OwnProps = {
onMoveToInbox: () => void;
onSearchInConversation: () => void;
onShowAllMedia: () => void;
onShowConversationDetails: () => void;
};
const getOutgoingCallButtonStyle = (

View file

@ -14,11 +14,12 @@ import { ConversationView } from '../../components/conversation/ConversationView
import { PanelType } from '../../types/Panels';
import { SmartChatColorPicker } from './ChatColorPicker';
import { SmartCompositionArea } from './CompositionArea';
import { SmartConversationNotificationsSettings } from './ConversationNotificationsSettings';
import { SmartConversationDetails } from './ConversationDetails';
import { SmartConversationHeader } from './ConversationHeader';
import { SmartConversationNotificationsSettings } from './ConversationNotificationsSettings';
import { SmartGV1Members } from './GV1Members';
import { SmartGroupLinkManagement } from './GroupLinkManagement';
import { SmartGroupV2Permissions } from './GroupV2Permissions';
import { SmartGV1Members } from './GV1Members';
import { SmartPendingInvites } from './PendingInvites';
import { SmartStickerManager } from './StickerManager';
import { SmartTimeline } from './Timeline';
@ -102,6 +103,14 @@ export function SmartConversationView({
);
}
if (topPanel.type === PanelType.ConversationDetails) {
return (
<div className="panel conversation-details-pane">
<SmartConversationDetails conversationId={conversationId} />
</div>
);
}
if (topPanel.type === PanelType.GroupInvites) {
return (
<div className="panel">

View file

@ -30,6 +30,7 @@ export type ReactPanelRenderType =
};
};
}
| { type: PanelType.ConversationDetails }
| { type: PanelType.GroupInvites }
| { type: PanelType.GroupLinkManagement }
| { type: PanelType.GroupPermissions }
@ -39,7 +40,6 @@ export type ReactPanelRenderType =
export type BackbonePanelRenderType =
| { type: PanelType.AllMedia }
| { type: PanelType.ConversationDetails }
| { type: PanelType.MessageDetails; args: { messageId: string } };
export type PanelRenderType = ReactPanelRenderType | BackbonePanelRenderType;
@ -54,6 +54,7 @@ export function isPanelHandledByReact(
return (
panel.type === PanelType.ChatColorEditor ||
panel.type === PanelType.ContactDetails ||
panel.type === PanelType.ConversationDetails ||
panel.type === PanelType.GroupInvites ||
panel.type === PanelType.GroupLinkManagement ||
panel.type === PanelType.GroupPermissions ||

View file

@ -184,9 +184,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const { searchInConversation } = window.reduxActions.search;
searchInConversation(this.model.id);
},
onShowConversationDetails: () => {
this.showConversationDetails();
},
onShowAllMedia: () => {
this.showAllMedia();
},
@ -783,53 +780,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return view;
}
showConversationDetails(): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.ConversationDetails,
});
}
getConversationDetails(): Backbone.View {
// Run a getProfiles in case member's capabilities have changed
// Redux should cover us on the return here so no need to await this.
if (this.model.throttledGetProfiles) {
this.model.throttledGetProfiles();
}
// these methods are used in more than one place and should probably be
// dried up and hoisted to methods on ConversationView
const onLeave = () => {
longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'onLeave',
task: () => this.model.leaveGroupV2(),
});
};
const props = {
addMembers: this.model.addMembersV2.bind(this.model),
conversationId: this.model.get('id'),
showAllMedia: this.showAllMedia.bind(this),
updateGroupAttributes: this.model.updateGroupAttributesV2.bind(
this.model
),
onLeave,
};
const view = new ReactWrapperView({
className: 'conversation-details-pane panel',
JSX: window.Signal.State.Roots.createConversationDetails(
window.reduxStore,
props
),
});
view.render();
return view;
}
showMessageDetail(messageId: string): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.MessageDetails,
@ -904,8 +854,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
let view: Backbone.View | undefined;
if (type === PanelType.AllMedia) {
view = this.getAllMedia();
} else if (type === PanelType.ConversationDetails) {
view = this.getConversationDetails();
} else if (panel.type === PanelType.MessageDetails) {
view = this.getMessageDetail(panel.args);
}

2
ts/window.d.ts vendored
View file

@ -37,7 +37,6 @@ import type { ConversationController } from './ConversationController';
import type { ReduxActions } from './state/types';
import type { createStore } from './state/createStore';
import type { createApp } from './state/roots/createApp';
import type { createConversationDetails } from './state/roots/createConversationDetails';
import type { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import type { createMessageDetail } from './state/roots/createMessageDetail';
import type { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
@ -161,7 +160,6 @@ export type SignalCoreType = {
createStore: typeof createStore;
Roots: {
createApp: typeof createApp;
createConversationDetails: typeof createConversationDetails;
createGroupV2JoinModal: typeof createGroupV2JoinModal;
createMessageDetail: typeof createMessageDetail;
createSafetyNumberViewer: typeof createSafetyNumberViewer;