Fix story reply box interactions

This commit is contained in:
Josh Perez 2022-04-22 23:16:13 -04:00 committed by GitHub
parent 7775f7d806
commit 72f979ea1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 128 deletions

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { usePopper } from 'react-popper';
import type { AttachmentType } from '../types/Attachment';
@ -52,6 +52,7 @@ export type PropsType = {
authorTitle: string;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
isGroupStory?: boolean;
isMyStory?: boolean;
onClose: () => unknown;
onReact: (emoji: string) => unknown;
@ -76,6 +77,7 @@ export const StoryViewsNRepliesModal = ({
authorTitle,
getPreferredBadge,
i18n,
isGroupStory,
isMyStory,
onClose,
onReact,
@ -91,7 +93,8 @@ export const StoryViewsNRepliesModal = ({
storyPreviewAttachment,
views,
}: PropsType): JSX.Element => {
const inputApiRef = React.useRef<InputApi | undefined>();
const inputApiRef = useRef<InputApi | undefined>();
const [bottom, setBottom] = useState<HTMLDivElement | null>(null);
const [messageBodyText, setMessageBodyText] = useState('');
const [showReactionPicker, setShowReactionPicker] = useState(false);
@ -122,13 +125,19 @@ export const StoryViewsNRepliesModal = ({
strategy: 'fixed',
});
useEffect(() => {
if (replies.length) {
bottom?.scrollIntoView({ behavior: 'smooth' });
}
}, [bottom, replies.length]);
let composerElement: JSX.Element | undefined;
if (!isMyStory) {
composerElement = (
<div className="StoryViewsNRepliesModal__compose-container">
<div className="StoryViewsNRepliesModal__composer">
{!replies.length && (
{!isGroupStory && (
<Quote
authorTitle={authorTitle}
conversationColor="ultramarine"
@ -154,7 +163,10 @@ export const StoryViewsNRepliesModal = ({
setMessageBodyText(messageText);
}}
onPickEmoji={insertEmoji}
onSubmit={onReply}
onSubmit={(...args) => {
inputApiRef.current?.reset();
onReply(...args);
}}
onTextTooLong={onTextTooLong}
placeholder={i18n('StoryViewsNRepliesModal__placeholder')}
theme={ThemeType.dark}
@ -204,12 +216,48 @@ export const StoryViewsNRepliesModal = ({
);
}
const repliesElement = replies.length ? (
<div className="StoryViewsNRepliesModal__replies">
{replies.map(reply =>
reply.reactionEmoji ? (
<div className="StoryViewsNRepliesModal__reaction" key={reply.id}>
<div className="StoryViewsNRepliesModal__reaction--container">
let repliesElement: JSX.Element | undefined;
if (replies.length) {
repliesElement = (
<div className="StoryViewsNRepliesModal__replies">
{replies.map(reply =>
reply.reactionEmoji ? (
<div className="StoryViewsNRepliesModal__reaction" key={reply.id}>
<div className="StoryViewsNRepliesModal__reaction--container">
<Avatar
acceptedMessageRequest={reply.acceptedMessageRequest}
avatarPath={reply.avatarPath}
badge={undefined}
color={getAvatarColor(reply.color)}
conversationType="direct"
i18n={i18n}
isMe={Boolean(reply.isMe)}
name={reply.name}
profileName={reply.profileName}
sharedGroupNames={reply.sharedGroupNames || []}
size={AvatarSize.TWENTY_EIGHT}
title={reply.title}
/>
<div className="StoryViewsNRepliesModal__reaction--body">
<div className="StoryViewsNRepliesModal__reply--title">
<ContactName
contactNameColor={reply.contactNameColor}
title={reply.title}
/>
</div>
{i18n('StoryViewsNRepliesModal__reacted')}
<MessageTimestamp
i18n={i18n}
module="StoryViewsNRepliesModal__reply--timestamp"
timestamp={reply.timestamp}
/>
</div>
</div>
<Emojify text={reply.reactionEmoji} />
</div>
) : (
<div className="StoryViewsNRepliesModal__reply" key={reply.id}>
<Avatar
acceptedMessageRequest={reply.acceptedMessageRequest}
avatarPath={reply.avatarPath}
@ -224,14 +272,32 @@ export const StoryViewsNRepliesModal = ({
size={AvatarSize.TWENTY_EIGHT}
title={reply.title}
/>
<div className="StoryViewsNRepliesModal__reaction--body">
<div
className={classNames(
'StoryViewsNRepliesModal__message-bubble',
{
'StoryViewsNRepliesModal__message-bubble--doe': Boolean(
reply.deletedForEveryone
),
}
)}
>
<div className="StoryViewsNRepliesModal__reply--title">
<ContactName
contactNameColor={reply.contactNameColor}
title={reply.title}
/>
</div>
{i18n('StoryViewsNRepliesModal__reacted')}
<MessageBody
i18n={i18n}
text={
reply.deletedForEveryone
? i18n('message--deletedForEveryone')
: String(reply.body)
}
/>
<MessageTimestamp
i18n={i18n}
module="StoryViewsNRepliesModal__reply--timestamp"
@ -239,58 +305,18 @@ export const StoryViewsNRepliesModal = ({
/>
</div>
</div>
<Emojify text={reply.reactionEmoji} />
</div>
) : (
<div className="StoryViewsNRepliesModal__reply" key={reply.id}>
<Avatar
acceptedMessageRequest={reply.acceptedMessageRequest}
avatarPath={reply.avatarPath}
badge={undefined}
color={getAvatarColor(reply.color)}
conversationType="direct"
i18n={i18n}
isMe={Boolean(reply.isMe)}
name={reply.name}
profileName={reply.profileName}
sharedGroupNames={reply.sharedGroupNames || []}
size={AvatarSize.TWENTY_EIGHT}
title={reply.title}
/>
<div
className={classNames('StoryViewsNRepliesModal__message-bubble', {
'StoryViewsNRepliesModal__message-bubble--doe': Boolean(
reply.deletedForEveryone
),
})}
>
<div className="StoryViewsNRepliesModal__reply--title">
<ContactName
contactNameColor={reply.contactNameColor}
title={reply.title}
/>
</div>
<MessageBody
i18n={i18n}
text={
reply.deletedForEveryone
? i18n('message--deletedForEveryone')
: String(reply.body)
}
/>
<MessageTimestamp
i18n={i18n}
module="StoryViewsNRepliesModal__reply--timestamp"
timestamp={reply.timestamp}
/>
</div>
</div>
)
)}
</div>
) : undefined;
)
)}
<div ref={setBottom} />
</div>
);
} else if (isGroupStory) {
repliesElement = (
<div className="StoryViewsNRepliesModal__replies--none">
{i18n('StoryViewsNRepliesModal__no-replies')}
</div>
);
}
const viewsElement = views.length ? (
<div className="StoryViewsNRepliesModal__views">
@ -358,28 +384,26 @@ export const StoryViewsNRepliesModal = ({
</Tabs>
) : undefined;
const hasOnlyViewsElement =
viewsElement && !repliesElement && !composerElement;
return (
<Modal
i18n={i18n}
moduleClassName={classNames('StoryViewsNRepliesModal', {
'StoryViewsNRepliesModal--group': Boolean(
views.length && replies.length
),
})}
moduleClassName="StoryViewsNRepliesModal"
onClose={onClose}
useFocusTrap={!hasOnlyViewsElement}
useFocusTrap={Boolean(composerElement)}
theme={Theme.Dark}
>
{tabsElement || (
<>
{viewsElement}
{repliesElement}
{composerElement}
</>
)}
<div
className={classNames({
'StoryViewsNRepliesModal--group': Boolean(isGroupStory),
})}
>
{tabsElement || (
<>
{viewsElement || repliesElement}
{composerElement}
</>
)}
</div>
</Modal>
);
};