Spam Reporting UI changes
This commit is contained in:
parent
e031d136a1
commit
8387f938eb
88 changed files with 2711 additions and 807 deletions
|
@ -108,7 +108,7 @@ export default {
|
|||
blockConversation: action('blockConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
title: '',
|
||||
conversationName: getDefaultConversation(),
|
||||
// GroupV1 Disabled Actions
|
||||
showGV2MigrationDialog: action('showGV2MigrationDialog'),
|
||||
// GroupV2
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
|
@ -43,6 +43,7 @@ import type { AciString } from '../types/ServiceId';
|
|||
import { AudioCapture } from './conversation/AudioCapture';
|
||||
import { CompositionUpload } from './CompositionUpload';
|
||||
import type {
|
||||
ConversationRemovalStage,
|
||||
ConversationType,
|
||||
PushPanelForConversationActionType,
|
||||
ShowConversationType,
|
||||
|
@ -73,16 +74,16 @@ import type { ShowToastAction } from '../state/ducks/toast';
|
|||
import type { DraftEditMessageType } from '../model-types.d';
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
acceptedMessageRequest?: boolean;
|
||||
removalStage?: 'justNotification' | 'messageRequest';
|
||||
acceptedMessageRequest: boolean | null;
|
||||
removalStage: ConversationRemovalStage | null;
|
||||
addAttachment: (
|
||||
conversationId: string,
|
||||
attachment: InMemoryAttachmentDraftType
|
||||
) => unknown;
|
||||
announcementsOnly?: boolean;
|
||||
areWeAdmin?: boolean;
|
||||
areWePending?: boolean;
|
||||
areWePendingApproval?: boolean;
|
||||
announcementsOnly: boolean | null;
|
||||
areWeAdmin: boolean | null;
|
||||
areWePending: boolean | null;
|
||||
areWePendingApproval: boolean | null;
|
||||
cancelRecording: () => unknown;
|
||||
completeRecording: (
|
||||
conversationId: string,
|
||||
|
@ -93,29 +94,29 @@ export type OwnProps = Readonly<{
|
|||
) => HydratedBodyRangesType | undefined;
|
||||
conversationId: string;
|
||||
discardEditMessage: (id: string) => unknown;
|
||||
draftEditMessage?: DraftEditMessageType;
|
||||
draftEditMessage: DraftEditMessageType | null;
|
||||
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
||||
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
||||
errorDialogAudioRecorderType: ErrorDialogAudioRecorderType | null;
|
||||
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
||||
focusCounter: number;
|
||||
groupAdmins: Array<ConversationType>;
|
||||
groupVersion?: 1 | 2;
|
||||
groupVersion: 1 | 2 | null;
|
||||
i18n: LocalizerType;
|
||||
imageToBlurHash: typeof imageToBlurHash;
|
||||
isDisabled: boolean;
|
||||
isFetchingUUID?: boolean;
|
||||
isFetchingUUID: boolean | null;
|
||||
isFormattingEnabled: boolean;
|
||||
isGroupV1AndDisabled?: boolean;
|
||||
isMissingMandatoryProfileSharing?: boolean;
|
||||
isSignalConversation?: boolean;
|
||||
lastEditableMessageId?: string;
|
||||
isGroupV1AndDisabled: boolean | null;
|
||||
isMissingMandatoryProfileSharing: boolean | null;
|
||||
isSignalConversation: boolean | null;
|
||||
lastEditableMessageId: string | null;
|
||||
recordingState: RecordingState;
|
||||
messageCompositionId: string;
|
||||
shouldHidePopovers?: boolean;
|
||||
isSMSOnly?: boolean;
|
||||
left?: boolean;
|
||||
shouldHidePopovers: boolean | null;
|
||||
isSMSOnly: boolean | null;
|
||||
left: boolean | null;
|
||||
linkPreviewLoading: boolean;
|
||||
linkPreviewResult?: LinkPreviewType;
|
||||
linkPreviewResult: LinkPreviewType | null;
|
||||
onClearAttachments(conversationId: string): unknown;
|
||||
onCloseLinkPreview(conversationId: string): unknown;
|
||||
platform: string;
|
||||
|
@ -149,15 +150,15 @@ export type OwnProps = Readonly<{
|
|||
voiceNoteAttachment?: InMemoryAttachmentDraftType;
|
||||
}
|
||||
): unknown;
|
||||
quotedMessageId?: string;
|
||||
quotedMessageProps?: ReadonlyDeep<
|
||||
quotedMessageId: string | null;
|
||||
quotedMessageProps: null | ReadonlyDeep<
|
||||
Omit<
|
||||
QuoteProps,
|
||||
'i18n' | 'onClick' | 'onClose' | 'withContentAbove' | 'isCompose'
|
||||
>
|
||||
>;
|
||||
quotedMessageAuthorAci?: AciString;
|
||||
quotedMessageSentAt?: number;
|
||||
quotedMessageAuthorAci: AciString | null;
|
||||
quotedMessageSentAt: number | null;
|
||||
|
||||
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
||||
scrollToMessage: (conversationId: string, messageId: string) => unknown;
|
||||
|
@ -210,6 +211,7 @@ export type Props = Pick<
|
|||
| 'blessedPacks'
|
||||
| 'recentStickers'
|
||||
| 'clearInstalledStickerPack'
|
||||
| 'showIntroduction'
|
||||
| 'clearShowIntroduction'
|
||||
| 'showPickerHint'
|
||||
| 'clearShowPickerHint'
|
||||
|
@ -220,7 +222,7 @@ export type Props = Pick<
|
|||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
} & OwnProps;
|
||||
|
||||
export function CompositionArea({
|
||||
export const CompositionArea = memo(function CompositionArea({
|
||||
// Base props
|
||||
addAttachment,
|
||||
conversationId,
|
||||
|
@ -291,6 +293,7 @@ export function CompositionArea({
|
|||
recentStickers,
|
||||
clearInstalledStickerPack,
|
||||
sendStickerMessage,
|
||||
showIntroduction,
|
||||
clearShowIntroduction,
|
||||
showPickerHint,
|
||||
clearShowPickerHint,
|
||||
|
@ -301,14 +304,18 @@ export function CompositionArea({
|
|||
conversationType,
|
||||
groupVersion,
|
||||
isBlocked,
|
||||
isHidden,
|
||||
isReported,
|
||||
isMissingMandatoryProfileSharing,
|
||||
left,
|
||||
removalStage,
|
||||
acceptConversation,
|
||||
blockConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
deleteConversation,
|
||||
title,
|
||||
conversationName,
|
||||
addedByName,
|
||||
// GroupV1 Disabled Actions
|
||||
isGroupV1AndDisabled,
|
||||
showGV2MigrationDialog,
|
||||
|
@ -356,8 +363,8 @@ export function CompositionArea({
|
|||
bodyRanges,
|
||||
message,
|
||||
// sent timestamp for the quote
|
||||
quoteSentAt: quotedMessageSentAt,
|
||||
quoteAuthorAci: quotedMessageAuthorAci,
|
||||
quoteSentAt: quotedMessageSentAt ?? undefined,
|
||||
quoteAuthorAci: quotedMessageAuthorAci ?? undefined,
|
||||
targetMessageId: editedMessageId,
|
||||
});
|
||||
} else {
|
||||
|
@ -469,12 +476,7 @@ export function CompositionArea({
|
|||
) {
|
||||
inputApiRef.current.reset();
|
||||
}
|
||||
}, [
|
||||
messageCompositionId,
|
||||
sendCounter,
|
||||
previousMessageCompositionId,
|
||||
previousSendCounter,
|
||||
]);
|
||||
}, [messageCompositionId, sendCounter, previousMessageCompositionId, previousSendCounter]);
|
||||
|
||||
const insertEmoji = useCallback(
|
||||
(e: EmojiPickDataType) => {
|
||||
|
@ -504,7 +506,7 @@ export function CompositionArea({
|
|||
|
||||
inputApiRef.current?.setContents(
|
||||
draftEditMessageBody ?? '',
|
||||
draftBodyRanges,
|
||||
draftBodyRanges ?? undefined,
|
||||
true
|
||||
);
|
||||
}, [draftBodyRanges, draftEditMessageBody, hasEditDraftChanged]);
|
||||
|
@ -520,7 +522,11 @@ export function CompositionArea({
|
|||
return;
|
||||
}
|
||||
|
||||
inputApiRef.current?.setContents(draftText, draftBodyRanges, true);
|
||||
inputApiRef.current?.setContents(
|
||||
draftText,
|
||||
draftBodyRanges ?? undefined,
|
||||
true
|
||||
);
|
||||
}, [conversationId, draftBodyRanges, draftText, previousConversationId]);
|
||||
|
||||
const handleToggleLarge = useCallback(() => {
|
||||
|
@ -637,6 +643,7 @@ export function CompositionArea({
|
|||
onPickSticker={(packId, stickerId) =>
|
||||
sendStickerMessage(conversationId, { packId, stickerId })
|
||||
}
|
||||
showIntroduction={showIntroduction}
|
||||
clearShowIntroduction={clearShowIntroduction}
|
||||
showPickerHint={showPickerHint}
|
||||
clearShowPickerHint={clearShowPickerHint}
|
||||
|
@ -735,16 +742,19 @@ export function CompositionArea({
|
|||
) {
|
||||
return (
|
||||
<MessageRequestActions
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
conversationId={conversationId}
|
||||
addedByName={addedByName}
|
||||
conversationType={conversationType}
|
||||
deleteConversation={deleteConversation}
|
||||
conversationId={conversationId}
|
||||
conversationName={conversationName}
|
||||
i18n={i18n}
|
||||
isBlocked={isBlocked}
|
||||
isHidden={removalStage !== undefined}
|
||||
title={title}
|
||||
isHidden={isHidden}
|
||||
isReported={isReported}
|
||||
acceptConversation={acceptConversation}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
deleteConversation={deleteConversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -788,14 +798,18 @@ export function CompositionArea({
|
|||
) {
|
||||
return (
|
||||
<MandatoryProfileSharingActions
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
addedByName={addedByName}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversationType}
|
||||
deleteConversation={deleteConversation}
|
||||
conversationName={conversationName}
|
||||
i18n={i18n}
|
||||
title={title}
|
||||
isBlocked={isBlocked}
|
||||
isReported={isReported}
|
||||
acceptConversation={acceptConversation}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
deleteConversation={deleteConversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -993,7 +1007,7 @@ export function CompositionArea({
|
|||
platform={platform}
|
||||
sendCounter={sendCounter}
|
||||
shouldHidePopovers={shouldHidePopovers}
|
||||
skinTone={skinTone}
|
||||
skinTone={skinTone ?? null}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
theme={theme}
|
||||
/>
|
||||
|
@ -1031,4 +1045,4 @@ export function CompositionArea({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,30 +21,38 @@ export default {
|
|||
args: {},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
i18n,
|
||||
disabled: overrideProps.disabled ?? false,
|
||||
draftText: overrideProps.draftText || undefined,
|
||||
draftBodyRanges: overrideProps.draftBodyRanges || [],
|
||||
clearQuotedMessage: action('clearQuotedMessage'),
|
||||
getPreferredBadge: () => undefined,
|
||||
getQuotedMessage: action('getQuotedMessage'),
|
||||
isFormattingEnabled:
|
||||
overrideProps.isFormattingEnabled === false
|
||||
? overrideProps.isFormattingEnabled
|
||||
: true,
|
||||
large: overrideProps.large ?? false,
|
||||
onCloseLinkPreview: action('onCloseLinkPreview'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
onSubmit: action('onSubmit'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
platform: 'darwin',
|
||||
sendCounter: 0,
|
||||
sortedGroupMembers: overrideProps.sortedGroupMembers ?? [],
|
||||
skinTone: overrideProps.skinTone ?? undefined,
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
});
|
||||
const useProps = (overrideProps: Partial<Props> = {}): Props => {
|
||||
const conversation = getDefaultConversation();
|
||||
return {
|
||||
i18n,
|
||||
conversationId: conversation.id,
|
||||
disabled: overrideProps.disabled ?? false,
|
||||
draftText: overrideProps.draftText ?? null,
|
||||
draftEditMessage: overrideProps.draftEditMessage ?? null,
|
||||
draftBodyRanges: overrideProps.draftBodyRanges || [],
|
||||
clearQuotedMessage: action('clearQuotedMessage'),
|
||||
getPreferredBadge: () => undefined,
|
||||
getQuotedMessage: action('getQuotedMessage'),
|
||||
isFormattingEnabled:
|
||||
overrideProps.isFormattingEnabled === false
|
||||
? overrideProps.isFormattingEnabled
|
||||
: true,
|
||||
large: overrideProps.large ?? false,
|
||||
onCloseLinkPreview: action('onCloseLinkPreview'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
onSubmit: action('onSubmit'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
platform: 'darwin',
|
||||
sendCounter: 0,
|
||||
sortedGroupMembers: overrideProps.sortedGroupMembers ?? [],
|
||||
skinTone: overrideProps.skinTone ?? null,
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
inputApi: null,
|
||||
shouldHidePopovers: null,
|
||||
linkPreviewResult: null,
|
||||
};
|
||||
};
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
const props = useProps();
|
||||
|
|
|
@ -96,22 +96,22 @@ export type InputApi = {
|
|||
|
||||
export type Props = Readonly<{
|
||||
children?: React.ReactNode;
|
||||
conversationId?: string;
|
||||
conversationId: string | null;
|
||||
i18n: LocalizerType;
|
||||
disabled?: boolean;
|
||||
draftEditMessage?: DraftEditMessageType;
|
||||
draftEditMessage: DraftEditMessageType | null;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
large?: boolean;
|
||||
inputApi?: React.MutableRefObject<InputApi | undefined>;
|
||||
large: boolean | null;
|
||||
inputApi: React.MutableRefObject<InputApi | undefined> | null;
|
||||
isFormattingEnabled: boolean;
|
||||
sendCounter: number;
|
||||
skinTone?: EmojiPickDataType['skinTone'];
|
||||
draftText?: string;
|
||||
draftBodyRanges?: HydratedBodyRangesType;
|
||||
skinTone: NonNullable<EmojiPickDataType['skinTone']> | null;
|
||||
draftText: string | null;
|
||||
draftBodyRanges: HydratedBodyRangesType | null;
|
||||
moduleClassName?: string;
|
||||
theme: ThemeType;
|
||||
placeholder?: string;
|
||||
sortedGroupMembers?: ReadonlyArray<ConversationType>;
|
||||
sortedGroupMembers: ReadonlyArray<ConversationType> | null;
|
||||
scrollerRef?: React.RefObject<HTMLDivElement>;
|
||||
onDirtyChange?(dirty: boolean): unknown;
|
||||
onEditorStateChange?(options: {
|
||||
|
@ -132,11 +132,11 @@ export type Props = Readonly<{
|
|||
): unknown;
|
||||
onScroll?: (ev: React.UIEvent<HTMLElement>) => void;
|
||||
platform: string;
|
||||
shouldHidePopovers?: boolean;
|
||||
shouldHidePopovers: boolean | null;
|
||||
getQuotedMessage?(): unknown;
|
||||
clearQuotedMessage?(): unknown;
|
||||
linkPreviewLoading?: boolean;
|
||||
linkPreviewResult?: LinkPreviewType;
|
||||
linkPreviewResult: LinkPreviewType | null;
|
||||
onCloseLinkPreview?(conversationId: string): unknown;
|
||||
}>;
|
||||
|
||||
|
@ -562,7 +562,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
onEditorStateChange({
|
||||
bodyRanges,
|
||||
caretLocation: selection ? selection.index : undefined,
|
||||
conversationId,
|
||||
conversationId: conversationId ?? undefined,
|
||||
messageText: text,
|
||||
sendCounter,
|
||||
});
|
||||
|
@ -612,7 +612,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
React.useEffect(() => {
|
||||
const emojiCompletion = emojiCompletionRef.current;
|
||||
|
||||
if (emojiCompletion === undefined || skinTone === undefined) {
|
||||
if (emojiCompletion == null || skinTone == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
|||
import * as grapheme from '../util/grapheme';
|
||||
|
||||
export type CompositionTextAreaProps = {
|
||||
bodyRanges?: HydratedBodyRangesType;
|
||||
bodyRanges: HydratedBodyRangesType | null;
|
||||
i18n: LocalizerType;
|
||||
isFormattingEnabled: boolean;
|
||||
maxLength?: number;
|
||||
|
@ -153,6 +153,17 @@ export function CompositionTextArea({
|
|||
scrollerRef={scrollerRef}
|
||||
sendCounter={0}
|
||||
theme={theme}
|
||||
skinTone={skinTone ?? null}
|
||||
// These do not apply in the forward modal because there isn't
|
||||
// strictly one conversation
|
||||
conversationId={null}
|
||||
sortedGroupMembers={null}
|
||||
// we don't edit in this context
|
||||
draftEditMessage={null}
|
||||
// rendered in the forward modal
|
||||
linkPreviewResult={null}
|
||||
// Panels appear behind this modal
|
||||
shouldHidePopovers={null}
|
||||
/>
|
||||
<div className="CompositionTextArea__emoji">
|
||||
<EmojiButton
|
||||
|
|
|
@ -470,7 +470,7 @@ function ForwardMessageEditor({
|
|||
) : null}
|
||||
|
||||
<RenderCompositionTextArea
|
||||
bodyRanges={draft.bodyRanges}
|
||||
bodyRanges={draft.bodyRanges ?? null}
|
||||
draftText={draft.messageBody ?? ''}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
EditHistoryMessagesType,
|
||||
FormattingWarningDataType,
|
||||
ForwardMessagesPropsType,
|
||||
MessageRequestActionsConfirmationPropsType,
|
||||
SafetyNumberChangedBlockingDataType,
|
||||
SendEditWarningDataType,
|
||||
UserNotFoundModalStateType,
|
||||
|
@ -59,6 +60,9 @@ export type PropsType = {
|
|||
// ForwardMessageModal
|
||||
forwardMessagesProps: ForwardMessagesPropsType | undefined;
|
||||
renderForwardMessagesModal: () => JSX.Element;
|
||||
// MessageRequestActionsConfirmation
|
||||
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||
renderMessageRequestActionsConfirmation: () => JSX.Element;
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible: boolean;
|
||||
renderProfileEditor: () => JSX.Element;
|
||||
|
@ -130,6 +134,9 @@ export function GlobalModalContainer({
|
|||
// ForwardMessageModal
|
||||
forwardMessagesProps,
|
||||
renderForwardMessagesModal,
|
||||
// MessageRequestActionsConfirmation
|
||||
messageRequestActionsConfirmationProps,
|
||||
renderMessageRequestActionsConfirmation,
|
||||
// ProfileEditor
|
||||
isProfileEditorVisible,
|
||||
renderProfileEditor,
|
||||
|
@ -223,6 +230,10 @@ export function GlobalModalContainer({
|
|||
return renderForwardMessagesModal();
|
||||
}
|
||||
|
||||
if (messageRequestActionsConfirmationProps) {
|
||||
return renderMessageRequestActionsConfirmation();
|
||||
}
|
||||
|
||||
if (isProfileEditorVisible) {
|
||||
return renderProfileEditor();
|
||||
}
|
||||
|
|
|
@ -176,13 +176,12 @@ export function MediaEditor({
|
|||
const [isEmojiPopperOpen, setEmojiPopperOpen] = useState<boolean>(false);
|
||||
|
||||
const [caption, setCaption] = useState(draftText ?? '');
|
||||
const [captionBodyRanges, setCaptionBodyRanges] = useState<
|
||||
DraftBodyRanges | undefined
|
||||
>(draftBodyRanges);
|
||||
const [captionBodyRanges, setCaptionBodyRanges] =
|
||||
useState<DraftBodyRanges | null>(draftBodyRanges);
|
||||
|
||||
const conversationSelector = useSelector(getConversationSelector);
|
||||
const hydratedBodyRanges = useMemo(
|
||||
() => hydrateRanges(captionBodyRanges, conversationSelector),
|
||||
() => hydrateRanges(captionBodyRanges ?? undefined, conversationSelector),
|
||||
[captionBodyRanges, conversationSelector]
|
||||
);
|
||||
|
||||
|
@ -1297,7 +1296,7 @@ export function MediaEditor({
|
|||
<div className="MediaEditor__tools--input dark-theme">
|
||||
<CompositionInput
|
||||
draftText={caption}
|
||||
draftBodyRanges={hydratedBodyRanges}
|
||||
draftBodyRanges={hydratedBodyRanges ?? null}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
inputApi={inputApiRef}
|
||||
|
@ -1308,6 +1307,7 @@ export function MediaEditor({
|
|||
setCaptionBodyRanges(bodyRanges);
|
||||
setCaption(messageText);
|
||||
}}
|
||||
skinTone={skinTone ?? null}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={noop}
|
||||
onTextTooLong={onTextTooLong}
|
||||
|
@ -1316,6 +1316,16 @@ export function MediaEditor({
|
|||
sendCounter={0}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
theme={ThemeType.dark}
|
||||
// Only needed for state updates and we need to override those
|
||||
conversationId={null}
|
||||
// Cannot enter media editor while editing
|
||||
draftEditMessage={null}
|
||||
// We don't use the large editor mode
|
||||
large={null}
|
||||
// panels do not appear over the media editor
|
||||
shouldHidePopovers={null}
|
||||
// link previews not displayed with media
|
||||
linkPreviewResult={null}
|
||||
>
|
||||
<EmojiButton
|
||||
className="StoryViewsNRepliesModal__emoji-button"
|
||||
|
@ -1394,7 +1404,7 @@ export function MediaEditor({
|
|||
contentType: IMAGE_PNG,
|
||||
data,
|
||||
caption: caption !== '' ? caption : undefined,
|
||||
captionBodyRanges,
|
||||
captionBodyRanges: captionBodyRanges ?? undefined,
|
||||
blurHash,
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
|||
import { noop } from 'lodash';
|
||||
import { animated } from '@react-spring/web';
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { ModalHost } from './ModalHost';
|
||||
import type { Theme } from '../util/theme';
|
||||
|
@ -37,6 +38,7 @@ type PropsType = {
|
|||
title?: ReactNode;
|
||||
useFocusTrap?: boolean;
|
||||
padded?: boolean;
|
||||
['aria-describedby']?: string;
|
||||
};
|
||||
|
||||
export type ModalPropsType = PropsType & {
|
||||
|
@ -65,6 +67,7 @@ export function Modal({
|
|||
hasFooterDivider = false,
|
||||
noTransform = false,
|
||||
padded = true,
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
}: Readonly<ModalPropsType>): JSX.Element | null {
|
||||
const { close, isClosed, modalStyles, overlayStyles } = useAnimated(
|
||||
onClose,
|
||||
|
@ -132,6 +135,7 @@ export function Modal({
|
|||
padded={padded}
|
||||
hasHeaderDivider={hasHeaderDivider}
|
||||
hasFooterDivider={hasFooterDivider}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
>
|
||||
{children}
|
||||
</ModalPage>
|
||||
|
@ -173,6 +177,7 @@ export function ModalPage({
|
|||
padded = true,
|
||||
hasHeaderDivider = false,
|
||||
hasFooterDivider = false,
|
||||
'aria-describedby': ariaDescribedBy,
|
||||
}: ModalPageProps): JSX.Element {
|
||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
@ -188,6 +193,8 @@ export function ModalPage({
|
|||
);
|
||||
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||
|
||||
const [id] = useState(() => uuid());
|
||||
|
||||
useScrollObserver(bodyRef, bodyInnerRef, scroll => {
|
||||
setScrolled(isScrolled(scroll));
|
||||
setScrolledToBottom(isScrolledToBottom(scroll));
|
||||
|
@ -198,7 +205,7 @@ export function ModalPage({
|
|||
<>
|
||||
{/* We don't want the click event to propagate to its container node. */}
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<div
|
||||
className={classNames(
|
||||
getClassName(''),
|
||||
|
@ -209,6 +216,10 @@ export function ModalPage({
|
|||
hasFooterDivider && getClassName('--footer-divider')
|
||||
)}
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
tabIndex={-1}
|
||||
aria-labelledby={title ? `${id}-title` : undefined}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
|
@ -234,6 +245,7 @@ export function ModalPage({
|
|||
)}
|
||||
{title && (
|
||||
<h1
|
||||
id={`${id}-title`}
|
||||
className={classNames(
|
||||
getClassName('__title'),
|
||||
hasXButton ? getClassName('__title--with-x-button') : null
|
||||
|
|
24
ts/components/SafetyTipsModal.stories.tsx
Normal file
24
ts/components/SafetyTipsModal.stories.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import type { SafetyTipsModalProps } from './SafetyTipsModal';
|
||||
import { SafetyTipsModal } from './SafetyTipsModal';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/SafetyTipsModal',
|
||||
component: SafetyTipsModal,
|
||||
args: {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
},
|
||||
} satisfies ComponentMeta<SafetyTipsModalProps>;
|
||||
|
||||
export function Default(args: SafetyTipsModalProps): JSX.Element {
|
||||
return <SafetyTipsModal {...args} />;
|
||||
}
|
216
ts/components/SafetyTipsModal.tsx
Normal file
216
ts/components/SafetyTipsModal.tsx
Normal file
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { UIEvent } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { Modal } from './Modal';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
|
||||
export type SafetyTipsModalProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onClose(): void;
|
||||
}>;
|
||||
|
||||
export function SafetyTipsModal({
|
||||
i18n,
|
||||
onClose,
|
||||
}: SafetyTipsModalProps): JSX.Element {
|
||||
const pages = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'crypto',
|
||||
title: i18n('icu:SafetyTipsModal__TipTitle--Crypto'),
|
||||
description: i18n('icu:SafetyTipsModal__TipDescription--Crypto'),
|
||||
imageUrl: 'images/safety-tips/safety-tip-crypto.png',
|
||||
},
|
||||
{
|
||||
key: 'vague',
|
||||
title: i18n('icu:SafetyTipsModal__TipTitle--Vague'),
|
||||
description: i18n('icu:SafetyTipsModal__TipDescription--Vague'),
|
||||
imageUrl: 'images/safety-tips/safety-tip-vague.png',
|
||||
},
|
||||
{
|
||||
key: 'links',
|
||||
title: i18n('icu:SafetyTipsModal__TipTitle--Links'),
|
||||
description: i18n('icu:SafetyTipsModal__TipDescription--Links'),
|
||||
imageUrl: 'images/safety-tips/safety-tip-links.png',
|
||||
},
|
||||
{
|
||||
key: 'business',
|
||||
title: i18n('icu:SafetyTipsModal__TipTitle--Business'),
|
||||
description: i18n('icu:SafetyTipsModal__TipDescription--Business'),
|
||||
imageUrl: 'images/safety-tips/safety-tip-business.png',
|
||||
},
|
||||
];
|
||||
}, [i18n]);
|
||||
|
||||
const [modalId] = useState(() => uuid());
|
||||
const [cardWrapperId] = useState(() => uuid());
|
||||
|
||||
function getCardIdForPage(pageIndex: number) {
|
||||
return `${cardWrapperId}_${pages[pageIndex].key}`;
|
||||
}
|
||||
|
||||
const maxPageIndex = pages.length - 1;
|
||||
const [pageIndex, setPageIndexInner] = useState(0);
|
||||
const reducedMotion = useReducedMotion();
|
||||
const scrollEndTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const [hasPageIndexChanged, setHasPageIndexChanged] = useState(false);
|
||||
function setPageIndex(nextPageIndex: number) {
|
||||
setPageIndexInner(nextPageIndex);
|
||||
setHasPageIndexChanged(true);
|
||||
}
|
||||
|
||||
function clearScrollEndTimer() {
|
||||
if (scrollEndTimer.current != null) {
|
||||
clearTimeout(scrollEndTimer.current);
|
||||
scrollEndTimer.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearScrollEndTimer();
|
||||
};
|
||||
}, []);
|
||||
|
||||
function scrollToPageIndex(nextPageIndex: number) {
|
||||
clearScrollEndTimer();
|
||||
setPageIndex(nextPageIndex);
|
||||
document.getElementById(getCardIdForPage(nextPageIndex))?.scrollIntoView({
|
||||
inline: 'center',
|
||||
behavior: reducedMotion ? 'instant' : 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
function handleScroll(event: UIEvent) {
|
||||
clearScrollEndTimer();
|
||||
const { scrollWidth, scrollLeft, clientWidth } = event.currentTarget;
|
||||
const maxScrollLeft = scrollWidth - clientWidth;
|
||||
const absScrollLeft = Math.abs(scrollLeft);
|
||||
const percentScrolled = absScrollLeft / maxScrollLeft;
|
||||
const scrolledPageIndex = Math.round(percentScrolled * maxPageIndex);
|
||||
scrollEndTimer.current = setTimeout(() => {
|
||||
setPageIndex(scrolledPageIndex);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
modalName="SafetyTipsModal"
|
||||
moduleClassName="SafetyTipsModal"
|
||||
noMouseClose
|
||||
hasXButton
|
||||
padded={false}
|
||||
title={i18n('icu:SafetyTipsModal__Title')}
|
||||
onClose={onClose}
|
||||
aria-describedby={`${modalId}-description`}
|
||||
modalFooter={
|
||||
<>
|
||||
<Button
|
||||
className="SafetyTipsModal__Button SafetyTipsModal__Button--Previous"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
aria-disabled={pageIndex === 0}
|
||||
aria-controls={cardWrapperId}
|
||||
onClick={() => {
|
||||
if (pageIndex === 0) {
|
||||
return;
|
||||
}
|
||||
scrollToPageIndex(pageIndex - 1);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:SafetyTipsModal__Button--Previous')}
|
||||
</Button>
|
||||
{pageIndex < maxPageIndex ? (
|
||||
<Button
|
||||
className="SafetyTipsModal__Button SafetyTipsModal__Button--Next"
|
||||
variant={ButtonVariant.Primary}
|
||||
aria-controls={cardWrapperId}
|
||||
onClick={() => {
|
||||
if (pageIndex === maxPageIndex) {
|
||||
return;
|
||||
}
|
||||
scrollToPageIndex(pageIndex + 1);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:SafetyTipsModal__Button--Next')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="SafetyTipsModal__Button SafetyTipsModal__Button--Next"
|
||||
variant={ButtonVariant.Primary}
|
||||
onClick={onClose}
|
||||
>
|
||||
{i18n('icu:SafetyTipsModal__Button--Done')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="SafetyTipsModal__Description" id={`${modalId}-description`}>
|
||||
{i18n('icu:SafetyTipsModal__Description')}
|
||||
</p>
|
||||
<div>
|
||||
<div
|
||||
className="SafetyTipsModal__CardWrapper"
|
||||
id={cardWrapperId}
|
||||
aria-live={hasPageIndexChanged ? 'assertive' : undefined}
|
||||
aria-atomic
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{pages.map((page, index) => {
|
||||
const isCurrentPage = pageIndex === index;
|
||||
return (
|
||||
<div
|
||||
id={getCardIdForPage(index)}
|
||||
key={page.key}
|
||||
className="SafetyTipsModal__Card"
|
||||
aria-hidden={!isCurrentPage}
|
||||
>
|
||||
<img
|
||||
role="presentation"
|
||||
alt=""
|
||||
className="SafetyTipsModal__CardImage"
|
||||
src={page.imageUrl}
|
||||
width={664}
|
||||
height={314}
|
||||
/>
|
||||
<h2 className="SafetyTipsModal__CardTitle">{page.title}</h2>
|
||||
<p className="SafetyTipsModal__CardDescription">
|
||||
{page.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="SafetyTipsModal__Dots">
|
||||
{pages.map((page, index) => {
|
||||
const isCurrentPage = pageIndex === index;
|
||||
return (
|
||||
<button
|
||||
key={page.key}
|
||||
className="SafetyTipsModal__DotsButton"
|
||||
type="button"
|
||||
aria-controls={cardWrapperId}
|
||||
aria-current={isCurrentPage ? 'step' : undefined}
|
||||
onClick={() => {
|
||||
scrollToPageIndex(index);
|
||||
}}
|
||||
>
|
||||
<div className="SafetyTipsModal__DotsButtonLabel">
|
||||
{i18n('icu:SafetyTipsModal__DotLabel', {
|
||||
page: index + 1,
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -96,7 +96,11 @@ export type PropsType = {
|
|||
> &
|
||||
Pick<
|
||||
MediaEditorPropsType,
|
||||
'isFormattingEnabled' | 'onPickEmoji' | 'onTextTooLong' | 'platform'
|
||||
| 'isFormattingEnabled'
|
||||
| 'onPickEmoji'
|
||||
| 'onTextTooLong'
|
||||
| 'platform'
|
||||
| 'sortedGroupMembers'
|
||||
>;
|
||||
|
||||
export function StoryCreator({
|
||||
|
@ -139,6 +143,7 @@ export function StoryCreator({
|
|||
setMyStoriesToAllSignalConnections,
|
||||
signalConnections,
|
||||
skinTone,
|
||||
sortedGroupMembers,
|
||||
theme,
|
||||
toggleGroupsForStorySend,
|
||||
toggleSignalConnectionsModal,
|
||||
|
@ -272,6 +277,9 @@ export function StoryCreator({
|
|||
platform={platform}
|
||||
recentStickers={recentStickers}
|
||||
skinTone={skinTone}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
draftText={null}
|
||||
draftBodyRanges={null}
|
||||
/>
|
||||
)}
|
||||
{!file && (
|
||||
|
|
|
@ -258,8 +258,15 @@ export function StoryViewsNRepliesModal({
|
|||
}
|
||||
platform={platform}
|
||||
sendCounter={0}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
skinTone={skinTone ?? null}
|
||||
sortedGroupMembers={sortedGroupMembers ?? null}
|
||||
theme={ThemeType.dark}
|
||||
conversationId={null}
|
||||
draftBodyRanges={null}
|
||||
draftEditMessage={null}
|
||||
large={null}
|
||||
shouldHidePopovers={null}
|
||||
linkPreviewResult={null}
|
||||
>
|
||||
<EmojiButton
|
||||
className="StoryViewsNRepliesModal__emoji-button"
|
||||
|
|
|
@ -121,6 +121,8 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.PinnedConversationsFull };
|
||||
case ToastType.ReactionFailed:
|
||||
return { toastType: ToastType.ReactionFailed };
|
||||
case ToastType.ReportedSpam:
|
||||
return { toastType: ToastType.ReportedSpam };
|
||||
case ToastType.ReportedSpamAndBlocked:
|
||||
return { toastType: ToastType.ReportedSpamAndBlocked };
|
||||
case ToastType.StickerPackInstallFailed:
|
||||
|
|
|
@ -371,6 +371,14 @@ export function renderToast({
|
|||
return <Toast onClose={hideToast}>{i18n('icu:Reactions--error')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ReportedSpam) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:MessageRequests--report-spam-success-toast')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ReportedSpamAndBlocked) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
|
|
@ -1,21 +1,47 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Emojify } from './Emojify';
|
||||
import type { ContactNameColorType } from '../../types/Colors';
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import { isSignalConversation as getIsSignalConversation } from '../../util/isSignalConversation';
|
||||
|
||||
export type PropsType = {
|
||||
export type ContactNameData = {
|
||||
contactNameColor?: ContactNameColorType;
|
||||
firstName?: string;
|
||||
isSignalConversation?: boolean;
|
||||
isMe?: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function useContactNameData(
|
||||
conversation: ConversationType | null,
|
||||
contactNameColor?: ContactNameColorType
|
||||
): ContactNameData | null {
|
||||
const { firstName, title, isMe } = conversation ?? {};
|
||||
const isSignalConversation =
|
||||
conversation != null ? getIsSignalConversation(conversation) : null;
|
||||
return useMemo(() => {
|
||||
if (title == null || isSignalConversation == null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
contactNameColor,
|
||||
firstName,
|
||||
isSignalConversation,
|
||||
isMe,
|
||||
title,
|
||||
};
|
||||
}, [contactNameColor, firstName, isSignalConversation, isMe, title]);
|
||||
}
|
||||
|
||||
export type PropsType = ContactNameData & {
|
||||
module?: string;
|
||||
preferFirstName?: boolean;
|
||||
title: string;
|
||||
onClick?: VoidFunction;
|
||||
};
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ export default {
|
|||
|
||||
const getCommonProps = () => ({
|
||||
acceptConversation: action('acceptConversation'),
|
||||
reportSpam: action('reportSpam'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
conversationId: 'some-conversation-id',
|
||||
|
|
|
@ -50,6 +50,7 @@ export type ReviewPropsType = Readonly<
|
|||
export type PropsType = {
|
||||
conversationId: string;
|
||||
acceptConversation: (conversationId: string) => unknown;
|
||||
reportSpam: (conversationId: string) => unknown;
|
||||
blockAndReportSpam: (conversationId: string) => unknown;
|
||||
blockConversation: (conversationId: string) => unknown;
|
||||
deleteConversation: (conversationId: string) => unknown;
|
||||
|
@ -75,6 +76,7 @@ enum ConfirmationStateType {
|
|||
export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
||||
const {
|
||||
acceptConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
conversationId,
|
||||
|
@ -111,19 +113,23 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
case ConfirmationStateType.ConfirmingBlock:
|
||||
return (
|
||||
<MessageRequestActionsConfirmation
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
addedByName={affectedConversation}
|
||||
conversationId={affectedConversation.id}
|
||||
conversationType="direct"
|
||||
deleteConversation={deleteConversation}
|
||||
conversationType={affectedConversation.type}
|
||||
conversationName={affectedConversation}
|
||||
i18n={i18n}
|
||||
title={affectedConversation.title}
|
||||
isBlocked={affectedConversation.isBlocked ?? false}
|
||||
isReported={affectedConversation.isReported ?? false}
|
||||
state={
|
||||
type === ConfirmationStateType.ConfirmingDelete
|
||||
? MessageRequestState.deleting
|
||||
: MessageRequestState.blocking
|
||||
}
|
||||
acceptConversation={acceptConversation}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
deleteConversation={deleteConversation}
|
||||
onChangeState={messageRequestState => {
|
||||
switch (messageRequestState) {
|
||||
case MessageRequestState.blocking:
|
||||
|
@ -138,10 +144,12 @@ export function ContactSpoofingReviewDialog(props: PropsType): JSX.Element {
|
|||
affectedConversation,
|
||||
});
|
||||
break;
|
||||
case MessageRequestState.reportingAndMaybeBlocking:
|
||||
case MessageRequestState.acceptedOptions:
|
||||
case MessageRequestState.unblocking:
|
||||
assertDev(
|
||||
false,
|
||||
'Got unexpected MessageRequestState.unblocking state. Clearing confiration state'
|
||||
`Got unexpected MessageRequestState.${MessageRequestState[messageRequestState]} state. Clearing confiration state`
|
||||
);
|
||||
setConfirmationState(undefined);
|
||||
break;
|
||||
|
|
|
@ -29,8 +29,15 @@ type ItemsType = Array<{
|
|||
props: Omit<ComponentProps<typeof ConversationHeader>, 'theme'>;
|
||||
}>;
|
||||
|
||||
const commonConversation = getDefaultConversation();
|
||||
const commonProps = {
|
||||
...getDefaultConversation(),
|
||||
...commonConversation,
|
||||
conversationId: commonConversation.id,
|
||||
conversationType: commonConversation.type,
|
||||
conversationName: commonConversation,
|
||||
addedByName: null,
|
||||
isBlocked: commonConversation.isBlocked ?? false,
|
||||
isReported: commonConversation.isReported ?? false,
|
||||
|
||||
cannotLeaveBecauseYouAreLastAdmin: false,
|
||||
showBackButton: false,
|
||||
|
@ -59,6 +66,12 @@ const commonProps = {
|
|||
setMuteExpiration: action('onSetMuteNotifications'),
|
||||
setPinned: action('setPinned'),
|
||||
viewUserStories: action('viewUserStories'),
|
||||
|
||||
acceptConversation: action('acceptConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
reportSpam: action('reportSpam'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
};
|
||||
|
||||
export function PrivateConvo(): JSX.Element {
|
||||
|
|
|
@ -41,6 +41,12 @@ import { PanelType } from '../../types/Panels';
|
|||
import { UserText } from '../UserText';
|
||||
import { Alert } from '../Alert';
|
||||
import { SizeObserver } from '../../hooks/useSizeObserver';
|
||||
import type { MessageRequestActionsConfirmationBaseProps } from './MessageRequestActionsConfirmation';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
} from './MessageRequestActionsConfirmation';
|
||||
import type { ContactNameData } from './ContactName';
|
||||
|
||||
export enum OutgoingCallButtonStyle {
|
||||
None,
|
||||
|
@ -60,6 +66,8 @@ export type PropsDataType = {
|
|||
isSelectMode: boolean;
|
||||
isSignalConversation?: boolean;
|
||||
theme: ThemeType;
|
||||
addedByName: ContactNameData | null;
|
||||
conversationName: ContactNameData;
|
||||
} & Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
|
@ -72,6 +80,8 @@ export type PropsDataType = {
|
|||
| 'groupVersion'
|
||||
| 'id'
|
||||
| 'isArchived'
|
||||
| 'isBlocked'
|
||||
| 'isReported'
|
||||
| 'isMe'
|
||||
| 'isPinned'
|
||||
| 'isVerified'
|
||||
|
@ -81,6 +91,7 @@ export type PropsDataType = {
|
|||
| 'name'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'removalStage'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'type'
|
||||
|
@ -106,7 +117,7 @@ export type PropsActionsType = {
|
|||
setMuteExpiration: (conversationId: string, seconds: number) => void;
|
||||
setPinned: (conversationId: string, value: boolean) => void;
|
||||
viewUserStories: ViewUserStoriesActionCreatorType;
|
||||
};
|
||||
} & MessageRequestActionsConfirmationBaseProps;
|
||||
|
||||
export type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
|
@ -127,6 +138,7 @@ type StateType = {
|
|||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: boolean;
|
||||
isNarrow: boolean;
|
||||
modalState: ModalState;
|
||||
messageRequestState: MessageRequestState;
|
||||
};
|
||||
|
||||
const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item';
|
||||
|
@ -149,6 +161,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: false,
|
||||
isNarrow: false,
|
||||
modalState: ModalState.NothingOpen,
|
||||
messageRequestState: MessageRequestState.default,
|
||||
};
|
||||
|
||||
this.menuTriggerRef = React.createRef();
|
||||
|
@ -156,6 +169,12 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
this.showMenuBound = this.showMenu.bind(this);
|
||||
}
|
||||
|
||||
private handleMessageRequestStateChange = (
|
||||
state: MessageRequestState
|
||||
): void => {
|
||||
this.setState({ messageRequestState: state });
|
||||
};
|
||||
|
||||
private showMenu(event: React.MouseEvent<HTMLButtonElement>): void {
|
||||
if (this.menuTriggerRef.current) {
|
||||
this.menuTriggerRef.current.handleContextClick(event);
|
||||
|
@ -328,6 +347,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
|
||||
private renderMenu(triggerId: string): ReactNode {
|
||||
const {
|
||||
acceptConversation,
|
||||
acceptedMessageRequest,
|
||||
canChangeTimer,
|
||||
cannotLeaveBecauseYouAreLastAdmin,
|
||||
|
@ -336,6 +356,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
i18n,
|
||||
id,
|
||||
isArchived,
|
||||
isBlocked,
|
||||
isMissingMandatoryProfileSharing,
|
||||
isPinned,
|
||||
isSignalConversation,
|
||||
|
@ -431,6 +452,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
{i18n('icu:archiveConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
this.setState({ hasDeleteMessagesConfirmation: true })
|
||||
|
@ -491,98 +513,164 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
return createPortal(
|
||||
<ContextMenu id={triggerId} rtl={isRTL}>
|
||||
{disableTimerChanges ? null : (
|
||||
<SubMenu hoverDelay={1} title={disappearingTitle} rtl={!isRTL}>
|
||||
{expireDurations}
|
||||
</SubMenu>
|
||||
)}
|
||||
<SubMenu hoverDelay={1} title={muteTitle} rtl={!isRTL}>
|
||||
{muteOptions.map(item => (
|
||||
{!acceptedMessageRequest && (
|
||||
<>
|
||||
{!isBlocked && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
messageRequestState: MessageRequestState.blocking,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__MenuItem--Block')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isBlocked && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
messageRequestState: MessageRequestState.unblocking,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__MenuItem--Unblock')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isBlocked && (
|
||||
<MenuItem onClick={acceptConversation}>
|
||||
{i18n('icu:ConversationHeader__MenuItem--Accept')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
disabled={item.disabled}
|
||||
onClick={() => {
|
||||
setMuteExpiration(id, item.value);
|
||||
this.setState({
|
||||
messageRequestState:
|
||||
MessageRequestState.reportingAndMaybeBlocking,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
{i18n('icu:ConversationHeader__MenuItem--ReportSpam')}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SubMenu>
|
||||
{!isGroup || hasGV2AdminEnabled ? (
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ConversationDetails,
|
||||
})
|
||||
}
|
||||
>
|
||||
{isGroup
|
||||
? i18n('icu:showConversationDetails')
|
||||
: i18n('icu:showConversationDetails--direct')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem
|
||||
onClick={() => pushPanelForConversation({ type: PanelType.AllMedia })}
|
||||
>
|
||||
{i18n('icu:viewRecentMedia')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
toggleSelectMode(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__menu__selectMessages')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
{!markedUnread ? (
|
||||
<MenuItem onClick={() => onMarkUnread(id)}>
|
||||
{i18n('icu:markUnread')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{isPinned ? (
|
||||
<MenuItem onClick={() => setPinned(id, false)}>
|
||||
{i18n('icu:unpinConversation')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => setPinned(id, true)}>
|
||||
{i18n('icu:pinConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isArchived ? (
|
||||
<MenuItem onClick={() => onMoveToInbox(id)}>
|
||||
{i18n('icu:moveConversationToInbox')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => onArchive(id)}>
|
||||
{i18n('icu:archiveConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => this.setState({ hasDeleteMessagesConfirmation: true })}
|
||||
>
|
||||
{i18n('icu:deleteMessagesInConversation')}
|
||||
</MenuItem>
|
||||
{isGroup && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (cannotLeaveBecauseYouAreLastAdmin) {
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: true,
|
||||
messageRequestState: MessageRequestState.deleting,
|
||||
});
|
||||
} else {
|
||||
this.setState({ hasLeaveGroupConfirmation: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:ConversationHeader__ContextMenu__LeaveGroupAction__title'
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__MenuItem--DeleteChat')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{acceptedMessageRequest && (
|
||||
<>
|
||||
{disableTimerChanges ? null : (
|
||||
<SubMenu hoverDelay={1} title={disappearingTitle} rtl={!isRTL}>
|
||||
{expireDurations}
|
||||
</SubMenu>
|
||||
)}
|
||||
</MenuItem>
|
||||
<SubMenu hoverDelay={1} title={muteTitle} rtl={!isRTL}>
|
||||
{muteOptions.map(item => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
disabled={item.disabled}
|
||||
onClick={() => {
|
||||
setMuteExpiration(id, item.value);
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SubMenu>
|
||||
{!isGroup || hasGV2AdminEnabled ? (
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation({
|
||||
type: PanelType.ConversationDetails,
|
||||
})
|
||||
}
|
||||
>
|
||||
{isGroup
|
||||
? i18n('icu:showConversationDetails')
|
||||
: i18n('icu:showConversationDetails--direct')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
pushPanelForConversation({ type: PanelType.AllMedia })
|
||||
}
|
||||
>
|
||||
{i18n('icu:viewRecentMedia')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
toggleSelectMode(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__menu__selectMessages')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
{!markedUnread ? (
|
||||
<MenuItem onClick={() => onMarkUnread(id)}>
|
||||
{i18n('icu:markUnread')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{isPinned ? (
|
||||
<MenuItem onClick={() => setPinned(id, false)}>
|
||||
{i18n('icu:unpinConversation')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => setPinned(id, true)}>
|
||||
{i18n('icu:pinConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isArchived ? (
|
||||
<MenuItem onClick={() => onMoveToInbox(id)}>
|
||||
{i18n('icu:moveConversationToInbox')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={() => onArchive(id)}>
|
||||
{i18n('icu:archiveConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
messageRequestState: MessageRequestState.blocking,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n('icu:ConversationHeader__MenuItem--Block')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() =>
|
||||
this.setState({ hasDeleteMessagesConfirmation: true })
|
||||
}
|
||||
>
|
||||
{i18n('icu:deleteMessagesInConversation')}
|
||||
</MenuItem>
|
||||
{isGroup && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (cannotLeaveBecauseYouAreLastAdmin) {
|
||||
this.setState({
|
||||
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: true,
|
||||
});
|
||||
} else {
|
||||
this.setState({ hasLeaveGroupConfirmation: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:ConversationHeader__ContextMenu__LeaveGroupAction__title'
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ContextMenu>,
|
||||
document.body
|
||||
|
@ -751,25 +839,35 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
|
||||
public override render(): ReactNode {
|
||||
const {
|
||||
addedByName,
|
||||
announcementsOnly,
|
||||
areWeAdmin,
|
||||
conversationName,
|
||||
expireTimer,
|
||||
hasPanelShowing,
|
||||
i18n,
|
||||
id,
|
||||
isBlocked,
|
||||
isReported,
|
||||
isSMSOnly,
|
||||
isSignalConversation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
outgoingCallButtonStyle,
|
||||
setDisappearingMessages,
|
||||
type,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
} = this.props;
|
||||
|
||||
if (hasPanelShowing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isNarrow, modalState } = this.state;
|
||||
const { isNarrow, modalState, messageRequestState } = this.state;
|
||||
const triggerId = `conversation-${id}`;
|
||||
|
||||
let modalNode: ReactNode;
|
||||
|
@ -829,6 +927,22 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
|||
{this.renderSearchButton()}
|
||||
{this.renderMoreButton(triggerId)}
|
||||
{this.renderMenu(triggerId)}
|
||||
<MessageRequestActionsConfirmation
|
||||
i18n={i18n}
|
||||
conversationId={id}
|
||||
conversationType={type}
|
||||
addedByName={addedByName}
|
||||
conversationName={conversationName}
|
||||
isBlocked={isBlocked ?? false}
|
||||
isReported={isReported ?? false}
|
||||
state={messageRequestState}
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
reportSpam={reportSpam}
|
||||
deleteConversation={deleteConversation}
|
||||
onChangeState={this.handleMessageRequestStateChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SizeObserver>
|
||||
|
|
|
@ -15,6 +15,8 @@ import { StoryViewModeType } from '../../types/Stories';
|
|||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
|
||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { SafetyTipsModal } from '../SafetyTipsModal';
|
||||
|
||||
export type Props = {
|
||||
about?: string;
|
||||
|
@ -42,6 +44,7 @@ const renderMembershipRow = ({
|
|||
i18n,
|
||||
isMe,
|
||||
onClickMessageRequestWarning,
|
||||
onToggleSafetyTips,
|
||||
phoneNumber,
|
||||
sharedGroupNames,
|
||||
}: Pick<
|
||||
|
@ -54,6 +57,7 @@ const renderMembershipRow = ({
|
|||
> &
|
||||
Required<Pick<Props, 'sharedGroupNames'>> & {
|
||||
onClickMessageRequestWarning: () => void;
|
||||
onToggleSafetyTips: (showSafetyTips: boolean) => void;
|
||||
}) => {
|
||||
if (conversationType !== 'direct') {
|
||||
return null;
|
||||
|
@ -67,6 +71,20 @@ const renderMembershipRow = ({
|
|||
);
|
||||
}
|
||||
|
||||
const safetyTipsButton = (
|
||||
<div>
|
||||
<Button
|
||||
className="module-conversation-hero__safety-tips-button"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={() => {
|
||||
onToggleSafetyTips(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:MessageRequestWarning__safety-tips')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (sharedGroupNames.length > 0) {
|
||||
return (
|
||||
<div className="module-conversation-hero__membership">
|
||||
|
@ -76,6 +94,7 @@ const renderMembershipRow = ({
|
|||
nameClassName="module-conversation-hero__membership__name"
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
/>
|
||||
{safetyTipsButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -86,6 +105,7 @@ const renderMembershipRow = ({
|
|||
return (
|
||||
<div className="module-conversation-hero__membership">
|
||||
{i18n('icu:no-groups-in-common')}
|
||||
{safetyTipsButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -107,6 +127,7 @@ const renderMembershipRow = ({
|
|||
{i18n('icu:MessageRequestWarning__learn-more')}
|
||||
</button>
|
||||
</div>
|
||||
{safetyTipsButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -136,6 +157,7 @@ export function ConversationHero({
|
|||
viewUserStories,
|
||||
toggleAboutContactModal,
|
||||
}: Props): JSX.Element {
|
||||
const [isShowingSafetyTips, setIsShowingSafetyTips] = useState(false);
|
||||
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
|
||||
useState(false);
|
||||
const closeMessageRequestWarning = () => {
|
||||
|
@ -248,6 +270,9 @@ export function ConversationHero({
|
|||
onClickMessageRequestWarning() {
|
||||
setIsShowingMessageRequestWarning(true);
|
||||
},
|
||||
onToggleSafetyTips(showSafetyTips: boolean) {
|
||||
setIsShowingSafetyTips(showSafetyTips);
|
||||
},
|
||||
phoneNumber,
|
||||
sharedGroupNames,
|
||||
})}
|
||||
|
@ -277,6 +302,15 @@ export function ConversationHero({
|
|||
{i18n('icu:MessageRequestWarning__dialog__details')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
{isShowingSafetyTips && (
|
||||
<SafetyTipsModal
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setIsShowingSafetyTips(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
/* eslint-enable no-nested-ternary */
|
||||
|
|
|
@ -8,9 +8,17 @@ import type { Props } from './MandatoryProfileSharingActions';
|
|||
import { MandatoryProfileSharingActions } from './MandatoryProfileSharingActions';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
} from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
type Args = {
|
||||
conversationType: 'direct' | 'group';
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/MandatoryProfileSharingActions',
|
||||
argTypes: {
|
||||
|
@ -20,34 +28,43 @@ export default {
|
|||
options: ['direct', 'group'],
|
||||
},
|
||||
},
|
||||
firstName: { control: { type: 'text' } },
|
||||
title: { control: { type: 'text' } },
|
||||
},
|
||||
args: {
|
||||
conversationId: '123',
|
||||
i18n,
|
||||
conversationType: 'direct',
|
||||
firstName: 'Cayce',
|
||||
title: 'Cayce Bollard',
|
||||
acceptConversation: action('acceptConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
} satisfies Meta<Args>;
|
||||
|
||||
export function Direct(args: Props): JSX.Element {
|
||||
function Example(args: Args) {
|
||||
const conversation =
|
||||
args.conversationType === 'group'
|
||||
? getDefaultGroup()
|
||||
: getDefaultConversation();
|
||||
const addedBy =
|
||||
args.conversationType === 'group' ? getDefaultConversation() : conversation;
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MandatoryProfileSharingActions {...args} />
|
||||
<MandatoryProfileSharingActions
|
||||
addedByName={addedBy}
|
||||
conversationType={conversation.type}
|
||||
conversationId={conversation.id}
|
||||
conversationName={conversation}
|
||||
i18n={i18n}
|
||||
isBlocked={conversation.isBlocked ?? false}
|
||||
isReported={conversation.isReported ?? false}
|
||||
acceptConversation={action('acceptConversation')}
|
||||
blockAndReportSpam={action('blockAndReportSpam')}
|
||||
blockConversation={action('blockConversation')}
|
||||
deleteConversation={action('deleteConversation')}
|
||||
reportSpam={action('reportSpam')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Direct(args: Props): JSX.Element {
|
||||
return <Example {...args} conversationType="direct" />;
|
||||
}
|
||||
|
||||
export function Group(args: Props): JSX.Element {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MandatoryProfileSharingActions {...args} conversationType="group" />
|
||||
</div>
|
||||
);
|
||||
return <Example {...args} conversationType="group" />;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { PropsType as ContactNameProps } from './ContactName';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import type { Props as MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||
import type { MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
|
@ -15,17 +14,20 @@ import type { LocalizerType } from '../../types/Util';
|
|||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
firstName?: string;
|
||||
} & Omit<ContactNameProps, 'module'> &
|
||||
Pick<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
| 'acceptConversation'
|
||||
| 'blockAndReportSpam'
|
||||
| 'blockConversation'
|
||||
| 'conversationId'
|
||||
| 'conversationType'
|
||||
| 'deleteConversation'
|
||||
>;
|
||||
} & Pick<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
| 'addedByName'
|
||||
| 'conversationId'
|
||||
| 'conversationType'
|
||||
| 'conversationName'
|
||||
| 'isBlocked'
|
||||
| 'isReported'
|
||||
| 'acceptConversation'
|
||||
| 'reportSpam'
|
||||
| 'blockAndReportSpam'
|
||||
| 'blockConversation'
|
||||
| 'deleteConversation'
|
||||
>;
|
||||
|
||||
const learnMoreLink = (parts: Array<JSX.Element | string>) => (
|
||||
<a
|
||||
|
@ -39,15 +41,18 @@ const learnMoreLink = (parts: Array<JSX.Element | string>) => (
|
|||
);
|
||||
|
||||
export function MandatoryProfileSharingActions({
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
addedByName,
|
||||
conversationId,
|
||||
conversationType,
|
||||
deleteConversation,
|
||||
firstName,
|
||||
conversationName,
|
||||
i18n,
|
||||
title,
|
||||
isBlocked,
|
||||
isReported,
|
||||
acceptConversation,
|
||||
reportSpam,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
deleteConversation,
|
||||
}: Props): JSX.Element {
|
||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||
|
||||
|
@ -56,7 +61,7 @@ export function MandatoryProfileSharingActions({
|
|||
key="name"
|
||||
className="module-message-request-actions__message__name"
|
||||
>
|
||||
<ContactName firstName={firstName} title={title} preferFirstName />
|
||||
<ContactName {...conversationName} preferFirstName />
|
||||
</strong>
|
||||
);
|
||||
|
||||
|
@ -64,19 +69,23 @@ export function MandatoryProfileSharingActions({
|
|||
<>
|
||||
{mrState !== MessageRequestState.default ? (
|
||||
<MessageRequestActionsConfirmation
|
||||
addedByName={addedByName}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversationType}
|
||||
conversationName={conversationName}
|
||||
i18n={i18n}
|
||||
isBlocked={isBlocked}
|
||||
isReported={isReported}
|
||||
state={mrState}
|
||||
acceptConversation={() => {
|
||||
throw new Error(
|
||||
'Should not be able to unblock from MandatoryProfileSharingActions'
|
||||
);
|
||||
}}
|
||||
blockConversation={blockConversation}
|
||||
conversationId={conversationId}
|
||||
deleteConversation={deleteConversation}
|
||||
i18n={i18n}
|
||||
reportSpam={reportSpam}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
title={title}
|
||||
conversationType={conversationType}
|
||||
state={mrState}
|
||||
onChangeState={setMrState}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -4,13 +4,23 @@
|
|||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { Props } from './MessageRequestActions';
|
||||
import { MessageRequestActions } from './MessageRequestActions';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
} from '../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
type Args = {
|
||||
conversationType: 'direct' | 'group';
|
||||
isBlocked: boolean;
|
||||
isHidden: boolean;
|
||||
isReported: boolean;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/MessageRequestActions',
|
||||
argTypes: {
|
||||
|
@ -20,19 +30,9 @@ export default {
|
|||
options: ['direct', 'group'],
|
||||
},
|
||||
},
|
||||
firstName: { control: { type: 'text' } },
|
||||
title: { control: { type: 'text' } },
|
||||
},
|
||||
args: {
|
||||
conversationId: '123',
|
||||
i18n,
|
||||
conversationType: 'direct',
|
||||
firstName: 'Cayce',
|
||||
title: 'Cayce Bollard',
|
||||
acceptConversation: action('acceptConversation'),
|
||||
blockAndReportSpam: action('blockAndReportSpam'),
|
||||
blockConversation: action('blockConversation'),
|
||||
deleteConversation: action('deleteConversation'),
|
||||
},
|
||||
decorators: [
|
||||
(Story: React.ComponentType): JSX.Element => {
|
||||
|
@ -43,20 +43,62 @@ export default {
|
|||
);
|
||||
},
|
||||
],
|
||||
} satisfies Meta<Props>;
|
||||
} satisfies Meta<Args>;
|
||||
|
||||
export function Direct(args: Props): JSX.Element {
|
||||
return <MessageRequestActions {...args} />;
|
||||
function Example(args: Args): JSX.Element {
|
||||
const conversation =
|
||||
args.conversationType === 'group'
|
||||
? getDefaultGroup()
|
||||
: getDefaultConversation();
|
||||
const addedBy =
|
||||
args.conversationType === 'group' ? getDefaultConversation() : conversation;
|
||||
return (
|
||||
<MessageRequestActions
|
||||
addedByName={addedBy}
|
||||
conversationType={conversation.type}
|
||||
conversationId={conversation.id}
|
||||
conversationName={conversation}
|
||||
i18n={i18n}
|
||||
isBlocked={args.isBlocked}
|
||||
isHidden={args.isHidden}
|
||||
isReported={args.isReported}
|
||||
acceptConversation={action('acceptConversation')}
|
||||
blockAndReportSpam={action('blockAndReportSpam')}
|
||||
blockConversation={action('blockConversation')}
|
||||
deleteConversation={action('deleteConversation')}
|
||||
reportSpam={action('reportSpam')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DirectBlocked(args: Props): JSX.Element {
|
||||
return <MessageRequestActions {...args} isBlocked />;
|
||||
export function Direct(args: Args): JSX.Element {
|
||||
return <Example {...args} />;
|
||||
}
|
||||
|
||||
export function Group(args: Props): JSX.Element {
|
||||
return <MessageRequestActions {...args} conversationType="group" />;
|
||||
export function DirectBlocked(args: Args): JSX.Element {
|
||||
return <Example {...args} isBlocked />;
|
||||
}
|
||||
|
||||
export function GroupBlocked(args: Props): JSX.Element {
|
||||
return <MessageRequestActions {...args} conversationType="group" isBlocked />;
|
||||
export function DirectReported(args: Args): JSX.Element {
|
||||
return <Example {...args} isReported />;
|
||||
}
|
||||
|
||||
export function DirectBlockedAndReported(args: Args): JSX.Element {
|
||||
return <Example {...args} isBlocked isReported />;
|
||||
}
|
||||
|
||||
export function Group(args: Args): JSX.Element {
|
||||
return <Example {...args} conversationType="group" />;
|
||||
}
|
||||
|
||||
export function GroupBlocked(args: Args): JSX.Element {
|
||||
return <Example {...args} conversationType="group" isBlocked />;
|
||||
}
|
||||
|
||||
export function GroupReported(args: Args): JSX.Element {
|
||||
return <Example {...args} conversationType="group" isReported />;
|
||||
}
|
||||
|
||||
export function GroupBlockedAndReported(args: Args): JSX.Element {
|
||||
return <Example {...args} conversationType="group" isBlocked isReported />;
|
||||
}
|
||||
|
|
|
@ -2,52 +2,57 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { PropsType as ContactNameProps } from './ContactName';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import type { Props as MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||
import type { MessageRequestActionsConfirmationProps } from './MessageRequestActionsConfirmation';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
} from './MessageRequestActionsConfirmation';
|
||||
import { Intl } from '../Intl';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
isHidden?: boolean;
|
||||
} & Omit<ContactNameProps, 'module'> &
|
||||
Omit<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
'i18n' | 'state' | 'onChangeState'
|
||||
>;
|
||||
isHidden: boolean | null;
|
||||
} & Omit<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
'i18n' | 'state' | 'onChangeState'
|
||||
>;
|
||||
|
||||
export function MessageRequestActions({
|
||||
addedByName,
|
||||
conversationId,
|
||||
conversationType,
|
||||
conversationName,
|
||||
i18n,
|
||||
isBlocked,
|
||||
isHidden,
|
||||
isReported,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
conversationId,
|
||||
conversationType,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
firstName,
|
||||
i18n,
|
||||
isHidden,
|
||||
isBlocked,
|
||||
title,
|
||||
}: Props): JSX.Element {
|
||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||
|
||||
const name = (
|
||||
<strong
|
||||
key="name"
|
||||
className="module-message-request-actions__message__name"
|
||||
>
|
||||
<ContactName firstName={firstName} title={title} preferFirstName />
|
||||
</strong>
|
||||
);
|
||||
const nameValue =
|
||||
conversationType === 'direct' ? conversationName : addedByName;
|
||||
|
||||
let message: JSX.Element | undefined;
|
||||
if (conversationType === 'direct') {
|
||||
strictAssert(nameValue != null, 'nameValue is null');
|
||||
const name = (
|
||||
<strong
|
||||
key="name"
|
||||
className="module-message-request-actions__message__name"
|
||||
>
|
||||
<ContactName {...nameValue} preferFirstName />
|
||||
</strong>
|
||||
);
|
||||
|
||||
if (isBlocked) {
|
||||
message = (
|
||||
<Intl
|
||||
|
@ -87,39 +92,26 @@ export function MessageRequestActions({
|
|||
<>
|
||||
{mrState !== MessageRequestState.default ? (
|
||||
<MessageRequestActionsConfirmation
|
||||
addedByName={addedByName}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversationType}
|
||||
conversationName={conversationName}
|
||||
i18n={i18n}
|
||||
isBlocked={isBlocked}
|
||||
isReported={isReported}
|
||||
state={mrState}
|
||||
acceptConversation={acceptConversation}
|
||||
blockAndReportSpam={blockAndReportSpam}
|
||||
blockConversation={blockConversation}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversationType}
|
||||
reportSpam={reportSpam}
|
||||
deleteConversation={deleteConversation}
|
||||
i18n={i18n}
|
||||
onChangeState={setMrState}
|
||||
state={mrState}
|
||||
title={title}
|
||||
/>
|
||||
) : null}
|
||||
<div className="module-message-request-actions">
|
||||
<p className="module-message-request-actions__message">{message}</p>
|
||||
<div className="module-message-request-actions__buttons">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.deleting);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
>
|
||||
{i18n('icu:MessageRequests--delete')}
|
||||
</Button>
|
||||
{isBlocked ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.unblocking);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
>
|
||||
{i18n('icu:MessageRequests--unblock')}
|
||||
</Button>
|
||||
) : (
|
||||
{!isBlocked && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.blocking);
|
||||
|
@ -129,6 +121,36 @@ export function MessageRequestActions({
|
|||
{i18n('icu:MessageRequests--block')}
|
||||
</Button>
|
||||
)}
|
||||
{(isReported || isBlocked) && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.deleting);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
>
|
||||
{i18n('icu:MessageRequests--delete')}
|
||||
</Button>
|
||||
)}
|
||||
{!isReported && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.reportingAndMaybeBlocking);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryDestructive}
|
||||
>
|
||||
{i18n('icu:MessageRequests--reportAndMaybeBlock')}
|
||||
</Button>
|
||||
)}
|
||||
{isBlocked && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.unblocking);
|
||||
}}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
>
|
||||
{i18n('icu:MessageRequests--unblock')}
|
||||
</Button>
|
||||
)}
|
||||
{!isBlocked ? (
|
||||
<Button
|
||||
onClick={() => acceptConversation(conversationId)}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import type { PropsType as ContactNameProps } from './ContactName';
|
||||
import type { ContactNameData } from './ContactName';
|
||||
import { ContactName } from './ContactName';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { Intl } from '../Intl';
|
||||
|
@ -12,38 +12,53 @@ export enum MessageRequestState {
|
|||
blocking,
|
||||
deleting,
|
||||
unblocking,
|
||||
reportingAndMaybeBlocking,
|
||||
acceptedOptions,
|
||||
default,
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
acceptConversation(conversationId: string): unknown;
|
||||
blockAndReportSpam(conversationId: string): unknown;
|
||||
blockConversation(conversationId: string): unknown;
|
||||
export type MessageRequestActionsConfirmationBaseProps = {
|
||||
addedByName: ContactNameData | null;
|
||||
conversationId: string;
|
||||
conversationType: 'group' | 'direct';
|
||||
deleteConversation(conversationId: string): unknown;
|
||||
i18n: LocalizerType;
|
||||
isBlocked?: boolean;
|
||||
onChangeState(state: MessageRequestState): unknown;
|
||||
state: MessageRequestState;
|
||||
} & Omit<ContactNameProps, 'module'>;
|
||||
conversationName: ContactNameData;
|
||||
isBlocked: boolean;
|
||||
isReported: boolean;
|
||||
acceptConversation(conversationId: string): void;
|
||||
blockAndReportSpam(conversationId: string): void;
|
||||
blockConversation(conversationId: string): void;
|
||||
reportSpam(conversationId: string): void;
|
||||
deleteConversation(conversationId: string): void;
|
||||
};
|
||||
|
||||
export type MessageRequestActionsConfirmationProps =
|
||||
MessageRequestActionsConfirmationBaseProps & {
|
||||
i18n: LocalizerType;
|
||||
state: MessageRequestState;
|
||||
onChangeState(state: MessageRequestState): void;
|
||||
};
|
||||
|
||||
export function MessageRequestActionsConfirmation({
|
||||
addedByName,
|
||||
conversationId,
|
||||
conversationType,
|
||||
conversationName,
|
||||
i18n,
|
||||
isBlocked,
|
||||
state,
|
||||
acceptConversation,
|
||||
blockAndReportSpam,
|
||||
blockConversation,
|
||||
conversationId,
|
||||
conversationType,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
i18n,
|
||||
onChangeState,
|
||||
state,
|
||||
title,
|
||||
}: Props): JSX.Element | null {
|
||||
}: MessageRequestActionsConfirmationProps): JSX.Element | null {
|
||||
if (state === MessageRequestState.blocking) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.blocking"
|
||||
dialogName="messageRequestActionsConfirmation.blocking"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
|
@ -54,7 +69,13 @@ export function MessageRequestActionsConfirmation({
|
|||
i18n={i18n}
|
||||
id="icu:MessageRequests--block-direct-confirm-title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
title: (
|
||||
<ContactName
|
||||
key="name"
|
||||
{...conversationName}
|
||||
preferFirstName
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
@ -62,21 +83,18 @@ export function MessageRequestActionsConfirmation({
|
|||
i18n={i18n}
|
||||
id="icu:MessageRequests--block-group-confirm-title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
title: (
|
||||
<ContactName
|
||||
key="name"
|
||||
{...conversationName}
|
||||
preferFirstName
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
actions={[
|
||||
...(conversationType === 'direct'
|
||||
? [
|
||||
{
|
||||
text: i18n('icu:MessageRequests--block-and-report-spam'),
|
||||
action: () => blockAndReportSpam(conversationId),
|
||||
style: 'negative' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
text: i18n('icu:MessageRequests--block'),
|
||||
action: () => blockConversation(conversationId),
|
||||
|
@ -91,10 +109,62 @@ export function MessageRequestActionsConfirmation({
|
|||
);
|
||||
}
|
||||
|
||||
if (state === MessageRequestState.reportingAndMaybeBlocking) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.reportingAndMaybeBlocking"
|
||||
dialogName="messageRequestActionsConfirmation.reportingAndMaybeBlocking"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
}}
|
||||
title={i18n('icu:MessageRequests--ReportAndMaybeBlockModal-title')}
|
||||
actions={[
|
||||
...(!isBlocked
|
||||
? ([
|
||||
{
|
||||
text: i18n(
|
||||
'icu:MessageRequests--ReportAndMaybeBlockModal-reportAndBlock'
|
||||
),
|
||||
action: () => blockAndReportSpam(conversationId),
|
||||
style: 'negative',
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
{
|
||||
text: i18n('icu:MessageRequests--ReportAndMaybeBlockModal-report'),
|
||||
action: () => reportSpam(conversationId),
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{conversationType === 'direct' ? (
|
||||
i18n('icu:MessageRequests--ReportAndMaybeBlockModal-body--direct')
|
||||
) : addedByName == null ? (
|
||||
i18n(
|
||||
'icu:MessageRequests--ReportAndMaybeBlockModal-body--group--unknown-contact'
|
||||
)
|
||||
) : (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:MessageRequests--ReportAndMaybeBlockModal-body--group"
|
||||
components={{
|
||||
name: <ContactName key="name" {...addedByName} preferFirstName />,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === MessageRequestState.unblocking) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.unblocking"
|
||||
dialogName="messageRequestActionsConfirmation.unblocking"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
|
@ -104,7 +174,9 @@ export function MessageRequestActionsConfirmation({
|
|||
i18n={i18n}
|
||||
id="icu:MessageRequests--unblock-direct-confirm-title"
|
||||
components={{
|
||||
name: <ContactName key="name" title={title} />,
|
||||
name: (
|
||||
<ContactName key="name" {...conversationName} preferFirstName />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
@ -126,7 +198,9 @@ export function MessageRequestActionsConfirmation({
|
|||
if (state === MessageRequestState.deleting) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.deleting"
|
||||
dialogName="messageRequestActionsConfirmation.deleting"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
|
@ -142,7 +216,13 @@ export function MessageRequestActionsConfirmation({
|
|||
i18n={i18n}
|
||||
id="icu:MessageRequests--delete-group-confirm-title"
|
||||
components={{
|
||||
title: <ContactName key="name" title={title} />,
|
||||
title: (
|
||||
<ContactName
|
||||
key="name"
|
||||
{...conversationName}
|
||||
preferFirstName
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
@ -165,5 +245,42 @@ export function MessageRequestActionsConfirmation({
|
|||
);
|
||||
}
|
||||
|
||||
if (state === MessageRequestState.acceptedOptions) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
key="messageRequestActionsConfirmation.acceptedOptions"
|
||||
dialogName="messageRequestActionsConfirmation.acceptedOptions"
|
||||
moduleClassName="MessageRequestActionsConfirmation"
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
text: i18n('icu:MessageRequests--reportAndMaybeBlock'),
|
||||
action: () =>
|
||||
onChangeState(MessageRequestState.reportingAndMaybeBlocking),
|
||||
style: 'negative',
|
||||
},
|
||||
{
|
||||
text: i18n('icu:MessageRequests--block'),
|
||||
action: () => onChangeState(MessageRequestState.blocking),
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:MessageRequests--AcceptedOptionsModal--body"
|
||||
components={{
|
||||
name: (
|
||||
<ContactName key="name" {...conversationName} preferFirstName />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { useState } from 'react';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
import { SystemMessage } from './SystemMessage';
|
||||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
import { MessageRequestState } from './MessageRequestActionsConfirmation';
|
||||
import { SafetyTipsModal } from '../SafetyTipsModal';
|
||||
import { MessageRequestResponseEvent } from '../../types/MessageRequestResponseEvent';
|
||||
|
||||
export type MessageRequestResponseNotificationData = {
|
||||
messageRequestResponseEvent: MessageRequestResponseEvent;
|
||||
};
|
||||
|
||||
export type MessageRequestResponseNotificationProps =
|
||||
MessageRequestResponseNotificationData & {
|
||||
i18n: LocalizerType;
|
||||
isBlocked: boolean;
|
||||
onOpenMessageRequestActionsConfirmation(state: MessageRequestState): void;
|
||||
};
|
||||
|
||||
export function MessageRequestResponseNotification({
|
||||
i18n,
|
||||
isBlocked,
|
||||
messageRequestResponseEvent: event,
|
||||
onOpenMessageRequestActionsConfirmation,
|
||||
}: MessageRequestResponseNotificationProps): JSX.Element | null {
|
||||
const [isSafetyTipsModalOpen, setIsSafetyTipsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{event === MessageRequestResponseEvent.ACCEPT && (
|
||||
<SystemMessage
|
||||
icon="thread"
|
||||
contents={i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Accepted'
|
||||
)}
|
||||
button={
|
||||
isBlocked ? null : (
|
||||
<Button
|
||||
className="MessageRequestResponseNotification__Button"
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SystemMessage}
|
||||
onClick={() => {
|
||||
onOpenMessageRequestActionsConfirmation(
|
||||
MessageRequestState.acceptedOptions
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:MessageRequestResponseNotification__Button--Options'
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{event === MessageRequestResponseEvent.BLOCK && (
|
||||
<SystemMessage
|
||||
icon="block"
|
||||
contents={i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Blocked'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{event === MessageRequestResponseEvent.SPAM && (
|
||||
<SystemMessage
|
||||
icon="spam"
|
||||
contents={i18n(
|
||||
'icu:MessageRequestResponseNotification__Message--Reported'
|
||||
)}
|
||||
button={
|
||||
<Button
|
||||
className="MessageRequestResponseNotification__Button"
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SystemMessage}
|
||||
onClick={() => {
|
||||
setIsSafetyTipsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:MessageRequestResponseNotification__Button--LearnMore'
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isSafetyTipsModalOpen && (
|
||||
<SafetyTipsModal
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setIsSafetyTipsModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -16,6 +16,7 @@ export type PropsType = {
|
|||
| 'audio-incoming'
|
||||
| 'audio-missed'
|
||||
| 'audio-outgoing'
|
||||
| 'block'
|
||||
| 'group'
|
||||
| 'group-access'
|
||||
| 'group-add'
|
||||
|
@ -30,6 +31,7 @@ export type PropsType = {
|
|||
| 'phone'
|
||||
| 'profile'
|
||||
| 'safety-number'
|
||||
| 'spam'
|
||||
| 'session-refresh'
|
||||
| 'thread'
|
||||
| 'timer'
|
||||
|
|
|
@ -335,6 +335,10 @@ const actions = () => ({
|
|||
viewStory: action('viewStory'),
|
||||
|
||||
onReplyToMessage: action('onReplyToMessage'),
|
||||
|
||||
onOpenMessageRequestActionsConfirmation: action(
|
||||
'onOpenMessageRequestActionsConfirmation'
|
||||
),
|
||||
});
|
||||
|
||||
const renderItem = ({
|
||||
|
@ -350,6 +354,7 @@ const renderItem = ({
|
|||
getPreferredBadge={() => undefined}
|
||||
id=""
|
||||
isTargeted={false}
|
||||
isBlocked={false}
|
||||
i18n={i18n}
|
||||
interactionMode="keyboard"
|
||||
isNextItemCallingNotification={false}
|
||||
|
@ -442,6 +447,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
getTimestampForMessage: Date.now,
|
||||
haveNewest: overrideProps.haveNewest ?? false,
|
||||
haveOldest: overrideProps.haveOldest ?? false,
|
||||
isBlocked: false,
|
||||
isConversationSelected: true,
|
||||
isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false,
|
||||
items: overrideProps.items ?? Object.keys(items),
|
||||
|
|
|
@ -81,6 +81,7 @@ export type PropsDataType = {
|
|||
|
||||
type PropsHousekeepingType = {
|
||||
id: string;
|
||||
isBlocked: boolean;
|
||||
isConversationSelected: boolean;
|
||||
isGroupV1AndDisabled?: boolean;
|
||||
isIncomingMessageRequest: boolean;
|
||||
|
@ -121,6 +122,7 @@ type PropsHousekeepingType = {
|
|||
containerElementRef: RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
conversationId: string;
|
||||
isBlocked: boolean;
|
||||
isOldestTimelineItem: boolean;
|
||||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
|
@ -786,6 +788,7 @@ export class Timeline extends React.Component<
|
|||
i18n,
|
||||
id,
|
||||
invitedContactsForNewlyCreatedGroup,
|
||||
isBlocked,
|
||||
isConversationSelected,
|
||||
isGroupV1AndDisabled,
|
||||
items,
|
||||
|
@ -928,6 +931,7 @@ export class Timeline extends React.Component<
|
|||
containerElementRef: this.containerRef,
|
||||
containerWidthBreakpoint: widthBreakpoint,
|
||||
conversationId: id,
|
||||
isBlocked,
|
||||
isOldestTimelineItem: haveOldest && itemIndex === 0,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
|
|
|
@ -59,6 +59,7 @@ const getDefaultProps = () => ({
|
|||
id: 'asdf',
|
||||
isNextItemCallingNotification: false,
|
||||
isTargeted: false,
|
||||
isBlocked: false,
|
||||
interactionMode: 'keyboard' as const,
|
||||
theme: ThemeType.light,
|
||||
platform: 'darwin',
|
||||
|
@ -118,6 +119,9 @@ const getDefaultProps = () => ({
|
|||
viewStory: action('viewStory'),
|
||||
|
||||
onReplyToMessage: action('onReplyToMessage'),
|
||||
onOpenMessageRequestActionsConfirmation: action(
|
||||
'onOpenMessageRequestActionsConfirmation'
|
||||
),
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
@ -56,6 +56,11 @@ import type { PropsDataType as PhoneNumberDiscoveryNotificationPropsType } from
|
|||
import { PhoneNumberDiscoveryNotification } from './PhoneNumberDiscoveryNotification';
|
||||
import { SystemMessage } from './SystemMessage';
|
||||
import { TimelineMessage } from './TimelineMessage';
|
||||
import {
|
||||
MessageRequestResponseNotification,
|
||||
type MessageRequestResponseNotificationData,
|
||||
} from './MessageRequestResponseNotification';
|
||||
import type { MessageRequestState } from './MessageRequestActionsConfirmation';
|
||||
|
||||
type CallHistoryType = {
|
||||
type: 'callHistory';
|
||||
|
@ -137,6 +142,10 @@ type PaymentEventType = {
|
|||
type: 'paymentEvent';
|
||||
data: Omit<PaymentEventNotificationPropsType, 'i18n'>;
|
||||
};
|
||||
type MessageRequestResponseNotificationType = {
|
||||
type: 'messageRequestResponse';
|
||||
data: MessageRequestResponseNotificationData;
|
||||
};
|
||||
|
||||
export type TimelineItemType = (
|
||||
| CallHistoryType
|
||||
|
@ -159,6 +168,7 @@ export type TimelineItemType = (
|
|||
| UnsupportedMessageType
|
||||
| VerificationNotificationType
|
||||
| PaymentEventType
|
||||
| MessageRequestResponseNotificationType
|
||||
) & { timestamp: number };
|
||||
|
||||
type PropsLocalType = {
|
||||
|
@ -166,10 +176,12 @@ type PropsLocalType = {
|
|||
conversationId: string;
|
||||
item?: TimelineItemType;
|
||||
id: string;
|
||||
isBlocked: boolean;
|
||||
isNextItemCallingNotification: boolean;
|
||||
isTargeted: boolean;
|
||||
targetMessage: (messageId: string, conversationId: string) => unknown;
|
||||
shouldRenderDateHeader: boolean;
|
||||
onOpenMessageRequestActionsConfirmation(state: MessageRequestState): void;
|
||||
platform: string;
|
||||
renderContact: SmartContactRendererType<JSX.Element>;
|
||||
renderUniversalTimerNotification: () => JSX.Element;
|
||||
|
@ -203,9 +215,11 @@ export const TimelineItem = memo(function TimelineItem({
|
|||
getPreferredBadge,
|
||||
i18n,
|
||||
id,
|
||||
isBlocked,
|
||||
isNextItemCallingNotification,
|
||||
isTargeted,
|
||||
item,
|
||||
onOpenMessageRequestActionsConfirmation,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
platform,
|
||||
|
@ -379,6 +393,17 @@ export const TimelineItem = memo(function TimelineItem({
|
|||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'messageRequestResponse') {
|
||||
notification = (
|
||||
<MessageRequestResponseNotification
|
||||
{...item.data}
|
||||
i18n={i18n}
|
||||
isBlocked={isBlocked}
|
||||
onOpenMessageRequestActionsConfirmation={
|
||||
onOpenMessageRequestActionsConfirmation
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Weird, yes, but the idea is to get a compile error when we aren't comprehensive
|
||||
// with our if/else checks above, but also log out the type we don't understand
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue