Removes Inbox Backbone view

This commit is contained in:
Josh Perez 2022-06-16 15:12:50 -04:00 committed by GitHub
parent 603b76c3d9
commit aa23c2def2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 496 additions and 808 deletions

View file

@ -862,9 +862,6 @@
"message": "what's new", "message": "what's new",
"description": "Clickable link that displays the latest release notes" "description": "Clickable link that displays the latest release notes"
}, },
"selectAContact": {
"message": "Select a contact or group to start chatting."
},
"typingAlt": { "typingAlt": {
"message": "Typing animation for this conversation", "message": "Typing animation for this conversation",
"description": "Used as the 'title' attribute for the typing animation" "description": "Used as the 'title' attribute for the typing animation"

View file

@ -106,24 +106,6 @@
</div> </div>
</script> </script>
<script type="text/x-tmpl-mustache" id="two-column">
<div class='module-title-bar-drag-area'></div>
<div class='left-pane-placeholder'></div>
<div class='conversation-stack'>
<div class='no-conversation-open'>
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
<h3>{{ welcomeToSignal }}</h3>
<p class="whats-new-placeholder"></p>
<p>{{ selectAContact }}</p>
</div>
<div id="toast"></div>
</div>
<div class='lightbox-container'></div>
</script>
<script type="text/x-tmpl-mustache" id="conversation"> <script type="text/x-tmpl-mustache" id="conversation">
<div class="ConversationView__template"></div> <div class="ConversationView__template"></div>
</script> </script>

View file

@ -26,24 +26,6 @@
</div> </div>
</script> </script>
<script type="text/x-tmpl-mustache" id="two-column">
<div class='module-title-bar-drag-area'></div>
<div class='left-pane-placeholder'></div>
<div class='conversation-stack'>
<div class='no-conversation-open'>
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
<h3>{{ welcomeToSignal }}</h3>
<p class="whats-new-placeholder"></p>
<p>{{ selectAContact }}</p>
</div>
<div id="toast"></div>
</div>
<div class='lightbox-container'></div>
</script>
<script type="text/x-tmpl-mustache" id="conversation"> <script type="text/x-tmpl-mustache" id="conversation">
<div id="ConversationView__template"></div> <div id="ConversationView__template"></div>
</script> </script>

View file

@ -1,6 +0,0 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as Views from './views';
export { Views };

View file

@ -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';
};

View file

@ -1,6 +0,0 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as Lightbox from './Lightbox';
export { Lightbox };

View file

@ -1508,6 +1508,10 @@ export async function startApp(): Promise<void> {
(key === 'c' || key === 'C') (key === 'c' || key === 'C')
) { ) {
conversation.trigger('unload', 'keyboard shortcut close'); conversation.trigger('unload', 'keyboard shortcut close');
window.reduxActions.conversations.showConversation({
conversationId: undefined,
messageId: undefined,
});
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return; return;

View file

@ -48,30 +48,35 @@ export const App = ({
appView, appView,
cancelConversationVerification, cancelConversationVerification,
conversationsStoppingSend, conversationsStoppingSend,
hasInitialLoadCompleted, executeMenuAction,
executeMenuRole,
getPreferredBadge, getPreferredBadge,
hasInitialLoadCompleted,
i18n, i18n,
isCustomizingPreferredReactions, isCustomizingPreferredReactions,
isShowingStoriesView,
isMaximized,
isFullScreen, isFullScreen,
isMaximized,
isShowingStoriesView,
isWindows11, isWindows11,
menuOptions,
platform,
localeMessages, localeMessages,
menuOptions,
openInbox,
platform,
registerSingleDevice,
renderCallManager, renderCallManager,
renderCustomizingPreferredReactionsModal, renderCustomizingPreferredReactionsModal,
renderGlobalModalContainer, renderGlobalModalContainer,
renderLeftPane,
renderSafetyNumber, renderSafetyNumber,
openInbox,
renderStories, renderStories,
requestVerification, requestVerification,
registerSingleDevice, selectedConversationId,
selectedMessage,
showConversation,
showWhatsNewModal,
theme, theme,
verifyConversationsStoppingSend,
executeMenuAction,
executeMenuRole,
titleBarDoubleClick, titleBarDoubleClick,
verifyConversationsStoppingSend,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
let contents; let contents;
@ -101,7 +106,12 @@ export const App = ({
renderCustomizingPreferredReactionsModal={ renderCustomizingPreferredReactionsModal={
renderCustomizingPreferredReactionsModal renderCustomizingPreferredReactionsModal
} }
renderLeftPane={renderLeftPane}
renderSafetyNumber={renderSafetyNumber} renderSafetyNumber={renderSafetyNumber}
selectedConversationId={selectedConversationId}
selectedMessage={selectedMessage}
showConversation={showConversation}
showWhatsNewModal={showWhatsNewModal}
theme={theme} theme={theme}
verifyConversationsStoppingSend={verifyConversationsStoppingSend} verifyConversationsStoppingSend={verifyConversationsStoppingSend}
/> />

View file

@ -79,8 +79,8 @@ const Wrapper = ({
getPreferredBadge={() => undefined} getPreferredBadge={() => undefined}
i18n={i18n} i18n={i18n}
id={id} id={id}
openConversationInternal={action('openConversationInternal')}
sentAt={1587358800000} sentAt={1587358800000}
showConversation={action('showConversation')}
snippet="Lorem <<left>>ipsum<<right>> wow" snippet="Lorem <<left>>ipsum<<right>> wow"
theme={ThemeType.light} theme={ThemeType.light}
to={defaultConversations[1]} to={defaultConversations[1]}

View file

@ -16,6 +16,7 @@ import { ScrollBehavior } from '../types/Util';
import { getConversationListWidthBreakpoint } from './_util'; import { getConversationListWidthBreakpoint } from './_util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid'; import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid';
import type { ShowConversationType } from '../state/ducks/conversations';
import type { PropsData as ConversationListItemPropsType } from './conversationList/ConversationListItem'; import type { PropsData as ConversationListItemPropsType } from './conversationList/ConversationListItem';
import { ConversationListItem } from './conversationList/ConversationListItem'; import { ConversationListItem } from './conversationList/ConversationListItem';
@ -154,7 +155,7 @@ export type PropsType = {
onSelectConversation: (conversationId: string, messageId?: string) => void; onSelectConversation: (conversationId: string, messageId?: string) => void;
renderMessageSearchResult: (id: string) => JSX.Element; renderMessageSearchResult: (id: string) => JSX.Element;
showChooseGroupMembers: () => void; showChooseGroupMembers: () => void;
showConversation: (conversationId: string) => void; showConversation: ShowConversationType;
} & LookupConversationWithoutUuidActionsType; } & LookupConversationWithoutUuidActionsType;
const NORMAL_ROW_HEIGHT = 76; const NORMAL_ROW_HEIGHT = 76;

View file

@ -2,22 +2,25 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import type * as Backbone from 'backbone';
import type { SafetyNumberProps } from './SafetyNumberChangeDialog'; import type { ConversationModel } from '../models/conversations';
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog'; import type {
import type { ConversationType } from '../state/ducks/conversations'; ConversationType,
import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; ShowConversationType,
} from '../state/ducks/conversations';
import type { ConversationView } from '../views/conversation_view';
import type { LocalizerType, ThemeType } from '../types/Util'; import type { LocalizerType, ThemeType } from '../types/Util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { SafetyNumberProps } from './SafetyNumberChangeDialog';
type InboxViewType = Backbone.View & { import * as log from '../logging/log';
onEmpty?: () => void; import { SECOND } from '../util/durations';
}; import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
import { ToastStickerPackInstallFailed } from './ToastStickerPackInstallFailed';
type InboxViewOptionsType = Backbone.ViewOptions & { import { WhatsNewLink } from './WhatsNewLink';
initialLoadComplete: boolean; import { showToast } from '../util/showToast';
window: typeof window; import { strictAssert } from '../util/assert';
};
export type PropsType = { export type PropsType = {
cancelConversationVerification: () => void; cancelConversationVerification: () => void;
@ -27,7 +30,12 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
isCustomizingPreferredReactions: boolean; isCustomizingPreferredReactions: boolean;
renderCustomizingPreferredReactionsModal: () => JSX.Element; renderCustomizingPreferredReactionsModal: () => JSX.Element;
renderLeftPane: () => JSX.Element;
renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element; renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element;
selectedConversationId?: string;
selectedMessage?: string;
showConversation: ShowConversationType;
showWhatsNewModal: () => unknown;
theme: ThemeType; theme: ThemeType;
verifyConversationsStoppingSend: () => void; verifyConversationsStoppingSend: () => void;
}; };
@ -40,38 +48,182 @@ export const Inbox = ({
i18n, i18n,
isCustomizingPreferredReactions, isCustomizingPreferredReactions,
renderCustomizingPreferredReactionsModal, renderCustomizingPreferredReactionsModal,
renderLeftPane,
renderSafetyNumber, renderSafetyNumber,
selectedConversationId,
selectedMessage,
showConversation,
showWhatsNewModal,
theme, theme,
verifyConversationsStoppingSend, verifyConversationsStoppingSend,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const hostRef = useRef<HTMLDivElement | null>(null); const [loadingMessageCount, setLoadingMessageCount] = useState(0);
const viewRef = useRef<InboxViewType | undefined>(undefined); const [internalHasInitialLoadCompleted, setInternalHasInitialLoadCompleted] =
useState(hasInitialLoadCompleted);
const conversationMountRef = useRef<HTMLDivElement | null>(null);
const conversationViewRef = useRef<ConversationView | null>(null);
const [prevConversation, setPrevConversation] = useState<
ConversationModel | undefined
>();
useEffect(() => { useEffect(() => {
const viewOptions: InboxViewOptionsType = { if (!selectedConversationId) {
el: hostRef.current, return;
initialLoadComplete: false, }
window,
};
const view = new window.Whisper.InboxView(viewOptions);
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 () => { return () => {
// [`Backbone.View.prototype.remove`][0] removes the DOM element and stops listening window.Whisper.events.off('loadingProgress', setLoadingMessageCount);
// to event listeners. Because React will do the first, we only want to do the window.Whisper.events.off('pack-install-failed', packInstallFailed);
// second. window.Whisper.events.off('refreshConversation', refreshConversation);
// [0]: https://github.com/jashkenas/backbone/blob/153dc41616a1f2663e4a86b705fefd412ecb4a7a/backbone.js#L1336-L1342 window.Whisper.events.off('setupAsNewDevice', unload);
viewRef.current?.stopListening(); window.Whisper.events.off('showConversation', onShowConversation);
viewRef.current = undefined;
}; };
}, []); }, [prevConversation, showConversation]);
useEffect(() => { useEffect(() => {
if (hasInitialLoadCompleted && viewRef.current && viewRef.current.onEmpty) { if (internalHasInitialLoadCompleted) {
viewRef.current.onEmpty(); return;
}
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 (
<div className="app-loading-screen">
<div className="module-title-bar-drag-area" />
<div className="content">
<div className="module-splash-screen__logo module-img--150" />
<div className="container">
<span className="dot" />
<span className="dot" />
<span className="dot" />
</div>
<div className="message">
{loadingMessageCount
? i18n('loadingMessages', [String(loadingMessageCount)])
: i18n('loading')}
</div>
</div>
</div>
);
} }
}, [hasInitialLoadCompleted, viewRef]);
let activeModal: ReactNode; let activeModal: ReactNode;
if (conversationsStoppingSend.length) { if (conversationsStoppingSend.length) {
@ -94,7 +246,28 @@ export const Inbox = ({
return ( return (
<> <>
<div className="Inbox" ref={hostRef} /> <div className="Inbox">
<div className="module-title-bar-drag-area" />
<div className="left-pane-wrapper">{renderLeftPane()}</div>
<div className="conversation-stack">
<div id="toast" />
<div className="conversation" ref={conversationMountRef} />
{!prevConversation && (
<div className="no-conversation-open">
<div className="module-splash-screen__logo module-img--128 module-logo-blue" />
<h3>{i18n('welcomeToSignal')}</h3>
<p className="whats-new-placeholder">
<WhatsNewLink
i18n={i18n}
showWhatsNewModal={showWhatsNewModal}
/>
</p>
</div>
)}
</div>
</div>
{activeModal} {activeModal}
</> </>
); );

View file

@ -124,7 +124,6 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
getPreferredBadge: () => undefined, getPreferredBadge: () => undefined,
i18n, i18n,
preferredWidthFromStorage: 320, preferredWidthFromStorage: 320,
openConversationInternal: action('openConversationInternal'),
regionCode: 'US', regionCode: 'US',
challengeStatus: select( challengeStatus: select(
'challengeStatus', 'challengeStatus',
@ -148,7 +147,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
getPreferredBadge={() => undefined} getPreferredBadge={() => undefined}
i18n={i18n} i18n={i18n}
id={id} id={id}
openConversationInternal={action('openConversationInternal')} showConversation={action('showConversation')}
sentAt={1587358800000} sentAt={1587358800000}
snippet="Lorem <<left>>ipsum<<right>> wow" snippet="Lorem <<left>>ipsum<<right>> wow"
theme={ThemeType.light} theme={ThemeType.light}

View file

@ -39,7 +39,7 @@ import {
getWidthFromPreferredWidth, getWidthFromPreferredWidth,
} from '../util/leftPaneWidth'; } from '../util/leftPaneWidth';
import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid'; 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 { ConversationList } from './ConversationList';
import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox'; import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
@ -99,25 +99,25 @@ export type PropsType = {
clearSearch: () => void; clearSearch: () => void;
closeMaximumGroupSizeModal: () => void; closeMaximumGroupSizeModal: () => void;
closeRecommendedGroupSizeModal: () => 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; composeDeleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
composeReplaceAvatar: ReplaceAvatarActionType; composeReplaceAvatar: ReplaceAvatarActionType;
composeSaveAvatarToDisk: SaveAvatarToDiskActionType; 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; toggleComposeEditingAvatar: () => unknown;
toggleConversationInChooseMembers: (conversationId: string) => void;
updateSearchTerm: (_: string) => void; updateSearchTerm: (_: string) => void;
// Render Props // Render Props
@ -137,8 +137,6 @@ export type PropsType = {
) => JSX.Element; ) => JSX.Element;
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element; renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
renderCrashReportDialog: () => JSX.Element; renderCrashReportDialog: () => JSX.Element;
showConversation: (conversationId: string) => void;
} & LookupConversationWithoutUuidActionsType; } & LookupConversationWithoutUuidActionsType;
export const LeftPane: React.FC<PropsType> = ({ export const LeftPane: React.FC<PropsType> = ({
@ -156,7 +154,6 @@ export const LeftPane: React.FC<PropsType> = ({
getPreferredBadge, getPreferredBadge,
i18n, i18n,
modeSpecificProps, modeSpecificProps,
openConversationInternal,
preferredWidthFromStorage, preferredWidthFromStorage,
renderCaptchaDialog, renderCaptchaDialog,
renderCrashReportDialog, renderCrashReportDialog,
@ -363,7 +360,7 @@ export const LeftPane: React.FC<PropsType> = ({
if (conversationToOpen) { if (conversationToOpen) {
const { conversationId, messageId } = conversationToOpen; const { conversationId, messageId } = conversationToOpen;
openConversationInternal({ conversationId, messageId }); showConversation({ conversationId, messageId });
if (openedByNumber) { if (openedByNumber) {
clearSearch(); clearSearch();
} }
@ -383,16 +380,16 @@ export const LeftPane: React.FC<PropsType> = ({
document.removeEventListener('keydown', onKeyDown); document.removeEventListener('keydown', onKeyDown);
}; };
}, [ }, [
clearSearch,
helper, helper,
openConversationInternal,
searchInConversation, searchInConversation,
selectedConversationId, selectedConversationId,
selectedMessageId, selectedMessageId,
showChooseGroupMembers, showChooseGroupMembers,
showConversation,
showInbox, showInbox,
startComposing, startComposing,
startSearch, startSearch,
clearSearch,
]); ]);
const requiresFullWidth = helper.requiresFullWidth(); const requiresFullWidth = helper.requiresFullWidth();
@ -488,13 +485,13 @@ export const LeftPane: React.FC<PropsType> = ({
const onSelectConversation = useCallback( const onSelectConversation = useCallback(
(conversationId: string, messageId?: string) => { (conversationId: string, messageId?: string) => {
openConversationInternal({ showConversation({
conversationId, conversationId,
messageId, messageId,
switchToAssociatedView: true, switchToAssociatedView: true,
}); });
}, },
[openConversationInternal] [showConversation]
); );
const previousSelectedConversationId = usePrevious( const previousSelectedConversationId = usePrevious(
@ -555,7 +552,7 @@ export const LeftPane: React.FC<PropsType> = ({
setComposeSearchTerm(event.target.value); setComposeSearchTerm(event.target.value);
}, },
updateSearchTerm, updateSearchTerm,
openConversationInternal, showConversation,
})} })}
<div className="module-left-pane__dialogs"> <div className="module-left-pane__dialogs">
{renderExpiredBuildDialog({ {renderExpiredBuildDialog({

View file

@ -4,7 +4,7 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import type { import type {
ConversationType, ConversationType,
OpenConversationInternalType, ShowConversationType,
} from '../state/ducks/conversations'; } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
@ -20,10 +20,10 @@ type PropsType = {
searchTerm: string; searchTerm: string;
startSearchCounter: number; startSearchCounter: number;
updateSearchTerm: (searchTerm: string) => void; updateSearchTerm: (searchTerm: string) => void;
openConversationInternal: OpenConversationInternalType; showConversation: ShowConversationType;
onEnterKeyDown?: ( onEnterKeyDown?: (
clearSearch: () => void, clearSearch: () => void,
openConversationInternal: OpenConversationInternalType showConversation: ShowConversationType
) => void; ) => void;
}; };
@ -36,7 +36,7 @@ export const LeftPaneSearchInput = ({
searchTerm, searchTerm,
startSearchCounter, startSearchCounter,
updateSearchTerm, updateSearchTerm,
openConversationInternal, showConversation,
onEnterKeyDown, onEnterKeyDown,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const inputRef = useRef<null | HTMLInputElement>(null); const inputRef = useRef<null | HTMLInputElement>(null);
@ -103,7 +103,7 @@ export const LeftPaneSearchInput = ({
}} }}
onKeyDown={event => { onKeyDown={event => {
if (onEnterKeyDown && event.key === 'Enter') { if (onEnterKeyDown && event.key === 'Enter') {
onEnterKeyDown(clearSearch, openConversationInternal); onEnterKeyDown(clearSearch, showConversation);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }

View file

@ -81,10 +81,10 @@ function getAttachmentWithThumbnail(url: string): AttachmentType {
const getDefaultProps = (): PropsType => ({ const getDefaultProps = (): PropsType => ({
hiddenStories: [], hiddenStories: [],
i18n, i18n,
openConversationInternal: action('openConversationInternal'),
preferredWidthFromStorage: 380, preferredWidthFromStorage: 380,
queueStoryDownload: action('queueStoryDownload'), queueStoryDownload: action('queueStoryDownload'),
renderStoryViewer: () => <div />, renderStoryViewer: () => <div />,
showConversation: action('showConversation'),
stories: [ stories: [
createStory({ createStory({
attachment: getAttachmentWithThumbnail( attachment: getAttachmentWithThumbnail(

View file

@ -7,6 +7,7 @@ import classNames from 'classnames';
import type { ConversationStoryType } from './StoryListItem'; import type { ConversationStoryType } from './StoryListItem';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { PropsType as SmartStoryViewerPropsType } from '../state/smart/StoryViewer'; import type { PropsType as SmartStoryViewerPropsType } from '../state/smart/StoryViewer';
import type { ShowConversationType } from '../state/ducks/conversations';
import { StoriesPane } from './StoriesPane'; import { StoriesPane } from './StoriesPane';
import { Theme, themeClassName } from '../util/theme'; import { Theme, themeClassName } from '../util/theme';
import { getWidthFromPreferredWidth } from '../util/leftPaneWidth'; import { getWidthFromPreferredWidth } from '../util/leftPaneWidth';
@ -16,9 +17,9 @@ export type PropsType = {
hiddenStories: Array<ConversationStoryType>; hiddenStories: Array<ConversationStoryType>;
i18n: LocalizerType; i18n: LocalizerType;
preferredWidthFromStorage: number; preferredWidthFromStorage: number;
openConversationInternal: (_: { conversationId: string }) => unknown;
renderStoryViewer: (props: SmartStoryViewerPropsType) => JSX.Element;
queueStoryDownload: (storyId: string) => unknown; queueStoryDownload: (storyId: string) => unknown;
renderStoryViewer: (props: SmartStoryViewerPropsType) => JSX.Element;
showConversation: ShowConversationType;
stories: Array<ConversationStoryType>; stories: Array<ConversationStoryType>;
toggleHideStories: (conversationId: string) => unknown; toggleHideStories: (conversationId: string) => unknown;
toggleStoriesView: () => unknown; toggleStoriesView: () => unknown;
@ -27,10 +28,10 @@ export type PropsType = {
export const Stories = ({ export const Stories = ({
hiddenStories, hiddenStories,
i18n, i18n,
openConversationInternal,
preferredWidthFromStorage, preferredWidthFromStorage,
queueStoryDownload, queueStoryDownload,
renderStoryViewer, renderStoryViewer,
showConversation,
stories, stories,
toggleHideStories, toggleHideStories,
toggleStoriesView, toggleStoriesView,
@ -119,8 +120,8 @@ export const Stories = ({
}); });
setConversationIdToView(clickedIdToView); setConversationIdToView(clickedIdToView);
}} }}
openConversationInternal={openConversationInternal}
queueStoryDownload={queueStoryDownload} queueStoryDownload={queueStoryDownload}
showConversation={showConversation}
stories={stories} stories={stories}
toggleHideStories={toggleHideStories} toggleHideStories={toggleHideStories}
toggleStoriesView={toggleStoriesView} toggleStoriesView={toggleStoriesView}

View file

@ -7,6 +7,7 @@ import classNames from 'classnames';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import type { ConversationStoryType, StoryViewType } from './StoryListItem'; import type { ConversationStoryType, StoryViewType } from './StoryListItem';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { ShowConversationType } from '../state/ducks/conversations';
import { SearchInput } from './SearchInput'; import { SearchInput } from './SearchInput';
import { StoryListItem } from './StoryListItem'; import { StoryListItem } from './StoryListItem';
@ -53,8 +54,8 @@ export type PropsType = {
hiddenStories: Array<ConversationStoryType>; hiddenStories: Array<ConversationStoryType>;
i18n: LocalizerType; i18n: LocalizerType;
onStoryClicked: (conversationId: string) => unknown; onStoryClicked: (conversationId: string) => unknown;
openConversationInternal: (_: { conversationId: string }) => unknown;
queueStoryDownload: (storyId: string) => unknown; queueStoryDownload: (storyId: string) => unknown;
showConversation: ShowConversationType;
stories: Array<ConversationStoryType>; stories: Array<ConversationStoryType>;
toggleHideStories: (conversationId: string) => unknown; toggleHideStories: (conversationId: string) => unknown;
toggleStoriesView: () => unknown; toggleStoriesView: () => unknown;
@ -64,8 +65,8 @@ export const StoriesPane = ({
hiddenStories, hiddenStories,
i18n, i18n,
onStoryClicked, onStoryClicked,
openConversationInternal,
queueStoryDownload, queueStoryDownload,
showConversation,
stories, stories,
toggleHideStories, toggleHideStories,
toggleStoriesView, toggleStoriesView,
@ -121,7 +122,7 @@ export const StoriesPane = ({
}} }}
onHideStory={toggleHideStories} onHideStory={toggleHideStories}
onGoToConversation={conversationId => { onGoToConversation={conversationId => {
openConversationInternal({ conversationId }); showConversation({ conversationId });
toggleStoriesView(); toggleStoriesView();
}} }}
queueStoryDownload={queueStoryDownload} queueStoryDownload={queueStoryDownload}
@ -150,7 +151,7 @@ export const StoriesPane = ({
}} }}
onHideStory={toggleHideStories} onHideStory={toggleHideStories}
onGoToConversation={conversationId => { onGoToConversation={conversationId => {
openConversationInternal({ conversationId }); showConversation({ conversationId });
toggleStoriesView(); toggleStoriesView();
}} }}
queueStoryDownload={queueStoryDownload} queueStoryDownload={queueStoryDownload}

View file

@ -44,8 +44,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
i18n, i18n,
isAdmin: boolean('isAdmin', overrideProps.isAdmin || false), isAdmin: boolean('isAdmin', overrideProps.isAdmin || false),
isMember: boolean('isMember', overrideProps.isMember || true), isMember: boolean('isMember', overrideProps.isMember || true),
openConversationInternal: action('openConversationInternal'),
removeMemberFromGroup: action('removeMemberFromGroup'), removeMemberFromGroup: action('removeMemberFromGroup'),
showConversation: action('showConversation'),
theme: ThemeType.light, theme: ThemeType.light,
toggleSafetyNumberModal: action('toggleSafetyNumberModal'), toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
toggleAdmin: action('toggleAdmin'), toggleAdmin: action('toggleAdmin'),

View file

@ -9,7 +9,10 @@ import { missingCaseError } from '../../util/missingCaseError';
import { About } from './About'; import { About } from './About';
import { Avatar } from '../Avatar'; import { Avatar } from '../Avatar';
import { AvatarLightbox } from '../AvatarLightbox'; import { AvatarLightbox } from '../AvatarLightbox';
import type { ConversationType } from '../../state/ducks/conversations'; import type {
ConversationType,
ShowConversationType,
} from '../../state/ducks/conversations';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import { BadgeDialog } from '../BadgeDialog'; import { BadgeDialog } from '../BadgeDialog';
@ -32,14 +35,8 @@ export type PropsDataType = {
type PropsActionType = { type PropsActionType = {
hideContactModal: () => void; hideContactModal: () => void;
openConversationInternal: (
options: Readonly<{
conversationId: string;
messageId?: string;
switchToAssociatedView?: boolean;
}>
) => void;
removeMemberFromGroup: (conversationId: string, contactId: string) => void; removeMemberFromGroup: (conversationId: string, contactId: string) => void;
showConversation: ShowConversationType;
toggleAdmin: (conversationId: string, contactId: string) => void; toggleAdmin: (conversationId: string, contactId: string) => void;
toggleSafetyNumberModal: (conversationId: string) => unknown; toggleSafetyNumberModal: (conversationId: string) => unknown;
updateConversationModelSharedGroups: (conversationId: string) => void; updateConversationModelSharedGroups: (conversationId: string) => void;
@ -69,8 +66,8 @@ export const ContactModal = ({
i18n, i18n,
isAdmin, isAdmin,
isMember, isMember,
openConversationInternal,
removeMemberFromGroup, removeMemberFromGroup,
showConversation,
theme, theme,
toggleAdmin, toggleAdmin,
toggleSafetyNumberModal, toggleSafetyNumberModal,
@ -205,7 +202,7 @@ export const ContactModal = ({
className="ContactModal__button ContactModal__send-message" className="ContactModal__button ContactModal__send-message"
onClick={() => { onClick={() => {
hideContactModal(); hideContactModal();
openConversationInternal({ conversationId: contact.id }); showConversation({ conversationId: contact.id });
}} }}
> >
<div className="ContactModal__bubble-icon"> <div className="ContactModal__bubble-icon">

View file

@ -53,7 +53,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
to: overrideProps.to as PropsType['to'], to: overrideProps.to as PropsType['to'],
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined), getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
isSelected: boolean('isSelected', overrideProps.isSelected || false), isSelected: boolean('isSelected', overrideProps.isSelected || false),
openConversationInternal: action('openConversationInternal'), showConversation: action('showConversation'),
isSearchingInConversation: boolean( isSearchingInConversation: boolean(
'isSearchingInConversation', 'isSearchingInConversation',
overrideProps.isSearchingInConversation || false overrideProps.isSearchingInConversation || false

View file

@ -15,7 +15,10 @@ import type {
ThemeType, ThemeType,
} from '../../types/Util'; } from '../../types/Util';
import { BaseConversationListItem } from './BaseConversationListItem'; 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'; import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
export type PropsDataType = { export type PropsDataType = {
@ -58,10 +61,7 @@ export type PropsDataType = {
type PropsHousekeepingType = { type PropsHousekeepingType = {
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType; i18n: LocalizerType;
openConversationInternal: (_: { showConversation: ShowConversationType;
conversationId: string;
messageId?: string;
}) => void;
theme: ThemeType; theme: ThemeType;
}; };
@ -147,15 +147,15 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
getPreferredBadge, getPreferredBadge,
i18n, i18n,
id, id,
openConversationInternal,
sentAt, sentAt,
showConversation,
snippet, snippet,
theme, theme,
to, to,
}) { }) {
const onClickItem = useCallback(() => { const onClickItem = useCallback(() => {
openConversationInternal({ conversationId, messageId: id }); showConversation({ conversationId, messageId: id });
}, [openConversationInternal, conversationId, id]); }, [showConversation, conversationId, id]);
if (!from || !to) { if (!from || !to) {
return <div />; return <div />;

View file

@ -11,6 +11,7 @@ import { BaseConversationListItem } from './BaseConversationListItem';
import type { ParsedE164Type } from '../../util/libphonenumberInstance'; import type { ParsedE164Type } from '../../util/libphonenumberInstance';
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid'; import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { ShowConversationType } from '../../state/ducks/conversations';
import { AvatarColors } from '../../types/Colors'; import { AvatarColors } from '../../types/Colors';
type PropsData = { type PropsData = {
@ -20,7 +21,7 @@ type PropsData = {
type PropsHousekeeping = { type PropsHousekeeping = {
i18n: LocalizerType; i18n: LocalizerType;
showConversation: (conversationId: string) => void; showConversation: ShowConversationType;
} & LookupConversationWithoutUuidActionsType; } & LookupConversationWithoutUuidActionsType;
export type Props = PropsData & PropsHousekeeping; export type Props = PropsData & PropsHousekeeping;
@ -55,7 +56,7 @@ export const StartNewConversation: FunctionComponent<Props> = React.memo(
}); });
if (conversationId !== undefined) { if (conversationId !== undefined) {
showConversation(conversationId); showConversation({ conversationId });
} }
}, [ }, [
showConversation, showConversation,

View file

@ -9,6 +9,7 @@ import { BaseConversationListItem } from './BaseConversationListItem';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid'; import { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid';
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid'; import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
import type { ShowConversationType } from '../../state/ducks/conversations';
type PropsData = { type PropsData = {
username: string; username: string;
@ -17,7 +18,7 @@ type PropsData = {
type PropsHousekeeping = { type PropsHousekeeping = {
i18n: LocalizerType; i18n: LocalizerType;
showConversation: (conversationId: string) => void; showConversation: ShowConversationType;
} & LookupConversationWithoutUuidActionsType; } & LookupConversationWithoutUuidActionsType;
export type Props = PropsData & PropsHousekeeping; export type Props = PropsData & PropsHousekeeping;
@ -44,7 +45,7 @@ export const UsernameSearchResultListItem: FunctionComponent<Props> = ({
}); });
if (conversationId !== undefined) { if (conversationId !== undefined) {
showConversation(conversationId); showConversation({ conversationId });
} }
}, [ }, [
username, username,

View file

@ -14,7 +14,7 @@ import type { PropsData as ConversationListItemPropsType } from '../conversation
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { import type {
ConversationType, ConversationType,
OpenConversationInternalType, ShowConversationType,
} from '../../state/ducks/conversations'; } from '../../state/ducks/conversations';
import { LeftPaneSearchInput } from '../LeftPaneSearchInput'; import { LeftPaneSearchInput } from '../LeftPaneSearchInput';
import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper'; import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper';
@ -84,13 +84,13 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
clearSearch, clearSearch,
i18n, i18n,
updateSearchTerm, updateSearchTerm,
openConversationInternal, showConversation,
}: Readonly<{ }: Readonly<{
clearConversationSearch: () => unknown; clearConversationSearch: () => unknown;
clearSearch: () => unknown; clearSearch: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => unknown; updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType; showConversation: ShowConversationType;
}>): ReactChild | null { }>): ReactChild | null {
if (!this.searchConversation) { if (!this.searchConversation) {
return null; return null;
@ -103,9 +103,9 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
i18n={i18n} i18n={i18n}
searchConversation={this.searchConversation} searchConversation={this.searchConversation}
searchTerm={this.searchTerm} searchTerm={this.searchTerm}
showConversation={showConversation}
startSearchCounter={this.startSearchCounter} startSearchCounter={this.startSearchCounter}
updateSearchTerm={updateSearchTerm} updateSearchTerm={updateSearchTerm}
openConversationInternal={openConversationInternal}
/> />
); );
} }

View file

@ -10,7 +10,7 @@ import type {
ReplaceAvatarActionType, ReplaceAvatarActionType,
SaveAvatarToDiskActionType, SaveAvatarToDiskActionType,
} from '../../types/Avatar'; } from '../../types/Avatar';
import type { OpenConversationInternalType } from '../../state/ducks/conversations'; import type { ShowConversationType } from '../../state/ducks/conversations';
export enum FindDirection { export enum FindDirection {
Up, Up,
@ -43,7 +43,7 @@ export abstract class LeftPaneHelper<T> {
event: ChangeEvent<HTMLInputElement> event: ChangeEvent<HTMLInputElement>
) => unknown; ) => unknown;
updateSearchTerm: (searchTerm: string) => unknown; updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType; showConversation: ShowConversationType;
}> }>
): null | ReactChild { ): null | ReactChild {
return null; return null;

View file

@ -9,7 +9,7 @@ import { Intl } from '../Intl';
import type { ToFindType } from './LeftPaneHelper'; import type { ToFindType } from './LeftPaneHelper';
import type { import type {
ConversationType, ConversationType,
OpenConversationInternalType, ShowConversationType,
} from '../../state/ducks/conversations'; } from '../../state/ducks/conversations';
import { LeftPaneHelper } from './LeftPaneHelper'; import { LeftPaneHelper } from './LeftPaneHelper';
import { getConversationInDirection } from './getConversationInDirection'; import { getConversationInDirection } from './getConversationInDirection';
@ -85,14 +85,14 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
clearConversationSearch, clearConversationSearch,
clearSearch, clearSearch,
i18n, i18n,
showConversation,
updateSearchTerm, updateSearchTerm,
openConversationInternal,
}: Readonly<{ }: Readonly<{
clearConversationSearch: () => unknown; clearConversationSearch: () => unknown;
clearSearch: () => unknown; clearSearch: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
showConversation: ShowConversationType;
updateSearchTerm: (searchTerm: string) => unknown; updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}>): ReactChild { }>): ReactChild {
return ( return (
<LeftPaneSearchInput <LeftPaneSearchInput
@ -102,9 +102,9 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
i18n={i18n} i18n={i18n}
searchConversation={this.searchConversation} searchConversation={this.searchConversation}
searchTerm={this.searchTerm} searchTerm={this.searchTerm}
showConversation={showConversation}
startSearchCounter={this.startSearchCounter} startSearchCounter={this.startSearchCounter}
updateSearchTerm={updateSearchTerm} updateSearchTerm={updateSearchTerm}
openConversationInternal={openConversationInternal}
/> />
); );
} }

View file

@ -13,7 +13,7 @@ import type { PropsData as ConversationListItemPropsType } from '../conversation
import { handleKeydownForSearch } from './handleKeydownForSearch'; import { handleKeydownForSearch } from './handleKeydownForSearch';
import type { import type {
ConversationType, ConversationType,
OpenConversationInternalType, ShowConversationType,
} from '../../state/ducks/conversations'; } from '../../state/ducks/conversations';
import { LeftPaneSearchInput } from '../LeftPaneSearchInput'; import { LeftPaneSearchInput } from '../LeftPaneSearchInput';
@ -104,14 +104,14 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
clearConversationSearch, clearConversationSearch,
clearSearch, clearSearch,
i18n, i18n,
showConversation,
updateSearchTerm, updateSearchTerm,
openConversationInternal,
}: Readonly<{ }: Readonly<{
clearConversationSearch: () => unknown; clearConversationSearch: () => unknown;
clearSearch: () => unknown; clearSearch: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
showConversation: ShowConversationType;
updateSearchTerm: (searchTerm: string) => unknown; updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}>): ReactChild { }>): ReactChild {
return ( return (
<LeftPaneSearchInput <LeftPaneSearchInput
@ -119,12 +119,12 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
clearSearch={clearSearch} clearSearch={clearSearch}
disabled={this.searchDisabled} disabled={this.searchDisabled}
i18n={i18n} i18n={i18n}
onEnterKeyDown={this.onEnterKeyDown}
searchConversation={this.searchConversation} searchConversation={this.searchConversation}
searchTerm={this.searchTerm} searchTerm={this.searchTerm}
showConversation={showConversation}
startSearchCounter={this.startSearchCounter} startSearchCounter={this.startSearchCounter}
updateSearchTerm={updateSearchTerm} updateSearchTerm={updateSearchTerm}
openConversationInternal={openConversationInternal}
onEnterKeyDown={this.onEnterKeyDown}
/> />
); );
} }
@ -361,13 +361,13 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
private onEnterKeyDown( private onEnterKeyDown(
clearSearch: () => unknown, clearSearch: () => unknown,
openConversationInternal: OpenConversationInternalType showConversation: ShowConversationType
): void { ): void {
const conversation = this.getConversationAndMessageAtIndex(0); const conversation = this.getConversationAndMessageAtIndex(0);
if (!conversation) { if (!conversation) {
return; return;
} }
openConversationInternal(conversation); showConversation(conversation);
clearSearch(); clearSearch();
} }
} }

View file

@ -74,7 +74,7 @@ export async function joinViaLink(hash: string): Promise<void> {
log.warn( log.warn(
`joinViaLink/${logId}: Already a member of group, opening conversation` `joinViaLink/${logId}: Already a member of group, opening conversation`
); );
window.reduxActions.conversations.openConversationInternal({ window.reduxActions.conversations.showConversation({
conversationId: existingConversation.id, conversationId: existingConversation.id,
}); });
showToast(ToastAlreadyGroupMember); showToast(ToastAlreadyGroupMember);
@ -166,7 +166,7 @@ export async function joinViaLink(hash: string): Promise<void> {
// We're waiting for the left pane to re-sort before we navigate to that conversation // We're waiting for the left pane to re-sort before we navigate to that conversation
await sleep(200); await sleep(200);
window.reduxActions.conversations.openConversationInternal({ window.reduxActions.conversations.showConversation({
conversationId: existingConversation.id, conversationId: existingConversation.id,
}); });
@ -253,7 +253,7 @@ export async function joinViaLink(hash: string): Promise<void> {
log.warn( log.warn(
`joinViaLink/${logId}: User is part of group on second check, opening conversation` `joinViaLink/${logId}: User is part of group on second check, opening conversation`
); );
window.reduxActions.conversations.openConversationInternal({ window.reduxActions.conversations.showConversation({
conversationId: targetConversation.id, conversationId: targetConversation.id,
}); });
return; return;
@ -347,7 +347,7 @@ export async function joinViaLink(hash: string): Promise<void> {
); );
} }
window.reduxActions.conversations.openConversationInternal({ window.reduxActions.conversations.showConversation({
conversationId: targetConversation.id, conversationId: targetConversation.id,
}); });
} catch (error) { } catch (error) {

View file

@ -3,7 +3,6 @@
// The idea with this file is to make it webpackable for the style guide // 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 Crypto from './Crypto';
import * as Curve from './Curve'; import * as Curve from './Curve';
import { start as conversationControllerStart } from './ConversationController'; import { start as conversationControllerStart } from './ConversationController';
@ -33,7 +32,6 @@ import { createForwardMessageModal } from './state/roots/createForwardMessageMod
import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement'; import { createGroupLinkManagement } from './state/roots/createGroupLinkManagement';
import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal'; import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal';
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal'; import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import { createLeftPane } from './state/roots/createLeftPane';
import { createMessageDetail } from './state/roots/createMessageDetail'; import { createMessageDetail } from './state/roots/createMessageDetail';
import { createConversationNotificationsSettings } from './state/roots/createConversationNotificationsSettings'; import { createConversationNotificationsSettings } from './state/roots/createConversationNotificationsSettings';
import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions'; import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions';
@ -424,7 +422,6 @@ export const setup = (options: {
createGroupV1MigrationModal, createGroupV1MigrationModal,
createGroupV2JoinModal, createGroupV2JoinModal,
createGroupV2Permissions, createGroupV2Permissions,
createLeftPane,
createMessageDetail, createMessageDetail,
createConversationNotificationsSettings, createConversationNotificationsSettings,
createPendingInvites, createPendingInvites,
@ -482,7 +479,6 @@ export const setup = (options: {
}; };
return { return {
Backbone,
Components, Components,
Crypto, Crypto,
Curve, Curve,

View file

@ -4,10 +4,11 @@
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
import type { import type {
SwitchToAssociatedViewActionType,
MessageDeletedActionType, MessageDeletedActionType,
MessageChangedActionType, MessageChangedActionType,
SelectedConversationChangedActionType,
} from './conversations'; } from './conversations';
import { SELECTED_CONVERSATION_CHANGED } from './conversations';
// State // State
@ -59,9 +60,9 @@ export function reducer(
state: Readonly<AudioPlayerStateType> = getEmptyState(), state: Readonly<AudioPlayerStateType> = getEmptyState(),
action: Readonly< action: Readonly<
| AudioPlayerActionType | AudioPlayerActionType
| SwitchToAssociatedViewActionType
| MessageDeletedActionType | MessageDeletedActionType
| MessageChangedActionType | MessageChangedActionType
| SelectedConversationChangedActionType
> >
): AudioPlayerStateType { ): AudioPlayerStateType {
if (action.type === 'audioPlayer/SET_ACTIVE_AUDIO_ID') { if (action.type === 'audioPlayer/SET_ACTIVE_AUDIO_ID') {
@ -75,7 +76,7 @@ export function reducer(
} }
// Reset activeAudioID on conversation change. // Reset activeAudioID on conversation change.
if (action.type === 'SWITCH_TO_ASSOCIATED_VIEW') { if (action.type === SELECTED_CONVERSATION_CHANGED) {
return { return {
...state, ...state,
activeAudioID: undefined, activeAudioID: undefined,

View file

@ -22,7 +22,6 @@ import { calling } from '../../services/calling';
import { getOwn } from '../../util/getOwn'; import { getOwn } from '../../util/getOwn';
import { assert, strictAssert } from '../../util/assert'; import { assert, strictAssert } from '../../util/assert';
import * as universalExpireTimer from '../../util/universalExpireTimer'; import * as universalExpireTimer from '../../util/universalExpireTimer';
import { trigger } from '../../shims/events';
import type { ToggleProfileEditorErrorActionType } from './globalModals'; import type { ToggleProfileEditorErrorActionType } from './globalModals';
import { TOGGLE_PROFILE_EDITOR_ERROR } from './globalModals'; import { TOGGLE_PROFILE_EDITOR_ERROR } from './globalModals';
import { isRecord } from '../../util/isRecord'; import { isRecord } from '../../util/isRecord';
@ -347,12 +346,6 @@ export type ConversationsStateType = {
messagesByConversation: MessagesByConversationType; messagesByConversation: MessagesByConversationType;
}; };
export type OpenConversationInternalType = (_: {
conversationId: string;
messageId?: string;
switchToAssociatedView?: boolean;
}) => void;
// Helpers // Helpers
export const getConversationCallMode = ( export const getConversationCallMode = (
@ -399,6 +392,8 @@ const CONVERSATION_STOPPED_BY_MISSING_VERIFICATION =
const DISCARD_MESSAGES = 'conversations/DISCARD_MESSAGES'; const DISCARD_MESSAGES = 'conversations/DISCARD_MESSAGES';
const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS'; const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS';
const UPDATE_USERNAME_SAVE_STATE = 'conversations/UPDATE_USERNAME_SAVE_STATE'; const UPDATE_USERNAME_SAVE_STATE = 'conversations/UPDATE_USERNAME_SAVE_STATE';
export const SELECTED_CONVERSATION_CHANGED =
'conversations/SELECTED_CONVERSATION_CHANGED';
export type CancelVerificationDataByConversationActionType = { export type CancelVerificationDataByConversationActionType = {
type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION; type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION;
@ -642,10 +637,11 @@ export type ClearUnreadMetricsActionType = {
}; };
}; };
export type SelectedConversationChangedActionType = { export type SelectedConversationChangedActionType = {
type: 'SELECTED_CONVERSATION_CHANGED'; type: typeof SELECTED_CONVERSATION_CHANGED;
payload: { payload: {
id: string; id?: string;
messageId?: string; messageId?: string;
switchToAssociatedView?: boolean;
}; };
}; };
type ReviewGroupMemberNameCollisionActionType = { type ReviewGroupMemberNameCollisionActionType = {
@ -710,10 +706,6 @@ type ShowChooseGroupMembersActionType = {
type StartSettingGroupMetadataActionType = { type StartSettingGroupMetadataActionType = {
type: 'START_SETTING_GROUP_METADATA'; type: 'START_SETTING_GROUP_METADATA';
}; };
export type SwitchToAssociatedViewActionType = {
type: 'SWITCH_TO_ASSOCIATED_VIEW';
payload: { conversationId: string };
};
export type ToggleConversationInChooseMembersActionType = { export type ToggleConversationInChooseMembersActionType = {
type: 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS'; type: 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS';
payload: { payload: {
@ -792,7 +784,6 @@ export type ConversationActionType =
| ShowInboxActionType | ShowInboxActionType
| StartComposingActionType | StartComposingActionType
| StartSettingGroupMetadataActionType | StartSettingGroupMetadataActionType
| SwitchToAssociatedViewActionType
| ToggleConversationInChooseMembersActionType | ToggleConversationInChooseMembersActionType
| ToggleComposeEditingAvatarActionType | ToggleComposeEditingAvatarActionType
| UpdateUsernameSaveStateActionType; | UpdateUsernameSaveStateActionType;
@ -831,8 +822,6 @@ export const actions = {
messagesAdded, messagesAdded,
messagesReset, messagesReset,
myProfileChanged, myProfileChanged,
openConversationExternal,
openConversationInternal,
removeAllConversations, removeAllConversations,
removeCustomColorOnConversations, removeCustomColorOnConversations,
removeMemberFromGroup, removeMemberFromGroup,
@ -1527,9 +1516,9 @@ function createGroup(
| CreateGroupPendingActionType | CreateGroupPendingActionType
| CreateGroupFulfilledActionType | CreateGroupFulfilledActionType
| CreateGroupRejectedActionType | CreateGroupRejectedActionType
| SwitchToAssociatedViewActionType | SelectedConversationChangedActionType
> { > {
return async (dispatch, getState, ...args) => { return async (dispatch, getState) => {
const { composer } = getState().conversations; const { composer } = getState().conversations;
if ( if (
composer?.step !== ComposerStep.SetGroupMetadata || composer?.step !== ComposerStep.SetGroupMetadata ||
@ -1559,10 +1548,12 @@ function createGroup(
), ),
}, },
}); });
openConversationInternal({ dispatch(
showConversation({
conversationId: conversation.id, conversationId: conversation.id,
switchToAssociatedView: true, switchToAssociatedView: true,
})(dispatch, getState, ...args); })
);
} catch (err) { } catch (err) {
log.error('Failed to create group', err && err.stack ? err.stack : err); log.error('Failed to create group', err && err.stack ? err.stack : err);
dispatch({ type: 'CREATE_GROUP_REJECTED' }); 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( function toggleHideStories(
conversationId: string conversationId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
@ -2039,12 +1988,26 @@ function showInbox(): ShowInboxActionType {
payload: null, payload: null,
}; };
} }
function showConversation(
conversationId: string type ShowConversationArgsType = {
): ThunkAction<void, RootStateType, unknown, ShowInboxActionType> { conversationId?: string;
return dispatch => { messageId?: string;
trigger('showConversation', conversationId); switchToAssociatedView?: boolean;
dispatch(showInbox()); };
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 { function showArchivedConversations(): ShowArchivedConversationsActionType {
@ -2475,7 +2438,7 @@ export function reducer(
}; };
} }
if (action.type === 'CREATE_GROUP_FULFILLED') { 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. // the work.
return { return {
...state, ...state,
@ -3065,14 +3028,31 @@ export function reducer(
}, },
}; };
} }
if (action.type === 'SELECTED_CONVERSATION_CHANGED') { if (action.type === SELECTED_CONVERSATION_CHANGED) {
const { payload } = action; const { payload } = action;
const { id } = payload; const { id, messageId, switchToAssociatedView } = payload;
return { const nextState = {
...omit(state, 'contactSpoofingReview'), ...omit(state, 'contactSpoofingReview'),
selectedConversationId: id, 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') { if (action.type === 'SHOW_INBOX') {
return { 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') { if (action.type === 'TOGGLE_CONVERSATION_IN_CHOOSE_MEMBERS') {
const { composer } = state; const { composer } = state;
if (composer?.step !== ComposerStep.ChooseGroupMembers) { if (composer?.step !== ComposerStep.ChooseGroupMembers) {

View file

@ -31,6 +31,7 @@ import {
getUserConversationId, getUserConversationId,
} from '../selectors/user'; } from '../selectors/user';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { SELECTED_CONVERSATION_CHANGED } from './conversations';
const { const {
searchMessages: dataSearchMessages, searchMessages: dataSearchMessages,
@ -431,7 +432,7 @@ export function reducer(
return getEmptyState(); return getEmptyState();
} }
if (action.type === 'SELECTED_CONVERSATION_CHANGED') { if (action.type === SELECTED_CONVERSATION_CHANGED) {
const { payload } = action; const { payload } = action;
const { id, messageId } = payload; const { id, messageId } = payload;
const { searchConversationId } = state; const { searchConversationId } = state;

View file

@ -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 => (
<Provider store={store}>
<SmartLeftPane />
</Provider>
);

View file

@ -10,6 +10,7 @@ import { App } from '../../components/App';
import { SmartCallManager } from './CallManager'; import { SmartCallManager } from './CallManager';
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal'; import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
import { SmartGlobalModalContainer } from './GlobalModalContainer'; import { SmartGlobalModalContainer } from './GlobalModalContainer';
import { SmartLeftPane } from './LeftPane';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer'; import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { SmartStories } from './Stories'; import { SmartStories } from './Stories';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
@ -47,6 +48,7 @@ const mapStateToProps = (state: StateType) => {
<SmartCustomizingPreferredReactionsModal /> <SmartCustomizingPreferredReactionsModal />
), ),
renderGlobalModalContainer: () => <SmartGlobalModalContainer />, renderGlobalModalContainer: () => <SmartGlobalModalContainer />,
renderLeftPane: () => <SmartLeftPane />,
renderSafetyNumber: (props: SafetyNumberProps) => ( renderSafetyNumber: (props: SafetyNumberProps) => (
<SmartSafetyNumberViewer {...props} /> <SmartSafetyNumberViewer {...props} />
), ),
@ -68,6 +70,8 @@ const mapStateToProps = (state: StateType) => {
registerSingleDevice: (number: string, code: string): Promise<void> => { registerSingleDevice: (number: string, code: string): Promise<void> => {
return window.getAccountManager().registerSingleDevice(number, code); return window.getAccountManager().registerSingleDevice(number, code);
}, },
selectedConversationId: state.conversations.selectedConversationId,
selectedMessage: state.conversations.selectedMessage,
theme: getTheme(state), theme: getTheme(state),
executeMenuRole: (role: MenuItemConstructorOptions['role']): void => { executeMenuRole: (role: MenuItemConstructorOptions['role']): void => {

View file

@ -33,8 +33,7 @@ function renderStoryViewer({
export function SmartStories(): JSX.Element | null { export function SmartStories(): JSX.Element | null {
const storiesActions = useStoriesActions(); const storiesActions = useStoriesActions();
const { openConversationInternal, toggleHideStories } = const { showConversation, toggleHideStories } = useConversationsActions();
useConversationsActions();
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector<StateType, LocalizerType>(getIntl);
@ -56,9 +55,9 @@ export function SmartStories(): JSX.Element | null {
<Stories <Stories
hiddenStories={hiddenStories} hiddenStories={hiddenStories}
i18n={i18n} i18n={i18n}
openConversationInternal={openConversationInternal}
preferredWidthFromStorage={preferredWidthFromStorage} preferredWidthFromStorage={preferredWidthFromStorage}
renderStoryViewer={renderStoryViewer} renderStoryViewer={renderStoryViewer}
showConversation={showConversation}
stories={stories} stories={stories}
toggleHideStories={toggleHideStories} toggleHideStories={toggleHideStories}
{...storiesActions} {...storiesActions}

View file

@ -41,8 +41,7 @@ export function SmartStoryViewer({
const storiesActions = useStoriesActions(); const storiesActions = useStoriesActions();
const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions(); const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions();
const { onUseEmoji } = useEmojisActions(); const { onUseEmoji } = useEmojisActions();
const { openConversationInternal, toggleHideStories } = const { showConversation, toggleHideStories } = useConversationsActions();
useConversationsActions();
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector<StateType, LocalizerType>(getIntl);
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
@ -74,7 +73,7 @@ export function SmartStoryViewer({
onClose={onClose} onClose={onClose}
onHideStory={toggleHideStories} onHideStory={toggleHideStories}
onGoToConversation={senderId => { onGoToConversation={senderId => {
openConversationInternal({ conversationId: senderId }); showConversation({ conversationId: senderId });
storiesActions.toggleStoriesView(); storiesActions.toggleStoriesView();
}} }}
onNextUserStories={onNextUserStories} onNextUserStories={onNextUserStories}

View file

@ -4,8 +4,11 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { actions } from '../../../state/ducks/audioPlayer'; import { actions } from '../../../state/ducks/audioPlayer';
import type { SwitchToAssociatedViewActionType } from '../../../state/ducks/conversations'; import type { SelectedConversationChangedActionType } from '../../../state/ducks/conversations';
import { actions as conversationsActions } from '../../../state/ducks/conversations'; import {
SELECTED_CONVERSATION_CHANGED,
actions as conversationsActions,
} from '../../../state/ducks/conversations';
import { noopAction } from '../../../state/ducks/noop'; import { noopAction } from '../../../state/ducks/noop';
import type { StateType } from '../../../state/reducer'; import type { StateType } from '../../../state/reducer';
@ -51,9 +54,9 @@ describe('both/state/ducks/audioPlayer', () => {
it('resets activeAudioID when changing the conversation', () => { it('resets activeAudioID when changing the conversation', () => {
const state = getInitializedState(); const state = getInitializedState();
const updated = rootReducer(state, <SwitchToAssociatedViewActionType>{ const updated = rootReducer(state, <SelectedConversationChangedActionType>{
type: 'SWITCH_TO_ASSOCIATED_VIEW', type: SELECTED_CONVERSATION_CHANGED,
payload: { conversationId: 'any' }, payload: { id: 'any' },
}); });
assert.strictEqual(updated.audioPlayer.activeAudioID, undefined); assert.strictEqual(updated.audioPlayer.activeAudioID, undefined);

View file

@ -15,13 +15,14 @@ import {
import type { import type {
CancelVerificationDataByConversationActionType, CancelVerificationDataByConversationActionType,
ConversationMessageType, ConversationMessageType,
ConversationsStateType,
ConversationType, ConversationType,
ConversationsStateType,
MessageType, MessageType,
SwitchToAssociatedViewActionType, SelectedConversationChangedActionType,
ToggleConversationInChooseMembersActionType, ToggleConversationInChooseMembersActionType,
} from '../../../state/ducks/conversations'; } from '../../../state/ducks/conversations';
import { import {
SELECTED_CONVERSATION_CHANGED,
actions, actions,
cancelConversationVerification, cancelConversationVerification,
clearCancelledConversationVerification, clearCancelledConversationVerification,
@ -56,7 +57,6 @@ const {
createGroup, createGroup,
discardMessages, discardMessages,
messageChanged, messageChanged,
openConversationInternal,
repairNewestMessage, repairNewestMessage,
repairOldestMessage, repairOldestMessage,
resetAllChatColors, resetAllChatColors,
@ -68,6 +68,7 @@ const {
setPreJoinConversation, setPreJoinConversation,
showArchivedConversations, showArchivedConversations,
showChooseGroupMembers, showChooseGroupMembers,
showConversation,
showInbox, showInbox,
startComposing, startComposing,
startSettingGroupMetadata, startSettingGroupMetadata,
@ -341,82 +342,40 @@ describe('both/state/ducks/conversations', () => {
}; };
} }
describe('openConversationInternal', () => { describe('showConversation', () => {
it("returns a thunk that triggers a 'showConversation' event when passed a conversation ID", () => { it('selects a conversation id', () => {
const dispatch = sinon.spy(); const state = {
...getEmptyState(),
};
const action = showConversation({ conversationId: 'abc123' });
const nextState = reducer(state, action);
openConversationInternal({ conversationId: 'abc123' })( assert.equal(nextState.selectedConversationId, 'abc123');
dispatch, assert.isUndefined(nextState.selectedMessage);
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
);
}); });
it("returns a thunk that triggers a 'showConversation' event when passed a conversation ID and message ID", () => { it('selects a conversation and a message', () => {
const dispatch = sinon.spy(); const state = {
...getEmptyState(),
openConversationInternal({ };
const action = showConversation({
conversationId: 'abc123', conversationId: 'abc123',
messageId: 'xyz987', messageId: 'xyz987',
})(dispatch, getEmptyRootState, null); });
const nextState = reducer(state, action);
sinon.assert.calledOnce( assert.equal(nextState.selectedConversationId, 'abc123');
window.Whisper.events.trigger as sinon.SinonSpy assert.equal(nextState.selectedMessage, 'xyz987');
);
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", () => { describe('showConversation switchToAssociatedView=true', () => {
const dispatch = sinon.spy(); let action: SelectedConversationChangedActionType;
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' },
});
});
describe('SWITCH_TO_ASSOCIATED_VIEW', () => {
let action: SwitchToAssociatedViewActionType;
beforeEach(() => { beforeEach(() => {
const dispatch = sinon.spy(); action = showConversation({
openConversationInternal({
conversationId: 'fake-conversation-id', conversationId: 'fake-conversation-id',
switchToAssociatedView: true, switchToAssociatedView: true,
})(dispatch, getEmptyRootState, null); });
[action] = dispatch.getCall(0).args;
}); });
it('shows the inbox if the conversation is not archived', () => { it('shows the inbox if the conversation is not archived', () => {
@ -451,13 +410,6 @@ describe('both/state/ducks/conversations', () => {
assert.isUndefined(result.composer); assert.isUndefined(result.composer);
assert.isTrue(result.showArchived); 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 null
); );
sinon.assert.calledWith(
window.Whisper.events.trigger as sinon.SinonSpy,
'showConversation',
'9876',
undefined
);
sinon.assert.calledWith(dispatch, { sinon.assert.calledWith(dispatch, {
type: 'CREATE_GROUP_FULFILLED', type: 'CREATE_GROUP_FULFILLED',
payload: { invitedUuids: [abc] }, 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 fulfilledAction = dispatch.getCall(1).args[0];
const result = reducer(conversationsState, fulfilledAction); const result = reducer(conversationsState, fulfilledAction);
assert.deepEqual(result.invitedUuidsForNewlyCreatedGroup, [abc]); assert.deepEqual(result.invitedUuidsForNewlyCreatedGroup, [abc]);
sinon.assert.calledWith(dispatch, {
type: 'SWITCH_TO_ASSOCIATED_VIEW',
payload: { conversationId: '9876' },
});
}); });
}); });

View file

@ -8300,22 +8300,6 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-07-21T18:34:59.251Z" "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(", "rule": "jQuery-html(",
"path": "ts/backbone/views/whisper_view.ts", "path": "ts/backbone/views/whisper_view.ts",
@ -8598,16 +8582,16 @@
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/Inbox.tsx", "path": "ts/components/Inbox.tsx",
"line": " const hostRef = useRef<HTMLDivElement | null>(null);", "line": " const conversationMountRef = useRef<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-07-30T16:57:33.618Z" "updated": "2022-06-15T01:24:12.761Z"
}, },
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/Inbox.tsx", "path": "ts/components/Inbox.tsx",
"line": " const viewRef = useRef<InboxViewType | undefined>(undefined);", "line": " const conversationViewRef = useRef<ConversationView | null>(null);",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-07-30T16:57:33.618Z" "updated": "2022-06-15T01:24:12.761Z"
}, },
{ {
"rule": "React-useRef", "rule": "React-useRef",
@ -9121,90 +9105,6 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2021-09-17T21:51:57.475Z" "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", "rule": "DOM-innerHTML",
"path": "ts/windows/loading/start.ts", "path": "ts/windows/loading/start.ts",

39
ts/util/showLightbox.tsx Normal file
View file

@ -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 <Lightbox /> 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(<Lightbox {...props} />, lightboxMountNode);
}

View file

@ -58,7 +58,7 @@ import {
import { getActiveCallState } from '../state/selectors/calling'; import { getActiveCallState } from '../state/selectors/calling';
import { getTheme } from '../state/selectors/user'; import { getTheme } from '../state/selectors/user';
import { ReactWrapperView } from './ReactWrapperView'; import { ReactWrapperView } from './ReactWrapperView';
import { Lightbox } from '../components/Lightbox'; import type { Lightbox } from '../components/Lightbox';
import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList'; import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList';
import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog'; import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog';
import type { import type {
@ -121,6 +121,11 @@ import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone';
import { ContactDetail } from '../components/conversation/ContactDetail'; import { ContactDetail } from '../components/conversation/ContactDetail';
import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery'; import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery';
import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent'; import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent';
import {
closeLightbox,
isLightboxOpen,
showLightbox,
} from '../util/showLightbox';
type AttachmentOptions = { type AttachmentOptions = {
messageId: string; messageId: string;
@ -1905,28 +1910,23 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
await message.markViewOnceMessageViewed(); await message.markViewOnceMessageViewed();
const closeLightbox = async () => { this.listenTo(message, 'expired', async () => {
log.info('displayTapToViewMessage: attempting to close lightbox'); 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'); log.info('displayTapToViewMessage: lightbox was already closed');
return; return;
} }
const { lightboxView } = this;
this.lightboxView = undefined;
this.stopListening(message); this.stopListening(message);
window.Signal.Backbone.Views.Lightbox.hide(); closeLightbox();
lightboxView.remove();
await deleteTempFile(tempPath); await deleteTempFile(tempPath);
}; });
this.listenTo(message, 'expired', closeLightbox);
this.listenTo(message, 'change', () => { this.listenTo(message, 'change', () => {
if (this.lightboxView) { showLightbox(getProps());
this.lightboxView.update(<Lightbox {...getProps()} />);
}
}); });
const getProps = (): ComponentProps<typeof Lightbox> => { const getProps = (): ComponentProps<typeof Lightbox> => {
@ -1934,7 +1934,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return { return {
close: () => { close: () => {
this.lightboxView?.remove(); closeLightbox();
}, },
i18n: window.i18n, i18n: window.i18n,
media: [ media: [
@ -1957,18 +1957,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}; };
}; };
if (this.lightboxView) { showLightbox(getProps());
this.lightboxView.remove();
this.lightboxView = undefined;
}
this.lightboxView = new ReactWrapperView({
className: 'lightbox-wrapper',
JSX: <Lightbox {...getProps()} />,
onClose: closeLightbox,
});
window.Signal.Backbone.Views.Lightbox.show(this.lightboxView.el);
log.info('displayTapToViewMessage: showed lightbox'); log.info('displayTapToViewMessage: showed lightbox');
} }
@ -2084,34 +2073,17 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
mediaItem.attachment.path === selectedMediaItem.attachment.path mediaItem.attachment.path === selectedMediaItem.attachment.path
); );
if (this.lightboxView) { showLightbox({
this.lightboxView.remove(); close: closeLightbox,
this.lightboxView = undefined; i18n: window.i18n,
} getConversation: getConversationSelector(window.reduxStore.getState()),
media,
this.lightboxView = new ReactWrapperView({ onForward: messageId => {
className: 'lightbox-wrapper',
JSX: (
<Lightbox
close={() => {
this.lightboxView?.remove();
}}
i18n={window.i18n}
getConversation={getConversationSelector(
window.reduxStore.getState()
)}
media={media}
onForward={messageId => {
this.showForwardMessageModal(messageId); this.showForwardMessageModal(messageId);
}} },
onSave={onSave} onSave,
selectedIndex={selectedIndex >= 0 ? selectedIndex : 0} selectedIndex: selectedIndex >= 0 ? selectedIndex : 0,
/>
),
onClose: () => window.Signal.Backbone.Views.Lightbox.hide(),
}); });
window.Signal.Backbone.Views.Lightbox.show(this.lightboxView.el);
} }
showLightbox({ showLightbox({

View file

@ -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<ConversationModel> = [];
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: (
<WhatsNewLink
i18n={window.i18n}
showWhatsNewModal={showWhatsNewModal}
/>
),
});
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?.();
}
},
});

3
ts/window.d.ts vendored
View file

@ -44,7 +44,6 @@ import { createGroupLinkManagement } from './state/roots/createGroupLinkManageme
import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal'; import { createGroupV1MigrationModal } from './state/roots/createGroupV1MigrationModal';
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal'; import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions'; import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions';
import { createLeftPane } from './state/roots/createLeftPane';
import { createMessageDetail } from './state/roots/createMessageDetail'; import { createMessageDetail } from './state/roots/createMessageDetail';
import { createConversationNotificationsSettings } from './state/roots/createConversationNotificationsSettings'; import { createConversationNotificationsSettings } from './state/roots/createConversationNotificationsSettings';
import { createPendingInvites } from './state/roots/createPendingInvites'; import { createPendingInvites } from './state/roots/createPendingInvites';
@ -137,7 +136,6 @@ export declare class WebAudioRecorderClass {
} }
export type SignalCoreType = { export type SignalCoreType = {
Backbone: any;
Crypto: typeof Crypto; Crypto: typeof Crypto;
Curve: typeof Curve; Curve: typeof Curve;
Data: typeof Data; Data: typeof Data;
@ -187,7 +185,6 @@ export type SignalCoreType = {
createGroupV1MigrationModal: typeof createGroupV1MigrationModal; createGroupV1MigrationModal: typeof createGroupV1MigrationModal;
createGroupV2JoinModal: typeof createGroupV2JoinModal; createGroupV2JoinModal: typeof createGroupV2JoinModal;
createGroupV2Permissions: typeof createGroupV2Permissions; createGroupV2Permissions: typeof createGroupV2Permissions;
createLeftPane: typeof createLeftPane;
createMessageDetail: typeof createMessageDetail; createMessageDetail: typeof createMessageDetail;
createConversationNotificationsSettings: typeof createConversationNotificationsSettings; createConversationNotificationsSettings: typeof createConversationNotificationsSettings;
createPendingInvites: typeof createPendingInvites; createPendingInvites: typeof createPendingInvites;

View file

@ -8,6 +8,5 @@ import '../../models/conversations';
import '../../backbone/views/whisper_view'; import '../../backbone/views/whisper_view';
import '../../views/conversation_view'; import '../../views/conversation_view';
import '../../views/inbox_view';
import '../../SignalProtocolStore'; import '../../SignalProtocolStore';
import '../../background'; import '../../background';