conversation_view: Move the last of the small functions to redux
This commit is contained in:
parent
86e92dda51
commit
1a68c3db62
59 changed files with 782 additions and 944 deletions
|
@ -97,6 +97,8 @@
|
|||
|
||||
&__linkNotification {
|
||||
@include font-body-2;
|
||||
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -68,7 +68,6 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
|
||||
showLightbox: shouldNeverBeCalled,
|
||||
showLightboxForViewOnceMedia: shouldNeverBeCalled,
|
||||
showMessageDetail: shouldNeverBeCalled,
|
||||
startConversation: shouldNeverBeCalled,
|
||||
theme: ThemeType.dark,
|
||||
viewStory: shouldNeverBeCalled,
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -55,6 +55,7 @@ const renderChange = (
|
|||
<GroupV2Change
|
||||
areWeAdmin={areWeAdmin ?? true}
|
||||
blockGroupLinkRequests={action('blockGroupLinkRequests')}
|
||||
conversationId="some-conversation-id"
|
||||
change={change}
|
||||
groupBannedMemberships={groupBannedMemberships}
|
||||
groupMemberships={groupMemberships}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 &
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
Loading…
Reference in a new issue