conversation_view: Move the last of the small functions to redux

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,29 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastConversationArchived } from './ToastConversationArchived';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
undo: action('undo'),
};
export default {
title: 'Components/ToastConversationArchived',
};
export const _ToastConversationArchived = (): JSX.Element => (
<ToastConversationArchived {...defaultProps} />
);
_ToastConversationArchived.story = {
name: 'ToastConversationArchived',
};

View file

@ -1,36 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
export type ToastPropsType = {
undo: () => unknown;
};
export type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
} & ToastPropsType;
export function ToastConversationArchived({
i18n,
onClose,
undo,
}: PropsType): JSX.Element {
return (
<Toast
toastAction={{
label: i18n('conversationArchivedUndo'),
onClick: () => {
undo();
onClose();
},
}}
onClose={onClose}
>
{i18n('conversationArchived')}
</Toast>
);
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastConversationMarkedUnread } from './ToastConversationMarkedUnread';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastConversationMarkedUnread',
};
export const _ToastConversationMarkedUnread = (): JSX.Element => (
<ToastConversationMarkedUnread {...defaultProps} />
);
_ToastConversationMarkedUnread.story = {
name: 'ToastConversationMarkedUnread',
};

View file

@ -1,18 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastConversationMarkedUnread({
i18n,
onClose,
}: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('conversationMarkedUnread')}</Toast>;
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastConversationUnarchived } from './ToastConversationUnarchived';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastConversationUnarchived',
};
export const _ToastConversationUnarchived = (): JSX.Element => (
<ToastConversationUnarchived {...defaultProps} />
);
_ToastConversationUnarchived.story = {
name: 'ToastConversationUnarchived',
};

View file

@ -1,18 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastConversationUnarchived({
i18n,
onClose,
}: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('conversationReturnedToInbox')}</Toast>;
}

View file

@ -17,6 +17,8 @@ export default {
component: ToastManager,
argTypes: {
hideToast: { action: true },
openFileInFolder: { action: true },
onUndoArchive: { action: true },
i18n: {
defaultValue: i18n,
},
@ -91,6 +93,30 @@ CannotStartGroupCall.args = {
},
};
export const ConversationArchived = Template.bind({});
ConversationArchived.args = {
toast: {
toastType: ToastType.ConversationArchived,
parameters: {
conversationId: 'some-conversation-id',
},
},
};
export const ConversationMarkedUnread = Template.bind({});
ConversationMarkedUnread.args = {
toast: {
toastType: ToastType.ConversationMarkedUnread,
},
};
export const ConversationUnarchived = Template.bind({});
ConversationUnarchived.args = {
toast: {
toastType: ToastType.ConversationUnarchived,
},
};
export const CopiedUsername = Template.bind({});
CopiedUsername.args = {
toast: {
@ -182,6 +208,13 @@ MaxAttachments.args = {
},
};
export const OriginalMessageNotFound = Template.bind({});
OriginalMessageNotFound.args = {
toast: {
toastType: ToastType.OriginalMessageNotFound,
},
};
export const MessageBodyTooLong = Template.bind({});
MessageBodyTooLong.args = {
toast: {

View file

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

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastMessageBodyTooLong } from './ToastMessageBodyTooLong';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastMessageBodyTooLong',
};
export const _ToastMessageBodyTooLong = (): JSX.Element => (
<ToastMessageBodyTooLong {...defaultProps} />
);
_ToastMessageBodyTooLong.story = {
name: 'ToastMessageBodyTooLong',
};

View file

@ -1,18 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastMessageBodyTooLong({
i18n,
onClose,
}: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('messageBodyTooLong')}</Toast>;
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastOriginalMessageNotFound } from './ToastOriginalMessageNotFound';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastOriginalMessageNotFound',
};
export const _ToastOriginalMessageNotFound = (): JSX.Element => (
<ToastOriginalMessageNotFound {...defaultProps} />
);
_ToastOriginalMessageNotFound.story = {
name: 'ToastOriginalMessageNotFound',
};

View file

@ -1,18 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastOriginalMessageNotFound({
i18n,
onClose,
}: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('originalMessageNotFound')}</Toast>;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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