Adjust story replies for direct conversations

This commit is contained in:
Josh Perez 2022-05-10 15:02:21 -04:00 committed by GitHub
parent fa7b7fcd08
commit 0ca66d6e95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 490 additions and 131 deletions

View file

@ -670,7 +670,7 @@ export const StoryViewer = ({
/>
{hasReplyModal && canReply && (
<StoryViewsNRepliesModal
authorTitle={title}
authorTitle={firstName || title}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
isGroupStory={isGroupStory}

View file

@ -27,6 +27,7 @@ import { Tabs } from './Tabs';
import { Theme } from '../util/theme';
import { ThemeType } from '../types/Util';
import { getAvatarColor } from '../types/Colors';
import { getStoryReplyText } from '../util/getStoryReplyText';
type ViewType = Pick<
ConversationType,
@ -135,88 +136,88 @@ export const StoryViewsNRepliesModal = ({
if (!isMyStory) {
composerElement = (
<div className="StoryViewsNRepliesModal__compose-container">
<div className="StoryViewsNRepliesModal__composer">
{!isGroupStory && (
<Quote
authorTitle={authorTitle}
conversationColor="ultramarine"
i18n={i18n}
isFromMe={false}
isViewOnce={false}
moduleClassName="StoryViewsNRepliesModal__quote"
rawAttachment={storyPreviewAttachment}
referencedMessageNotFound={false}
text={i18n('message--getNotificationText--text-with-emoji', {
text: i18n('message--getNotificationText--photo'),
emoji: '📷',
})}
/>
)}
<CompositionInput
draftText={messageBodyText}
getPreferredBadge={getPreferredBadge}
<>
{!isGroupStory && (
<Quote
authorTitle={authorTitle}
conversationColor="ultramarine"
i18n={i18n}
inputApi={inputApiRef}
moduleClassName="StoryViewsNRepliesModal__input"
onEditorStateChange={messageText => {
setMessageBodyText(messageText);
}}
onPickEmoji={insertEmoji}
onSubmit={(...args) => {
inputApiRef.current?.reset();
onReply(...args);
}}
onTextTooLong={onTextTooLong}
placeholder={
isGroupStory
? i18n('StoryViewer__reply-group')
: i18n('StoryViewer__reply')
}
theme={ThemeType.dark}
>
<EmojiButton
className="StoryViewsNRepliesModal__emoji-button"
i18n={i18n}
onPickEmoji={insertEmoji}
onClose={focusComposer}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
/>
</CompositionInput>
</div>
<button
aria-label={i18n('StoryViewsNRepliesModal__react')}
className="StoryViewsNRepliesModal__react"
onClick={() => {
setShowReactionPicker(!showReactionPicker);
}}
ref={setReferenceElement}
type="button"
/>
{showReactionPicker && (
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<ReactionPicker
i18n={i18n}
onClose={() => {
setShowReactionPicker(false);
}}
onPick={emoji => {
setShowReactionPicker(false);
onReact(emoji);
}}
onSetSkinTone={onSetSkinTone}
preferredReactionEmoji={preferredReactionEmoji}
renderEmojiPicker={renderEmojiPicker}
/>
</div>
isFromMe={false}
isStoryReply
isViewOnce={false}
moduleClassName="StoryViewsNRepliesModal__quote"
rawAttachment={storyPreviewAttachment}
referencedMessageNotFound={false}
text={getStoryReplyText(i18n, storyPreviewAttachment)}
/>
)}
</div>
<div className="StoryViewsNRepliesModal__compose-container">
<div className="StoryViewsNRepliesModal__composer">
<CompositionInput
draftText={messageBodyText}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
inputApi={inputApiRef}
moduleClassName="StoryViewsNRepliesModal__input"
onEditorStateChange={messageText => {
setMessageBodyText(messageText);
}}
onPickEmoji={insertEmoji}
onSubmit={(...args) => {
inputApiRef.current?.reset();
onReply(...args);
}}
onTextTooLong={onTextTooLong}
placeholder={
isGroupStory
? i18n('StoryViewer__reply-group')
: i18n('StoryViewer__reply')
}
theme={ThemeType.dark}
>
<EmojiButton
className="StoryViewsNRepliesModal__emoji-button"
i18n={i18n}
onPickEmoji={insertEmoji}
onClose={focusComposer}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
/>
</CompositionInput>
</div>
<button
aria-label={i18n('StoryViewsNRepliesModal__react')}
className="StoryViewsNRepliesModal__react"
onClick={() => {
setShowReactionPicker(!showReactionPicker);
}}
ref={setReferenceElement}
type="button"
/>
{showReactionPicker && (
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<ReactionPicker
i18n={i18n}
onClose={() => {
setShowReactionPicker(false);
}}
onPick={emoji => {
setShowReactionPicker(false);
onReact(emoji);
}}
onSetSkinTone={onSetSkinTone}
preferredReactionEmoji={preferredReactionEmoji}
renderEmojiPicker={renderEmojiPicker}
/>
</div>
)}
</div>
</>
);
}

View file

@ -1540,15 +1540,53 @@ story.add('Story reply', () => {
const conversation = getDefaultConversation();
return renderThree({
...createProps({ text: 'Wow!' }),
...createProps({ direction: 'outgoing', text: 'Wow!' }),
storyReplyContext: {
authorTitle: conversation.title,
authorTitle: conversation.firstName || conversation.title,
conversationColor: ConversationColors[0],
isFromMe: false,
rawAttachment: fakeAttachment({
url: '/fixtures/snow.jpg',
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
}),
text: 'Photo',
},
});
});
story.add('Story reply (yours)', () => {
const conversation = getDefaultConversation();
return renderThree({
...createProps({ direction: 'incoming', text: 'Wow!' }),
storyReplyContext: {
authorTitle: conversation.firstName || conversation.title,
conversationColor: ConversationColors[0],
isFromMe: true,
rawAttachment: fakeAttachment({
url: '/fixtures/snow.jpg',
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
}),
text: 'Photo',
},
});
});
story.add('Story reply (emoji)', () => {
const conversation = getDefaultConversation();
return renderThree({
...createProps({ direction: 'outgoing', text: 'Wow!' }),
storyReplyContext: {
authorTitle: conversation.firstName || conversation.title,
conversationColor: ConversationColors[0],
emoji: '💄',
isFromMe: false,
rawAttachment: fakeAttachment({
url: '/fixtures/snow.jpg',
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
}),
text: 'Photo',
},
});
});

View file

@ -227,8 +227,11 @@ export type PropsData = {
authorTitle: string;
conversationColor: ConversationColorType;
customColor?: CustomColorType;
emoji?: string;
isFromMe: boolean;
rawAttachment?: QuotedAttachmentType;
referencedMessageNotFound?: boolean;
text: string;
};
previews: Array<LinkPreviewType>;
@ -1299,27 +1302,35 @@ export class Message extends React.PureComponent<Props, State> {
}
return (
<Quote
authorTitle={storyReplyContext.authorTitle}
conversationColor={conversationColor}
curveTopLeft={curveTopLeft}
curveTopRight={curveTopRight}
customColor={customColor}
i18n={i18n}
isFromMe={storyReplyContext.isFromMe}
isIncoming={isIncoming}
isViewOnce={false}
moduleClassName="StoryReplyQuote"
onClick={() => {
// TODO DESKTOP-3255
}}
rawAttachment={storyReplyContext.rawAttachment}
referencedMessageNotFound={false}
text={i18n('message--getNotificationText--text-with-emoji', {
text: i18n('message--getNotificationText--photo'),
emoji: '📷',
})}
/>
<>
{storyReplyContext.emoji && (
<div className="module-message__quote-story-reaction-header">
{i18n('Quote__story-reaction', [storyReplyContext.authorTitle])}
</div>
)}
<Quote
authorTitle={storyReplyContext.authorTitle}
conversationColor={conversationColor}
curveTopLeft={curveTopLeft}
curveTopRight={curveTopRight}
customColor={customColor}
i18n={i18n}
isFromMe={storyReplyContext.isFromMe}
isIncoming={isIncoming}
isStoryReply
isViewOnce={false}
moduleClassName="StoryReplyQuote"
onClick={() => {
// TODO DESKTOP-3255
}}
rawAttachment={storyReplyContext.rawAttachment}
reactionEmoji={storyReplyContext.emoji}
referencedMessageNotFound={Boolean(
storyReplyContext.referencedMessageNotFound
)}
text={storyReplyContext.text}
/>
</>
);
}

View file

@ -521,3 +521,51 @@ story.add('Custom Color', () => (
/>
</>
));
story.add('isStoryReply', () => {
const props = createProps({
text: 'Wow!',
});
return (
<Quote
{...props}
authorTitle="Amanda"
isStoryReply
moduleClassName="StoryReplyQuote"
onClose={undefined}
rawAttachment={{
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
}}
/>
);
});
story.add('isStoryReply emoji', () => {
const props = createProps();
return (
<Quote
{...props}
authorTitle="Charlie"
isStoryReply
moduleClassName="StoryReplyQuote"
onClose={undefined}
rawAttachment={{
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: pngUrl,
objectUrl: pngUrl,
},
}}
reactionEmoji="🏋️"
/>
);
});

View file

@ -17,6 +17,8 @@ import type {
CustomColorType,
} from '../../types/Colors';
import { ContactName } from './ContactName';
import { Emojify } from './Emojify';
import { TextAttachment } from '../TextAttachment';
import { getTextWithMentions } from '../../util/getTextWithMentions';
import { getClassNamesFor } from '../../util/getClassNamesFor';
import { getCustomColorStyle } from '../../util/getCustomColorStyle';
@ -31,12 +33,14 @@ export type Props = {
i18n: LocalizerType;
isFromMe: boolean;
isIncoming?: boolean;
isStoryReply?: boolean;
moduleClassName?: string;
onClick?: () => void;
onClose?: () => void;
text: string;
rawAttachment?: QuotedAttachmentType;
isViewOnce: boolean;
reactionEmoji?: string;
referencedMessageNotFound: boolean;
doubleCheckMissingQuoteReference?: () => unknown;
};
@ -51,6 +55,13 @@ export type QuotedAttachmentType = Pick<
>;
function validateQuote(quote: Props): boolean {
if (
quote.isStoryReply &&
(quote.referencedMessageNotFound || quote.reactionEmoji)
) {
return true;
}
if (quote.text) {
return true;
}
@ -250,7 +261,7 @@ export class Quote extends React.Component<Props, State> {
}
public renderIconContainer(): JSX.Element | null {
const { rawAttachment, isViewOnce } = this.props;
const { rawAttachment, isViewOnce, i18n } = this.props;
const { imageBroken } = this.state;
const attachment = getAttachment(rawAttachment);
@ -265,9 +276,16 @@ export class Quote extends React.Component<Props, State> {
return this.renderIcon('view-once');
}
// TODO DESKTOP-3433
if (textAttachment) {
return this.renderIcon('image');
return (
<div className={this.getClassName('__icon-container')}>
<TextAttachment
i18n={i18n}
isThumbnail
textAttachment={textAttachment}
/>
</div>
);
}
if (GoogleChrome.isVideoTypeSupported(contentType)) {
@ -385,7 +403,17 @@ export class Quote extends React.Component<Props, State> {
}
public renderAuthor(): JSX.Element {
const { authorTitle, i18n, isFromMe, isIncoming } = this.props;
const { authorTitle, i18n, isFromMe, isIncoming, isStoryReply } =
this.props;
const title = isFromMe ? i18n('you') : <ContactName title={authorTitle} />;
const author = isStoryReply ? (
<>
{title} &middot; {i18n('Quote__story')}
</>
) : (
title
);
return (
<div
@ -394,7 +422,7 @@ export class Quote extends React.Component<Props, State> {
isIncoming ? this.getClassName('__primary__author--incoming') : null
)}
>
{isFromMe ? i18n('you') : <ContactName title={authorTitle} />}
{author}
</div>
);
}
@ -405,10 +433,11 @@ export class Quote extends React.Component<Props, State> {
customColor,
i18n,
isIncoming,
isStoryReply,
referencedMessageNotFound,
} = this.props;
if (!referencedMessageNotFound) {
if (!referencedMessageNotFound || isStoryReply) {
return null;
}
@ -452,6 +481,8 @@ export class Quote extends React.Component<Props, State> {
customColor,
isIncoming,
onClick,
rawAttachment,
reactionEmoji,
referencedMessageNotFound,
} = this.props;
@ -486,6 +517,17 @@ export class Quote extends React.Component<Props, State> {
{this.renderGenericFile()}
{this.renderText()}
</div>
{reactionEmoji && (
<div
className={
rawAttachment
? this.getClassName('__reaction-emoji')
: this.getClassName('__reaction-emoji--story-unavailable')
}
>
<Emojify text={reactionEmoji} />
</div>
)}
{this.renderIconContainer()}
{this.renderClose()}
</button>