ConversationView: Pull various functions out of getMessageActions

This commit is contained in:
Scott Nonnenberg 2022-12-19 17:04:47 -08:00 committed by GitHub
parent 5a98fc2f4c
commit 1e282ee5d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 440 additions and 570 deletions

View file

@ -49,7 +49,7 @@ export default {
queueStoryDownload: {
action: true,
},
retrySend: {
retryMessageSend: {
action: true,
},
viewStory: { action: true },

View file

@ -27,7 +27,7 @@ export type PropsType = {
onForward: (storyId: string) => unknown;
onSave: (story: StoryViewType) => unknown;
queueStoryDownload: (storyId: string) => unknown;
retrySend: (messageId: string) => unknown;
retryMessageSend: (messageId: string) => unknown;
viewStory: ViewStoryActionCreatorType;
hasViewReceiptSetting: boolean;
};
@ -40,7 +40,7 @@ export function MyStories({
onForward,
onSave,
queueStoryDownload,
retrySend,
retryMessageSend,
viewStory,
hasViewReceiptSetting,
}: PropsType): JSX.Element {
@ -95,7 +95,7 @@ export function MyStories({
onForward={onForward}
onSave={onSave}
queueStoryDownload={queueStoryDownload}
retrySend={retrySend}
retryMessageSend={retryMessageSend}
setConfirmDeleteStory={setConfirmDeleteStory}
story={story}
viewStory={viewStory}
@ -120,7 +120,7 @@ type StorySentPropsType = Pick<
| 'onForward'
| 'onSave'
| 'queueStoryDownload'
| 'retrySend'
| 'retryMessageSend'
| 'viewStory'
> & {
setConfirmDeleteStory: (_: StoryViewType | undefined) => unknown;
@ -133,7 +133,7 @@ function StorySent({
onForward,
onSave,
queueStoryDownload,
retrySend,
retryMessageSend,
setConfirmDeleteStory,
story,
viewStory,
@ -155,7 +155,7 @@ function StorySent({
sendStatus === ResolvedSendStatus.PartiallySent)
) {
setWasManuallyRetried(true);
retrySend(story.messageId);
retryMessageSend(story.messageId);
return;
}

View file

@ -45,7 +45,7 @@ export default {
},
queueStoryDownload: { action: true },
renderStoryCreator: { action: true },
retrySend: { action: true },
retryMessageSend: { action: true },
showConversation: { action: true },
showStoriesSettings: { action: true },
showToast: { action: true },

View file

@ -42,7 +42,7 @@ export type PropsType = {
preferredWidthFromStorage: number;
queueStoryDownload: (storyId: string) => unknown;
renderStoryCreator: () => JSX.Element;
retrySend: (messageId: string) => unknown;
retryMessageSend: (messageId: string) => unknown;
setAddStoryData: (data: AddStoryData) => unknown;
showConversation: ShowConversationType;
showStoriesSettings: () => unknown;
@ -70,7 +70,7 @@ export function Stories({
preferredWidthFromStorage,
queueStoryDownload,
renderStoryCreator,
retrySend,
retryMessageSend,
setAddStoryData,
showConversation,
showStoriesSettings,
@ -111,7 +111,7 @@ export function Stories({
onForward={onForwardStory}
onSave={onSaveStory}
queueStoryDownload={queueStoryDownload}
retrySend={retrySend}
retryMessageSend={retryMessageSend}
viewStory={viewStory}
/>
) : (

View file

@ -56,7 +56,7 @@ export default {
},
queueStoryDownload: { action: true },
renderEmojiPicker: { action: true },
retrySend: { action: true },
retryMessageSend: { action: true },
showToast: { action: true },
skinTone: {
defaultValue: 0,

View file

@ -101,7 +101,7 @@ export type PropsType = {
recentEmojis?: Array<string>;
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
replyState?: ReplyStateType;
retrySend: (messageId: string) => unknown;
retryMessageSend: (messageId: string) => unknown;
saveAttachment: SaveAttachmentActionCreatorType;
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
showToast: ShowToastActionCreatorType;
@ -153,7 +153,7 @@ export function StoryViewer({
recentEmojis,
renderEmojiPicker,
replyState,
retrySend,
retryMessageSend,
saveAttachment,
setHasAllStoriesUnmuted,
showToast,
@ -555,7 +555,7 @@ export function StoryViewer({
];
}
function doRetrySend() {
function doRetryMessageSend() {
if (wasManuallyRetried) {
return;
}
@ -568,7 +568,7 @@ export function StoryViewer({
}
setWasManuallyRetried(true);
retrySend(messageId);
retryMessageSend(messageId);
}
return (
@ -800,7 +800,7 @@ export function StoryViewer({
{sendStatus === ResolvedSendStatus.Failed && !wasManuallyRetried && (
<button
className="StoryViewer__actions__failed"
onClick={doRetrySend}
onClick={doRetryMessageSend}
type="button"
>
{i18n('StoryViewer__failed')}
@ -810,7 +810,7 @@ export function StoryViewer({
!wasManuallyRetried && (
<button
className="StoryViewer__actions__failed"
onClick={doRetrySend}
onClick={doRetryMessageSend}
type="button"
>
{i18n('StoryViewer__partial-fail')}

View file

@ -54,7 +54,6 @@ const MESSAGE_DEFAULT_PROPS = {
isMessageRequestAccepted: true,
kickOffAttachmentDownload: shouldNeverBeCalled,
markAttachmentAsCorrupted: shouldNeverBeCalled,
markViewed: shouldNeverBeCalled,
messageExpanded: shouldNeverBeCalled,
openGiftBadge: shouldNeverBeCalled,
openLink: shouldNeverBeCalled,

View file

@ -1,24 +0,0 @@
// Copyright 2022 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 = {
i18n: LocalizerType;
isIncoming: boolean;
onClose: () => unknown;
};
export function ToastCannotOpenGiftBadge({
i18n,
isIncoming,
onClose,
}: ToastPropsType): JSX.Element {
const key = `message--giftBadge--unopened--toast--${
isIncoming ? 'incoming' : 'outgoing'
}`;
return <Toast onClose={onClose}>{i18n(key)}</Toast>;
}

View file

@ -70,6 +70,20 @@ CannotMixMultiAndNonMultiAttachments.args = {
},
};
export const CannotOpenGiftBadgeIncoming = Template.bind({});
CannotOpenGiftBadgeIncoming.args = {
toast: {
toastType: ToastType.CannotOpenGiftBadgeIncoming,
},
};
export const CannotOpenGiftBadgeOutgoing = Template.bind({});
CannotOpenGiftBadgeOutgoing.args = {
toast: {
toastType: ToastType.CannotOpenGiftBadgeOutgoing,
},
};
export const CannotStartGroupCall = Template.bind({});
CannotStartGroupCall.args = {
toast: {
@ -182,6 +196,13 @@ PinnedConversationsFull.args = {
},
};
export const ReactionFailed = Template.bind({});
ReactionFailed.args = {
toast: {
toastType: ToastType.ReactionFailed,
},
};
export const ReportedSpamAndBlocked = Template.bind({});
ReportedSpamAndBlocked.args = {
toast: {
@ -231,6 +252,20 @@ StoryVideoUnsupported.args = {
},
};
export const TapToViewExpiredIncoming = Template.bind({});
TapToViewExpiredIncoming.args = {
toast: {
toastType: ToastType.TapToViewExpiredIncoming,
},
};
export const TapToViewExpiredOutgoing = Template.bind({});
TapToViewExpiredOutgoing.args = {
toast: {
toastType: ToastType.TapToViewExpiredOutgoing,
},
};
export const UnableToLoadAttachment = Template.bind({});
UnableToLoadAttachment.args = {
toast: {

View file

@ -60,6 +60,22 @@ export function ToastManager({
);
}
if (toastType === ToastType.CannotOpenGiftBadgeIncoming) {
return (
<Toast onClose={hideToast}>
{i18n('message--giftBadge--unopened--toast--incoming')}
</Toast>
);
}
if (toastType === ToastType.CannotOpenGiftBadgeOutgoing) {
return (
<Toast onClose={hideToast}>
{i18n('message--giftBadge--unopened--toast--outgoing')}
</Toast>
);
}
if (toastType === ToastType.CannotStartGroupCall) {
return (
<Toast onClose={hideToast}>
@ -173,6 +189,10 @@ export function ToastManager({
return <Toast onClose={hideToast}>{i18n('pinnedConversationsFull')}</Toast>;
}
if (toastType === ToastType.ReactionFailed) {
return <Toast onClose={hideToast}>{i18n('Reactions--error')}</Toast>;
}
if (toastType === ToastType.StoryMuted) {
return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
@ -221,6 +241,22 @@ export function ToastManager({
);
}
if (toastType === ToastType.TapToViewExpiredIncoming) {
return (
<Toast onClose={hideToast}>
{i18n('Message--tap-to-view--incoming--expired-toast')}
</Toast>
);
}
if (toastType === ToastType.TapToViewExpiredOutgoing) {
return (
<Toast onClose={hideToast}>
{i18n('Message--tap-to-view--outgoing--expired-toast')}
</Toast>
);
}
if (toastType === ToastType.UnableToLoadAttachment) {
return <Toast onClose={hideToast}>{i18n('unableToLoadAttachment')}</Toast>;
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastReactionFailed } from './ToastReactionFailed';
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/ToastReactionFailed',
};
export const _ToastReactionFailed = (): JSX.Element => (
<ToastReactionFailed {...defaultProps} />
);
_ToastReactionFailed.story = {
name: 'ToastReactionFailed',
};

View file

@ -1,15 +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 ToastReactionFailed({ i18n, onClose }: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('Reactions--error')}</Toast>;
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastTapToViewExpiredIncoming } from './ToastTapToViewExpiredIncoming';
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/ToastTapToViewExpiredIncoming',
};
export const _ToastTapToViewExpiredIncoming = (): JSX.Element => (
<ToastTapToViewExpiredIncoming {...defaultProps} />
);
_ToastTapToViewExpiredIncoming.story = {
name: 'ToastTapToViewExpiredIncoming',
};

View file

@ -1,22 +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 ToastTapToViewExpiredIncoming({
i18n,
onClose,
}: PropsType): JSX.Element {
return (
<Toast onClose={onClose}>
{i18n('Message--tap-to-view--incoming--expired-toast')}
</Toast>
);
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastTapToViewExpiredOutgoing } from './ToastTapToViewExpiredOutgoing';
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/ToastTapToViewExpiredOutgoing',
};
export const _ToastTapToViewExpiredOutgoing = (): JSX.Element => (
<ToastTapToViewExpiredOutgoing {...defaultProps} />
);
_ToastTapToViewExpiredOutgoing.story = {
name: 'ToastTapToViewExpiredOutgoing',
};

View file

@ -1,22 +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 ToastTapToViewExpiredOutgoing({
i18n,
onClose,
}: PropsType): JSX.Element {
return (
<Toast onClose={onClose}>
{i18n('Message--tap-to-view--outgoing--expired-toast')}
</Toast>
);
}

View file

@ -91,6 +91,7 @@ import type { AnyPaymentEvent } from '../../types/Payment';
import { Emojify } from './Emojify';
import { getPaymentEventDescription } from '../../messages/helpers';
import { PanelType } from '../../types/Panels';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 16;
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
@ -317,7 +318,6 @@ export type PropsActions = {
attachment: AttachmentType;
messageId: string;
}) => void;
markViewed(messageId: string): void;
saveAttachment: SaveAttachmentActionCreatorType;
showLightbox: (options: {
attachment: AttachmentType;
@ -325,7 +325,6 @@ export type PropsActions = {
}) => void;
showLightboxForViewOnceMedia: (messageId: string) => unknown;
openLink: (url: string) => void;
scrollToQuotedMessage: (options: {
authorId: string;
sentAt: number;
@ -1072,7 +1071,6 @@ export class Message extends React.PureComponent<Props, State> {
i18n,
id,
kickOffAttachmentDownload,
openLink,
previews,
quote,
shouldCollapseAbove,
@ -1123,7 +1121,7 @@ export class Message extends React.PureComponent<Props, State> {
});
return;
}
openLink(first.url);
openLinkInWebBrowser(first.url);
}
: noop;
const contents = (
@ -1208,14 +1206,14 @@ export class Message extends React.PureComponent<Props, State> {
event.stopPropagation();
event.preventDefault();
openLink(first.url);
openLinkInWebBrowser(first.url);
}
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
openLink(first.url);
openLinkInWebBrowser(first.url);
}}
>
{contents}
@ -2277,15 +2275,8 @@ export class Message extends React.PureComponent<Props, State> {
return;
}
if (attachments && !isDownloaded(attachments[0])) {
event.preventDefault();
event.stopPropagation();
kickOffAttachmentDownload({
attachment: attachments[0],
messageId: id,
});
return;
}
event.preventDefault();
event.stopPropagation();
if (isTapToViewExpired) {
const action =
@ -2293,13 +2284,21 @@ export class Message extends React.PureComponent<Props, State> {
? showExpiredOutgoingTapToViewToast
: showExpiredIncomingTapToViewToast;
action();
} else {
event.preventDefault();
event.stopPropagation();
showLightboxForViewOnceMedia(id);
return;
}
if (attachments && !isDownloaded(attachments[0])) {
kickOffAttachmentDownload({
attachment: attachments[0],
messageId: id,
});
return;
}
showLightboxForViewOnceMedia(id);
return;
}

View file

@ -77,10 +77,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
markViewed: action('markViewed'),
showConversation: action('showConversation'),
openGiftBadge: action('openGiftBadge'),
openLink: action('openLink'),
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
saveAttachment: action('saveAttachment'),
pushPanelForConversation: action('pushPanelForConversation'),

View file

@ -68,7 +68,6 @@ export type PropsData = {
i18n: LocalizerType;
theme: ThemeType;
getPreferredBadge: PreferredBadgeSelectorType;
markViewed: (messageId: string) => void;
} & Pick<
MessagePropsType,
| 'getPreferredBadge'
@ -79,14 +78,7 @@ export type PropsData = {
export type PropsBackboneActions = Pick<
MessagePropsType,
| 'kickOffAttachmentDownload'
| 'markAttachmentAsCorrupted'
| 'openGiftBadge'
| 'openLink'
| 'renderAudioAttachment'
| 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast'
| 'startConversation'
'renderAudioAttachment' | 'startConversation'
>;
export type PropsReduxActions = Pick<
@ -94,10 +86,15 @@ export type PropsReduxActions = Pick<
| 'checkForAccount'
| 'clearSelectedMessage'
| 'doubleCheckMissingQuoteReference'
| 'kickOffAttachmentDownload'
| 'markAttachmentAsCorrupted'
| 'openGiftBadge'
| 'pushPanelForConversation'
| 'saveAttachment'
| 'showContactModal'
| 'showConversation'
| 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast'
| 'showLightbox'
| 'showLightboxForViewOnceMedia'
| 'viewStory'
@ -289,9 +286,7 @@ export class MessageDetail extends React.Component<Props> {
interactionMode,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed,
openGiftBadge,
openLink,
pushPanelForConversation,
renderAudioAttachment,
saveAttachment,
@ -334,11 +329,9 @@ export class MessageDetail extends React.Component<Props> {
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
markViewed={markViewed}
messageExpanded={noop}
showConversation={showConversation}
openGiftBadge={openGiftBadge}
openLink={openLink}
pushPanelForConversation={pushPanelForConversation}
renderAudioAttachment={renderAudioAttachment}
saveAttachment={saveAttachment}

View file

@ -110,11 +110,9 @@ const defaultMessageProps: TimelineMessagesProps = {
isMessageRequestAccepted: true,
kickOffAttachmentDownload: action('default--kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
markViewed: action('default--markViewed'),
messageExpanded: action('default--message-expanded'),
showConversation: action('default--showConversation'),
openGiftBadge: action('openGiftBadge'),
openLink: action('default--openLink'),
previews: [],
reactToMessage: action('default--reactToMessage'),
readStatus: ReadStatus.Read,
@ -122,7 +120,7 @@ const defaultMessageProps: TimelineMessagesProps = {
renderReactionPicker: () => <div />,
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
setQuoteByMessageId: action('default--setQuoteByMessageId'),
retrySend: action('default--retrySend'),
retryMessageSend: action('default--retryMessageSend'),
retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
saveAttachment: action('saveAttachment'),
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),

View file

@ -278,23 +278,22 @@ const actions = () => ({
reactToMessage: action('reactToMessage'),
setQuoteByMessageId: action('setQuoteByMessageId'),
retryDeleteForEveryone: action('retryDeleteForEveryone'),
retrySend: action('retrySend'),
retryMessageSend: action('retryMessageSend'),
deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
showMessageDetail: action('showMessageDetail'),
saveAttachment: action('saveAttachment'),
pushPanelForConversation: action('pushPanelForConversation'),
showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'),
showConversation: action('showConversation'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
markViewed: action('markViewed'),
messageExpanded: action('messageExpanded'),
showLightbox: action('showLightbox'),
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
openLink: action('openLink'),
openGiftBadge: action('openGiftBadge'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
showExpiredIncomingTapToViewToast: action(
@ -307,8 +306,6 @@ const actions = () => ({
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
downloadNewVersion: action('downloadNewVersion'),
startCallingLobby: action('startCallingLobby'),
startConversation: action('startConversation'),
returnToActiveCall: action('returnToActiveCall'),

View file

@ -19,11 +19,8 @@ import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary';
import { WidthBreakpoint } from '../_util';
import type { PropsActions as MessageActionsType } from './TimelineMessage';
import type { PropsActions as UnsupportedMessageActionsType } from './UnsupportedMessage';
import type { PropsActionsType as ChatSessionRefreshedNotificationActionsType } from './ChatSessionRefreshedNotification';
import type { PropsActionsType as GroupV2ChangeActionsType } from './GroupV2Change';
import { ErrorBoundary } from './ErrorBoundary';
import type { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
import { Intl } from '../Intl';
import { TimelineWarning } from './TimelineWarning';
import { TimelineWarnings } from './TimelineWarnings';
@ -47,6 +44,8 @@ import {
} from '../../util/scrollUtil';
import { LastSeenIndicator } from './LastSeenIndicator';
import { MINUTE } from '../../util/durations';
import type { PropsActionsType as DeliveryIssueNotificationActionsType } from './DeliveryIssueNotification';
import type { PropsActionsType as GroupV2ChangeActionsType } from './GroupV2Change';
const AT_BOTTOM_THRESHOLD = 15;
const AT_BOTTOM_DETECTOR_STYLE = { height: AT_BOTTOM_THRESHOLD };
@ -125,7 +124,7 @@ type PropsHousekeepingType = {
theme: ThemeType;
renderItem: (props: {
actionProps: PropsActionsType;
actionProps: PropsActionsFromBackboneForChildrenType;
containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
conversationId: string;
@ -146,41 +145,47 @@ type PropsHousekeepingType = {
) => JSX.Element;
};
export type PropsActionsFromBackboneForChildrenType = Pick<
MessageActionsType,
'scrollToQuotedMessage' | 'showMessageDetail' | 'startConversation'
> &
ChatSessionRefreshedNotificationActionsType &
DeliveryIssueNotificationActionsType &
GroupV2ChangeActionsType;
export type PropsActionsType = {
// From Backbone
acknowledgeGroupMemberNameCollisions: (
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
) => void;
loadOlderMessages: (messageId: string) => unknown;
loadNewerMessages: (messageId: string) => unknown;
loadNewestMessages: (messageId: string, setFocus?: boolean) => unknown;
markMessageRead: (messageId: string) => unknown;
removeMember: (conversationId: string) => unknown;
unblurAvatar: () => void;
updateSharedGroups: () => unknown;
// From Redux
acceptConversation: (conversationId: string) => unknown;
blockConversation: (conversationId: string) => unknown;
blockAndReportSpam: (conversationId: string) => unknown;
clearInvitedUuidsForNewlyCreatedGroup: () => void;
clearSelectedMessage: () => unknown;
closeContactSpoofingReview: () => void;
deleteConversation: (conversationId: string) => unknown;
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
peekGroupCallForTheFirstTime: (conversationId: string) => unknown;
peekGroupCallIfItHasMembers: (conversationId: string) => unknown;
reviewGroupMemberNameCollision: (groupConversationId: string) => void;
reviewMessageRequestNameCollision: (
_: Readonly<{
safeConversationId: string;
}>
) => void;
learnMoreAboutDeliveryIssue: () => unknown;
loadOlderMessages: (messageId: string) => unknown;
loadNewerMessages: (messageId: string) => unknown;
loadNewestMessages: (messageId: string, setFocus?: boolean) => unknown;
markMessageRead: (messageId: string) => unknown;
blockConversation: (conversationId: string) => unknown;
blockAndReportSpam: (conversationId: string) => unknown;
deleteConversation: (conversationId: string) => unknown;
acceptConversation: (conversationId: string) => unknown;
peekGroupCallForTheFirstTime: (conversationId: string) => unknown;
peekGroupCallIfItHasMembers: (conversationId: string) => unknown;
removeMember: (conversationId: string) => unknown;
selectMessage: (messageId: string, conversationId: string) => unknown;
clearSelectedMessage: () => unknown;
unblurAvatar: () => void;
updateSharedGroups: () => unknown;
} & MessageActionsType &
SafetyNumberActionsType &
UnsupportedMessageActionsType &
GroupV2ChangeActionsType &
ChatSessionRefreshedNotificationActionsType;
showContactModal: (contactId: string, conversationId?: string) => void;
} & PropsActionsFromBackboneForChildrenType;
export type PropsType = PropsDataType &
PropsHousekeepingType &
@ -209,69 +214,29 @@ const getActions = createSelector(
// use `createSelector` to memoize them by the last seen `props` object.
(props: PropsType) => props,
(props: PropsType): PropsActionsType => {
(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, [
'acknowledgeGroupMemberNameCollisions',
'blockGroupLinkRequests',
'clearInvitedUuidsForNewlyCreatedGroup',
'closeContactSpoofingReview',
'setIsNearBottom',
'reviewGroupMemberNameCollision',
'reviewMessageRequestNameCollision',
'learnMoreAboutDeliveryIssue',
'loadOlderMessages',
'loadNewerMessages',
'loadNewestMessages',
'markMessageRead',
'markViewed',
'acceptConversation',
'blockAndReportSpam',
'blockConversation',
'deleteConversation',
'peekGroupCallForTheFirstTime',
'peekGroupCallIfItHasMembers',
'removeMember',
'selectMessage',
'clearSelectedMessage',
'unblurAvatar',
'updateSharedGroups',
'doubleCheckMissingQuoteReference',
'checkForAccount',
'reactToMessage',
'retryDeleteForEveryone',
'retrySend',
'toggleForwardMessageModal',
'deleteMessage',
'deleteMessageForEveryone',
'showConversation',
'showMessageDetail',
'openGiftBadge',
'setQuoteByMessageId',
'showContactModal',
'kickOffAttachmentDownload',
'markAttachmentAsCorrupted',
'messageExpanded',
'saveAttachment',
'showLightbox',
'showLightboxForViewOnceMedia',
'openLink',
'pushPanelForConversation',
// MessageActionsType
'scrollToQuotedMessage',
'showExpiredIncomingTapToViewToast',
'showExpiredOutgoingTapToViewToast',
'showMessageDetail',
'startConversation',
'toggleSafetyNumberModal',
'downloadNewVersion',
// ChatSessionRefreshedNotificationActionsType
'contactSupport',
'viewStory',
// DeliveryIssueNotificationActionsType
'learnMoreAboutDeliveryIssue',
// GroupV2ChangeActionsType
'blockGroupLinkRequests',
]);
const safe: AssertProps<PropsActionsType, typeof unsafe> = unsafe;
const safe: AssertProps<
PropsActionsFromBackboneForChildrenType,
typeof unsafe
> = unsafe;
return safe;
}

View file

@ -68,14 +68,13 @@ const getDefaultProps = () => ({
contactSupport: action('contactSupport'),
setQuoteByMessageId: action('setQuoteByMessageId'),
retryDeleteForEveryone: action('retryDeleteForEveryone'),
retrySend: action('retrySend'),
retryMessageSend: action('retryMessageSend'),
blockGroupLinkRequests: action('blockGroupLinkRequests'),
deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
learnMoreAboutDeliveryIssue: action('learnMoreAboutDeliveryIssue'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
markViewed: action('markViewed'),
messageExpanded: action('messageExpanded'),
showMessageDetail: action('showMessageDetail'),
showConversation: action('showConversation'),
@ -93,9 +92,7 @@ const getDefaultProps = () => ({
showExpiredOutgoingTapToViewToast: action(
'showExpiredIncomingTapToViewToast'
),
openLink: action('openLink'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
downloadNewVersion: action('downloadNewVersion'),
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
startCallingLobby: action('startCallingLobby'),
startConversation: action('startConversation'),

View file

@ -26,10 +26,7 @@ import type { PropsData as ChangeNumberNotificationProps } from './ChangeNumberN
import { ChangeNumberNotification } from './ChangeNumberNotification';
import type { CallingNotificationType } from '../../util/callingNotification';
import { InlineNotificationWrapper } from './InlineNotificationWrapper';
import type {
PropsActions as UnsupportedMessageActionsType,
PropsData as UnsupportedMessageProps,
} from './UnsupportedMessage';
import type { PropsData as UnsupportedMessageProps } from './UnsupportedMessage';
import { UnsupportedMessage } from './UnsupportedMessage';
import type { PropsData as TimerNotificationProps } from './TimerNotification';
import { TimerNotification } from './TimerNotification';
@ -177,7 +174,6 @@ type PropsActionsType = MessageActionsType &
DeliveryIssueActionProps &
GroupV2ChangeActionsType &
PropsChatSessionRefreshedActionsType &
UnsupportedMessageActionsType &
SafetyNumberActionsType;
export type PropsType = PropsLocalType &

View file

@ -277,11 +277,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
isTapToViewExpired: overrideProps.isTapToViewExpired,
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
markViewed: action('markViewed'),
messageExpanded: action('messageExpanded'),
showConversation: action('showConversation'),
openGiftBadge: action('openGiftBadge'),
openLink: action('openLink'),
previews: overrideProps.previews || [],
quote: overrideProps.quote || undefined,
reactions: overrideProps.reactions,
@ -295,7 +293,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
renderAudioAttachment,
saveAttachment: action('saveAttachment'),
setQuoteByMessageId: action('setQuoteByMessageId'),
retrySend: action('retrySend'),
retryMessageSend: action('retryMessageSend'),
retryDeleteForEveryone: action('retryDeleteForEveryone'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
selectMessage: action('selectMessage'),

View file

@ -49,7 +49,7 @@ export type PropsActions = {
id: string,
{ emoji, remove }: { emoji: string; remove: boolean }
) => void;
retrySend: (id: string) => void;
retryMessageSend: (id: string) => void;
retryDeleteForEveryone: (id: string) => void;
setQuoteByMessageId: (conversationId: string, messageId: string) => void;
} & MessagePropsActions;
@ -99,7 +99,7 @@ export function TimelineMessage(props: Props): JSX.Element {
setQuoteByMessageId,
renderReactionPicker,
renderEmojiPicker,
retrySend,
retryMessageSend,
retryDeleteForEveryone,
selectedReaction,
toggleForwardMessageModal,
@ -395,7 +395,7 @@ export function TimelineMessage(props: Props): JSX.Element {
onDownload={handleDownload}
onReplyToMessage={handleReplyToMessage}
onReact={handleReact}
onRetrySend={canRetry ? () => retrySend(id) : undefined}
onRetryMessageSend={canRetry ? () => retryMessageSend(id) : undefined}
onRetryDeleteForEveryone={
canRetryDeleteForEveryone
? () => retryDeleteForEveryone(id)
@ -575,7 +575,7 @@ type MessageContextProps = {
onDownload: (() => void) | undefined;
onReplyToMessage: (() => void) | undefined;
onReact: (() => void) | undefined;
onRetrySend: (() => void) | undefined;
onRetryMessageSend: (() => void) | undefined;
onRetryDeleteForEveryone: (() => void) | undefined;
onForward: (() => void) | undefined;
onDeleteForMe: () => void;
@ -591,7 +591,7 @@ const MessageContextMenu = ({
onReplyToMessage,
onReact,
onMoreInfo,
onRetrySend,
onRetryMessageSend,
onRetryDeleteForEveryone,
onForward,
onDeleteForMe,
@ -660,7 +660,7 @@ const MessageContextMenu = ({
>
{i18n('moreInfo')}
</MenuItem>
{onRetrySend && (
{onRetryMessageSend && (
<MenuItem
attributes={{
className:
@ -670,7 +670,7 @@ const MessageContextMenu = ({
event.stopPropagation();
event.preventDefault();
onRetrySend();
onRetryMessageSend();
}}
>
{i18n('retrySend')}

View file

@ -3,7 +3,6 @@
import * as React from 'react';
import { boolean, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
@ -26,7 +25,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
i18n,
canProcessNow: boolean('canProcessNow', overrideProps.canProcessNow || false),
contact: overrideProps.contact || ({} as ContactType),
downloadNewVersion: action('downloadNewVersion'),
});
export function FromSomeone(): JSX.Element {

View file

@ -8,6 +8,7 @@ import { Button, ButtonSize, ButtonVariant } from '../Button';
import { ContactName } from './ContactName';
import { Intl } from '../Intl';
import type { LocalizerType } from '../../types/Util';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
export type ContactType = {
id: string;
@ -23,21 +24,16 @@ export type PropsData = {
contact: ContactType;
};
export type PropsActions = {
downloadNewVersion: () => unknown;
};
type PropsHousekeeping = {
i18n: LocalizerType;
};
export type Props = PropsData & PropsHousekeeping & PropsActions;
export type Props = PropsData & PropsHousekeeping;
export function UnsupportedMessage({
canProcessNow,
contact,
i18n,
downloadNewVersion,
}: Props): JSX.Element {
const { isMe } = contact;
@ -75,7 +71,7 @@ export function UnsupportedMessage({
<div className="SystemMessage__line">
<Button
onClick={() => {
downloadNewVersion();
openLinkInWebBrowser('https://signal.org/download');
}}
size={ButtonSize.Small}
variant={ButtonVariant.SystemMessage}

View file

@ -67,6 +67,7 @@ import { writeDraftAttachment } from '../../util/writeDraftAttachment';
import { getMessageById } from '../../messages/getMessageById';
import { canReply } from '../selectors/message';
import { getConversationSelector } from '../selectors/conversations';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { useBoundActions } from '../../hooks/useBoundActions';
// State
@ -143,6 +144,7 @@ export const actions = {
addPendingAttachment,
onEditorStateChange,
processAttachments,
reactToMessage,
removeAttachment,
replaceAttachments,
resetComposer,
@ -810,6 +812,44 @@ function replaceAttachments(
};
}
function reactToMessage(
messageId: string,
reaction: { emoji: string; remove: boolean }
): ThunkAction<
void,
RootStateType,
unknown,
NoopActionType | ShowToastActionType
> {
return async dispatch => {
const { emoji, remove } = reaction;
try {
await enqueueReactionForSend({
messageId,
emoji,
remove,
});
dispatch({
type: 'NOOP',
payload: null,
});
} catch (error) {
log.error(
'reactToMessage: Error sending reaction',
error,
messageId,
reaction
);
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.ReactionFailed,
},
});
}
};
}
function resetComposer(): ResetComposerActionType {
return {
type: RESET_COMPOSER,

View file

@ -90,7 +90,10 @@ import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
import type { NoopActionType } from './noop';
import { conversationJobQueue } from '../../jobs/conversationJobQueue';
import {
conversationJobQueue,
conversationQueueJobEnum,
} from '../../jobs/conversationJobQueue';
import type { TimelineMessageLoadingState } from '../../util/timelineUtil';
import {
isDirectConversation,
@ -121,6 +124,9 @@ import {
} from '../../groups';
import { getMessageById } from '../../messages/getMessageById';
import type { PanelRenderType } from '../../types/Panels';
import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue';
import { isOlderThan } from '../../util/timestamp';
import { DAY } from '../../util/durations';
import { isNotNil } from '../../util/isNotNil';
// State
@ -893,14 +899,17 @@ export const actions = {
generateNewGroupLink,
getProfilesForConversation,
initiateMigrationToGroupV2,
kickOffAttachmentDownload,
leaveGroup,
loadRecentMediaItems,
markAttachmentAsCorrupted,
messageChanged,
messageDeleted,
messageExpanded,
messagesAdded,
messagesReset,
myProfileChanged,
openGiftBadge,
popPanelForConversation,
pushPanelForConversation,
removeAllConversations,
@ -910,6 +919,8 @@ export const actions = {
repairOldestMessage,
replaceAvatar,
resetAllChatColors,
retryDeleteForEveryone,
retryMessageSend,
reviewGroupMemberNameCollision,
reviewMessageRequestNameCollision,
revokePendingMembershipsFromGroupV2,
@ -938,6 +949,8 @@ export const actions = {
showArchivedConversations,
showChooseGroupMembers,
showConversation,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showInbox,
startComposing,
startSettingGroupMetadata,
@ -1561,6 +1574,136 @@ function resetAllChatColors(): ThunkAction<
};
}
function kickOffAttachmentDownload(
options: Readonly<{ messageId: string }>
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(options.messageId);
if (!message) {
throw new Error(
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
);
}
await message.queueAttachmentDownloads();
dispatch({
type: 'NOOP',
payload: null,
});
};
}
type AttachmentOptions = {
messageId: string;
attachment: AttachmentType;
};
function markAttachmentAsCorrupted(
options: AttachmentOptions
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(options.messageId);
if (!message) {
throw new Error(
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
);
}
message.markAttachmentAsCorrupted(options.attachment);
dispatch({
type: 'NOOP',
payload: null,
});
};
}
function openGiftBadge(
messageId: string
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
if (!message) {
throw new Error(`openGiftBadge: Message ${messageId} missing!`);
}
dispatch({
type: SHOW_TOAST,
payload: {
toastType: isIncoming(message.attributes)
? ToastType.CannotOpenGiftBadgeIncoming
: ToastType.CannotOpenGiftBadgeOutgoing,
},
});
};
}
function retryMessageSend(
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
if (!message) {
throw new Error(`retryMessageSend: Message ${messageId} missing!`);
}
await message.retrySend();
dispatch({
type: 'NOOP',
payload: null,
});
};
}
export function retryDeleteForEveryone(
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
if (!message) {
throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`);
}
if (isOlderThan(message.get('sent_at'), DAY)) {
throw new Error(
'retryDeleteForEveryone: Message too old to retry delete for everyone!'
);
}
try {
const conversation = message.getConversation();
if (!conversation) {
throw new Error(
`retryDeleteForEveryone: Conversation for ${messageId} missing!`
);
}
const jobData: ConversationQueueJobData = {
type: conversationQueueJobEnum.enum.DeleteForEveryone,
conversationId: conversation.id,
messageId,
recipients: conversation.getRecipients(),
revision: conversation.get('revision'),
targetTimestamp: message.get('sent_at'),
};
log.info(
`retryDeleteForEveryone: Adding job for message ${message.idForLogging()}!`
);
await conversationJobQueue.add(jobData);
dispatch({
type: 'NOOP',
payload: null,
});
} catch (error) {
log.error(
'retryDeleteForEveryone: Failed to queue delete for everyone',
Errors.toLogFormat(error)
);
}
};
}
// update the conversation voice note playback rate preference for the conversation
export function setVoiceNotePlaybackRate({
conversationId,
@ -2990,6 +3133,27 @@ function updateConversationModelSharedGroups(
};
}
function showExpiredIncomingTapToViewToast(): ShowToastActionType {
log.info(
'showExpiredIncomingTapToViewToastShowing expired tap-to-view toast for an incoming message'
);
return {
type: SHOW_TOAST,
payload: {
toastType: ToastType.TapToViewExpiredIncoming,
},
};
}
function showExpiredOutgoingTapToViewToast(): ShowToastActionType {
log.info('Showing expired tap-to-view toast for an outgoing message');
return {
type: SHOW_TOAST,
payload: {
toastType: ToastType.TapToViewExpiredOutgoing,
},
};
}
function showInbox(): ShowInboxActionType {
return {
type: 'SHOW_INBOX',

View file

@ -24,7 +24,6 @@ import dataInterface from '../../sql/Client';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
import { assertDev } from '../../util/assert';
import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified';
import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
@ -35,7 +34,6 @@ import { markOnboardingStoryAsRead } from '../../util/markOnboardingStoryAsRead'
import { markViewed } from '../../services/MessageUpdater';
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
import { replaceIndex } from '../../util/replaceIndex';
import { showToast } from '../../util/showToast';
import type { DurationInSeconds } from '../../util/durations';
import { hasFailed, isDownloaded, isDownloading } from '../../types/Attachment';
import {
@ -57,6 +55,9 @@ import { verifyStoryListMembers as doVerifyStoryListMembers } from '../../util/v
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
import { getOwn } from '../../util/getOwn';
import { SHOW_TOAST } from './toast';
import { ToastType } from '../../types/Toast';
import type { ShowToastActionType } from './toast';
export type StoryDataType = {
attachment?: AttachmentType;
@ -492,7 +493,12 @@ function queueStoryDownload(
function reactToStory(
nextReaction: string,
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
): ThunkAction<
void,
RootStateType,
unknown,
ShowToastActionType | NoopActionType
> {
return async dispatch => {
try {
await enqueueReactionForSend({
@ -500,15 +506,19 @@ function reactToStory(
emoji: nextReaction,
remove: false,
});
dispatch({
type: 'NOOP',
payload: null,
});
} catch (error) {
log.error('Error enqueuing reaction', error, messageId, nextReaction);
showToast(ToastReactionFailed);
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.ReactionFailed,
},
});
}
dispatch({
type: 'NOOP',
payload: null,
});
};
}

View file

@ -8,6 +8,7 @@ import type { NoopActionType } from './noop';
import type { ReplacementValuesType } from '../../types/Util';
import { useBoundActions } from '../../hooks/useBoundActions';
import type { ToastType } from '../../types/Toast';
// State
export type ToastStateType = {

View file

@ -12,7 +12,7 @@ import { SmartMessageDetail } from '../smart/MessageDetail';
export const createMessageDetail = (
store: Store,
props: Omit<OwnProps, 'markViewed'>
props: OwnProps
): ReactElement => (
<Provider store={store}>
<SmartMessageDetail {...props} />

View file

@ -12,7 +12,6 @@ import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
import { renderAudioAttachment } from './renderAudioAttachment';
import { getContactNameColorSelector } from '../selectors/conversations';
import { markViewed } from '../ducks/conversations';
export { Contact } from '../../components/conversation/MessageDetail';
export type OwnProps = Omit<
@ -26,7 +25,6 @@ export type OwnProps = Omit<
| 'theme'
| 'showContactModal'
| 'showConversation'
| 'markViewed'
>;
const mapStateToProps = (
@ -40,12 +38,6 @@ const mapStateToProps = (
receivedAt,
sentAt,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
openGiftBadge,
openLink,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
startConversation,
} = props;
@ -72,14 +64,7 @@ const mapStateToProps = (
interactionMode: getInteractionMode(state),
theme: getTheme(state),
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed,
openGiftBadge,
openLink,
renderAudioAttachment,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
startConversation,
};
};

View file

@ -21,7 +21,6 @@ import {
getStories,
shouldShowStoriesView,
} from '../selectors/stories';
import { retryMessageSend } from '../../util/retryMessageSend';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoriesActions } from '../ducks/stories';
@ -33,8 +32,12 @@ function renderStoryCreator(): JSX.Element {
export function SmartStories(): JSX.Element | null {
const storiesActions = useStoriesActions();
const { saveAttachment, showConversation, toggleHideStories } =
useConversationsActions();
const {
retryMessageSend,
saveAttachment,
showConversation,
toggleHideStories,
} = useConversationsActions();
const { showStoriesSettings, toggleForwardMessageModal } =
useGlobalModalActions();
const { showToast } = useToastActions();
@ -83,7 +86,7 @@ export function SmartStories(): JSX.Element | null {
}}
preferredWidthFromStorage={preferredWidthFromStorage}
renderStoryCreator={renderStoryCreator}
retrySend={retryMessageSend}
retryMessageSend={retryMessageSend}
showConversation={showConversation}
showStoriesSettings={showStoriesSettings}
showToast={showToast}

View file

@ -29,7 +29,6 @@ import {
import { isInFullScreenCall } from '../selectors/calling';
import { isSignalConversation } from '../../util/isSignalConversation';
import { renderEmojiPicker } from './renderEmojiPicker';
import { retryMessageSend } from '../../util/retryMessageSend';
import { strictAssert } from '../../util/assert';
import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled';
import { useActions as useEmojisActions } from '../ducks/emojis';
@ -42,8 +41,12 @@ import { useIsWindowActive } from '../../hooks/useIsWindowActive';
export function SmartStoryViewer(): JSX.Element | null {
const storiesActions = useStoriesActions();
const { onUseEmoji } = useEmojisActions();
const { saveAttachment, showConversation, toggleHideStories } =
useConversationsActions();
const {
retryMessageSend,
saveAttachment,
showConversation,
toggleHideStories,
} = useConversationsActions();
const { onSetSkinTone } = useItemsActions();
const { showToast } = useToastActions();
@ -135,7 +138,7 @@ export function SmartStoryViewer(): JSX.Element | null {
recentEmojis={recentEmojis}
renderEmojiPicker={renderEmojiPicker}
replyState={replyState}
retrySend={retryMessageSend}
retryMessageSend={retryMessageSend}
showToast={showToast}
skinTone={skinTone}
story={storyView}

View file

@ -8,10 +8,10 @@ import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type {
PropsActionsType as TimelineActionsType,
ContactSpoofingReviewPropType,
WarningType as TimelineWarningType,
PropsType as ComponentPropsType,
PropsActionsFromBackboneForChildrenType,
} from '../../components/conversation/Timeline';
import { Timeline } from '../../components/conversation/Timeline';
import type { StateType } from '../reducer';
@ -62,30 +62,31 @@ type ExternalProps = {
export type TimelinePropsType = ExternalProps &
Pick<
ComponentPropsType,
// All of these are the ones we need from backbone
// Used by Timeline itself
| 'acknowledgeGroupMemberNameCollisions'
| 'contactSupport'
| 'blockGroupLinkRequests'
| 'downloadNewVersion'
| 'kickOffAttachmentDownload'
| 'learnMoreAboutDeliveryIssue'
| 'loadOlderMessages'
| 'loadNewerMessages'
| 'loadNewestMessages'
| 'loadOlderMessages'
| 'markAttachmentAsCorrupted'
| 'markMessageRead'
| 'openGiftBadge'
| 'openLink'
| 'reactToMessage'
| 'removeMember'
| 'retryDeleteForEveryone'
| 'retrySend'
| 'scrollToQuotedMessage'
| 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast'
| 'showMessageDetail'
| 'startConversation'
| 'unblurAvatar'
| 'updateSharedGroups'
// MessageActionsType
| 'scrollToQuotedMessage'
| 'showMessageDetail'
| 'startConversation'
// ChatSessionRefreshedNotificationActionsType
| 'contactSupport'
// DeliveryIssueNotificationActionsType
| 'learnMoreAboutDeliveryIssue'
// GroupV2ChangeActionsType
| 'blockGroupLinkRequests'
>;
function renderItem({
@ -99,7 +100,7 @@ function renderItem({
previousMessageId,
unreadIndicatorPlacement,
}: {
actionProps: TimelineActionsType;
actionProps: PropsActionsFromBackboneForChildrenType;
containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
conversationId: string;
@ -269,7 +270,7 @@ const getContactSpoofingReview = (
}
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const mapStateToProps = (state: StateType, props: TimelinePropsType) => {
const { id, ...actions } = props;
const conversation = getConversationSelector(state)(id);

View file

@ -6,6 +6,8 @@ export enum ToastType {
Blocked = 'Blocked',
BlockedGroup = 'BlockedGroup',
CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments',
CannotOpenGiftBadgeIncoming = 'CannotOpenGiftBadgeIncoming',
CannotOpenGiftBadgeOutgoing = 'CannotOpenGiftBadgeOutgoing',
CannotStartGroupCall = 'CannotStartGroupCall',
CopiedUsername = 'CopiedUsername',
CopiedUsernameLink = 'CopiedUsernameLink',
@ -21,6 +23,7 @@ export enum ToastType {
MaxAttachments = 'MaxAttachments',
MessageBodyTooLong = 'MessageBodyTooLong',
PinnedConversationsFull = 'PinnedConversationsFull',
ReactionFailed = 'ReactionFailed',
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
StoryMuted = 'StoryMuted',
StoryReact = 'StoryReact',
@ -28,6 +31,8 @@ export enum ToastType {
StoryVideoError = 'StoryVideoError',
StoryVideoTooLong = 'StoryVideoTooLong',
StoryVideoUnsupported = 'StoryVideoUnsupported',
TapToViewExpiredIncoming = 'TapToViewExpiredIncoming',
TapToViewExpiredOutgoing = 'TapToViewExpiredOutgoing',
UnableToLoadAttachment = 'UnableToLoadAttachment',
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
UserAddedToGroup = 'UserAddedToGroup',

View file

@ -1,55 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
import {
conversationJobQueue,
conversationQueueJobEnum,
} from '../jobs/conversationJobQueue';
import { isOlderThan } from './timestamp';
import { DAY } from './durations';
export async function retryDeleteForEveryone(messageId: string): Promise<void> {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`);
}
if (isOlderThan(message.get('sent_at'), DAY)) {
throw new Error(
'retryDeleteForEveryone: Message too old to retry delete for everyone!'
);
}
try {
const conversation = message.getConversation();
if (!conversation) {
throw new Error(
`retryDeleteForEveryone: Conversation for ${messageId} missing!`
);
}
const jobData: ConversationQueueJobData = {
type: conversationQueueJobEnum.enum.DeleteForEveryone,
conversationId: conversation.id,
messageId,
recipients: conversation.getRecipients(),
revision: conversation.get('revision'),
targetTimestamp: message.get('sent_at'),
};
log.info(
`retryDeleteForEveryone: Adding job for message ${message.idForLogging()}!`
);
await conversationJobQueue.add(jobData);
} catch (error) {
log.error(
'retryDeleteForEveryone: Failed to queue delete for everyone',
Errors.toLogFormat(error)
);
throw error;
}
}

View file

@ -1,12 +0,0 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { getMessageById } from '../messages/getMessageById';
export async function retryMessageSend(messageId: string): Promise<void> {
const message = await getMessageById(messageId);
if (!message) {
throw new Error(`retryMessageSend: Message ${messageId} missing!`);
}
await message.retrySend();
}

View file

@ -6,10 +6,6 @@ import { render, unmountComponentAtNode } from 'react-dom';
import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember';
import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
import type {
ToastCannotOpenGiftBadge,
ToastPropsType as ToastCannotOpenGiftBadgePropsType,
} from '../components/ToastCannotOpenGiftBadge';
import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed';
import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved';
import type {
@ -32,19 +28,12 @@ import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs';
import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
import type { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
import type { ToastReactionFailed } from '../components/ToastReactionFailed';
import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed';
import type { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
import type { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment';
export function showToast(Toast: typeof ToastAlreadyGroupMember): void;
export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): void;
export function showToast(
Toast: typeof ToastCannotOpenGiftBadge,
props: Omit<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'>
): void;
export function showToast(Toast: typeof ToastCaptchaFailed): void;
export function showToast(Toast: typeof ToastCaptchaSolved): void;
export function showToast(
@ -66,10 +55,7 @@ export function showToast(Toast: typeof ToastLinkCopied): void;
export function showToast(Toast: typeof ToastLoadingFullLogs): void;
export function showToast(Toast: typeof ToastMessageBodyTooLong): void;
export function showToast(Toast: typeof ToastOriginalMessageNotFound): void;
export function showToast(Toast: typeof ToastReactionFailed): void;
export function showToast(Toast: typeof ToastStickerPackInstallFailed): void;
export function showToast(Toast: typeof ToastTapToViewExpiredIncoming): void;
export function showToast(Toast: typeof ToastTapToViewExpiredOutgoing): void;
export function showToast(Toast: typeof ToastVoiceNoteLimit): void;
export function showToast(
Toast: typeof ToastVoiceNoteMustBeOnlyAttachment

View file

@ -16,10 +16,8 @@ import type { MediaItemType } from '../types/MediaItem';
import { getMessageById } from '../messages/getMessageById';
import { getContactId } from '../messages/helpers';
import { strictAssert } from '../util/assert';
import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend';
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
import { isGroup } from '../util/whatTypeOfConversation';
import { isIncoming } from '../state/selectors/message';
import { getActiveCallState } from '../state/selectors/calling';
import { ReactWrapperView } from './ReactWrapperView';
import * as log from '../logging/log';
@ -29,17 +27,11 @@ import { ToastConversationMarkedUnread } from '../components/ToastConversationMa
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
import { ToastReactionFailed } from '../components/ToastReactionFailed';
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge';
import { retryMessageSend } from '../util/retryMessageSend';
import { isNotNil } from '../util/isNotNil';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { showToast } from '../util/showToast';
import { UUIDKind } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone';
import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery';
import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent';
import {
@ -53,11 +45,6 @@ import { clearConversationDraftAttachments } from '../util/clearConversationDraf
import type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels';
import { PanelType, isPanelHandledByReact } from '../types/Panels';
type AttachmentOptions = {
messageId: string;
attachment: AttachmentType;
};
const { Message } = window.Signal.Types;
type BackbonePanelType = { panelType: PanelType; view: Backbone.View };
@ -68,21 +55,6 @@ const { getAbsoluteAttachmentPath, upgradeMessageSchema } =
const { getMessagesBySentAt } = window.Signal.Data;
type MessageActionsType = {
downloadNewVersion: () => unknown;
kickOffAttachmentDownload: (
options: Readonly<{ messageId: string }>
) => unknown;
markAttachmentAsCorrupted: (options: AttachmentOptions) => unknown;
openGiftBadge: (messageId: string) => unknown;
openLink: (url: string) => unknown;
reactToMessage: (
messageId: string,
reaction: { emoji: string; remove: boolean }
) => unknown;
retrySend: (messageId: string) => unknown;
retryDeleteForEveryone: (messageId: string) => unknown;
showExpiredIncomingTapToViewToast: () => unknown;
showExpiredOutgoingTapToViewToast: () => unknown;
showMessageDetail: (messageId: string) => unknown;
startConversation: (e164: string, uuid: UUIDStringType) => unknown;
};
@ -372,82 +344,11 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
getMessageActions(): MessageActionsType {
const reactToMessage = async (
messageId: string,
reaction: { emoji: string; remove: boolean }
) => {
const { emoji, remove } = reaction;
try {
await enqueueReactionForSend({
messageId,
emoji,
remove,
});
} catch (error) {
log.error('Error sending reaction', error, messageId, reaction);
showToast(ToastReactionFailed);
}
};
const retrySend = retryMessageSend;
const showMessageDetail = (messageId: string) => {
this.showMessageDetail(messageId);
};
const kickOffAttachmentDownload = async (
options: Readonly<{ messageId: string }>
) => {
const message = window.MessageController.getById(options.messageId);
if (!message) {
throw new Error(
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
);
}
await message.queueAttachmentDownloads();
};
const markAttachmentAsCorrupted = (options: AttachmentOptions) => {
const message = window.MessageController.getById(options.messageId);
if (!message) {
throw new Error(
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
);
}
message.markAttachmentAsCorrupted(options.attachment);
};
const openGiftBadge = (messageId: string): void => {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(`openGiftBadge: Message ${messageId} missing!`);
}
showToast(ToastCannotOpenGiftBadge, {
isIncoming: isIncoming(message.attributes),
});
};
const openLink = openLinkInWebBrowser;
const downloadNewVersion = () => {
openLinkInWebBrowser('https://signal.org/download');
};
const showExpiredIncomingTapToViewToast = () => {
log.info('Showing expired tap-to-view toast for an incoming message');
showToast(ToastTapToViewExpiredIncoming);
};
const showExpiredOutgoingTapToViewToast = () => {
log.info('Showing expired tap-to-view toast for an outgoing message');
showToast(ToastTapToViewExpiredOutgoing);
};
return {
downloadNewVersion,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
openGiftBadge,
openLink,
reactToMessage,
retrySend,
retryDeleteForEveryone,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showMessageDetail,
startConversation,
};