From aa23c2def2795980db4d0ee05b9aea640eeead57 Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Thu, 16 Jun 2022 15:12:50 -0400 Subject: [PATCH] Removes Inbox Backbone view --- _locales/en/messages.json | 3 - background.html | 18 -- test/index.html | 18 -- ts/backbone/index.ts | 6 - ts/backbone/views/Lightbox.ts | 25 -- ts/backbone/views/index.ts | 6 - ts/background.ts | 4 + ts/components/App.tsx | 30 ++- ts/components/ConversationList.stories.tsx | 2 +- ts/components/ConversationList.tsx | 3 +- ts/components/Inbox.tsx | 241 +++++++++++++++--- ts/components/LeftPane.stories.tsx | 3 +- ts/components/LeftPane.tsx | 47 ++-- ts/components/LeftPaneSearchInput.tsx | 10 +- ts/components/Stories.stories.tsx | 2 +- ts/components/Stories.tsx | 9 +- ts/components/StoriesPane.tsx | 9 +- .../conversation/ContactModal.stories.tsx | 2 +- ts/components/conversation/ContactModal.tsx | 17 +- .../MessageSearchResult.stories.tsx | 2 +- .../conversationList/MessageSearchResult.tsx | 16 +- .../conversationList/StartNewConversation.tsx | 5 +- .../UsernameSearchResultListItem.tsx | 5 +- .../leftPane/LeftPaneArchiveHelper.tsx | 8 +- ts/components/leftPane/LeftPaneHelper.tsx | 4 +- .../leftPane/LeftPaneInboxHelper.tsx | 8 +- .../leftPane/LeftPaneSearchHelper.tsx | 14 +- ts/groups/joinViaLink.tsx | 8 +- ts/signal.ts | 4 - ts/state/ducks/audioPlayer.ts | 7 +- ts/state/ducks/conversations.ts | 142 ++++------- ts/state/ducks/search.ts | 3 +- ts/state/roots/createLeftPane.tsx | 15 -- ts/state/smart/App.tsx | 4 + ts/state/smart/Stories.tsx | 5 +- ts/state/smart/StoryViewer.tsx | 5 +- ts/test-both/state/ducks/audioPlayer_test.ts | 13 +- .../state/ducks/conversations_test.ts | 121 +++------ ts/util/lint/exceptions.json | 108 +------- ts/util/showLightbox.tsx | 39 +++ ts/views/conversation_view.tsx | 78 ++---- ts/views/inbox_view.tsx | 231 ----------------- ts/window.d.ts | 3 - ts/windows/main/phase3-post-signal.ts | 1 - 44 files changed, 496 insertions(+), 808 deletions(-) delete mode 100644 ts/backbone/index.ts delete mode 100644 ts/backbone/views/Lightbox.ts delete mode 100644 ts/backbone/views/index.ts delete mode 100644 ts/state/roots/createLeftPane.tsx create mode 100644 ts/util/showLightbox.tsx delete mode 100644 ts/views/inbox_view.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index eb1fcc060..21a073772 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -862,9 +862,6 @@ "message": "what's new", "description": "Clickable link that displays the latest release notes" }, - "selectAContact": { - "message": "Select a contact or group to start chatting." - }, "typingAlt": { "message": "Typing animation for this conversation", "description": "Used as the 'title' attribute for the typing animation" diff --git a/background.html b/background.html index c04ae6230..57f1bb46a 100644 --- a/background.html +++ b/background.html @@ -106,24 +106,6 @@ - - diff --git a/test/index.html b/test/index.html index c3f78b45b..5b144543c 100644 --- a/test/index.html +++ b/test/index.html @@ -26,24 +26,6 @@ - - diff --git a/ts/backbone/index.ts b/ts/backbone/index.ts deleted file mode 100644 index 877381f87..000000000 --- a/ts/backbone/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as Views from './views'; - -export { Views }; diff --git a/ts/backbone/views/Lightbox.ts b/ts/backbone/views/Lightbox.ts deleted file mode 100644 index 47a7797f4..000000000 --- a/ts/backbone/views/Lightbox.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -export const show = (element: HTMLElement): void => { - const container: HTMLDivElement | null = document.querySelector( - '.lightbox-container' - ); - if (!container) { - throw new TypeError("'.lightbox-container' is required"); - } - container.innerHTML = ''; - container.style.display = 'block'; - container.appendChild(element); -}; - -export const hide = (): void => { - const container: HTMLDivElement | null = document.querySelector( - '.lightbox-container' - ); - if (!container) { - return; - } - container.innerHTML = ''; - container.style.display = 'none'; -}; diff --git a/ts/backbone/views/index.ts b/ts/backbone/views/index.ts deleted file mode 100644 index 5271224f8..000000000 --- a/ts/backbone/views/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as Lightbox from './Lightbox'; - -export { Lightbox }; diff --git a/ts/background.ts b/ts/background.ts index e20b7464a..6ac47cdb3 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1508,6 +1508,10 @@ export async function startApp(): Promise { (key === 'c' || key === 'C') ) { conversation.trigger('unload', 'keyboard shortcut close'); + window.reduxActions.conversations.showConversation({ + conversationId: undefined, + messageId: undefined, + }); event.preventDefault(); event.stopPropagation(); return; diff --git a/ts/components/App.tsx b/ts/components/App.tsx index 5a78fe564..68540021c 100644 --- a/ts/components/App.tsx +++ b/ts/components/App.tsx @@ -48,30 +48,35 @@ export const App = ({ appView, cancelConversationVerification, conversationsStoppingSend, - hasInitialLoadCompleted, + executeMenuAction, + executeMenuRole, getPreferredBadge, + hasInitialLoadCompleted, i18n, isCustomizingPreferredReactions, - isShowingStoriesView, - isMaximized, isFullScreen, + isMaximized, + isShowingStoriesView, isWindows11, - menuOptions, - platform, localeMessages, + menuOptions, + openInbox, + platform, + registerSingleDevice, renderCallManager, renderCustomizingPreferredReactionsModal, renderGlobalModalContainer, + renderLeftPane, renderSafetyNumber, - openInbox, renderStories, requestVerification, - registerSingleDevice, + selectedConversationId, + selectedMessage, + showConversation, + showWhatsNewModal, theme, - verifyConversationsStoppingSend, - executeMenuAction, - executeMenuRole, titleBarDoubleClick, + verifyConversationsStoppingSend, }: PropsType): JSX.Element => { let contents; @@ -101,7 +106,12 @@ export const App = ({ renderCustomizingPreferredReactionsModal={ renderCustomizingPreferredReactionsModal } + renderLeftPane={renderLeftPane} renderSafetyNumber={renderSafetyNumber} + selectedConversationId={selectedConversationId} + selectedMessage={selectedMessage} + showConversation={showConversation} + showWhatsNewModal={showWhatsNewModal} theme={theme} verifyConversationsStoppingSend={verifyConversationsStoppingSend} /> diff --git a/ts/components/ConversationList.stories.tsx b/ts/components/ConversationList.stories.tsx index b84cf709a..1e0cbb58c 100644 --- a/ts/components/ConversationList.stories.tsx +++ b/ts/components/ConversationList.stories.tsx @@ -79,8 +79,8 @@ const Wrapper = ({ getPreferredBadge={() => undefined} i18n={i18n} id={id} - openConversationInternal={action('openConversationInternal')} sentAt={1587358800000} + showConversation={action('showConversation')} snippet="Lorem <>ipsum<> wow" theme={ThemeType.light} to={defaultConversations[1]} diff --git a/ts/components/ConversationList.tsx b/ts/components/ConversationList.tsx index 64dd8ffb3..a314d5e55 100644 --- a/ts/components/ConversationList.tsx +++ b/ts/components/ConversationList.tsx @@ -16,6 +16,7 @@ import { ScrollBehavior } from '../types/Util'; import { getConversationListWidthBreakpoint } from './_util'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid'; +import type { ShowConversationType } from '../state/ducks/conversations'; import type { PropsData as ConversationListItemPropsType } from './conversationList/ConversationListItem'; import { ConversationListItem } from './conversationList/ConversationListItem'; @@ -154,7 +155,7 @@ export type PropsType = { onSelectConversation: (conversationId: string, messageId?: string) => void; renderMessageSearchResult: (id: string) => JSX.Element; showChooseGroupMembers: () => void; - showConversation: (conversationId: string) => void; + showConversation: ShowConversationType; } & LookupConversationWithoutUuidActionsType; const NORMAL_ROW_HEIGHT = 76; diff --git a/ts/components/Inbox.tsx b/ts/components/Inbox.tsx index 826674f42..4e8b397e8 100644 --- a/ts/components/Inbox.tsx +++ b/ts/components/Inbox.tsx @@ -2,22 +2,25 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ReactNode } from 'react'; -import React, { useEffect, useRef } from 'react'; -import type * as Backbone from 'backbone'; -import type { SafetyNumberProps } from './SafetyNumberChangeDialog'; -import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog'; -import type { ConversationType } from '../state/ducks/conversations'; -import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; +import React, { useEffect, useRef, useState } from 'react'; + +import type { ConversationModel } from '../models/conversations'; +import type { + ConversationType, + ShowConversationType, +} from '../state/ducks/conversations'; +import type { ConversationView } from '../views/conversation_view'; import type { LocalizerType, ThemeType } from '../types/Util'; +import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; +import type { SafetyNumberProps } from './SafetyNumberChangeDialog'; -type InboxViewType = Backbone.View & { - onEmpty?: () => void; -}; - -type InboxViewOptionsType = Backbone.ViewOptions & { - initialLoadComplete: boolean; - window: typeof window; -}; +import * as log from '../logging/log'; +import { SECOND } from '../util/durations'; +import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog'; +import { ToastStickerPackInstallFailed } from './ToastStickerPackInstallFailed'; +import { WhatsNewLink } from './WhatsNewLink'; +import { showToast } from '../util/showToast'; +import { strictAssert } from '../util/assert'; export type PropsType = { cancelConversationVerification: () => void; @@ -27,7 +30,12 @@ export type PropsType = { i18n: LocalizerType; isCustomizingPreferredReactions: boolean; renderCustomizingPreferredReactionsModal: () => JSX.Element; + renderLeftPane: () => JSX.Element; renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element; + selectedConversationId?: string; + selectedMessage?: string; + showConversation: ShowConversationType; + showWhatsNewModal: () => unknown; theme: ThemeType; verifyConversationsStoppingSend: () => void; }; @@ -40,38 +48,182 @@ export const Inbox = ({ i18n, isCustomizingPreferredReactions, renderCustomizingPreferredReactionsModal, + renderLeftPane, renderSafetyNumber, + selectedConversationId, + selectedMessage, + showConversation, + showWhatsNewModal, theme, verifyConversationsStoppingSend, }: PropsType): JSX.Element => { - const hostRef = useRef(null); - const viewRef = useRef(undefined); + const [loadingMessageCount, setLoadingMessageCount] = useState(0); + const [internalHasInitialLoadCompleted, setInternalHasInitialLoadCompleted] = + useState(hasInitialLoadCompleted); + + const conversationMountRef = useRef(null); + const conversationViewRef = useRef(null); + + const [prevConversation, setPrevConversation] = useState< + ConversationModel | undefined + >(); useEffect(() => { - const viewOptions: InboxViewOptionsType = { - el: hostRef.current, - initialLoadComplete: false, - window, - }; - const view = new window.Whisper.InboxView(viewOptions); + if (!selectedConversationId) { + return; + } - viewRef.current = view; + const conversation = window.ConversationController.get( + selectedConversationId + ); + strictAssert(conversation, 'Conversation must be found'); + + conversation.setMarkedUnread(false); + + if (!prevConversation || prevConversation.id !== selectedConversationId) { + // We create a mount point because when calling .remove() on the Backbone + // view it'll also remove the mount point along with it. + const viewMountNode = document.createElement('div'); + conversationMountRef.current?.appendChild(viewMountNode); + + // Make sure to unload the previous conversation along with calling + // Backbone's remove so that it is taken out of the DOM. + if (prevConversation) { + prevConversation.trigger('unload', 'opened another conversation'); + } + conversationViewRef.current?.remove(); + + // Can't import ConversationView directly because conversation_view + // needs access to window.Signal first. + const view = new window.Whisper.ConversationView({ + el: viewMountNode, + model: conversation, + }); + conversationViewRef.current = view; + + setPrevConversation(conversation); + + conversation.trigger('opened', selectedMessage); + } else if (selectedMessage) { + conversation.trigger('scroll-to-message', selectedMessage); + } + + // Make sure poppers are positioned properly + window.dispatchEvent(new Event('resize')); + }, [prevConversation, selectedConversationId, selectedMessage]); + + // Whenever the selectedConversationId is cleared we should also ensure + // that prevConversation is cleared too. + useEffect(() => { + if (prevConversation && !selectedConversationId) { + setPrevConversation(undefined); + } + }, [prevConversation, selectedConversationId]); + + useEffect(() => { + function refreshConversation({ + newId, + oldId, + }: { + newId: string; + oldId: string; + }) { + if (prevConversation && prevConversation.get('id') === oldId) { + showConversation({ conversationId: newId }); + } + } + + // Close current opened conversation to reload the group information once + // linked. + function unload() { + if (!prevConversation) { + return; + } + prevConversation.trigger('unload', 'force unload requested'); + } + + function onShowConversation(id: string, messageId?: string): void { + showConversation({ conversationId: id, messageId }); + } + + function packInstallFailed() { + showToast(ToastStickerPackInstallFailed); + } + + window.Whisper.events.on('loadingProgress', setLoadingMessageCount); + window.Whisper.events.on('pack-install-failed', packInstallFailed); + window.Whisper.events.on('refreshConversation', refreshConversation); + window.Whisper.events.on('setupAsNewDevice', unload); + window.Whisper.events.on('showConversation', onShowConversation); return () => { - // [`Backbone.View.prototype.remove`][0] removes the DOM element and stops listening - // to event listeners. Because React will do the first, we only want to do the - // second. - // [0]: https://github.com/jashkenas/backbone/blob/153dc41616a1f2663e4a86b705fefd412ecb4a7a/backbone.js#L1336-L1342 - viewRef.current?.stopListening(); - viewRef.current = undefined; + window.Whisper.events.off('loadingProgress', setLoadingMessageCount); + window.Whisper.events.off('pack-install-failed', packInstallFailed); + window.Whisper.events.off('refreshConversation', refreshConversation); + window.Whisper.events.off('setupAsNewDevice', unload); + window.Whisper.events.off('showConversation', onShowConversation); }; - }, []); + }, [prevConversation, showConversation]); useEffect(() => { - if (hasInitialLoadCompleted && viewRef.current && viewRef.current.onEmpty) { - viewRef.current.onEmpty(); + if (internalHasInitialLoadCompleted) { + return; } - }, [hasInitialLoadCompleted, viewRef]); + + const interval = setInterval(() => { + const status = window.getSocketStatus(); + switch (status) { + case 'CONNECTING': + break; + case 'OPEN': + // if we've connected, we can wait for real empty event + clearInterval(interval); + break; + case 'CLOSING': + case 'CLOSED': + clearInterval(interval); + // if we failed to connect, we pretend we loaded + setInternalHasInitialLoadCompleted(true); + break; + default: + log.warn( + `startConnectionListener: Found unexpected socket status ${status}; setting load to done manually.` + ); + setInternalHasInitialLoadCompleted(true); + break; + } + }, SECOND); + + return () => { + clearInterval(interval); + }; + }, [internalHasInitialLoadCompleted]); + + useEffect(() => { + setInternalHasInitialLoadCompleted(hasInitialLoadCompleted); + }, [hasInitialLoadCompleted]); + + if (!internalHasInitialLoadCompleted) { + return ( +
+
+ +
+
+
+ + + +
+
+ {loadingMessageCount + ? i18n('loadingMessages', [String(loadingMessageCount)]) + : i18n('loading')} +
+
+
+ ); + } let activeModal: ReactNode; if (conversationsStoppingSend.length) { @@ -94,7 +246,28 @@ export const Inbox = ({ return ( <> -
+
+
+ +
{renderLeftPane()}
+ +
+
+
+ {!prevConversation && ( +
+
+

{i18n('welcomeToSignal')}

+

+ +

+
+ )} +
+
{activeModal} ); diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index 0d39cfcce..c34c5e027 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -124,7 +124,6 @@ const useProps = (overrideProps: Partial = {}): PropsType => { getPreferredBadge: () => undefined, i18n, preferredWidthFromStorage: 320, - openConversationInternal: action('openConversationInternal'), regionCode: 'US', challengeStatus: select( 'challengeStatus', @@ -148,7 +147,7 @@ const useProps = (overrideProps: Partial = {}): PropsType => { getPreferredBadge={() => undefined} i18n={i18n} id={id} - openConversationInternal={action('openConversationInternal')} + showConversation={action('showConversation')} sentAt={1587358800000} snippet="Lorem <>ipsum<> wow" theme={ThemeType.light} diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index b0603f1d6..5120d5746 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -39,7 +39,7 @@ import { getWidthFromPreferredWidth, } from '../util/leftPaneWidth'; import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid'; -import type { OpenConversationInternalType } from '../state/ducks/conversations'; +import type { ShowConversationType } from '../state/ducks/conversations'; import { ConversationList } from './ConversationList'; import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox'; @@ -99,25 +99,25 @@ export type PropsType = { clearSearch: () => void; closeMaximumGroupSizeModal: () => void; closeRecommendedGroupSizeModal: () => void; - createGroup: () => void; - openConversationInternal: OpenConversationInternalType; - savePreferredLeftPaneWidth: (_: number) => void; - searchInConversation: (conversationId: string) => unknown; - setComposeSearchTerm: (composeSearchTerm: string) => void; - setComposeGroupAvatar: (_: undefined | Uint8Array) => void; - setComposeGroupName: (_: string) => void; - setComposeGroupExpireTimer: (_: number) => void; - showArchivedConversations: () => void; - showInbox: () => void; - startComposing: () => void; - startSearch: () => unknown; - showChooseGroupMembers: () => void; - startSettingGroupMetadata: () => void; - toggleConversationInChooseMembers: (conversationId: string) => void; composeDeleteAvatarFromDisk: DeleteAvatarFromDiskActionType; composeReplaceAvatar: ReplaceAvatarActionType; composeSaveAvatarToDisk: SaveAvatarToDiskActionType; + createGroup: () => void; + savePreferredLeftPaneWidth: (_: number) => void; + searchInConversation: (conversationId: string) => unknown; + setComposeGroupAvatar: (_: undefined | Uint8Array) => void; + setComposeGroupExpireTimer: (_: number) => void; + setComposeGroupName: (_: string) => void; + setComposeSearchTerm: (composeSearchTerm: string) => void; + showArchivedConversations: () => void; + showChooseGroupMembers: () => void; + showConversation: ShowConversationType; + showInbox: () => void; + startComposing: () => void; + startSearch: () => unknown; + startSettingGroupMetadata: () => void; toggleComposeEditingAvatar: () => unknown; + toggleConversationInChooseMembers: (conversationId: string) => void; updateSearchTerm: (_: string) => void; // Render Props @@ -137,8 +137,6 @@ export type PropsType = { ) => JSX.Element; renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element; renderCrashReportDialog: () => JSX.Element; - - showConversation: (conversationId: string) => void; } & LookupConversationWithoutUuidActionsType; export const LeftPane: React.FC = ({ @@ -156,7 +154,6 @@ export const LeftPane: React.FC = ({ getPreferredBadge, i18n, modeSpecificProps, - openConversationInternal, preferredWidthFromStorage, renderCaptchaDialog, renderCrashReportDialog, @@ -363,7 +360,7 @@ export const LeftPane: React.FC = ({ if (conversationToOpen) { const { conversationId, messageId } = conversationToOpen; - openConversationInternal({ conversationId, messageId }); + showConversation({ conversationId, messageId }); if (openedByNumber) { clearSearch(); } @@ -383,16 +380,16 @@ export const LeftPane: React.FC = ({ document.removeEventListener('keydown', onKeyDown); }; }, [ + clearSearch, helper, - openConversationInternal, searchInConversation, selectedConversationId, selectedMessageId, showChooseGroupMembers, + showConversation, showInbox, startComposing, startSearch, - clearSearch, ]); const requiresFullWidth = helper.requiresFullWidth(); @@ -488,13 +485,13 @@ export const LeftPane: React.FC = ({ const onSelectConversation = useCallback( (conversationId: string, messageId?: string) => { - openConversationInternal({ + showConversation({ conversationId, messageId, switchToAssociatedView: true, }); }, - [openConversationInternal] + [showConversation] ); const previousSelectedConversationId = usePrevious( @@ -555,7 +552,7 @@ export const LeftPane: React.FC = ({ setComposeSearchTerm(event.target.value); }, updateSearchTerm, - openConversationInternal, + showConversation, })}
{renderExpiredBuildDialog({ diff --git a/ts/components/LeftPaneSearchInput.tsx b/ts/components/LeftPaneSearchInput.tsx index 77bee1e66..3392a9fe7 100644 --- a/ts/components/LeftPaneSearchInput.tsx +++ b/ts/components/LeftPaneSearchInput.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useRef } from 'react'; import type { ConversationType, - OpenConversationInternalType, + ShowConversationType, } from '../state/ducks/conversations'; import type { LocalizerType } from '../types/Util'; import { Avatar, AvatarSize } from './Avatar'; @@ -20,10 +20,10 @@ type PropsType = { searchTerm: string; startSearchCounter: number; updateSearchTerm: (searchTerm: string) => void; - openConversationInternal: OpenConversationInternalType; + showConversation: ShowConversationType; onEnterKeyDown?: ( clearSearch: () => void, - openConversationInternal: OpenConversationInternalType + showConversation: ShowConversationType ) => void; }; @@ -36,7 +36,7 @@ export const LeftPaneSearchInput = ({ searchTerm, startSearchCounter, updateSearchTerm, - openConversationInternal, + showConversation, onEnterKeyDown, }: PropsType): JSX.Element => { const inputRef = useRef(null); @@ -103,7 +103,7 @@ export const LeftPaneSearchInput = ({ }} onKeyDown={event => { if (onEnterKeyDown && event.key === 'Enter') { - onEnterKeyDown(clearSearch, openConversationInternal); + onEnterKeyDown(clearSearch, showConversation); event.preventDefault(); event.stopPropagation(); } diff --git a/ts/components/Stories.stories.tsx b/ts/components/Stories.stories.tsx index 299025510..c22ef475c 100644 --- a/ts/components/Stories.stories.tsx +++ b/ts/components/Stories.stories.tsx @@ -81,10 +81,10 @@ function getAttachmentWithThumbnail(url: string): AttachmentType { const getDefaultProps = (): PropsType => ({ hiddenStories: [], i18n, - openConversationInternal: action('openConversationInternal'), preferredWidthFromStorage: 380, queueStoryDownload: action('queueStoryDownload'), renderStoryViewer: () =>
, + showConversation: action('showConversation'), stories: [ createStory({ attachment: getAttachmentWithThumbnail( diff --git a/ts/components/Stories.tsx b/ts/components/Stories.tsx index adb735edf..9a45ff11a 100644 --- a/ts/components/Stories.tsx +++ b/ts/components/Stories.tsx @@ -7,6 +7,7 @@ import classNames from 'classnames'; import type { ConversationStoryType } from './StoryListItem'; import type { LocalizerType } from '../types/Util'; import type { PropsType as SmartStoryViewerPropsType } from '../state/smart/StoryViewer'; +import type { ShowConversationType } from '../state/ducks/conversations'; import { StoriesPane } from './StoriesPane'; import { Theme, themeClassName } from '../util/theme'; import { getWidthFromPreferredWidth } from '../util/leftPaneWidth'; @@ -16,9 +17,9 @@ export type PropsType = { hiddenStories: Array; i18n: LocalizerType; preferredWidthFromStorage: number; - openConversationInternal: (_: { conversationId: string }) => unknown; - renderStoryViewer: (props: SmartStoryViewerPropsType) => JSX.Element; queueStoryDownload: (storyId: string) => unknown; + renderStoryViewer: (props: SmartStoryViewerPropsType) => JSX.Element; + showConversation: ShowConversationType; stories: Array; toggleHideStories: (conversationId: string) => unknown; toggleStoriesView: () => unknown; @@ -27,10 +28,10 @@ export type PropsType = { export const Stories = ({ hiddenStories, i18n, - openConversationInternal, preferredWidthFromStorage, queueStoryDownload, renderStoryViewer, + showConversation, stories, toggleHideStories, toggleStoriesView, @@ -119,8 +120,8 @@ export const Stories = ({ }); setConversationIdToView(clickedIdToView); }} - openConversationInternal={openConversationInternal} queueStoryDownload={queueStoryDownload} + showConversation={showConversation} stories={stories} toggleHideStories={toggleHideStories} toggleStoriesView={toggleStoriesView} diff --git a/ts/components/StoriesPane.tsx b/ts/components/StoriesPane.tsx index 172f3c9d0..14ce6af31 100644 --- a/ts/components/StoriesPane.tsx +++ b/ts/components/StoriesPane.tsx @@ -7,6 +7,7 @@ import classNames from 'classnames'; import { isNotNil } from '../util/isNotNil'; import type { ConversationStoryType, StoryViewType } from './StoryListItem'; import type { LocalizerType } from '../types/Util'; +import type { ShowConversationType } from '../state/ducks/conversations'; import { SearchInput } from './SearchInput'; import { StoryListItem } from './StoryListItem'; @@ -53,8 +54,8 @@ export type PropsType = { hiddenStories: Array; i18n: LocalizerType; onStoryClicked: (conversationId: string) => unknown; - openConversationInternal: (_: { conversationId: string }) => unknown; queueStoryDownload: (storyId: string) => unknown; + showConversation: ShowConversationType; stories: Array; toggleHideStories: (conversationId: string) => unknown; toggleStoriesView: () => unknown; @@ -64,8 +65,8 @@ export const StoriesPane = ({ hiddenStories, i18n, onStoryClicked, - openConversationInternal, queueStoryDownload, + showConversation, stories, toggleHideStories, toggleStoriesView, @@ -121,7 +122,7 @@ export const StoriesPane = ({ }} onHideStory={toggleHideStories} onGoToConversation={conversationId => { - openConversationInternal({ conversationId }); + showConversation({ conversationId }); toggleStoriesView(); }} queueStoryDownload={queueStoryDownload} @@ -150,7 +151,7 @@ export const StoriesPane = ({ }} onHideStory={toggleHideStories} onGoToConversation={conversationId => { - openConversationInternal({ conversationId }); + showConversation({ conversationId }); toggleStoriesView(); }} queueStoryDownload={queueStoryDownload} diff --git a/ts/components/conversation/ContactModal.stories.tsx b/ts/components/conversation/ContactModal.stories.tsx index 67297d555..2dbc60f91 100644 --- a/ts/components/conversation/ContactModal.stories.tsx +++ b/ts/components/conversation/ContactModal.stories.tsx @@ -44,8 +44,8 @@ const createProps = (overrideProps: Partial = {}): PropsType => ({ i18n, isAdmin: boolean('isAdmin', overrideProps.isAdmin || false), isMember: boolean('isMember', overrideProps.isMember || true), - openConversationInternal: action('openConversationInternal'), removeMemberFromGroup: action('removeMemberFromGroup'), + showConversation: action('showConversation'), theme: ThemeType.light, toggleSafetyNumberModal: action('toggleSafetyNumberModal'), toggleAdmin: action('toggleAdmin'), diff --git a/ts/components/conversation/ContactModal.tsx b/ts/components/conversation/ContactModal.tsx index 9c71ff0bf..92e939237 100644 --- a/ts/components/conversation/ContactModal.tsx +++ b/ts/components/conversation/ContactModal.tsx @@ -9,7 +9,10 @@ import { missingCaseError } from '../../util/missingCaseError'; import { About } from './About'; import { Avatar } from '../Avatar'; import { AvatarLightbox } from '../AvatarLightbox'; -import type { ConversationType } from '../../state/ducks/conversations'; +import type { + ConversationType, + ShowConversationType, +} from '../../state/ducks/conversations'; import { Modal } from '../Modal'; import type { LocalizerType, ThemeType } from '../../types/Util'; import { BadgeDialog } from '../BadgeDialog'; @@ -32,14 +35,8 @@ export type PropsDataType = { type PropsActionType = { hideContactModal: () => void; - openConversationInternal: ( - options: Readonly<{ - conversationId: string; - messageId?: string; - switchToAssociatedView?: boolean; - }> - ) => void; removeMemberFromGroup: (conversationId: string, contactId: string) => void; + showConversation: ShowConversationType; toggleAdmin: (conversationId: string, contactId: string) => void; toggleSafetyNumberModal: (conversationId: string) => unknown; updateConversationModelSharedGroups: (conversationId: string) => void; @@ -69,8 +66,8 @@ export const ContactModal = ({ i18n, isAdmin, isMember, - openConversationInternal, removeMemberFromGroup, + showConversation, theme, toggleAdmin, toggleSafetyNumberModal, @@ -205,7 +202,7 @@ export const ContactModal = ({ className="ContactModal__button ContactModal__send-message" onClick={() => { hideContactModal(); - openConversationInternal({ conversationId: contact.id }); + showConversation({ conversationId: contact.id }); }} >
diff --git a/ts/components/conversationList/MessageSearchResult.stories.tsx b/ts/components/conversationList/MessageSearchResult.stories.tsx index 6b0573cc9..096a17a50 100644 --- a/ts/components/conversationList/MessageSearchResult.stories.tsx +++ b/ts/components/conversationList/MessageSearchResult.stories.tsx @@ -53,7 +53,7 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ to: overrideProps.to as PropsType['to'], getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined), isSelected: boolean('isSelected', overrideProps.isSelected || false), - openConversationInternal: action('openConversationInternal'), + showConversation: action('showConversation'), isSearchingInConversation: boolean( 'isSearchingInConversation', overrideProps.isSearchingInConversation || false diff --git a/ts/components/conversationList/MessageSearchResult.tsx b/ts/components/conversationList/MessageSearchResult.tsx index 205403799..f24422075 100644 --- a/ts/components/conversationList/MessageSearchResult.tsx +++ b/ts/components/conversationList/MessageSearchResult.tsx @@ -15,7 +15,10 @@ import type { ThemeType, } from '../../types/Util'; import { BaseConversationListItem } from './BaseConversationListItem'; -import type { ConversationType } from '../../state/ducks/conversations'; +import type { + ConversationType, + ShowConversationType, +} from '../../state/ducks/conversations'; import type { PreferredBadgeSelectorType } from '../../state/selectors/badges'; export type PropsDataType = { @@ -58,10 +61,7 @@ export type PropsDataType = { type PropsHousekeepingType = { getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; - openConversationInternal: (_: { - conversationId: string; - messageId?: string; - }) => void; + showConversation: ShowConversationType; theme: ThemeType; }; @@ -147,15 +147,15 @@ export const MessageSearchResult: FunctionComponent = React.memo( getPreferredBadge, i18n, id, - openConversationInternal, sentAt, + showConversation, snippet, theme, to, }) { const onClickItem = useCallback(() => { - openConversationInternal({ conversationId, messageId: id }); - }, [openConversationInternal, conversationId, id]); + showConversation({ conversationId, messageId: id }); + }, [showConversation, conversationId, id]); if (!from || !to) { return
; diff --git a/ts/components/conversationList/StartNewConversation.tsx b/ts/components/conversationList/StartNewConversation.tsx index 12b1ed921..a5070931f 100644 --- a/ts/components/conversationList/StartNewConversation.tsx +++ b/ts/components/conversationList/StartNewConversation.tsx @@ -11,6 +11,7 @@ import { BaseConversationListItem } from './BaseConversationListItem'; import type { ParsedE164Type } from '../../util/libphonenumberInstance'; import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid'; import type { LocalizerType } from '../../types/Util'; +import type { ShowConversationType } from '../../state/ducks/conversations'; import { AvatarColors } from '../../types/Colors'; type PropsData = { @@ -20,7 +21,7 @@ type PropsData = { type PropsHousekeeping = { i18n: LocalizerType; - showConversation: (conversationId: string) => void; + showConversation: ShowConversationType; } & LookupConversationWithoutUuidActionsType; export type Props = PropsData & PropsHousekeeping; @@ -55,7 +56,7 @@ export const StartNewConversation: FunctionComponent = React.memo( }); if (conversationId !== undefined) { - showConversation(conversationId); + showConversation({ conversationId }); } }, [ showConversation, diff --git a/ts/components/conversationList/UsernameSearchResultListItem.tsx b/ts/components/conversationList/UsernameSearchResultListItem.tsx index 645cfb623..38a91ad5f 100644 --- a/ts/components/conversationList/UsernameSearchResultListItem.tsx +++ b/ts/components/conversationList/UsernameSearchResultListItem.tsx @@ -9,6 +9,7 @@ import { BaseConversationListItem } from './BaseConversationListItem'; import type { LocalizerType } from '../../types/Util'; import { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid'; import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid'; +import type { ShowConversationType } from '../../state/ducks/conversations'; type PropsData = { username: string; @@ -17,7 +18,7 @@ type PropsData = { type PropsHousekeeping = { i18n: LocalizerType; - showConversation: (conversationId: string) => void; + showConversation: ShowConversationType; } & LookupConversationWithoutUuidActionsType; export type Props = PropsData & PropsHousekeeping; @@ -44,7 +45,7 @@ export const UsernameSearchResultListItem: FunctionComponent = ({ }); if (conversationId !== undefined) { - showConversation(conversationId); + showConversation({ conversationId }); } }, [ username, diff --git a/ts/components/leftPane/LeftPaneArchiveHelper.tsx b/ts/components/leftPane/LeftPaneArchiveHelper.tsx index 27977c42d..f96a83f21 100644 --- a/ts/components/leftPane/LeftPaneArchiveHelper.tsx +++ b/ts/components/leftPane/LeftPaneArchiveHelper.tsx @@ -14,7 +14,7 @@ import type { PropsData as ConversationListItemPropsType } from '../conversation import type { LocalizerType } from '../../types/Util'; import type { ConversationType, - OpenConversationInternalType, + ShowConversationType, } from '../../state/ducks/conversations'; import { LeftPaneSearchInput } from '../LeftPaneSearchInput'; import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper'; @@ -84,13 +84,13 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper unknown; clearSearch: () => unknown; i18n: LocalizerType; updateSearchTerm: (searchTerm: string) => unknown; - openConversationInternal: OpenConversationInternalType; + showConversation: ShowConversationType; }>): ReactChild | null { if (!this.searchConversation) { return null; @@ -103,9 +103,9 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper ); } diff --git a/ts/components/leftPane/LeftPaneHelper.tsx b/ts/components/leftPane/LeftPaneHelper.tsx index 0dd4cc09b..5cfb9b030 100644 --- a/ts/components/leftPane/LeftPaneHelper.tsx +++ b/ts/components/leftPane/LeftPaneHelper.tsx @@ -10,7 +10,7 @@ import type { ReplaceAvatarActionType, SaveAvatarToDiskActionType, } from '../../types/Avatar'; -import type { OpenConversationInternalType } from '../../state/ducks/conversations'; +import type { ShowConversationType } from '../../state/ducks/conversations'; export enum FindDirection { Up, @@ -43,7 +43,7 @@ export abstract class LeftPaneHelper { event: ChangeEvent ) => unknown; updateSearchTerm: (searchTerm: string) => unknown; - openConversationInternal: OpenConversationInternalType; + showConversation: ShowConversationType; }> ): null | ReactChild { return null; diff --git a/ts/components/leftPane/LeftPaneInboxHelper.tsx b/ts/components/leftPane/LeftPaneInboxHelper.tsx index d296ff3e7..332405144 100644 --- a/ts/components/leftPane/LeftPaneInboxHelper.tsx +++ b/ts/components/leftPane/LeftPaneInboxHelper.tsx @@ -9,7 +9,7 @@ import { Intl } from '../Intl'; import type { ToFindType } from './LeftPaneHelper'; import type { ConversationType, - OpenConversationInternalType, + ShowConversationType, } from '../../state/ducks/conversations'; import { LeftPaneHelper } from './LeftPaneHelper'; import { getConversationInDirection } from './getConversationInDirection'; @@ -85,14 +85,14 @@ export class LeftPaneInboxHelper extends LeftPaneHelper clearConversationSearch, clearSearch, i18n, + showConversation, updateSearchTerm, - openConversationInternal, }: Readonly<{ clearConversationSearch: () => unknown; clearSearch: () => unknown; i18n: LocalizerType; + showConversation: ShowConversationType; updateSearchTerm: (searchTerm: string) => unknown; - openConversationInternal: OpenConversationInternalType; }>): ReactChild { return ( i18n={i18n} searchConversation={this.searchConversation} searchTerm={this.searchTerm} + showConversation={showConversation} startSearchCounter={this.startSearchCounter} updateSearchTerm={updateSearchTerm} - openConversationInternal={openConversationInternal} /> ); } diff --git a/ts/components/leftPane/LeftPaneSearchHelper.tsx b/ts/components/leftPane/LeftPaneSearchHelper.tsx index badfb56b4..83fe769f8 100644 --- a/ts/components/leftPane/LeftPaneSearchHelper.tsx +++ b/ts/components/leftPane/LeftPaneSearchHelper.tsx @@ -13,7 +13,7 @@ import type { PropsData as ConversationListItemPropsType } from '../conversation import { handleKeydownForSearch } from './handleKeydownForSearch'; import type { ConversationType, - OpenConversationInternalType, + ShowConversationType, } from '../../state/ducks/conversations'; import { LeftPaneSearchInput } from '../LeftPaneSearchInput'; @@ -104,14 +104,14 @@ export class LeftPaneSearchHelper extends LeftPaneHelper unknown; clearSearch: () => unknown; i18n: LocalizerType; + showConversation: ShowConversationType; updateSearchTerm: (searchTerm: string) => unknown; - openConversationInternal: OpenConversationInternalType; }>): ReactChild { return ( ); } @@ -361,13 +361,13 @@ export class LeftPaneSearchHelper extends LeftPaneHelper unknown, - openConversationInternal: OpenConversationInternalType + showConversation: ShowConversationType ): void { const conversation = this.getConversationAndMessageAtIndex(0); if (!conversation) { return; } - openConversationInternal(conversation); + showConversation(conversation); clearSearch(); } } diff --git a/ts/groups/joinViaLink.tsx b/ts/groups/joinViaLink.tsx index 39054360c..1379e486a 100644 --- a/ts/groups/joinViaLink.tsx +++ b/ts/groups/joinViaLink.tsx @@ -74,7 +74,7 @@ export async function joinViaLink(hash: string): Promise { log.warn( `joinViaLink/${logId}: Already a member of group, opening conversation` ); - window.reduxActions.conversations.openConversationInternal({ + window.reduxActions.conversations.showConversation({ conversationId: existingConversation.id, }); showToast(ToastAlreadyGroupMember); @@ -166,7 +166,7 @@ export async function joinViaLink(hash: string): Promise { // We're waiting for the left pane to re-sort before we navigate to that conversation await sleep(200); - window.reduxActions.conversations.openConversationInternal({ + window.reduxActions.conversations.showConversation({ conversationId: existingConversation.id, }); @@ -253,7 +253,7 @@ export async function joinViaLink(hash: string): Promise { log.warn( `joinViaLink/${logId}: User is part of group on second check, opening conversation` ); - window.reduxActions.conversations.openConversationInternal({ + window.reduxActions.conversations.showConversation({ conversationId: targetConversation.id, }); return; @@ -347,7 +347,7 @@ export async function joinViaLink(hash: string): Promise { ); } - window.reduxActions.conversations.openConversationInternal({ + window.reduxActions.conversations.showConversation({ conversationId: targetConversation.id, }); } catch (error) { diff --git a/ts/signal.ts b/ts/signal.ts index 5a7757c46..72684580c 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -3,7 +3,6 @@ // The idea with this file is to make it webpackable for the style guide -import * as Backbone from './backbone'; import * as Crypto from './Crypto'; import * as Curve from './Curve'; import { start as conversationControllerStart } from './ConversationController'; @@ -33,7 +32,6 @@ import { createForwardMessageModal } from './state/roots/createForwardMessageMod import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement'; import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal'; import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal'; -import { createLeftPane } from './state/roots/createLeftPane'; import { createMessageDetail } from './state/roots/createMessageDetail'; import { createConversationNotificationsSettings } from './state/roots/createConversationNotificationsSettings'; import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions'; @@ -424,7 +422,6 @@ export const setup = (options: { createGroupV1MigrationModal, createGroupV2JoinModal, createGroupV2Permissions, - createLeftPane, createMessageDetail, createConversationNotificationsSettings, createPendingInvites, @@ -482,7 +479,6 @@ export const setup = (options: { }; return { - Backbone, Components, Crypto, Curve, diff --git a/ts/state/ducks/audioPlayer.ts b/ts/state/ducks/audioPlayer.ts index d10fed7b7..45fc79f89 100644 --- a/ts/state/ducks/audioPlayer.ts +++ b/ts/state/ducks/audioPlayer.ts @@ -4,10 +4,11 @@ import { useBoundActions } from '../../hooks/useBoundActions'; import type { - SwitchToAssociatedViewActionType, MessageDeletedActionType, MessageChangedActionType, + SelectedConversationChangedActionType, } from './conversations'; +import { SELECTED_CONVERSATION_CHANGED } from './conversations'; // State @@ -59,9 +60,9 @@ export function reducer( state: Readonly = getEmptyState(), action: Readonly< | AudioPlayerActionType - | SwitchToAssociatedViewActionType | MessageDeletedActionType | MessageChangedActionType + | SelectedConversationChangedActionType > ): AudioPlayerStateType { if (action.type === 'audioPlayer/SET_ACTIVE_AUDIO_ID') { @@ -75,7 +76,7 @@ export function reducer( } // Reset activeAudioID on conversation change. - if (action.type === 'SWITCH_TO_ASSOCIATED_VIEW') { + if (action.type === SELECTED_CONVERSATION_CHANGED) { return { ...state, activeAudioID: undefined, diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 6bb554c52..b7e93eb43 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -22,7 +22,6 @@ import { calling } from '../../services/calling'; import { getOwn } from '../../util/getOwn'; import { assert, strictAssert } from '../../util/assert'; import * as universalExpireTimer from '../../util/universalExpireTimer'; -import { trigger } from '../../shims/events'; import type { ToggleProfileEditorErrorActionType } from './globalModals'; import { TOGGLE_PROFILE_EDITOR_ERROR } from './globalModals'; import { isRecord } from '../../util/isRecord'; @@ -347,12 +346,6 @@ export type ConversationsStateType = { messagesByConversation: MessagesByConversationType; }; -export type OpenConversationInternalType = (_: { - conversationId: string; - messageId?: string; - switchToAssociatedView?: boolean; -}) => void; - // Helpers export const getConversationCallMode = ( @@ -399,6 +392,8 @@ const CONVERSATION_STOPPED_BY_MISSING_VERIFICATION = const DISCARD_MESSAGES = 'conversations/DISCARD_MESSAGES'; const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS'; const UPDATE_USERNAME_SAVE_STATE = 'conversations/UPDATE_USERNAME_SAVE_STATE'; +export const SELECTED_CONVERSATION_CHANGED = + 'conversations/SELECTED_CONVERSATION_CHANGED'; export type CancelVerificationDataByConversationActionType = { type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION; @@ -642,10 +637,11 @@ export type ClearUnreadMetricsActionType = { }; }; export type SelectedConversationChangedActionType = { - type: 'SELECTED_CONVERSATION_CHANGED'; + type: typeof SELECTED_CONVERSATION_CHANGED; payload: { - id: string; + id?: string; messageId?: string; + switchToAssociatedView?: boolean; }; }; type ReviewGroupMemberNameCollisionActionType = { @@ -710,10 +706,6 @@ type ShowChooseGroupMembersActionType = { type StartSettingGroupMetadataActionType = { type: 'START_SETTING_GROUP_METADATA'; }; -export type SwitchToAssociatedViewActionType = { - type: 'SWITCH_TO_ASSOCIATED_VIEW'; - payload: { conversationId: string }; -}; export type ToggleConversationInChooseMembersActionType = { type: 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS'; payload: { @@ -792,7 +784,6 @@ export type ConversationActionType = | ShowInboxActionType | StartComposingActionType | StartSettingGroupMetadataActionType - | SwitchToAssociatedViewActionType | ToggleConversationInChooseMembersActionType | ToggleComposeEditingAvatarActionType | UpdateUsernameSaveStateActionType; @@ -831,8 +822,6 @@ export const actions = { messagesAdded, messagesReset, myProfileChanged, - openConversationExternal, - openConversationInternal, removeAllConversations, removeCustomColorOnConversations, removeMemberFromGroup, @@ -1527,9 +1516,9 @@ function createGroup( | CreateGroupPendingActionType | CreateGroupFulfilledActionType | CreateGroupRejectedActionType - | SwitchToAssociatedViewActionType + | SelectedConversationChangedActionType > { - return async (dispatch, getState, ...args) => { + return async (dispatch, getState) => { const { composer } = getState().conversations; if ( composer?.step !== ComposerStep.SetGroupMetadata || @@ -1559,10 +1548,12 @@ function createGroup( ), }, }); - openConversationInternal({ - conversationId: conversation.id, - switchToAssociatedView: true, - })(dispatch, getState, ...args); + dispatch( + showConversation({ + conversationId: conversation.id, + switchToAssociatedView: true, + }) + ); } catch (err) { log.error('Failed to create group', err && err.stack ? err.stack : err); dispatch({ type: 'CREATE_GROUP_REJECTED' }); @@ -1924,48 +1915,6 @@ function toggleConversationInChooseMembers( }; } -// Note: we need two actions here to simplify. Operations outside of the left pane can -// trigger an 'openConversation' so we go through Whisper.events for all -// conversation selection. Internal just triggers the Whisper.event, and External -// makes the changes to the store. -function openConversationInternal({ - conversationId, - messageId, - switchToAssociatedView, -}: Readonly<{ - conversationId: string; - messageId?: string; - switchToAssociatedView?: boolean; -}>): ThunkAction< - void, - RootStateType, - unknown, - SwitchToAssociatedViewActionType -> { - return dispatch => { - trigger('showConversation', conversationId, messageId); - - if (switchToAssociatedView) { - dispatch({ - type: 'SWITCH_TO_ASSOCIATED_VIEW', - payload: { conversationId }, - }); - } - }; -} -function openConversationExternal( - id: string, - messageId?: string -): SelectedConversationChangedActionType { - return { - type: 'SELECTED_CONVERSATION_CHANGED', - payload: { - id, - messageId, - }, - }; -} - function toggleHideStories( conversationId: string ): ThunkAction { @@ -2039,12 +1988,26 @@ function showInbox(): ShowInboxActionType { payload: null, }; } -function showConversation( - conversationId: string -): ThunkAction { - return dispatch => { - trigger('showConversation', conversationId); - dispatch(showInbox()); + +type ShowConversationArgsType = { + conversationId?: string; + messageId?: string; + switchToAssociatedView?: boolean; +}; +export type ShowConversationType = (_: ShowConversationArgsType) => unknown; + +function showConversation({ + conversationId, + messageId, + switchToAssociatedView, +}: ShowConversationArgsType): SelectedConversationChangedActionType { + return { + type: SELECTED_CONVERSATION_CHANGED, + payload: { + id: conversationId, + messageId, + switchToAssociatedView, + }, }; } function showArchivedConversations(): ShowArchivedConversationsActionType { @@ -2475,7 +2438,7 @@ export function reducer( }; } if (action.type === 'CREATE_GROUP_FULFILLED') { - // We don't do much here and instead rely on `openConversationInternal` to do most of + // We don't do much here and instead rely on `showConversation` to do most of // the work. return { ...state, @@ -3065,14 +3028,31 @@ export function reducer( }, }; } - if (action.type === 'SELECTED_CONVERSATION_CHANGED') { + if (action.type === SELECTED_CONVERSATION_CHANGED) { const { payload } = action; - const { id } = payload; + const { id, messageId, switchToAssociatedView } = payload; - return { + const nextState = { ...omit(state, 'contactSpoofingReview'), selectedConversationId: id, }; + + if (messageId) { + nextState.selectedMessage = messageId; + } + + if (switchToAssociatedView && id) { + const conversation = getOwn(state.conversationLookup, id); + if (!conversation) { + return nextState; + } + return { + ...omit(nextState, 'composer'), + showArchived: Boolean(conversation.isArchived), + }; + } + + return nextState; } if (action.type === 'SHOW_INBOX') { return { @@ -3432,20 +3412,6 @@ export function reducer( } } - if (action.type === 'SWITCH_TO_ASSOCIATED_VIEW') { - const conversation = getOwn( - state.conversationLookup, - action.payload.conversationId - ); - if (!conversation) { - return state; - } - return { - ...omit(state, 'composer'), - showArchived: Boolean(conversation.isArchived), - }; - } - if (action.type === 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS') { const { composer } = state; if (composer?.step !== ComposerStep.ChooseGroupMembers) { diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 2fa82686a..a29a97b51 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -31,6 +31,7 @@ import { getUserConversationId, } from '../selectors/user'; import { strictAssert } from '../../util/assert'; +import { SELECTED_CONVERSATION_CHANGED } from './conversations'; const { searchMessages: dataSearchMessages, @@ -431,7 +432,7 @@ export function reducer( return getEmptyState(); } - if (action.type === 'SELECTED_CONVERSATION_CHANGED') { + if (action.type === SELECTED_CONVERSATION_CHANGED) { const { payload } = action; const { id, messageId } = payload; const { searchConversationId } = state; diff --git a/ts/state/roots/createLeftPane.tsx b/ts/state/roots/createLeftPane.tsx deleted file mode 100644 index 4398b5779..000000000 --- a/ts/state/roots/createLeftPane.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2019-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { Provider } from 'react-redux'; - -import type { Store } from 'redux'; - -import { SmartLeftPane } from '../smart/LeftPane'; - -export const createLeftPane = (store: Store): React.ReactElement => ( - - - -); diff --git a/ts/state/smart/App.tsx b/ts/state/smart/App.tsx index f9236c7dc..bdde0fa85 100644 --- a/ts/state/smart/App.tsx +++ b/ts/state/smart/App.tsx @@ -10,6 +10,7 @@ import { App } from '../../components/App'; import { SmartCallManager } from './CallManager'; import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal'; import { SmartGlobalModalContainer } from './GlobalModalContainer'; +import { SmartLeftPane } from './LeftPane'; import { SmartSafetyNumberViewer } from './SafetyNumberViewer'; import { SmartStories } from './Stories'; import type { StateType } from '../reducer'; @@ -47,6 +48,7 @@ const mapStateToProps = (state: StateType) => { ), renderGlobalModalContainer: () => , + renderLeftPane: () => , renderSafetyNumber: (props: SafetyNumberProps) => ( ), @@ -68,6 +70,8 @@ const mapStateToProps = (state: StateType) => { registerSingleDevice: (number: string, code: string): Promise => { return window.getAccountManager().registerSingleDevice(number, code); }, + selectedConversationId: state.conversations.selectedConversationId, + selectedMessage: state.conversations.selectedMessage, theme: getTheme(state), executeMenuRole: (role: MenuItemConstructorOptions['role']): void => { diff --git a/ts/state/smart/Stories.tsx b/ts/state/smart/Stories.tsx index f451929be..b04e9fa1e 100644 --- a/ts/state/smart/Stories.tsx +++ b/ts/state/smart/Stories.tsx @@ -33,8 +33,7 @@ function renderStoryViewer({ export function SmartStories(): JSX.Element | null { const storiesActions = useStoriesActions(); - const { openConversationInternal, toggleHideStories } = - useConversationsActions(); + const { showConversation, toggleHideStories } = useConversationsActions(); const i18n = useSelector(getIntl); @@ -56,9 +55,9 @@ export function SmartStories(): JSX.Element | null { (getIntl); const getPreferredBadge = useSelector(getPreferredBadgeSelector); @@ -74,7 +73,7 @@ export function SmartStoryViewer({ onClose={onClose} onHideStory={toggleHideStories} onGoToConversation={senderId => { - openConversationInternal({ conversationId: senderId }); + showConversation({ conversationId: senderId }); storiesActions.toggleStoriesView(); }} onNextUserStories={onNextUserStories} diff --git a/ts/test-both/state/ducks/audioPlayer_test.ts b/ts/test-both/state/ducks/audioPlayer_test.ts index c1445c753..7be2ad787 100644 --- a/ts/test-both/state/ducks/audioPlayer_test.ts +++ b/ts/test-both/state/ducks/audioPlayer_test.ts @@ -4,8 +4,11 @@ import { assert } from 'chai'; import { actions } from '../../../state/ducks/audioPlayer'; -import type { SwitchToAssociatedViewActionType } from '../../../state/ducks/conversations'; -import { actions as conversationsActions } from '../../../state/ducks/conversations'; +import type { SelectedConversationChangedActionType } from '../../../state/ducks/conversations'; +import { + SELECTED_CONVERSATION_CHANGED, + actions as conversationsActions, +} from '../../../state/ducks/conversations'; import { noopAction } from '../../../state/ducks/noop'; import type { StateType } from '../../../state/reducer'; @@ -51,9 +54,9 @@ describe('both/state/ducks/audioPlayer', () => { it('resets activeAudioID when changing the conversation', () => { const state = getInitializedState(); - const updated = rootReducer(state, { - type: 'SWITCH_TO_ASSOCIATED_VIEW', - payload: { conversationId: 'any' }, + const updated = rootReducer(state, { + type: SELECTED_CONVERSATION_CHANGED, + payload: { id: 'any' }, }); assert.strictEqual(updated.audioPlayer.activeAudioID, undefined); diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index 320f94317..a2552285c 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -15,13 +15,14 @@ import { import type { CancelVerificationDataByConversationActionType, ConversationMessageType, - ConversationsStateType, ConversationType, + ConversationsStateType, MessageType, - SwitchToAssociatedViewActionType, + SelectedConversationChangedActionType, ToggleConversationInChooseMembersActionType, } from '../../../state/ducks/conversations'; import { + SELECTED_CONVERSATION_CHANGED, actions, cancelConversationVerification, clearCancelledConversationVerification, @@ -56,7 +57,6 @@ const { createGroup, discardMessages, messageChanged, - openConversationInternal, repairNewestMessage, repairOldestMessage, resetAllChatColors, @@ -68,6 +68,7 @@ const { setPreJoinConversation, showArchivedConversations, showChooseGroupMembers, + showConversation, showInbox, startComposing, startSettingGroupMetadata, @@ -341,82 +342,40 @@ describe('both/state/ducks/conversations', () => { }; } - describe('openConversationInternal', () => { - it("returns a thunk that triggers a 'showConversation' event when passed a conversation ID", () => { - const dispatch = sinon.spy(); + describe('showConversation', () => { + it('selects a conversation id', () => { + const state = { + ...getEmptyState(), + }; + const action = showConversation({ conversationId: 'abc123' }); + const nextState = reducer(state, action); - openConversationInternal({ conversationId: 'abc123' })( - dispatch, - getEmptyRootState, - null - ); - - sinon.assert.calledOnce( - window.Whisper.events.trigger as sinon.SinonSpy - ); - sinon.assert.calledWith( - window.Whisper.events.trigger as sinon.SinonSpy, - 'showConversation', - 'abc123', - undefined - ); + assert.equal(nextState.selectedConversationId, 'abc123'); + assert.isUndefined(nextState.selectedMessage); }); - it("returns a thunk that triggers a 'showConversation' event when passed a conversation ID and message ID", () => { - const dispatch = sinon.spy(); - - openConversationInternal({ + it('selects a conversation and a message', () => { + const state = { + ...getEmptyState(), + }; + const action = showConversation({ conversationId: 'abc123', messageId: 'xyz987', - })(dispatch, getEmptyRootState, null); - - sinon.assert.calledOnce( - window.Whisper.events.trigger as sinon.SinonSpy - ); - sinon.assert.calledWith( - window.Whisper.events.trigger as sinon.SinonSpy, - 'showConversation', - 'abc123', - 'xyz987' - ); - }); - - it("returns a thunk that doesn't dispatch any actions by default", () => { - const dispatch = sinon.spy(); - - openConversationInternal({ conversationId: 'abc123' })( - dispatch, - getEmptyRootState, - null - ); - - sinon.assert.notCalled(dispatch); - }); - - it('dispatches a SWITCH_TO_ASSOCIATED_VIEW action if called with a flag', () => { - const dispatch = sinon.spy(); - - openConversationInternal({ - conversationId: 'abc123', - switchToAssociatedView: true, - })(dispatch, getEmptyRootState, null); - - sinon.assert.calledWith(dispatch, { - type: 'SWITCH_TO_ASSOCIATED_VIEW', - payload: { conversationId: 'abc123' }, }); + const nextState = reducer(state, action); + + assert.equal(nextState.selectedConversationId, 'abc123'); + assert.equal(nextState.selectedMessage, 'xyz987'); }); - describe('SWITCH_TO_ASSOCIATED_VIEW', () => { - let action: SwitchToAssociatedViewActionType; + describe('showConversation switchToAssociatedView=true', () => { + let action: SelectedConversationChangedActionType; beforeEach(() => { - const dispatch = sinon.spy(); - openConversationInternal({ + action = showConversation({ conversationId: 'fake-conversation-id', switchToAssociatedView: true, - })(dispatch, getEmptyRootState, null); - [action] = dispatch.getCall(0).args; + }); }); it('shows the inbox if the conversation is not archived', () => { @@ -451,13 +410,6 @@ describe('both/state/ducks/conversations', () => { assert.isUndefined(result.composer); assert.isTrue(result.showArchived); }); - - it('does nothing if the conversation is not found', () => { - const state = getEmptyState(); - const result = reducer(state, action); - - assert.strictEqual(result, state); - }); }); }); @@ -769,26 +721,23 @@ describe('both/state/ducks/conversations', () => { null ); - sinon.assert.calledWith( - window.Whisper.events.trigger as sinon.SinonSpy, - 'showConversation', - '9876', - undefined - ); - sinon.assert.calledWith(dispatch, { type: 'CREATE_GROUP_FULFILLED', payload: { invitedUuids: [abc] }, }); + sinon.assert.calledWith(dispatch, { + type: SELECTED_CONVERSATION_CHANGED, + payload: { + id: '9876', + messageId: undefined, + switchToAssociatedView: true, + }, + }); + const fulfilledAction = dispatch.getCall(1).args[0]; const result = reducer(conversationsState, fulfilledAction); assert.deepEqual(result.invitedUuidsForNewlyCreatedGroup, [abc]); - - sinon.assert.calledWith(dispatch, { - type: 'SWITCH_TO_ASSOCIATED_VIEW', - payload: { conversationId: '9876' }, - }); }); }); diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 1e0195889..ba9aef5ea 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -8300,22 +8300,6 @@ "reasonCategory": "falseMatch", "updated": "2020-07-21T18:34:59.251Z" }, - { - "rule": "DOM-innerHTML", - "path": "ts/backbone/views/Lightbox.ts", - "line": " container.innerHTML = '';", - "reasonCategory": "usageTrusted", - "updated": "2018-09-17T20:50:40.689Z", - "reasonDetail": "Hard-coded value" - }, - { - "rule": "DOM-innerHTML", - "path": "ts/backbone/views/Lightbox.ts", - "line": " container.innerHTML = '';", - "reasonCategory": "usageTrusted", - "updated": "2018-09-17T20:50:40.689Z", - "reasonDetail": "Hard-coded value" - }, { "rule": "jQuery-html(", "path": "ts/backbone/views/whisper_view.ts", @@ -8598,16 +8582,16 @@ { "rule": "React-useRef", "path": "ts/components/Inbox.tsx", - "line": " const hostRef = useRef(null);", + "line": " const conversationMountRef = useRef(null);", "reasonCategory": "usageTrusted", - "updated": "2021-07-30T16:57:33.618Z" + "updated": "2022-06-15T01:24:12.761Z" }, { "rule": "React-useRef", "path": "ts/components/Inbox.tsx", - "line": " const viewRef = useRef(undefined);", + "line": " const conversationViewRef = useRef(null);", "reasonCategory": "usageTrusted", - "updated": "2021-07-30T16:57:33.618Z" + "updated": "2022-06-15T01:24:12.761Z" }, { "rule": "React-useRef", @@ -9121,90 +9105,6 @@ "reasonCategory": "falseMatch", "updated": "2021-09-17T21:51:57.475Z" }, - { - "rule": "jQuery-$(", - "path": "ts/views/inbox_view.tsx", - "line": " template: () => $('#app-loading-screen').html(),", - "reasonCategory": "usageTrusted", - "updated": "2021-09-15T21:07:50.995Z" - }, - { - "rule": "jQuery-$(", - "path": "ts/views/inbox_view.tsx", - "line": " this.$('.message').text(message);", - "reasonCategory": "usageTrusted", - "updated": "2021-09-15T21:07:50.995Z" - }, - { - "rule": "jQuery-$(", - "path": "ts/views/inbox_view.tsx", - "line": " template: () => $('#two-column').html(),", - "reasonCategory": "usageTrusted", - "updated": "2021-09-15T21:07:50.995Z" - }, - { - "rule": "jQuery-$(", - "path": "ts/views/inbox_view.tsx", - "line": " el: this.$('.conversation-stack'),", - "reasonCategory": "usageTrusted", - "updated": "2021-09-15T21:07:50.995Z" - }, - { - "rule": "jQuery-$(", - "path": "ts/views/inbox_view.tsx", - "line": " this.$('.no-conversation-open').toggle(!isAnyConversationOpen);", - "reasonCategory": "usageTrusted", - "updated": "2021-10-08T17:40:22.770Z" - }, - { - "rule": "jQuery-$(", - "path": "ts/views/inbox_view.tsx", - "line": " this.$('.left-pane-placeholder').replaceWith(this.leftPaneView.el);", - "reasonCategory": "usageTrusted", - "updated": "2021-10-08T17:40:22.770Z" - }, - { - "rule": "jQuery-$(", - "path": "ts/views/inbox_view.tsx", - "line": " this.$('.whats-new-placeholder').append(this.whatsNewLink.el);", - "reasonCategory": "usageTrusted", - "updated": "2021-10-22T20:58:48.103Z" - }, - { - "rule": "jQuery-append(", - "path": "ts/views/inbox_view.tsx", - "line": " this.$('.whats-new-placeholder').append(this.whatsNewLink.el);", - "reasonCategory": "usageTrusted", - "updated": "2021-10-22T20:58:48.103Z" - }, - { - "rule": "jQuery-appendTo(", - "path": "ts/views/inbox_view.tsx", - "line": " view.$el.appendTo(this.el);", - "reasonCategory": "usageTrusted", - "updated": "2021-09-15T21:07:50.995Z" - }, - { - "rule": "jQuery-html(", - "path": "ts/views/inbox_view.tsx", - "line": " template: () => $('#app-loading-screen').html(),", - "reasonCategory": "usageTrusted", - "updated": "2021-09-15T21:07:50.995Z" - }, - { - "rule": "jQuery-html(", - "path": "ts/views/inbox_view.tsx", - "line": " template: () => $('#two-column').html(),", - "reasonCategory": "usageTrusted", - "updated": "2021-09-15T21:07:50.995Z" - }, - { - "rule": "jQuery-prependTo(", - "path": "ts/views/inbox_view.tsx", - "line": " this.appLoadingScreen.$el.prependTo(this.el);", - "reasonCategory": "usageTrusted", - "updated": "2021-09-15T21:07:50.995Z" - }, { "rule": "DOM-innerHTML", "path": "ts/windows/loading/start.ts", diff --git a/ts/util/showLightbox.tsx b/ts/util/showLightbox.tsx new file mode 100644 index 000000000..6b072ef11 --- /dev/null +++ b/ts/util/showLightbox.tsx @@ -0,0 +1,39 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { render } from 'react-dom'; +import type { PropsType } from '../components/Lightbox'; +import { Lightbox } from '../components/Lightbox'; + +// NOTE: This file is temporarily here for convenicence of use by +// conversation_view while it is transitioning from Backbone into pure React. +// Please use directly and DO NOT USE THESE FUNCTIONS. + +let lightboxMountNode: HTMLElement | undefined; + +export function isLightboxOpen(): boolean { + return Boolean(lightboxMountNode); +} + +export function closeLightbox(): void { + if (!lightboxMountNode) { + return; + } + + window.ReactDOM.unmountComponentAtNode(lightboxMountNode); + document.body.removeChild(lightboxMountNode); + lightboxMountNode = undefined; +} + +export function showLightbox(props: PropsType): void { + if (lightboxMountNode) { + closeLightbox(); + } + + lightboxMountNode = document.createElement('div'); + lightboxMountNode.setAttribute('data-id', 'lightbox'); + document.body.appendChild(lightboxMountNode); + + render(, lightboxMountNode); +} diff --git a/ts/views/conversation_view.tsx b/ts/views/conversation_view.tsx index 144d80298..bc963ba79 100644 --- a/ts/views/conversation_view.tsx +++ b/ts/views/conversation_view.tsx @@ -58,7 +58,7 @@ import { import { getActiveCallState } from '../state/selectors/calling'; import { getTheme } from '../state/selectors/user'; import { ReactWrapperView } from './ReactWrapperView'; -import { Lightbox } from '../components/Lightbox'; +import type { Lightbox } from '../components/Lightbox'; import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList'; import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog'; import type { @@ -121,6 +121,11 @@ import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone'; import { ContactDetail } from '../components/conversation/ContactDetail'; import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery'; import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent'; +import { + closeLightbox, + isLightboxOpen, + showLightbox, +} from '../util/showLightbox'; type AttachmentOptions = { messageId: string; @@ -1905,28 +1910,23 @@ export class ConversationView extends window.Backbone.View { await message.markViewOnceMessageViewed(); - const closeLightbox = async () => { + this.listenTo(message, 'expired', async () => { log.info('displayTapToViewMessage: attempting to close lightbox'); - if (!this.lightboxView) { + // This isn't really a bullet-proof check because the lightbox could + // be open while we're viewing a regular media message + if (!isLightboxOpen()) { log.info('displayTapToViewMessage: lightbox was already closed'); return; } - const { lightboxView } = this; - this.lightboxView = undefined; - this.stopListening(message); - window.Signal.Backbone.Views.Lightbox.hide(); - lightboxView.remove(); + closeLightbox(); await deleteTempFile(tempPath); - }; - this.listenTo(message, 'expired', closeLightbox); + }); this.listenTo(message, 'change', () => { - if (this.lightboxView) { - this.lightboxView.update(); - } + showLightbox(getProps()); }); const getProps = (): ComponentProps => { @@ -1934,7 +1934,7 @@ export class ConversationView extends window.Backbone.View { return { close: () => { - this.lightboxView?.remove(); + closeLightbox(); }, i18n: window.i18n, media: [ @@ -1957,18 +1957,7 @@ export class ConversationView extends window.Backbone.View { }; }; - if (this.lightboxView) { - this.lightboxView.remove(); - this.lightboxView = undefined; - } - - this.lightboxView = new ReactWrapperView({ - className: 'lightbox-wrapper', - JSX: , - onClose: closeLightbox, - }); - - window.Signal.Backbone.Views.Lightbox.show(this.lightboxView.el); + showLightbox(getProps()); log.info('displayTapToViewMessage: showed lightbox'); } @@ -2084,34 +2073,17 @@ export class ConversationView extends window.Backbone.View { mediaItem.attachment.path === selectedMediaItem.attachment.path ); - if (this.lightboxView) { - this.lightboxView.remove(); - this.lightboxView = undefined; - } - - this.lightboxView = new ReactWrapperView({ - className: 'lightbox-wrapper', - JSX: ( - { - this.lightboxView?.remove(); - }} - i18n={window.i18n} - getConversation={getConversationSelector( - window.reduxStore.getState() - )} - media={media} - onForward={messageId => { - this.showForwardMessageModal(messageId); - }} - onSave={onSave} - selectedIndex={selectedIndex >= 0 ? selectedIndex : 0} - /> - ), - onClose: () => window.Signal.Backbone.Views.Lightbox.hide(), + showLightbox({ + close: closeLightbox, + i18n: window.i18n, + getConversation: getConversationSelector(window.reduxStore.getState()), + media, + onForward: messageId => { + this.showForwardMessageModal(messageId); + }, + onSave, + selectedIndex: selectedIndex >= 0 ? selectedIndex : 0, }); - - window.Signal.Backbone.Views.Lightbox.show(this.lightboxView.el); } showLightbox({ diff --git a/ts/views/inbox_view.tsx b/ts/views/inbox_view.tsx deleted file mode 100644 index 67c70ea02..000000000 --- a/ts/views/inbox_view.tsx +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2014-2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as React from 'react'; -import * as Backbone from 'backbone'; -import * as log from '../logging/log'; -import type { ConversationModel } from '../models/conversations'; -import { ReactWrapperView } from './ReactWrapperView'; -import { showToast } from '../util/showToast'; -import { strictAssert } from '../util/assert'; -import { WhatsNewLink } from '../components/WhatsNewLink'; -import { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed'; - -window.Whisper = window.Whisper || {}; -const { Whisper } = window; - -class ConversationStack extends Backbone.View { - public override className = 'conversation-stack'; - - private conversationStack: Array = []; - - private getTopConversation(): undefined | ConversationModel { - return this.conversationStack[this.conversationStack.length - 1]; - } - - public open(conversation: ConversationModel, messageId: string): void { - const topConversation = this.getTopConversation(); - - if (!topConversation || topConversation.id !== conversation.id) { - const view = new Whisper.ConversationView({ - model: conversation, - }); - this.listenTo(conversation, 'unload', () => this.onUnload(conversation)); - this.listenTo(conversation, 'showSafetyNumber', () => - view.showSafetyNumber() - ); - view.$el.appendTo(this.el); - - if (topConversation) { - topConversation.trigger('unload', 'opened another conversation'); - } - - this.conversationStack.push(conversation); - - conversation.trigger('opened', messageId); - } else if (messageId) { - conversation.trigger('scroll-to-message', messageId); - } - - this.render(); - } - - public unload(): void { - this.getTopConversation()?.trigger('unload', 'force unload requested'); - } - - private onUnload(conversation: ConversationModel) { - this.stopListening(conversation); - this.conversationStack = this.conversationStack.filter( - (c: ConversationModel) => c !== conversation - ); - - this.render(); - } - - public override render(): ConversationStack { - const isAnyConversationOpen = Boolean(this.conversationStack.length); - this.$('.no-conversation-open').toggle(!isAnyConversationOpen); - - // Make sure poppers are positioned properly - window.dispatchEvent(new Event('resize')); - - return this; - } -} - -const AppLoadingScreen = Whisper.View.extend({ - template: () => $('#app-loading-screen').html(), - className: 'app-loading-screen', - updateProgress(count: number) { - if (count > 0) { - const message = window.i18n('loadingMessages', [count.toString()]); - this.$('.message').text(message); - } - }, - render_attributes: { - message: window.i18n('loading'), - }, -}); - -Whisper.InboxView = Whisper.View.extend({ - template: () => $('#two-column').html(), - className: 'Inbox', - initialize( - options: { - initialLoadComplete?: boolean; - window?: typeof window; - } = {} - ) { - this.ready = false; - this.render(); - - this.conversation_stack = new ConversationStack({ - el: this.$('.conversation-stack'), - }); - - this.renderWhatsNew(); - - Whisper.events.on('refreshConversation', ({ oldId, newId }) => { - const convo = this.conversation_stack.lastConversation; - if (convo && convo.get('id') === oldId) { - this.conversation_stack.open(newId); - } - }); - - // Close current opened conversation to reload the group information once - // linked. - Whisper.events.on('setupAsNewDevice', () => { - this.conversation_stack.unload(); - }); - - window.Whisper.events.on('showConversation', (id, messageId) => { - const conversation = window.ConversationController.get(id); - strictAssert(conversation, 'Conversation must be found'); - - conversation.setMarkedUnread(false); - - const { openConversationExternal } = window.reduxActions.conversations; - if (openConversationExternal) { - openConversationExternal(conversation.id, messageId); - } - - this.conversation_stack.open(conversation, messageId); - }); - - window.Whisper.events.on('loadingProgress', count => { - const view = this.appLoadingScreen; - if (view) { - view.updateProgress(count); - } - }); - - if (!options.initialLoadComplete) { - this.appLoadingScreen = new AppLoadingScreen(); - this.appLoadingScreen.render(); - this.appLoadingScreen.$el.prependTo(this.el); - this.startConnectionListener(); - } else { - this.setupLeftPane(); - } - - Whisper.events.on('pack-install-failed', () => { - showToast(ToastStickerPackInstallFailed); - }); - }, - render_attributes: { - welcomeToSignal: window.i18n('welcomeToSignal'), - // TODO DESKTOP-1451: add back the selectAContact message - selectAContact: '', - }, - events: { - click: 'onClick', - }, - renderWhatsNew() { - if (this.whatsNewLink) { - return; - } - const { showWhatsNewModal } = window.reduxActions.globalModals; - this.whatsNewLink = new ReactWrapperView({ - JSX: ( - - ), - }); - this.$('.whats-new-placeholder').append(this.whatsNewLink.el); - }, - setupLeftPane() { - if (this.leftPaneView) { - return; - } - this.leftPaneView = new ReactWrapperView({ - className: 'left-pane-wrapper', - JSX: window.Signal.State.Roots.createLeftPane(window.reduxStore), - }); - - this.$('.left-pane-placeholder').replaceWith(this.leftPaneView.el); - }, - startConnectionListener() { - this.interval = setInterval(() => { - const status = window.getSocketStatus(); - switch (status) { - case 'CONNECTING': - break; - case 'OPEN': - clearInterval(this.interval); - // if we've connected, we can wait for real empty event - this.interval = null; - break; - case 'CLOSING': - case 'CLOSED': - clearInterval(this.interval); - this.interval = null; - // if we failed to connect, we pretend we got an empty event - this.onEmpty(); - break; - default: - log.warn( - `startConnectionListener: Found unexpected socket status ${status}; calling onEmpty() manually.` - ); - this.onEmpty(); - break; - } - }, 1000); - }, - onEmpty() { - this.setupLeftPane(); - - const view = this.appLoadingScreen; - if (view) { - this.appLoadingScreen = null; - view.remove(); - - const searchInput = document.querySelector( - '.LeftPaneSearchInput__input' - ) as HTMLElement; - searchInput?.focus?.(); - } - }, -}); diff --git a/ts/window.d.ts b/ts/window.d.ts index 2135139d4..da9093899 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -44,7 +44,6 @@ import { createGroupLinkManagement } from './state/roots/createGroupLinkManageme import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal'; import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal'; import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions'; -import { createLeftPane } from './state/roots/createLeftPane'; import { createMessageDetail } from './state/roots/createMessageDetail'; import { createConversationNotificationsSettings } from './state/roots/createConversationNotificationsSettings'; import { createPendingInvites } from './state/roots/createPendingInvites'; @@ -137,7 +136,6 @@ export declare class WebAudioRecorderClass { } export type SignalCoreType = { - Backbone: any; Crypto: typeof Crypto; Curve: typeof Curve; Data: typeof Data; @@ -187,7 +185,6 @@ export type SignalCoreType = { createGroupV1MigrationModal: typeof createGroupV1MigrationModal; createGroupV2JoinModal: typeof createGroupV2JoinModal; createGroupV2Permissions: typeof createGroupV2Permissions; - createLeftPane: typeof createLeftPane; createMessageDetail: typeof createMessageDetail; createConversationNotificationsSettings: typeof createConversationNotificationsSettings; createPendingInvites: typeof createPendingInvites; diff --git a/ts/windows/main/phase3-post-signal.ts b/ts/windows/main/phase3-post-signal.ts index 11f5a8744..7c7e235cd 100644 --- a/ts/windows/main/phase3-post-signal.ts +++ b/ts/windows/main/phase3-post-signal.ts @@ -8,6 +8,5 @@ import '../../models/conversations'; import '../../backbone/views/whisper_view'; import '../../views/conversation_view'; -import '../../views/inbox_view'; import '../../SignalProtocolStore'; import '../../background';