Use <Message /> for group story replies

This commit is contained in:
Josh Perez 2022-08-04 21:29:44 -04:00 committed by GitHub
parent dca848389c
commit 5dc42122a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 169 additions and 78 deletions

View file

@ -34,7 +34,6 @@
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-15); @include color-svg('../images/icons/v2/x-24.svg', $color-gray-15);
} }
right: 28px; right: 28px;
top: 0;
z-index: $z-index-above-above-base; z-index: $z-index-above-above-base;
} }

View file

@ -13,6 +13,7 @@
&__replies { &__replies {
flex: 1; flex: 1;
margin: 0 -16px;
max-height: 75vh; max-height: 75vh;
overflow-y: overlay; overflow-y: overlay;
@ -127,7 +128,7 @@
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 12px 0; padding: 12px 16px;
&--container { &--container {
display: flex; display: flex;

View file

@ -10,6 +10,7 @@ import enMessages from '../../_locales/en/messages.json';
import { IMAGE_JPEG } from '../types/MIME'; import { IMAGE_JPEG } from '../types/MIME';
import { SendStatus } from '../messages/MessageSendState'; import { SendStatus } from '../messages/MessageSendState';
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal'; import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
import { UUID } from '../types/UUID';
import { fakeAttachment } from '../test-both/helpers/fakeAttachment'; import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
@ -69,6 +70,9 @@ function getViewsAndReplies() {
const p3 = getDefaultConversation(); const p3 = getDefaultConversation();
const p4 = getDefaultConversation(); const p4 = getDefaultConversation();
const p5 = getDefaultConversation(); const p5 = getDefaultConversation();
const p6 = getDefaultConversation({
isMe: true,
});
const views = [ const views = [
{ {
@ -100,20 +104,52 @@ function getViewsAndReplies() {
const replies = [ const replies = [
{ {
...p2, author: p2,
body: 'So cute ❤️', body: 'So cute ❤️',
conversationId: p2.id,
id: UUID.generate().toString(),
timestamp: Date.now() - 24 * durations.MINUTE, timestamp: Date.now() - 24 * durations.MINUTE,
}, },
{ {
...p3, author: p3,
body: "That's awesome", body: "That's awesome",
conversationId: p3.id,
id: UUID.generate().toString(),
timestamp: Date.now() - 13 * durations.MINUTE, timestamp: Date.now() - 13 * durations.MINUTE,
}, },
{ {
...p4, author: p3,
body: 'Very awesome',
conversationId: p3.id,
id: UUID.generate().toString(),
timestamp: Date.now() - 13 * durations.MINUTE,
},
{
author: p3,
body: 'Did I mention how awesome this is?',
conversationId: p3.id,
id: UUID.generate().toString(),
timestamp: Date.now() - 12 * durations.MINUTE,
},
{
author: p4,
conversationId: p4.id,
id: UUID.generate().toString(),
reactionEmoji: '❤️', reactionEmoji: '❤️',
timestamp: Date.now() - 5 * durations.MINUTE, timestamp: Date.now() - 5 * durations.MINUTE,
}, },
{
author: p6,
body: 'Thanks everyone!',
conversationId: p6.id,
id: UUID.generate().toString(),
sendStateByConversationId: {
[p1.id]: {
status: SendStatus.Pending,
},
},
timestamp: Date.now(),
},
]; ];
return { return {

View file

@ -16,7 +16,7 @@ import { CompositionInput } from './CompositionInput';
import { ContactName } from './conversation/ContactName'; import { ContactName } from './conversation/ContactName';
import { EmojiButton } from './emoji/EmojiButton'; import { EmojiButton } from './emoji/EmojiButton';
import { Emojify } from './conversation/Emojify'; import { Emojify } from './conversation/Emojify';
import { MessageBody } from './conversation/MessageBody'; import { Message, TextDirection } from './conversation/Message';
import { MessageTimestamp } from './conversation/MessageTimestamp'; import { MessageTimestamp } from './conversation/MessageTimestamp';
import { Modal } from './Modal'; import { Modal } from './Modal';
import { Quote } from './conversation/Quote'; import { Quote } from './conversation/Quote';
@ -24,8 +24,58 @@ import { ReactionPicker } from './conversation/ReactionPicker';
import { Tabs } from './Tabs'; import { Tabs } from './Tabs';
import { Theme } from '../util/theme'; import { Theme } from '../util/theme';
import { ThemeType } from '../types/Util'; import { ThemeType } from '../types/Util';
import { WidthBreakpoint } from './_util';
import { getAvatarColor } from '../types/Colors'; import { getAvatarColor } from '../types/Colors';
import { getStoryReplyText } from '../util/getStoryReplyText'; 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: () => <div />,
renderEmojiPicker: () => <div />,
renderReactionPicker: () => <div />,
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,
};
enum Tab { enum Tab {
Replies = 'Replies', Replies = 'Replies',
@ -79,6 +129,7 @@ export const StoryViewsNRepliesModal = ({
storyPreviewAttachment, storyPreviewAttachment,
views, views,
}: PropsType): JSX.Element | null => { }: PropsType): JSX.Element | null => {
const containerElementRef = useRef<HTMLDivElement | null>(null);
const inputApiRef = useRef<InputApi | undefined>(); const inputApiRef = useRef<InputApi | undefined>();
const [bottom, setBottom] = useState<HTMLDivElement | null>(null); const [bottom, setBottom] = useState<HTMLDivElement | null>(null);
const [messageBodyText, setMessageBodyText] = useState(''); const [messageBodyText, setMessageBodyText] = useState('');
@ -211,30 +262,34 @@ export const StoryViewsNRepliesModal = ({
if (replies.length) { if (replies.length) {
repliesElement = ( repliesElement = (
<div className="StoryViewsNRepliesModal__replies"> <div
{replies.map(reply => className="StoryViewsNRepliesModal__replies"
ref={containerElementRef}
>
{replies.map((reply, index) =>
reply.reactionEmoji ? ( reply.reactionEmoji ? (
<div className="StoryViewsNRepliesModal__reaction" key={reply.id}> <div className="StoryViewsNRepliesModal__reaction" key={reply.id}>
<div className="StoryViewsNRepliesModal__reaction--container"> <div className="StoryViewsNRepliesModal__reaction--container">
<Avatar <Avatar
acceptedMessageRequest={reply.acceptedMessageRequest} acceptedMessageRequest={reply.author.acceptedMessageRequest}
avatarPath={reply.avatarPath} avatarPath={reply.author.avatarPath}
badge={undefined} badge={getPreferredBadge(reply.author.badges)}
color={getAvatarColor(reply.color)} color={getAvatarColor(reply.author.color)}
conversationType="direct" conversationType="direct"
i18n={i18n} i18n={i18n}
isMe={Boolean(reply.isMe)} isMe={Boolean(reply.author.isMe)}
name={reply.name} name={reply.author.name}
profileName={reply.profileName} profileName={reply.author.profileName}
sharedGroupNames={reply.sharedGroupNames || []} sharedGroupNames={reply.author.sharedGroupNames || []}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
title={reply.title} theme={ThemeType.dark}
title={reply.author.title}
/> />
<div className="StoryViewsNRepliesModal__reaction--body"> <div className="StoryViewsNRepliesModal__reaction--body">
<div className="StoryViewsNRepliesModal__reply--title"> <div className="StoryViewsNRepliesModal__reply--title">
<ContactName <ContactName
contactNameColor={reply.contactNameColor} contactNameColor={reply.contactNameColor}
title={reply.title} title={reply.author.title}
/> />
</div> </div>
{i18n('StoryViewsNRepliesModal__reacted')} {i18n('StoryViewsNRepliesModal__reacted')}
@ -249,54 +304,34 @@ export const StoryViewsNRepliesModal = ({
<Emojify text={reply.reactionEmoji} /> <Emojify text={reply.reactionEmoji} />
</div> </div>
) : ( ) : (
<div className="StoryViewsNRepliesModal__reply" key={reply.id}> <div key={reply.id}>
<Avatar <Message
acceptedMessageRequest={reply.acceptedMessageRequest} {...MESSAGE_DEFAULT_PROPS}
avatarPath={reply.avatarPath} author={reply.author}
badge={undefined} containerElementRef={containerElementRef}
color={getAvatarColor(reply.color)} conversationColor="ultramarine"
conversationType="direct" conversationId={reply.conversationId}
conversationTitle={reply.author.title}
conversationType="group"
direction="incoming"
disableMenu
getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
isMe={Boolean(reply.isMe)} id={reply.id}
name={reply.name} interactionMode="mouse"
profileName={reply.profileName} readStatus={reply.readStatus}
sharedGroupNames={reply.sharedGroupNames || []} renderingContext="StoryViewsNRepliesModal"
size={AvatarSize.TWENTY_EIGHT} shouldCollapseAbove={
title={reply.title} reply.conversationId === replies[index - 1]?.conversationId
}
shouldCollapseBelow={
reply.conversationId === replies[index + 1]?.conversationId
}
shouldHideMetadata={false}
text={reply.body}
textDirection={TextDirection.Default}
timestamp={reply.timestamp}
/> />
<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
disableJumbomoji
i18n={i18n}
text={
reply.deletedForEveryone
? i18n('message--deletedForEveryone')
: String(reply.body)
}
/>
<MessageTimestamp
i18n={i18n}
module="StoryViewsNRepliesModal__reply--timestamp"
timestamp={reply.timestamp}
/>
</div>
</div> </div>
) )
)} )}

View file

@ -263,7 +263,7 @@ export type PropsData = {
isTapToViewExpired?: boolean; isTapToViewExpired?: boolean;
isTapToViewError?: boolean; isTapToViewError?: boolean;
readStatus: ReadStatus; readStatus?: ReadStatus;
expirationLength?: number; expirationLength?: number;
expirationTimestamp?: number; expirationTimestamp?: number;

View file

@ -80,8 +80,10 @@ function getAvatarData(
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarPath'
| 'badges'
| 'color' | 'color'
| 'isMe' | 'isMe'
| 'id'
| 'name' | 'name'
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
@ -90,8 +92,10 @@ function getAvatarData(
return pick(conversation, [ return pick(conversation, [
'acceptedMessageRequest', 'acceptedMessageRequest',
'avatarPath', 'avatarPath',
'badges',
'color', 'color',
'isMe', 'isMe',
'id',
'name', 'name',
'profileName', 'profileName',
'sharedGroupNames', 'sharedGroupNames',
@ -212,11 +216,12 @@ export const getStoryReplies = createSelector(
const conversation = conversationSelector(reaction.fromId); const conversation = conversationSelector(reaction.fromId);
return { return {
...getAvatarData(conversation), author: getAvatarData(conversation),
contactNameColor: contactNameColorSelector( contactNameColor: contactNameColorSelector(
foundStory.conversationId, foundStory.conversationId,
conversation.id conversation.id
), ),
conversationId: reaction.fromId,
id: getReactionUniqueId(reaction), id: getReactionUniqueId(reaction),
reactionEmoji: reaction.emoji, reactionEmoji: reaction.emoji,
timestamp: reaction.timestamp, timestamp: reaction.timestamp,
@ -231,12 +236,14 @@ export const getStoryReplies = createSelector(
: conversationSelector(reply.sourceUuid || reply.source); : conversationSelector(reply.sourceUuid || reply.source);
return { return {
...getAvatarData(conversation), author: getAvatarData(conversation),
...pick(reply, ['body', 'deletedForEveryone', 'id', 'timestamp']), ...pick(reply, ['body', 'deletedForEveryone', 'id', 'timestamp']),
contactNameColor: contactNameColorSelector( contactNameColor: contactNameColorSelector(
reply.conversationId, reply.conversationId,
conversation.id conversation.id
), ),
conversationId: conversation.id,
readStatus: reply.readStatus,
}; };
}); });

View file

@ -5,25 +5,31 @@ import type { AttachmentType } from './Attachment';
import type { ContactNameColorType } from './Colors'; import type { ContactNameColorType } from './Colors';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from './Util'; import type { LocalizerType } from './Util';
import type { ReadStatus } from '../messages/MessageReadStatus';
import type { SendStatus } from '../messages/MessageSendState'; import type { SendStatus } from '../messages/MessageSendState';
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists'; import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
export type ReplyType = Pick< export type ReplyType = {
ConversationType, author: Pick<
| 'acceptedMessageRequest' ConversationType,
| 'avatarPath' | 'acceptedMessageRequest'
| 'color' | 'avatarPath'
| 'isMe' | 'badges'
| 'name' | 'color'
| 'profileName' | 'id'
| 'sharedGroupNames' | 'isMe'
| 'title' | 'name'
> & { | 'profileName'
| 'sharedGroupNames'
| 'title'
>;
body?: string; body?: string;
contactNameColor?: ContactNameColorType; contactNameColor?: ContactNameColorType;
conversationId: string;
deletedForEveryone?: boolean; deletedForEveryone?: boolean;
id: string; id: string;
reactionEmoji?: string; reactionEmoji?: string;
readStatus?: ReadStatus;
timestamp: number; timestamp: number;
}; };

View file

@ -9231,6 +9231,13 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2022-02-15T17:57:06.507Z" "updated": "2022-02-15T17:57:06.507Z"
}, },
{
"rule": "React-useRef",
"path": "ts/components/StoryViewsNRepliesModal.tsx",
"line": " const containerElementRef = useRef<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-08-04T00:52:01.080Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/TextAttachment.tsx", "path": "ts/components/TextAttachment.tsx",