Adjust story replies for direct conversations
This commit is contained in:
parent
fa7b7fcd08
commit
0ca66d6e95
19 changed files with 490 additions and 131 deletions
|
@ -670,7 +670,7 @@ export const StoryViewer = ({
|
|||
/>
|
||||
{hasReplyModal && canReply && (
|
||||
<StoryViewsNRepliesModal
|
||||
authorTitle={title}
|
||||
authorTitle={firstName || title}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
isGroupStory={isGroupStory}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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="🏋️"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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} · {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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue