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