From 4fedbb2237646285f433d0d34ef18d303b0720a5 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:05:56 -0600 Subject: [PATCH] Move understanding of Chats tab location into Nav Co-authored-by: Scott Nonnenberg --- .storybook/preview.tsx | 11 + ts/CI/benchmarkConversationOpen.preload.ts | 6 +- ts/ConversationController.preload.ts | 7 +- ts/components/LeftPane.dom.stories.tsx | 1 + ts/components/LeftPane.dom.tsx | 3 + ts/components/NavTabs.dom.tsx | 7 + .../leftPane/LeftPaneInboxHelper.dom.tsx | 6 +- ts/hooks/useKeyboardShortcuts.dom.tsx | 2 +- ts/jobs/helpers/sendResendRequest.preload.ts | 6 +- ts/services/MessageCache.preload.ts | 6 +- .../addGlobalKeyboardShortcuts.preload.ts | 9 +- .../createExpiringEntityCleanupService.std.ts | 2 +- ts/shims/reloadSelectedConversation.dom.ts | 7 +- ts/state/ducks/audioRecorder.preload.ts | 3 +- ts/state/ducks/composer.preload.ts | 32 +- ts/state/ducks/conversations.preload.ts | 636 +++++++----------- ts/state/ducks/nav.std.ts | 264 +++++++- ts/state/ducks/search.preload.ts | 16 +- ts/state/selectors/audioPlayer.preload.ts | 2 +- ts/state/selectors/conversations.dom.ts | 124 ++-- ts/state/selectors/nav.preload.ts | 63 -- ts/state/selectors/nav.std.ts | 105 +++ ts/state/selectors/search.preload.ts | 2 +- ts/state/selectors/stories.preload.ts | 7 +- ts/state/smart/AllMedia.preload.tsx | 3 +- ts/state/smart/CallsTab.preload.tsx | 2 +- ts/state/smart/ChatsTab.preload.tsx | 4 +- ts/state/smart/CompositionArea.preload.tsx | 5 +- .../smart/CompositionRecording.preload.tsx | 2 +- .../CompositionRecordingDraft.preload.tsx | 6 +- ts/state/smart/ContactDetail.preload.tsx | 9 +- ts/state/smart/ContactName.preload.tsx | 6 +- .../smart/ConversationDetails.preload.tsx | 5 +- ts/state/smart/ConversationHeader.preload.tsx | 5 +- ts/state/smart/ConversationPanel.preload.tsx | 13 +- ts/state/smart/ConversationView.preload.tsx | 7 +- .../smart/GroupMemberLabelEditor.preload.tsx | 5 +- ts/state/smart/HeroRow.preload.tsx | 5 +- ts/state/smart/LeftPane.preload.tsx | 13 +- .../smart/LeftPaneChatFolders.preload.tsx | 2 +- ...onversationListItemContextMenu.preload.tsx | 2 +- ts/state/smart/MessageAudio.preload.tsx | 10 +- ts/state/smart/MessageDetail.preload.tsx | 230 ++++--- ts/state/smart/NavTabs.preload.tsx | 2 +- ts/state/smart/PinnedMessagesBar.preload.tsx | 7 +- .../smart/PinnedMessagesPanel.preload.tsx | 5 +- ts/state/smart/Preferences.preload.tsx | 8 +- ts/state/smart/ProfileEditor.preload.tsx | 2 +- ts/state/smart/StoriesTab.preload.tsx | 7 +- ts/state/smart/Timeline.preload.tsx | 2 +- ts/state/smart/TimelineItem.preload.tsx | 4 +- ts/state/smart/ToastManager.preload.tsx | 6 +- .../AttachmentDownloadManager_test.preload.ts | 4 +- .../state/ducks/conversations_test.preload.ts | 116 +--- ts/test-mock/storage/conflict_test.node.ts | 6 +- .../state/ducks/composer_test.preload.ts | 12 +- .../selectors/conversations_test.preload.ts | 30 - .../state/selectors/search_test.preload.ts | 19 +- ts/types/Nav.std.ts | 22 +- ts/windows/main/start.preload.ts | 16 +- 60 files changed, 1010 insertions(+), 919 deletions(-) delete mode 100644 ts/state/selectors/nav.preload.ts create mode 100644 ts/state/selectors/nav.std.ts diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index da4c2f967b..92136acd06 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -30,6 +30,7 @@ import { LocaleEmojiListSchema } from '../ts/types/emoji.std.js'; import { FunProvider } from '../ts/components/fun/FunProvider.dom.js'; import { EmojiSkinTone } from '../ts/components/fun/data/emojis.std.js'; import { MOCK_GIFS_PAGINATED_ONE_PAGE } from '../ts/components/fun/mocks.dom.js'; +import { NavTab } from '../ts/types/Nav.std.js'; import type { FunEmojiSelection } from '../ts/components/fun/panels/FunPanelEmojis.dom.js'; import type { FunGifSelection } from '../ts/components/fun/panels/FunPanelGifs.dom.js'; @@ -78,6 +79,16 @@ export const globalTypes = { const mockStore: Store = createStore( combineReducers({ calling: (state = {}) => state, + nav: ( + state = { + selectedLocation: { + tab: NavTab.Chats, + details: { + conversationId: undefined, + }, + }, + } + ) => state, conversations: ( state = { conversationLookup: {}, diff --git a/ts/CI/benchmarkConversationOpen.preload.ts b/ts/CI/benchmarkConversationOpen.preload.ts index 135b93470e..4c31e6b4ac 100644 --- a/ts/CI/benchmarkConversationOpen.preload.ts +++ b/ts/CI/benchmarkConversationOpen.preload.ts @@ -19,6 +19,7 @@ import type { MessageAttributesType } from '../model-types.d.ts'; import { createLogger } from '../logging/log.std.js'; import { postSaveUpdates } from '../util/cleanup.preload.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; +import { getSelectedConversationId } from '../state/selectors/nav.std.js'; const log = createLogger('benchmarkConversationOpen'); @@ -123,8 +124,7 @@ export async function benchmarkConversationOpen({ // eslint-disable-next-line no-param-reassign conversationId = - conversationId || - window.reduxStore.getState().conversations.selectedConversationId; + conversationId || getSelectedConversationId(window.reduxStore.getState()); strictAssert(conversationId, 'Must open a conversation for benchmarking'); @@ -224,7 +224,7 @@ async function showEmptyInbox() { window.SignalCI, 'CI not enabled; ensure this is a staging build' ); - if (!window.reduxStore.getState().conversations.selectedConversationId) { + if (!getSelectedConversationId(window.reduxStore.getState())) { return; } const promise = window.SignalCI.waitForEvent('empty-inbox:rendered', { diff --git a/ts/ConversationController.preload.ts b/ts/ConversationController.preload.ts index 5ad4ba7aba..ae05b76b4f 100644 --- a/ts/ConversationController.preload.ts +++ b/ts/ConversationController.preload.ts @@ -65,6 +65,7 @@ import type { PniString, } from './types/ServiceId.std.js'; import { itemStorage } from './textsecure/Storage.preload.js'; +import { getSelectedConversationId } from './state/selectors/nav.std.js'; const { debounce, pick, uniq, without } = lodash; @@ -1455,8 +1456,7 @@ export class ConversationController { await migrateConversationMessages(obsoleteId, currentId); if ( - window.reduxStore.getState().conversations.selectedConversationId === - obsoleteId + getSelectedConversationId(window.reduxStore.getState()) === obsoleteId ) { log.warn(`${logId}: opening new conversation`); window.reduxActions.conversations.showConversation({ @@ -1473,8 +1473,7 @@ export class ConversationController { drop(current.updateLastMessage()); if ( - window.reduxStore.getState().conversations.selectedConversationId === - current.id + getSelectedConversationId(window.reduxStore.getState()) === current.id ) { // TODO: DESKTOP-4807 drop(current.loadNewestMessages(undefined, undefined)); diff --git a/ts/components/LeftPane.dom.stories.tsx b/ts/components/LeftPane.dom.stories.tsx index e2c91f918e..8ba05b1132 100644 --- a/ts/components/LeftPane.dom.stories.tsx +++ b/ts/components/LeftPane.dom.stories.tsx @@ -359,6 +359,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => { ), selectedChatFolder: null, selectedConversationId: undefined, + selectedLocation: undefined, targetedMessageId: undefined, openUsernameReservationModal: action('openUsernameReservationModal'), saveAlerts: async () => action('saveAlerts')(), diff --git a/ts/components/LeftPane.dom.tsx b/ts/components/LeftPane.dom.tsx index 11d02618cd..63d1bd38f2 100644 --- a/ts/components/LeftPane.dom.tsx +++ b/ts/components/LeftPane.dom.tsx @@ -131,6 +131,7 @@ export type PropsType = { preferredWidthFromStorage: number; selectedChatFolder: ChatFolder | null; selectedConversationId: undefined | string; + selectedLocation: Location | undefined; targetedMessageId: undefined | string; challengeStatus: 'idle' | 'required' | 'pending'; setChallengeStatus: (status: 'idle') => void; @@ -281,6 +282,7 @@ export function LeftPane({ selectedConversationId, targetedMessageId, toggleNavTabsCollapse, + selectedLocation, setChallengeStatus, setComposeGroupAvatar, setComposeGroupExpireTimer, @@ -909,6 +911,7 @@ export function LeftPane({ helper.getEmptyViewNode({ i18n, selectedChatFolder, + selectedLocation, changeLocation, })} {!isEmpty && ( diff --git a/ts/components/NavTabs.dom.tsx b/ts/components/NavTabs.dom.tsx index 6fe8577142..cc6595038e 100644 --- a/ts/components/NavTabs.dom.tsx +++ b/ts/components/NavTabs.dom.tsx @@ -252,6 +252,13 @@ export function NavTabs({ state: ProfileEditorPage.None, }, }); + } else if (tab === NavTab.Chats) { + onChangeLocation({ + tab: NavTab.Chats, + details: { + conversationId: undefined, + }, + }); } else { onChangeLocation({ tab }); } diff --git a/ts/components/leftPane/LeftPaneInboxHelper.dom.tsx b/ts/components/leftPane/LeftPaneInboxHelper.dom.tsx index 6852210875..cc9ab76888 100644 --- a/ts/components/leftPane/LeftPaneInboxHelper.dom.tsx +++ b/ts/components/leftPane/LeftPaneInboxHelper.dom.tsx @@ -151,10 +151,12 @@ export class LeftPaneInboxHelper extends LeftPaneHelper i18n, selectedChatFolder, changeLocation, + selectedLocation, }: Readonly<{ i18n: LocalizerType; selectedChatFolder: ChatFolder | null; changeLocation: (location: Location) => void; + selectedLocation: Location | undefined; }>): ReactNode | null { if (this.getRowCount() === 0) { if (selectedChatFolder?.folderType === ChatFolderType.CUSTOM) { @@ -173,9 +175,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper page: SettingsPage.EditChatFolder, chatFolderId: selectedChatFolder.id, initChatFolderParams: null, - previousLocation: { - tab: NavTab.Chats, - }, + previousLocation: selectedLocation ?? null, }, }); }} diff --git a/ts/hooks/useKeyboardShortcuts.dom.tsx b/ts/hooks/useKeyboardShortcuts.dom.tsx index 66d6dac1d5..07ba7fa033 100644 --- a/ts/hooks/useKeyboardShortcuts.dom.tsx +++ b/ts/hooks/useKeyboardShortcuts.dom.tsx @@ -5,7 +5,7 @@ import { useCallback, useEffect } from 'react'; import lodash from 'lodash'; import { useSelector } from 'react-redux'; import * as KeyboardLayout from '../services/keyboardLayout.dom.js'; -import { getHasPanelOpen } from '../state/selectors/conversations.dom.js'; +import { getHasPanelOpen } from '../state/selectors/nav.std.js'; import { isInFullScreenCall } from '../state/selectors/calling.std.js'; import { isShowingAnyModal } from '../state/selectors/globalModals.std.js'; diff --git a/ts/jobs/helpers/sendResendRequest.preload.ts b/ts/jobs/helpers/sendResendRequest.preload.ts index eb610df271..710415f2df 100644 --- a/ts/jobs/helpers/sendResendRequest.preload.ts +++ b/ts/jobs/helpers/sendResendRequest.preload.ts @@ -27,6 +27,7 @@ import { retryPlaceholders } from '../../services/retryPlaceholders.std.js'; import type { LoggerType } from '../../types/Logging.std.js'; import { startAutomaticSessionReset } from '../../util/handleRetry.preload.js'; import * as Bytes from '../../Bytes.std.js'; +import { getSelectedConversationId } from '../../state/selectors/nav.std.js'; function failoverToLocalReset( logger: LoggerType, @@ -125,8 +126,9 @@ export async function sendResendRequest( if (contentHint === ContentHint.Resendable) { log.info('contentHint is RESENDABLE, adding placeholder'); - const state = window.reduxStore.getState(); - const selectedId = state.conversations.selectedConversationId; + const selectedId = getSelectedConversationId( + window.reduxStore.getState() + ); const wasOpened = selectedId === targetConversationId; await retryPlaceholders.add({ diff --git a/ts/services/MessageCache.preload.ts b/ts/services/MessageCache.preload.ts index 3912f03da4..4c1695ea9c 100644 --- a/ts/services/MessageCache.preload.ts +++ b/ts/services/MessageCache.preload.ts @@ -19,6 +19,7 @@ import type { MessageAttributesType } from '../model-types.d.ts'; import type { SendStateByConversationId } from '../messages/MessageSendState.std.js'; import type { StoredJob } from '../jobs/types.std.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; +import { getSelectedConversationId } from '../state/selectors/nav.std.js'; const { throttle } = lodash; @@ -136,8 +137,9 @@ export class MessageCache { const timeLastAccessed = this.#state.lastAccessedAt.get(messageId) ?? 0; const conversation = getMessageConversation(message.attributes); - const state = window.reduxStore.getState(); - const selectedId = state?.conversations?.selectedConversationId; + const selectedId = getSelectedConversationId( + window.reduxStore.getState() + ); const inActiveConversation = conversation && selectedId && conversation.id === selectedId; diff --git a/ts/services/addGlobalKeyboardShortcuts.preload.ts b/ts/services/addGlobalKeyboardShortcuts.preload.ts index 8574ad94e3..123612860c 100644 --- a/ts/services/addGlobalKeyboardShortcuts.preload.ts +++ b/ts/services/addGlobalKeyboardShortcuts.preload.ts @@ -10,6 +10,7 @@ import { matchOrQueryFocusable } from '../util/focusableSelectors.std.js'; import { getQuotedMessageSelector } from '../state/selectors/composer.preload.js'; import { removeLinkPreview } from './LinkPreview.preload.js'; import { ForwardMessagesModalType } from '../components/ForwardMessagesModal.dom.js'; +import { getSelectedConversationId } from '../state/selectors/nav.std.js'; const log = createLogger('addGlobalKeyboardShortcuts'); @@ -24,7 +25,7 @@ export function addGlobalKeyboardShortcuts(): void { const commandOrCtrl = commandKey || controlKey; const state = window.reduxStore.getState(); - const { selectedConversationId } = state.conversations; + const selectedConversationId = getSelectedConversationId(state); const conversation = window.ConversationController.get( selectedConversationId ); @@ -205,7 +206,7 @@ export function addGlobalKeyboardShortcuts(): void { // Send Escape to active conversation so it can close panels if (conversation && key === 'Escape') { - window.reduxActions.conversations.popPanelForConversation(); + window.reduxActions.nav.popPanelForConversation(); event.preventDefault(); event.stopPropagation(); return; @@ -296,7 +297,7 @@ export function addGlobalKeyboardShortcuts(): void { shiftKey && (key === 'm' || key === 'M') ) { - window.reduxActions.conversations.pushPanelForConversation({ + window.reduxActions.nav.pushPanelForConversation({ type: PanelType.AllMedia, }); event.preventDefault(); @@ -394,7 +395,7 @@ export function addGlobalKeyboardShortcuts(): void { return; } - window.reduxActions.conversations.pushPanelForConversation({ + window.reduxActions.nav.pushPanelForConversation({ type: PanelType.MessageDetails, args: { messageId: targetedMessage, diff --git a/ts/services/expiring/createExpiringEntityCleanupService.std.ts b/ts/services/expiring/createExpiringEntityCleanupService.std.ts index c58b03df3f..8bcf5e11e7 100644 --- a/ts/services/expiring/createExpiringEntityCleanupService.std.ts +++ b/ts/services/expiring/createExpiringEntityCleanupService.std.ts @@ -157,7 +157,7 @@ export function createExpiringEntityCleanupService( log.info('scheduled timer fired, running'); } catch (error: unknown) { log.warn( - 'scheduled timer was cancelled, not running', + 'scheduled timer was canceled, not running', Errors.toLogFormat(error) ); return true; diff --git a/ts/shims/reloadSelectedConversation.dom.ts b/ts/shims/reloadSelectedConversation.dom.ts index 852184560f..75b0bbe461 100644 --- a/ts/shims/reloadSelectedConversation.dom.ts +++ b/ts/shims/reloadSelectedConversation.dom.ts @@ -1,9 +1,12 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { getSelectedConversationId } from '../state/selectors/nav.std.js'; + export function reloadSelectedConversation(): void { - const { conversations } = window.reduxStore.getState(); - const { selectedConversationId } = conversations; + const selectedConversationId = getSelectedConversationId( + window.reduxStore.getState() + ); if (!selectedConversationId) { return; } diff --git a/ts/state/ducks/audioRecorder.preload.ts b/ts/state/ducks/audioRecorder.preload.ts index 86458f7dc6..d0da01212a 100644 --- a/ts/state/ducks/audioRecorder.preload.ts +++ b/ts/state/ducks/audioRecorder.preload.ts @@ -21,6 +21,7 @@ import { ErrorDialogAudioRecorderType, RecordingState, } from '../../types/AudioRecorder.std.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; const log = createLogger('audioRecorder'); @@ -155,7 +156,7 @@ export function completeRecording( const state = getState(); const isSelectedConversation = - state.conversations.selectedConversationId === conversationId; + getSelectedConversationId(state) === conversationId; if (!isSelectedConversation) { log.warn( diff --git a/ts/state/ducks/composer.preload.ts b/ts/state/ducks/composer.preload.ts index d254f3bada..d9e5eae6ab 100644 --- a/ts/state/ducks/composer.preload.ts +++ b/ts/state/ducks/composer.preload.ts @@ -78,11 +78,7 @@ import { writeDraftAttachment } from '../../util/writeDraftAttachment.preload.js import { getMessageById } from '../../messages/getMessageById.preload.js'; import { canReply, isNormalBubble } from '../selectors/message.preload.js'; import { getAuthorId } from '../../messages/sources.preload.js'; -import { - getActivePanel, - getConversationSelector, - getSelectedConversationId, -} from '../selectors/conversations.dom.js'; +import { getConversationSelector } from '../selectors/conversations.dom.js'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend.preload.js'; import { enqueuePollTerminateForSend } from '../../polls/enqueuePollTerminateForSend.preload.js'; import { useBoundActions } from '../../hooks/useBoundActions.std.js'; @@ -108,6 +104,10 @@ import { } from '../../util/GoogleChrome.std.js'; import type { StateThunk } from '../types.std.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; +import { + getActivePanel, + getSelectedConversationId, +} from '../selectors/nav.std.js'; const { debounce, isEqual } = lodash; @@ -379,7 +379,7 @@ function scrollToQuotedMessage({ return; } - if (getState().conversations.selectedConversationId !== conversationId) { + if (getSelectedConversationId(getState()) !== conversationId) { return; } @@ -441,7 +441,7 @@ function scrollToPollMessage( return; } - if (getState().conversations.selectedConversationId !== conversationId) { + if (getSelectedConversationId(getState()) !== conversationId) { return; } @@ -460,10 +460,10 @@ export function saveDraftRecordingIfNeeded(): ThunkAction< never > { return (dispatch, getState) => { - const { conversations, audioRecorder } = getState(); - const { selectedConversationId: conversationId } = conversations; + const state = getState(); + const conversationId = getSelectedConversationId(state); - if (!getIsRecording(audioRecorder) || !conversationId) { + if (!getIsRecording(state.audioRecorder) || !conversationId) { return; } @@ -915,7 +915,7 @@ export function setQuoteByMessageId( const quote = await makeQuote(message.attributes); // In case the conversation changed while we were about to set the quote - if (getState().conversations.selectedConversationId !== conversationId) { + if (getSelectedConversationId(getState()) !== conversationId) { return; } @@ -943,7 +943,7 @@ function addAttachment( const state = getState(); const isSelectedConversation = - state.conversations.selectedConversationId === conversationId; + getSelectedConversationId(state) === conversationId; const conversationComposerState = getComposerStateForConversation( state.composer, @@ -1017,7 +1017,7 @@ function addPendingAttachment( const state = getState(); const isSelectedConversation = - state.conversations.selectedConversationId === conversationId; + getSelectedConversationId(state) === conversationId; const conversationComposerState = getComposerStateForConversation( state.composer, @@ -1053,7 +1053,7 @@ export function setComposerFocus( conversationId: string ): ThunkAction { return async (dispatch, getState) => { - if (getState().conversations.selectedConversationId !== conversationId) { + if (getSelectedConversationId(getState()) !== conversationId) { return; } @@ -1149,7 +1149,7 @@ function processAttachments({ // If the call came from a conversation we are no longer in we do not // update the state. - if (getState().conversations.selectedConversationId !== conversationId) { + if (getSelectedConversationId(getState()) !== conversationId) { return; } @@ -1381,7 +1381,7 @@ export function replaceAttachments( return (dispatch, getState) => { // If the call came from a conversation we are no longer in we do not // update the state. - if (getState().conversations.selectedConversationId !== conversationId) { + if (getSelectedConversationId(getState()) !== conversationId) { return; } diff --git a/ts/state/ducks/conversations.preload.ts b/ts/state/ducks/conversations.preload.ts index 6863065293..790eb7a591 100644 --- a/ts/state/ducks/conversations.preload.ts +++ b/ts/state/ducks/conversations.preload.ts @@ -96,8 +96,6 @@ import { getMessagesByConversation, getPendingAvatarDownloadSelector, getAllConversations, - getActivePanel, - getSelectedConversationId, } from '../selectors/conversations.dom.js'; import { getIntl } from '../selectors/user.std.js'; import type { @@ -185,12 +183,7 @@ import { isWithinMaxEdits, MESSAGE_MAX_EDIT_COUNT, } from '../../util/canEditMessage.dom.js'; -import type { ChangeLocationAction } from './nav.std.js'; -import { - CHANGE_LOCATION, - changeLocation, - actions as navActions, -} from './nav.std.js'; +import { changeLocation, popPanelForConversation } from './nav.std.js'; import { NavTab, ProfileEditorPage, @@ -216,7 +209,10 @@ import { import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types.std.js'; import { markCallHistoryReadInConversation } from './callHistory.preload.js'; import type { CapabilitiesType } from '../../types/Capabilities.d.ts'; -import { actions as searchActions } from './search.preload.js'; +import { + updateSearchResultsOnConversationUpdate, + maybeRemoveReadConversations, +} from './search.preload.js'; import type { SearchActionType } from './search.preload.js'; import { getNotificationTextForMessage } from '../../util/getNotificationTextForMessage.preload.js'; import { doubleCheckMissingQuoteReference as doDoubleCheckMissingQuoteReference } from '../../util/doubleCheckMissingQuoteReference.preload.js'; @@ -249,18 +245,13 @@ import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js'; import { getPinnedMessageExpiresAt } from '../../util/pinnedMessages.std.js'; import { pinnedMessagesCleanupService } from '../../services/expiring/pinnedMessagesCleanupService.preload.js'; import { getPinnedMessageTarget } from '../../util/getPinMessageTarget.preload.js'; +import { + getActivePanel, + getSelectedConversationId, +} from '../selectors/nav.std.js'; -const { - chunk, - difference, - fromPairs, - isEqual, - omit, - orderBy, - pick, - values, - without, -} = lodash; +const { chunk, difference, fromPairs, omit, orderBy, pick, values, without } = + lodash; const log = createLogger('conversations'); @@ -521,11 +512,12 @@ export type ConversationPreloadDataType = ReadonlyDeep<{ export type MessagesResetDataType = ReadonlyDeep< ConversationPreloadDataType & { scrollToMessageId?: string; + selectedConversationId: string | undefined; } >; export type MessagesResetOptionsType = SetOptional< - MessagesResetDataType, + Omit, 'unboundedFetch' >; @@ -617,17 +609,10 @@ export type ConversationsStateType = ReadonlyDeep<{ conversationsByServiceId: ConversationLookupType; conversationsByGroupId: ConversationLookupType; conversationsByUsername: ConversationLookupType; - selectedConversationId?: string; + targetedMessage: string | undefined; targetedMessageCounter: number; targetedMessageSource: TargetedMessageSource | undefined; - targetedConversationPanels: { - isAnimating: boolean; - wasAnimated: boolean; - direction: 'push' | 'pop' | undefined; - stack: ReadonlyArray; - watermark: number; - }; lastSelectedMessage: MessageTimestamps | undefined; selectedMessageIds: ReadonlyArray | undefined; @@ -703,10 +688,6 @@ const DISCARD_MESSAGES = 'conversations/DISCARD_MESSAGES'; const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS'; export const TARGETED_CONVERSATION_CHANGED = 'conversations/TARGETED_CONVERSATION_CHANGED'; -const PUSH_PANEL = 'conversations/PUSH_PANEL'; -const POP_PANEL = 'conversations/POP_PANEL'; -const PANEL_ANIMATION_DONE = 'conversations/PANEL_ANIMATION_DONE'; -const PANEL_ANIMATION_STARTED = 'conversations/PANEL_ANIMATION_STARTED'; export const MARK_READ = 'conversations/MARK_READ'; export const MESSAGE_CHANGED = 'MESSAGE_CHANGED'; export const MESSAGE_DELETED = 'MESSAGE_DELETED'; @@ -851,7 +832,6 @@ export type MessageTargetedActionType = ReadonlyDeep<{ type: 'MESSAGE_TARGETED'; payload: { messageId: string; - conversationId: string; }; }>; export type ToggleSelectMessagesActionType = ReadonlyDeep<{ @@ -1071,28 +1051,12 @@ export type ToggleConversationInChooseMembersActionType = ReadonlyDeep<{ }; }>; -type PushPanelActionType = ReadonlyDeep<{ - type: typeof PUSH_PANEL; - payload: PanelArgsType; -}>; -type PopPanelActionType = ReadonlyDeep<{ - type: typeof POP_PANEL; - payload: null; -}>; -type PanelAnimationDoneActionType = ReadonlyDeep<{ - type: typeof PANEL_ANIMATION_DONE; - payload: null; -}>; -type PanelAnimationStartedActionType = ReadonlyDeep<{ - type: typeof PANEL_ANIMATION_STARTED; - payload: null; -}>; - type PinnedMessagesReplace = ReadonlyDeep<{ type: typeof PINNED_MESSAGES_REPLACE; payload: { conversationId: string; pinnedMessagesPreloadData: ReadonlyArray; + selectedConversationId: string | undefined; }; }>; @@ -1111,6 +1075,7 @@ export type ConsumePreloadDataActionType = ReadonlyDeep<{ type: typeof CONSUME_PRELOAD_DATA; payload: { conversationId: string; + selectedConversationId: string | undefined; }; }>; @@ -1149,10 +1114,6 @@ export type ConversationActionType = | MessageTargetedActionType | MessagesAddedActionType | MessagesResetActionType - | PanelAnimationStartedActionType - | PanelAnimationDoneActionType - | PopPanelActionType - | PushPanelActionType | PinnedMessagesReplace | RemoveAllConversationsActionType | RepairNewestMessageActionType @@ -1257,10 +1218,6 @@ export const actions = { onPinnedMessageAdd, onPinnedMessageRemove, openGiftBadge, - popPanelForConversation, - pushPanelForConversation, - panelAnimationDone, - panelAnimationStarted, removeAllConversations, removeConversation, removeCustomColorOnConversations, @@ -2024,11 +1981,10 @@ function destroyMessages( await conversation.destroyMessages({ source: 'local-delete' }); - // Deselect the conversation - if ( - getState().conversations.selectedConversationId === conversationId - ) { - showConversation({ conversationId: undefined }); + // Deselect the conversation if it's cusrrently showing + const selectedConversationId = getSelectedConversationId(getState()); + if (selectedConversationId === conversationId) { + dispatch(showConversation({ conversationId: undefined })); } // Clear search state, in case it's showing in search @@ -3009,14 +2965,58 @@ function setPreJoinConversation( function conversationsUpdated( data: Array -): ThunkAction { +): ThunkAction< + void, + RootStateType, + unknown, + | CloseContactSpoofingReviewActionType + | ConversationsUpdatedActionType + | ShowInboxActionType +> { return (dispatch, getState) => { + const state = getState(); + const selectedConversationId = getSelectedConversationId(state); for (const conversation of data) { calling.groupMembersChanged(conversation.id); } - const { conversationLookup: oldConversationLookup } = - getState().conversations; + const conversationState = state.conversations; + const { conversationLookup: oldConversationLookup } = conversationState; + + if (selectedConversationId) { + const newSelectedConversation = data.findLast( + convo => convo.id === selectedConversationId + ); + const previousSelectedConversation = + oldConversationLookup[selectedConversationId]; + + if (newSelectedConversation && previousSelectedConversation) { + // Archived -> Inbox: we go back to the normal inbox view + if ( + previousSelectedConversation.isArchived && + !newSelectedConversation.isArchived + ) { + dispatch(showInbox()); + } + // Inbox -> Archived: no conversation is selected + if ( + !previousSelectedConversation.isArchived && + newSelectedConversation.isArchived + ) { + // Note: With today's stacked conversations architecture, this can result in + // weird behavior - no selected conversation in the left pane, but a + // conversation showing in the right pane. + dispatch(showConversation({ conversationId: undefined })); + } + // Not Blocked -> Blocked: No need for contact spoofing review + if ( + !previousSelectedConversation.isBlocked && + newSelectedConversation.isBlocked + ) { + dispatch(closeContactSpoofingReview()); + } + } + } dispatch({ type: 'CONVERSATIONS_UPDATED', @@ -3026,20 +3026,23 @@ function conversationsUpdated( }); dispatch( - searchActions.updateSearchResultsOnConversationUpdate( - oldConversationLookup, - data - ) + updateSearchResultsOnConversationUpdate(oldConversationLookup, data) ); }; } -function conversationRemoved(id: string): ConversationRemovedActionType { - return { - type: 'CONVERSATION_REMOVED', - payload: { - id, - }, +function conversationRemoved( + id: string +): ThunkAction { + return dispatch => { + dispatch(onConversationClosed(id, 'removed')); + + dispatch({ + type: 'CONVERSATION_REMOVED', + payload: { + id, + }, + }); }; } @@ -3084,7 +3087,7 @@ function createGroup( ), }, }); - showConversation({ + await showConversation({ conversationId: conversation.id, switchToAssociatedView: true, })(dispatch, getState, null); @@ -3105,13 +3108,23 @@ function removeAllConversations(): RemoveAllConversationsActionType { function targetMessage( messageId: string, conversationId: string -): MessageTargetedActionType { - return { - type: 'MESSAGE_TARGETED', - payload: { - messageId, - conversationId, - }, +): ThunkAction { + return async (dispatch, getState) => { + const selectedConversationId = getSelectedConversationId(getState()); + + if (conversationId !== selectedConversationId) { + log.warn( + "targetMessage: Provided conversationId didn't match selected conversation" + ); + return; + } + + dispatch({ + type: 'MESSAGE_TARGETED', + payload: { + messageId, + }, + }); }; } @@ -3123,11 +3136,19 @@ function toggleSelectMessage( ): ThunkAction { return async (dispatch, getState) => { const state = getState(); - const { conversations } = state; + const { conversations, nav } = state; + const { selectedLocation } = nav; + + if (selectedLocation.tab !== NavTab.Chats) { + log.warn('toggleSelectMessage: Not on chats tab'); + return; + } + + const selectedConversationId = getSelectedConversationId(state); let toggledMessageIds: ReadonlyArray; if (shift && conversations.lastSelectedMessage != null) { - if (conversationId !== conversations.selectedConversationId) { + if (conversationId !== selectedConversationId) { throw new Error("toggleSelectMessage: conversationId doesn't match"); } @@ -3312,10 +3333,20 @@ function messagesAdded({ }): ThunkAction { return (dispatch, getState) => { const state = getState(); + const { nav } = state; + const { selectedLocation } = nav; + + if (selectedLocation.tab !== NavTab.Chats) { + log.warn('messagesAdded: Not on chats tab'); + return; + } + + const selectedConversationId = getSelectedConversationId(state); + if ( isNewMessage && state.items.audioMessage && - conversationId === state.conversations.selectedConversationId && + conversationId === selectedConversationId && isActive && !isJustSent && messages.some(isIncoming) @@ -3370,25 +3401,35 @@ function messagesReset({ pinnedMessagesPreloadData, scrollToMessageId, unboundedFetch, -}: MessagesResetOptionsType): MessagesResetActionType { - for (const message of messages) { - strictAssert( - message.conversationId === conversationId, - `messagesReset(${conversationId}): invalid message conversationId ` + - `${message.conversationId}` - ); - } +}: MessagesResetOptionsType): ThunkAction< + void, + RootStateType, + unknown, + MessagesResetActionType +> { + return (dispatch, getState) => { + const selectedConversationId = getSelectedConversationId(getState()); - return { - type: MESSAGES_RESET, - payload: { - unboundedFetch: unboundedFetch ?? false, - conversationId, - messages, - metrics, - pinnedMessagesPreloadData, - scrollToMessageId, - }, + for (const message of messages) { + strictAssert( + message.conversationId === conversationId, + `messagesReset(${conversationId}): invalid message conversationId ` + + `${message.conversationId}` + ); + } + + dispatch({ + type: MESSAGES_RESET, + payload: { + unboundedFetch: unboundedFetch ?? false, + conversationId, + messages, + metrics, + pinnedMessagesPreloadData, + selectedConversationId, + scrollToMessageId, + }, + }); }; } function addPreloadData( @@ -3410,12 +3451,17 @@ function addPreloadData( } function consumePreloadData( conversationId: string -): ConsumePreloadDataActionType { - return { - type: CONSUME_PRELOAD_DATA, - payload: { - conversationId, - }, +): ThunkAction { + return async (dispatch, getState) => { + const selectedConversationId = getSelectedConversationId(getState()); + + dispatch({ + type: CONSUME_PRELOAD_DATA, + payload: { + selectedConversationId, + conversationId, + }, + }); }; } function setMessageLoadingState( @@ -3481,62 +3527,6 @@ export type PushPanelForConversationActionType = ReadonlyDeep< (panel: PanelArgsType) => unknown >; -function pushPanelForConversation( - panel: PanelArgsType -): ThunkAction { - return async (dispatch, getState) => { - const { conversations } = getState(); - const { targetedConversationPanels } = conversations; - const activePanel = - targetedConversationPanels.stack[targetedConversationPanels.watermark]; - if (panel.type === activePanel?.type && isEqual(panel, activePanel)) { - return; - } - - dispatch({ - type: PUSH_PANEL, - payload: panel, - }); - }; -} - -export type PopPanelForConversationActionType = ReadonlyDeep<() => unknown>; - -function popPanelForConversation(): ThunkAction< - void, - RootStateType, - unknown, - PopPanelActionType -> { - return (dispatch, getState) => { - const { conversations } = getState(); - const { targetedConversationPanels } = conversations; - - if (!targetedConversationPanels.stack.length) { - return; - } - - dispatch({ - type: POP_PANEL, - payload: null, - }); - }; -} - -function panelAnimationStarted(): PanelAnimationStartedActionType { - return { - type: PANEL_ANIMATION_STARTED, - payload: null, - }; -} - -function panelAnimationDone(): PanelAnimationDoneActionType { - return { - type: PANEL_ANIMATION_DONE, - payload: null, - }; -} - function deleteMessagesForEveryone( messageIds: ReadonlyArray ): ThunkAction< @@ -4800,24 +4790,58 @@ function showConversation({ void, RootStateType, unknown, - TargetedConversationChangedActionType | ChangeLocationAction + TargetedConversationChangedActionType > { - return (dispatch, getState) => { - const { conversations, nav } = getState(); + return async (dispatch, getState) => { + const logId = `showConversation/${conversationId}`; + const { nav } = getState(); + const { selectedLocation: originalLocation } = nav; - if (nav.selectedLocation.tab !== NavTab.Chats) { - dispatch( - navActions.changeLocation({ - tab: NavTab.Chats, - }) - ); + window.ConversationController.get(conversationId)?.onOpenStart(); + + // Optimistically update state to prepare for this load + dispatch({ + type: TARGETED_CONVERSATION_CHANGED, + payload: { + conversationId, + messageId, + switchToAssociatedView, + }, + }); + + // Attempt to change the location - note that this might be canceled + await changeLocation({ + tab: NavTab.Chats, + details: { + conversationId, + }, + })(dispatch, getState, undefined); + + const { selectedLocation: newLocation } = getState().nav; + if ( + newLocation.tab !== NavTab.Chats || + newLocation.details.conversationId !== conversationId + ) { + log.warn(`${logId}: navigation was canceled`); + return; + } + + if (originalLocation.tab !== NavTab.Chats) { const conversation = window.ConversationController.get(conversationId); - conversation?.setMarkedUnread(false); + if (!conversation) { + log.warn(`${logId}: Conversation does not exist!`); + return; + } + + conversation.setMarkedUnread(false); } dispatch(updateChatFolderStateOnTargetConversationChanged(conversationId)); - if (conversationId === conversations.selectedConversationId) { + if ( + originalLocation.tab === NavTab.Chats && + originalLocation.details.conversationId === conversationId + ) { if (!conversationId) { return; } @@ -4831,25 +4855,19 @@ function showConversation({ } // notify composer in case we need to stop recording a voice note - if (conversations.selectedConversationId) { + if ( + originalLocation.tab === NavTab.Chats && + originalLocation.details.conversationId && + originalLocation.details.conversationId !== conversationId + ) { dispatch(saveDraftRecordingIfNeeded()); dispatch( onConversationClosed( - conversations.selectedConversationId, + originalLocation.details.conversationId, 'showConversation' ) ); } - window.ConversationController.get(conversationId)?.onOpenStart(); - - dispatch({ - type: TARGETED_CONVERSATION_CHANGED, - payload: { - conversationId, - messageId, - switchToAssociatedView, - }, - }); }; } @@ -4968,7 +4986,7 @@ function onConversationClosed( conversationId: string, reason: string ): ThunkAction { - return async dispatch => { + return async (dispatch, getState) => { const conversation = window.ConversationController.get(conversationId); // Conversation was removed due to the merge if (!conversation) { @@ -4978,6 +4996,19 @@ function onConversationClosed( } const logId = `onConversationClosed/${conversation?.idForLogging() ?? conversationId}`; + const state = getState(); + const selectedConversationId = getSelectedConversationId(state); + + // If we're still on this conversation, but we want to close it, go to splash screen + if (selectedConversationId === conversationId) { + await changeLocation({ + tab: NavTab.Chats, + details: { + conversationId: undefined, + }, + })(dispatch, getState, null); + } + log.info(`${logId}: unloading due to ${reason}`); if (conversation?.get('draftChanged')) { @@ -5013,7 +5044,7 @@ function onConversationClosed( }, }); - dispatch(searchActions.maybeRemoveReadConversations([conversationId])); + dispatch(maybeRemoveReadConversations([conversationId])); }; } @@ -5125,6 +5156,7 @@ function onPinnedMessagesChanged( payload: { conversationId, pinnedMessagesPreloadData, + selectedConversationId, }, }); }; @@ -5221,13 +5253,6 @@ export function getEmptyState(): ConversationsStateType { showArchived: false, hasContactSpoofingReview: false, pendingRequestedAvatarDownload: {}, - targetedConversationPanels: { - isAnimating: false, - wasAnimated: false, - direction: undefined, - stack: [], - watermark: -1, - }, }; } @@ -5340,45 +5365,6 @@ export function updateConversationLookups( return result; } -function updateRootStateDueToConversationUpdate( - state: ConversationsStateType, - conversation: ConversationType -): ConversationsStateType { - if (state.selectedConversationId !== conversation.id) { - return state; - } - - let { showArchived } = state; - const { selectedConversationId, conversationLookup } = state; - const existing = conversationLookup[conversation.id]; - - const keysToOmit: Array = []; - const keyValuesToAdd: { hasContactSpoofingReview?: false } = {}; - - // Archived -> Inbox: we go back to the normal inbox view - if (existing.isArchived && !conversation.isArchived) { - showArchived = false; - } - // Inbox -> Archived: no conversation is selected - // Note: With today's stacked conversations architecture, this can result in weird - // behavior - no selected conversation in the left pane, but a conversation show - // in the right pane. - if (!existing.isArchived && conversation.isArchived) { - keysToOmit.push('selectedConversationId'); - } - - if (!existing.isBlocked && conversation.isBlocked) { - keyValuesToAdd.hasContactSpoofingReview = false; - } - - return { - ...omit(state, keysToOmit), - ...keyValuesToAdd, - selectedConversationId, - showArchived, - }; -} - function closeComposerModal( state: Readonly, modalToClose: 'maximumGroupSizeModalState' | 'recommendedGroupSizeModalState' @@ -5595,6 +5581,7 @@ function updateMessageLookup( conversationId, messages, metrics, + selectedConversationId, scrollToMessageId, unboundedFetch, pinnedMessagesPreloadData, @@ -5641,7 +5628,7 @@ function updateMessageLookup( return { ...state, preloadData: undefined, - ...(state.selectedConversationId === conversationId + ...(selectedConversationId === conversationId ? { targetedMessage: scrollToMessageId, targetedMessageCounter: state.targetedMessageCounter + 1, @@ -5741,11 +5728,7 @@ function dropPreloadData( export function reducer( state: Readonly = getEmptyState(), - action: Readonly< - | ConversationActionType - | StoryDistributionListsActionType - | ChangeLocationAction - > + action: Readonly ): ConversationsStateType { if (action.type === CLEAR_CONVERSATIONS_PENDING_VERIFICATION) { return { @@ -5948,26 +5931,12 @@ export function reducer( hasProfileUpdateError: newErrorState, }; } + if (action.type === 'CONVERSATIONS_UPDATED') { const { payload } = action; const { data: conversations } = payload; const { conversationLookup } = state; - const { selectedConversationId } = state; - - const selectedConversation = conversations.find( - convo => convo.id === selectedConversationId - ); - - let updatedState = state; - - if (selectedConversation) { - updatedState = updateRootStateDueToConversationUpdate( - state, - selectedConversation - ); - } - const existingConversations = conversations .map(conversation => conversationLookup[conversation.id]) .filter(isNotNil); @@ -5978,7 +5947,7 @@ export function reducer( } return { - ...updatedState, + ...state, conversationLookup: newConversationLookup, ...updateConversationLookups(conversations, existingConversations, state), }; @@ -5989,7 +5958,6 @@ export function reducer( const { conversationLookup } = state; const existing = getOwn(conversationLookup, id); - onConversationClosed(id, 'removed'); // No need to make a change if we didn't have a record of this conversation! if (!existing) { return state; @@ -6010,22 +5978,10 @@ export function reducer( } const { messageIds } = existingConversation; - const selectedConversationId = - state.selectedConversationId !== conversationId - ? state.selectedConversationId - : undefined; return { ...state, hasContactSpoofingReview: false, - selectedConversationId, - targetedConversationPanels: { - isAnimating: false, - wasAnimated: false, - direction: undefined, - stack: [], - watermark: -1, - }, messagesLookup: maybeDropMessageIdsFromMessagesLookup( state.messagesLookup, [...messageIds], @@ -6080,11 +6036,7 @@ export function reducer( }; } if (action.type === 'MESSAGE_TARGETED') { - const { messageId, conversationId } = action.payload; - - if (state.selectedConversationId !== conversationId) { - return state; - } + const { messageId } = action.payload; return { ...state, @@ -6451,8 +6403,8 @@ export function reducer( }; } if (action.type === CONSUME_PRELOAD_DATA) { - const { preloadData, selectedConversationId } = state; - const { conversationId } = action.payload; + const { preloadData } = state; + const { conversationId, selectedConversationId } = action.payload; if (!preloadData) { return state; } @@ -6463,7 +6415,10 @@ export function reducer( return dropPreloadData(state); } - return updateMessageLookup(state, preloadData); + return updateMessageLookup(state, { + ...preloadData, + selectedConversationId, + }); } if (action.type === 'SET_MESSAGE_LOADING_STATE') { const { payload } = action; @@ -6568,10 +6523,6 @@ export function reducer( existingConversation.scrollToMessageCounter + 1, }, }, - targetedConversationPanels: { - ...state.targetedConversationPanels, - watermark: -1, - }, }; } if (action.type === MESSAGE_DELETED) { @@ -6897,14 +6848,13 @@ export function reducer( } } - const nextState = { + const nextState: ConversationsStateType = { ...state, preloadData: state.preloadData?.conversationId === conversationId ? state.preloadData : undefined, hasContactSpoofingReview: false, - selectedConversationId: conversationId, targetedMessage: messageId ?? lastCenterMessageId, targetedMessageSource: messageId ? TargetedMessageSource.NavigateToMessage @@ -6933,82 +6883,6 @@ export function reducer( }; } - if (action.type === PUSH_PANEL) { - const currentStack = state.targetedConversationPanels.stack; - const watermark = Math.min( - state.targetedConversationPanels.watermark + 1, - currentStack.length - ); - const stack = [...currentStack.slice(0, watermark), action.payload]; - - const targetedConversationPanels = { - isAnimating: false, - wasAnimated: false, - direction: 'push' as const, - stack, - watermark, - }; - - return { - ...state, - targetedConversationPanels, - }; - } - - if (action.type === POP_PANEL) { - if (state.targetedConversationPanels.watermark === -1) { - return state; - } - - const poppedPanel = - state.targetedConversationPanels.stack[ - state.targetedConversationPanels.watermark - ]; - - if (!poppedPanel) { - return state; - } - - const watermark = Math.max( - state.targetedConversationPanels.watermark - 1, - -1 - ); - - const targetedConversationPanels = { - isAnimating: false, - wasAnimated: false, - direction: 'pop' as const, - stack: state.targetedConversationPanels.stack, - watermark, - }; - - return { - ...state, - targetedConversationPanels, - }; - } - - if (action.type === PANEL_ANIMATION_STARTED) { - return { - ...state, - targetedConversationPanels: { - ...state.targetedConversationPanels, - isAnimating: true, - }, - }; - } - - if (action.type === PANEL_ANIMATION_DONE) { - return { - ...state, - targetedConversationPanels: { - ...state.targetedConversationPanels, - isAnimating: false, - wasAnimated: true, - }, - }; - } - if (action.type === 'SET_RECENT_MEDIA_ITEMS') { const { id, recentMediaItems } = action.payload; const { conversationLookup } = state; @@ -7592,32 +7466,6 @@ export function reducer( }; } - if ( - action.type === CHANGE_LOCATION && - action.payload.selectedLocation.tab === NavTab.Chats - ) { - const { messagesByConversation, selectedConversationId } = state; - if (selectedConversationId == null) { - return state; - } - - const existingConversation = messagesByConversation[selectedConversationId]; - if (existingConversation == null) { - return state; - } - - return { - ...state, - messagesByConversation: { - ...messagesByConversation, - [selectedConversationId]: { - ...existingConversation, - isNearBottom: true, - }, - }, - }; - } - if (action.type === SET_PENDING_REQUESTED_AVATAR_DOWNLOAD) { const { conversationId, value } = action.payload; @@ -7631,7 +7479,11 @@ export function reducer( } if (action.type === PINNED_MESSAGES_REPLACE) { - const { conversationId, pinnedMessagesPreloadData } = action.payload; + const { + conversationId, + pinnedMessagesPreloadData, + selectedConversationId, + } = action.payload; const extraMessagesLookup: Record = {}; const pinnedMessages: Array = []; @@ -7645,7 +7497,7 @@ export function reducer( return { ...state, messagesLookup: - state.selectedConversationId !== conversationId + selectedConversationId !== conversationId ? state.messagesLookup : { ...state.messagesLookup, diff --git a/ts/state/ducks/nav.std.ts b/ts/state/ducks/nav.std.ts index edcf590ba1..11bed629f8 100644 --- a/ts/state/ducks/nav.std.ts +++ b/ts/state/ducks/nav.std.ts @@ -1,17 +1,25 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { isEqual } from 'lodash'; + import type { ReadonlyDeep } from 'type-fest'; import type { ThunkAction } from 'redux-thunk'; import { createLogger } from '../../logging/log.std.js'; import { useBoundActions } from '../../hooks/useBoundActions.std.js'; -import { NavTab, SettingsPage } from '../../types/Nav.std.js'; import { beforeNavigateService } from '../../services/BeforeNavigate.std.js'; +import { NavTab, SettingsPage } from '../../types/Nav.std.js'; +import { + getActivePanel, + getPanels, + getSelectedLocation, +} from '../selectors/nav.std.js'; +import type { PanelArgsType } from '../../types/Panels.std.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.js'; import type { StateType as RootStateType } from '../reducer.preload.js'; -import type { Location } from '../../types/Nav.std.js'; +import type { Location, PanelInfo } from '../../types/Nav.std.js'; const log = createLogger('nav'); @@ -28,22 +36,47 @@ function printLocation(location: Location): string { return `${location.tab}`; } +function getDefaultPanels(): PanelInfo { + return { + isAnimating: false, + wasAnimated: false, + direction: undefined, + stack: [], + watermark: -1, + }; +} + // State export type NavStateType = ReadonlyDeep<{ selectedLocation: Location; + lastChatTabLocation?: Location; }>; // Actions export const CHANGE_LOCATION = 'nav/CHANGE_LOCATION'; +const PANEL_ANIMATION_DONE = 'nav/PANEL_ANIMATION_DONE'; +const PANEL_ANIMATION_STARTED = 'nav/PANEL_ANIMATION_STARTED'; export type ChangeLocationAction = ReadonlyDeep<{ type: typeof CHANGE_LOCATION; payload: { selectedLocation: Location }; }>; +type PanelAnimationDoneActionType = ReadonlyDeep<{ + type: typeof PANEL_ANIMATION_DONE; + payload: null; +}>; +type PanelAnimationStartedActionType = ReadonlyDeep<{ + type: typeof PANEL_ANIMATION_STARTED; + payload: null; +}>; -export type NavActionType = ReadonlyDeep; +export type NavActionType = ReadonlyDeep< + | ChangeLocationAction + | PanelAnimationDoneActionType + | PanelAnimationStartedActionType +>; // Action Creators @@ -72,8 +105,157 @@ export function changeLocation( }; } +function panelAnimationStarted(): PanelAnimationStartedActionType { + return { + type: PANEL_ANIMATION_STARTED, + payload: null, + }; +} + +function panelAnimationDone(): PanelAnimationDoneActionType { + return { + type: PANEL_ANIMATION_DONE, + payload: null, + }; +} + +function pushPanelForConversation( + panel: PanelArgsType +): ThunkAction { + return async (dispatch, getState) => { + const state = getState(); + const existingLocation = getSelectedLocation(state); + + const logId = `pushPanelForConversation/${panel.type}`; + + if (existingLocation.tab !== NavTab.Chats) { + log.warn(`${logId}: Not on Chats tab; on ${existingLocation.tab} tab!`); + return; + } + + const activePanel = getActivePanel(getState()); + if (panel.type === activePanel?.type && isEqual(panel, activePanel)) { + log.warn(`${logId}: Already on ${panel.type} panel!`); + return; + } + + const panels = getPanels(state) || getDefaultPanels(); + const currentStack = panels.stack; + const watermark = Math.min(panels.watermark + 1, currentStack.length); + const stack = [...currentStack.slice(0, watermark), panel]; + + const newPanels = { + isAnimating: false, + wasAnimated: false, + direction: 'push' as const, + stack, + watermark, + }; + + const newLocation = { + ...existingLocation, + details: { + ...existingLocation.details, + panels: newPanels, + }, + }; + + const needToCancel = await beforeNavigateService.shouldCancelNavigation({ + context: logId, + existingLocation, + newLocation, + }); + + if (needToCancel) { + log.info(`${logId}: Canceling navigation`); + return; + } + + dispatch({ + type: CHANGE_LOCATION, + payload: { selectedLocation: newLocation }, + }); + }; +} + +export type PopPanelForConversationActionType = ReadonlyDeep<() => unknown>; + +export function popPanelForConversation(): ThunkAction< + void, + RootStateType, + unknown, + ChangeLocationAction +> { + return async (dispatch, getState) => { + const state = getState(); + const panels = getPanels(state); + const existingLocation = getSelectedLocation(state); + + const logId = `popPanelForConversation/length=${panels?.stack.length}`; + + if (existingLocation.tab !== NavTab.Chats) { + log.warn(`${logId}: Not on Chats tab; on ${existingLocation.tab} tab!`); + return; + } + + if (!panels || panels.stack.length === 0) { + log.warn(`${logId}: No panel to pop!`); + return; + } + + if (panels.watermark === -1) { + log.warn(`${logId}: Watermark is already -1`); + return; + } + + const poppedPanel = panels.stack[panels.watermark]; + if (!poppedPanel) { + log.warn(`${logId}: No panel found at watermark=${panels.watermark}`); + return; + } + + const watermark = Math.max(panels.watermark - 1, -1); + + const newPanels = { + isAnimating: false, + wasAnimated: false, + direction: 'pop' as const, + stack: panels.stack, + watermark, + }; + + const newLocation = { + ...existingLocation, + details: { + ...existingLocation.details, + panels: newPanels, + }, + }; + + const needToCancel = await beforeNavigateService.shouldCancelNavigation({ + context: logId, + existingLocation, + newLocation, + }); + + if (needToCancel) { + log.info(`${logId}: Canceling navigation`); + return; + } + + dispatch({ + type: CHANGE_LOCATION, + payload: { selectedLocation: newLocation }, + }); + }; +} + export const actions = { changeLocation, + panelAnimationDone, + panelAnimationStarted, + popPanelForConversation, + pushPanelForConversation, }; export const useNavActions = (): BoundActionCreatorsMapObject => @@ -85,6 +267,9 @@ export function getEmptyState(): NavStateType { return { selectedLocation: { tab: NavTab.Chats, + details: { + conversationId: undefined, + }, }, }; } @@ -94,9 +279,80 @@ export function reducer( action: Readonly ): NavStateType { if (action.type === CHANGE_LOCATION) { + let { selectedLocation } = action.payload; + let { lastChatTabLocation } = state; + + // Save last Chats Tab location if switching away from Chats Tab + if ( + selectedLocation.tab !== NavTab.Chats && + state.selectedLocation.tab === NavTab.Chats + ) { + lastChatTabLocation = state.selectedLocation; + } + + // Restore last Chats Tab location if: + // - switching back to Chats Tab + // - conversationId not set + if ( + selectedLocation.tab === NavTab.Chats && + !selectedLocation.details.conversationId && + state.selectedLocation.tab !== NavTab.Chats && + state.lastChatTabLocation + ) { + selectedLocation = state.lastChatTabLocation; + } + return { ...state, - selectedLocation: action.payload.selectedLocation, + selectedLocation, + lastChatTabLocation, + }; + } + + if (action.type === PANEL_ANIMATION_STARTED) { + if (state.selectedLocation.tab !== NavTab.Chats) { + return state; + } + if (!state.selectedLocation.details.panels) { + return state; + } + + return { + ...state, + selectedLocation: { + ...state.selectedLocation, + details: { + ...state.selectedLocation.details, + panels: { + ...state.selectedLocation.details.panels, + isAnimating: true, + }, + }, + }, + }; + } + + if (action.type === PANEL_ANIMATION_DONE) { + if (state.selectedLocation.tab !== NavTab.Chats) { + return state; + } + if (!state.selectedLocation.details.panels) { + return state; + } + + return { + ...state, + selectedLocation: { + ...state.selectedLocation, + details: { + ...state.selectedLocation.details, + panels: { + ...state.selectedLocation.details.panels, + isAnimating: false, + wasAnimated: true, + }, + }, + }, }; } diff --git a/ts/state/ducks/search.preload.ts b/ts/state/ducks/search.preload.ts index b2102239a4..97c0313b03 100644 --- a/ts/state/ducks/search.preload.ts +++ b/ts/state/ducks/search.preload.ts @@ -48,6 +48,7 @@ import { searchConversationTitles } from '../../util/searchConversationTitles.st import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js'; import { isConversationSMSOnly } from '../../util/isConversationSMSOnly.std.js'; import { isConversationUnread } from '../../util/countUnreadStats.std.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; const { debounce, omit, reject } = lodash; @@ -278,7 +279,7 @@ function refreshSearch(): ThunkAction< }; } -function updateSearchResultsOnConversationUpdate( +export function updateSearchResultsOnConversationUpdate( oldConversationLookup: ConversationLookupType, updatedConversations: Array ): ThunkAction< @@ -316,7 +317,7 @@ function updateSearchResultsOnConversationUpdate( type: 'MAYBE_REMOVE_READ_CONVERSATIONS', payload: { conversations: updatedConversations, - selectedConversationId: state.conversations.selectedConversationId, + selectedConversationId: getSelectedConversationId(state), }, }); }; @@ -344,7 +345,7 @@ function shouldRemoveConversationFromUnreadList( return false; } -function maybeRemoveReadConversations( +export function maybeRemoveReadConversations( conversationIds: Array ): ThunkAction< void, @@ -353,9 +354,11 @@ function maybeRemoveReadConversations( MaybeRemoveReadConversationsActionType > { return (dispatch, getState) => { + const state = getState(); const { - conversations: { selectedConversationId, conversationLookup }, - } = getState(); + conversations: { conversationLookup }, + } = state; + const selectedConversationId = getSelectedConversationId(state); const conversations = conversationIds .map(id => conversationLookup[id]) @@ -430,8 +433,7 @@ const doSearch = debounce( const noteToSelf = i18n('icu:noteToSelf').toLowerCase(); const ourConversationId = getUserConversationId(state); const searchConversationId = getSearchConversation(state)?.id; - - const { selectedConversationId } = state.conversations; + const selectedConversationId = getSelectedConversationId(state); strictAssert(ourConversationId, 'doSearch our conversation is missing'); diff --git a/ts/state/selectors/audioPlayer.preload.ts b/ts/state/selectors/audioPlayer.preload.ts index 6993054644..604f4c9cd3 100644 --- a/ts/state/selectors/audioPlayer.preload.ts +++ b/ts/state/selectors/audioPlayer.preload.ts @@ -13,7 +13,6 @@ import { getConversationByIdSelector, getConversations, getConversationSelector, - getSelectedConversationId, } from './conversations.dom.js'; import type { StateType } from '../reducer.preload.js'; import { createLogger } from '../../logging/log.std.js'; @@ -24,6 +23,7 @@ import * as Attachment from '../../util/Attachment.std.js'; import type { ActiveAudioPlayerStateType } from '../ducks/audioPlayer.preload.js'; import { isVoiceMessagePlayed } from '../../util/isVoiceMessagePlayed.std.js'; import type { ServiceIdString } from '../../types/ServiceId.std.js'; +import { getSelectedConversationId } from './nav.std.js'; const log = createLogger('audioPlayer'); diff --git a/ts/state/selectors/conversations.dom.ts b/ts/state/selectors/conversations.dom.ts index 7f53d7aea3..7c3652da0e 100644 --- a/ts/state/selectors/conversations.dom.ts +++ b/ts/state/selectors/conversations.dom.ts @@ -59,12 +59,12 @@ import { import { getBadgeCountMutedConversations, getPinnedConversationIds, + getStoriesEnabled, } from './items.dom.js'; import { createLogger } from '../../logging/log.std.js'; import { TimelineMessageLoadingState } from '../../util/timelineUtil.std.js'; import { isSignalConversation } from '../../util/isSignalConversation.dom.js'; import { reduce } from '../../util/iterables.std.js'; -import type { PanelArgsType } from '../../types/Panels.std.js'; import type { HasStories } from '../../types/Stories.std.js'; import { getHasStoriesSelector } from './stories2.dom.js'; import { canEditMessage } from '../../util/canEditMessage.dom.js'; @@ -92,6 +92,10 @@ import { countAllChatFoldersMutedStats } from '../../util/countMutedStats.std.js import { getActiveProfile } from './notificationProfiles.dom.js'; import type { PinnedMessage } from '../../types/PinnedMessage.std.js'; import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js'; +import { getSelectedConversationId, getSelectedNavTab } from './nav.std.js'; +import { getCallHistoryUnreadCount } from './callHistory.std.js'; +import { NavTab } from '../../types/Nav.std.js'; +import { ReadStatus } from '../../messages/MessageReadStatus.std.js'; const { isNumber, pick } = lodash; @@ -155,12 +159,7 @@ export const getConversationsByGroupId = createSelector( return state.conversationsByGroupId; } ); -export const getHasPanelOpen = createSelector( - getConversations, - (state: ConversationsStateType): boolean => { - return state.targetedConversationPanels.watermark > 0; - } -); + export const getConversationsByUsername = createSelector( getConversations, (state: ConversationsStateType): ConversationLookupType => { @@ -203,13 +202,6 @@ export const getSafeConversationWithSameTitle = createSelector( } ); -export const getSelectedConversationId = createSelector( - getConversations, - (state: ConversationsStateType): string | undefined => { - return state.selectedConversationId; - } -); - type TargetedMessageType = { id: string; counter: number; @@ -1469,57 +1461,77 @@ export const getHideStoryConversationIds = createSelector( ) ); -export const getActivePanel = createSelector( - getConversations, - (conversations): PanelArgsType | undefined => - conversations.targetedConversationPanels.stack[ - conversations.targetedConversationPanels.watermark - ] -); +export const getStoriesState = (state: StateType): StoriesStateType => + state.stories; -type PanelInformationType = { - currPanel: PanelArgsType | undefined; - direction: 'push' | 'pop'; - prevPanel: PanelArgsType | undefined; -}; - -export const getPanelInformation = createSelector( - getConversations, - getActivePanel, - (conversations, currPanel): PanelInformationType | undefined => { - const { direction, watermark } = conversations.targetedConversationPanels; - - if (!direction) { - return; +export const getStoriesNotificationCount = createSelector( + getStoriesEnabled, + getHideStoryConversationIds, + getStoriesState, + ( + storiesEnabled, + hideStoryConversationIds, + { lastOpenedAtTimestamp, stories } + ): number => { + if (!storiesEnabled) { + return 0; } - const watermarkDirection = - direction === 'push' ? watermark - 1 : watermark + 1; - const prevPanel = - conversations.targetedConversationPanels.stack[watermarkDirection]; + const hiddenConversationIds = new Set(hideStoryConversationIds); + + return new Set( + stories + .filter( + story => + story.readStatus === ReadStatus.Unread && + !story.deletedForEveryone && + story.timestamp > (lastOpenedAtTimestamp || 0) && + !hiddenConversationIds.has(story.conversationId) + ) + .map(story => story.conversationId) + ).size; + } +); + +export const getOtherTabsUnreadStats = createSelector( + getSelectedNavTab, + getAllConversationsUnreadStats, + getCallHistoryUnreadCount, + getStoriesNotificationCount, + ( + selectedNavTab, + conversationsUnreadStats, + callHistoryUnreadCount, + storiesNotificationCount + ): UnreadStats => { + let unreadCount = 0; + let unreadMentionsCount = 0; + let readChatsMarkedUnreadCount = 0; + + if (selectedNavTab !== NavTab.Chats) { + unreadCount += conversationsUnreadStats.unreadCount; + unreadMentionsCount += conversationsUnreadStats.unreadMentionsCount; + readChatsMarkedUnreadCount += + conversationsUnreadStats.readChatsMarkedUnreadCount; + } + + // Note: Conversation unread stats includes the call history unread count. + if (selectedNavTab !== NavTab.Calls) { + unreadCount += callHistoryUnreadCount; + } + + if (selectedNavTab !== NavTab.Stories) { + unreadCount += storiesNotificationCount; + } return { - currPanel, - direction, - prevPanel, + unreadCount, + unreadMentionsCount, + readChatsMarkedUnreadCount, }; } ); -export const getIsPanelAnimating = createSelector( - getConversations, - (conversations): boolean => { - return conversations.targetedConversationPanels.isAnimating; - } -); - -export const getWasPanelAnimated = createSelector( - getConversations, - (conversations): boolean => { - return conversations.targetedConversationPanels.wasAnimated; - } -); - // Note that this doesn't take into account max edit count. See canEditMessage. export const getLastEditableMessageId = createSelector( getConversationMessages, diff --git a/ts/state/selectors/nav.preload.ts b/ts/state/selectors/nav.preload.ts deleted file mode 100644 index bacb815033..0000000000 --- a/ts/state/selectors/nav.preload.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2023 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { createSelector } from 'reselect'; -import { getAllConversationsUnreadStats } from './conversations.dom.js'; -import { getStoriesNotificationCount } from './stories.preload.js'; -import { getCallHistoryUnreadCount } from './callHistory.std.js'; -import { NavTab } from '../../types/Nav.std.js'; - -import type { StateType } from '../reducer.preload.js'; -import type { NavStateType } from '../ducks/nav.std.js'; -import type { UnreadStats } from '../../util/countUnreadStats.std.js'; - -function getNav(state: StateType): NavStateType { - return state.nav; -} - -export const getSelectedNavTab = createSelector(getNav, nav => { - return nav.selectedLocation.tab; -}); - -export const getSelectedLocation = createSelector(getNav, nav => { - return nav.selectedLocation; -}); - -export const getOtherTabsUnreadStats = createSelector( - getSelectedNavTab, - getAllConversationsUnreadStats, - getCallHistoryUnreadCount, - getStoriesNotificationCount, - ( - selectedNavTab, - conversationsUnreadStats, - callHistoryUnreadCount, - storiesNotificationCount - ): UnreadStats => { - let unreadCount = 0; - let unreadMentionsCount = 0; - let readChatsMarkedUnreadCount = 0; - - if (selectedNavTab !== NavTab.Chats) { - unreadCount += conversationsUnreadStats.unreadCount; - unreadMentionsCount += conversationsUnreadStats.unreadMentionsCount; - readChatsMarkedUnreadCount += - conversationsUnreadStats.readChatsMarkedUnreadCount; - } - - // Note: Conversation unread stats includes the call history unread count. - if (selectedNavTab !== NavTab.Calls) { - unreadCount += callHistoryUnreadCount; - } - - if (selectedNavTab !== NavTab.Stories) { - unreadCount += storiesNotificationCount; - } - - return { - unreadCount, - unreadMentionsCount, - readChatsMarkedUnreadCount, - }; - } -); diff --git a/ts/state/selectors/nav.std.ts b/ts/state/selectors/nav.std.ts new file mode 100644 index 0000000000..397d6c75ff --- /dev/null +++ b/ts/state/selectors/nav.std.ts @@ -0,0 +1,105 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { createSelector } from 'reselect'; + +import { NavTab } from '../../types/Nav.std.js'; + +import type { PanelArgsType } from '../../types/Panels.std.js'; +import type { Location, PanelInfo } from '../../types/Nav.std.js'; +import type { StateType } from '../reducer.preload.js'; +import type { NavStateType } from '../ducks/nav.std.js'; + +function getNav(state: StateType): NavStateType { + return state.nav; +} + +export const getSelectedNavTab = createSelector(getNav, nav => { + return nav.selectedLocation.tab; +}); + +export const getSelectedLocation = createSelector(getNav, nav => { + return nav.selectedLocation; +}); + +export const getSelectedConversationId = createSelector( + getSelectedLocation, + (selectedLocation: Location): string | undefined => { + if (selectedLocation.tab !== NavTab.Chats) { + return; + } + + return selectedLocation.details.conversationId; + } +); + +export const getPanels = createSelector( + getSelectedLocation, + (selectedLocation: Location): PanelInfo | undefined => { + if (selectedLocation.tab !== NavTab.Chats) { + return; + } + + return selectedLocation.details.panels; + } +); +export const getHasPanelOpen = createSelector(getPanels, (panels): boolean => { + return Boolean(panels && panels.watermark > 0); +}); + +export const getActivePanel = createSelector( + getPanels, + (panels: PanelInfo | undefined): PanelArgsType | undefined => { + if (!panels) { + return undefined; + } + + return panels.stack[panels.watermark]; + } +); + +type PanelInformationType = { + currPanel: PanelArgsType | undefined; + direction: 'push' | 'pop'; + prevPanel: PanelArgsType | undefined; +}; + +export const getPanelInformation = createSelector( + getPanels, + getActivePanel, + (panels, currPanel): PanelInformationType | undefined => { + if (!panels) { + return; + } + + const { direction, watermark } = panels; + + if (!direction) { + return; + } + + const watermarkDirection = + direction === 'push' ? watermark - 1 : watermark + 1; + const prevPanel = panels.stack[watermarkDirection]; + + return { + currPanel, + direction, + prevPanel, + }; + } +); + +export const getIsPanelAnimating = createSelector( + getPanels, + (panels): boolean => { + return Boolean(panels?.isAnimating); + } +); + +export const getWasPanelAnimated = createSelector( + getPanels, + (panels): boolean => { + return Boolean(panels?.wasAnimated); + } +); diff --git a/ts/state/selectors/search.preload.ts b/ts/state/selectors/search.preload.ts index afd20f9509..f80650b166 100644 --- a/ts/state/selectors/search.preload.ts +++ b/ts/state/selectors/search.preload.ts @@ -26,7 +26,6 @@ import type { GetConversationByIdType } from './conversations.dom.js'; import { getConversationLookup, getConversationSelector, - getSelectedConversationId, } from './conversations.dom.js'; import { hydrateRanges } from '../../util/BodyRange.node.js'; @@ -34,6 +33,7 @@ import type { RawBodyRange } from '../../types/BodyRange.std.js'; import { createLogger } from '../../logging/log.std.js'; import { getOwn } from '../../util/getOwn.std.js'; import type { MessageAttributesType } from '../../model-types.js'; +import { getSelectedConversationId } from './nav.std.js'; const log = createLogger('search'); diff --git a/ts/state/selectors/stories.preload.ts b/ts/state/selectors/stories.preload.ts index 7835fd6662..4e1a6fd4b9 100644 --- a/ts/state/selectors/stories.preload.ts +++ b/ts/state/selectors/stories.preload.ts @@ -15,7 +15,6 @@ import type { StorySendStateType, StoryViewType, } from '../../types/Stories.std.js'; -import type { StateType } from '../reducer.preload.js'; import type { SelectedStoryDataType, StoryDataType, @@ -32,7 +31,9 @@ import { getHideStoryConversationIds, getMe, PLACEHOLDER_CONTACT_ID, + getStoriesState, } from './conversations.dom.js'; +import { getStoriesEnabled } from './items.dom.js'; import { getUserConversationId } from './user.std.js'; import { getDistributionListSelector } from './storyDistributionLists.dom.js'; import { calculateExpirationTimestamp } from '../../util/expirationTimer.std.js'; @@ -45,15 +46,11 @@ import { } from '../../util/resolveStorySendStatus.std.js'; import { BodyRange } from '../../types/BodyRange.std.js'; import { hydrateRanges } from '../../util/BodyRange.node.js'; -import { getStoriesEnabled } from './items.dom.js'; const { pick } = lodash; const log = createLogger('stories'); -export const getStoriesState = (state: StateType): StoriesStateType => - state.stories; - export const hasSelectedStoryData = createSelector( getStoriesState, ({ selectedStoryData }): boolean => Boolean(selectedStoryData) diff --git a/ts/state/smart/AllMedia.preload.tsx b/ts/state/smart/AllMedia.preload.tsx index 8640dc0742..aed598116f 100644 --- a/ts/state/smart/AllMedia.preload.tsx +++ b/ts/state/smart/AllMedia.preload.tsx @@ -17,6 +17,7 @@ import { MediaItem, type PropsType as MediaItemPropsType, } from './MediaItem.preload.js'; +import { useNavActions } from '../ducks/nav.std.js'; const log = createLogger('AllMedia'); @@ -47,10 +48,10 @@ export const SmartAllMedia = memo(function SmartAllMedia({ const { initialLoad, loadMore } = useMediaGalleryActions(); const { saveAttachment, - pushPanelForConversation, kickOffAttachmentDownload, cancelAttachmentDownload, } = useConversationsActions(); + const { pushPanelForConversation } = useNavActions(); const { showLightbox } = useLightboxActions(); const { loadVoiceNoteAudio } = useAudioPlayerActions(); const i18n = useSelector(getIntl); diff --git a/ts/state/smart/CallsTab.preload.tsx b/ts/state/smart/CallsTab.preload.tsx index f91612f623..78aee32109 100644 --- a/ts/state/smart/CallsTab.preload.tsx +++ b/ts/state/smart/CallsTab.preload.tsx @@ -13,6 +13,7 @@ import { CallsTab } from '../../components/CallsTab.preload.js'; import { getAllConversations, getConversationSelector, + getOtherTabsUnreadStats, } from '../selectors/conversations.dom.js'; import { filterAndSortConversations } from '../../util/filterAndSortConversations.std.js'; import type { @@ -37,7 +38,6 @@ import { useCallHistoryActions } from '../ducks/callHistory.preload.js'; import { getCallHistoryEdition } from '../selectors/callHistory.std.js'; import { getHasPendingUpdate } from '../selectors/updates.std.js'; import { getHasAnyFailedStorySends } from '../selectors/stories.preload.js'; -import { getOtherTabsUnreadStats } from '../selectors/nav.preload.js'; import { SmartCallLinkDetails } from './CallLinkDetails.preload.js'; import type { CallLinkType } from '../../types/CallLink.std.js'; import { filterCallLinks } from '../../util/filterCallLinks.dom.js'; diff --git a/ts/state/smart/ChatsTab.preload.tsx b/ts/state/smart/ChatsTab.preload.tsx index 707a151848..0bd38cddba 100644 --- a/ts/state/smart/ChatsTab.preload.tsx +++ b/ts/state/smart/ChatsTab.preload.tsx @@ -22,9 +22,9 @@ import { getNavTabsCollapsed } from '../selectors/items.dom.js'; import { useItemsActions } from '../ducks/items.preload.js'; import { getHasAnyFailedStorySends } from '../selectors/stories.preload.js'; import { getHasPendingUpdate } from '../selectors/updates.std.js'; -import { getOtherTabsUnreadStats } from '../selectors/nav.preload.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; import { - getSelectedConversationId, + getOtherTabsUnreadStats, getTargetedMessage, getTargetedMessageSource, } from '../selectors/conversations.dom.js'; diff --git a/ts/state/smart/CompositionArea.preload.tsx b/ts/state/smart/CompositionArea.preload.tsx index 88cfa68fe3..b85ac7b46b 100644 --- a/ts/state/smart/CompositionArea.preload.tsx +++ b/ts/state/smart/CompositionArea.preload.tsx @@ -26,12 +26,12 @@ import { getCachedConversationMemberColorsSelector, getConversationSelector, getGroupAdminsSelector, - getHasPanelOpen, getLastEditableMessageId, getMessages, getSelectedMessageIds, isMissingRequiredProfileSharing, } from '../selectors/conversations.dom.js'; +import { getHasPanelOpen } from '../selectors/nav.std.js'; import { getSharedGroupNames } from '../../util/sharedGroupNames.dom.js'; import { getDefaultConversationColor, @@ -59,6 +59,7 @@ import { isConversationEverUnregistered } from '../../util/isConversationUnregis import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js'; import { isConversationMuted } from '../../util/isConversationMuted.std.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; +import { useNavActions } from '../ducks/nav.std.js'; function renderSmartCompositionRecording() { return ; @@ -204,7 +205,6 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ setComposerFocus, } = useComposerActions(); const { - pushPanelForConversation, discardEditMessage, acceptConversation, blockAndReportSpam, @@ -217,6 +217,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ setMuteExpiration, showConversation, } = useConversationsActions(); + const { pushPanelForConversation } = useNavActions(); const { cancelRecording, completeRecording, startRecording, errorRecording } = useAudioRecorderActions(); const { onUseEmoji } = useEmojisActions(); diff --git a/ts/state/smart/CompositionRecording.preload.tsx b/ts/state/smart/CompositionRecording.preload.tsx index 1201cedf51..4d76bb2631 100644 --- a/ts/state/smart/CompositionRecording.preload.tsx +++ b/ts/state/smart/CompositionRecording.preload.tsx @@ -7,7 +7,7 @@ import { CompositionRecording } from '../../components/CompositionRecording.dom. import { useAudioRecorderActions } from '../ducks/audioRecorder.preload.js'; import { useComposerActions } from '../ducks/composer.preload.js'; import { useToastActions } from '../ducks/toast.preload.js'; -import { getSelectedConversationId } from '../selectors/conversations.dom.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; import { getIntl } from '../selectors/user.std.js'; export const SmartCompositionRecording = memo( diff --git a/ts/state/smart/CompositionRecordingDraft.preload.tsx b/ts/state/smart/CompositionRecordingDraft.preload.tsx index 59bfdc2d65..9a138124e3 100644 --- a/ts/state/smart/CompositionRecordingDraft.preload.tsx +++ b/ts/state/smart/CompositionRecordingDraft.preload.tsx @@ -11,10 +11,8 @@ import { } from '../ducks/audioPlayer.preload.js'; import { useComposerActions } from '../ducks/composer.preload.js'; import { selectAudioPlayerActive } from '../selectors/audioPlayer.preload.js'; -import { - getConversationByIdSelector, - getSelectedConversationId, -} from '../selectors/conversations.dom.js'; +import { getConversationByIdSelector } from '../selectors/conversations.dom.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; import { getIntl } from '../selectors/user.std.js'; export type SmartCompositionRecordingDraftProps = { diff --git a/ts/state/smart/ContactDetail.preload.tsx b/ts/state/smart/ContactDetail.preload.tsx index 74bb65ba5f..3d4370020f 100644 --- a/ts/state/smart/ContactDetail.preload.tsx +++ b/ts/state/smart/ContactDetail.preload.tsx @@ -17,6 +17,7 @@ import type { EmbeddedContactForUIType, } from '../../types/EmbeddedContact.std.js'; import { getAccountSelector } from '../selectors/accounts.std.js'; +import { useNavActions } from '../ducks/nav.std.js'; export type OwnProps = Pick; @@ -67,11 +68,9 @@ export const SmartContactDetail = memo(function SmartContactDetail({ }: OwnProps): React.JSX.Element | null { const i18n = useSelector(getIntl); const messageLookup = useSelector(getMessages); - const { - cancelAttachmentDownload, - kickOffAttachmentDownload, - popPanelForConversation, - } = useConversationsActions(); + const { cancelAttachmentDownload, kickOffAttachmentDownload } = + useConversationsActions(); + const { popPanelForConversation } = useNavActions(); const contact = useLookupContact(messageLookup[messageId]?.contact?.[0]); diff --git a/ts/state/smart/ContactName.preload.tsx b/ts/state/smart/ContactName.preload.tsx index 18a0110e7d..dae869156e 100644 --- a/ts/state/smart/ContactName.preload.tsx +++ b/ts/state/smart/ContactName.preload.tsx @@ -4,10 +4,8 @@ import React, { memo, useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { ContactName } from '../../components/conversation/ContactName.dom.js'; import { getIntl } from '../selectors/user.std.js'; -import { - getConversationSelector, - getSelectedConversationId, -} from '../selectors/conversations.dom.js'; +import { getConversationSelector } from '../selectors/conversations.dom.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; import { useGlobalModalActions } from '../ducks/globalModals.preload.js'; type ExternalProps = { diff --git a/ts/state/smart/ConversationDetails.preload.tsx b/ts/state/smart/ConversationDetails.preload.tsx index 776cd661dd..a59f566a47 100644 --- a/ts/state/smart/ConversationDetails.preload.tsx +++ b/ts/state/smart/ConversationDetails.preload.tsx @@ -31,7 +31,7 @@ import { getDefaultConversationColor, getItems, } from '../selectors/items.dom.js'; -import { getSelectedNavTab } from '../selectors/nav.preload.js'; +import { getSelectedNavTab } from '../selectors/nav.std.js'; import { getIntl, getTheme, @@ -53,6 +53,7 @@ import { DataReader } from '../../sql/Client.preload.js'; import { isFeaturedEnabledSelector } from '../../util/isFeatureEnabled.dom.js'; import { getCanAddLabel } from '../../types/GroupMemberLabels.std.js'; import { useToastActions } from '../ducks/toast.preload.js'; +import { useNavActions } from '../ducks/nav.std.js'; const { sortBy } = lodash; @@ -128,7 +129,6 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({ deleteAvatarFromDisk, getProfilesForConversation, leaveGroup, - pushPanelForConversation, replaceAvatar, saveAvatarToDisk, setDisappearingMessages, @@ -138,6 +138,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({ updateGroupAttributes, updateNicknameAndNote, } = useConversationsActions(); + const { pushPanelForConversation } = useNavActions(); const { onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, diff --git a/ts/state/smart/ConversationHeader.preload.tsx b/ts/state/smart/ConversationHeader.preload.tsx index 8e7c629140..4d2a7f194d 100644 --- a/ts/state/smart/ConversationHeader.preload.tsx +++ b/ts/state/smart/ConversationHeader.preload.tsx @@ -37,10 +37,10 @@ import { import { getConversationByServiceIdSelector, getConversationSelector, - getHasPanelOpen, isMissingRequiredProfileSharing as getIsMissingRequiredProfileSharing, getSelectedMessageIds, } from '../selectors/conversations.dom.js'; +import { getHasPanelOpen } from '../selectors/nav.std.js'; import { getHasStoriesSelector } from '../selectors/stories2.dom.js'; import { getIntl, getTheme, getUserACI } from '../selectors/user.std.js'; import { isConversationEverUnregistered } from '../../util/isConversationUnregistered.dom.js'; @@ -53,6 +53,7 @@ import type { SmartMiniPlayerProps } from './MiniPlayer.preload.js'; import { SmartMiniPlayer } from './MiniPlayer.preload.js'; import { SmartPinnedMessagesBar } from './PinnedMessagesBar.preload.js'; import { getContactSpoofingWarningSelector } from '../selectors/timeline.preload.js'; +import { useNavActions } from '../ducks/nav.std.js'; function renderCollidingAvatars( props: SmartCollidingAvatarsProps @@ -141,7 +142,6 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({ onArchive, onMarkUnread, onMoveToInbox, - pushPanelForConversation, setDisappearingMessages, setMuteExpiration, setPinned, @@ -154,6 +154,7 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({ acknowledgeGroupMemberNameCollisions, reviewConversationNameCollision, } = useConversationsActions(); + const { pushPanelForConversation } = useNavActions(); const { onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, diff --git a/ts/state/smart/ConversationPanel.preload.tsx b/ts/state/smart/ConversationPanel.preload.tsx index d46171708f..f22818bd0c 100644 --- a/ts/state/smart/ConversationPanel.preload.tsx +++ b/ts/state/smart/ConversationPanel.preload.tsx @@ -33,15 +33,15 @@ import { getIntl } from '../selectors/user.std.js'; import { getPanelInformation, getWasPanelAnimated, -} from '../selectors/conversations.dom.js'; +} from '../selectors/nav.std.js'; import { focusableSelector } from '../../util/focusableSelectors.std.js'; import { missingCaseError } from '../../util/missingCaseError.std.js'; -import { useConversationsActions } from '../ducks/conversations.preload.js'; import { useReducedMotion } from '../../hooks/useReducedMotion.dom.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import { SmartPinnedMessagesPanel } from './PinnedMessagesPanel.preload.js'; import { SmartMiniPlayer } from './MiniPlayer.preload.js'; import { SmartGroupMemberLabelEditor } from './GroupMemberLabelEditor.preload.js'; +import { useNavActions } from '../ducks/nav.std.js'; const log = createLogger('ConversationPanel'); @@ -106,8 +106,7 @@ export const ConversationPanel = memo(function ConversationPanel({ conversationId: string; }) { const panelInformation = useSelector(getPanelInformation); - const { panelAnimationDone, panelAnimationStarted } = - useConversationsActions(); + const { panelAnimationDone, panelAnimationStarted } = useNavActions(); const animateRef = useRef(null); const overlayRef = useRef(null); @@ -288,7 +287,7 @@ const PanelContainer = forwardRef< ref ): React.JSX.Element { const i18n = useSelector(getIntl); - const { popPanelForConversation } = useConversationsActions(); + const { popPanelForConversation } = useNavActions(); const conversationTitle = getConversationTitleForPanelType(i18n, panel.type); let info: React.JSX.Element | undefined; @@ -398,7 +397,9 @@ function PanelElement({ } if (panel.type === PanelType.MessageDetails) { - return ; + const { messageId } = panel.args; + + return ; } if (panel.type === PanelType.NotificationSettings) { diff --git a/ts/state/smart/ConversationView.preload.tsx b/ts/state/smart/ConversationView.preload.tsx index f9c06ee534..202fadb75d 100644 --- a/ts/state/smart/ConversationView.preload.tsx +++ b/ts/state/smart/ConversationView.preload.tsx @@ -8,11 +8,8 @@ import { ConversationView } from '../../components/conversation/ConversationView import { SmartCompositionArea } from './CompositionArea.preload.js'; import { SmartConversationHeader } from './ConversationHeader.preload.js'; import { SmartTimeline } from './Timeline.preload.js'; -import { - getActivePanel, - getIsPanelAnimating, - getSelectedMessageIds, -} from '../selectors/conversations.dom.js'; +import { getSelectedMessageIds } from '../selectors/conversations.dom.js'; +import { getActivePanel, getIsPanelAnimating } from '../selectors/nav.std.js'; import { useComposerActions } from '../ducks/composer.preload.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; import { isShowingAnyModal } from '../selectors/globalModals.std.js'; diff --git a/ts/state/smart/GroupMemberLabelEditor.preload.tsx b/ts/state/smart/GroupMemberLabelEditor.preload.tsx index 3412473f81..03513de9aa 100644 --- a/ts/state/smart/GroupMemberLabelEditor.preload.tsx +++ b/ts/state/smart/GroupMemberLabelEditor.preload.tsx @@ -14,6 +14,7 @@ import { useConversationsActions } from '../ducks/conversations.preload.js'; import { createLogger } from '../../logging/log.std.js'; import { getPreferredBadgeSelector } from '../selectors/badges.preload.js'; import { isNotNil } from '../../util/isNotNil.std.js'; +import { useNavActions } from '../ducks/nav.std.js'; const log = createLogger('SmartGroupMemberLabelEditor'); @@ -33,8 +34,8 @@ export const SmartGroupMemberLabelEditor = memo( const conversation = conversationSelector(conversationId); const me = conversationSelector(user.ourAci); - const { updateGroupMemberLabel, popPanelForConversation } = - useConversationsActions(); + const { updateGroupMemberLabel } = useConversationsActions(); + const { popPanelForConversation } = useNavActions(); const getMemberColors = useSelector( getCachedConversationMemberColorsSelector ); diff --git a/ts/state/smart/HeroRow.preload.tsx b/ts/state/smart/HeroRow.preload.tsx index 47f7ac949c..890de6cc64 100644 --- a/ts/state/smart/HeroRow.preload.tsx +++ b/ts/state/smart/HeroRow.preload.tsx @@ -22,6 +22,7 @@ import { useGlobalModalActions } from '../ducks/globalModals.preload.js'; import { useStoriesActions } from '../ducks/stories.preload.js'; import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation.preload.js'; import { getGroupMemberships } from '../../util/getGroupMemberships.dom.js'; +import { useNavActions } from '../ducks/nav.std.js'; type SmartHeroRowProps = Readonly<{ id: string; @@ -72,8 +73,8 @@ export const SmartHeroRow = memo(function SmartHeroRow({ const isSignalConversationValue = isSignalConversation(conversation); const fromOrAddedByTrustedContact = isFromOrAddedByTrustedContact(conversation); - const { pushPanelForConversation, startAvatarDownload } = - useConversationsActions(); + const { startAvatarDownload } = useConversationsActions(); + const { pushPanelForConversation } = useNavActions(); const { toggleAboutContactModal, toggleProfileNameWarningModal } = useGlobalModalActions(); const openConversationDetails = useCallback(() => { diff --git a/ts/state/smart/LeftPane.preload.tsx b/ts/state/smart/LeftPane.preload.tsx index 95be03c96c..bdf25ed718 100644 --- a/ts/state/smart/LeftPane.preload.tsx +++ b/ts/state/smart/LeftPane.preload.tsx @@ -54,13 +54,16 @@ import { getMaximumGroupSizeModalState, getMe, getRecommendedGroupSizeModalState, - getSelectedConversationId, getShowArchived, getTargetedMessage, hasGroupCreationError, isCreatingGroup, isEditingAvatar, } from '../selectors/conversations.dom.js'; +import { + getSelectedConversationId, + getSelectedLocation, +} from '../selectors/nav.std.js'; import { getCrashReportCount } from '../selectors/crashReports.std.js'; import { hasExpired } from '../selectors/expiration.dom.js'; import { @@ -341,6 +344,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({ getBackupMediaDownloadProgress ); const isOnline = useSelector(getNetworkIsOnline); + const selectedLocation = useSelector(getSelectedLocation); const serverAlerts = useSelector(getServerAlerts); @@ -397,12 +401,10 @@ export const SmartLeftPane = memo(function SmartLeftPane({ tab: NavTab.Settings, details: { page: SettingsPage.ChatFolders, - previousLocation: { - tab: NavTab.Chats, - }, + previousLocation: selectedLocation, }, }); - }, [changeLocation]); + }, [changeLocation, selectedLocation]); const maybePreloadConversation = useCallback( (conversationId: string) => { @@ -513,6 +515,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({ setComposeGroupName={setComposeGroupName} setComposeSearchTerm={setComposeSearchTerm} setComposeSelectedRegion={setComposeSelectedRegion} + selectedLocation={selectedLocation} setIsFetchingUUID={setIsFetchingUUID} showArchivedConversations={showArchivedConversations} showChooseGroupMembers={showChooseGroupMembers} diff --git a/ts/state/smart/LeftPaneChatFolders.preload.tsx b/ts/state/smart/LeftPaneChatFolders.preload.tsx index 1e1c0af32f..f3ec83a3ce 100644 --- a/ts/state/smart/LeftPaneChatFolders.preload.tsx +++ b/ts/state/smart/LeftPaneChatFolders.preload.tsx @@ -18,7 +18,7 @@ import { useNavActions } from '../ducks/nav.std.js'; import { NavTab, SettingsPage } from '../../types/Nav.std.js'; import { isChatFoldersEnabled } from '../../util/isChatFoldersEnabled.dom.js'; import type { ChatFolderId } from '../../types/ChatFolder.std.js'; -import { getSelectedLocation } from '../selectors/nav.preload.js'; +import { getSelectedLocation } from '../selectors/nav.std.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; export const SmartLeftPaneChatFolders = memo( diff --git a/ts/state/smart/LeftPaneConversationListItemContextMenu.preload.tsx b/ts/state/smart/LeftPaneConversationListItemContextMenu.preload.tsx index 135cd5087b..6b4496e84b 100644 --- a/ts/state/smart/LeftPaneConversationListItemContextMenu.preload.tsx +++ b/ts/state/smart/LeftPaneConversationListItemContextMenu.preload.tsx @@ -19,7 +19,7 @@ import { useChatFolderActions } from '../ducks/chatFolders.preload.js'; import { useNavActions } from '../ducks/nav.std.js'; import { NavTab, SettingsPage } from '../../types/Nav.std.js'; import type { ChatFolderParams } from '../../types/ChatFolder.std.js'; -import { getSelectedLocation } from '../selectors/nav.preload.js'; +import { getSelectedLocation } from '../selectors/nav.std.js'; import { getIsActivelySearching } from '../selectors/search.preload.js'; export const SmartLeftPaneConversationListItemContextMenu: FC = diff --git a/ts/state/smart/MessageAudio.preload.tsx b/ts/state/smart/MessageAudio.preload.tsx index cd02f37a71..3ebd13e80a 100644 --- a/ts/state/smart/MessageAudio.preload.tsx +++ b/ts/state/smart/MessageAudio.preload.tsx @@ -15,12 +15,10 @@ import { selectAudioPlayerActive, selectVoiceNoteAndConsecutive, } from '../selectors/audioPlayer.preload.js'; -import { useConversationsActions } from '../ducks/conversations.preload.js'; import { createLogger } from '../../logging/log.std.js'; -import { - getConversationByIdSelector, - getSelectedConversationId, -} from '../selectors/conversations.dom.js'; +import { getConversationByIdSelector } from '../selectors/conversations.dom.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; +import { useNavActions } from '../ducks/nav.std.js'; const log = createLogger('MessageAudio'); @@ -35,7 +33,7 @@ export const SmartMessageAudio = memo(function SmartMessageAudio({ const active = useSelector(selectAudioPlayerActive); const { loadVoiceNoteAudio, setIsPlaying, setPlaybackRate, setPosition } = useAudioPlayerActions(); - const { pushPanelForConversation } = useConversationsActions(); + const { pushPanelForConversation } = useNavActions(); const getVoiceNoteData = useSelector(selectVoiceNoteAndConsecutive); const getConversationById = useSelector(getConversationByIdSelector); diff --git a/ts/state/smart/MessageDetail.preload.tsx b/ts/state/smart/MessageDetail.preload.tsx index fe6bca2248..12a20376b7 100644 --- a/ts/state/smart/MessageDetail.preload.tsx +++ b/ts/state/smart/MessageDetail.preload.tsx @@ -6,10 +6,7 @@ import { useSelector } from 'react-redux'; import type { Props as MessageDetailProps } from '../../components/conversation/MessageDetail.dom.js'; import { MessageDetail } from '../../components/conversation/MessageDetail.dom.js'; -import { - getActivePanel, - getContactNameColorSelector, -} from '../selectors/conversations.dom.js'; +import { getContactNameColorSelector } from '../selectors/conversations.dom.js'; import { getIntl, getInteractionMode, @@ -26,8 +23,10 @@ import { useConversationsActions } from '../ducks/conversations.preload.js'; import { useGlobalModalActions } from '../ducks/globalModals.preload.js'; import { useLightboxActions } from '../ducks/lightbox.preload.js'; import { useStoriesActions } from '../ducks/stories.preload.js'; -import { PanelType } from '../../types/Panels.std.js'; import { createLogger } from '../../logging/log.std.js'; +import { useNavActions } from '../ducks/nav.std.js'; +import { getPanelInformation } from '../selectors/nav.std.js'; +import { PanelType } from '../../types/Panels.std.js'; export type { Contact } from '../../components/conversation/MessageDetail.dom.js'; export type OwnProps = Pick< @@ -37,124 +36,121 @@ export type OwnProps = Pick< const log = createLogger('SmartMessageDetail'); -export const SmartMessageDetail = memo( - function SmartMessageDetail(): React.JSX.Element | null { - const activePanel = useSelector(getActivePanel); - const getMessageDetails = useSelector(getMessageDetailsSelector); - const getContactNameColor = useSelector(getContactNameColorSelector); - const getPreferredBadge = useSelector(getPreferredBadgeSelector); - const i18n = useSelector(getIntl); - const platform = useSelector(getPlatform); - const interactionMode = useSelector(getInteractionMode); - const theme = useSelector(getTheme); - const { checkForAccount } = useAccountsActions(); - const { endPoll } = useComposerActions(); - const { - cancelAttachmentDownload, - clearTargetedMessage: clearSelectedMessage, - doubleCheckMissingQuoteReference, - kickOffAttachmentDownload, - markAttachmentAsCorrupted, - messageExpanded, - openGiftBadge, - popPanelForConversation, - pushPanelForConversation, - retryMessageSend, - sendPollVote, - saveAttachment, - saveAttachments, - showAttachmentDownloadStillInProgressToast, - showConversation, - showExpiredIncomingTapToViewToast, - showExpiredOutgoingTapToViewToast, - showMediaNoLongerAvailableToast, - showSpoiler, - } = useConversationsActions(); - const { - showContactModal, - showEditHistoryModal, - showTapToViewNotAvailableModal, - toggleSafetyNumberModal, - } = useGlobalModalActions(); - const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions(); - const { viewStory } = useStoriesActions(); +export const SmartMessageDetail = memo(function SmartMessageDetail({ + messageId, +}: { + messageId: string | undefined; +}): React.JSX.Element | null { + const getMessageDetails = useSelector(getMessageDetailsSelector); + const getContactNameColor = useSelector(getContactNameColorSelector); + const getPreferredBadge = useSelector(getPreferredBadgeSelector); + const i18n = useSelector(getIntl); + const platform = useSelector(getPlatform); + const interactionMode = useSelector(getInteractionMode); + const theme = useSelector(getTheme); + const { checkForAccount } = useAccountsActions(); + const { endPoll } = useComposerActions(); + const { + cancelAttachmentDownload, + clearTargetedMessage: clearSelectedMessage, + doubleCheckMissingQuoteReference, + kickOffAttachmentDownload, + markAttachmentAsCorrupted, + messageExpanded, + openGiftBadge, + retryMessageSend, + sendPollVote, + saveAttachment, + saveAttachments, + showAttachmentDownloadStillInProgressToast, + showConversation, + showExpiredIncomingTapToViewToast, + showExpiredOutgoingTapToViewToast, + showMediaNoLongerAvailableToast, + showSpoiler, + } = useConversationsActions(); + const { popPanelForConversation, pushPanelForConversation } = useNavActions(); + const { + showContactModal, + showEditHistoryModal, + showTapToViewNotAvailableModal, + toggleSafetyNumberModal, + } = useGlobalModalActions(); + const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions(); + const { viewStory } = useStoriesActions(); - let messageId: string | null = null; + const messageDetails = messageId ? getMessageDetails(messageId) : undefined; - if (activePanel?.type === PanelType.MessageDetails) { - messageId = activePanel.args.messageId; - } else { - log.error(`Rendering, but current panel is ${activePanel?.type}`); - } - - const messageDetails = messageId ? getMessageDetails(messageId) : undefined; - - useEffect(() => { - if (!messageDetails) { - popPanelForConversation(); - } - }, [messageDetails, popPanelForConversation]); - - if (!messageDetails) { + // Only pop current panel if we are actually the current panel - when we're animating + // out, we don't want to affect the current location. + const currPanelType = useSelector(getPanelInformation)?.currPanel?.type; + useEffect(() => { + if (!messageDetails && currPanelType === PanelType.MessageDetails) { log.error( - `No message details found for message ${messageId}, leaving this screen.` + `MessageDetail: Current panel, and no details for message ${messageId}, popping panel.` ); popPanelForConversation(); - return null; } + }, [currPanelType, messageDetails, messageId, popPanelForConversation]); - const { contacts, errors, message, receivedAt } = messageDetails; - - const contactNameColor = - message.conversationType === 'group' - ? getContactNameColor(message.conversationId, message.author.id) - : undefined; - - return ( - + if (!messageDetails) { + log.error( + `MessageDetail: No details found for message ${messageId}, rendering nothing.` ); + return null; } -); + + const { contacts, errors, message, receivedAt } = messageDetails; + + const contactNameColor = + message.conversationType === 'group' + ? getContactNameColor(message.conversationId, message.author.id) + : undefined; + + return ( + + ); +}); diff --git a/ts/state/smart/NavTabs.preload.tsx b/ts/state/smart/NavTabs.preload.tsx index a318c44626..c0759c03c0 100644 --- a/ts/state/smart/NavTabs.preload.tsx +++ b/ts/state/smart/NavTabs.preload.tsx @@ -21,7 +21,7 @@ import { getProfileMovedModalNeeded, getStoriesEnabled, } from '../selectors/items.dom.js'; -import { getSelectedNavTab } from '../selectors/nav.preload.js'; +import { getSelectedNavTab } from '../selectors/nav.std.js'; import { useNavActions } from '../ducks/nav.std.js'; import { getHasPendingUpdate } from '../selectors/updates.std.js'; import { getCallHistoryUnreadCount } from '../selectors/callHistory.std.js'; diff --git a/ts/state/smart/PinnedMessagesBar.preload.tsx b/ts/state/smart/PinnedMessagesBar.preload.tsx index 990ff22a91..e249849b0a 100644 --- a/ts/state/smart/PinnedMessagesBar.preload.tsx +++ b/ts/state/smart/PinnedMessagesBar.preload.tsx @@ -14,11 +14,11 @@ import { orderBy } from 'lodash'; import { getIntl } from '../selectors/user.std.js'; import { getConversationSelector, - getSelectedConversationId, getPinnedMessages, getMessages, getConversationIsReady, } from '../selectors/conversations.dom.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; import { strictAssert } from '../../util/assert.std.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; import type { @@ -42,6 +42,7 @@ import * as Attachment from '../../util/Attachment.std.js'; import * as MIME from '../../types/MIME.std.js'; import * as EmbeddedContact from '../../types/EmbeddedContact.std.js'; import type { StateSelector } from '../types.std.js'; +import { useNavActions } from '../ducks/nav.std.js'; function getPinMessageAttachment( props: MessagePropsType @@ -377,8 +378,8 @@ export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() { const pins = useSelector(selectPins); const canPinMessages = getCanPinMessages(conversation); - const { onPinnedMessageRemove, pushPanelForConversation, scrollToMessage } = - useConversationsActions(); + const { onPinnedMessageRemove, scrollToMessage } = useConversationsActions(); + const { pushPanelForConversation } = useNavActions(); const [current, setCurrent] = useState(() => { return getLastPinId(pins); diff --git a/ts/state/smart/PinnedMessagesPanel.preload.tsx b/ts/state/smart/PinnedMessagesPanel.preload.tsx index 577e38c729..c45e322dce 100644 --- a/ts/state/smart/PinnedMessagesPanel.preload.tsx +++ b/ts/state/smart/PinnedMessagesPanel.preload.tsx @@ -14,6 +14,7 @@ import type { SmartTimelineItemProps } from './TimelineItem.preload.js'; import { SmartTimelineItem } from './TimelineItem.preload.js'; import { canPinMessages as getCanPinMessages } from '../selectors/message.preload.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; +import { useNavActions } from '../ducks/nav.std.js'; export type SmartPinnedMessagesPanelProps = Readonly<{ conversationId: string; @@ -29,8 +30,8 @@ export const SmartPinnedMessagesPanel = memo(function SmartPinnedMessagesPanel( const i18n = useSelector(getIntl); const conversationSelector = useSelector(getConversationByIdSelector); const conversation = conversationSelector(props.conversationId); - const { onPinnedMessageRemove, popPanelForConversation } = - useConversationsActions(); + const { onPinnedMessageRemove } = useConversationsActions(); + const { popPanelForConversation } = useNavActions(); strictAssert( conversation, diff --git a/ts/state/smart/Preferences.preload.tsx b/ts/state/smart/Preferences.preload.tsx index e9b92f93e8..1e40081cb0 100644 --- a/ts/state/smart/Preferences.preload.tsx +++ b/ts/state/smart/Preferences.preload.tsx @@ -12,6 +12,7 @@ import { useConversationsActions } from '../ducks/conversations.preload.js'; import { getConversationsWithCustomColorSelector, getMe, + getOtherTabsUnreadStats, } from '../selectors/conversations.dom.js'; import { getCustomColors, @@ -68,9 +69,9 @@ import { useUpdatesActions } from '../ducks/updates.preload.js'; import { getUpdateDialogType } from '../selectors/updates.std.js'; import { getHasAnyFailedStorySends } from '../selectors/stories.preload.js'; import { - getOtherTabsUnreadStats, + getSelectedConversationId, getSelectedLocation, -} from '../selectors/nav.preload.js'; +} from '../selectors/nav.std.js'; import { getPreferredBadgeSelector } from '../selectors/badges.preload.js'; import { SmartProfileEditor } from './ProfileEditor.preload.js'; import { useNavActions } from '../ducks/nav.std.js'; @@ -273,8 +274,7 @@ export function SmartPreferences(): React.JSX.Element | null { account.captureChange('universalExpireTimer'); // Add a notification to the currently open conversation - const state = window.reduxStore.getState(); - const selectedId = state.conversations.selectedConversationId; + const selectedId = getSelectedConversationId(window.reduxStore.getState()); if (selectedId) { const conversation = window.ConversationController.get(selectedId); assertDev(conversation, "Conversation wasn't found"); diff --git a/ts/state/smart/ProfileEditor.preload.tsx b/ts/state/smart/ProfileEditor.preload.tsx index 992020e561..71940e989e 100644 --- a/ts/state/smart/ProfileEditor.preload.tsx +++ b/ts/state/smart/ProfileEditor.preload.tsx @@ -27,7 +27,7 @@ import { getUsernameLinkState, } from '../selectors/username.std.js'; import { SmartUsernameEditor } from './UsernameEditor.preload.js'; -import { getSelectedLocation } from '../selectors/nav.preload.js'; +import { getSelectedLocation } from '../selectors/nav.std.js'; import { useNavActions } from '../ducks/nav.std.js'; import { NavTab, SettingsPage } from '../../types/Nav.std.js'; diff --git a/ts/state/smart/StoriesTab.preload.tsx b/ts/state/smart/StoriesTab.preload.tsx index 3909b8c8eb..6825ab472b 100644 --- a/ts/state/smart/StoriesTab.preload.tsx +++ b/ts/state/smart/StoriesTab.preload.tsx @@ -8,7 +8,10 @@ import { renderToastManagerWithoutMegaphone } from './ToastManager.preload.js'; import { StoriesTab } from '../../components/StoriesTab.dom.js'; import { getMaximumOutgoingAttachmentSizeInKb } from '../../types/AttachmentSize.std.js'; import type { ConfigKeyType } from '../../RemoteConfig.dom.js'; -import { getMe } from '../selectors/conversations.dom.js'; +import { + getMe, + getOtherTabsUnreadStats, +} from '../selectors/conversations.dom.js'; import { getIntl, getTheme } from '../selectors/user.std.js'; import { getPreferredBadgeSelector } from '../selectors/badges.preload.js'; import { @@ -30,7 +33,7 @@ import { useToastActions } from '../ducks/toast.preload.js'; import { useAudioPlayerActions } from '../ducks/audioPlayer.preload.js'; import { useItemsActions } from '../ducks/items.preload.js'; import { getHasPendingUpdate } from '../selectors/updates.std.js'; -import { getOtherTabsUnreadStats } from '../selectors/nav.preload.js'; + import { getIsStoriesSettingsVisible } from '../selectors/globalModals.std.js'; import type { StoryViewType } from '../../types/Stories.std.js'; import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal.dom.js'; diff --git a/ts/state/smart/Timeline.preload.tsx b/ts/state/smart/Timeline.preload.tsx index f10695244b..9b27ad0349 100644 --- a/ts/state/smart/Timeline.preload.tsx +++ b/ts/state/smart/Timeline.preload.tsx @@ -13,9 +13,9 @@ import { getHasContactSpoofingReview, getInvitedContactsForNewlyCreatedGroup, getMessages, - getSelectedConversationId, getTargetedMessage, } from '../selectors/conversations.dom.js'; +import { getSelectedConversationId } from '../selectors/nav.std.js'; import { getIntl, getTheme } from '../selectors/user.std.js'; import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog.preload.js'; import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog.preload.js'; diff --git a/ts/state/smart/TimelineItem.preload.tsx b/ts/state/smart/TimelineItem.preload.tsx index ba0ae2ad6c..39a50897b0 100644 --- a/ts/state/smart/TimelineItem.preload.tsx +++ b/ts/state/smart/TimelineItem.preload.tsx @@ -42,6 +42,7 @@ import { renderReactionPicker } from './renderReactionPicker.dom.js'; import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation.dom.js'; import { TargetedMessageSource } from '../ducks/conversationsEnums.std.js'; import type { MessageInteractivity } from '../../components/conversation/Message.dom.js'; +import { useNavActions } from '../ducks/nav.std.js'; import { DataReader } from '../../sql/Client.preload.js'; import { isInternalFeaturesEnabled } from '../../util/isInternalFeaturesEnabled.dom.js'; @@ -137,7 +138,6 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( messageExpanded, onPinnedMessageRemove, openGiftBadge, - pushPanelForConversation, retryDeleteForEveryone, retryMessageSend, saveAttachment, @@ -154,6 +154,8 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( toggleSelectMessage, } = useConversationsActions(); + const { pushPanelForConversation } = useNavActions(); + const { endPoll, reactToMessage, diff --git a/ts/state/smart/ToastManager.preload.tsx b/ts/state/smart/ToastManager.preload.tsx index adace9f3f2..2e39729af3 100644 --- a/ts/state/smart/ToastManager.preload.tsx +++ b/ts/state/smart/ToastManager.preload.tsx @@ -16,11 +16,11 @@ import { import { hasSelectedStoryData } from '../selectors/stories.preload.js'; import { shouldShowLightbox } from '../selectors/lightbox.std.js'; import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling.std.js'; -import { getSelectedNavTab } from '../selectors/nav.preload.js'; import { - getMe, getSelectedConversationId, -} from '../selectors/conversations.dom.js'; + getSelectedNavTab, +} from '../selectors/nav.std.js'; +import { getMe } from '../selectors/conversations.dom.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; import { useCallingActions } from '../ducks/calling.preload.js'; import { useToastActions } from '../ducks/toast.preload.js'; diff --git a/ts/test-electron/services/AttachmentDownloadManager_test.preload.ts b/ts/test-electron/services/AttachmentDownloadManager_test.preload.ts index f1f5b569ec..bd3b6e46b8 100644 --- a/ts/test-electron/services/AttachmentDownloadManager_test.preload.ts +++ b/ts/test-electron/services/AttachmentDownloadManager_test.preload.ts @@ -669,7 +669,7 @@ describe('AttachmentDownloadManager', () => { ); assert.strictEqual(runJob.callCount, 1); }); - it('will not retry a job if manually cancelled', async () => { + it('will not retry a job if manually canceled', async () => { const jobs = await addJobs(1); const jobAttempts = getPromisesForAttempts(jobs[0], 2); @@ -679,7 +679,7 @@ describe('AttachmentDownloadManager', () => { await jobAttempts[0].started; await downloadStarted.promise; - // user-cancelled behavior + // user-canceled behavior downloadManager?.cancelJobs(JobCancelReason.UserInitiated, () => true); await assert.isRejected(jobAttempts[0].completed as Promise); diff --git a/ts/test-electron/state/ducks/conversations_test.preload.ts b/ts/test-electron/state/ducks/conversations_test.preload.ts index 49beda0dbe..f321ae8983 100644 --- a/ts/test-electron/state/ducks/conversations_test.preload.ts +++ b/ts/test-electron/state/ducks/conversations_test.preload.ts @@ -476,52 +476,7 @@ describe('both/state/ducks/conversations', () => { } describe('showConversation', () => { - it('does not select a conversation if it does not exist', () => { - const state = { - ...getEmptyState(), - }; - const dispatch = sinon.spy(); - showConversation({ conversationId: 'abc123' })( - dispatch, - getEmptyRootState, - null - ); - const action = dispatch.getCall(0).args[0]; - const nextState = reducer(state, action); - - assert.isUndefined(nextState.selectedConversationId); - assert.isUndefined(nextState.targetedMessage); - }); - - it('selects a conversation id', () => { - const conversation = getDefaultConversation({ - id: 'abc123', - }); - const state = { - ...getEmptyState(), - conversationLookup: { - [conversation.id]: conversation, - }, - }; - const dispatch = sinon.spy(); - showConversation({ conversationId: 'abc123' })( - dispatch, - getEmptyRootState, - null - ); - const action = dispatch - .getCalls() - .map(call => call.args[0]) - .find(a => { - return a.type === TARGETED_CONVERSATION_CHANGED; - }); - const nextState = reducer(state, action); - - assert.equal(nextState.selectedConversationId, 'abc123'); - assert.isUndefined(nextState.targetedMessage); - }); - - it('selects a conversation and a message', () => { + it('selects a conversation and a message', async () => { const conversation = getDefaultConversation({ id: 'abc123', }); @@ -533,7 +488,7 @@ describe('both/state/ducks/conversations', () => { }; const dispatch = sinon.spy(); - showConversation({ + await showConversation({ conversationId: 'abc123', messageId: 'xyz987', })(dispatch, getEmptyRootState, null); @@ -543,16 +498,15 @@ describe('both/state/ducks/conversations', () => { .find(a => a.type === TARGETED_CONVERSATION_CHANGED); const nextState = reducer(state, action); - assert.equal(nextState.selectedConversationId, 'abc123'); assert.equal(nextState.targetedMessage, 'xyz987'); }); describe('showConversation switchToAssociatedView=true', () => { let action: TargetedConversationChangedActionType; - beforeEach(() => { + beforeEach(async () => { const dispatch = sinon.spy(); - showConversation({ + await showConversation({ conversationId: 'fake-conversation-id', switchToAssociatedView: true, })(dispatch, getEmptyRootState, null); @@ -2692,68 +2646,6 @@ describe('both/state/ducks/conversations', () => { }; assert.deepEqual(actual, expected); }); - - it('updates root state if conversation is selected', () => { - const conversation1 = getDefaultConversation({ isArchived: true }); - const conversation2 = getDefaultConversation(); - strictAssert(conversation1.serviceId, 'must exist'); - strictAssert(conversation1.e164, 'must exist'); - strictAssert(conversation2.serviceId, 'must exist'); - strictAssert(conversation2.e164, 'must exist'); - - const state: ConversationsStateType = { - ...getEmptyState(), - selectedConversationId: conversation1.id, - showArchived: true, - conversationLookup: { - [conversation1.id]: conversation1, - [conversation2.id]: conversation2, - }, - conversationsByE164: { - [conversation1.e164]: conversation1, - [conversation2.e164]: conversation2, - }, - conversationsByServiceId: { - [conversation1.serviceId]: conversation1, - [conversation2.serviceId]: conversation2, - }, - }; - - const updatedConversation1 = { - ...conversation1, - isArchived: false, - }; - const updatedConversation2 = { - ...conversation2, - active_at: 12345, - }; - - const action: ConversationsUpdatedActionType = { - type: 'CONVERSATIONS_UPDATED', - payload: { - data: [updatedConversation1, updatedConversation2], - }, - }; - - const actual = reducer(state, action); - const expected: ConversationsStateType = { - ...state, - showArchived: false, - conversationLookup: { - [conversation1.id]: updatedConversation1, - [conversation2.id]: updatedConversation2, - }, - conversationsByE164: { - [conversation1.e164]: updatedConversation1, - [conversation2.e164]: updatedConversation2, - }, - conversationsByServiceId: { - [conversation1.serviceId]: updatedConversation1, - [conversation2.serviceId]: updatedConversation2, - }, - }; - assert.deepEqual(actual, expected); - }); }); }); }); diff --git a/ts/test-mock/storage/conflict_test.node.ts b/ts/test-mock/storage/conflict_test.node.ts index 245daf889d..0d2bb8df6c 100644 --- a/ts/test-mock/storage/conflict_test.node.ts +++ b/ts/test-mock/storage/conflict_test.node.ts @@ -110,9 +110,11 @@ describe('storage service', function (this: Mocha.Suite) { await app.waitForManifestVersion(archivedVersion); debug('waiting for archived chats to appear again'); - await leftPane.getByLabel('Archived Chats').waitFor(); + await leftPane.getByLabel('Archived Chats').click(); + + // Conversation was closed on re-archive - need to open it again + await leftPane.locator(`[data-testid="${testid}"]`).click(); - // Conversation should be still open await conversationStack .getByRole('button', { name: 'More Info' }) .click(); diff --git a/ts/test-node/state/ducks/composer_test.preload.ts b/ts/test-node/state/ducks/composer_test.preload.ts index ac3dcb4c23..f15b228829 100644 --- a/ts/test-node/state/ducks/composer_test.preload.ts +++ b/ts/test-node/state/ducks/composer_test.preload.ts @@ -19,6 +19,7 @@ import { reducer as rootReducer } from '../../../state/reducer.preload.js'; import { IMAGE_JPEG } from '../../../types/MIME.std.js'; import type { AttachmentDraftType } from '../../../types/Attachment.std.js'; import { fakeDraftAttachment } from '../../../test-helpers/fakeAttachment.std.js'; +import { NavTab } from '../../../types/Nav.std.js'; const { noop } = lodash; @@ -36,13 +37,20 @@ describe('both/state/ducks/composer', () => { }, }; - function getRootStateFunction(selectedConversationId?: string) { + function getRootStateFunction(conversationId?: string) { const state = rootReducer(undefined, noopAction()); return () => ({ ...state, + nav: { + selectedLocation: { + tab: NavTab.Chats as const, + details: { + conversationId, + }, + }, + }, conversations: { ...state.conversations, - selectedConversationId, }, }); } diff --git a/ts/test-node/state/selectors/conversations_test.preload.ts b/ts/test-node/state/selectors/conversations_test.preload.ts index d920e97287..76e5375bd6 100644 --- a/ts/test-node/state/selectors/conversations_test.preload.ts +++ b/ts/test-node/state/selectors/conversations_test.preload.ts @@ -39,7 +39,6 @@ import { getMaximumGroupSizeModalState, getPlaceholderContact, getRecommendedGroupSizeModalState, - getSelectedConversationId, hasGroupCreationError, isCreatingGroup, } from '../../../state/selectors/conversations.dom.js'; @@ -1649,35 +1648,6 @@ describe('both/state/selectors/conversations-extra', () => { }); }); - describe('#getSelectedConversationId', () => { - it('returns undefined if no conversation is selected', () => { - const state = { - ...getEmptyRootState(), - conversations: { - ...getEmptyState(), - conversationLookup: { - abc123: makeConversation('abc123'), - }, - }, - }; - assert.isUndefined(getSelectedConversationId(state)); - }); - - it('returns the selected conversation ID', () => { - const state = { - ...getEmptyRootState(), - conversations: { - ...getEmptyState(), - conversationLookup: { - abc123: makeConversation('abc123'), - }, - selectedConversationId: 'abc123', - }, - }; - assert.strictEqual(getSelectedConversationId(state), 'abc123'); - }); - }); - describe('#getContactNameColorSelector', () => { it('returns the right color order sorted by UUID ASC', () => { const group: ConversationType = { diff --git a/ts/test-node/state/selectors/search_test.preload.ts b/ts/test-node/state/selectors/search_test.preload.ts index a0a1994d26..efcd23133b 100644 --- a/ts/test-node/state/selectors/search_test.preload.ts +++ b/ts/test-node/state/selectors/search_test.preload.ts @@ -30,6 +30,7 @@ import { ReadStatus } from '../../../messages/MessageReadStatus.std.js'; import type { StateType } from '../../../state/reducer.preload.js'; import { reducer as rootReducer } from '../../../state/reducer.preload.js'; +import { NavTab } from '../../../types/Nav.std.js'; describe('both/state/selectors/search', () => { const NOW = 1_000_000; @@ -463,10 +464,17 @@ describe('both/state/selectors/search', () => { const state: StateType = { ...getEmptyRootState(), + nav: { + selectedLocation: { + tab: NavTab.Chats, + details: { + conversationId: 'selected-id', + }, + }, + }, conversations: { ...getEmptyConversationState(), conversationLookup: makeLookup(conversations, 'id'), - selectedConversationId: 'selected-id', }, search: { ...getEmptySearchState(), @@ -502,10 +510,17 @@ describe('both/state/selectors/search', () => { const state: StateType = { ...getEmptyRootState(), + nav: { + selectedLocation: { + tab: NavTab.Chats, + details: { + conversationId: '2', + }, + }, + }, conversations: { ...getEmptyConversationState(), conversationLookup: makeLookup(conversations, 'id'), - selectedConversationId: '2', }, search: { ...getEmptySearchState(), diff --git a/ts/types/Nav.std.ts b/ts/types/Nav.std.ts index d9cfa4d9c4..29c1d22b40 100644 --- a/ts/types/Nav.std.ts +++ b/ts/types/Nav.std.ts @@ -6,24 +6,30 @@ import type { ChatFolderId, ChatFolderParams } from './ChatFolder.std.js'; import type { PanelArgsType } from './Panels.std.js'; export type Location = ReadonlyDeep< + | { + tab: NavTab.Chats; + details: ChatDetails; + } | { tab: NavTab.Settings; details: SettingsLocation; } - | { tab: Exclude } + | { tab: Exclude } >; export type ChatDetails = ReadonlyDeep<{ conversationId?: string; - panels?: { - isAnimating: boolean; - wasAnimated: boolean; - direction: 'push' | 'pop' | undefined; - stack: ReadonlyArray; - watermark: number; - }; + panels?: PanelInfo; }>; +export type PanelInfo = { + isAnimating: boolean; + wasAnimated: boolean; + direction: 'push' | 'pop' | undefined; + stack: ReadonlyArray; + watermark: number; +}; + export type SettingsLocation = ReadonlyDeep< | { page: SettingsPage.Profile; diff --git a/ts/windows/main/start.preload.ts b/ts/windows/main/start.preload.ts index b64eca120d..48bd31393e 100644 --- a/ts/windows/main/start.preload.ts +++ b/ts/windows/main/start.preload.ts @@ -32,6 +32,7 @@ import { isProduction } from '../../util/version.std.js'; import { benchmarkConversationOpen } from '../../CI/benchmarkConversationOpen.preload.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import { IMAGE_PNG } from '../../types/MIME.std.js'; +import { getSelectedConversationId } from '../../state/selectors/nav.std.js'; const { has } = lodash; @@ -63,13 +64,15 @@ if ( const SignalDebug = { cdsLookup: (options: CdsLookupOptionsType) => cdsLookup(options), getSelectedConversation: () => { - const conversationId = - window.reduxStore.getState().conversations.selectedConversationId; + const conversationId = getSelectedConversationId( + window.reduxStore.getState() + ); return window.ConversationController.get(conversationId)?.attributes; }, archiveSessionsForCurrentConversation: async () => { - const conversationId = - window.reduxStore.getState().conversations.selectedConversationId; + const conversationId = getSelectedConversationId( + window.reduxStore.getState() + ); await window.ConversationController.archiveSessionsForConversation( conversationId ); @@ -115,8 +118,9 @@ if ( calling._iceServerOverride = override; }, sendViewOnceImageInSelectedConversation: async () => { - const conversationId = - window.reduxStore.getState().conversations.selectedConversationId; + const conversationId = getSelectedConversationId( + window.reduxStore.getState() + ); const conversation = window.ConversationController.get(conversationId); if (!conversation) { throw new Error('No conversation selected');