Fix story reply box interactions
This commit is contained in:
parent
7775f7d806
commit
72f979ea1d
7 changed files with 180 additions and 128 deletions
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue