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 {
@include font-body-2;
margin-top: 15px;
text-align: center;
user-select: none;

View file

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

View file

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

View file

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

View file

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

View file

@ -68,7 +68,6 @@ const MESSAGE_DEFAULT_PROPS = {
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightbox: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled,
showMessageDetail: shouldNeverBeCalled,
startConversation: shouldNeverBeCalled,
theme: ThemeType.dark,
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,
argTypes: {
hideToast: { action: true },
openFileInFolder: { action: true },
onUndoArchive: { action: true },
i18n: {
defaultValue: i18n,
},
@ -91,6 +93,30 @@ CannotStartGroupCall.args = {
},
};
export const ConversationArchived = Template.bind({});
ConversationArchived.args = {
toast: {
toastType: ToastType.ConversationArchived,
parameters: {
conversationId: 'some-conversation-id',
},
},
};
export const ConversationMarkedUnread = Template.bind({});
ConversationMarkedUnread.args = {
toast: {
toastType: ToastType.ConversationMarkedUnread,
},
};
export const ConversationUnarchived = Template.bind({});
ConversationUnarchived.args = {
toast: {
toastType: ToastType.ConversationUnarchived,
},
};
export const CopiedUsername = Template.bind({});
CopiedUsername.args = {
toast: {
@ -182,6 +208,13 @@ MaxAttachments.args = {
},
};
export const OriginalMessageNotFound = Template.bind({});
OriginalMessageNotFound.args = {
toast: {
toastType: ToastType.OriginalMessageNotFound,
},
};
export const MessageBodyTooLong = Template.bind({});
MessageBodyTooLong.args = {
toast: {

View file

@ -5,7 +5,6 @@ import React from 'react';
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
import { SECOND } from '../util/durations';
import { Toast } from './Toast';
import { ToastMessageBodyTooLong } from './ToastMessageBodyTooLong';
import { missingCaseError } from '../util/missingCaseError';
import { ToastType } from '../types/Toast';
@ -13,6 +12,7 @@ export type PropsType = {
hideToast: () => unknown;
i18n: LocalizerType;
openFileInFolder: (target: string) => unknown;
onUndoArchive: (conversaetionId: string) => unknown;
toast?: {
toastType: ToastType;
parameters?: ReplacementValuesType;
@ -25,6 +25,7 @@ export function ToastManager({
hideToast,
i18n,
openFileInFolder,
onUndoArchive,
toast,
}: PropsType): JSX.Element | null {
if (toast === undefined) {
@ -84,6 +85,36 @@ export function ToastManager({
);
}
if (toastType === ToastType.ConversationArchived) {
return (
<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) {
return (
<Toast onClose={hideToast} timeout={3 * SECOND}>
@ -174,15 +205,11 @@ export function ToastManager({
}
if (toastType === ToastType.MessageBodyTooLong) {
return <ToastMessageBodyTooLong i18n={i18n} onClose={hideToast} />;
return <Toast onClose={hideToast}>{i18n('messageBodyTooLong')}</Toast>;
}
if (toastType === ToastType.ReportedSpamAndBlocked) {
return (
<Toast onClose={hideToast}>
{i18n('MessageRequests--block-and-report-spam-success-toast')}
</Toast>
);
if (toastType === ToastType.OriginalMessageNotFound) {
return <Toast onClose={hideToast}>{i18n('originalMessageNotFound')}</Toast>;
}
if (toastType === ToastType.PinnedConversationsFull) {
@ -193,6 +220,14 @@ export function ToastManager({
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) {
return (
<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
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
@ -15,10 +14,5 @@ export default {
};
export function Default(): JSX.Element {
return (
<ChatSessionRefreshedNotification
contactSupport={action('contactSupport')}
i18n={i18n}
/>
);
return <ChatSessionRefreshedNotification i18n={i18n} />;
}

View file

@ -9,21 +9,19 @@ import type { LocalizerType } from '../../types/Util';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
import { mapToSupportLocale } from '../../util/mapToSupportLocale';
type PropsHousekeepingType = {
i18n: LocalizerType;
};
export type PropsActionsType = {
contactSupport: () => unknown;
};
export type PropsType = PropsHousekeepingType & PropsActionsType;
export type PropsType = PropsHousekeepingType;
export function ChatSessionRefreshedNotification(
props: PropsType
): ReactElement {
const { contactSupport, i18n } = props;
const { i18n } = props;
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const openDialog = useCallback(() => {
@ -35,8 +33,15 @@ export function ChatSessionRefreshedNotification(
const wrappedContactSupport = useCallback(() => {
setIsDialogOpen(false);
contactSupport();
}, [contactSupport, setIsDialogOpen]);
const baseUrl =
'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed';
const locale = window.getLocale();
const supportLocale = mapToSupportLocale(locale);
const url = baseUrl.replace('LOCALE', supportLocale);
openLinkInWebBrowser(url);
}, [setIsDialogOpen]);
return (
<>

View file

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

View file

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

View file

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

View file

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

View file

@ -134,6 +134,16 @@ DirectNoGroupsNoDataNotAccepted.story = {
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({});
GroupManyMembers.args = {
conversationType: 'group',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,6 +48,7 @@ import {
maybeGrabLinkPreview,
removeLinkPreview,
resetLinkPreview,
suspendLinkPreviews,
} from '../../services/LinkPreview';
import { getMaximumAttachmentSize } from '../../util/attachments';
import { getRecipientsByConversation } from '../../util/getRecipientsByConversation';
@ -66,9 +67,13 @@ import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessa
import { writeDraftAttachment } from '../../util/writeDraftAttachment';
import { getMessageById } from '../../messages/getMessageById';
import { canReply } from '../selectors/message';
import { getContactId } from '../../messages/helpers';
import { getConversationSelector } from '../selectors/conversations';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { useBoundActions } from '../../hooks/useBoundActions';
import { scrollToMessage } from './conversations';
import type { ScrollToMessageActionType } from './conversations';
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
// State
@ -142,18 +147,23 @@ type ComposerActionType =
export const actions = {
addAttachment,
addPendingAttachment,
cancelJoinRequest,
onClearAttachments,
onCloseLinkPreview,
onEditorStateChange,
onTextTooLong,
processAttachments,
reactToMessage,
removeAttachment,
replaceAttachments,
resetComposer,
scrollToQuotedMessage,
sendMultiMediaMessage,
sendStickerMessage,
setComposerDisabledState,
setComposerFocus,
setQuoteByMessageId,
setMediaQualitySetting,
setQuoteByMessageId,
setQuotedMessage,
};
@ -161,6 +171,97 @@ export const useComposerActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function onClearAttachments(conversationId: string): NoopActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('onClearAttachments: No conversation found');
}
clearConversationDraftAttachments(
conversation.id,
conversation.get('draftAttachments')
);
return {
type: 'NOOP',
payload: null,
};
}
function cancelJoinRequest(conversationId: string): NoopActionType {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('cancelJoinRequest: No conversation found');
}
longRunningTaskWrapper({
idForLogging: conversation.idForLogging(),
name: 'cancelJoinRequest',
task: async () => conversation.cancelJoinRequest(),
});
return {
type: 'NOOP',
payload: null,
};
}
function onCloseLinkPreview(): NoopActionType {
suspendLinkPreviews();
removeLinkPreview();
return {
type: 'NOOP',
payload: null,
};
}
function onTextTooLong(): ShowToastActionType {
return {
type: SHOW_TOAST,
payload: {
toastType: ToastType.MessageBodyTooLong,
},
};
}
function scrollToQuotedMessage({
authorId,
conversationId,
sentAt,
}: Readonly<{
authorId: string;
conversationId: string;
sentAt: number;
}>): ThunkAction<
void,
RootStateType,
unknown,
ShowToastActionType | ScrollToMessageActionType
> {
return async (dispatch, getState) => {
const messages = await window.Signal.Data.getMessagesBySentAt(sentAt);
const message = messages.find(item =>
Boolean(
item.conversationId === conversationId &&
authorId &&
getContactId(item) === authorId
)
);
if (!message) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.OriginalMessageNotFound,
},
});
return;
}
scrollToMessage(conversationId, message.id)(dispatch, getState, undefined);
};
}
function sendMultiMediaMessage(
conversationId: string,
options: {

View file

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

View file

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

View file

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

View file

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

View file

@ -5,33 +5,39 @@ import * as React from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import type { PropsType as DownstreamPropsType } from '../../components/conversation/ContactSpoofingReviewDialog';
import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog';
import type { ConversationType } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations';
import type { GetConversationByIdType } from '../selectors/conversations';
import { getConversationSelector } from '../selectors/conversations';
import { ContactSpoofingType } from '../../util/contactSpoofing';
import { useGlobalModalActions } from '../ducks/globalModals';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user';
export type PropsType = Omit<DownstreamPropsType, 'type'> &
(
| {
type: ContactSpoofingType.DirectConversationWithSameTitle;
possiblyUnsafeConversation: ConversationType;
safeConversation: ConversationType;
}
| {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
groupConversationId: string;
collisionInfoByTitle: Record<
string,
Array<{
oldName?: string;
conversation: ConversationType;
}>
>;
}
);
export type PropsType =
| {
conversationId: string;
onClose: () => void;
} & (
| {
type: ContactSpoofingType.DirectConversationWithSameTitle;
possiblyUnsafeConversation: ConversationType;
safeConversation: ConversationType;
}
| {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
groupConversationId: string;
collisionInfoByTitle: Record<
string,
Array<{
oldName?: string;
conversation: ConversationType;
}>
>;
}
);
export function SmartContactSpoofingReviewDialog(
props: PropsType
@ -42,14 +48,39 @@ export function SmartContactSpoofingReviewDialog(
getConversationSelector
);
const {
acceptConversation,
blockAndReportSpam,
blockConversation,
deleteConversation,
removeMember,
} = useConversationsActions();
const { showContactModal } = useGlobalModalActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const sharedProps = {
acceptConversation,
blockAndReportSpam,
blockConversation,
deleteConversation,
getPreferredBadge,
i18n,
removeMember,
showContactModal,
theme,
};
if (type === ContactSpoofingType.MultipleGroupMembersWithSameTitle) {
return (
<ContactSpoofingReviewDialog
{...props}
{...sharedProps}
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 = {
id: string;
onArchive: () => void;
onGoBack: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void;
onSearchInConversation: () => void;
};
const getOutgoingCallButtonStyle = (

View file

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

View file

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

View file

@ -10,8 +10,6 @@ import { mapDispatchToProps } from '../actions';
import type {
ContactSpoofingReviewPropType,
WarningType as TimelineWarningType,
PropsType as ComponentPropsType,
PropsActionsFromBackboneForChildrenType,
} from '../../components/conversation/Timeline';
import { Timeline } from '../../components/conversation/Timeline';
import type { StateType } from '../reducer';
@ -50,47 +48,12 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
import type { WidthBreakpoint } from '../../components/_util';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { markViewed } from '../ducks/conversations';
type ExternalProps = {
id: string;
// Note: most action creators are not wired into redux; for now they
// are provided by ConversationView in setupTimeline().
};
export type TimelinePropsType = ExternalProps &
Pick<
ComponentPropsType,
// All of these are the ones we need from backbone
// Used by Timeline itself
| 'acknowledgeGroupMemberNameCollisions'
| 'loadOlderMessages'
| 'loadNewerMessages'
| 'loadNewestMessages'
| 'markMessageRead'
| 'removeMember'
| 'unblurAvatar'
| 'updateSharedGroups'
// MessageActionsType
| 'scrollToQuotedMessage'
| 'showMessageDetail'
| 'startConversation'
// ChatSessionRefreshedNotificationActionsType
| 'contactSupport'
// DeliveryIssueNotificationActionsType
| 'learnMoreAboutDeliveryIssue'
// GroupV2ChangeActionsType
| 'blockGroupLinkRequests'
>;
function renderItem({
actionProps,
containerElementRef,
containerWidthBreakpoint,
conversationId,
@ -100,7 +63,6 @@ function renderItem({
previousMessageId,
unreadIndicatorPlacement,
}: {
actionProps: PropsActionsFromBackboneForChildrenType;
containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
conversationId: string;
@ -112,7 +74,6 @@ function renderItem({
}): JSX.Element {
return (
<SmartTimelineItem
{...actionProps}
containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint}
conversationId={conversationId}
@ -134,18 +95,8 @@ function renderContactSpoofingReviewDialog(
return <SmartContactSpoofingReviewDialog {...props} />;
}
function renderHeroRow(
id: string,
unblurAvatar: () => void,
updateSharedGroups: () => unknown
): JSX.Element {
return (
<SmartHeroRow
id={id}
unblurAvatar={unblurAvatar}
updateSharedGroups={updateSharedGroups}
/>
);
function renderHeroRow(id: string): JSX.Element {
return <SmartHeroRow id={id} />;
}
function renderTypingBubble(id: string): JSX.Element {
return <SmartTypingBubble id={id} />;
@ -270,8 +221,8 @@ const getContactSpoofingReview = (
}
};
const mapStateToProps = (state: StateType, props: TimelinePropsType) => {
const { id, ...actions } = props;
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id } = props;
const conversation = getConversationSelector(state)(id);
@ -307,8 +258,6 @@ const mapStateToProps = (state: StateType, props: TimelinePropsType) => {
renderContactSpoofingReviewDialog,
renderHeroRow,
renderTypingBubble,
markViewed,
...actions,
};
};

View file

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

View file

@ -8,12 +8,6 @@ import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMem
import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed';
import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved';
import type {
ToastConversationArchived,
ToastPropsType as ToastConversationArchivedPropsType,
} from '../components/ToastConversationArchived';
import type { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
import type { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
import type {
ToastInternalError,
ToastPropsType as ToastInternalErrorPropsType,
@ -25,9 +19,7 @@ import type {
import type { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied';
import type { ToastLinkCopied } from '../components/ToastLinkCopied';
import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs';
import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
import type { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed';
import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment';
@ -36,12 +28,6 @@ export function showToast(Toast: typeof ToastAlreadyGroupMember): void;
export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): void;
export function showToast(Toast: typeof ToastCaptchaFailed): void;
export function showToast(Toast: typeof ToastCaptchaSolved): void;
export function showToast(
Toast: typeof ToastConversationArchived,
props: ToastConversationArchivedPropsType
): void;
export function showToast(Toast: typeof ToastConversationMarkedUnread): void;
export function showToast(Toast: typeof ToastConversationUnarchived): void;
export function showToast(
Toast: typeof ToastInternalError,
props: ToastInternalErrorPropsType
@ -53,8 +39,6 @@ export function showToast(
export function showToast(Toast: typeof ToastGroupLinkCopied): void;
export function showToast(Toast: typeof ToastLinkCopied): void;
export function showToast(Toast: typeof ToastLoadingFullLogs): void;
export function showToast(Toast: typeof ToastMessageBodyTooLong): void;
export function showToast(Toast: typeof ToastOriginalMessageNotFound): void;
export function showToast(Toast: typeof ToastStickerPackInstallFailed): void;
export function showToast(Toast: typeof ToastVoiceNoteLimit): void;
export function showToast(

View file

@ -8,43 +8,22 @@ import { render } from 'mustache';
import type { ConversationModel } from '../models/conversations';
import { getMessageById } from '../messages/getMessageById';
import { getContactId } from '../messages/helpers';
import { strictAssert } from '../util/assert';
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
import { isGroup } from '../util/whatTypeOfConversation';
import { getActiveCallState } from '../state/selectors/calling';
import { ReactWrapperView } from './ReactWrapperView';
import * as log from '../logging/log';
import { createConversationView } from '../state/roots/createConversationView';
import { ToastConversationArchived } from '../components/ToastConversationArchived';
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { showToast } from '../util/showToast';
import { UUIDKind } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import {
removeLinkPreview,
suspendLinkPreviews,
} from '../services/LinkPreview';
import { SECOND } from '../util/durations';
import { startConversation } from '../util/startConversation';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { clearConversationDraftAttachments } from '../util/clearConversationDraftAttachments';
import type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels';
import { PanelType, isPanelHandledByReact } from '../types/Panels';
import { UUIDKind } from '../types/UUID';
type BackbonePanelType = { panelType: PanelType; view: Backbone.View };
const { getMessagesBySentAt } = window.Signal.Data;
type MessageActionsType = {
showMessageDetail: (messageId: string) => unknown;
startConversation: (e164: string, uuid: UUIDStringType) => unknown;
};
export class ConversationView extends window.Backbone.View<ConversationModel> {
// Sub-views
private contactModalView?: Backbone.View;
@ -69,12 +48,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.unload(`model trigger - ${reason}`)
);
// These are triggered by background.ts for keyboard handling
this.listenTo(this.model, 'escape-pressed', () => {
window.reduxActions.conversations.popPanelForConversation(this.model.id);
});
this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
this.listenTo(this.model, 'pushPanel', this.pushPanel);
this.listenTo(this.model, 'popPanel', this.popPanel);
@ -116,209 +89,18 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
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
window.reduxActions.composer.resetComposer();
const compositionAreaProps = {
id: this.model.id,
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
onCancelJoinRequest: async () => {
await window.showConfirmationDialog({
dialogName: 'GroupV2CancelRequestToJoin',
message: window.i18n(
'GroupV2--join--cancel-request-to-join--confirmation'
),
okText: window.i18n('GroupV2--join--cancel-request-to-join--yes'),
cancelText: window.i18n('GroupV2--join--cancel-request-to-join--no'),
resolve: () => {
longRunningTaskWrapper({
idForLogging: this.model.idForLogging(),
name: 'onCancelJoinRequest',
task: async () => this.model.cancelJoinRequest(),
});
},
});
},
onClearAttachments: () =>
clearConversationDraftAttachments(
this.model.id,
this.model.get('draftAttachments')
),
onSelectMediaQuality: (isHQ: boolean) => {
window.reduxActions.composer.setMediaQualitySetting(isHQ);
},
onCloseLinkPreview: () => {
suspendLinkPreviews();
removeLinkPreview();
},
};
// createConversationView root
const JSX = createConversationView(window.reduxStore, {
conversationId: this.model.id,
compositionAreaProps,
conversationHeaderProps,
timelineProps,
});
this.conversationView = new ReactWrapperView({ JSX });
this.$('.ConversationView__template').append(this.conversationView.el);
}
getMessageActions(): MessageActionsType {
const showMessageDetail = (messageId: string) => {
this.showMessageDetail(messageId);
};
return {
showMessageDetail,
startConversation,
};
}
unload(reason: string): void {
log.info(
'unloading conversation',
@ -445,13 +227,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.model.updateVerified();
}
showMessageDetail(messageId: string): void {
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
type: PanelType.MessageDetails,
args: { messageId },
});
}
getMessageDetail({
messageId,
}: {
@ -459,7 +234,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}): Backbone.View | undefined {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(`showMessageDetail: Message ${messageId} missing!`);
throw new Error(`getMessageDetail: Message ${messageId} missing!`);
}
if (!message.isNormalBubble()) {
@ -470,7 +245,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
...message.getPropsForMessageDetail(
window.ConversationController.getOurConversationIdOrThrow()
),
...this.getMessageActions(),
});
const onClose = () => {