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

@ -7026,6 +7026,10 @@
"message": "Type a reply...", "message": "Type a reply...",
"description": "Placeholder text for the story reply modal" "description": "Placeholder text for the story reply modal"
}, },
"StoryViewsNRepliesModal__no-replies": {
"message": "No replies yet",
"description": "Placeholder text for when there are no replies"
},
"StoryViewsNRepliesModal__tab--views": { "StoryViewsNRepliesModal__tab--views": {
"message": "Views", "message": "Views",
"description": "Title for views tab" "description": "Title for views tab"

View file

@ -3,9 +3,27 @@
.StoryViewsNRepliesModal { .StoryViewsNRepliesModal {
min-width: 320px; min-width: 320px;
overflow: hidden;
&--group { &--group {
min-height: 360px; display: flex;
flex-direction: column;
min-height: 400px;
}
&__replies {
flex: 1;
max-height: 75vh;
overflow-y: overlay;
&--none {
align-items: center;
color: $color-gray-45;
display: flex;
flex: 1;
justify-content: center;
user-select: none;
}
} }
&__overlay-container { &__overlay-container {

View file

@ -6,7 +6,6 @@
&__story { &__story {
align-items: center; align-items: center;
border-radius: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 1280px; height: 1280px;

View file

@ -60,6 +60,7 @@ export type PropsType = {
replyState?: ReplyStateType; replyState?: ReplyStateType;
skinTone?: number; skinTone?: number;
stories: Array<StoryViewType>; stories: Array<StoryViewType>;
views?: Array<string>;
}; };
const CAPTION_BUFFER = 20; const CAPTION_BUFFER = 20;
@ -88,6 +89,7 @@ export const StoryViewer = ({
replyState, replyState,
skinTone, skinTone,
stories, stories,
views,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [currentStoryIndex, setCurrentStoryIndex] = useState(0); const [currentStoryIndex, setCurrentStoryIndex] = useState(0);
const [storyDuration, setStoryDuration] = useState<number | undefined>(); const [storyDuration, setStoryDuration] = useState<number | undefined>();
@ -268,7 +270,7 @@ export const StoryViewer = ({
const replies = const replies =
replyState && replyState.messageId === messageId ? replyState.replies : []; replyState && replyState.messageId === messageId ? replyState.replies : [];
const viewCount = 0; const viewCount = (views || []).length;
const replyCount = replies.length; const replyCount = replies.length;
return ( return (
@ -388,49 +390,46 @@ export const StoryViewer = ({
))} ))}
</div> </div>
<div className="StoryViewer__actions"> <div className="StoryViewer__actions">
{isMe ? ( {canReply && (
<> <button
{viewCount && className="StoryViewer__reply"
(viewCount === 1 ? ( onClick={() => setHasReplyModal(true)}
<Intl tabIndex={0}
i18n={i18n} type="button"
id="MyStories__views--singular" >
components={[<strong>{viewCount}</strong>]} <>
/> {viewCount > 0 &&
) : ( (viewCount === 1 ? (
<Intl <Intl
i18n={i18n} i18n={i18n}
id="MyStories__views--plural" id="MyStories__views--singular"
components={[<strong>{viewCount}</strong>]} components={[<strong>{viewCount}</strong>]}
/> />
))} ) : (
{viewCount && replyCount && ' '} <Intl
{replyCount && i18n={i18n}
(replyCount === 1 ? ( id="MyStories__views--plural"
<Intl components={[<strong>{viewCount}</strong>]}
i18n={i18n} />
id="MyStories__replies--singular" ))}
components={[<strong>{replyCount}</strong>]} {viewCount > 0 && replyCount > 0 && ' '}
/> {replyCount > 0 &&
) : ( (replyCount === 1 ? (
<Intl <Intl
i18n={i18n} i18n={i18n}
id="MyStories__replies--plural" id="MyStories__replies--singular"
components={[<strong>{replyCount}</strong>]} components={[<strong>{replyCount}</strong>]}
/> />
))} ) : (
</> <Intl
) : ( i18n={i18n}
canReply && ( id="MyStories__replies--plural"
<button components={[<strong>{replyCount}</strong>]}
className="StoryViewer__reply" />
onClick={() => setHasReplyModal(true)} ))}
tabIndex={0} {!viewCount && !replyCount && i18n('StoryViewer__reply')}
type="button" </>
> </button>
{i18n('StoryViewer__reply')}
</button>
)
)} )}
</div> </div>
</div> </div>
@ -454,13 +453,16 @@ export const StoryViewer = ({
authorTitle={title} authorTitle={title}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
isGroupStory={isGroupStory}
isMyStory={isMe} isMyStory={isMe}
onClose={() => setHasReplyModal(false)} onClose={() => setHasReplyModal(false)}
onReact={emoji => { onReact={emoji => {
onReactToStory(emoji, visibleStory); onReactToStory(emoji, visibleStory);
}} }}
onReply={(message, mentions, replyTimestamp) => { onReply={(message, mentions, replyTimestamp) => {
setHasReplyModal(false); if (!isGroupStory) {
setHasReplyModal(false);
}
onReplyToStory(message, mentions, replyTimestamp, visibleStory); onReplyToStory(message, mentions, replyTimestamp, visibleStory);
}} }}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}

View file

@ -112,12 +112,17 @@ story.add('Views only', () => (
/> />
)); ));
story.add('In a group (no replies)', () => (
<StoryViewsNRepliesModal {...getDefaultProps()} isGroupStory />
));
story.add('In a group', () => { story.add('In a group', () => {
const { views, replies } = getViewsAndReplies(); const { views, replies } = getViewsAndReplies();
return ( return (
<StoryViewsNRepliesModal <StoryViewsNRepliesModal
{...getDefaultProps()} {...getDefaultProps()}
isGroupStory
replies={replies} replies={replies}
views={views} views={views}
/> />

View file

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

View file

@ -7702,7 +7702,7 @@
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/StoryViewsNRepliesModal.tsx", "path": "ts/components/StoryViewsNRepliesModal.tsx",
"line": " const inputApiRef = React.useRef<InputApi | undefined>();", "line": " const inputApiRef = useRef<InputApi | undefined>();",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2022-02-15T17:57:06.507Z" "updated": "2022-02-15T17:57:06.507Z"
}, },