conversation_view: Move the last of the small functions to redux

This commit is contained in:
Scott Nonnenberg 2022-12-20 19:25:10 -08:00 committed by GitHub
parent 86e92dda51
commit 1a68c3db62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 782 additions and 944 deletions

View file

@ -97,6 +97,8 @@
&__linkNotification { &__linkNotification {
@include font-body-2; @include font-body-2;
margin-top: 15px;
text-align: center; text-align: center;
user-select: none; user-select: none;

View file

@ -142,8 +142,6 @@ import { deleteAllLogs } from './util/deleteAllLogs';
import { ReactWrapperView } from './views/ReactWrapperView'; import { ReactWrapperView } from './views/ReactWrapperView';
import { ToastCaptchaFailed } from './components/ToastCaptchaFailed'; import { ToastCaptchaFailed } from './components/ToastCaptchaFailed';
import { ToastCaptchaSolved } from './components/ToastCaptchaSolved'; import { ToastCaptchaSolved } from './components/ToastCaptchaSolved';
import { ToastConversationArchived } from './components/ToastConversationArchived';
import { ToastConversationUnarchived } from './components/ToastConversationUnarchived';
import { showToast } from './util/showToast'; import { showToast } from './util/showToast';
import { startInteractionMode } from './windows/startInteractionMode'; import { startInteractionMode } from './windows/startInteractionMode';
import type { MainWindowStatsType } from './windows/context'; import type { MainWindowStatsType } from './windows/context';
@ -1454,7 +1452,9 @@ export async function startApp(): Promise<void> {
// Send Escape to active conversation so it can close panels // Send Escape to active conversation so it can close panels
if (conversation && key === 'Escape') { if (conversation && key === 'Escape') {
conversation.trigger('escape-pressed'); window.reduxActions.conversations.popPanelForConversation(
conversation.id
);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return; return;
@ -1530,7 +1530,9 @@ export async function startApp(): Promise<void> {
) { ) {
window.reduxActions.conversations.pushPanelForConversation( window.reduxActions.conversations.pushPanelForConversation(
conversation.id, conversation.id,
{ type: PanelType.AllMedia } {
type: PanelType.AllMedia,
}
); );
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -1551,16 +1553,10 @@ export async function startApp(): Promise<void> {
shiftKey && shiftKey &&
(key === 'a' || key === 'A') (key === 'a' || key === 'A')
) { ) {
conversation.setArchived(true); event.preventDefault();
conversation.trigger('unload', 'keyboard shortcut archive'); event.stopPropagation();
showToast(ToastConversationArchived, {
undo: () => { window.reduxActions.conversations.onArchive(conversation.id);
conversation.setArchived(false);
window.reduxActions.conversations.showConversation({
conversationId: conversation.get('id'),
});
},
});
// It's very likely that the act of archiving a conversation will set focus to // 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. // 'none,' or the top-level body element. This resets it to the left pane.
@ -1573,8 +1569,6 @@ export async function startApp(): Promise<void> {
} }
} }
event.preventDefault();
event.stopPropagation();
return; return;
} }
if ( if (
@ -1584,11 +1578,11 @@ export async function startApp(): Promise<void> {
shiftKey && shiftKey &&
(key === 'u' || key === 'U') (key === 'u' || key === 'U')
) { ) {
conversation.setArchived(false);
showToast(ToastConversationUnarchived);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
window.reduxActions.conversations.onMoveToInbox(conversation.id);
return; return;
} }
@ -1603,13 +1597,15 @@ export async function startApp(): Promise<void> {
shiftKey && shiftKey &&
(key === 'c' || key === 'C') (key === 'c' || key === 'C')
) { ) {
event.preventDefault();
event.stopPropagation();
conversation.trigger('unload', 'keyboard shortcut close'); conversation.trigger('unload', 'keyboard shortcut close');
window.reduxActions.conversations.showConversation({ window.reduxActions.conversations.showConversation({
conversationId: undefined, conversationId: undefined,
messageId: undefined, messageId: undefined,
}); });
event.preventDefault();
event.stopPropagation();
return; return;
} }
@ -1622,14 +1618,22 @@ export async function startApp(): Promise<void> {
!shiftKey && !shiftKey &&
(key === 'd' || key === 'D') (key === 'd' || key === 'D')
) { ) {
event.preventDefault();
event.stopPropagation();
const { selectedMessage } = state.conversations; const { selectedMessage } = state.conversations;
if (!selectedMessage) { if (!selectedMessage) {
return; return;
} }
conversation.trigger('show-message-details', selectedMessage); window.reduxActions.conversations.pushPanelForConversation(
event.preventDefault(); conversation.id,
event.stopPropagation(); {
type: PanelType.MessageDetails,
args: { messageId: selectedMessage },
}
);
return; return;
} }
@ -1640,6 +1644,9 @@ export async function startApp(): Promise<void> {
shiftKey && shiftKey &&
(key === 'r' || key === 'R') (key === 'r' || key === 'R')
) { ) {
event.preventDefault();
event.stopPropagation();
const { selectedMessage } = state.conversations; const { selectedMessage } = state.conversations;
const composerState = window.reduxStore const composerState = window.reduxStore
@ -1652,8 +1659,6 @@ export async function startApp(): Promise<void> {
quote ? undefined : selectedMessage quote ? undefined : selectedMessage
); );
event.preventDefault();
event.stopPropagation();
return; return;
} }
@ -1664,12 +1669,12 @@ export async function startApp(): Promise<void> {
!shiftKey && !shiftKey &&
(key === 's' || key === 'S') (key === 's' || key === 'S')
) { ) {
event.preventDefault();
event.stopPropagation();
const { selectedMessage } = state.conversations; const { selectedMessage } = state.conversations;
if (selectedMessage) { if (selectedMessage) {
event.preventDefault();
event.stopPropagation();
window.reduxActions.conversations.saveAttachmentFromMessage( window.reduxActions.conversations.saveAttachmentFromMessage(
selectedMessage selectedMessage
); );

View file

@ -41,6 +41,7 @@ type PropsType = {
isMaximized: boolean; isMaximized: boolean;
isFullScreen: boolean; isFullScreen: boolean;
menuOptions: MenuOptionsType; menuOptions: MenuOptionsType;
onUndoArchive: (conversationId: string) => unknown;
openFileInFolder: (target: string) => unknown; openFileInFolder: (target: string) => unknown;
hasCustomTitleBar: boolean; hasCustomTitleBar: boolean;
hideMenuBar: boolean; hideMenuBar: boolean;
@ -73,6 +74,7 @@ export function App({
isShowingStoriesView, isShowingStoriesView,
hasCustomTitleBar, hasCustomTitleBar,
menuOptions, menuOptions,
onUndoArchive,
openInbox, openInbox,
openFileInFolder, openFileInFolder,
registerSingleDevice, registerSingleDevice,
@ -183,6 +185,7 @@ export function App({
<ToastManager <ToastManager
hideToast={hideToast} hideToast={hideToast}
i18n={i18n} i18n={i18n}
onUndoArchive={onUndoArchive}
openFileInFolder={openFileInFolder} openFileInFolder={openFileInFolder}
toast={toast} toast={toast}
/> />

View file

@ -68,7 +68,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
// MediaEditor // MediaEditor
imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
// MediaQualitySelector // MediaQualitySelector
onSelectMediaQuality: action('onSelectMediaQuality'), setMediaQualitySetting: action('setMediaQualitySetting'),
shouldSendHighQualityAttachments: Boolean( shouldSendHighQualityAttachments: Boolean(
overrideProps.shouldSendHighQualityAttachments overrideProps.shouldSendHighQualityAttachments
), ),
@ -116,8 +116,12 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
Boolean(overrideProps.announcementsOnly) Boolean(overrideProps.announcementsOnly)
), ),
areWeAdmin: boolean('areWeAdmin', Boolean(overrideProps.areWeAdmin)), areWeAdmin: boolean('areWeAdmin', Boolean(overrideProps.areWeAdmin)),
areWePendingApproval: boolean(
'areWePendingApproval',
Boolean(overrideProps.areWePendingApproval)
),
groupAdmins: [], groupAdmins: [],
onCancelJoinRequest: action('onCancelJoinRequest'), cancelJoinRequest: action('cancelJoinRequest'),
showConversation: action('showConversation'), showConversation: action('showConversation'),
// SMS-only // SMS-only
isSMSOnly: overrideProps.isSMSOnly || false, isSMSOnly: overrideProps.isSMSOnly || false,
@ -193,6 +197,20 @@ export function Attachments(): JSX.Element {
return <CompositionArea {...props} />; return <CompositionArea {...props} />;
} }
export function PendingApproval(): JSX.Element {
return (
<CompositionArea
{...useProps({
areWePendingApproval: true,
})}
/>
);
}
AnnouncementsOnlyGroup.story = {
name: 'Announcements Only group',
};
export function AnnouncementsOnlyGroup(): JSX.Element { export function AnnouncementsOnlyGroup(): JSX.Element {
return ( return (
<CompositionArea <CompositionArea

View file

@ -101,13 +101,13 @@ export type OwnProps = Readonly<{
linkPreviewLoading: boolean; linkPreviewLoading: boolean;
linkPreviewResult?: LinkPreviewType; linkPreviewResult?: LinkPreviewType;
messageRequestsEnabled?: boolean; messageRequestsEnabled?: boolean;
onClearAttachments(): unknown; onClearAttachments(conversationId: string): unknown;
onCloseLinkPreview(): unknown; onCloseLinkPreview(): unknown;
processAttachments: (options: { processAttachments: (options: {
conversationId: string; conversationId: string;
files: ReadonlyArray<File>; files: ReadonlyArray<File>;
}) => unknown; }) => unknown;
onSelectMediaQuality(isHQ: boolean): unknown; setMediaQualitySetting(isHQ: boolean): unknown;
sendStickerMessage( sendStickerMessage(
id: string, id: string,
opts: { packId: string; stickerId: number } opts: { packId: string; stickerId: number }
@ -170,7 +170,7 @@ export type Props = Pick<
> & > &
MessageRequestActionsProps & MessageRequestActionsProps &
Pick<GroupV1DisabledActionsPropsType, 'showGV2MigrationDialog'> & Pick<GroupV1DisabledActionsPropsType, 'showGV2MigrationDialog'> &
Pick<GroupV2PendingApprovalActionsPropsType, 'onCancelJoinRequest'> & { Pick<GroupV2PendingApprovalActionsPropsType, 'cancelJoinRequest'> & {
pushPanelForConversation: PushPanelForConversationActionType; pushPanelForConversation: PushPanelForConversationActionType;
} & OwnProps; } & OwnProps;
@ -211,7 +211,7 @@ export function CompositionArea({
quotedMessageProps, quotedMessageProps,
scrollToMessage, scrollToMessage,
// MediaQualitySelector // MediaQualitySelector
onSelectMediaQuality, setMediaQualitySetting,
shouldSendHighQualityAttachments, shouldSendHighQualityAttachments,
// CompositionInput // CompositionInput
onEditorStateChange, onEditorStateChange,
@ -261,7 +261,7 @@ export function CompositionArea({
announcementsOnly, announcementsOnly,
areWeAdmin, areWeAdmin,
groupAdmins, groupAdmins,
onCancelJoinRequest, cancelJoinRequest,
showConversation, showConversation,
// SMS-only contacts // SMS-only contacts
isSMSOnly, isSMSOnly,
@ -393,7 +393,7 @@ export function CompositionArea({
<MediaQualitySelector <MediaQualitySelector
i18n={i18n} i18n={i18n}
isHighQuality={shouldSendHighQualityAttachments} isHighQuality={shouldSendHighQualityAttachments}
onSelectQuality={onSelectMediaQuality} onSelectQuality={setMediaQualitySetting}
/> />
</div> </div>
) : null} ) : null}
@ -592,8 +592,9 @@ export function CompositionArea({
if (areWePendingApproval) { if (areWePendingApproval) {
return ( return (
<GroupV2PendingApprovalActions <GroupV2PendingApprovalActions
cancelJoinRequest={cancelJoinRequest}
conversationId={conversationId}
i18n={i18n} i18n={i18n}
onCancelJoinRequest={onCancelJoinRequest}
/> />
); );
} }
@ -683,7 +684,7 @@ export function CompositionArea({
i18n={i18n} i18n={i18n}
onAddAttachment={launchAttachmentPicker} onAddAttachment={launchAttachmentPicker}
onClickAttachment={maybeEditAttachment} onClickAttachment={maybeEditAttachment}
onClose={onClearAttachments} onClose={() => onClearAttachments(conversationId)}
onCloseAttachment={attachment => { onCloseAttachment={attachment => {
if (attachment.path) { if (attachment.path) {
removeAttachment(conversationId, attachment.path); removeAttachment(conversationId, attachment.path);

View file

@ -68,7 +68,6 @@ const MESSAGE_DEFAULT_PROPS = {
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled, showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightbox: shouldNeverBeCalled, showLightbox: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled, showLightboxForViewOnceMedia: shouldNeverBeCalled,
showMessageDetail: shouldNeverBeCalled,
startConversation: shouldNeverBeCalled, startConversation: shouldNeverBeCalled,
theme: ThemeType.dark, theme: ThemeType.dark,
viewStory: shouldNeverBeCalled, viewStory: shouldNeverBeCalled,

View file

@ -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 {...defaultProps} />
);
_ToastConversationArchived.story = {
name: 'ToastConversationArchived',
};

View file

@ -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 (
<Toast
toastAction={{
label: i18n('conversationArchivedUndo'),
onClick: () => {
undo();
onClose();
},
}}
onClose={onClose}
>
{i18n('conversationArchived')}
</Toast>
);
}

View file

@ -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 {...defaultProps} />
);
_ToastConversationMarkedUnread.story = {
name: 'ToastConversationMarkedUnread',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('conversationMarkedUnread')}</Toast>;
}

View file

@ -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 {...defaultProps} />
);
_ToastConversationUnarchived.story = {
name: 'ToastConversationUnarchived',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('conversationReturnedToInbox')}</Toast>;
}

View file

@ -17,6 +17,8 @@ export default {
component: ToastManager, component: ToastManager,
argTypes: { argTypes: {
hideToast: { action: true }, hideToast: { action: true },
openFileInFolder: { action: true },
onUndoArchive: { action: true },
i18n: { i18n: {
defaultValue: 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({}); export const CopiedUsername = Template.bind({});
CopiedUsername.args = { CopiedUsername.args = {
toast: { toast: {
@ -182,6 +208,13 @@ MaxAttachments.args = {
}, },
}; };
export const OriginalMessageNotFound = Template.bind({});
OriginalMessageNotFound.args = {
toast: {
toastType: ToastType.OriginalMessageNotFound,
},
};
export const MessageBodyTooLong = Template.bind({}); export const MessageBodyTooLong = Template.bind({});
MessageBodyTooLong.args = { MessageBodyTooLong.args = {
toast: { toast: {

View file

@ -5,7 +5,6 @@ import React from 'react';
import type { LocalizerType, ReplacementValuesType } from '../types/Util'; import type { LocalizerType, ReplacementValuesType } from '../types/Util';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import { Toast } from './Toast'; import { Toast } from './Toast';
import { ToastMessageBodyTooLong } from './ToastMessageBodyTooLong';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { ToastType } from '../types/Toast'; import { ToastType } from '../types/Toast';
@ -13,6 +12,7 @@ export type PropsType = {
hideToast: () => unknown; hideToast: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
openFileInFolder: (target: string) => unknown; openFileInFolder: (target: string) => unknown;
onUndoArchive: (conversaetionId: string) => unknown;
toast?: { toast?: {
toastType: ToastType; toastType: ToastType;
parameters?: ReplacementValuesType; parameters?: ReplacementValuesType;
@ -25,6 +25,7 @@ export function ToastManager({
hideToast, hideToast,
i18n, i18n,
openFileInFolder, openFileInFolder,
onUndoArchive,
toast, toast,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
if (toast === undefined) { if (toast === undefined) {
@ -84,6 +85,36 @@ export function ToastManager({
); );
} }
if (toastType === ToastType.ConversationArchived) {
return (
<Toast
onClose={hideToast}
toastAction={{
label: i18n('conversationArchivedUndo'),
onClick: () => {
if (toast.parameters && 'conversationId' in toast.parameters) {
onUndoArchive(String(toast.parameters.conversationId));
}
},
}}
>
{i18n('conversationArchived')}
</Toast>
);
}
if (toastType === ToastType.ConversationMarkedUnread) {
return (
<Toast onClose={hideToast}>{i18n('conversationMarkedUnread')}</Toast>
);
}
if (toastType === ToastType.ConversationUnarchived) {
return (
<Toast onClose={hideToast}>{i18n('conversationReturnedToInbox')}</Toast>
);
}
if (toastType === ToastType.CopiedUsername) { if (toastType === ToastType.CopiedUsername) {
return ( return (
<Toast onClose={hideToast} timeout={3 * SECOND}> <Toast onClose={hideToast} timeout={3 * SECOND}>
@ -174,15 +205,11 @@ export function ToastManager({
} }
if (toastType === ToastType.MessageBodyTooLong) { if (toastType === ToastType.MessageBodyTooLong) {
return <ToastMessageBodyTooLong i18n={i18n} onClose={hideToast} />; return <Toast onClose={hideToast}>{i18n('messageBodyTooLong')}</Toast>;
} }
if (toastType === ToastType.ReportedSpamAndBlocked) { if (toastType === ToastType.OriginalMessageNotFound) {
return ( return <Toast onClose={hideToast}>{i18n('originalMessageNotFound')}</Toast>;
<Toast onClose={hideToast}>
{i18n('MessageRequests--block-and-report-spam-success-toast')}
</Toast>
);
} }
if (toastType === ToastType.PinnedConversationsFull) { if (toastType === ToastType.PinnedConversationsFull) {
@ -193,6 +220,14 @@ export function ToastManager({
return <Toast onClose={hideToast}>{i18n('Reactions--error')}</Toast>; return <Toast onClose={hideToast}>{i18n('Reactions--error')}</Toast>;
} }
if (toastType === ToastType.ReportedSpamAndBlocked) {
return (
<Toast onClose={hideToast}>
{i18n('MessageRequests--block-and-report-spam-success-toast')}
</Toast>
);
}
if (toastType === ToastType.StoryMuted) { if (toastType === ToastType.StoryMuted) {
return ( return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}> <Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>

View file

@ -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 {...defaultProps} />
);
_ToastMessageBodyTooLong.story = {
name: 'ToastMessageBodyTooLong',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('messageBodyTooLong')}</Toast>;
}

View file

@ -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 {...defaultProps} />
);
_ToastOriginalMessageNotFound.story = {
name: 'ToastOriginalMessageNotFound',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('originalMessageNotFound')}</Toast>;
}

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../util/setupI18n'; import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
@ -15,10 +14,5 @@ export default {
}; };
export function Default(): JSX.Element { export function Default(): JSX.Element {
return ( return <ChatSessionRefreshedNotification i18n={i18n} />;
<ChatSessionRefreshedNotification
contactSupport={action('contactSupport')}
i18n={i18n}
/>
);
} }

View file

@ -9,21 +9,19 @@ import type { LocalizerType } from '../../types/Util';
import { Button, ButtonSize, ButtonVariant } from '../Button'; import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage'; import { SystemMessage } from './SystemMessage';
import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog'; import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
import { mapToSupportLocale } from '../../util/mapToSupportLocale';
type PropsHousekeepingType = { type PropsHousekeepingType = {
i18n: LocalizerType; i18n: LocalizerType;
}; };
export type PropsActionsType = { export type PropsType = PropsHousekeepingType;
contactSupport: () => unknown;
};
export type PropsType = PropsHousekeepingType & PropsActionsType;
export function ChatSessionRefreshedNotification( export function ChatSessionRefreshedNotification(
props: PropsType props: PropsType
): ReactElement { ): ReactElement {
const { contactSupport, i18n } = props; const { i18n } = props;
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false); const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const openDialog = useCallback(() => { const openDialog = useCallback(() => {
@ -35,8 +33,15 @@ export function ChatSessionRefreshedNotification(
const wrappedContactSupport = useCallback(() => { const wrappedContactSupport = useCallback(() => {
setIsDialogOpen(false); 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 ( return (
<> <>

View file

@ -22,12 +22,13 @@ const getCommonProps = () => ({
acceptConversation: action('acceptConversation'), acceptConversation: action('acceptConversation'),
blockAndReportSpam: action('blockAndReportSpam'), blockAndReportSpam: action('blockAndReportSpam'),
blockConversation: action('blockConversation'), blockConversation: action('blockConversation'),
conversationId: 'some-conversation-id',
deleteConversation: action('deleteConversation'), deleteConversation: action('deleteConversation'),
getPreferredBadge: () => undefined, getPreferredBadge: () => undefined,
groupConversationId: 'convo-id', groupConversationId: 'convo-id',
i18n, i18n,
onClose: action('onClose'), onClose: action('onClose'),
onShowContactModal: action('onShowContactModal'), showContactModal: action('showContactModal'),
removeMember: action('removeMember'), removeMember: action('removeMember'),
theme: ThemeType.light, theme: ThemeType.light,
}); });

View file

@ -25,6 +25,7 @@ import { missingCaseError } from '../../util/missingCaseError';
import { isInSystemContacts } from '../../util/isInSystemContacts'; import { isInSystemContacts } from '../../util/isInSystemContacts';
export type PropsType = { export type PropsType = {
conversationId: string;
acceptConversation: (conversationId: string) => unknown; acceptConversation: (conversationId: string) => unknown;
blockAndReportSpam: (conversationId: string) => unknown; blockAndReportSpam: (conversationId: string) => unknown;
blockConversation: (conversationId: string) => unknown; blockConversation: (conversationId: string) => unknown;
@ -32,8 +33,11 @@ export type PropsType = {
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType; i18n: LocalizerType;
onClose: () => void; onClose: () => void;
onShowContactModal: (contactId: string, conversationId?: string) => unknown; showContactModal: (contactId: string, conversationId?: string) => unknown;
removeMember: (conversationId: string) => unknown; removeMember: (
conversationId: string,
memberConversationId: string
) => unknown;
theme: ThemeType; theme: ThemeType;
} & ( } & (
| { | {
@ -65,11 +69,12 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
acceptConversation, acceptConversation,
blockAndReportSpam, blockAndReportSpam,
blockConversation, blockConversation,
conversationId,
deleteConversation, deleteConversation,
getPreferredBadge, getPreferredBadge,
i18n, i18n,
onClose, onClose,
onShowContactModal, showContactModal,
removeMember, removeMember,
theme, theme,
} = props; } = props;
@ -150,7 +155,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
setConfirmationState(undefined); setConfirmationState(undefined);
}} }}
onRemove={() => { onRemove={() => {
removeMember(affectedConversation.id); removeMember(conversationId, affectedConversation.id);
}} }}
/> />
); );
@ -218,7 +223,7 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
onClick={() => { onClick={() => {
onShowContactModal(safeConversation.id); showContactModal(safeConversation.id);
}} }}
theme={theme} theme={theme}
/> />

View file

@ -39,7 +39,6 @@ const commonProps = {
setDisappearingMessages: action('setDisappearingMessages'), setDisappearingMessages: action('setDisappearingMessages'),
destroyMessages: action('destroyMessages'), destroyMessages: action('destroyMessages'),
onSearchInConversation: action('onSearchInConversation'),
onOutgoingAudioCallInConversation: action( onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation' 'onOutgoingAudioCallInConversation'
), ),
@ -47,12 +46,12 @@ const commonProps = {
'onOutgoingVideoCallInConversation' 'onOutgoingVideoCallInConversation'
), ),
onGoBack: action('onGoBack'),
onArchive: action('onArchive'), onArchive: action('onArchive'),
onMarkUnread: action('onMarkUnread'), onMarkUnread: action('onMarkUnread'),
onMoveToInbox: action('onMoveToInbox'), onMoveToInbox: action('onMoveToInbox'),
pushPanelForConversation: action('pushPanelForConversation'), pushPanelForConversation: action('pushPanelForConversation'),
popPanelForConversation: action('popPanelForConversation'),
searchInConversation: action('searchInConversation'),
setMuteExpiration: action('onSetMuteNotifications'), setMuteExpiration: action('onSetMuteNotifications'),
setPinned: action('setPinned'), setPinned: action('setPinned'),
viewUserStories: action('viewUserStories'), viewUserStories: action('viewUserStories'),

View file

@ -20,6 +20,7 @@ import { InContactsIcon } from '../InContactsIcon';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import type { import type {
ConversationType, ConversationType,
PopPanelForConversationActionType,
PushPanelForConversationActionType, PushPanelForConversationActionType,
} from '../../state/ducks/conversations'; } from '../../state/ducks/conversations';
import type { BadgeType } from '../../badges/types'; import type { BadgeType } from '../../badges/types';
@ -85,14 +86,14 @@ export type PropsDataType = {
export type PropsActionsType = { export type PropsActionsType = {
destroyMessages: (conversationId: string) => void; destroyMessages: (conversationId: string) => void;
onArchive: () => void; onArchive: (conversationId: string) => void;
onGoBack: () => void; onMarkUnread: (conversationId: string) => void;
onMarkUnread: () => void; onMoveToInbox: (conversationId: string) => void;
onMoveToInbox: () => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void; onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void; onOutgoingVideoCallInConversation: (conversationId: string) => void;
onSearchInConversation: () => void;
pushPanelForConversation: PushPanelForConversationActionType; pushPanelForConversation: PushPanelForConversationActionType;
popPanelForConversation: PopPanelForConversationActionType;
searchInConversation: (conversationId: string) => void;
setDisappearingMessages: ( setDisappearingMessages: (
conversationId: string, conversationId: string,
seconds: DurationInSeconds seconds: DurationInSeconds
@ -153,12 +154,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
} }
private renderBackButton(): ReactNode { private renderBackButton(): ReactNode {
const { i18n, onGoBack, showBackButton } = this.props; const { i18n, id, popPanelForConversation, showBackButton } = this.props;
return ( return (
<button <button
type="button" type="button"
onClick={onGoBack} onClick={() => popPanelForConversation(id)}
className={classNames( className={classNames(
'module-ConversationHeader__back-icon', 'module-ConversationHeader__back-icon',
showBackButton ? 'module-ConversationHeader__back-icon--show' : null showBackButton ? 'module-ConversationHeader__back-icon--show' : null
@ -314,12 +315,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
} }
private renderSearchButton(): ReactNode { private renderSearchButton(): ReactNode {
const { i18n, onSearchInConversation, showBackButton } = this.props; const { i18n, id, searchInConversation, showBackButton } = this.props;
return ( return (
<button <button
type="button" type="button"
onClick={onSearchInConversation} onClick={() => searchInConversation(id)}
className={classNames( className={classNames(
'module-ConversationHeader__button', 'module-ConversationHeader__button',
'module-ConversationHeader__button--search', 'module-ConversationHeader__button--search',
@ -501,14 +502,18 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
</MenuItem> </MenuItem>
<MenuItem divider /> <MenuItem divider />
{!markedUnread ? ( {!markedUnread ? (
<MenuItem onClick={onMarkUnread}>{i18n('markUnread')}</MenuItem> <MenuItem onClick={() => onMarkUnread(id)}>
{i18n('markUnread')}
</MenuItem>
) : null} ) : null}
{isArchived ? ( {isArchived ? (
<MenuItem onClick={onMoveToInbox}> <MenuItem onClick={() => onMoveToInbox(id)}>
{i18n('moveConversationToInbox')} {i18n('moveConversationToInbox')}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem> <MenuItem onClick={() => onArchive(id)}>
{i18n('archiveConversation')}
</MenuItem>
)} )}
<MenuItem <MenuItem
onClick={() => this.setState({ hasDeleteMessagesConfirmation: true })} onClick={() => this.setState({ hasDeleteMessagesConfirmation: true })}

View file

@ -134,6 +134,16 @@ DirectNoGroupsNoDataNotAccepted.story = {
name: 'Direct (No Groups, No Data, Not Accepted)', name: 'Direct (No Groups, No Data, Not Accepted)',
}; };
export const DirectNoGroupsNotAcceptedWithAvatar = Template.bind({});
DirectNoGroupsNotAcceptedWithAvatar.args = {
...getDefaultConversation(),
acceptedMessageRequest: false,
profileName: '',
};
DirectNoGroupsNotAcceptedWithAvatar.story = {
name: 'Direct (No Groups, No Data, Not Accepted, With Avatar)',
};
export const GroupManyMembers = Template.bind({}); export const GroupManyMembers = Template.bind({});
GroupManyMembers.args = { GroupManyMembers.args = {
conversationType: 'group', conversationType: 'group',

View file

@ -30,9 +30,9 @@ export type Props = {
name?: string; name?: string;
phoneNumber?: string; phoneNumber?: string;
sharedGroupNames?: Array<string>; sharedGroupNames?: Array<string>;
unblurAvatar: () => void; unblurAvatar: (conversationId: string) => void;
unblurredAvatarPath?: string; unblurredAvatarPath?: string;
updateSharedGroups: () => unknown; updateSharedGroups: (conversationId: string) => unknown;
theme: ThemeType; theme: ThemeType;
viewUserStories: ViewUserStoriesActionCreatorType; viewUserStories: ViewUserStoriesActionCreatorType;
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>; } & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
@ -133,8 +133,8 @@ export function ConversationHero({
useEffect(() => { useEffect(() => {
// Kick off the expensive hydration of the current sharedGroupNames // Kick off the expensive hydration of the current sharedGroupNames
updateSharedGroups(); updateSharedGroups(id);
}, [updateSharedGroups]); }, [id, updateSharedGroups]);
let avatarBlur: AvatarBlur = AvatarBlur.NoBlur; let avatarBlur: AvatarBlur = AvatarBlur.NoBlur;
let avatarOnClick: undefined | (() => void); let avatarOnClick: undefined | (() => void);
@ -148,7 +148,7 @@ export function ConversationHero({
}) })
) { ) {
avatarBlur = AvatarBlur.BlurPictureWithClickToView; avatarBlur = AvatarBlur.BlurPictureWithClickToView;
avatarOnClick = unblurAvatar; avatarOnClick = () => unblurAvatar(id);
} else if (hasStories) { } else if (hasStories) {
avatarOnClick = () => { avatarOnClick = () => {
viewUserStories({ viewUserStories({

View file

@ -22,7 +22,6 @@ export function Default(): JSX.Element {
i18n={i18n} i18n={i18n}
sender={sender} sender={sender}
inGroup={false} inGroup={false}
learnMoreAboutDeliveryIssue={action('learnMoreAboutDeliveryIssue')}
onClose={action('onClose')} onClose={action('onClose')}
/> />
); );
@ -34,7 +33,6 @@ export function InGroup(): JSX.Element {
i18n={i18n} i18n={i18n}
sender={sender} sender={sender}
inGroup inGroup
learnMoreAboutDeliveryIssue={action('learnMoreAboutDeliveryIssue')}
onClose={action('onClose')} onClose={action('onClose')}
/> />
); );

View file

@ -12,17 +12,17 @@ import { Emojify } from './Emojify';
import { useRestoreFocus } from '../../hooks/useRestoreFocus'; import { useRestoreFocus } from '../../hooks/useRestoreFocus';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
export type PropsType = { export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
sender: ConversationType; sender: ConversationType;
inGroup: boolean; inGroup: boolean;
learnMoreAboutDeliveryIssue: () => unknown;
onClose: () => unknown; onClose: () => unknown;
}; };
export function DeliveryIssueDialog(props: PropsType): React.ReactElement { export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
const { i18n, inGroup, learnMoreAboutDeliveryIssue, sender, onClose } = props; const { i18n, inGroup, sender, onClose } = props;
const key = inGroup const key = inGroup
? 'DeliveryIssue--summary--group' ? 'DeliveryIssue--summary--group'
@ -34,7 +34,11 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
const footer = ( const footer = (
<> <>
<Button <Button
onClick={learnMoreAboutDeliveryIssue} onClick={() =>
openLinkInWebBrowser(
'https://support.signal.org/hc/articles/4404859745690'
)
}
size={ButtonSize.Medium} size={ButtonSize.Medium}
variant={ButtonVariant.Secondary} variant={ButtonVariant.Secondary}
> >

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../util/setupI18n'; import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
@ -18,12 +17,7 @@ const sender = getDefaultConversation();
export function Default(): JSX.Element { export function Default(): JSX.Element {
return ( return (
<DeliveryIssueNotification <DeliveryIssueNotification i18n={i18n} inGroup={false} sender={sender} />
i18n={i18n}
inGroup={false}
learnMoreAboutDeliveryIssue={action('learnMoreAboutDeliveryIssue')}
sender={sender}
/>
); );
} }
@ -33,7 +27,6 @@ export function WithALongName(): JSX.Element {
<DeliveryIssueNotification <DeliveryIssueNotification
i18n={i18n} i18n={i18n}
inGroup={false} inGroup={false}
learnMoreAboutDeliveryIssue={action('learnMoreAboutDeliveryIssue')}
sender={getDefaultConversation({ sender={getDefaultConversation({
firstName: longName, firstName: longName,
name: longName, name: longName,
@ -49,12 +42,5 @@ WithALongName.story = {
}; };
export function InGroup(): JSX.Element { export function InGroup(): JSX.Element {
return ( return <DeliveryIssueNotification i18n={i18n} inGroup sender={sender} />;
<DeliveryIssueNotification
i18n={i18n}
inGroup
learnMoreAboutDeliveryIssue={action('learnMoreAboutDeliveryIssue')}
sender={sender}
/>
);
} }

View file

@ -18,22 +18,16 @@ export type PropsDataType = {
inGroup: boolean; inGroup: boolean;
}; };
export type PropsActionsType = {
learnMoreAboutDeliveryIssue: () => unknown;
};
type PropsHousekeepingType = { type PropsHousekeepingType = {
i18n: LocalizerType; i18n: LocalizerType;
}; };
export type PropsType = PropsDataType & export type PropsType = PropsDataType & PropsHousekeepingType;
PropsActionsType &
PropsHousekeepingType;
export function DeliveryIssueNotification( export function DeliveryIssueNotification(
props: PropsType props: PropsType
): ReactElement | null { ): ReactElement | null {
const { i18n, inGroup, sender, learnMoreAboutDeliveryIssue } = props; const { i18n, inGroup, sender } = props;
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false); const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const openDialog = useCallback(() => { const openDialog = useCallback(() => {
@ -74,7 +68,6 @@ export function DeliveryIssueNotification(
<DeliveryIssueDialog <DeliveryIssueDialog
i18n={i18n} i18n={i18n}
inGroup={inGroup} inGroup={inGroup}
learnMoreAboutDeliveryIssue={learnMoreAboutDeliveryIssue}
sender={sender} sender={sender}
onClose={closeDialog} onClose={closeDialog}
/> />

View file

@ -55,6 +55,7 @@ const renderChange = (
<GroupV2Change <GroupV2Change
areWeAdmin={areWeAdmin ?? true} areWeAdmin={areWeAdmin ?? true}
blockGroupLinkRequests={action('blockGroupLinkRequests')} blockGroupLinkRequests={action('blockGroupLinkRequests')}
conversationId="some-conversation-id"
change={change} change={change}
groupBannedMemberships={groupBannedMemberships} groupBannedMemberships={groupBannedMemberships}
groupMemberships={groupMemberships} groupMemberships={groupMemberships}

View file

@ -24,6 +24,7 @@ import { ConfirmationDialog } from '../ConfirmationDialog';
export type PropsDataType = { export type PropsDataType = {
areWeAdmin: boolean; areWeAdmin: boolean;
conversationId: string;
groupMemberships?: Array<{ groupMemberships?: Array<{
uuid: UUIDStringType; uuid: UUIDStringType;
isAdmin: boolean; isAdmin: boolean;
@ -36,7 +37,10 @@ export type PropsDataType = {
}; };
export type PropsActionsType = { export type PropsActionsType = {
blockGroupLinkRequests: (uuid: UUIDStringType) => unknown; blockGroupLinkRequests: (
conversationId: string,
uuid: UUIDStringType
) => unknown;
}; };
export type PropsHousekeepingType = { export type PropsHousekeepingType = {
@ -130,6 +134,7 @@ function getIcon(
function GroupV2Detail({ function GroupV2Detail({
areWeAdmin, areWeAdmin,
blockGroupLinkRequests, blockGroupLinkRequests,
conversationId,
detail, detail,
isLastText, isLastText,
fromId, fromId,
@ -143,7 +148,11 @@ function GroupV2Detail({
text, text,
}: { }: {
areWeAdmin: boolean; areWeAdmin: boolean;
blockGroupLinkRequests: (uuid: UUIDStringType) => unknown; blockGroupLinkRequests: (
conversationId: string,
uuid: UUIDStringType
) => unknown;
conversationId: string;
detail: GroupV2ChangeDetailType; detail: GroupV2ChangeDetailType;
isLastText: boolean; isLastText: boolean;
groupMemberships?: Array<{ groupMemberships?: Array<{
@ -209,7 +218,7 @@ function GroupV2Detail({
title={i18n('PendingRequests--block--title')} title={i18n('PendingRequests--block--title')}
actions={[ actions={[
{ {
action: () => blockGroupLinkRequests(detail.uuid), action: () => blockGroupLinkRequests(conversationId, detail.uuid),
text: i18n('PendingRequests--block--confirm'), text: i18n('PendingRequests--block--confirm'),
style: 'affirmative', style: 'affirmative',
}, },
@ -282,6 +291,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
areWeAdmin, areWeAdmin,
blockGroupLinkRequests, blockGroupLinkRequests,
change, change,
conversationId,
groupBannedMemberships, groupBannedMemberships,
groupMemberships, groupMemberships,
groupName, groupName,
@ -304,6 +314,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
<GroupV2Detail <GroupV2Detail
areWeAdmin={areWeAdmin} areWeAdmin={areWeAdmin}
blockGroupLinkRequests={blockGroupLinkRequests} blockGroupLinkRequests={blockGroupLinkRequests}
conversationId={conversationId}
detail={detail} detail={detail}
isLastText={isLastText} isLastText={isLastText}
fromId={change.from} fromId={change.from}

View file

@ -12,8 +12,9 @@ import enMessages from '../../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const createProps = (): GroupV2PendingApprovalActionsPropsType => ({ const createProps = (): GroupV2PendingApprovalActionsPropsType => ({
cancelJoinRequest: action('cancelJoinRequest'),
conversationId: 'some-random-id',
i18n, i18n,
onCancelJoinRequest: action('onCancelJoinRequest'),
}); });
export default { export default {

View file

@ -3,16 +3,21 @@
import * as React from 'react'; import * as React from 'react';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { ConfirmationDialog } from '../ConfirmationDialog';
export type PropsType = { export type PropsType = {
conversationId: string;
i18n: LocalizerType; i18n: LocalizerType;
onCancelJoinRequest: () => unknown; cancelJoinRequest: (conversationId: string) => unknown;
}; };
export function GroupV2PendingApprovalActions({ export function GroupV2PendingApprovalActions({
cancelJoinRequest,
conversationId,
i18n, i18n,
onCancelJoinRequest,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const [isConfirming, setIsConfirming] = React.useState(false);
return ( return (
<div className="module-group-v2-pending-approval-actions"> <div className="module-group-v2-pending-approval-actions">
<p className="module-group-v2-pending-approval-actions__message"> <p className="module-group-v2-pending-approval-actions__message">
@ -21,13 +26,30 @@ export function GroupV2PendingApprovalActions({
<div className="module-group-v2-pending-approval-actions__buttons"> <div className="module-group-v2-pending-approval-actions__buttons">
<button <button
type="button" type="button"
onClick={onCancelJoinRequest} onClick={() => setIsConfirming(true)}
tabIndex={0} tabIndex={0}
className="module-group-v2-pending-approval-actions__buttons__button" className="module-group-v2-pending-approval-actions__buttons__button"
> >
{i18n('GroupV2--join--cancel-request-to-join')} {i18n('GroupV2--join--cancel-request-to-join')}
</button> </button>
</div> </div>
{isConfirming ? (
<ConfirmationDialog
actions={[
{
text: i18n('GroupV2--join--cancel-request-to-join--yes'),
style: 'negative',
action: () => 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')}
</ConfirmationDialog>
) : undefined}
</div> </div>
); );
} }

View file

@ -167,7 +167,6 @@ export type AudioAttachmentProps = {
id: string; id: string;
conversationId: string; conversationId: string;
played: boolean; played: boolean;
showMessageDetail: (id: string) => void;
status?: MessageStatusType; status?: MessageStatusType;
textPending?: boolean; textPending?: boolean;
timestamp: number; timestamp: number;
@ -302,8 +301,6 @@ export type PropsActions = {
messageExpanded: (id: string, displayLimit: number) => unknown; messageExpanded: (id: string, displayLimit: number) => unknown;
checkForAccount: (phoneNumber: string) => unknown; checkForAccount: (phoneNumber: string) => unknown;
showMessageDetail: (id: string) => void;
startConversation: (e164: string, uuid: UUIDStringType) => void; startConversation: (e164: string, uuid: UUIDStringType) => void;
showConversation: ShowConversationType; showConversation: ShowConversationType;
openGiftBadge: (messageId: string) => void; openGiftBadge: (messageId: string) => void;
@ -327,6 +324,7 @@ export type PropsActions = {
scrollToQuotedMessage: (options: { scrollToQuotedMessage: (options: {
authorId: string; authorId: string;
conversationId: string;
sentAt: number; sentAt: number;
}) => void; }) => void;
selectMessage?: (messageId: string, conversationId: string) => unknown; selectMessage?: (messageId: string, conversationId: string) => unknown;
@ -752,6 +750,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
const { const {
conversationId,
deletedForEveryone, deletedForEveryone,
direction, direction,
expirationLength, expirationLength,
@ -760,17 +759,18 @@ export class Message extends React.PureComponent<Props, State> {
isTapToViewExpired, isTapToViewExpired,
status, status,
i18n, i18n,
pushPanelForConversation,
text, text,
textAttachment, textAttachment,
timestamp, timestamp,
id, id,
showMessageDetail,
} = this.props; } = this.props;
const isStickerLike = isSticker || this.canRenderStickerLikeEmoji(); const isStickerLike = isSticker || this.canRenderStickerLikeEmoji();
return ( return (
<MessageMetadata <MessageMetadata
conversationId={conversationId}
deletedForEveryone={deletedForEveryone} deletedForEveryone={deletedForEveryone}
direction={direction} direction={direction}
expirationLength={expirationLength} expirationLength={expirationLength}
@ -783,7 +783,7 @@ export class Message extends React.PureComponent<Props, State> {
isSticker={isStickerLike} isSticker={isStickerLike}
isTapToViewExpired={isTapToViewExpired} isTapToViewExpired={isTapToViewExpired}
onWidthMeasured={isInline ? this.updateMetadataWidth : undefined} onWidthMeasured={isInline ? this.updateMetadataWidth : undefined}
showMessageDetail={showMessageDetail} pushPanelForConversation={pushPanelForConversation}
status={status} status={status}
textPending={textAttachment?.pending} textPending={textAttachment?.pending}
timestamp={timestamp} timestamp={timestamp}
@ -841,7 +841,6 @@ export class Message extends React.PureComponent<Props, State> {
reducedMotion, reducedMotion,
renderAudioAttachment, renderAudioAttachment,
renderingContext, renderingContext,
showMessageDetail,
showLightbox, showLightbox,
shouldCollapseAbove, shouldCollapseAbove,
shouldCollapseBelow, shouldCollapseBelow,
@ -967,7 +966,6 @@ export class Message extends React.PureComponent<Props, State> {
id, id,
conversationId, conversationId,
played, played,
showMessageDetail,
status, status,
textPending: textAttachment?.pending, textPending: textAttachment?.pending,
timestamp, timestamp,
@ -1453,6 +1451,7 @@ export class Message extends React.PureComponent<Props, State> {
public renderQuote(): JSX.Element | null { public renderQuote(): JSX.Element | null {
const { const {
conversationColor, conversationColor,
conversationId,
conversationTitle, conversationTitle,
customColor, customColor,
direction, direction,
@ -1475,6 +1474,7 @@ export class Message extends React.PureComponent<Props, State> {
: () => { : () => {
scrollToQuotedMessage({ scrollToQuotedMessage({
authorId: quote.authorId, authorId: quote.authorId,
conversationId,
sentAt: quote.sentAt, sentAt: quote.sentAt,
}); });
}; };

View file

@ -16,6 +16,7 @@ import type { ComputePeaksResult } from '../GlobalAudioContext';
import { MessageMetadata } from './MessageMetadata'; import { MessageMetadata } from './MessageMetadata';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer'; import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer';
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
export type OwnProps = Readonly<{ export type OwnProps = Readonly<{
active: ActiveAudioPlayerStateType | undefined; active: ActiveAudioPlayerStateType | undefined;
@ -34,7 +35,6 @@ export type OwnProps = Readonly<{
id: string; id: string;
conversationId: string; conversationId: string;
played: boolean; played: boolean;
showMessageDetail: (id: string) => void;
status?: MessageStatusType; status?: MessageStatusType;
textPending?: boolean; textPending?: boolean;
timestamp: number; timestamp: number;
@ -51,6 +51,7 @@ export type DispatchProps = Readonly<{
position: number, position: number,
isConsecutive: boolean isConsecutive: boolean
) => void; ) => void;
pushPanelForConversation: PushPanelForConversationActionType;
setCurrentTime: (currentTime: number) => void; setCurrentTime: (currentTime: number) => void;
setPlaybackRate: (conversationId: string, rate: number) => void; setPlaybackRate: (conversationId: string, rate: number) => void;
setIsPlaying: (value: boolean) => void; setIsPlaying: (value: boolean) => void;
@ -263,7 +264,6 @@ export function MessageAudio(props: Props): JSX.Element {
expirationTimestamp, expirationTimestamp,
id, id,
played, played,
showMessageDetail,
status, status,
textPending, textPending,
timestamp, timestamp,
@ -273,6 +273,7 @@ export function MessageAudio(props: Props): JSX.Element {
computePeaks, computePeaks,
setPlaybackRate, setPlaybackRate,
loadAndPlayMessageAudio, loadAndPlayMessageAudio,
pushPanelForConversation,
setCurrentTime, setCurrentTime,
setIsPlaying, setIsPlaying,
} = props; } = props;
@ -591,6 +592,7 @@ export function MessageAudio(props: Props): JSX.Element {
{!withContentBelow && !collapseMetadata && ( {!withContentBelow && !collapseMetadata && (
<MessageMetadata <MessageMetadata
conversationId={conversationId}
direction={direction} direction={direction}
expirationLength={expirationLength} expirationLength={expirationLength}
expirationTimestamp={expirationTimestamp} expirationTimestamp={expirationTimestamp}
@ -600,7 +602,7 @@ export function MessageAudio(props: Props): JSX.Element {
isShowingImage={false} isShowingImage={false}
isSticker={false} isSticker={false}
isTapToViewExpired={false} isTapToViewExpired={false}
showMessageDetail={showMessageDetail} pushPanelForConversation={pushPanelForConversation}
status={status} status={status}
textPending={textPending} textPending={textPending}
timestamp={timestamp} timestamp={timestamp}

View file

@ -68,18 +68,9 @@ export type PropsData = {
i18n: LocalizerType; i18n: LocalizerType;
theme: ThemeType; theme: ThemeType;
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
} & Pick< } & Pick<MessagePropsType, 'getPreferredBadge' | 'interactionMode'>;
MessagePropsType,
| 'getPreferredBadge'
| 'interactionMode'
| 'expirationLength'
| 'expirationTimestamp'
>;
export type PropsBackboneActions = Pick< export type PropsSmartActions = Pick<MessagePropsType, 'renderAudioAttachment'>;
MessagePropsType,
'renderAudioAttachment' | 'startConversation'
>;
export type PropsReduxActions = Pick< export type PropsReduxActions = Pick<
MessagePropsType, MessagePropsType,
@ -97,13 +88,13 @@ export type PropsReduxActions = Pick<
| 'showExpiredOutgoingTapToViewToast' | 'showExpiredOutgoingTapToViewToast'
| 'showLightbox' | 'showLightbox'
| 'showLightboxForViewOnceMedia' | 'showLightboxForViewOnceMedia'
| 'startConversation'
| 'viewStory' | 'viewStory'
> & { > & {
toggleSafetyNumberModal: (contactId: string) => void; toggleSafetyNumberModal: (contactId: string) => void;
}; };
export type ExternalProps = PropsData & PropsBackboneActions; export type Props = PropsData & PropsSmartActions & PropsReduxActions;
export type Props = PropsData & PropsBackboneActions & PropsReduxActions;
const contactSortCollator = new Intl.Collator(); const contactSortCollator = new Intl.Collator();
@ -280,7 +271,6 @@ export class MessageDetail extends React.Component<Props> {
contactNameColor, contactNameColor,
showLightboxForViewOnceMedia, showLightboxForViewOnceMedia,
doubleCheckMissingQuoteReference, doubleCheckMissingQuoteReference,
expirationTimestamp,
getPreferredBadge, getPreferredBadge,
i18n, i18n,
interactionMode, interactionMode,
@ -300,8 +290,8 @@ export class MessageDetail extends React.Component<Props> {
viewStory, viewStory,
} = this.props; } = this.props;
const timeRemaining = expirationTimestamp const timeRemaining = message.expirationTimestamp
? DurationInSeconds.fromMillis(expirationTimestamp - Date.now()) ? DurationInSeconds.fromMillis(message.expirationTimestamp - Date.now())
: undefined; : undefined;
return ( return (
@ -348,9 +338,6 @@ export class MessageDetail extends React.Component<Props> {
showExpiredOutgoingTapToViewToast={ showExpiredOutgoingTapToViewToast={
showExpiredOutgoingTapToViewToast showExpiredOutgoingTapToViewToast
} }
showMessageDetail={() => {
log.warn('MessageDetail: showMessageDetail called!');
}}
showLightbox={showLightbox} showLightbox={showLightbox}
startConversation={startConversation} startConversation={startConversation}
theme={theme} theme={theme}

View file

@ -11,8 +11,11 @@ import type { DirectionType, MessageStatusType } from './Message';
import { ExpireTimer } from './ExpireTimer'; import { ExpireTimer } from './ExpireTimer';
import { MessageTimestamp } from './MessageTimestamp'; import { MessageTimestamp } from './MessageTimestamp';
import { Spinner } from '../Spinner'; import { Spinner } from '../Spinner';
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
import { PanelType } from '../../types/Panels';
type PropsType = { type PropsType = {
conversationId: string;
deletedForEveryone?: boolean; deletedForEveryone?: boolean;
direction: DirectionType; direction: DirectionType;
expirationLength?: number; expirationLength?: number;
@ -25,13 +28,14 @@ type PropsType = {
isSticker?: boolean; isSticker?: boolean;
isTapToViewExpired?: boolean; isTapToViewExpired?: boolean;
onWidthMeasured?: (width: number) => unknown; onWidthMeasured?: (width: number) => unknown;
showMessageDetail: (id: string) => void; pushPanelForConversation: PushPanelForConversationActionType;
status?: MessageStatusType; status?: MessageStatusType;
textPending?: boolean; textPending?: boolean;
timestamp: number; timestamp: number;
}; };
export function MessageMetadata({ export function MessageMetadata({
conversationId,
deletedForEveryone, deletedForEveryone,
direction, direction,
expirationLength, expirationLength,
@ -44,7 +48,7 @@ export function MessageMetadata({
isSticker, isSticker,
isTapToViewExpired, isTapToViewExpired,
onWidthMeasured, onWidthMeasured,
showMessageDetail, pushPanelForConversation,
status, status,
textPending, textPending,
timestamp, timestamp,
@ -76,7 +80,10 @@ export function MessageMetadata({
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
showMessageDetail(id); pushPanelForConversation(conversationId, {
type: PanelType.MessageDetails,
args: { messageId: id },
});
}} }}
> >
{deletedForEveryone {deletedForEveryone

View file

@ -137,7 +137,6 @@ const defaultMessageProps: TimelineMessagesProps = {
'showExpiredOutgoingTapToViewToast' 'showExpiredOutgoingTapToViewToast'
), ),
toggleForwardMessageModal: action('default--toggleForwardMessageModal'), toggleForwardMessageModal: action('default--toggleForwardMessageModal'),
showMessageDetail: action('default--showMessageDetail'),
showLightbox: action('default--showLightbox'), showLightbox: action('default--showLightbox'),
startConversation: action('default--startConversation'), startConversation: action('default--startConversation'),
status: 'sent', status: 'sent',

View file

@ -266,7 +266,6 @@ const actions = () => ({
'clearInvitedUuidsForNewlyCreatedGroup' 'clearInvitedUuidsForNewlyCreatedGroup'
), ),
setIsNearBottom: action('setIsNearBottom'), setIsNearBottom: action('setIsNearBottom'),
learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'),
loadOlderMessages: action('loadOlderMessages'), loadOlderMessages: action('loadOlderMessages'),
loadNewerMessages: action('loadNewerMessages'), loadNewerMessages: action('loadNewerMessages'),
loadNewestMessages: action('loadNewestMessages'), loadNewestMessages: action('loadNewestMessages'),
@ -281,7 +280,6 @@ const actions = () => ({
retryMessageSend: action('retryMessageSend'), retryMessageSend: action('retryMessageSend'),
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'), deleteMessageForEveryone: action('deleteMessageForEveryone'),
showMessageDetail: action('showMessageDetail'),
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),
pushPanelForConversation: action('pushPanelForConversation'), pushPanelForConversation: action('pushPanelForConversation'),
showContactDetail: action('showContactDetail'), showContactDetail: action('showContactDetail'),
@ -310,20 +308,12 @@ const actions = () => ({
startConversation: action('startConversation'), startConversation: action('startConversation'),
returnToActiveCall: action('returnToActiveCall'), returnToActiveCall: action('returnToActiveCall'),
contactSupport: action('contactSupport'),
closeContactSpoofingReview: action('closeContactSpoofingReview'), closeContactSpoofingReview: action('closeContactSpoofingReview'),
reviewGroupMemberNameCollision: action('reviewGroupMemberNameCollision'), reviewGroupMemberNameCollision: action('reviewGroupMemberNameCollision'),
reviewMessageRequestNameCollision: action( reviewMessageRequestNameCollision: action(
'reviewMessageRequestNameCollision' 'reviewMessageRequestNameCollision'
), ),
acceptConversation: action('acceptConversation'),
blockAndReportSpam: action('blockAndReportSpam'),
blockConversation: action('blockConversation'),
deleteConversation: action('deleteConversation'),
removeMember: action('removeMember'),
unblurAvatar: action('unblurAvatar'), unblurAvatar: action('unblurAvatar'),
peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'), peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'),
@ -371,10 +361,23 @@ const renderItem = ({
const renderContactSpoofingReviewDialog = ( const renderContactSpoofingReviewDialog = (
props: SmartContactSpoofingReviewDialogPropsType 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) { if (props.type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
return ( return (
<ContactSpoofingReviewDialog <ContactSpoofingReviewDialog
{...props} {...props}
{...sharedProps}
group={{ group={{
...getDefaultConversation(), ...getDefaultConversation(),
areWeAdmin: true, areWeAdmin: true,
@ -383,7 +386,7 @@ const renderContactSpoofingReviewDialog = (
); );
} }
return <ContactSpoofingReviewDialog {...props} />; return <ContactSpoofingReviewDialog {...props} {...sharedProps} />;
}; };
const getAbout = () => text('about', '👍 Free to chat'); const getAbout = () => text('about', '👍 Free to chat');

View file

@ -1,16 +1,15 @@
// Copyright 2019-2022 Signal Messenger, LLC // Copyright 2019-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 classNames from 'classnames';
import type { ReactChild, ReactNode, RefObject } from 'react'; import type { ReactChild, ReactNode, RefObject } from 'react';
import React from 'react'; import React from 'react';
import { createSelector } from 'reselect';
import Measure from 'react-measure'; import Measure from 'react-measure';
import { ScrollDownButton } from './ScrollDownButton'; 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 { ConversationType } from '../../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
import { assertDev, strictAssert } from '../../util/assert'; import { assertDev, strictAssert } from '../../util/assert';
@ -18,8 +17,6 @@ import { missingCaseError } from '../../util/missingCaseError';
import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
import { WidthBreakpoint } from '../_util'; import { WidthBreakpoint } from '../_util';
import type { PropsActions as MessageActionsType } from './TimelineMessage';
import type { PropsActionsType as ChatSessionRefreshedNotificationActionsType } from './ChatSessionRefreshedNotification';
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from './ErrorBoundary';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
import { TimelineWarning } from './TimelineWarning'; import { TimelineWarning } from './TimelineWarning';
@ -44,8 +41,6 @@ import {
} from '../../util/scrollUtil'; } from '../../util/scrollUtil';
import { LastSeenIndicator } from './LastSeenIndicator'; import { LastSeenIndicator } from './LastSeenIndicator';
import { MINUTE } from '../../util/durations'; 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_THRESHOLD = 15;
const AT_BOTTOM_DETECTOR_STYLE = { height: AT_BOTTOM_THRESHOLD }; const AT_BOTTOM_DETECTOR_STYLE = { height: AT_BOTTOM_THRESHOLD };
@ -124,7 +119,6 @@ type PropsHousekeepingType = {
theme: ThemeType; theme: ThemeType;
renderItem: (props: { renderItem: (props: {
actionProps: PropsActionsFromBackboneForChildrenType;
containerElementRef: RefObject<HTMLElement>; containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint; containerWidthBreakpoint: WidthBreakpoint;
conversationId: string; conversationId: string;
@ -134,46 +128,31 @@ type PropsHousekeepingType = {
previousMessageId: undefined | string; previousMessageId: undefined | string;
unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement; unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement;
}) => JSX.Element; }) => JSX.Element;
renderHeroRow: ( renderHeroRow: (id: string) => JSX.Element;
id: string,
unblurAvatar: () => void,
updateSharedGroups: () => unknown
) => JSX.Element;
renderTypingBubble: (id: string) => JSX.Element; renderTypingBubble: (id: string) => JSX.Element;
renderContactSpoofingReviewDialog: ( renderContactSpoofingReviewDialog: (
props: SmartContactSpoofingReviewDialogPropsType props: SmartContactSpoofingReviewDialogPropsType
) => JSX.Element; ) => JSX.Element;
}; };
export type PropsActionsFromBackboneForChildrenType = Pick<
MessageActionsType,
'scrollToQuotedMessage' | 'showMessageDetail' | 'startConversation'
> &
ChatSessionRefreshedNotificationActionsType &
DeliveryIssueNotificationActionsType &
GroupV2ChangeActionsType;
export type PropsActionsType = { export type PropsActionsType = {
// From Backbone // From Backbone
acknowledgeGroupMemberNameCollisions: ( acknowledgeGroupMemberNameCollisions: (
conversationId: string,
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle> groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
) => void; ) => 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; clearInvitedUuidsForNewlyCreatedGroup: () => void;
clearSelectedMessage: () => unknown; clearSelectedMessage: () => unknown;
closeContactSpoofingReview: () => void; 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; setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
peekGroupCallForTheFirstTime: (conversationId: string) => unknown; peekGroupCallForTheFirstTime: (conversationId: string) => unknown;
peekGroupCallIfItHasMembers: (conversationId: string) => unknown; peekGroupCallIfItHasMembers: (conversationId: string) => unknown;
@ -183,9 +162,7 @@ export type PropsActionsType = {
safeConversationId: string; safeConversationId: string;
}> }>
) => void; ) => void;
selectMessage: (messageId: string, conversationId: string) => unknown; };
showContactModal: (contactId: string, conversationId?: string) => void;
} & PropsActionsFromBackboneForChildrenType;
export type PropsType = PropsDataType & export type PropsType = PropsDataType &
PropsHousekeepingType & PropsHousekeepingType &
@ -209,39 +186,6 @@ type SnapshotType =
| { scrollTop: number } | { scrollTop: number }
| { scrollBottom: 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< export class Timeline extends React.Component<
PropsType, PropsType,
StateType, StateType,
@ -348,7 +292,7 @@ export class Timeline extends React.Component<
} else { } else {
const lastId = last(items); const lastId = last(items);
if (lastId) { if (lastId) {
loadNewestMessages(lastId, setFocus); loadNewestMessages(id, lastId, setFocus);
} }
} }
}; };
@ -472,7 +416,7 @@ export class Timeline extends React.Component<
maxRowIndex >= 0 && maxRowIndex >= 0 &&
rowIndex >= maxRowIndex - LOAD_NEWER_THRESHOLD rowIndex >= maxRowIndex - LOAD_NEWER_THRESHOLD
) { ) {
loadNewerMessages(newestBottomVisibleMessageId); loadNewerMessages(id, newestBottomVisibleMessageId);
} }
} }
@ -482,7 +426,7 @@ export class Timeline extends React.Component<
oldestPartiallyVisibleMessageId && oldestPartiallyVisibleMessageId &&
oldestPartiallyVisibleMessageId === items[0] oldestPartiallyVisibleMessageId === items[0]
) { ) {
loadOlderMessages(oldestPartiallyVisibleMessageId); loadOlderMessages(id, oldestPartiallyVisibleMessageId);
} }
}; };
@ -522,10 +466,10 @@ export class Timeline extends React.Component<
} }
private markNewestBottomVisibleMessageRead = throttle((): void => { private markNewestBottomVisibleMessageRead = throttle((): void => {
const { markMessageRead } = this.props; const { id, markMessageRead } = this.props;
const { newestBottomVisibleMessageId } = this.state; const { newestBottomVisibleMessageId } = this.state;
if (newestBottomVisibleMessageId) { if (newestBottomVisibleMessageId) {
markMessageRead(newestBottomVisibleMessageId); markMessageRead(id, newestBottomVisibleMessageId);
} }
}, 500); }, 500);
@ -792,14 +736,10 @@ export class Timeline extends React.Component<
public override render(): JSX.Element | null { public override render(): JSX.Element | null {
const { const {
acceptConversation,
acknowledgeGroupMemberNameCollisions, acknowledgeGroupMemberNameCollisions,
blockAndReportSpam,
blockConversation,
clearInvitedUuidsForNewlyCreatedGroup, clearInvitedUuidsForNewlyCreatedGroup,
closeContactSpoofingReview, closeContactSpoofingReview,
contactSpoofingReview, contactSpoofingReview,
deleteConversation,
getPreferredBadge, getPreferredBadge,
getTimestampForMessage, getTimestampForMessage,
haveNewest, haveNewest,
@ -813,19 +753,15 @@ export class Timeline extends React.Component<
items, items,
messageLoadingState, messageLoadingState,
oldestUnseenIndex, oldestUnseenIndex,
removeMember,
renderContactSpoofingReviewDialog, renderContactSpoofingReviewDialog,
renderHeroRow, renderHeroRow,
renderItem, renderItem,
renderTypingBubble, renderTypingBubble,
reviewGroupMemberNameCollision, reviewGroupMemberNameCollision,
reviewMessageRequestNameCollision, reviewMessageRequestNameCollision,
showContactModal,
theme, theme,
totalUnseen, totalUnseen,
unblurAvatar,
unreadCount, unreadCount,
updateSharedGroups,
} = this.props; } = this.props;
const { const {
hasRecentlyScrolled, hasRecentlyScrolled,
@ -866,8 +802,6 @@ export class Timeline extends React.Component<
(areUnreadBelowCurrentPosition || areSomeMessagesBelowCurrentPosition) (areUnreadBelowCurrentPosition || areSomeMessagesBelowCurrentPosition)
); );
const actionProps = getActions(this.props);
let floatingHeader: ReactNode; let floatingHeader: ReactNode;
// It's possible that a message was removed from `items` but we still have its ID in // 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. // state. `getTimestampForMessage` might return undefined in that case.
@ -938,7 +872,6 @@ export class Timeline extends React.Component<
> >
<ErrorBoundary i18n={i18n} showDebugLog={showDebugLog}> <ErrorBoundary i18n={i18n} showDebugLog={showDebugLog}>
{renderItem({ {renderItem({
actionProps,
containerElementRef: this.containerRef, containerElementRef: this.containerRef,
containerWidthBreakpoint: widthBreakpoint, containerWidthBreakpoint: widthBreakpoint,
conversationId: id, conversationId: id,
@ -1011,7 +944,7 @@ export class Timeline extends React.Component<
/> />
); );
onClose = () => { onClose = () => {
acknowledgeGroupMemberNameCollisions(groupNameCollisions); acknowledgeGroupMemberNameCollisions(id, groupNameCollisions);
}; };
break; break;
} }
@ -1047,16 +980,8 @@ export class Timeline extends React.Component<
let contactSpoofingReviewDialog: ReactNode; let contactSpoofingReviewDialog: ReactNode;
if (contactSpoofingReview) { if (contactSpoofingReview) {
const commonProps = { const commonProps = {
acceptConversation, conversationId: id,
blockAndReportSpam,
blockConversation,
deleteConversation,
getPreferredBadge,
i18n,
onClose: closeContactSpoofingReview, onClose: closeContactSpoofingReview,
onShowContactModal: showContactModal,
removeMember,
theme,
}; };
switch (contactSpoofingReview.type) { switch (contactSpoofingReview.type) {
@ -1138,7 +1063,7 @@ export class Timeline extends React.Component<
{Timeline.getWarning(this.props, this.state) && ( {Timeline.getWarning(this.props, this.state) && (
<div style={{ height: lastMeasuredWarningHeight }} /> <div style={{ height: lastMeasuredWarningHeight }} />
)} )}
{renderHeroRow(id, unblurAvatar, updateSharedGroups)} {renderHeroRow(id)}
</> </>
)} )}

View file

@ -65,7 +65,6 @@ const getDefaultProps = () => ({
reactToMessage: action('reactToMessage'), reactToMessage: action('reactToMessage'),
checkForAccount: action('checkForAccount'), checkForAccount: action('checkForAccount'),
clearSelectedMessage: action('clearSelectedMessage'), clearSelectedMessage: action('clearSelectedMessage'),
contactSupport: action('contactSupport'),
setQuoteByMessageId: action('setQuoteByMessageId'), setQuoteByMessageId: action('setQuoteByMessageId'),
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),
retryMessageSend: action('retryMessageSend'), retryMessageSend: action('retryMessageSend'),
@ -73,10 +72,8 @@ const getDefaultProps = () => ({
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'), deleteMessageForEveryone: action('deleteMessageForEveryone'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'), kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'), markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
messageExpanded: action('messageExpanded'), messageExpanded: action('messageExpanded'),
showMessageDetail: action('showMessageDetail'),
showConversation: action('showConversation'), showConversation: action('showConversation'),
openGiftBadge: action('openGiftBadge'), openGiftBadge: action('openGiftBadge'),
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),

View file

@ -15,12 +15,8 @@ import type {
} from './TimelineMessage'; } from './TimelineMessage';
import type { PropsActionsType as CallingNotificationActionsType } from './CallingNotification'; import type { PropsActionsType as CallingNotificationActionsType } from './CallingNotification';
import { CallingNotification } from './CallingNotification'; import { CallingNotification } from './CallingNotification';
import type { PropsActionsType as PropsChatSessionRefreshedActionsType } from './ChatSessionRefreshedNotification';
import { ChatSessionRefreshedNotification } from './ChatSessionRefreshedNotification'; import { ChatSessionRefreshedNotification } from './ChatSessionRefreshedNotification';
import type { import type { PropsDataType as DeliveryIssueProps } from './DeliveryIssueNotification';
PropsActionsType as DeliveryIssueActionProps,
PropsDataType as DeliveryIssueProps,
} from './DeliveryIssueNotification';
import { DeliveryIssueNotification } from './DeliveryIssueNotification'; import { DeliveryIssueNotification } from './DeliveryIssueNotification';
import type { PropsData as ChangeNumberNotificationProps } from './ChangeNumberNotification'; import type { PropsData as ChangeNumberNotificationProps } from './ChangeNumberNotification';
import { ChangeNumberNotification } from './ChangeNumberNotification'; import { ChangeNumberNotification } from './ChangeNumberNotification';
@ -171,9 +167,7 @@ type PropsLocalType = {
type PropsActionsType = MessageActionsType & type PropsActionsType = MessageActionsType &
CallingNotificationActionsType & CallingNotificationActionsType &
DeliveryIssueActionProps &
GroupV2ChangeActionsType & GroupV2ChangeActionsType &
PropsChatSessionRefreshedActionsType &
SafetyNumberActionsType; SafetyNumberActionsType;
export type PropsType = PropsLocalType & export type PropsType = PropsLocalType &

View file

@ -202,15 +202,17 @@ function MessageAudioContainer({
return ( return (
<MessageAudio <MessageAudio
{...props} {...props}
id="storybook" conversationId="some-conversation-id"
renderingContext="storybook"
computePeaks={computePeaks}
active={active} active={active}
played={_played} computePeaks={computePeaks}
id="storybook"
loadAndPlayMessageAudio={loadAndPlayMessageAudio} loadAndPlayMessageAudio={loadAndPlayMessageAudio}
played={_played}
pushPanelForConversation={action('pushPanelForConversation')}
renderingContext="storybook"
setCurrentTime={setCurrentTimeAction}
setIsPlaying={setIsPlayingAction} setIsPlaying={setIsPlayingAction}
setPlaybackRate={setPlaybackRateAction} setPlaybackRate={setPlaybackRateAction}
setCurrentTime={setCurrentTimeAction}
/> />
); );
} }
@ -315,7 +317,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
'showExpiredOutgoingTapToViewToast' 'showExpiredOutgoingTapToViewToast'
), ),
toggleForwardMessageModal: action('toggleForwardMessageModal'), toggleForwardMessageModal: action('toggleForwardMessageModal'),
showMessageDetail: action('showMessageDetail'),
showLightbox: action('showLightbox'), showLightbox: action('showLightbox'),
startConversation: action('startConversation'), startConversation: action('startConversation'),
status: overrideProps.status || 'sent', status: overrideProps.status || 'sent',

View file

@ -27,6 +27,8 @@ import { doesMessageBodyOverflow } from './MessageBodyReadMore';
import type { Props as ReactionPickerProps } from './ReactionPicker'; import type { Props as ReactionPickerProps } from './ReactionPicker';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts'; import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
import { PanelType } from '../../types/Panels';
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
export type PropsData = { export type PropsData = {
canDownload: boolean; canDownload: boolean;
@ -45,6 +47,7 @@ export type PropsActions = {
}) => void; }) => void;
deleteMessageForEveryone: (id: string) => void; deleteMessageForEveryone: (id: string) => void;
toggleForwardMessageModal: (id: string) => void; toggleForwardMessageModal: (id: string) => void;
pushPanelForConversation: PushPanelForConversationActionType;
reactToMessage: ( reactToMessage: (
id: string, id: string,
{ emoji, remove }: { emoji: string; remove: boolean } { emoji, remove }: { emoji: string; remove: boolean }
@ -95,6 +98,7 @@ export function TimelineMessage(props: Props): JSX.Element {
isSelected, isSelected,
isSticker, isSticker,
isTapToView, isTapToView,
pushPanelForConversation,
reactToMessage, reactToMessage,
setQuoteByMessageId, setQuoteByMessageId,
renderReactionPicker, renderReactionPicker,
@ -103,7 +107,6 @@ export function TimelineMessage(props: Props): JSX.Element {
retryDeleteForEveryone, retryDeleteForEveryone,
selectedReaction, selectedReaction,
toggleForwardMessageModal, toggleForwardMessageModal,
showMessageDetail,
text, text,
timestamp, timestamp,
kickOffAttachmentDownload, kickOffAttachmentDownload,
@ -406,7 +409,12 @@ export function TimelineMessage(props: Props): JSX.Element {
onDeleteForEveryone={ onDeleteForEveryone={
canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined
} }
onMoreInfo={() => showMessageDetail(id)} onMoreInfo={() =>
pushPanelForConversation(conversationId, {
type: PanelType.MessageDetails,
args: { messageId: id },
})
}
/> />
</> </>
); );

View file

@ -42,10 +42,7 @@ import { missingCaseError } from '../util/missingCaseError';
import { dropNull } from '../util/dropNull'; import { dropNull } from '../util/dropNull';
import { incrementMessageCounter } from '../util/incrementMessageCounter'; import { incrementMessageCounter } from '../util/incrementMessageCounter';
import type { ConversationModel } from './conversations'; import type { ConversationModel } from './conversations';
import type { import type { Contact as SmartMessageDetailContact } from '../state/smart/MessageDetail';
OwnProps as SmartMessageDetailPropsType,
Contact as SmartMessageDetailContact,
} from '../state/smart/MessageDetail';
import { getCallingNotificationText } from '../util/callingNotification'; import { getCallingNotificationText } from '../util/callingNotification';
import type { import type {
ProcessedDataMessage, ProcessedDataMessage,
@ -53,6 +50,7 @@ import type {
ProcessedUnidentifiedDeliveryStatus, ProcessedUnidentifiedDeliveryStatus,
CallbackResultType, CallbackResultType,
} from '../textsecure/Types.d'; } from '../textsecure/Types.d';
import type { Props as PropsForMessageDetails } from '../components/conversation/MessageDetail';
import { SendMessageProtoError } from '../textsecure/Errors'; import { SendMessageProtoError } from '../textsecure/Errors';
import * as expirationTimer from '../util/expirationTimer'; import * as expirationTimer from '../util/expirationTimer';
import { getUserLanguages } from '../util/userLanguages'; import { getUserLanguages } from '../util/userLanguages';
@ -186,7 +184,6 @@ import type { StickerWithHydratedData } from '../types/Stickers';
import { getStringForConversationMerge } from '../util/getStringForConversationMerge'; import { getStringForConversationMerge } from '../util/getStringForConversationMerge';
import { getStringForPhoneNumberDiscovery } from '../util/getStringForPhoneNumberDiscovery'; import { getStringForPhoneNumberDiscovery } from '../util/getStringForPhoneNumberDiscovery';
import { getTitle, renderNumber } from '../util/getTitle'; import { getTitle, renderNumber } from '../util/getTitle';
import { DurationInSeconds } from '../util/durations';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
function isSameUuid( function isSameUuid(
@ -265,15 +262,9 @@ async function shouldReplyNotifyUser(
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
type PropsForMessageDetail = Pick< export type MinimalPropsForMessageDetails = Pick<
SmartMessageDetailPropsType, PropsForMessageDetails,
| 'sentAt' 'sentAt' | 'receivedAt' | 'message' | 'errors' | 'contacts'
| 'receivedAt'
| 'message'
| 'errors'
| 'contacts'
| 'expirationLength'
| 'expirationTimestamp'
>; >;
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -482,7 +473,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}); });
} }
getPropsForMessageDetail(ourConversationId: string): PropsForMessageDetail { getPropsForMessageDetail(
ourConversationId: string
): MinimalPropsForMessageDetails {
const newIdentity = window.i18n('newIdentity'); const newIdentity = window.i18n('newIdentity');
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError'; const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
@ -578,21 +571,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}; };
}); });
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 { return {
sentAt: this.get('sent_at'), sentAt: this.get('sent_at'),
receivedAt: this.getReceivedAt(), receivedAt: this.getReceivedAt(),
expirationLength,
expirationTimestamp,
message: getPropsForMessage(this.attributes, { message: getPropsForMessage(this.attributes, {
conversationSelector: findAndFormatContact, conversationSelector: findAndFormatContact,
ourConversationId, ourConversationId,

View file

@ -48,6 +48,7 @@ import {
maybeGrabLinkPreview, maybeGrabLinkPreview,
removeLinkPreview, removeLinkPreview,
resetLinkPreview, resetLinkPreview,
suspendLinkPreviews,
} from '../../services/LinkPreview'; } from '../../services/LinkPreview';
import { getMaximumAttachmentSize } from '../../util/attachments'; import { getMaximumAttachmentSize } from '../../util/attachments';
import { getRecipientsByConversation } from '../../util/getRecipientsByConversation'; import { getRecipientsByConversation } from '../../util/getRecipientsByConversation';
@ -66,9 +67,13 @@ import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessa
import { writeDraftAttachment } from '../../util/writeDraftAttachment'; import { writeDraftAttachment } from '../../util/writeDraftAttachment';
import { getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { canReply } from '../selectors/message'; import { canReply } from '../selectors/message';
import { getContactId } from '../../messages/helpers';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
import { scrollToMessage } from './conversations';
import type { ScrollToMessageActionType } from './conversations';
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
// State // State
@ -142,18 +147,23 @@ type ComposerActionType =
export const actions = { export const actions = {
addAttachment, addAttachment,
addPendingAttachment, addPendingAttachment,
cancelJoinRequest,
onClearAttachments,
onCloseLinkPreview,
onEditorStateChange, onEditorStateChange,
onTextTooLong,
processAttachments, processAttachments,
reactToMessage, reactToMessage,
removeAttachment, removeAttachment,
replaceAttachments, replaceAttachments,
resetComposer, resetComposer,
scrollToQuotedMessage,
sendMultiMediaMessage, sendMultiMediaMessage,
sendStickerMessage, sendStickerMessage,
setComposerDisabledState, setComposerDisabledState,
setComposerFocus, setComposerFocus,
setQuoteByMessageId,
setMediaQualitySetting, setMediaQualitySetting,
setQuoteByMessageId,
setQuotedMessage, setQuotedMessage,
}; };
@ -161,6 +171,97 @@ export const useComposerActions = (): BoundActionCreatorsMapObject<
typeof actions typeof actions
> => useBoundActions(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( function sendMultiMediaMessage(
conversationId: string, conversationId: string,
options: { options: {

View file

@ -105,6 +105,7 @@ import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue'; import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { isIncoming, isOutgoing } from '../selectors/message'; import { isIncoming, isOutgoing } from '../selectors/message';
import { getActiveCallState } from '../selectors/calling';
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage'; import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
import type { ShowToastActionType } from './toast'; import type { ShowToastActionType } from './toast';
import { SHOW_TOAST } from './toast'; import { SHOW_TOAST } from './toast';
@ -128,6 +129,7 @@ import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue';
import { isOlderThan } from '../../util/timestamp'; import { isOlderThan } from '../../util/timestamp';
import { DAY } from '../../util/durations'; import { DAY } from '../../util/durations';
import { isNotNil } from '../../util/isNotNil'; import { isNotNil } from '../../util/isNotNil';
import { startConversation } from '../../util/startConversation';
// State // State
@ -875,10 +877,12 @@ export type ConversationActionType =
export const actions = { export const actions = {
acceptConversation, acceptConversation,
acknowledgeGroupMemberNameCollisions,
addMembersToGroup, addMembersToGroup,
approvePendingMembershipFromGroupV2, approvePendingMembershipFromGroupV2,
blockAndReportSpam, blockAndReportSpam,
blockConversation, blockConversation,
blockGroupLinkRequests,
cancelConversationVerification, cancelConversationVerification,
changeHasGroupLink, changeHasGroupLink,
clearCancelledConversationVerification, clearCancelledConversationVerification,
@ -901,8 +905,8 @@ export const actions = {
createGroup, createGroup,
deleteAvatarFromDisk, deleteAvatarFromDisk,
deleteConversation, deleteConversation,
deleteMessageForEveryone,
deleteMessage, deleteMessage,
deleteMessageForEveryone,
destroyMessages, destroyMessages,
discardMessages, discardMessages,
doubleCheckMissingQuoteReference, doubleCheckMissingQuoteReference,
@ -911,8 +915,12 @@ export const actions = {
initiateMigrationToGroupV2, initiateMigrationToGroupV2,
kickOffAttachmentDownload, kickOffAttachmentDownload,
leaveGroup, leaveGroup,
loadNewerMessages,
loadNewestMessages,
loadOlderMessages,
loadRecentMediaItems, loadRecentMediaItems,
markAttachmentAsCorrupted, markAttachmentAsCorrupted,
markMessageRead,
messageChanged, messageChanged,
messageDeleted, messageDeleted,
messageExpanded, messageExpanded,
@ -920,11 +928,16 @@ export const actions = {
messagesAdded, messagesAdded,
messagesReset, messagesReset,
myProfileChanged, myProfileChanged,
onArchive,
onMarkUnread,
onMoveToInbox,
onUndoArchive,
openGiftBadge, openGiftBadge,
popPanelForConversation, popPanelForConversation,
pushPanelForConversation, pushPanelForConversation,
removeAllConversations, removeAllConversations,
removeCustomColorOnConversations, removeCustomColorOnConversations,
removeMember,
removeMemberFromGroup, removeMemberFromGroup,
repairNewestMessage, repairNewestMessage,
repairOldestMessage, repairOldestMessage,
@ -964,14 +977,17 @@ export const actions = {
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showInbox, showInbox,
startComposing, startComposing,
startConversation,
startSettingGroupMetadata, startSettingGroupMetadata,
toggleAdmin, toggleAdmin,
toggleComposeEditingAvatar, toggleComposeEditingAvatar,
toggleConversationInChooseMembers, toggleConversationInChooseMembers,
toggleGroupsForStorySend, toggleGroupsForStorySend,
toggleHideStories, toggleHideStories,
unblurAvatar,
updateConversationModelSharedGroups, updateConversationModelSharedGroups,
updateGroupAttributes, updateGroupAttributes,
updateSharedGroups,
verifyConversationsStoppingSend, verifyConversationsStoppingSend,
}; };
@ -979,6 +995,230 @@ export const useConversationsActions = (): BoundActionCreatorsMapObject<
typeof actions typeof actions
> => useBoundActions(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<GroupNameCollisionsWithIdsByTitle>
): 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<void, RootStateType, unknown, NoopActionType> {
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( function filterAvatarData(
avatars: ReadonlyArray<AvatarDataType>, avatars: ReadonlyArray<AvatarDataType>,
data: AvatarDataType data: AvatarDataType
@ -2314,6 +2554,10 @@ function pushPanelForConversation(
}; };
} }
export type PopPanelForConversationActionType = (
conversationId: string
) => unknown;
function popPanelForConversation( function popPanelForConversation(
conversationId: string conversationId: string
): ThunkAction<void, RootStateType, unknown, PopPanelActionType> { ): ThunkAction<void, RootStateType, unknown, PopPanelActionType> {
@ -2830,7 +3074,7 @@ function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionT
return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' }; return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' };
} }
function scrollToMessage( export function scrollToMessage(
conversationId: string, conversationId: string,
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, ScrollToMessageActionType> { ): ThunkAction<void, RootStateType, unknown, ScrollToMessageActionType> {

View file

@ -7,12 +7,11 @@ import { Provider } from 'react-redux';
import type { Store } from 'redux'; import type { Store } from 'redux';
import type { OwnProps } from '../smart/MessageDetail';
import { SmartMessageDetail } from '../smart/MessageDetail'; import { SmartMessageDetail } from '../smart/MessageDetail';
export const createMessageDetail = ( export const createMessageDetail = (
store: Store, store: Store,
props: OwnProps props: Parameters<typeof SmartMessageDetail>[0]
): ReactElement => ( ): ReactElement => (
<Provider store={store}> <Provider store={store}>
<SmartMessageDetail {...props} /> <SmartMessageDetail {...props} />

View file

@ -1087,6 +1087,7 @@ function getPropsForGroupV2Change(
return { return {
areWeAdmin: Boolean(conversation.areWeAdmin), areWeAdmin: Boolean(conversation.areWeAdmin),
conversationId: conversation.id,
groupName: conversation?.type === 'group' ? conversation?.name : undefined, groupName: conversation?.type === 'group' ? conversation?.name : undefined,
groupMemberships: conversation.memberships, groupMemberships: conversation.memberships,
groupBannedMemberships: conversation.bannedMemberships, groupBannedMemberships: conversation.bannedMemberships,

View file

@ -11,8 +11,7 @@ import { getIntl } from '../selectors/user';
import { useActions as useEmojiActions } from '../ducks/emojis'; import { useActions as useEmojiActions } from '../ducks/emojis';
import { useActions as useItemsActions } from '../ducks/items'; import { useActions as useItemsActions } from '../ducks/items';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { showToast } from '../../util/showToast'; import { useComposerActions } from '../ducks/composer';
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
export type SmartCompositionTextAreaProps = Pick< export type SmartCompositionTextAreaProps = Pick<
CompositionTextAreaProps, CompositionTextAreaProps,
@ -34,6 +33,7 @@ export function SmartCompositionTextArea(
const { onUseEmoji: onPickEmoji } = useEmojiActions(); const { onUseEmoji: onPickEmoji } = useEmojiActions();
const { onSetSkinTone } = useItemsActions(); const { onSetSkinTone } = useItemsActions();
const { onTextTooLong } = useComposerActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
@ -44,7 +44,7 @@ export function SmartCompositionTextArea(
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
onTextTooLong={() => showToast(ToastMessageBodyTooLong)} onTextTooLong={onTextTooLong}
/> />
); );
} }

View file

@ -5,33 +5,39 @@ import * as React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { PropsType as DownstreamPropsType } from '../../components/conversation/ContactSpoofingReviewDialog';
import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog'; import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog';
import type { ConversationType } from '../ducks/conversations'; import type { ConversationType } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations';
import type { GetConversationByIdType } from '../selectors/conversations'; import type { GetConversationByIdType } from '../selectors/conversations';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { ContactSpoofingType } from '../../util/contactSpoofing'; 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<DownstreamPropsType, 'type'> & export type PropsType =
( | {
| { conversationId: string;
type: ContactSpoofingType.DirectConversationWithSameTitle; onClose: () => void;
possiblyUnsafeConversation: ConversationType; } & (
safeConversation: ConversationType; | {
} type: ContactSpoofingType.DirectConversationWithSameTitle;
| { possiblyUnsafeConversation: ConversationType;
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle; safeConversation: ConversationType;
groupConversationId: string; }
collisionInfoByTitle: Record< | {
string, type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
Array<{ groupConversationId: string;
oldName?: string; collisionInfoByTitle: Record<
conversation: ConversationType; string,
}> Array<{
>; oldName?: string;
} conversation: ConversationType;
); }>
>;
}
);
export function SmartContactSpoofingReviewDialog( export function SmartContactSpoofingReviewDialog(
props: PropsType props: PropsType
@ -42,14 +48,39 @@ export function SmartContactSpoofingReviewDialog(
getConversationSelector 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) { if (type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
return ( return (
<ContactSpoofingReviewDialog <ContactSpoofingReviewDialog
{...props} {...props}
{...sharedProps}
group={getConversation(props.groupConversationId)} group={getConversation(props.groupConversationId)}
/> />
); );
} }
return <ContactSpoofingReviewDialog {...props} />; return <ContactSpoofingReviewDialog {...props} {...sharedProps} />;
} }

View file

@ -29,12 +29,6 @@ import { isSignalConversation } from '../../util/isSignalConversation';
export type OwnProps = { export type OwnProps = {
id: string; id: string;
onArchive: () => void;
onGoBack: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void;
onSearchInConversation: () => void;
}; };
const getOutgoingCallButtonStyle = ( const getOutgoingCallButtonStyle = (

View file

@ -3,11 +3,8 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { CompositionAreaPropsType } from './CompositionArea';
import type { OwnProps as ConversationHeaderPropsType } from './ConversationHeader';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { ReactPanelRenderType } from '../../types/Panels'; import type { ReactPanelRenderType } from '../../types/Panels';
import type { TimelinePropsType } from './Timeline';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { ContactDetail } from '../../components/conversation/ContactDetail'; import { ContactDetail } from '../../components/conversation/ContactDetail';
import { ConversationView } from '../../components/conversation/ConversationView'; import { ConversationView } from '../../components/conversation/ConversationView';
@ -26,31 +23,17 @@ import { SmartStickerManager } from './StickerManager';
import { SmartTimeline } from './Timeline'; import { SmartTimeline } from './Timeline';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getTopPanelRenderableByReact } from '../selectors/conversations'; import { getTopPanelRenderableByReact } from '../selectors/conversations';
import { startConversation } from '../../util/startConversation';
import { useComposerActions } from '../ducks/composer'; import { useComposerActions } from '../ducks/composer';
import { useConversationsActions } from '../ducks/conversations';
export type PropsType = { export type PropsType = {
conversationId: string; conversationId: string;
compositionAreaProps: Pick<
CompositionAreaPropsType,
| 'id'
| 'onCancelJoinRequest'
| 'onClearAttachments'
| 'onCloseLinkPreview'
| 'onEditorStateChange'
| 'onSelectMediaQuality'
| 'onTextTooLong'
>;
conversationHeaderProps: ConversationHeaderPropsType;
timelineProps: TimelinePropsType;
}; };
export function SmartConversationView({ export function SmartConversationView({
compositionAreaProps,
conversationHeaderProps,
conversationId, conversationId,
timelineProps,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const { startConversation } = useConversationsActions();
const topPanel = useSelector<StateType, ReactPanelRenderType | undefined>( const topPanel = useSelector<StateType, ReactPanelRenderType | undefined>(
getTopPanelRenderableByReact getTopPanelRenderableByReact
); );
@ -62,13 +45,11 @@ export function SmartConversationView({
<ConversationView <ConversationView
conversationId={conversationId} conversationId={conversationId}
processAttachments={processAttachments} processAttachments={processAttachments}
renderCompositionArea={() => ( renderCompositionArea={() => <SmartCompositionArea id={conversationId} />}
<SmartCompositionArea {...compositionAreaProps} />
)}
renderConversationHeader={() => ( renderConversationHeader={() => (
<SmartConversationHeader {...conversationHeaderProps} /> <SmartConversationHeader id={conversationId} />
)} )}
renderTimeline={() => <SmartTimeline {...timelineProps} />} renderTimeline={() => <SmartTimeline id={conversationId} />}
renderPanel={() => { renderPanel={() => {
if (!topPanel) { if (!topPanel) {
return; return;

View file

@ -3,7 +3,7 @@
import { connect } from 'react-redux'; 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 { MessageDetail } from '../../components/conversation/MessageDetail';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
@ -12,34 +12,25 @@ import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getInteractionMode, getTheme } from '../selectors/user'; import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
import { renderAudioAttachment } from './renderAudioAttachment'; import { renderAudioAttachment } from './renderAudioAttachment';
import { getContactNameColorSelector } from '../selectors/conversations'; import { getContactNameColorSelector } from '../selectors/conversations';
import type { MinimalPropsForMessageDetails } from '../../models/messages';
export { Contact } from '../../components/conversation/MessageDetail'; export { Contact } from '../../components/conversation/MessageDetail';
export type OwnProps = Omit< export type PropsWithExtraFunctions = MinimalPropsForMessageDetails &
MessageDetailProps, Pick<
| 'getPreferredBadge' MessageDetailProps,
| 'i18n' | 'contactNameColor'
| 'interactionMode' | 'getPreferredBadge'
| 'renderAudioAttachment' | 'i18n'
| 'renderEmojiPicker' | 'interactionMode'
| 'renderReactionPicker' | 'renderAudioAttachment'
| 'theme' | 'theme'
| 'showContactModal' >;
| 'showConversation'
>;
const mapStateToProps = ( const mapStateToProps = (
state: StateType, state: StateType,
props: OwnProps props: MinimalPropsForMessageDetails
): MessageDetailProps => { ): PropsWithExtraFunctions => {
const { const { contacts, errors, message, receivedAt, sentAt } = props;
contacts,
errors,
message,
receivedAt,
sentAt,
startConversation,
} = props;
const contactNameColor = const contactNameColor =
message.conversationType === 'group' message.conversationType === 'group'
@ -65,7 +56,6 @@ const mapStateToProps = (
theme: getTheme(state), theme: getTheme(state),
renderAudioAttachment, renderAudioAttachment,
startConversation,
}; };
}; };

View file

@ -10,8 +10,6 @@ import { mapDispatchToProps } from '../actions';
import type { import type {
ContactSpoofingReviewPropType, ContactSpoofingReviewPropType,
WarningType as TimelineWarningType, WarningType as TimelineWarningType,
PropsType as ComponentPropsType,
PropsActionsFromBackboneForChildrenType,
} from '../../components/conversation/Timeline'; } from '../../components/conversation/Timeline';
import { Timeline } from '../../components/conversation/Timeline'; import { Timeline } from '../../components/conversation/Timeline';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
@ -50,47 +48,12 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil'; import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
import type { WidthBreakpoint } from '../../components/_util'; import type { WidthBreakpoint } from '../../components/_util';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { markViewed } from '../ducks/conversations';
type ExternalProps = { type ExternalProps = {
id: string; 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({ function renderItem({
actionProps,
containerElementRef, containerElementRef,
containerWidthBreakpoint, containerWidthBreakpoint,
conversationId, conversationId,
@ -100,7 +63,6 @@ function renderItem({
previousMessageId, previousMessageId,
unreadIndicatorPlacement, unreadIndicatorPlacement,
}: { }: {
actionProps: PropsActionsFromBackboneForChildrenType;
containerElementRef: RefObject<HTMLElement>; containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint; containerWidthBreakpoint: WidthBreakpoint;
conversationId: string; conversationId: string;
@ -112,7 +74,6 @@ function renderItem({
}): JSX.Element { }): JSX.Element {
return ( return (
<SmartTimelineItem <SmartTimelineItem
{...actionProps}
containerElementRef={containerElementRef} containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint} containerWidthBreakpoint={containerWidthBreakpoint}
conversationId={conversationId} conversationId={conversationId}
@ -134,18 +95,8 @@ function renderContactSpoofingReviewDialog(
return <SmartContactSpoofingReviewDialog {...props} />; return <SmartContactSpoofingReviewDialog {...props} />;
} }
function renderHeroRow( function renderHeroRow(id: string): JSX.Element {
id: string, return <SmartHeroRow id={id} />;
unblurAvatar: () => void,
updateSharedGroups: () => unknown
): JSX.Element {
return (
<SmartHeroRow
id={id}
unblurAvatar={unblurAvatar}
updateSharedGroups={updateSharedGroups}
/>
);
} }
function renderTypingBubble(id: string): JSX.Element { function renderTypingBubble(id: string): JSX.Element {
return <SmartTypingBubble id={id} />; return <SmartTypingBubble id={id} />;
@ -270,8 +221,8 @@ const getContactSpoofingReview = (
} }
}; };
const mapStateToProps = (state: StateType, props: TimelinePropsType) => { const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id, ...actions } = props; const { id } = props;
const conversation = getConversationSelector(state)(id); const conversation = getConversationSelector(state)(id);
@ -307,8 +258,6 @@ const mapStateToProps = (state: StateType, props: TimelinePropsType) => {
renderContactSpoofingReviewDialog, renderContactSpoofingReviewDialog,
renderHeroRow, renderHeroRow,
renderTypingBubble, renderTypingBubble,
markViewed,
...actions,
}; };
}; };

View file

@ -9,6 +9,9 @@ export enum ToastType {
CannotOpenGiftBadgeIncoming = 'CannotOpenGiftBadgeIncoming', CannotOpenGiftBadgeIncoming = 'CannotOpenGiftBadgeIncoming',
CannotOpenGiftBadgeOutgoing = 'CannotOpenGiftBadgeOutgoing', CannotOpenGiftBadgeOutgoing = 'CannotOpenGiftBadgeOutgoing',
CannotStartGroupCall = 'CannotStartGroupCall', CannotStartGroupCall = 'CannotStartGroupCall',
ConversationArchived = 'ConversationArchived',
ConversationMarkedUnread = 'ConversationMarkedUnread',
ConversationUnarchived = 'ConversationUnarchived',
CopiedUsername = 'CopiedUsername', CopiedUsername = 'CopiedUsername',
CopiedUsernameLink = 'CopiedUsernameLink', CopiedUsernameLink = 'CopiedUsernameLink',
DangerousFileType = 'DangerousFileType', DangerousFileType = 'DangerousFileType',
@ -22,6 +25,7 @@ export enum ToastType {
LeftGroup = 'LeftGroup', LeftGroup = 'LeftGroup',
MaxAttachments = 'MaxAttachments', MaxAttachments = 'MaxAttachments',
MessageBodyTooLong = 'MessageBodyTooLong', MessageBodyTooLong = 'MessageBodyTooLong',
OriginalMessageNotFound = 'OriginalMessageNotFound',
PinnedConversationsFull = 'PinnedConversationsFull', PinnedConversationsFull = 'PinnedConversationsFull',
ReactionFailed = 'ReactionFailed', ReactionFailed = 'ReactionFailed',
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked', ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',

View file

@ -8,12 +8,6 @@ import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMem
import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin'; import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed'; import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed';
import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved'; 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 { import type {
ToastInternalError, ToastInternalError,
ToastPropsType as ToastInternalErrorPropsType, ToastPropsType as ToastInternalErrorPropsType,
@ -25,9 +19,7 @@ import type {
import type { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied'; import type { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied';
import type { ToastLinkCopied } from '../components/ToastLinkCopied'; import type { ToastLinkCopied } from '../components/ToastLinkCopied';
import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs'; 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 { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed';
import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit'; import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment'; 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 ToastAlreadyRequestedToJoin): void;
export function showToast(Toast: typeof ToastCaptchaFailed): void; export function showToast(Toast: typeof ToastCaptchaFailed): void;
export function showToast(Toast: typeof ToastCaptchaSolved): 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( export function showToast(
Toast: typeof ToastInternalError, Toast: typeof ToastInternalError,
props: ToastInternalErrorPropsType props: ToastInternalErrorPropsType
@ -53,8 +39,6 @@ export function showToast(
export function showToast(Toast: typeof ToastGroupLinkCopied): void; export function showToast(Toast: typeof ToastGroupLinkCopied): void;
export function showToast(Toast: typeof ToastLinkCopied): void; export function showToast(Toast: typeof ToastLinkCopied): void;
export function showToast(Toast: typeof ToastLoadingFullLogs): 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 ToastStickerPackInstallFailed): void;
export function showToast(Toast: typeof ToastVoiceNoteLimit): void; export function showToast(Toast: typeof ToastVoiceNoteLimit): void;
export function showToast( export function showToast(

View file

@ -8,43 +8,22 @@ import { render } from 'mustache';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import { getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { getContactId } from '../messages/helpers';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
import { isGroup } from '../util/whatTypeOfConversation'; import { isGroup } from '../util/whatTypeOfConversation';
import { getActiveCallState } from '../state/selectors/calling';
import { ReactWrapperView } from './ReactWrapperView'; import { ReactWrapperView } from './ReactWrapperView';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { createConversationView } from '../state/roots/createConversationView'; 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 { import {
removeLinkPreview, removeLinkPreview,
suspendLinkPreviews, suspendLinkPreviews,
} from '../services/LinkPreview'; } from '../services/LinkPreview';
import { SECOND } from '../util/durations'; 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 type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels';
import { PanelType, isPanelHandledByReact } from '../types/Panels'; import { PanelType, isPanelHandledByReact } from '../types/Panels';
import { UUIDKind } from '../types/UUID';
type BackbonePanelType = { panelType: PanelType; view: Backbone.View }; 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<ConversationModel> { export class ConversationView extends window.Backbone.View<ConversationModel> {
// Sub-views // Sub-views
private contactModalView?: Backbone.View; private contactModalView?: Backbone.View;
@ -69,12 +48,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.unload(`model trigger - ${reason}`) 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, 'pushPanel', this.pushPanel);
this.listenTo(this.model, 'popPanel', this.popPanel); this.listenTo(this.model, 'popPanel', this.popPanel);
@ -116,209 +89,18 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} }
setupConversationView(): void { 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<GroupNameCollisionsWithIdsByTitle>
): 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 // setupCompositionArea
window.reduxActions.composer.resetComposer(); 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 // createConversationView root
const JSX = createConversationView(window.reduxStore, { const JSX = createConversationView(window.reduxStore, {
conversationId: this.model.id, conversationId: this.model.id,
compositionAreaProps,
conversationHeaderProps,
timelineProps,
}); });
this.conversationView = new ReactWrapperView({ JSX }); this.conversationView = new ReactWrapperView({ JSX });
this.$('.ConversationView__template').append(this.conversationView.el); this.$('.ConversationView__template').append(this.conversationView.el);
} }
getMessageActions(): MessageActionsType {
const showMessageDetail = (messageId: string) => {
this.showMessageDetail(messageId);
};
return {
showMessageDetail,
startConversation,
};
}
unload(reason: string): void { unload(reason: string): void {
log.info( log.info(
'unloading conversation', 'unloading conversation',
@ -445,13 +227,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.model.updateVerified(); this.model.updateVerified();
} }
showMessageDetail(messageId: string): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.MessageDetails,
args: { messageId },
});
}
getMessageDetail({ getMessageDetail({
messageId, messageId,
}: { }: {
@ -459,7 +234,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}): Backbone.View | undefined { }): Backbone.View | undefined {
const message = window.MessageController.getById(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error(`showMessageDetail: Message ${messageId} missing!`); throw new Error(`getMessageDetail: Message ${messageId} missing!`);
} }
if (!message.isNormalBubble()) { if (!message.isNormalBubble()) {
@ -470,7 +245,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
...message.getPropsForMessageDetail( ...message.getPropsForMessageDetail(
window.ConversationController.getOurConversationIdOrThrow() window.ConversationController.getOurConversationIdOrThrow()
), ),
...this.getMessageActions(),
}); });
const onClose = () => { const onClose = () => {