// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, {
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { usePopper } from 'react-popper';
import type { AttachmentType } from '../types/Attachment';
import type { BodyRangeType, LocalizerType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { InputApi } from './CompositionInput';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
import type { ReplyType, StorySendStateType } from '../types/Stories';
import { StoryViewTargetType } from '../types/Stories';
import { Avatar, AvatarSize } from './Avatar';
import { CompositionInput } from './CompositionInput';
import { ContactName } from './conversation/ContactName';
import { EmojiButton } from './emoji/EmojiButton';
import { Emojify } from './conversation/Emojify';
import { Message, TextDirection } from './conversation/Message';
import { MessageTimestamp } from './conversation/MessageTimestamp';
import { Modal } from './Modal';
import { Quote } from './conversation/Quote';
import { ReactionPicker } from './conversation/ReactionPicker';
import { Tabs } from './Tabs';
import { Theme } from '../util/theme';
import { ThemeType } from '../types/Util';
import { WidthBreakpoint } from './_util';
import { getAvatarColor } from '../types/Colors';
import { getStoryReplyText } from '../util/getStoryReplyText';
import { shouldNeverBeCalled } from '../util/shouldNeverBeCalled';
// Menu is disabled so these actions are inaccessible. We also don't support
// link previews, tap to view messages, attachments, or gifts. Just regular
// text messages and reactions.
const MESSAGE_DEFAULT_PROPS = {
canDeleteForEveryone: false,
canDownload: false,
canReact: false,
canReply: false,
canRetry: false,
canRetryDeleteForEveryone: false,
checkForAccount: shouldNeverBeCalled,
clearSelectedMessage: shouldNeverBeCalled,
containerWidthBreakpoint: WidthBreakpoint.Medium,
deleteMessage: shouldNeverBeCalled,
deleteMessageForEveryone: shouldNeverBeCalled,
displayTapToViewMessage: shouldNeverBeCalled,
doubleCheckMissingQuoteReference: shouldNeverBeCalled,
downloadAttachment: shouldNeverBeCalled,
isBlocked: false,
isMessageRequestAccepted: true,
kickOffAttachmentDownload: shouldNeverBeCalled,
markAttachmentAsCorrupted: shouldNeverBeCalled,
markViewed: shouldNeverBeCalled,
messageExpanded: shouldNeverBeCalled,
openConversation: shouldNeverBeCalled,
openGiftBadge: shouldNeverBeCalled,
openLink: shouldNeverBeCalled,
previews: [],
reactToMessage: shouldNeverBeCalled,
renderAudioAttachment: () =>
,
renderEmojiPicker: () => ,
renderReactionPicker: () => ,
replyToMessage: shouldNeverBeCalled,
retryDeleteForEveryone: shouldNeverBeCalled,
retrySend: shouldNeverBeCalled,
scrollToQuotedMessage: shouldNeverBeCalled,
showContactDetail: shouldNeverBeCalled,
showContactModal: shouldNeverBeCalled,
showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showForwardMessageModal: shouldNeverBeCalled,
showMessageDetail: shouldNeverBeCalled,
showVisualAttachment: shouldNeverBeCalled,
startConversation: shouldNeverBeCalled,
theme: ThemeType.dark,
viewStory: shouldNeverBeCalled,
};
export enum StoryViewsNRepliesTab {
Replies = 'Replies',
Views = 'Views',
}
export type PropsType = {
authorTitle: string;
canReply: boolean;
getPreferredBadge: PreferredBadgeSelectorType;
hasReadReceiptSetting: boolean;
hasViewsCapability: boolean;
i18n: LocalizerType;
group: Pick | undefined;
onClose: () => unknown;
onReact: (emoji: string) => unknown;
onReply: (
message: string,
mentions: Array,
timestamp: number
) => unknown;
onSetSkinTone: (tone: number) => unknown;
onTextTooLong: () => unknown;
onUseEmoji: (_: EmojiPickDataType) => unknown;
preferredReactionEmoji: Array;
recentEmojis?: Array;
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
replies: Array;
skinTone?: number;
sortedGroupMembers?: Array;
storyPreviewAttachment?: AttachmentType;
views: Array;
viewTarget: StoryViewTargetType;
onChangeViewTarget: (target: StoryViewTargetType) => unknown;
};
export const StoryViewsNRepliesModal = ({
authorTitle,
canReply,
getPreferredBadge,
hasReadReceiptSetting,
hasViewsCapability,
i18n,
group,
onClose,
onReact,
onReply,
onSetSkinTone,
onTextTooLong,
onUseEmoji,
preferredReactionEmoji,
recentEmojis,
renderEmojiPicker,
replies,
skinTone,
sortedGroupMembers,
storyPreviewAttachment,
views,
viewTarget,
onChangeViewTarget,
}: PropsType): JSX.Element | null => {
const containerElementRef = useRef(null);
const inputApiRef = useRef();
const shouldScrollToBottomRef = useRef(true);
const bottomRef = useRef(null);
const [messageBodyText, setMessageBodyText] = useState('');
const [showReactionPicker, setShowReactionPicker] = useState(false);
const currentTab = useMemo(() => {
return viewTarget === StoryViewTargetType.Replies
? StoryViewsNRepliesTab.Replies
: StoryViewsNRepliesTab.Views;
}, [viewTarget]);
const onTabChange = (tab: string) => {
onChangeViewTarget(
tab === StoryViewsNRepliesTab.Replies
? StoryViewTargetType.Replies
: StoryViewTargetType.Views
);
};
const focusComposer = useCallback(() => {
if (inputApiRef.current) {
inputApiRef.current.focus();
}
}, [inputApiRef]);
const insertEmoji = useCallback(
(e: EmojiPickDataType) => {
if (inputApiRef.current) {
inputApiRef.current.insertEmoji(e);
onUseEmoji(e);
}
},
[inputApiRef, onUseEmoji]
);
const [referenceElement, setReferenceElement] =
useState(null);
const [popperElement, setPopperElement] = useState(
null
);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'top-start',
strategy: 'fixed',
});
let composerElement: JSX.Element | undefined;
useLayoutEffect(() => {
if (
currentTab === StoryViewsNRepliesTab.Replies &&
replies.length &&
shouldScrollToBottomRef.current
) {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
shouldScrollToBottomRef.current = false;
}
}, [currentTab, replies.length]);
if (group && group.left) {
composerElement = (
{i18n('icu:StoryViewsNRepliesModal__not-a-member')}
);
} else if (canReply) {
composerElement = (
<>
{!group && (
)}
{
setMessageBodyText(messageText);
}}
onPickEmoji={onUseEmoji}
onSubmit={(...args) => {
inputApiRef.current?.reset();
shouldScrollToBottomRef.current = true;
onReply(...args);
}}
onTextTooLong={onTextTooLong}
placeholder={
group
? i18n('StoryViewer__reply-group')
: i18n('StoryViewer__reply')
}
sortedGroupMembers={sortedGroupMembers}
theme={ThemeType.dark}
>
>
);
}
let repliesElement: JSX.Element | undefined;
if (replies.length) {
repliesElement = (
{replies.map((reply, index) =>
reply.reactionEmoji ? (
{i18n('StoryViewsNRepliesModal__reacted')}
) : (
)
)}
);
} else if (group) {
repliesElement = (
{i18n('StoryViewsNRepliesModal__no-replies')}
);
}
let viewsElement: JSX.Element | undefined;
if (hasViewsCapability && !hasReadReceiptSetting) {
viewsElement = (
{i18n('StoryViewsNRepliesModal__read-receipts-off')}
);
} else if (views.length) {
viewsElement = (
);
} else if (hasViewsCapability) {
viewsElement = (
{i18n('StoryViewsNRepliesModal__no-views')}
);
}
const tabsElement =
viewsElement && repliesElement ? (
{({ selectedTab }) => (
<>
{selectedTab === StoryViewsNRepliesTab.Views && viewsElement}
{selectedTab === StoryViewsNRepliesTab.Replies && (
<>
{repliesElement}
{composerElement}
>
)}
>
)}
) : undefined;
if (!tabsElement && !viewsElement && !repliesElement && !composerElement) {
return null;
}
return (
{tabsElement || (
<>
{viewsElement || repliesElement}
{composerElement}
>
)}
);
};