diff --git a/ts/components/conversation/ConversationView.tsx b/ts/components/conversation/ConversationView.tsx index eb22801e3..e44c5ae49 100644 --- a/ts/components/conversation/ConversationView.tsx +++ b/ts/components/conversation/ConversationView.tsx @@ -12,6 +12,7 @@ export type PropsType = { renderCompositionArea: () => JSX.Element; renderConversationHeader: () => JSX.Element; renderTimeline: () => JSX.Element; + renderPanel: () => JSX.Element | undefined; }; export function ConversationView({ @@ -20,6 +21,7 @@ export function ConversationView({ renderCompositionArea, renderConversationHeader, renderTimeline, + renderPanel, }: PropsType): JSX.Element { const onDrop = React.useCallback( (event: React.DragEvent) => { @@ -93,6 +95,7 @@ export function ConversationView({ {renderCompositionArea()} + {renderPanel()} ); } diff --git a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx index 721a80f8c..76a4d3bce 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx @@ -80,7 +80,7 @@ const createProps = ( setDisappearingMessages: action('setDisappearingMessages'), showAllMedia: action('showAllMedia'), showContactModal: action('showContactModal'), - showChatColorEditor: action('showChatColorEditor'), + pushPanelForConversation: action('pushPanelForConversation'), showGroupLinkManagement: action('showGroupLinkManagement'), showGroupV2Permissions: action('showGroupV2Permissions'), showConversationNotificationsSettings: action( diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index 1accd88c9..bf883eec1 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -8,6 +8,7 @@ import { Button, ButtonIconType, ButtonVariant } from '../../Button'; import { Tooltip } from '../../Tooltip'; import type { ConversationType, + PushPanelForConversationActionType, ShowConversationType, } from '../../../state/ducks/conversations'; import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges'; @@ -50,6 +51,7 @@ import type { } from '../../../types/Avatar'; import { isConversationMuted } from '../../../util/isConversationMuted'; import { ConversationDetailsGroups } from './ConversationDetailsGroups'; +import { PanelType } from '../../../types/Panels'; enum ModalState { NothingOpen, @@ -80,7 +82,6 @@ export type StateProps = { pendingApprovalMemberships: ReadonlyArray; pendingMemberships: ReadonlyArray; showAllMedia: () => void; - showChatColorEditor: () => void; showGroupLinkManagement: () => void; showGroupV2Permissions: () => void; showPendingInvites: () => void; @@ -110,6 +111,7 @@ type ActionProps = { loadRecentMediaItems: (id: string, limit: number) => void; onOutgoingAudioCallInConversation: (conversationId: string) => unknown; onOutgoingVideoCallInConversation: (conversationId: string) => unknown; + pushPanelForConversation: PushPanelForConversationActionType; replaceAvatar: ReplaceAvatarActionType; saveAvatarToDisk: SaveAvatarToDiskActionType; searchInConversation: (id: string) => unknown; @@ -149,6 +151,7 @@ export function ConversationDetails({ onOutgoingVideoCallInConversation, pendingApprovalMemberships, pendingMemberships, + pushPanelForConversation, renderChooseGroupMembersModal, renderConfirmAdditionsModal, replaceAvatar, @@ -157,7 +160,6 @@ export function ConversationDetails({ setDisappearingMessages, setMuteExpiration, showAllMedia, - showChatColorEditor, showContactModal, showConversationNotificationsSettings, showConversation, @@ -426,7 +428,11 @@ export function ConversationDetails({ /> } label={i18n('showChatColorEditor')} - onClick={showChatColorEditor} + onClick={() => { + pushPanelForConversation(conversation.id, { + type: PanelType.ChatColorEditor, + }); + }} right={
; showArchived: boolean; composer?: ComposerStateType; contactSpoofingReview?: ContactSpoofingReviewStateType; @@ -457,7 +457,8 @@ const DISCARD_MESSAGES = 'conversations/DISCARD_MESSAGES'; const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS'; export const SELECTED_CONVERSATION_CHANGED = 'conversations/SELECTED_CONVERSATION_CHANGED'; - +const PUSH_PANEL = 'conversations/PUSH_PANEL'; +const POP_PANEL = 'conversations/POP_PANEL'; export const SET_VOICE_NOTE_PLAYBACK_RATE = 'conversations/SET_VOICE_NOTE_PLAYBACK_RATE'; @@ -678,14 +679,6 @@ export type SetIsNearBottomActionType = { isNearBottom: boolean; }; }; -export type SetConversationHeaderTitleActionType = { - type: 'SET_CONVERSATION_HEADER_TITLE'; - payload: { title?: string }; -}; -export type SetSelectedConversationPanelDepthActionType = { - type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH'; - payload: { panelDepth: number }; -}; export type ScrollToMessageActionType = { type: 'SCROLL_TO_MESSAGE'; payload: { @@ -781,6 +774,14 @@ export type ToggleConversationInChooseMembersActionType = { maxGroupSize: number; }; }; +type PushPanelActionType = { + type: typeof PUSH_PANEL; + payload: PanelRenderType; +}; +type PopPanelActionType = { + type: typeof POP_PANEL; + payload: null; +}; type ReplaceAvatarsActionType = { type: typeof REPLACE_AVATARS; @@ -822,6 +823,8 @@ export type ConversationActionType = | MessageSelectedActionType | MessagesAddedActionType | MessagesResetActionType + | PopPanelActionType + | PushPanelActionType | RemoveAllConversationsActionType | RepairNewestMessageActionType | RepairOldestMessageActionType @@ -834,13 +837,11 @@ export type ConversationActionType = | SetComposeGroupExpireTimerActionType | SetComposeGroupNameActionType | SetComposeSearchTermActionType - | SetConversationHeaderTitleActionType | SetIsFetchingUUIDActionType | SetIsNearBottomActionType | SetMessageLoadingStateActionType | SetPreJoinConversationActionType | SetRecentMediaItemsActionType - | SetSelectedConversationPanelDepthActionType | ShowArchivedConversationsActionType | ShowChooseGroupMembersActionType | ShowInboxActionType @@ -885,14 +886,16 @@ export const actions = { discardMessages, doubleCheckMissingQuoteReference, generateNewGroupLink, - loadRecentMediaItems, initiateMigrationToGroupV2, + loadRecentMediaItems, messageChanged, messageDeleted, messageExpanded, messagesAdded, messagesReset, myProfileChanged, + popPanelForConversation, + pushPanelForConversation, removeAllConversations, removeCustomColorOnConversations, removeMemberFromGroup, @@ -924,8 +927,6 @@ export const actions = { setMuteExpiration, setPinned, setPreJoinConversation, - setSelectedConversationHeaderTitle, - setSelectedConversationPanelDepth, setVoiceNotePlaybackRate, showArchivedConversations, showChooseGroupMembers, @@ -2064,20 +2065,61 @@ function setIsFetchingUUID( }, }; } -function setSelectedConversationHeaderTitle( - title?: string -): SetConversationHeaderTitleActionType { + +export type PushPanelForConversationActionType = ( + conversationId: string, + panel: PanelRenderType +) => unknown; + +function pushPanelForConversation( + conversationId: string, + panel: PanelRenderType +): PushPanelActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error( + `addPanelToConversation: No conversation found for conversation ${conversationId}` + ); + } + + conversation.trigger('pushPanel', panel); + return { - type: 'SET_CONVERSATION_HEADER_TITLE', - payload: { title }, + type: PUSH_PANEL, + payload: panel, }; } -function setSelectedConversationPanelDepth( - panelDepth: number -): SetSelectedConversationPanelDepthActionType { - return { - type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH', - payload: { panelDepth }, + +function popPanelForConversation( + conversationId: string +): ThunkAction { + return (dispatch, getState) => { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error( + `addPanelToConversation: No conversation found for conversation ${conversationId}` + ); + } + + const { conversations } = getState(); + const { selectedConversationPanels } = conversations; + + if (!selectedConversationPanels.length) { + return; + } + + const panel = [...selectedConversationPanels].pop(); + + if (!panel) { + return; + } + + conversation.trigger('popPanel', panel); + + dispatch({ + type: POP_PANEL, + payload: null, + }); }; } @@ -2869,8 +2911,7 @@ export function getEmptyState(): ConversationsStateType { selectedMessageCounter: 0, selectedMessageSource: undefined, showArchived: false, - selectedConversationTitle: '', - selectedConversationPanelDepth: 0, + selectedConversationPanels: [], }; } @@ -3375,7 +3416,7 @@ export function reducer( return { ...omit(state, 'contactSpoofingReview'), selectedConversationId, - selectedConversationPanelDepth: 0, + selectedConversationPanels: [], messagesLookup: omit(state.messagesLookup, messageIds), messagesByConversation: omit(state.messagesByConversation, [id]), }; @@ -3423,12 +3464,6 @@ export function reducer( }, }; } - if (action.type === 'SET_SELECTED_CONVERSATION_PANEL_DEPTH') { - return { - ...state, - selectedConversationPanelDepth: action.payload.panelDepth, - }; - } if (action.type === 'MESSAGE_SELECTED') { const { messageId, conversationId } = action.payload; @@ -4180,10 +4215,24 @@ export function reducer( }; } - if (action.type === 'SET_CONVERSATION_HEADER_TITLE') { + if (action.type === PUSH_PANEL) { return { ...state, - selectedConversationTitle: action.payload.title, + selectedConversationPanels: [ + ...state.selectedConversationPanels, + action.payload, + ], + }; + } + + if (action.type === POP_PANEL) { + const { selectedConversationPanels } = state; + const nextPanels = [...selectedConversationPanels]; + nextPanels.pop(); + + return { + ...state, + selectedConversationPanels: nextPanels, }; } diff --git a/ts/state/roots/createChatColorPicker.tsx b/ts/state/roots/createChatColorPicker.tsx deleted file mode 100644 index b9dbd9188..000000000 --- a/ts/state/roots/createChatColorPicker.tsx +++ /dev/null @@ -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 { SmartChatColorPickerProps } from '../smart/ChatColorPicker'; -import { SmartChatColorPicker } from '../smart/ChatColorPicker'; - -export const createChatColorPicker = ( - store: Store, - props: SmartChatColorPickerProps -): React.ReactElement => ( - - - -); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 97f72c284..718e0f4d9 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -64,6 +64,9 @@ import * as log from '../../logging/log'; import { TimelineMessageLoadingState } from '../../util/timelineUtil'; import { isSignalConversation } from '../../util/isSignalConversation'; import { reduce } from '../../util/iterables'; +import { getConversationTitleForPanelType } from '../../util/getConversationTitleForPanelType'; +import type { ReactPanelRenderType, PanelRenderType } from '../../types/Panels'; +import { isPanelHandledByReact } from '../../types/Panels'; let placeholderContact: ConversationType; export const getPlaceholderContact = (): ConversationType => { @@ -1131,3 +1134,34 @@ export const getHideStoryConversationIds = createSelector( conversationId => conversationLookup[conversationId].hideStory ) ); + +const getTopPanel = createSelector( + getConversations, + (conversations): PanelRenderType | undefined => + conversations.selectedConversationPanels[ + conversations.selectedConversationPanels.length - 1 + ] +); + +export const getTopPanelRenderableByReact = createSelector( + getConversations, + (conversations): ReactPanelRenderType | undefined => { + const topPanel = + conversations.selectedConversationPanels[ + conversations.selectedConversationPanels.length - 1 + ]; + + if (!isPanelHandledByReact(topPanel)) { + return; + } + + return topPanel; + } +); + +export const getConversationTitle = createSelector( + getIntl, + getTopPanel, + (i18n, panel): string | undefined => + getConversationTitleForPanelType(i18n, panel?.type) +); diff --git a/ts/state/smart/ConversationDetails.tsx b/ts/state/smart/ConversationDetails.tsx index e7868b387..89d61df75 100644 --- a/ts/state/smart/ConversationDetails.tsx +++ b/ts/state/smart/ConversationDetails.tsx @@ -38,7 +38,6 @@ export type SmartConversationDetailsProps = { addMembers: (conversationIds: ReadonlyArray) => Promise; conversationId: string; showAllMedia: () => void; - showChatColorEditor: () => void; showGroupLinkManagement: () => void; showGroupV2Permissions: () => void; showConversationNotificationsSettings: () => void; diff --git a/ts/state/smart/ConversationHeader.tsx b/ts/state/smart/ConversationHeader.tsx index 97740e797..5ac0ed7b7 100644 --- a/ts/state/smart/ConversationHeader.tsx +++ b/ts/state/smart/ConversationHeader.tsx @@ -12,6 +12,7 @@ import { import { getPreferredBadgeSelector } from '../selectors/badges'; import { getConversationSelector, + getConversationTitle, isMissingRequiredProfileSharing, } from '../selectors/conversations'; import { CallMode } from '../../types/Calling'; @@ -108,14 +109,14 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => { 'unblurredAvatarPath', ]), badge: getPreferredBadgeSelector(state)(conversation.badges), - conversationTitle: state.conversations.selectedConversationTitle, + conversationTitle: getConversationTitle(state), hasStories, isMissingMandatoryProfileSharing: isMissingRequiredProfileSharing(conversation), isSMSOnly: isConversationSMSOnly(conversation), isSignalConversation: isSignalConversation(conversation), i18n: getIntl(state), - showBackButton: state.conversations.selectedConversationPanelDepth > 0, + showBackButton: state.conversations.selectedConversationPanels.length > 0, outgoingCallButtonStyle: getOutgoingCallButtonStyle(conversation, state), theme: getTheme(state), }; diff --git a/ts/state/smart/ConversationView.tsx b/ts/state/smart/ConversationView.tsx index dd3f361ee..776d8f762 100644 --- a/ts/state/smart/ConversationView.tsx +++ b/ts/state/smart/ConversationView.tsx @@ -3,15 +3,19 @@ import React from 'react'; import { connect } from 'react-redux'; -import { mapDispatchToProps } from '../actions'; -import { ConversationView } from '../../components/conversation/ConversationView'; -import type { StateType } from '../reducer'; import type { CompositionAreaPropsType } from './CompositionArea'; -import { SmartCompositionArea } from './CompositionArea'; import type { OwnProps as ConversationHeaderPropsType } from './ConversationHeader'; -import { SmartConversationHeader } from './ConversationHeader'; +import type { StateType } from '../reducer'; import type { TimelinePropsType } from './Timeline'; +import * as log from '../../logging/log'; +import { ConversationView } from '../../components/conversation/ConversationView'; +import { PanelType } from '../../types/Panels'; +import { SmartChatColorPicker } from './ChatColorPicker'; +import { SmartCompositionArea } from './CompositionArea'; +import { SmartConversationHeader } from './ConversationHeader'; import { SmartTimeline } from './Timeline'; +import { getTopPanelRenderableByReact } from '../selectors/conversations'; +import { mapDispatchToProps } from '../actions'; export type PropsType = { conversationId: string; @@ -31,7 +35,7 @@ export type PropsType = { timelineProps: TimelinePropsType; }; -const mapStateToProps = (_state: StateType, props: PropsType) => { +const mapStateToProps = (state: StateType, props: PropsType) => { const { compositionAreaProps, conversationHeaderProps, @@ -39,6 +43,8 @@ const mapStateToProps = (_state: StateType, props: PropsType) => { timelineProps, } = props; + const topPanel = getTopPanelRenderableByReact(state); + return { conversationId, renderCompositionArea: () => ( @@ -48,6 +54,24 @@ const mapStateToProps = (_state: StateType, props: PropsType) => { ), renderTimeline: () => , + renderPanel: () => { + if (!topPanel) { + return; + } + + if (topPanel.type === PanelType.ChatColorEditor) { + return ( +
+ +
+ ); + } + + const unknownPanelType: never = topPanel.type; + log.warn(`renderPanel: Got unexpected panel type ${unknownPanelType}`); + + return undefined; + }, }; }; diff --git a/ts/types/Panels.ts b/ts/types/Panels.ts new file mode 100644 index 000000000..ad0e2a801 --- /dev/null +++ b/ts/types/Panels.ts @@ -0,0 +1,54 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { EmbeddedContactType } from './EmbeddedContact'; +import type { UUIDStringType } from './UUID'; + +export enum PanelType { + AllMedia = 'AllMedia', + ChatColorEditor = 'ChatColorEditor', + ContactDetails = 'ContactDetails', + ConversationDetails = 'ConversationDetails', + GroupInvites = 'GroupInvites', + GroupLinkManagement = 'GroupLinkManagement', + GroupPermissions = 'GroupPermissions', + GroupV1Members = 'GroupV1Members', + MessageDetails = 'MessageDetails', + NotificationSettings = 'NotificationSettings', + StickerManager = 'StickerManager', +} + +export type ReactPanelRenderType = { type: PanelType.ChatColorEditor }; + +export type BackbonePanelRenderType = + | { type: PanelType.AllMedia } + | { + type: PanelType.ContactDetails; + args: { + contact: EmbeddedContactType; + signalAccount?: { + phoneNumber: string; + uuid: UUIDStringType; + }; + }; + } + | { type: PanelType.ConversationDetails } + | { type: PanelType.GroupInvites } + | { type: PanelType.GroupLinkManagement } + | { type: PanelType.GroupPermissions } + | { type: PanelType.GroupV1Members } + | { type: PanelType.MessageDetails; args: { messageId: string } } + | { type: PanelType.NotificationSettings } + | { type: PanelType.StickerManager }; + +export type PanelRenderType = ReactPanelRenderType | BackbonePanelRenderType; + +export function isPanelHandledByReact( + panel: PanelRenderType +): panel is ReactPanelRenderType { + if (!panel) { + return false; + } + + return panel.type === PanelType.ChatColorEditor; +} diff --git a/ts/util/getConversationTitleForPanelType.ts b/ts/util/getConversationTitleForPanelType.ts new file mode 100644 index 000000000..cd5b7335c --- /dev/null +++ b/ts/util/getConversationTitleForPanelType.ts @@ -0,0 +1,59 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { LocalizerType } from '../types/Util'; +import * as log from '../logging/log'; +import { PanelType } from '../types/Panels'; + +export function getConversationTitleForPanelType( + i18n: LocalizerType, + panelType: PanelType | undefined +): string | undefined { + if (!panelType) { + return undefined; + } + + if (panelType === PanelType.AllMedia) { + return i18n('allMedia'); + } + + if (panelType === PanelType.ChatColorEditor) { + return i18n('ChatColorPicker__menu-title'); + } + + if (panelType === PanelType.ConversationDetails) { + return ''; + } + + if (panelType === PanelType.GroupInvites) { + return i18n('ConversationDetails--requests-and-invites'); + } + + if (panelType === PanelType.GroupLinkManagement) { + return i18n('ConversationDetails--group-link'); + } + + if (panelType === PanelType.GroupPermissions) { + return i18n('permissions'); + } + + if (panelType === PanelType.NotificationSettings) { + return i18n('ConversationDetails--notifications'); + } + + if ( + panelType === PanelType.ContactDetails || + panelType === PanelType.GroupV1Members || + panelType === PanelType.MessageDetails || + panelType === PanelType.StickerManager + ) { + return undefined; + } + + const unknownType: never = panelType; + log.warn( + `getConversationTitleForPanelType: Got unexpected type ${unknownType}` + ); + + return undefined; +} diff --git a/ts/views/conversation_view.tsx b/ts/views/conversation_view.tsx index b824017d0..ca8d913d4 100644 --- a/ts/views/conversation_view.tsx +++ b/ts/views/conversation_view.tsx @@ -56,16 +56,18 @@ import { SECOND } from '../util/durations'; import { startConversation } from '../util/startConversation'; import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; import { hasDraftAttachments } from '../util/hasDraftAttachments'; +import type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels'; +import { PanelType, isPanelHandledByReact } from '../types/Panels'; type AttachmentOptions = { messageId: string; attachment: AttachmentType; }; -type PanelType = { view: Backbone.View; headerTitle?: string }; - const { Message } = window.Signal.Types; +type BackbonePanelType = { panelType: PanelType; view: Backbone.View }; + const { getAbsoluteAttachmentPath, upgradeMessageSchema } = window.Signal.Migrations; @@ -125,7 +127,7 @@ export class ConversationView extends window.Backbone.View { private stickerPreviewModalView?: Backbone.View; // Panel support - private panels: Array = []; + private panels: Array = []; private previousFocus?: HTMLElement; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -143,7 +145,9 @@ export class ConversationView extends window.Backbone.View { // These are triggered by background.ts for keyboard handling this.listenTo(this.model, 'open-all-media', this.showAllMedia); - this.listenTo(this.model, 'escape-pressed', this.resetPanel); + this.listenTo(this.model, 'escape-pressed', () => { + window.reduxActions.conversations.popPanelForConversation(this.model.id); + }); this.listenTo(this.model, 'show-message-details', this.showMessageDetail); this.listenTo(this.model, 'delete-message', this.deleteMessage); this.listenTo(this.model, 'remove-link-review', removeLinkPreview); @@ -157,6 +161,9 @@ export class ConversationView extends window.Backbone.View { this.setupConversationView(); this.updateAttachmentsView(); + + this.listenTo(this.model, 'pushPanel', this.pushPanel); + this.listenTo(this.model, 'popPanel', this.popPanel); } override events(): Record { @@ -212,7 +219,9 @@ export class ConversationView extends window.Backbone.View { this.showGV1Members(); }, onGoBack: () => { - this.resetPanel(); + window.reduxActions.conversations.popPanelForConversation( + this.model.id + ); }, onArchive: () => { @@ -237,7 +246,6 @@ export class ConversationView extends window.Backbone.View { showToast(ToastConversationUnarchived); }, }; - window.reduxActions.conversations.setSelectedConversationHeaderTitle(); // setupTimeline @@ -544,7 +552,6 @@ export class ConversationView extends window.Backbone.View { const panel = this.panels[i]; panel.view.remove(); } - window.reduxActions.conversations.setSelectedConversationPanelDepth(0); } removeLinkPreview(); @@ -624,6 +631,12 @@ export class ConversationView extends window.Backbone.View { } showAllMedia(): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.AllMedia, + }); + } + + getAllMedia(): Backbone.View | undefined { if (document.querySelectorAll('.module-media-gallery').length) { return; } @@ -807,19 +820,24 @@ export class ConversationView extends window.Backbone.View { unsubscribe(); }, }); - const headerTitle = window.i18n('allMedia'); const update = async () => { const props = await getProps(); view.update(); }; - this.addPanel({ view, headerTitle }); - update(); + + return view; } showGV1Members(): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.GroupV1Members, + }); + } + + getGV1Members(): Backbone.View { const { contactCollection, id } = this.model; const memberships = @@ -855,8 +873,9 @@ export class ConversationView extends window.Backbone.View { ), }); - this.addPanel({ view }); view.render(); + + return view; } deleteMessage(messageId: string): void { @@ -877,12 +896,20 @@ export class ConversationView extends window.Backbone.View { } else { this.model.decrementMessageCount(); } - this.resetPanel(); + window.reduxActions.conversations.popPanelForConversation( + this.model.id + ); }, }); } showGroupLinkManagement(): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.GroupLinkManagement, + }); + } + + getGroupLinkManagement(): Backbone.View { const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createGroupLinkManagement( @@ -892,13 +919,19 @@ export class ConversationView extends window.Backbone.View { } ), }); - const headerTitle = window.i18n('ConversationDetails--group-link'); - this.addPanel({ view, headerTitle }); view.render(); + + return view; } showGroupV2Permissions(): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.GroupPermissions, + }); + } + + getGroupV2Permissions(): Backbone.View { const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createGroupV2Permissions( @@ -908,13 +941,19 @@ export class ConversationView extends window.Backbone.View { } ), }); - const headerTitle = window.i18n('permissions'); - this.addPanel({ view, headerTitle }); view.render(); + + return view; } showPendingInvites(): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.GroupInvites, + }); + } + + getPendingInvites(): Backbone.View { const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createPendingInvites(window.reduxStore, { @@ -922,15 +961,19 @@ export class ConversationView extends window.Backbone.View { ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), }), }); - const headerTitle = window.i18n( - 'ConversationDetails--requests-and-invites' - ); - this.addPanel({ view, headerTitle }); view.render(); + + return view; } showConversationNotificationsSettings(): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.NotificationSettings, + }); + } + + getConversationNotificationsSettings(): Backbone.View { const view = new ReactWrapperView({ className: 'panel', JSX: window.Signal.State.Roots.createConversationNotificationsSettings( @@ -940,26 +983,19 @@ export class ConversationView extends window.Backbone.View { } ), }); - const headerTitle = window.i18n('ConversationDetails--notifications'); - this.addPanel({ view, headerTitle }); view.render(); - } - showChatColorEditor(): void { - const view = new ReactWrapperView({ - className: 'panel', - JSX: window.Signal.State.Roots.createChatColorPicker(window.reduxStore, { - conversationId: this.model.get('id'), - }), - }); - const headerTitle = window.i18n('ChatColorPicker__menu-title'); - - this.addPanel({ view, headerTitle }); - view.render(); + 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) { @@ -981,7 +1017,6 @@ export class ConversationView extends window.Backbone.View { addMembers: this.model.addMembersV2.bind(this.model), conversationId: this.model.get('id'), showAllMedia: this.showAllMedia.bind(this), - showChatColorEditor: this.showChatColorEditor.bind(this), showGroupLinkManagement: this.showGroupLinkManagement.bind(this), showGroupV2Permissions: this.showGroupV2Permissions.bind(this), showConversationNotificationsSettings: @@ -1000,13 +1035,24 @@ export class ConversationView extends window.Backbone.View { props ), }); - const headerTitle = ''; - this.addPanel({ view, headerTitle }); view.render(); + + return view; } showMessageDetail(messageId: string): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.MessageDetails, + args: { messageId }, + }); + } + + getMessageDetail({ + messageId, + }: { + messageId: string; + }): Backbone.View | undefined { const message = window.MessageController.getById(messageId); if (!message) { throw new Error(`showMessageDetail: Message ${messageId} missing!`); @@ -1025,7 +1071,7 @@ export class ConversationView extends window.Backbone.View { const onClose = () => { this.stopListening(message, 'change', update); - this.resetPanel(); + window.reduxActions.conversations.popPanelForConversation(this.model.id); }; const view = new ReactWrapperView({ @@ -1048,21 +1094,31 @@ export class ConversationView extends window.Backbone.View { this.listenTo(message, 'expired', onClose); // We could listen to all involved contacts, but we'll call that overkill - this.addPanel({ view }); view.render(); + + return view; } showStickerManager(): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.StickerManager, + }); + } + + getStickerManager(): Backbone.View { const view = new ReactWrapperView({ className: ['sticker-manager-wrapper', 'panel'].join(' '), JSX: window.Signal.State.Roots.createStickerManager(window.reduxStore), onClose: () => { - this.resetPanel(); + window.reduxActions.conversations.popPanelForConversation( + this.model.id + ); }, }); - this.addPanel({ view }); view.render(); + + return view; } showContactDetail({ @@ -1075,6 +1131,22 @@ export class ConversationView extends window.Backbone.View { uuid: UUIDStringType; }; }): void { + window.reduxActions.conversations.pushPanelForConversation(this.model.id, { + type: PanelType.ContactDetails, + args: { contact, signalAccount }, + }); + } + + getContactDetail({ + contact, + signalAccount, + }: { + contact: EmbeddedContactType; + signalAccount?: { + phoneNumber: string; + uuid: UUIDStringType; + }; + }): Backbone.View { const view = new ReactWrapperView({ className: 'contact-detail-pane panel', JSX: ( @@ -1090,11 +1162,13 @@ export class ConversationView extends window.Backbone.View { /> ), onClose: () => { - this.resetPanel(); + window.reduxActions.conversations.popPanelForConversation( + this.model.id + ); }, }); - this.addPanel({ view }); + return view; } async openConversation( @@ -1108,32 +1182,63 @@ export class ConversationView extends window.Backbone.View { ); } - addPanel(panel: PanelType): void { + pushPanel(panel: PanelRenderType): void { + if (isPanelHandledByReact(panel)) { + return; + } + this.panels = this.panels || []; if (this.panels.length === 0) { this.previousFocus = document.activeElement as HTMLElement; } - this.panels.unshift(panel); - panel.view.$el.insertAfter(this.$('.panel').last()); - panel.view.$el.one('animationend', () => { - panel.view.$el.addClass('panel--static'); - }); + const { type } = panel as BackbonePanelRenderType; - window.reduxActions.conversations.setSelectedConversationPanelDepth( - this.panels.length - ); - window.reduxActions.conversations.setSelectedConversationHeaderTitle( - panel.headerTitle - ); - } - resetPanel(): void { - if (!this.panels || !this.panels.length) { + let view: Backbone.View | undefined; + if (type === PanelType.AllMedia) { + view = this.getAllMedia(); + } else if (panel.type === PanelType.ContactDetails) { + view = this.getContactDetail(panel.args); + } else if (type === PanelType.ConversationDetails) { + view = this.getConversationDetails(); + } else if (type === PanelType.GroupInvites) { + view = this.getPendingInvites(); + } else if (type === PanelType.GroupLinkManagement) { + view = this.getGroupLinkManagement(); + } else if (type === PanelType.GroupPermissions) { + view = this.getGroupV2Permissions(); + } else if (type === PanelType.GroupV1Members) { + view = this.getGV1Members(); + } else if (type === PanelType.NotificationSettings) { + view = this.getConversationNotificationsSettings(); + } else if (panel.type === PanelType.MessageDetails) { + view = this.getMessageDetail(panel.args); + } else if (type === PanelType.StickerManager) { + view = this.getStickerManager(); + } + + if (!view) { return; } - const panel = this.panels.shift(); + this.panels.push({ + panelType: type, + view, + }); + + view.$el.insertAfter(this.$('.panel').last()); + view.$el.one('animationend', () => { + if (view) { + view.$el.addClass('panel--static'); + } + }); + } + + popPanel(poppedPanel: PanelRenderType): void { + if (!this.panels || !this.panels.length) { + return; + } if ( this.panels.length === 0 && @@ -1144,36 +1249,42 @@ export class ConversationView extends window.Backbone.View { this.previousFocus = undefined; } + const panel = this.panels[this.panels.length - 1]; + + if (!panel) { + return; + } + + if (isPanelHandledByReact(poppedPanel)) { + return; + } + + this.panels.pop(); + + if (panel.panelType !== poppedPanel.type) { + log.warn('popPanel: last panel was not of same type'); + return; + } + if (this.panels.length > 0) { - this.panels[0].view.$el.fadeIn(250); + this.panels[this.panels.length - 1].view.$el.fadeIn(250); } - if (panel) { - let timeout: ReturnType | undefined; - const removePanel = () => { - if (!timeout) { - return; - } + let timeout: ReturnType | undefined; + const removePanel = () => { + if (!timeout) { + return; + } - clearTimeout(timeout); - timeout = undefined; + clearTimeout(timeout); + timeout = undefined; - panel.view.remove(); - }; - panel.view.$el - .addClass('panel--remove') - .one('transitionend', removePanel); + panel.view.remove(); + }; + panel.view.$el.addClass('panel--remove').one('transitionend', removePanel); - // Backup, in case things go wrong with the transitionend event - timeout = setTimeout(removePanel, SECOND); - } - - window.reduxActions.conversations.setSelectedConversationPanelDepth( - this.panels.length - ); - window.reduxActions.conversations.setSelectedConversationHeaderTitle( - this.panels[0]?.headerTitle - ); + // Backup, in case things go wrong with the transitionend event + timeout = setTimeout(removePanel, SECOND); } async clearAttachments(): Promise { diff --git a/ts/window.d.ts b/ts/window.d.ts index e348514f9..a0df487ab 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -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 { createChatColorPicker } from './state/roots/createChatColorPicker'; import type { createConversationDetails } from './state/roots/createConversationDetails'; import type { createGroupLinkManagement } from './state/roots/createGroupLinkManagement'; import type { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal'; @@ -167,7 +166,6 @@ export type SignalCoreType = { createStore: typeof createStore; Roots: { createApp: typeof createApp; - createChatColorPicker: typeof createChatColorPicker; createConversationDetails: typeof createConversationDetails; createGroupLinkManagement: typeof createGroupLinkManagement; createGroupV2JoinModal: typeof createGroupV2JoinModal;