diff --git a/stylesheets/components/ConversationHero.scss b/stylesheets/components/ConversationHero.scss index 9a038bc5f803..356ed1c8718e 100644 --- a/stylesheets/components/ConversationHero.scss +++ b/stylesheets/components/ConversationHero.scss @@ -97,6 +97,8 @@ &__linkNotification { @include font-body-2; + + margin-top: 15px; text-align: center; user-select: none; diff --git a/ts/background.ts b/ts/background.ts index 725bf726fb65..735ad0adc43a 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -142,8 +142,6 @@ import { deleteAllLogs } from './util/deleteAllLogs'; import { ReactWrapperView } from './views/ReactWrapperView'; import { ToastCaptchaFailed } from './components/ToastCaptchaFailed'; import { ToastCaptchaSolved } from './components/ToastCaptchaSolved'; -import { ToastConversationArchived } from './components/ToastConversationArchived'; -import { ToastConversationUnarchived } from './components/ToastConversationUnarchived'; import { showToast } from './util/showToast'; import { startInteractionMode } from './windows/startInteractionMode'; import type { MainWindowStatsType } from './windows/context'; @@ -1454,7 +1452,9 @@ export async function startApp(): Promise { // Send Escape to active conversation so it can close panels if (conversation && key === 'Escape') { - conversation.trigger('escape-pressed'); + window.reduxActions.conversations.popPanelForConversation( + conversation.id + ); event.preventDefault(); event.stopPropagation(); return; @@ -1530,7 +1530,9 @@ export async function startApp(): Promise { ) { window.reduxActions.conversations.pushPanelForConversation( conversation.id, - { type: PanelType.AllMedia } + { + type: PanelType.AllMedia, + } ); event.preventDefault(); event.stopPropagation(); @@ -1551,16 +1553,10 @@ export async function startApp(): Promise { shiftKey && (key === 'a' || key === 'A') ) { - conversation.setArchived(true); - conversation.trigger('unload', 'keyboard shortcut archive'); - showToast(ToastConversationArchived, { - undo: () => { - conversation.setArchived(false); - window.reduxActions.conversations.showConversation({ - conversationId: conversation.get('id'), - }); - }, - }); + event.preventDefault(); + event.stopPropagation(); + + window.reduxActions.conversations.onArchive(conversation.id); // It's very likely that the act of archiving a conversation will set focus to // 'none,' or the top-level body element. This resets it to the left pane. @@ -1573,8 +1569,6 @@ export async function startApp(): Promise { } } - event.preventDefault(); - event.stopPropagation(); return; } if ( @@ -1584,11 +1578,11 @@ export async function startApp(): Promise { shiftKey && (key === 'u' || key === 'U') ) { - conversation.setArchived(false); - showToast(ToastConversationUnarchived); - event.preventDefault(); event.stopPropagation(); + + window.reduxActions.conversations.onMoveToInbox(conversation.id); + return; } @@ -1603,13 +1597,15 @@ export async function startApp(): Promise { shiftKey && (key === 'c' || key === 'C') ) { + event.preventDefault(); + event.stopPropagation(); + conversation.trigger('unload', 'keyboard shortcut close'); window.reduxActions.conversations.showConversation({ conversationId: undefined, messageId: undefined, }); - event.preventDefault(); - event.stopPropagation(); + return; } @@ -1622,14 +1618,22 @@ export async function startApp(): Promise { !shiftKey && (key === 'd' || key === 'D') ) { + event.preventDefault(); + event.stopPropagation(); + const { selectedMessage } = state.conversations; if (!selectedMessage) { return; } - conversation.trigger('show-message-details', selectedMessage); - event.preventDefault(); - event.stopPropagation(); + window.reduxActions.conversations.pushPanelForConversation( + conversation.id, + { + type: PanelType.MessageDetails, + args: { messageId: selectedMessage }, + } + ); + return; } @@ -1640,6 +1644,9 @@ export async function startApp(): Promise { shiftKey && (key === 'r' || key === 'R') ) { + event.preventDefault(); + event.stopPropagation(); + const { selectedMessage } = state.conversations; const composerState = window.reduxStore @@ -1652,8 +1659,6 @@ export async function startApp(): Promise { quote ? undefined : selectedMessage ); - event.preventDefault(); - event.stopPropagation(); return; } @@ -1664,12 +1669,12 @@ export async function startApp(): Promise { !shiftKey && (key === 's' || key === 'S') ) { + event.preventDefault(); + event.stopPropagation(); + const { selectedMessage } = state.conversations; if (selectedMessage) { - event.preventDefault(); - event.stopPropagation(); - window.reduxActions.conversations.saveAttachmentFromMessage( selectedMessage ); diff --git a/ts/components/App.tsx b/ts/components/App.tsx index f0ab191931b0..6a52d4b5e353 100644 --- a/ts/components/App.tsx +++ b/ts/components/App.tsx @@ -41,6 +41,7 @@ type PropsType = { isMaximized: boolean; isFullScreen: boolean; menuOptions: MenuOptionsType; + onUndoArchive: (conversationId: string) => unknown; openFileInFolder: (target: string) => unknown; hasCustomTitleBar: boolean; hideMenuBar: boolean; @@ -73,6 +74,7 @@ export function App({ isShowingStoriesView, hasCustomTitleBar, menuOptions, + onUndoArchive, openInbox, openFileInFolder, registerSingleDevice, @@ -183,6 +185,7 @@ export function App({ diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index 94b13d5b5241..770e644f535f 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -68,7 +68,7 @@ const useProps = (overrideProps: Partial = {}): Props => ({ // MediaEditor imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', // MediaQualitySelector - onSelectMediaQuality: action('onSelectMediaQuality'), + setMediaQualitySetting: action('setMediaQualitySetting'), shouldSendHighQualityAttachments: Boolean( overrideProps.shouldSendHighQualityAttachments ), @@ -116,8 +116,12 @@ const useProps = (overrideProps: Partial = {}): Props => ({ Boolean(overrideProps.announcementsOnly) ), areWeAdmin: boolean('areWeAdmin', Boolean(overrideProps.areWeAdmin)), + areWePendingApproval: boolean( + 'areWePendingApproval', + Boolean(overrideProps.areWePendingApproval) + ), groupAdmins: [], - onCancelJoinRequest: action('onCancelJoinRequest'), + cancelJoinRequest: action('cancelJoinRequest'), showConversation: action('showConversation'), // SMS-only isSMSOnly: overrideProps.isSMSOnly || false, @@ -193,6 +197,20 @@ export function Attachments(): JSX.Element { return ; } +export function PendingApproval(): JSX.Element { + return ( + + ); +} + +AnnouncementsOnlyGroup.story = { + name: 'Announcements Only group', +}; + export function AnnouncementsOnlyGroup(): JSX.Element { return ( ; }) => unknown; - onSelectMediaQuality(isHQ: boolean): unknown; + setMediaQualitySetting(isHQ: boolean): unknown; sendStickerMessage( id: string, opts: { packId: string; stickerId: number } @@ -170,7 +170,7 @@ export type Props = Pick< > & MessageRequestActionsProps & Pick & - Pick & { + Pick & { pushPanelForConversation: PushPanelForConversationActionType; } & OwnProps; @@ -211,7 +211,7 @@ export function CompositionArea({ quotedMessageProps, scrollToMessage, // MediaQualitySelector - onSelectMediaQuality, + setMediaQualitySetting, shouldSendHighQualityAttachments, // CompositionInput onEditorStateChange, @@ -261,7 +261,7 @@ export function CompositionArea({ announcementsOnly, areWeAdmin, groupAdmins, - onCancelJoinRequest, + cancelJoinRequest, showConversation, // SMS-only contacts isSMSOnly, @@ -393,7 +393,7 @@ export function CompositionArea({ ) : null} @@ -592,8 +592,9 @@ export function CompositionArea({ if (areWePendingApproval) { return ( ); } @@ -683,7 +684,7 @@ export function CompositionArea({ i18n={i18n} onAddAttachment={launchAttachmentPicker} onClickAttachment={maybeEditAttachment} - onClose={onClearAttachments} + onClose={() => onClearAttachments(conversationId)} onCloseAttachment={attachment => { if (attachment.path) { removeAttachment(conversationId, attachment.path); diff --git a/ts/components/StoryViewsNRepliesModal.tsx b/ts/components/StoryViewsNRepliesModal.tsx index bc6f2013b59f..a9402cbb6351 100644 --- a/ts/components/StoryViewsNRepliesModal.tsx +++ b/ts/components/StoryViewsNRepliesModal.tsx @@ -68,7 +68,6 @@ const MESSAGE_DEFAULT_PROPS = { showExpiredOutgoingTapToViewToast: shouldNeverBeCalled, showLightbox: shouldNeverBeCalled, showLightboxForViewOnceMedia: shouldNeverBeCalled, - showMessageDetail: shouldNeverBeCalled, startConversation: shouldNeverBeCalled, theme: ThemeType.dark, viewStory: shouldNeverBeCalled, diff --git a/ts/components/ToastConversationArchived.stories.tsx b/ts/components/ToastConversationArchived.stories.tsx deleted file mode 100644 index 20d31bfa47e2..000000000000 --- a/ts/components/ToastConversationArchived.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastConversationArchived } from './ToastConversationArchived'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), - undo: action('undo'), -}; - -export default { - title: 'Components/ToastConversationArchived', -}; - -export const _ToastConversationArchived = (): JSX.Element => ( - -); - -_ToastConversationArchived.story = { - name: 'ToastConversationArchived', -}; diff --git a/ts/components/ToastConversationArchived.tsx b/ts/components/ToastConversationArchived.tsx deleted file mode 100644 index de5a5cc4f556..000000000000 --- a/ts/components/ToastConversationArchived.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type ToastPropsType = { - undo: () => unknown; -}; - -export type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -} & ToastPropsType; - -export function ToastConversationArchived({ - i18n, - onClose, - undo, -}: PropsType): JSX.Element { - return ( - { - undo(); - onClose(); - }, - }} - onClose={onClose} - > - {i18n('conversationArchived')} - - ); -} diff --git a/ts/components/ToastConversationMarkedUnread.stories.tsx b/ts/components/ToastConversationMarkedUnread.stories.tsx deleted file mode 100644 index 4f134c2de9a0..000000000000 --- a/ts/components/ToastConversationMarkedUnread.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastConversationMarkedUnread } from './ToastConversationMarkedUnread'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastConversationMarkedUnread', -}; - -export const _ToastConversationMarkedUnread = (): JSX.Element => ( - -); - -_ToastConversationMarkedUnread.story = { - name: 'ToastConversationMarkedUnread', -}; diff --git a/ts/components/ToastConversationMarkedUnread.tsx b/ts/components/ToastConversationMarkedUnread.tsx deleted file mode 100644 index 797f6db97478..000000000000 --- a/ts/components/ToastConversationMarkedUnread.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastConversationMarkedUnread({ - i18n, - onClose, -}: PropsType): JSX.Element { - return {i18n('conversationMarkedUnread')}; -} diff --git a/ts/components/ToastConversationUnarchived.stories.tsx b/ts/components/ToastConversationUnarchived.stories.tsx deleted file mode 100644 index 25d0734e3883..000000000000 --- a/ts/components/ToastConversationUnarchived.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastConversationUnarchived } from './ToastConversationUnarchived'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastConversationUnarchived', -}; - -export const _ToastConversationUnarchived = (): JSX.Element => ( - -); - -_ToastConversationUnarchived.story = { - name: 'ToastConversationUnarchived', -}; diff --git a/ts/components/ToastConversationUnarchived.tsx b/ts/components/ToastConversationUnarchived.tsx deleted file mode 100644 index f4a47dc75c27..000000000000 --- a/ts/components/ToastConversationUnarchived.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastConversationUnarchived({ - i18n, - onClose, -}: PropsType): JSX.Element { - return {i18n('conversationReturnedToInbox')}; -} diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 869d97c1bf43..0a5bdc4b9362 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -17,6 +17,8 @@ export default { component: ToastManager, argTypes: { hideToast: { action: true }, + openFileInFolder: { action: true }, + onUndoArchive: { action: true }, i18n: { defaultValue: i18n, }, @@ -91,6 +93,30 @@ CannotStartGroupCall.args = { }, }; +export const ConversationArchived = Template.bind({}); +ConversationArchived.args = { + toast: { + toastType: ToastType.ConversationArchived, + parameters: { + conversationId: 'some-conversation-id', + }, + }, +}; + +export const ConversationMarkedUnread = Template.bind({}); +ConversationMarkedUnread.args = { + toast: { + toastType: ToastType.ConversationMarkedUnread, + }, +}; + +export const ConversationUnarchived = Template.bind({}); +ConversationUnarchived.args = { + toast: { + toastType: ToastType.ConversationUnarchived, + }, +}; + export const CopiedUsername = Template.bind({}); CopiedUsername.args = { toast: { @@ -182,6 +208,13 @@ MaxAttachments.args = { }, }; +export const OriginalMessageNotFound = Template.bind({}); +OriginalMessageNotFound.args = { + toast: { + toastType: ToastType.OriginalMessageNotFound, + }, +}; + export const MessageBodyTooLong = Template.bind({}); MessageBodyTooLong.args = { toast: { diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index 6797eec5d46c..d810a77baec3 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -5,7 +5,6 @@ import React from 'react'; import type { LocalizerType, ReplacementValuesType } from '../types/Util'; import { SECOND } from '../util/durations'; import { Toast } from './Toast'; -import { ToastMessageBodyTooLong } from './ToastMessageBodyTooLong'; import { missingCaseError } from '../util/missingCaseError'; import { ToastType } from '../types/Toast'; @@ -13,6 +12,7 @@ export type PropsType = { hideToast: () => unknown; i18n: LocalizerType; openFileInFolder: (target: string) => unknown; + onUndoArchive: (conversaetionId: string) => unknown; toast?: { toastType: ToastType; parameters?: ReplacementValuesType; @@ -25,6 +25,7 @@ export function ToastManager({ hideToast, i18n, openFileInFolder, + onUndoArchive, toast, }: PropsType): JSX.Element | null { if (toast === undefined) { @@ -84,6 +85,36 @@ export function ToastManager({ ); } + if (toastType === ToastType.ConversationArchived) { + return ( + { + if (toast.parameters && 'conversationId' in toast.parameters) { + onUndoArchive(String(toast.parameters.conversationId)); + } + }, + }} + > + {i18n('conversationArchived')} + + ); + } + + if (toastType === ToastType.ConversationMarkedUnread) { + return ( + {i18n('conversationMarkedUnread')} + ); + } + + if (toastType === ToastType.ConversationUnarchived) { + return ( + {i18n('conversationReturnedToInbox')} + ); + } + if (toastType === ToastType.CopiedUsername) { return ( @@ -174,15 +205,11 @@ export function ToastManager({ } if (toastType === ToastType.MessageBodyTooLong) { - return ; + return {i18n('messageBodyTooLong')}; } - if (toastType === ToastType.ReportedSpamAndBlocked) { - return ( - - {i18n('MessageRequests--block-and-report-spam-success-toast')} - - ); + if (toastType === ToastType.OriginalMessageNotFound) { + return {i18n('originalMessageNotFound')}; } if (toastType === ToastType.PinnedConversationsFull) { @@ -193,6 +220,14 @@ export function ToastManager({ return {i18n('Reactions--error')}; } + if (toastType === ToastType.ReportedSpamAndBlocked) { + return ( + + {i18n('MessageRequests--block-and-report-spam-success-toast')} + + ); + } + if (toastType === ToastType.StoryMuted) { return ( diff --git a/ts/components/ToastMessageBodyTooLong.stories.tsx b/ts/components/ToastMessageBodyTooLong.stories.tsx deleted file mode 100644 index e4a23e334f86..000000000000 --- a/ts/components/ToastMessageBodyTooLong.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastMessageBodyTooLong } from './ToastMessageBodyTooLong'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastMessageBodyTooLong', -}; - -export const _ToastMessageBodyTooLong = (): JSX.Element => ( - -); - -_ToastMessageBodyTooLong.story = { - name: 'ToastMessageBodyTooLong', -}; diff --git a/ts/components/ToastMessageBodyTooLong.tsx b/ts/components/ToastMessageBodyTooLong.tsx deleted file mode 100644 index 68b75f5b7ae7..000000000000 --- a/ts/components/ToastMessageBodyTooLong.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastMessageBodyTooLong({ - i18n, - onClose, -}: PropsType): JSX.Element { - return {i18n('messageBodyTooLong')}; -} diff --git a/ts/components/ToastOriginalMessageNotFound.stories.tsx b/ts/components/ToastOriginalMessageNotFound.stories.tsx deleted file mode 100644 index a56a21f2bc88..000000000000 --- a/ts/components/ToastOriginalMessageNotFound.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastOriginalMessageNotFound } from './ToastOriginalMessageNotFound'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastOriginalMessageNotFound', -}; - -export const _ToastOriginalMessageNotFound = (): JSX.Element => ( - -); - -_ToastOriginalMessageNotFound.story = { - name: 'ToastOriginalMessageNotFound', -}; diff --git a/ts/components/ToastOriginalMessageNotFound.tsx b/ts/components/ToastOriginalMessageNotFound.tsx deleted file mode 100644 index d300a05ff7cb..000000000000 --- a/ts/components/ToastOriginalMessageNotFound.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastOriginalMessageNotFound({ - i18n, - onClose, -}: PropsType): JSX.Element { - return {i18n('originalMessageNotFound')}; -} diff --git a/ts/components/conversation/ChatSessionRefreshedNotification.stories.tsx b/ts/components/conversation/ChatSessionRefreshedNotification.stories.tsx index 545a1c37bf48..ebe5884bac53 100644 --- a/ts/components/conversation/ChatSessionRefreshedNotification.stories.tsx +++ b/ts/components/conversation/ChatSessionRefreshedNotification.stories.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; -import { action } from '@storybook/addon-actions'; import { setupI18n } from '../../util/setupI18n'; import enMessages from '../../../_locales/en/messages.json'; @@ -15,10 +14,5 @@ export default { }; export function Default(): JSX.Element { - return ( - - ); + return ; } diff --git a/ts/components/conversation/ChatSessionRefreshedNotification.tsx b/ts/components/conversation/ChatSessionRefreshedNotification.tsx index 2331d74c6935..361020863d59 100644 --- a/ts/components/conversation/ChatSessionRefreshedNotification.tsx +++ b/ts/components/conversation/ChatSessionRefreshedNotification.tsx @@ -9,21 +9,19 @@ import type { LocalizerType } from '../../types/Util'; import { Button, ButtonSize, ButtonVariant } from '../Button'; import { SystemMessage } from './SystemMessage'; import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog'; +import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser'; +import { mapToSupportLocale } from '../../util/mapToSupportLocale'; type PropsHousekeepingType = { i18n: LocalizerType; }; -export type PropsActionsType = { - contactSupport: () => unknown; -}; - -export type PropsType = PropsHousekeepingType & PropsActionsType; +export type PropsType = PropsHousekeepingType; export function ChatSessionRefreshedNotification( props: PropsType ): ReactElement { - const { contactSupport, i18n } = props; + const { i18n } = props; const [isDialogOpen, setIsDialogOpen] = useState(false); const openDialog = useCallback(() => { @@ -35,8 +33,15 @@ export function ChatSessionRefreshedNotification( const wrappedContactSupport = useCallback(() => { setIsDialogOpen(false); - contactSupport(); - }, [contactSupport, setIsDialogOpen]); + + const baseUrl = + 'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed'; + const locale = window.getLocale(); + const supportLocale = mapToSupportLocale(locale); + const url = baseUrl.replace('LOCALE', supportLocale); + + openLinkInWebBrowser(url); + }, [setIsDialogOpen]); return ( <> diff --git a/ts/components/conversation/ContactSpoofingReviewDialog.stories.tsx b/ts/components/conversation/ContactSpoofingReviewDialog.stories.tsx index 397ae7624d40..f9cc4e7fcb19 100644 --- a/ts/components/conversation/ContactSpoofingReviewDialog.stories.tsx +++ b/ts/components/conversation/ContactSpoofingReviewDialog.stories.tsx @@ -22,12 +22,13 @@ const getCommonProps = () => ({ acceptConversation: action('acceptConversation'), blockAndReportSpam: action('blockAndReportSpam'), blockConversation: action('blockConversation'), + conversationId: 'some-conversation-id', deleteConversation: action('deleteConversation'), getPreferredBadge: () => undefined, groupConversationId: 'convo-id', i18n, onClose: action('onClose'), - onShowContactModal: action('onShowContactModal'), + showContactModal: action('showContactModal'), removeMember: action('removeMember'), theme: ThemeType.light, }); diff --git a/ts/components/conversation/ContactSpoofingReviewDialog.tsx b/ts/components/conversation/ContactSpoofingReviewDialog.tsx index 20f7dac99c4c..c78bbf1dd71f 100644 --- a/ts/components/conversation/ContactSpoofingReviewDialog.tsx +++ b/ts/components/conversation/ContactSpoofingReviewDialog.tsx @@ -25,6 +25,7 @@ import { missingCaseError } from '../../util/missingCaseError'; import { isInSystemContacts } from '../../util/isInSystemContacts'; export type PropsType = { + conversationId: string; acceptConversation: (conversationId: string) => unknown; blockAndReportSpam: (conversationId: string) => unknown; blockConversation: (conversationId: string) => unknown; @@ -32,8 +33,11 @@ export type PropsType = { getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; onClose: () => void; - onShowContactModal: (contactId: string, conversationId?: string) => unknown; - removeMember: (conversationId: string) => unknown; + showContactModal: (contactId: string, conversationId?: string) => unknown; + removeMember: ( + conversationId: string, + memberConversationId: string + ) => unknown; theme: ThemeType; } & ( | { @@ -65,11 +69,12 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element { acceptConversation, blockAndReportSpam, blockConversation, + conversationId, deleteConversation, getPreferredBadge, i18n, onClose, - onShowContactModal, + showContactModal, removeMember, theme, } = props; @@ -150,7 +155,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element { setConfirmationState(undefined); }} onRemove={() => { - removeMember(affectedConversation.id); + removeMember(conversationId, affectedConversation.id); }} /> ); @@ -218,7 +223,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element { getPreferredBadge={getPreferredBadge} i18n={i18n} onClick={() => { - onShowContactModal(safeConversation.id); + showContactModal(safeConversation.id); }} theme={theme} /> diff --git a/ts/components/conversation/ConversationHeader.stories.tsx b/ts/components/conversation/ConversationHeader.stories.tsx index 2a123e4ab183..a3cef53eca65 100644 --- a/ts/components/conversation/ConversationHeader.stories.tsx +++ b/ts/components/conversation/ConversationHeader.stories.tsx @@ -39,7 +39,6 @@ const commonProps = { setDisappearingMessages: action('setDisappearingMessages'), destroyMessages: action('destroyMessages'), - onSearchInConversation: action('onSearchInConversation'), onOutgoingAudioCallInConversation: action( 'onOutgoingAudioCallInConversation' ), @@ -47,12 +46,12 @@ const commonProps = { 'onOutgoingVideoCallInConversation' ), - onGoBack: action('onGoBack'), - onArchive: action('onArchive'), onMarkUnread: action('onMarkUnread'), onMoveToInbox: action('onMoveToInbox'), pushPanelForConversation: action('pushPanelForConversation'), + popPanelForConversation: action('popPanelForConversation'), + searchInConversation: action('searchInConversation'), setMuteExpiration: action('onSetMuteNotifications'), setPinned: action('setPinned'), viewUserStories: action('viewUserStories'), diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 9e09d0c1c523..3b19fac5cb4f 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -20,6 +20,7 @@ import { InContactsIcon } from '../InContactsIcon'; import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ConversationType, + PopPanelForConversationActionType, PushPanelForConversationActionType, } from '../../state/ducks/conversations'; import type { BadgeType } from '../../badges/types'; @@ -85,14 +86,14 @@ export type PropsDataType = { export type PropsActionsType = { destroyMessages: (conversationId: string) => void; - onArchive: () => void; - onGoBack: () => void; - onMarkUnread: () => void; - onMoveToInbox: () => void; + onArchive: (conversationId: string) => void; + onMarkUnread: (conversationId: string) => void; + onMoveToInbox: (conversationId: string) => void; onOutgoingAudioCallInConversation: (conversationId: string) => void; onOutgoingVideoCallInConversation: (conversationId: string) => void; - onSearchInConversation: () => void; pushPanelForConversation: PushPanelForConversationActionType; + popPanelForConversation: PopPanelForConversationActionType; + searchInConversation: (conversationId: string) => void; setDisappearingMessages: ( conversationId: string, seconds: DurationInSeconds @@ -153,12 +154,12 @@ export class ConversationHeader extends React.Component { } private renderBackButton(): ReactNode { - const { i18n, onGoBack, showBackButton } = this.props; + const { i18n, id, popPanelForConversation, showBackButton } = this.props; return ( + {isConfirming ? ( + cancelJoinRequest(conversationId), + }, + ]} + cancelText={i18n('GroupV2--join--cancel-request-to-join--no')} + dialogName="GroupV2CancelRequestToJoin" + i18n={i18n} + onClose={() => setIsConfirming(false)} + > + {i18n('GroupV2--join--cancel-request-to-join--confirmation')} + + ) : undefined} ); } diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 8d5e4544139b..4d47fd62060a 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -167,7 +167,6 @@ export type AudioAttachmentProps = { id: string; conversationId: string; played: boolean; - showMessageDetail: (id: string) => void; status?: MessageStatusType; textPending?: boolean; timestamp: number; @@ -302,8 +301,6 @@ export type PropsActions = { messageExpanded: (id: string, displayLimit: number) => unknown; checkForAccount: (phoneNumber: string) => unknown; - showMessageDetail: (id: string) => void; - startConversation: (e164: string, uuid: UUIDStringType) => void; showConversation: ShowConversationType; openGiftBadge: (messageId: string) => void; @@ -327,6 +324,7 @@ export type PropsActions = { scrollToQuotedMessage: (options: { authorId: string; + conversationId: string; sentAt: number; }) => void; selectMessage?: (messageId: string, conversationId: string) => unknown; @@ -752,6 +750,7 @@ export class Message extends React.PureComponent { } const { + conversationId, deletedForEveryone, direction, expirationLength, @@ -760,17 +759,18 @@ export class Message extends React.PureComponent { isTapToViewExpired, status, i18n, + pushPanelForConversation, text, textAttachment, timestamp, id, - showMessageDetail, } = this.props; const isStickerLike = isSticker || this.canRenderStickerLikeEmoji(); return ( { isSticker={isStickerLike} isTapToViewExpired={isTapToViewExpired} onWidthMeasured={isInline ? this.updateMetadataWidth : undefined} - showMessageDetail={showMessageDetail} + pushPanelForConversation={pushPanelForConversation} status={status} textPending={textAttachment?.pending} timestamp={timestamp} @@ -841,7 +841,6 @@ export class Message extends React.PureComponent { reducedMotion, renderAudioAttachment, renderingContext, - showMessageDetail, showLightbox, shouldCollapseAbove, shouldCollapseBelow, @@ -967,7 +966,6 @@ export class Message extends React.PureComponent { id, conversationId, played, - showMessageDetail, status, textPending: textAttachment?.pending, timestamp, @@ -1453,6 +1451,7 @@ export class Message extends React.PureComponent { public renderQuote(): JSX.Element | null { const { conversationColor, + conversationId, conversationTitle, customColor, direction, @@ -1475,6 +1474,7 @@ export class Message extends React.PureComponent { : () => { scrollToQuotedMessage({ authorId: quote.authorId, + conversationId, sentAt: quote.sentAt, }); }; diff --git a/ts/components/conversation/MessageAudio.tsx b/ts/components/conversation/MessageAudio.tsx index 45e4a1b30c2b..617491467f67 100644 --- a/ts/components/conversation/MessageAudio.tsx +++ b/ts/components/conversation/MessageAudio.tsx @@ -16,6 +16,7 @@ import type { ComputePeaksResult } from '../GlobalAudioContext'; import { MessageMetadata } from './MessageMetadata'; import * as log from '../../logging/log'; import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer'; +import type { PushPanelForConversationActionType } from '../../state/ducks/conversations'; export type OwnProps = Readonly<{ active: ActiveAudioPlayerStateType | undefined; @@ -34,7 +35,6 @@ export type OwnProps = Readonly<{ id: string; conversationId: string; played: boolean; - showMessageDetail: (id: string) => void; status?: MessageStatusType; textPending?: boolean; timestamp: number; @@ -51,6 +51,7 @@ export type DispatchProps = Readonly<{ position: number, isConsecutive: boolean ) => void; + pushPanelForConversation: PushPanelForConversationActionType; setCurrentTime: (currentTime: number) => void; setPlaybackRate: (conversationId: string, rate: number) => void; setIsPlaying: (value: boolean) => void; @@ -263,7 +264,6 @@ export function MessageAudio(props: Props): JSX.Element { expirationTimestamp, id, played, - showMessageDetail, status, textPending, timestamp, @@ -273,6 +273,7 @@ export function MessageAudio(props: Props): JSX.Element { computePeaks, setPlaybackRate, loadAndPlayMessageAudio, + pushPanelForConversation, setCurrentTime, setIsPlaying, } = props; @@ -591,6 +592,7 @@ export function MessageAudio(props: Props): JSX.Element { {!withContentBelow && !collapseMetadata && ( ; +} & Pick; -export type PropsBackboneActions = Pick< - MessagePropsType, - 'renderAudioAttachment' | 'startConversation' ->; +export type PropsSmartActions = Pick; export type PropsReduxActions = Pick< MessagePropsType, @@ -97,13 +88,13 @@ export type PropsReduxActions = Pick< | 'showExpiredOutgoingTapToViewToast' | 'showLightbox' | 'showLightboxForViewOnceMedia' + | 'startConversation' | 'viewStory' > & { toggleSafetyNumberModal: (contactId: string) => void; }; -export type ExternalProps = PropsData & PropsBackboneActions; -export type Props = PropsData & PropsBackboneActions & PropsReduxActions; +export type Props = PropsData & PropsSmartActions & PropsReduxActions; const contactSortCollator = new Intl.Collator(); @@ -280,7 +271,6 @@ export class MessageDetail extends React.Component { contactNameColor, showLightboxForViewOnceMedia, doubleCheckMissingQuoteReference, - expirationTimestamp, getPreferredBadge, i18n, interactionMode, @@ -300,8 +290,8 @@ export class MessageDetail extends React.Component { viewStory, } = this.props; - const timeRemaining = expirationTimestamp - ? DurationInSeconds.fromMillis(expirationTimestamp - Date.now()) + const timeRemaining = message.expirationTimestamp + ? DurationInSeconds.fromMillis(message.expirationTimestamp - Date.now()) : undefined; return ( @@ -348,9 +338,6 @@ export class MessageDetail extends React.Component { showExpiredOutgoingTapToViewToast={ showExpiredOutgoingTapToViewToast } - showMessageDetail={() => { - log.warn('MessageDetail: showMessageDetail called!'); - }} showLightbox={showLightbox} startConversation={startConversation} theme={theme} diff --git a/ts/components/conversation/MessageMetadata.tsx b/ts/components/conversation/MessageMetadata.tsx index 8aa5e2a6393b..b8676d7ac6dd 100644 --- a/ts/components/conversation/MessageMetadata.tsx +++ b/ts/components/conversation/MessageMetadata.tsx @@ -11,8 +11,11 @@ import type { DirectionType, MessageStatusType } from './Message'; import { ExpireTimer } from './ExpireTimer'; import { MessageTimestamp } from './MessageTimestamp'; import { Spinner } from '../Spinner'; +import type { PushPanelForConversationActionType } from '../../state/ducks/conversations'; +import { PanelType } from '../../types/Panels'; type PropsType = { + conversationId: string; deletedForEveryone?: boolean; direction: DirectionType; expirationLength?: number; @@ -25,13 +28,14 @@ type PropsType = { isSticker?: boolean; isTapToViewExpired?: boolean; onWidthMeasured?: (width: number) => unknown; - showMessageDetail: (id: string) => void; + pushPanelForConversation: PushPanelForConversationActionType; status?: MessageStatusType; textPending?: boolean; timestamp: number; }; export function MessageMetadata({ + conversationId, deletedForEveryone, direction, expirationLength, @@ -44,7 +48,7 @@ export function MessageMetadata({ isSticker, isTapToViewExpired, onWidthMeasured, - showMessageDetail, + pushPanelForConversation, status, textPending, timestamp, @@ -76,7 +80,10 @@ export function MessageMetadata({ event.stopPropagation(); event.preventDefault(); - showMessageDetail(id); + pushPanelForConversation(conversationId, { + type: PanelType.MessageDetails, + args: { messageId: id }, + }); }} > {deletedForEveryone diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index 7ecb8f59d7dd..e24bd2d51bc5 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -137,7 +137,6 @@ const defaultMessageProps: TimelineMessagesProps = { 'showExpiredOutgoingTapToViewToast' ), toggleForwardMessageModal: action('default--toggleForwardMessageModal'), - showMessageDetail: action('default--showMessageDetail'), showLightbox: action('default--showLightbox'), startConversation: action('default--startConversation'), status: 'sent', diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 2dc9f3d51318..e9d1b71e9176 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -266,7 +266,6 @@ const actions = () => ({ 'clearInvitedUuidsForNewlyCreatedGroup' ), setIsNearBottom: action('setIsNearBottom'), - learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'), loadOlderMessages: action('loadOlderMessages'), loadNewerMessages: action('loadNewerMessages'), loadNewestMessages: action('loadNewestMessages'), @@ -281,7 +280,6 @@ const actions = () => ({ retryMessageSend: action('retryMessageSend'), deleteMessage: action('deleteMessage'), deleteMessageForEveryone: action('deleteMessageForEveryone'), - showMessageDetail: action('showMessageDetail'), saveAttachment: action('saveAttachment'), pushPanelForConversation: action('pushPanelForConversation'), showContactDetail: action('showContactDetail'), @@ -310,20 +308,12 @@ const actions = () => ({ startConversation: action('startConversation'), returnToActiveCall: action('returnToActiveCall'), - contactSupport: action('contactSupport'), - closeContactSpoofingReview: action('closeContactSpoofingReview'), reviewGroupMemberNameCollision: action('reviewGroupMemberNameCollision'), reviewMessageRequestNameCollision: action( 'reviewMessageRequestNameCollision' ), - acceptConversation: action('acceptConversation'), - blockAndReportSpam: action('blockAndReportSpam'), - blockConversation: action('blockConversation'), - deleteConversation: action('deleteConversation'), - removeMember: action('removeMember'), - unblurAvatar: action('unblurAvatar'), peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'), @@ -371,10 +361,23 @@ const renderItem = ({ const renderContactSpoofingReviewDialog = ( props: SmartContactSpoofingReviewDialogPropsType ) => { + const sharedProps = { + acceptConversation: action('acceptConversation'), + blockAndReportSpam: action('blockAndReportSpam'), + blockConversation: action('blockConversation'), + deleteConversation: action('deleteConversation'), + getPreferredBadge: () => undefined, + i18n, + removeMember: action('removeMember'), + showContactModal: action('showContactModal'), + theme: ThemeType.dark, + }; + if (props.type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) { return ( ; + return ; }; const getAbout = () => text('about', '👍 Free to chat'); diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 0555ed243cda..658a284070ca 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -1,16 +1,15 @@ // Copyright 2019-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { first, get, isNumber, last, pick, throttle } from 'lodash'; +import { first, get, isNumber, last, throttle } from 'lodash'; import classNames from 'classnames'; import type { ReactChild, ReactNode, RefObject } from 'react'; import React from 'react'; -import { createSelector } from 'reselect'; import Measure from 'react-measure'; import { ScrollDownButton } from './ScrollDownButton'; -import type { AssertProps, LocalizerType, ThemeType } from '../../types/Util'; +import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ConversationType } from '../../state/ducks/conversations'; import type { PreferredBadgeSelectorType } from '../../state/selectors/badges'; import { assertDev, strictAssert } from '../../util/assert'; @@ -18,8 +17,6 @@ import { missingCaseError } from '../../util/missingCaseError'; import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary'; import { WidthBreakpoint } from '../_util'; -import type { PropsActions as MessageActionsType } from './TimelineMessage'; -import type { PropsActionsType as ChatSessionRefreshedNotificationActionsType } from './ChatSessionRefreshedNotification'; import { ErrorBoundary } from './ErrorBoundary'; import { Intl } from '../Intl'; import { TimelineWarning } from './TimelineWarning'; @@ -44,8 +41,6 @@ import { } from '../../util/scrollUtil'; import { LastSeenIndicator } from './LastSeenIndicator'; import { MINUTE } from '../../util/durations'; -import type { PropsActionsType as DeliveryIssueNotificationActionsType } from './DeliveryIssueNotification'; -import type { PropsActionsType as GroupV2ChangeActionsType } from './GroupV2Change'; const AT_BOTTOM_THRESHOLD = 15; const AT_BOTTOM_DETECTOR_STYLE = { height: AT_BOTTOM_THRESHOLD }; @@ -124,7 +119,6 @@ type PropsHousekeepingType = { theme: ThemeType; renderItem: (props: { - actionProps: PropsActionsFromBackboneForChildrenType; containerElementRef: RefObject; containerWidthBreakpoint: WidthBreakpoint; conversationId: string; @@ -134,46 +128,31 @@ type PropsHousekeepingType = { previousMessageId: undefined | string; unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement; }) => JSX.Element; - renderHeroRow: ( - id: string, - unblurAvatar: () => void, - updateSharedGroups: () => unknown - ) => JSX.Element; + renderHeroRow: (id: string) => JSX.Element; renderTypingBubble: (id: string) => JSX.Element; renderContactSpoofingReviewDialog: ( props: SmartContactSpoofingReviewDialogPropsType ) => JSX.Element; }; -export type PropsActionsFromBackboneForChildrenType = Pick< - MessageActionsType, - 'scrollToQuotedMessage' | 'showMessageDetail' | 'startConversation' -> & - ChatSessionRefreshedNotificationActionsType & - DeliveryIssueNotificationActionsType & - GroupV2ChangeActionsType; - export type PropsActionsType = { // From Backbone acknowledgeGroupMemberNameCollisions: ( + conversationId: string, groupNameCollisions: Readonly ) => void; - loadOlderMessages: (messageId: string) => unknown; - loadNewerMessages: (messageId: string) => unknown; - loadNewestMessages: (messageId: string, setFocus?: boolean) => unknown; - markMessageRead: (messageId: string) => unknown; - removeMember: (conversationId: string) => unknown; - unblurAvatar: () => void; - updateSharedGroups: () => unknown; - - // From Redux - acceptConversation: (conversationId: string) => unknown; - blockConversation: (conversationId: string) => unknown; - blockAndReportSpam: (conversationId: string) => unknown; clearInvitedUuidsForNewlyCreatedGroup: () => void; clearSelectedMessage: () => unknown; closeContactSpoofingReview: () => void; - deleteConversation: (conversationId: string) => unknown; + loadOlderMessages: (conversationId: string, messageId: string) => unknown; + loadNewerMessages: (conversationId: string, messageId: string) => unknown; + loadNewestMessages: ( + conversationId: string, + messageId: string, + setFocus?: boolean + ) => unknown; + markMessageRead: (conversationId: string, messageId: string) => unknown; + selectMessage: (messageId: string, conversationId: string) => unknown; setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown; peekGroupCallForTheFirstTime: (conversationId: string) => unknown; peekGroupCallIfItHasMembers: (conversationId: string) => unknown; @@ -183,9 +162,7 @@ export type PropsActionsType = { safeConversationId: string; }> ) => void; - selectMessage: (messageId: string, conversationId: string) => unknown; - showContactModal: (contactId: string, conversationId?: string) => void; -} & PropsActionsFromBackboneForChildrenType; +}; export type PropsType = PropsDataType & PropsHousekeepingType & @@ -209,39 +186,6 @@ type SnapshotType = | { scrollTop: number } | { scrollBottom: number }; -const getActions = createSelector( - // It is expensive to pick so many properties out of the `props` object so we - // use `createSelector` to memoize them by the last seen `props` object. - (props: PropsType) => props, - - (props: PropsType): PropsActionsFromBackboneForChildrenType => { - // Note: Because TimelineItem is smart, we only need to include action creators here - // which are passed in from backbone and not available via mapDispatchToProps - const unsafe = pick(props, [ - // MessageActionsType - 'scrollToQuotedMessage', - 'showMessageDetail', - 'startConversation', - - // ChatSessionRefreshedNotificationActionsType - 'contactSupport', - - // DeliveryIssueNotificationActionsType - 'learnMoreAboutDeliveryIssue', - - // GroupV2ChangeActionsType - 'blockGroupLinkRequests', - ]); - - const safe: AssertProps< - PropsActionsFromBackboneForChildrenType, - typeof unsafe - > = unsafe; - - return safe; - } -); - export class Timeline extends React.Component< PropsType, StateType, @@ -348,7 +292,7 @@ export class Timeline extends React.Component< } else { const lastId = last(items); if (lastId) { - loadNewestMessages(lastId, setFocus); + loadNewestMessages(id, lastId, setFocus); } } }; @@ -472,7 +416,7 @@ export class Timeline extends React.Component< maxRowIndex >= 0 && rowIndex >= maxRowIndex - LOAD_NEWER_THRESHOLD ) { - loadNewerMessages(newestBottomVisibleMessageId); + loadNewerMessages(id, newestBottomVisibleMessageId); } } @@ -482,7 +426,7 @@ export class Timeline extends React.Component< oldestPartiallyVisibleMessageId && oldestPartiallyVisibleMessageId === items[0] ) { - loadOlderMessages(oldestPartiallyVisibleMessageId); + loadOlderMessages(id, oldestPartiallyVisibleMessageId); } }; @@ -522,10 +466,10 @@ export class Timeline extends React.Component< } private markNewestBottomVisibleMessageRead = throttle((): void => { - const { markMessageRead } = this.props; + const { id, markMessageRead } = this.props; const { newestBottomVisibleMessageId } = this.state; if (newestBottomVisibleMessageId) { - markMessageRead(newestBottomVisibleMessageId); + markMessageRead(id, newestBottomVisibleMessageId); } }, 500); @@ -792,14 +736,10 @@ export class Timeline extends React.Component< public override render(): JSX.Element | null { const { - acceptConversation, acknowledgeGroupMemberNameCollisions, - blockAndReportSpam, - blockConversation, clearInvitedUuidsForNewlyCreatedGroup, closeContactSpoofingReview, contactSpoofingReview, - deleteConversation, getPreferredBadge, getTimestampForMessage, haveNewest, @@ -813,19 +753,15 @@ export class Timeline extends React.Component< items, messageLoadingState, oldestUnseenIndex, - removeMember, renderContactSpoofingReviewDialog, renderHeroRow, renderItem, renderTypingBubble, reviewGroupMemberNameCollision, reviewMessageRequestNameCollision, - showContactModal, theme, totalUnseen, - unblurAvatar, unreadCount, - updateSharedGroups, } = this.props; const { hasRecentlyScrolled, @@ -866,8 +802,6 @@ export class Timeline extends React.Component< (areUnreadBelowCurrentPosition || areSomeMessagesBelowCurrentPosition) ); - const actionProps = getActions(this.props); - let floatingHeader: ReactNode; // It's possible that a message was removed from `items` but we still have its ID in // state. `getTimestampForMessage` might return undefined in that case. @@ -938,7 +872,6 @@ export class Timeline extends React.Component< > {renderItem({ - actionProps, containerElementRef: this.containerRef, containerWidthBreakpoint: widthBreakpoint, conversationId: id, @@ -1011,7 +944,7 @@ export class Timeline extends React.Component< /> ); onClose = () => { - acknowledgeGroupMemberNameCollisions(groupNameCollisions); + acknowledgeGroupMemberNameCollisions(id, groupNameCollisions); }; break; } @@ -1047,16 +980,8 @@ export class Timeline extends React.Component< let contactSpoofingReviewDialog: ReactNode; if (contactSpoofingReview) { const commonProps = { - acceptConversation, - blockAndReportSpam, - blockConversation, - deleteConversation, - getPreferredBadge, - i18n, + conversationId: id, onClose: closeContactSpoofingReview, - onShowContactModal: showContactModal, - removeMember, - theme, }; switch (contactSpoofingReview.type) { @@ -1138,7 +1063,7 @@ export class Timeline extends React.Component< {Timeline.getWarning(this.props, this.state) && (
)} - {renderHeroRow(id, unblurAvatar, updateSharedGroups)} + {renderHeroRow(id)} )} diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index 95af78e34ca6..9b1eefe44486 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -65,7 +65,6 @@ const getDefaultProps = () => ({ reactToMessage: action('reactToMessage'), checkForAccount: action('checkForAccount'), clearSelectedMessage: action('clearSelectedMessage'), - contactSupport: action('contactSupport'), setQuoteByMessageId: action('setQuoteByMessageId'), retryDeleteForEveryone: action('retryDeleteForEveryone'), retryMessageSend: action('retryMessageSend'), @@ -73,10 +72,8 @@ const getDefaultProps = () => ({ deleteMessage: action('deleteMessage'), deleteMessageForEveryone: action('deleteMessageForEveryone'), kickOffAttachmentDownload: action('kickOffAttachmentDownload'), - learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'), markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'), messageExpanded: action('messageExpanded'), - showMessageDetail: action('showMessageDetail'), showConversation: action('showConversation'), openGiftBadge: action('openGiftBadge'), saveAttachment: action('saveAttachment'), diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx index bfe45cd0fa07..bf49919c3302 100644 --- a/ts/components/conversation/TimelineItem.tsx +++ b/ts/components/conversation/TimelineItem.tsx @@ -15,12 +15,8 @@ import type { } from './TimelineMessage'; import type { PropsActionsType as CallingNotificationActionsType } from './CallingNotification'; import { CallingNotification } from './CallingNotification'; -import type { PropsActionsType as PropsChatSessionRefreshedActionsType } from './ChatSessionRefreshedNotification'; import { ChatSessionRefreshedNotification } from './ChatSessionRefreshedNotification'; -import type { - PropsActionsType as DeliveryIssueActionProps, - PropsDataType as DeliveryIssueProps, -} from './DeliveryIssueNotification'; +import type { PropsDataType as DeliveryIssueProps } from './DeliveryIssueNotification'; import { DeliveryIssueNotification } from './DeliveryIssueNotification'; import type { PropsData as ChangeNumberNotificationProps } from './ChangeNumberNotification'; import { ChangeNumberNotification } from './ChangeNumberNotification'; @@ -171,9 +167,7 @@ type PropsLocalType = { type PropsActionsType = MessageActionsType & CallingNotificationActionsType & - DeliveryIssueActionProps & GroupV2ChangeActionsType & - PropsChatSessionRefreshedActionsType & SafetyNumberActionsType; export type PropsType = PropsLocalType & diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index 1051cb8e5fd4..e7930364d7ca 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -202,15 +202,17 @@ function MessageAudioContainer({ return ( ); } @@ -315,7 +317,6 @@ const createProps = (overrideProps: Partial = {}): Props => ({ 'showExpiredOutgoingTapToViewToast' ), toggleForwardMessageModal: action('toggleForwardMessageModal'), - showMessageDetail: action('showMessageDetail'), showLightbox: action('showLightbox'), startConversation: action('startConversation'), status: overrideProps.status || 'sent', diff --git a/ts/components/conversation/TimelineMessage.tsx b/ts/components/conversation/TimelineMessage.tsx index 494ecd3eade6..f0f48a336f19 100644 --- a/ts/components/conversation/TimelineMessage.tsx +++ b/ts/components/conversation/TimelineMessage.tsx @@ -27,6 +27,8 @@ import { doesMessageBodyOverflow } from './MessageBodyReadMore'; import type { Props as ReactionPickerProps } from './ReactionPicker'; import { ConfirmationDialog } from '../ConfirmationDialog'; import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts'; +import { PanelType } from '../../types/Panels'; +import type { PushPanelForConversationActionType } from '../../state/ducks/conversations'; export type PropsData = { canDownload: boolean; @@ -45,6 +47,7 @@ export type PropsActions = { }) => void; deleteMessageForEveryone: (id: string) => void; toggleForwardMessageModal: (id: string) => void; + pushPanelForConversation: PushPanelForConversationActionType; reactToMessage: ( id: string, { emoji, remove }: { emoji: string; remove: boolean } @@ -95,6 +98,7 @@ export function TimelineMessage(props: Props): JSX.Element { isSelected, isSticker, isTapToView, + pushPanelForConversation, reactToMessage, setQuoteByMessageId, renderReactionPicker, @@ -103,7 +107,6 @@ export function TimelineMessage(props: Props): JSX.Element { retryDeleteForEveryone, selectedReaction, toggleForwardMessageModal, - showMessageDetail, text, timestamp, kickOffAttachmentDownload, @@ -406,7 +409,12 @@ export function TimelineMessage(props: Props): JSX.Element { onDeleteForEveryone={ canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined } - onMoreInfo={() => showMessageDetail(id)} + onMoreInfo={() => + pushPanelForConversation(conversationId, { + type: PanelType.MessageDetails, + args: { messageId: id }, + }) + } /> ); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 2e419ff7eea4..4b8394f87871 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -42,10 +42,7 @@ import { missingCaseError } from '../util/missingCaseError'; import { dropNull } from '../util/dropNull'; import { incrementMessageCounter } from '../util/incrementMessageCounter'; import type { ConversationModel } from './conversations'; -import type { - OwnProps as SmartMessageDetailPropsType, - Contact as SmartMessageDetailContact, -} from '../state/smart/MessageDetail'; +import type { Contact as SmartMessageDetailContact } from '../state/smart/MessageDetail'; import { getCallingNotificationText } from '../util/callingNotification'; import type { ProcessedDataMessage, @@ -53,6 +50,7 @@ import type { ProcessedUnidentifiedDeliveryStatus, CallbackResultType, } from '../textsecure/Types.d'; +import type { Props as PropsForMessageDetails } from '../components/conversation/MessageDetail'; import { SendMessageProtoError } from '../textsecure/Errors'; import * as expirationTimer from '../util/expirationTimer'; import { getUserLanguages } from '../util/userLanguages'; @@ -186,7 +184,6 @@ import type { StickerWithHydratedData } from '../types/Stickers'; import { getStringForConversationMerge } from '../util/getStringForConversationMerge'; import { getStringForPhoneNumberDiscovery } from '../util/getStringForPhoneNumberDiscovery'; import { getTitle, renderNumber } from '../util/getTitle'; -import { DurationInSeconds } from '../util/durations'; import dataInterface from '../sql/Client'; function isSameUuid( @@ -265,15 +262,9 @@ async function shouldReplyNotifyUser( /* eslint-disable more/no-then */ -type PropsForMessageDetail = Pick< - SmartMessageDetailPropsType, - | 'sentAt' - | 'receivedAt' - | 'message' - | 'errors' - | 'contacts' - | 'expirationLength' - | 'expirationTimestamp' +export type MinimalPropsForMessageDetails = Pick< + PropsForMessageDetails, + 'sentAt' | 'receivedAt' | 'message' | 'errors' | 'contacts' >; window.Whisper = window.Whisper || {}; @@ -482,7 +473,9 @@ export class MessageModel extends window.Backbone.Model { }); } - getPropsForMessageDetail(ourConversationId: string): PropsForMessageDetail { + getPropsForMessageDetail( + ourConversationId: string + ): MinimalPropsForMessageDetails { const newIdentity = window.i18n('newIdentity'); const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError'; @@ -578,21 +571,9 @@ export class MessageModel extends window.Backbone.Model { }; }); - const expireTimer = this.get('expireTimer'); - const expirationStartTimestamp = this.get('expirationStartTimestamp'); - const expirationLength = isNumber(expireTimer) - ? DurationInSeconds.toMillis(expireTimer) - : undefined; - const expirationTimestamp = expirationTimer.calculateExpirationTimestamp({ - expireTimer, - expirationStartTimestamp, - }); - return { sentAt: this.get('sent_at'), receivedAt: this.getReceivedAt(), - expirationLength, - expirationTimestamp, message: getPropsForMessage(this.attributes, { conversationSelector: findAndFormatContact, ourConversationId, diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index bd1c03b26eb8..0d08bb611ddf 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -48,6 +48,7 @@ import { maybeGrabLinkPreview, removeLinkPreview, resetLinkPreview, + suspendLinkPreviews, } from '../../services/LinkPreview'; import { getMaximumAttachmentSize } from '../../util/attachments'; import { getRecipientsByConversation } from '../../util/getRecipientsByConversation'; @@ -66,9 +67,13 @@ import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessa import { writeDraftAttachment } from '../../util/writeDraftAttachment'; import { getMessageById } from '../../messages/getMessageById'; import { canReply } from '../selectors/message'; +import { getContactId } from '../../messages/helpers'; import { getConversationSelector } from '../selectors/conversations'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend'; import { useBoundActions } from '../../hooks/useBoundActions'; +import { scrollToMessage } from './conversations'; +import type { ScrollToMessageActionType } from './conversations'; +import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper'; // State @@ -142,18 +147,23 @@ type ComposerActionType = export const actions = { addAttachment, addPendingAttachment, + cancelJoinRequest, + onClearAttachments, + onCloseLinkPreview, onEditorStateChange, + onTextTooLong, processAttachments, reactToMessage, removeAttachment, replaceAttachments, resetComposer, + scrollToQuotedMessage, sendMultiMediaMessage, sendStickerMessage, setComposerDisabledState, setComposerFocus, - setQuoteByMessageId, setMediaQualitySetting, + setQuoteByMessageId, setQuotedMessage, }; @@ -161,6 +171,97 @@ export const useComposerActions = (): BoundActionCreatorsMapObject< typeof actions > => useBoundActions(actions); +function onClearAttachments(conversationId: string): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('onClearAttachments: No conversation found'); + } + + clearConversationDraftAttachments( + conversation.id, + conversation.get('draftAttachments') + ); + + return { + type: 'NOOP', + payload: null, + }; +} + +function cancelJoinRequest(conversationId: string): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('cancelJoinRequest: No conversation found'); + } + + longRunningTaskWrapper({ + idForLogging: conversation.idForLogging(), + name: 'cancelJoinRequest', + task: async () => conversation.cancelJoinRequest(), + }); + + return { + type: 'NOOP', + payload: null, + }; +} + +function onCloseLinkPreview(): NoopActionType { + suspendLinkPreviews(); + removeLinkPreview(); + + return { + type: 'NOOP', + payload: null, + }; +} +function onTextTooLong(): ShowToastActionType { + return { + type: SHOW_TOAST, + payload: { + toastType: ToastType.MessageBodyTooLong, + }, + }; +} + +function scrollToQuotedMessage({ + authorId, + conversationId, + sentAt, +}: Readonly<{ + authorId: string; + conversationId: string; + sentAt: number; +}>): ThunkAction< + void, + RootStateType, + unknown, + ShowToastActionType | ScrollToMessageActionType +> { + return async (dispatch, getState) => { + const messages = await window.Signal.Data.getMessagesBySentAt(sentAt); + const message = messages.find(item => + Boolean( + item.conversationId === conversationId && + authorId && + getContactId(item) === authorId + ) + ); + + if (!message) { + dispatch({ + type: SHOW_TOAST, + payload: { + toastType: ToastType.OriginalMessageNotFound, + }, + }); + return; + } + + scrollToMessage(conversationId, message.id)(dispatch, getState, undefined); + }; +} + function sendMultiMediaMessage( conversationId: string, options: { diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index bb8e26cdc924..9695a2ee1fb8 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -105,6 +105,7 @@ import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue'; import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { isIncoming, isOutgoing } from '../selectors/message'; +import { getActiveCallState } from '../selectors/calling'; import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage'; import type { ShowToastActionType } from './toast'; import { SHOW_TOAST } from './toast'; @@ -128,6 +129,7 @@ import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue'; import { isOlderThan } from '../../util/timestamp'; import { DAY } from '../../util/durations'; import { isNotNil } from '../../util/isNotNil'; +import { startConversation } from '../../util/startConversation'; // State @@ -875,10 +877,12 @@ export type ConversationActionType = export const actions = { acceptConversation, + acknowledgeGroupMemberNameCollisions, addMembersToGroup, approvePendingMembershipFromGroupV2, blockAndReportSpam, blockConversation, + blockGroupLinkRequests, cancelConversationVerification, changeHasGroupLink, clearCancelledConversationVerification, @@ -901,8 +905,8 @@ export const actions = { createGroup, deleteAvatarFromDisk, deleteConversation, - deleteMessageForEveryone, deleteMessage, + deleteMessageForEveryone, destroyMessages, discardMessages, doubleCheckMissingQuoteReference, @@ -911,8 +915,12 @@ export const actions = { initiateMigrationToGroupV2, kickOffAttachmentDownload, leaveGroup, + loadNewerMessages, + loadNewestMessages, + loadOlderMessages, loadRecentMediaItems, markAttachmentAsCorrupted, + markMessageRead, messageChanged, messageDeleted, messageExpanded, @@ -920,11 +928,16 @@ export const actions = { messagesAdded, messagesReset, myProfileChanged, + onArchive, + onMarkUnread, + onMoveToInbox, + onUndoArchive, openGiftBadge, popPanelForConversation, pushPanelForConversation, removeAllConversations, removeCustomColorOnConversations, + removeMember, removeMemberFromGroup, repairNewestMessage, repairOldestMessage, @@ -964,14 +977,17 @@ export const actions = { showExpiredOutgoingTapToViewToast, showInbox, startComposing, + startConversation, startSettingGroupMetadata, toggleAdmin, toggleComposeEditingAvatar, toggleConversationInChooseMembers, toggleGroupsForStorySend, toggleHideStories, + unblurAvatar, updateConversationModelSharedGroups, updateGroupAttributes, + updateSharedGroups, verifyConversationsStoppingSend, }; @@ -979,6 +995,230 @@ export const useConversationsActions = (): BoundActionCreatorsMapObject< typeof actions > => useBoundActions(actions); +function onArchive(conversationId: string): ShowToastActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('onArchive: Conversation not found!'); + } + + conversation.setArchived(true); + conversation.trigger('unload', 'archive'); + + return { + type: SHOW_TOAST, + payload: { + toastType: ToastType.ConversationArchived, + parameters: { + conversationId, + }, + }, + }; +} +function onUndoArchive( + conversationId: string +): SelectedConversationChangedActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('onUndoArchive: Conversation not found!'); + } + + conversation.setArchived(false); + return showConversation({ + conversationId, + }); +} + +function onMarkUnread(conversationId: string): ShowToastActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('onMarkUnread: Conversation not found!'); + } + + conversation.setMarkedUnread(true); + + return { + type: SHOW_TOAST, + payload: { + toastType: ToastType.ConversationMarkedUnread, + }, + }; +} +function onMoveToInbox(conversationId: string): ShowToastActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('onMoveToInbox: Conversation not found!'); + } + + conversation.setArchived(false); + + return { + type: SHOW_TOAST, + payload: { + toastType: ToastType.ConversationUnarchived, + }, + }; +} + +function acknowledgeGroupMemberNameCollisions( + conversationId: string, + groupNameCollisions: Readonly +): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error( + 'acknowledgeGroupMemberNameCollisions: Conversation not found!' + ); + } + + conversation.acknowledgeGroupMemberNameCollisions(groupNameCollisions); + + return { + type: 'NOOP', + payload: null, + }; +} +function blockGroupLinkRequests( + conversationId: string, + uuid: UUIDStringType +): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('blockGroupLinkRequests: Conversation not found!'); + } + + conversation.blockGroupLinkRequests(uuid); + + return { + type: 'NOOP', + payload: null, + }; +} +function loadNewerMessages( + conversationId: string, + newestMessageId: string +): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('loadNewerMessages: Conversation not found!'); + } + + conversation.loadNewerMessages(newestMessageId); + + return { + type: 'NOOP', + payload: null, + }; +} +function loadNewestMessages( + conversationId: string, + newestMessageId: string | undefined, + setFocus: boolean | undefined +): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('loadNewestMessages: Conversation not found!'); + } + + conversation.loadNewestMessages(newestMessageId, setFocus); + + return { + type: 'NOOP', + payload: null, + }; +} +function loadOlderMessages( + conversationId: string, + oldestMessageId: string +): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('loadOlderMessages: Conversation not found!'); + } + + conversation.loadOlderMessages(oldestMessageId); + return { + type: 'NOOP', + payload: null, + }; +} + +function markMessageRead( + conversationId: string, + messageId: string +): ThunkAction { + return async (_dispatch, getState) => { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('markMessageRead: Conversation not found!'); + } + + if (!window.SignalContext.activeWindowService.isActive()) { + return; + } + + const activeCall = getActiveCallState(getState()); + if (activeCall && !activeCall.pip) { + return; + } + + const message = await getMessageById(messageId); + if (!message) { + throw new Error(`markMessageRead: failed to load message ${messageId}`); + } + + await conversation.markRead(message.get('received_at'), { + newestSentAt: message.get('sent_at'), + sendReadReceipts: true, + }); + }; +} +function removeMember( + conversationId: string, + memberConversationId: string +): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('removeMember: Conversation not found!'); + } + + longRunningTaskWrapper({ + idForLogging: conversation.idForLogging(), + name: 'removeMember', + task: () => conversation.removeFromGroupV2(memberConversationId), + }); + + return { + type: 'NOOP', + payload: null, + }; +} +function unblurAvatar(conversationId: string): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('unblurAvatar: Conversation not found!'); + } + + conversation.unblurAvatar(); + + return { + type: 'NOOP', + payload: null, + }; +} +function updateSharedGroups(conversationId: string): NoopActionType { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('updateSharedGroups: Conversation not found!'); + } + + conversation.throttledUpdateSharedGroups?.(); + + return { + type: 'NOOP', + payload: null, + }; +} + function filterAvatarData( avatars: ReadonlyArray, data: AvatarDataType @@ -2314,6 +2554,10 @@ function pushPanelForConversation( }; } +export type PopPanelForConversationActionType = ( + conversationId: string +) => unknown; + function popPanelForConversation( conversationId: string ): ThunkAction { @@ -2830,7 +3074,7 @@ function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionT return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' }; } -function scrollToMessage( +export function scrollToMessage( conversationId: string, messageId: string ): ThunkAction { diff --git a/ts/state/roots/createMessageDetail.tsx b/ts/state/roots/createMessageDetail.tsx index c934bee42865..c0e2d191f4ed 100644 --- a/ts/state/roots/createMessageDetail.tsx +++ b/ts/state/roots/createMessageDetail.tsx @@ -7,12 +7,11 @@ import { Provider } from 'react-redux'; import type { Store } from 'redux'; -import type { OwnProps } from '../smart/MessageDetail'; import { SmartMessageDetail } from '../smart/MessageDetail'; export const createMessageDetail = ( store: Store, - props: OwnProps + props: Parameters[0] ): ReactElement => ( diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index efb8a6b29d16..69e75cedbb7b 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -1087,6 +1087,7 @@ function getPropsForGroupV2Change( return { areWeAdmin: Boolean(conversation.areWeAdmin), + conversationId: conversation.id, groupName: conversation?.type === 'group' ? conversation?.name : undefined, groupMemberships: conversation.memberships, groupBannedMemberships: conversation.bannedMemberships, diff --git a/ts/state/smart/CompositionTextArea.tsx b/ts/state/smart/CompositionTextArea.tsx index 1550362f3988..66e29ba99e9f 100644 --- a/ts/state/smart/CompositionTextArea.tsx +++ b/ts/state/smart/CompositionTextArea.tsx @@ -11,8 +11,7 @@ import { getIntl } from '../selectors/user'; import { useActions as useEmojiActions } from '../ducks/emojis'; import { useActions as useItemsActions } from '../ducks/items'; import { getPreferredBadgeSelector } from '../selectors/badges'; -import { showToast } from '../../util/showToast'; -import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong'; +import { useComposerActions } from '../ducks/composer'; export type SmartCompositionTextAreaProps = Pick< CompositionTextAreaProps, @@ -34,6 +33,7 @@ export function SmartCompositionTextArea( const { onUseEmoji: onPickEmoji } = useEmojiActions(); const { onSetSkinTone } = useItemsActions(); + const { onTextTooLong } = useComposerActions(); const getPreferredBadge = useSelector(getPreferredBadgeSelector); @@ -44,7 +44,7 @@ export function SmartCompositionTextArea( onPickEmoji={onPickEmoji} onSetSkinTone={onSetSkinTone} getPreferredBadge={getPreferredBadge} - onTextTooLong={() => showToast(ToastMessageBodyTooLong)} + onTextTooLong={onTextTooLong} /> ); } diff --git a/ts/state/smart/ContactSpoofingReviewDialog.tsx b/ts/state/smart/ContactSpoofingReviewDialog.tsx index f73ac1b15255..c7c46a96e3c1 100644 --- a/ts/state/smart/ContactSpoofingReviewDialog.tsx +++ b/ts/state/smart/ContactSpoofingReviewDialog.tsx @@ -5,33 +5,39 @@ import * as React from 'react'; import { useSelector } from 'react-redux'; import type { StateType } from '../reducer'; -import type { PropsType as DownstreamPropsType } from '../../components/conversation/ContactSpoofingReviewDialog'; import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog'; import type { ConversationType } from '../ducks/conversations'; +import { useConversationsActions } from '../ducks/conversations'; import type { GetConversationByIdType } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations'; import { ContactSpoofingType } from '../../util/contactSpoofing'; +import { useGlobalModalActions } from '../ducks/globalModals'; +import { getPreferredBadgeSelector } from '../selectors/badges'; +import { getIntl, getTheme } from '../selectors/user'; -export type PropsType = Omit & - ( - | { - type: ContactSpoofingType.DirectConversationWithSameTitle; - possiblyUnsafeConversation: ConversationType; - safeConversation: ConversationType; - } - | { - type: ContactSpoofingType.MultipleGroupMembersWithSameTitle; - groupConversationId: string; - collisionInfoByTitle: Record< - string, - Array<{ - oldName?: string; - conversation: ConversationType; - }> - >; - } - ); +export type PropsType = + | { + conversationId: string; + onClose: () => void; + } & ( + | { + type: ContactSpoofingType.DirectConversationWithSameTitle; + possiblyUnsafeConversation: ConversationType; + safeConversation: ConversationType; + } + | { + type: ContactSpoofingType.MultipleGroupMembersWithSameTitle; + groupConversationId: string; + collisionInfoByTitle: Record< + string, + Array<{ + oldName?: string; + conversation: ConversationType; + }> + >; + } + ); export function SmartContactSpoofingReviewDialog( props: PropsType @@ -42,14 +48,39 @@ export function SmartContactSpoofingReviewDialog( getConversationSelector ); + const { + acceptConversation, + blockAndReportSpam, + blockConversation, + deleteConversation, + removeMember, + } = useConversationsActions(); + const { showContactModal } = useGlobalModalActions(); + const getPreferredBadge = useSelector(getPreferredBadgeSelector); + const i18n = useSelector(getIntl); + const theme = useSelector(getTheme); + + const sharedProps = { + acceptConversation, + blockAndReportSpam, + blockConversation, + deleteConversation, + getPreferredBadge, + i18n, + removeMember, + showContactModal, + theme, + }; + if (type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) { return ( ); } - return ; + return ; } diff --git a/ts/state/smart/ConversationHeader.tsx b/ts/state/smart/ConversationHeader.tsx index 94af999bab14..70ef5f18f9da 100644 --- a/ts/state/smart/ConversationHeader.tsx +++ b/ts/state/smart/ConversationHeader.tsx @@ -29,12 +29,6 @@ import { isSignalConversation } from '../../util/isSignalConversation'; export type OwnProps = { id: string; - - onArchive: () => void; - onGoBack: () => void; - onMarkUnread: () => void; - onMoveToInbox: () => void; - onSearchInConversation: () => void; }; const getOutgoingCallButtonStyle = ( diff --git a/ts/state/smart/ConversationView.tsx b/ts/state/smart/ConversationView.tsx index d25b9ad79ee3..a8515733ec36 100644 --- a/ts/state/smart/ConversationView.tsx +++ b/ts/state/smart/ConversationView.tsx @@ -3,11 +3,8 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import type { CompositionAreaPropsType } from './CompositionArea'; -import type { OwnProps as ConversationHeaderPropsType } from './ConversationHeader'; import type { StateType } from '../reducer'; import type { ReactPanelRenderType } from '../../types/Panels'; -import type { TimelinePropsType } from './Timeline'; import * as log from '../../logging/log'; import { ContactDetail } from '../../components/conversation/ContactDetail'; import { ConversationView } from '../../components/conversation/ConversationView'; @@ -26,31 +23,17 @@ import { SmartStickerManager } from './StickerManager'; import { SmartTimeline } from './Timeline'; import { getIntl } from '../selectors/user'; import { getTopPanelRenderableByReact } from '../selectors/conversations'; -import { startConversation } from '../../util/startConversation'; import { useComposerActions } from '../ducks/composer'; +import { useConversationsActions } from '../ducks/conversations'; export type PropsType = { conversationId: string; - compositionAreaProps: Pick< - CompositionAreaPropsType, - | 'id' - | 'onCancelJoinRequest' - | 'onClearAttachments' - | 'onCloseLinkPreview' - | 'onEditorStateChange' - | 'onSelectMediaQuality' - | 'onTextTooLong' - >; - conversationHeaderProps: ConversationHeaderPropsType; - timelineProps: TimelinePropsType; }; export function SmartConversationView({ - compositionAreaProps, - conversationHeaderProps, conversationId, - timelineProps, }: PropsType): JSX.Element { + const { startConversation } = useConversationsActions(); const topPanel = useSelector( getTopPanelRenderableByReact ); @@ -62,13 +45,11 @@ export function SmartConversationView({ ( - - )} + renderCompositionArea={() => } renderConversationHeader={() => ( - + )} - renderTimeline={() => } + renderTimeline={() => } renderPanel={() => { if (!topPanel) { return; diff --git a/ts/state/smart/MessageDetail.tsx b/ts/state/smart/MessageDetail.tsx index 504cd6e18e3a..bdb22954a55c 100644 --- a/ts/state/smart/MessageDetail.tsx +++ b/ts/state/smart/MessageDetail.tsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; -import type { ExternalProps as MessageDetailProps } from '../../components/conversation/MessageDetail'; +import type { Props as MessageDetailProps } from '../../components/conversation/MessageDetail'; import { MessageDetail } from '../../components/conversation/MessageDetail'; import { mapDispatchToProps } from '../actions'; @@ -12,34 +12,25 @@ import { getPreferredBadgeSelector } from '../selectors/badges'; import { getIntl, getInteractionMode, getTheme } from '../selectors/user'; import { renderAudioAttachment } from './renderAudioAttachment'; import { getContactNameColorSelector } from '../selectors/conversations'; +import type { MinimalPropsForMessageDetails } from '../../models/messages'; export { Contact } from '../../components/conversation/MessageDetail'; -export type OwnProps = Omit< - MessageDetailProps, - | 'getPreferredBadge' - | 'i18n' - | 'interactionMode' - | 'renderAudioAttachment' - | 'renderEmojiPicker' - | 'renderReactionPicker' - | 'theme' - | 'showContactModal' - | 'showConversation' ->; +export type PropsWithExtraFunctions = MinimalPropsForMessageDetails & + Pick< + MessageDetailProps, + | 'contactNameColor' + | 'getPreferredBadge' + | 'i18n' + | 'interactionMode' + | 'renderAudioAttachment' + | 'theme' + >; const mapStateToProps = ( state: StateType, - props: OwnProps -): MessageDetailProps => { - const { - contacts, - errors, - message, - receivedAt, - sentAt, - - startConversation, - } = props; + props: MinimalPropsForMessageDetails +): PropsWithExtraFunctions => { + const { contacts, errors, message, receivedAt, sentAt } = props; const contactNameColor = message.conversationType === 'group' @@ -65,7 +56,6 @@ const mapStateToProps = ( theme: getTheme(state), renderAudioAttachment, - startConversation, }; }; diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx index e58b18802842..955b343a87a0 100644 --- a/ts/state/smart/Timeline.tsx +++ b/ts/state/smart/Timeline.tsx @@ -10,8 +10,6 @@ import { mapDispatchToProps } from '../actions'; import type { ContactSpoofingReviewPropType, WarningType as TimelineWarningType, - PropsType as ComponentPropsType, - PropsActionsFromBackboneForChildrenType, } from '../../components/conversation/Timeline'; import { Timeline } from '../../components/conversation/Timeline'; import type { StateType } from '../reducer'; @@ -50,47 +48,12 @@ import { ContactSpoofingType } from '../../util/contactSpoofing'; import type { UnreadIndicatorPlacement } from '../../util/timelineUtil'; import type { WidthBreakpoint } from '../../components/_util'; import { getPreferredBadgeSelector } from '../selectors/badges'; -import { markViewed } from '../ducks/conversations'; type ExternalProps = { id: string; - - // Note: most action creators are not wired into redux; for now they - // are provided by ConversationView in setupTimeline(). }; -export type TimelinePropsType = ExternalProps & - Pick< - ComponentPropsType, - // All of these are the ones we need from backbone - - // Used by Timeline itself - | 'acknowledgeGroupMemberNameCollisions' - | 'loadOlderMessages' - | 'loadNewerMessages' - | 'loadNewestMessages' - | 'markMessageRead' - | 'removeMember' - | 'unblurAvatar' - | 'updateSharedGroups' - - // MessageActionsType - | 'scrollToQuotedMessage' - | 'showMessageDetail' - | 'startConversation' - - // ChatSessionRefreshedNotificationActionsType - | 'contactSupport' - - // DeliveryIssueNotificationActionsType - | 'learnMoreAboutDeliveryIssue' - - // GroupV2ChangeActionsType - | 'blockGroupLinkRequests' - >; - function renderItem({ - actionProps, containerElementRef, containerWidthBreakpoint, conversationId, @@ -100,7 +63,6 @@ function renderItem({ previousMessageId, unreadIndicatorPlacement, }: { - actionProps: PropsActionsFromBackboneForChildrenType; containerElementRef: RefObject; containerWidthBreakpoint: WidthBreakpoint; conversationId: string; @@ -112,7 +74,6 @@ function renderItem({ }): JSX.Element { return ( ; } -function renderHeroRow( - id: string, - unblurAvatar: () => void, - updateSharedGroups: () => unknown -): JSX.Element { - return ( - - ); +function renderHeroRow(id: string): JSX.Element { + return ; } function renderTypingBubble(id: string): JSX.Element { return ; @@ -270,8 +221,8 @@ const getContactSpoofingReview = ( } }; -const mapStateToProps = (state: StateType, props: TimelinePropsType) => { - const { id, ...actions } = props; +const mapStateToProps = (state: StateType, props: ExternalProps) => { + const { id } = props; const conversation = getConversationSelector(state)(id); @@ -307,8 +258,6 @@ const mapStateToProps = (state: StateType, props: TimelinePropsType) => { renderContactSpoofingReviewDialog, renderHeroRow, renderTypingBubble, - markViewed, - ...actions, }; }; diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx index cd1da9a9c920..e94e27cf439e 100644 --- a/ts/types/Toast.tsx +++ b/ts/types/Toast.tsx @@ -9,6 +9,9 @@ export enum ToastType { CannotOpenGiftBadgeIncoming = 'CannotOpenGiftBadgeIncoming', CannotOpenGiftBadgeOutgoing = 'CannotOpenGiftBadgeOutgoing', CannotStartGroupCall = 'CannotStartGroupCall', + ConversationArchived = 'ConversationArchived', + ConversationMarkedUnread = 'ConversationMarkedUnread', + ConversationUnarchived = 'ConversationUnarchived', CopiedUsername = 'CopiedUsername', CopiedUsernameLink = 'CopiedUsernameLink', DangerousFileType = 'DangerousFileType', @@ -22,6 +25,7 @@ export enum ToastType { LeftGroup = 'LeftGroup', MaxAttachments = 'MaxAttachments', MessageBodyTooLong = 'MessageBodyTooLong', + OriginalMessageNotFound = 'OriginalMessageNotFound', PinnedConversationsFull = 'PinnedConversationsFull', ReactionFailed = 'ReactionFailed', ReportedSpamAndBlocked = 'ReportedSpamAndBlocked', diff --git a/ts/util/showToast.tsx b/ts/util/showToast.tsx index 7d9304b3dede..018198d7bc50 100644 --- a/ts/util/showToast.tsx +++ b/ts/util/showToast.tsx @@ -8,12 +8,6 @@ import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMem import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin'; import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed'; import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved'; -import type { - ToastConversationArchived, - ToastPropsType as ToastConversationArchivedPropsType, -} from '../components/ToastConversationArchived'; -import type { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread'; -import type { ToastConversationUnarchived } from '../components/ToastConversationUnarchived'; import type { ToastInternalError, ToastPropsType as ToastInternalErrorPropsType, @@ -25,9 +19,7 @@ import type { import type { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied'; import type { ToastLinkCopied } from '../components/ToastLinkCopied'; import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs'; -import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong'; -import type { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound'; import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed'; import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit'; import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment'; @@ -36,12 +28,6 @@ export function showToast(Toast: typeof ToastAlreadyGroupMember): void; export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): void; export function showToast(Toast: typeof ToastCaptchaFailed): void; export function showToast(Toast: typeof ToastCaptchaSolved): void; -export function showToast( - Toast: typeof ToastConversationArchived, - props: ToastConversationArchivedPropsType -): void; -export function showToast(Toast: typeof ToastConversationMarkedUnread): void; -export function showToast(Toast: typeof ToastConversationUnarchived): void; export function showToast( Toast: typeof ToastInternalError, props: ToastInternalErrorPropsType @@ -53,8 +39,6 @@ export function showToast( export function showToast(Toast: typeof ToastGroupLinkCopied): void; export function showToast(Toast: typeof ToastLinkCopied): void; export function showToast(Toast: typeof ToastLoadingFullLogs): void; -export function showToast(Toast: typeof ToastMessageBodyTooLong): void; -export function showToast(Toast: typeof ToastOriginalMessageNotFound): void; export function showToast(Toast: typeof ToastStickerPackInstallFailed): void; export function showToast(Toast: typeof ToastVoiceNoteLimit): void; export function showToast( diff --git a/ts/views/conversation_view.tsx b/ts/views/conversation_view.tsx index 9edeabeec35e..d27f6a70f524 100644 --- a/ts/views/conversation_view.tsx +++ b/ts/views/conversation_view.tsx @@ -8,43 +8,22 @@ import { render } from 'mustache'; import type { ConversationModel } from '../models/conversations'; import { getMessageById } from '../messages/getMessageById'; -import { getContactId } from '../messages/helpers'; import { strictAssert } from '../util/assert'; -import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions'; import { isGroup } from '../util/whatTypeOfConversation'; -import { getActiveCallState } from '../state/selectors/calling'; import { ReactWrapperView } from './ReactWrapperView'; import * as log from '../logging/log'; import { createConversationView } from '../state/roots/createConversationView'; -import { ToastConversationArchived } from '../components/ToastConversationArchived'; -import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread'; -import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived'; -import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong'; -import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound'; -import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; -import { showToast } from '../util/showToast'; -import { UUIDKind } from '../types/UUID'; -import type { UUIDStringType } from '../types/UUID'; import { removeLinkPreview, suspendLinkPreviews, } from '../services/LinkPreview'; import { SECOND } from '../util/durations'; -import { startConversation } from '../util/startConversation'; -import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; -import { clearConversationDraftAttachments } from '../util/clearConversationDraftAttachments'; import type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels'; import { PanelType, isPanelHandledByReact } from '../types/Panels'; +import { UUIDKind } from '../types/UUID'; type BackbonePanelType = { panelType: PanelType; view: Backbone.View }; -const { getMessagesBySentAt } = window.Signal.Data; - -type MessageActionsType = { - showMessageDetail: (messageId: string) => unknown; - startConversation: (e164: string, uuid: UUIDStringType) => unknown; -}; - export class ConversationView extends window.Backbone.View { // Sub-views private contactModalView?: Backbone.View; @@ -69,12 +48,6 @@ export class ConversationView extends window.Backbone.View { this.unload(`model trigger - ${reason}`) ); - // These are triggered by background.ts for keyboard handling - this.listenTo(this.model, 'escape-pressed', () => { - window.reduxActions.conversations.popPanelForConversation(this.model.id); - }); - this.listenTo(this.model, 'show-message-details', this.showMessageDetail); - this.listenTo(this.model, 'pushPanel', this.pushPanel); this.listenTo(this.model, 'popPanel', this.popPanel); @@ -116,209 +89,18 @@ export class ConversationView extends window.Backbone.View { } setupConversationView(): void { - // setupHeader - const conversationHeaderProps = { - id: this.model.id, - - onSearchInConversation: () => { - const { searchInConversation } = window.reduxActions.search; - searchInConversation(this.model.id); - }, - onGoBack: () => { - window.reduxActions.conversations.popPanelForConversation( - this.model.id - ); - }, - - onArchive: () => { - this.model.setArchived(true); - this.model.trigger('unload', 'archive'); - - showToast(ToastConversationArchived, { - undo: () => { - this.model.setArchived(false); - window.reduxActions.conversations.showConversation({ - conversationId: this.model.id, - }); - }, - }); - }, - onMarkUnread: () => { - this.model.setMarkedUnread(true); - - showToast(ToastConversationMarkedUnread); - }, - onMoveToInbox: () => { - this.model.setArchived(false); - - showToast(ToastConversationUnarchived); - }, - }; - - // setupTimeline - - const contactSupport = () => { - const baseUrl = - 'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed'; - const locale = window.getLocale(); - const supportLocale = window.Signal.Util.mapToSupportLocale(locale); - const url = baseUrl.replace('LOCALE', supportLocale); - - openLinkInWebBrowser(url); - }; - - const learnMoreAboutDeliveryIssue = () => { - openLinkInWebBrowser( - 'https://support.signal.org/hc/articles/4404859745690' - ); - }; - - const scrollToQuotedMessage = async ( - options: Readonly<{ - authorId: string; - sentAt: number; - }> - ) => { - const { authorId, sentAt } = options; - - const conversationId = this.model.id; - const messages = await getMessagesBySentAt(sentAt); - const message = messages.find(item => - Boolean( - item.conversationId === conversationId && - authorId && - getContactId(item) === authorId - ) - ); - - if (!message) { - showToast(ToastOriginalMessageNotFound); - return; - } - - window.reduxActions.conversations.scrollToMessage( - conversationId, - message.id - ); - }; - - const markMessageRead = async (messageId: string) => { - if (!window.SignalContext.activeWindowService.isActive()) { - return; - } - - const activeCall = getActiveCallState(window.reduxStore.getState()); - if (activeCall && !activeCall.pip) { - return; - } - - const message = await getMessageById(messageId); - if (!message) { - throw new Error(`markMessageRead: failed to load message ${messageId}`); - } - - await this.model.markRead(message.get('received_at'), { - newestSentAt: message.get('sent_at'), - sendReadReceipts: true, - }); - }; - - const timelineProps = { - id: this.model.id, - - ...this.getMessageActions(), - - acknowledgeGroupMemberNameCollisions: ( - groupNameCollisions: Readonly - ): void => { - this.model.acknowledgeGroupMemberNameCollisions(groupNameCollisions); - }, - blockGroupLinkRequests: (uuid: UUIDStringType) => { - this.model.blockGroupLinkRequests(uuid); - }, - contactSupport, - learnMoreAboutDeliveryIssue, - loadNewerMessages: this.model.loadNewerMessages.bind(this.model), - loadNewestMessages: this.model.loadNewestMessages.bind(this.model), - loadOlderMessages: this.model.loadOlderMessages.bind(this.model), - markMessageRead, - removeMember: (conversationId: string) => { - longRunningTaskWrapper({ - idForLogging: this.model.idForLogging(), - name: 'removeMember', - task: () => this.model.removeFromGroupV2(conversationId), - }); - }, - scrollToQuotedMessage, - unblurAvatar: () => { - this.model.unblurAvatar(); - }, - updateSharedGroups: () => this.model.throttledUpdateSharedGroups?.(), - }; - // setupCompositionArea window.reduxActions.composer.resetComposer(); - const compositionAreaProps = { - id: this.model.id, - onTextTooLong: () => showToast(ToastMessageBodyTooLong), - onCancelJoinRequest: async () => { - await window.showConfirmationDialog({ - dialogName: 'GroupV2CancelRequestToJoin', - message: window.i18n( - 'GroupV2--join--cancel-request-to-join--confirmation' - ), - okText: window.i18n('GroupV2--join--cancel-request-to-join--yes'), - cancelText: window.i18n('GroupV2--join--cancel-request-to-join--no'), - resolve: () => { - longRunningTaskWrapper({ - idForLogging: this.model.idForLogging(), - name: 'onCancelJoinRequest', - task: async () => this.model.cancelJoinRequest(), - }); - }, - }); - }, - - onClearAttachments: () => - clearConversationDraftAttachments( - this.model.id, - this.model.get('draftAttachments') - ), - onSelectMediaQuality: (isHQ: boolean) => { - window.reduxActions.composer.setMediaQualitySetting(isHQ); - }, - - onCloseLinkPreview: () => { - suspendLinkPreviews(); - removeLinkPreview(); - }, - }; - // createConversationView root - const JSX = createConversationView(window.reduxStore, { conversationId: this.model.id, - compositionAreaProps, - conversationHeaderProps, - timelineProps, }); this.conversationView = new ReactWrapperView({ JSX }); this.$('.ConversationView__template').append(this.conversationView.el); } - getMessageActions(): MessageActionsType { - const showMessageDetail = (messageId: string) => { - this.showMessageDetail(messageId); - }; - - return { - showMessageDetail, - startConversation, - }; - } - unload(reason: string): void { log.info( 'unloading conversation', @@ -445,13 +227,6 @@ export class ConversationView extends window.Backbone.View { this.model.updateVerified(); } - showMessageDetail(messageId: string): void { - window.reduxActions.conversations.pushPanelForConversation(this.model.id, { - type: PanelType.MessageDetails, - args: { messageId }, - }); - } - getMessageDetail({ messageId, }: { @@ -459,7 +234,7 @@ export class ConversationView extends window.Backbone.View { }): Backbone.View | undefined { const message = window.MessageController.getById(messageId); if (!message) { - throw new Error(`showMessageDetail: Message ${messageId} missing!`); + throw new Error(`getMessageDetail: Message ${messageId} missing!`); } if (!message.isNormalBubble()) { @@ -470,7 +245,6 @@ export class ConversationView extends window.Backbone.View { ...message.getPropsForMessageDetail( window.ConversationController.getOurConversationIdOrThrow() ), - ...this.getMessageActions(), }); const onClose = () => {