Support for incoming gift badges
This commit is contained in:
parent
6b4bea6330
commit
0ba6a0926e
41 changed files with 1476 additions and 164 deletions
|
@ -191,6 +191,7 @@ story.add('Quote', () => (
|
|||
quotedMessageProps: {
|
||||
text: 'something',
|
||||
conversationColor: ConversationColors[10],
|
||||
isGiftBadge: false,
|
||||
isViewOnce: false,
|
||||
referencedMessageNotFound: false,
|
||||
authorTitle: 'Someone',
|
||||
|
|
57
ts/components/OutgoingGiftBadgeModal.stories.tsx
Normal file
57
ts/components/OutgoingGiftBadgeModal.stories.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import type { PropsType } from './OutgoingGiftBadgeModal';
|
||||
import { OutgoingGiftBadgeModal } from './OutgoingGiftBadgeModal';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { BadgeCategory } from '../badges/BadgeCategory';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const getPreferredBadge = () => ({
|
||||
category: BadgeCategory.Donor,
|
||||
descriptionTemplate: 'This is a description of the badge',
|
||||
id: 'BOOST-3',
|
||||
images: [
|
||||
{
|
||||
transparent: {
|
||||
localPath: '/fixtures/orange-heart.svg',
|
||||
url: 'http://someplace',
|
||||
},
|
||||
},
|
||||
],
|
||||
name: 'heart',
|
||||
});
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
recipientTitle: text(
|
||||
'recipientTitle',
|
||||
overrideProps.recipientTitle || 'Default Name'
|
||||
),
|
||||
badgeId: text('badgeId', overrideProps.badgeId || 'heart'),
|
||||
getPreferredBadge,
|
||||
hideOutgoingGiftBadgeModal: action('hideOutgoingGiftBadgeModal'),
|
||||
i18n,
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/OutgoingGiftBadgeModal', module);
|
||||
|
||||
story.add('Normal', () => {
|
||||
return <OutgoingGiftBadgeModal {...createProps()} />;
|
||||
});
|
||||
|
||||
story.add('Missing badge', () => {
|
||||
const props = {
|
||||
...createProps(),
|
||||
getPreferredBadge: () => undefined,
|
||||
};
|
||||
|
||||
return <OutgoingGiftBadgeModal {...props} />;
|
||||
});
|
77
ts/components/OutgoingGiftBadgeModal.tsx
Normal file
77
ts/components/OutgoingGiftBadgeModal.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath';
|
||||
import { Modal } from './Modal';
|
||||
import { BadgeImageTheme } from '../badges/BadgeImageTheme';
|
||||
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
const CLASS_NAME = 'OutgoingGiftBadgeModal';
|
||||
|
||||
export type PropsType = {
|
||||
recipientTitle: string;
|
||||
i18n: LocalizerType;
|
||||
badgeId: string;
|
||||
hideOutgoingGiftBadgeModal: () => unknown;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
};
|
||||
|
||||
export const OutgoingGiftBadgeModal = ({
|
||||
recipientTitle,
|
||||
i18n,
|
||||
badgeId,
|
||||
hideOutgoingGiftBadgeModal,
|
||||
getPreferredBadge,
|
||||
}: PropsType): JSX.Element => {
|
||||
const badge = getPreferredBadge([{ id: badgeId }]);
|
||||
const badgeSize = 140;
|
||||
const badgeImagePath = getBadgeImageFileLocalPath(
|
||||
badge,
|
||||
badgeSize,
|
||||
BadgeImageTheme.Transparent
|
||||
);
|
||||
|
||||
const badgeElement = badge ? (
|
||||
<img
|
||||
className={`${CLASS_NAME}__badge`}
|
||||
src={badgeImagePath}
|
||||
alt={badge.name}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
`${CLASS_NAME}__badge`,
|
||||
`${CLASS_NAME}__badge--missing`
|
||||
)}
|
||||
aria-label={i18n('giftBadge--missing')}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
moduleClassName={`${CLASS_NAME}__container`}
|
||||
onClose={hideOutgoingGiftBadgeModal}
|
||||
hasXButton
|
||||
useFocusTrap
|
||||
>
|
||||
<div className={CLASS_NAME}>
|
||||
<div className={`${CLASS_NAME}__title`}>
|
||||
{i18n('modal--giftBadge--title')}
|
||||
</div>
|
||||
<div className={`${CLASS_NAME}__description`}>
|
||||
{i18n('modal--giftBadge--description', { name: recipientTitle })}
|
||||
</div>
|
||||
{badgeElement}
|
||||
<div className={`${CLASS_NAME}__badge-summary`}>
|
||||
{i18n('message--giftBadge')}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -143,6 +143,7 @@ export const StoryViewsNRepliesModal = ({
|
|||
conversationColor="ultramarine"
|
||||
i18n={i18n}
|
||||
isFromMe={false}
|
||||
isGiftBadge={false}
|
||||
isStoryReply
|
||||
isViewOnce={false}
|
||||
moduleClassName="StoryViewsNRepliesModal__quote"
|
||||
|
|
24
ts/components/ToastCannotOpenGiftBadge.tsx
Normal file
24
ts/components/ToastCannotOpenGiftBadge.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type ToastPropsType = {
|
||||
i18n: LocalizerType;
|
||||
isIncoming: boolean;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export const ToastCannotOpenGiftBadge = ({
|
||||
i18n,
|
||||
isIncoming,
|
||||
onClose,
|
||||
}: ToastPropsType): JSX.Element => {
|
||||
const key = `message--giftBadge--unopened--toast--${
|
||||
isIncoming ? 'incoming' : 'outgoing'
|
||||
}`;
|
||||
|
||||
return <Toast onClose={onClose}>{i18n(key)}</Toast>;
|
||||
};
|
|
@ -12,7 +12,7 @@ import { SignalService } from '../../protobuf';
|
|||
import { ConversationColors } from '../../types/Colors';
|
||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||
import type { Props, AudioAttachmentProps } from './Message';
|
||||
import { TextDirection, Message } from './Message';
|
||||
import { GiftBadgeStates, Message, TextDirection } from './Message';
|
||||
import {
|
||||
AUDIO_MP3,
|
||||
IMAGE_JPEG,
|
||||
|
@ -30,7 +30,7 @@ import enMessages from '../../../_locales/en/messages.json';
|
|||
import { pngUrl } from '../../storybook/Fixtures';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
import { DAY, HOUR, MINUTE, SECOND } from '../../util/durations';
|
||||
import { ContactFormType } from '../../types/EmbeddedContact';
|
||||
|
||||
import {
|
||||
|
@ -40,6 +40,7 @@ import {
|
|||
import { getFakeBadge } from '../../test-both/helpers/getFakeBadge';
|
||||
import { ThemeType } from '../../types/Util';
|
||||
import { UUID } from '../../types/UUID';
|
||||
import { BadgeCategory } from '../../badges/BadgeCategory';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -119,6 +120,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
conversationColor:
|
||||
overrideProps.conversationColor ||
|
||||
select('conversationColor', ConversationColors, ConversationColors[0]),
|
||||
conversationTitle:
|
||||
overrideProps.conversationTitle ||
|
||||
text('conversationTitle', 'Conversation Title'),
|
||||
conversationId: text('conversationId', overrideProps.conversationId || ''),
|
||||
conversationType: overrideProps.conversationType || 'direct',
|
||||
contact: overrideProps.contact,
|
||||
|
@ -138,8 +142,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
number('expirationTimestamp', overrideProps.expirationTimestamp || 0) ||
|
||||
undefined,
|
||||
getPreferredBadge: overrideProps.getPreferredBadge || (() => undefined),
|
||||
giftBadge: overrideProps.giftBadge,
|
||||
i18n,
|
||||
id: text('id', overrideProps.id || ''),
|
||||
id: text('id', overrideProps.id || 'random-message-id'),
|
||||
renderingContext: 'storybook',
|
||||
interactionMode: overrideProps.interactionMode || 'keyboard',
|
||||
isSticker: isBoolean(overrideProps.isSticker)
|
||||
|
@ -159,6 +164,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
markViewed: action('markViewed'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
openConversation: action('openConversation'),
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
openLink: action('openLink'),
|
||||
previews: overrideProps.previews || [],
|
||||
reactions: overrideProps.reactions,
|
||||
|
@ -1218,6 +1224,7 @@ story.add('Other File Type', () => {
|
|||
contentType: stringToMIMEType('text/plain'),
|
||||
fileName: 'my-resume.txt',
|
||||
url: 'my-resume.txt',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1233,6 +1240,7 @@ story.add('Other File Type with Caption', () => {
|
|||
contentType: stringToMIMEType('text/plain'),
|
||||
fileName: 'my-resume.txt',
|
||||
url: 'my-resume.txt',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1250,6 +1258,7 @@ story.add('Other File Type with Long Filename', () => {
|
|||
fileName:
|
||||
'INSERT-APP-NAME_INSERT-APP-APPLE-ID_AppStore_AppsGamesWatch.psd.zip',
|
||||
url: 'a2/a2334324darewer4234',
|
||||
fileSize: '10MB',
|
||||
}),
|
||||
],
|
||||
status: 'sent',
|
||||
|
@ -1714,3 +1723,101 @@ story.add('EmbeddedContact: Loading Avatar', () => {
|
|||
});
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
||||
story.add('Gift Badge: Unopened', () => {
|
||||
const props = createProps({
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Unopened,
|
||||
expiration: Date.now() + DAY * 30,
|
||||
level: 3,
|
||||
},
|
||||
});
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
||||
const getPreferredBadge = () => ({
|
||||
category: BadgeCategory.Donor,
|
||||
descriptionTemplate: 'This is a description of the badge',
|
||||
id: 'BOOST-3',
|
||||
images: [
|
||||
{
|
||||
transparent: {
|
||||
localPath: '/fixtures/orange-heart.svg',
|
||||
url: 'http://someplace',
|
||||
},
|
||||
},
|
||||
],
|
||||
name: 'heart',
|
||||
});
|
||||
|
||||
story.add('Gift Badge: Redeemed (30 days)', () => {
|
||||
const props = createProps({
|
||||
getPreferredBadge,
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Redeemed,
|
||||
expiration: Date.now() + DAY * 30 + SECOND,
|
||||
level: 3,
|
||||
},
|
||||
});
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
||||
story.add('Gift Badge: Redeemed (24 hours)', () => {
|
||||
const props = createProps({
|
||||
getPreferredBadge,
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Redeemed,
|
||||
expiration: Date.now() + DAY + SECOND,
|
||||
level: 3,
|
||||
},
|
||||
});
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
||||
story.add('Gift Badge: Redeemed (60 minutes)', () => {
|
||||
const props = createProps({
|
||||
getPreferredBadge,
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Redeemed,
|
||||
expiration: Date.now() + HOUR + SECOND,
|
||||
level: 3,
|
||||
},
|
||||
});
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
||||
story.add('Gift Badge: Redeemed (1 minute)', () => {
|
||||
const props = createProps({
|
||||
getPreferredBadge,
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Redeemed,
|
||||
expiration: Date.now() + MINUTE + SECOND,
|
||||
level: 3,
|
||||
},
|
||||
});
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
||||
story.add('Gift Badge: Redeemed (expired)', () => {
|
||||
const props = createProps({
|
||||
getPreferredBadge,
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Redeemed,
|
||||
expiration: Date.now(),
|
||||
level: 3,
|
||||
},
|
||||
});
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
||||
story.add('Gift Badge: Missing Badge', () => {
|
||||
const props = createProps({
|
||||
getPreferredBadge: () => undefined,
|
||||
giftBadge: {
|
||||
state: GiftBadgeStates.Redeemed,
|
||||
expiration: Date.now() + MINUTE + SECOND,
|
||||
level: 3,
|
||||
},
|
||||
});
|
||||
return renderBothDirections(props);
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { ReactNode, RefObject } from 'react';
|
|||
import React from 'react';
|
||||
import ReactDOM, { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import getDirection from 'direction';
|
||||
import { drop, groupBy, orderBy, take, unescape } from 'lodash';
|
||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
|
@ -41,6 +42,7 @@ import { LinkPreviewDate } from './LinkPreviewDate';
|
|||
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||
import { shouldUseFullSizeLinkPreviewImage } from '../../linkPreviews/shouldUseFullSizeLinkPreviewImage';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
import { OutgoingGiftBadgeModal } from '../OutgoingGiftBadgeModal';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
|
@ -69,6 +71,7 @@ import type {
|
|||
LocalizerType,
|
||||
ThemeType,
|
||||
} from '../../types/Util';
|
||||
|
||||
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
|
||||
import type {
|
||||
ContactNameColorType,
|
||||
|
@ -84,6 +87,9 @@ import { offsetDistanceModifier } from '../../util/popperUtil';
|
|||
import * as KeyboardLayout from '../../services/keyboardLayout';
|
||||
import { StopPropagation } from '../StopPropagation';
|
||||
import type { UUIDStringType } from '../../types/UUID';
|
||||
import { DAY, HOUR, MINUTE, SECOND } from '../../util/durations';
|
||||
import { BadgeImageTheme } from '../../badges/BadgeImageTheme';
|
||||
import { getBadgeImageFileLocalPath } from '../../badges/getBadgeImageFileLocalPath';
|
||||
|
||||
type Trigger = {
|
||||
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
|
@ -116,6 +122,7 @@ const SENT_STATUSES = new Set<MessageStatusType>([
|
|||
'sent',
|
||||
'viewed',
|
||||
]);
|
||||
const GIFT_BADGE_UPDATE_INTERVAL = 30 * SECOND;
|
||||
|
||||
enum MetadataPlacement {
|
||||
NotRendered,
|
||||
|
@ -171,11 +178,22 @@ export type AudioAttachmentProps = {
|
|||
onFirstPlayed(): void;
|
||||
};
|
||||
|
||||
export enum GiftBadgeStates {
|
||||
Unopened = 'Unopened',
|
||||
Redeemed = 'Redeemed',
|
||||
}
|
||||
export type GiftBadgeType = {
|
||||
level: number;
|
||||
expiration: number;
|
||||
state: GiftBadgeStates.Redeemed | GiftBadgeStates.Unopened;
|
||||
};
|
||||
|
||||
export type PropsData = {
|
||||
id: string;
|
||||
renderingContext: string;
|
||||
contactNameColor?: ContactNameColorType;
|
||||
conversationColor: ConversationColorType;
|
||||
conversationTitle: string;
|
||||
customColor?: CustomColorType;
|
||||
conversationId: string;
|
||||
displayLimit?: number;
|
||||
|
@ -207,6 +225,7 @@ export type PropsData = {
|
|||
reducedMotion?: boolean;
|
||||
conversationType: ConversationTypeType;
|
||||
attachments?: Array<AttachmentType>;
|
||||
giftBadge?: GiftBadgeType;
|
||||
quote?: {
|
||||
conversationColor: ConversationColorType;
|
||||
customColor?: CustomColorType;
|
||||
|
@ -222,6 +241,7 @@ export type PropsData = {
|
|||
bodyRanges?: BodyRangesType;
|
||||
referencedMessageNotFound: boolean;
|
||||
isViewOnce: boolean;
|
||||
isGiftBadge: boolean;
|
||||
};
|
||||
storyReplyContext?: {
|
||||
authorTitle: string;
|
||||
|
@ -299,6 +319,7 @@ export type PropsActions = {
|
|||
|
||||
startConversation: (e164: string, uuid: UUIDStringType) => void;
|
||||
openConversation: (conversationId: string, messageId?: string) => void;
|
||||
openGiftBadge: (messageId: string) => void;
|
||||
showContactDetail: (options: {
|
||||
contact: EmbeddedContactType;
|
||||
signalAccount?: {
|
||||
|
@ -357,6 +378,9 @@ type State = {
|
|||
reactionViewerRoot: HTMLDivElement | null;
|
||||
reactionPickerRoot: HTMLDivElement | null;
|
||||
|
||||
giftBadgeCounter: number | null;
|
||||
showOutgoingGiftBadgeModal: boolean;
|
||||
|
||||
hasDeleteForEveryoneTimerExpired: boolean;
|
||||
};
|
||||
|
||||
|
@ -374,6 +398,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
public expirationCheckInterval: NodeJS.Timeout | undefined;
|
||||
|
||||
public giftBadgeInterval: NodeJS.Timeout | undefined;
|
||||
|
||||
public expiredTimeout: NodeJS.Timeout | undefined;
|
||||
|
||||
public selectedTimeout: NodeJS.Timeout | undefined;
|
||||
|
@ -396,6 +422,9 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
reactionViewerRoot: null,
|
||||
reactionPickerRoot: null,
|
||||
|
||||
giftBadgeCounter: null,
|
||||
showOutgoingGiftBadgeModal: false,
|
||||
|
||||
hasDeleteForEveryoneTimerExpired:
|
||||
this.getTimeRemainingForDeleteForEveryone() <= 0,
|
||||
};
|
||||
|
@ -490,6 +519,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
this.startSelectedTimer();
|
||||
this.startDeleteForEveryoneTimerIfApplicable();
|
||||
this.startGiftBadgeInterval();
|
||||
|
||||
const { isSelected } = this.props;
|
||||
if (isSelected) {
|
||||
|
@ -519,6 +549,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
clearTimeoutIfNecessary(this.expirationCheckInterval);
|
||||
clearTimeoutIfNecessary(this.expiredTimeout);
|
||||
clearTimeoutIfNecessary(this.deleteForEveryoneTimeout);
|
||||
clearTimeoutIfNecessary(this.giftBadgeInterval);
|
||||
this.toggleReactionViewer(true);
|
||||
this.toggleReactionPicker(true);
|
||||
}
|
||||
|
@ -559,6 +590,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
deletedForEveryone,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
giftBadge,
|
||||
i18n,
|
||||
shouldHideMetadata,
|
||||
status,
|
||||
text,
|
||||
|
@ -576,6 +609,17 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return MetadataPlacement.NotRendered;
|
||||
}
|
||||
|
||||
if (giftBadge) {
|
||||
const description = i18n('message--giftBadge--unopened');
|
||||
const isDescriptionRTL = getDirection(description) === 'rtl';
|
||||
|
||||
if (giftBadge.state === GiftBadgeStates.Unopened && !isDescriptionRTL) {
|
||||
return MetadataPlacement.InlineWithText;
|
||||
}
|
||||
|
||||
return MetadataPlacement.Bottom;
|
||||
}
|
||||
|
||||
if (!text && !deletedForEveryone) {
|
||||
return isAudio(attachments)
|
||||
? MetadataPlacement.RenderedByMessageAudioComponent
|
||||
|
@ -635,6 +679,24 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
public startGiftBadgeInterval(): void {
|
||||
const { giftBadge } = this.props;
|
||||
|
||||
if (!giftBadge) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.giftBadgeInterval = setInterval(() => {
|
||||
this.updateGiftBadgeCounter();
|
||||
}, GIFT_BADGE_UPDATE_INTERVAL);
|
||||
}
|
||||
|
||||
public updateGiftBadgeCounter(): void {
|
||||
this.setState((state: State) => ({
|
||||
giftBadgeCounter: (state.giftBadgeCounter || 0) + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
private getTimeRemainingForDeleteForEveryone(): number {
|
||||
const { timestamp } = this.props;
|
||||
return Math.max(timestamp - Date.now() + THREE_HOURS, 0);
|
||||
|
@ -1054,17 +1116,17 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
public renderPreview(): JSX.Element | null {
|
||||
const {
|
||||
id,
|
||||
attachments,
|
||||
conversationType,
|
||||
direction,
|
||||
i18n,
|
||||
id,
|
||||
kickOffAttachmentDownload,
|
||||
openLink,
|
||||
previews,
|
||||
quote,
|
||||
shouldCollapseAbove,
|
||||
theme,
|
||||
kickOffAttachmentDownload,
|
||||
} = this.props;
|
||||
|
||||
// Attachments take precedence over Link Previews
|
||||
|
@ -1205,6 +1267,188 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderGiftBadge(): JSX.Element | null {
|
||||
const { conversationTitle, direction, getPreferredBadge, giftBadge, i18n } =
|
||||
this.props;
|
||||
const { showOutgoingGiftBadgeModal } = this.state;
|
||||
if (!giftBadge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (giftBadge.state === GiftBadgeStates.Unopened) {
|
||||
const description = i18n('message--giftBadge--unopened');
|
||||
const isRTL = getDirection(description) === 'rtl';
|
||||
const { metadataWidth } = this.state;
|
||||
|
||||
return (
|
||||
<div className="module-message__unopened-gift-badge__container">
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__unopened-gift-badge',
|
||||
`module-message__unopened-gift-badge--${direction}`
|
||||
)}
|
||||
aria-label={i18n('message--giftBadge--unopened--label')}
|
||||
>
|
||||
<div
|
||||
className="module-message__unopened-gift-badge__ribbon-horizontal"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="module-message__unopened-gift-badge__ribbon-vertical"
|
||||
aria-hidden
|
||||
/>
|
||||
<img
|
||||
className="module-message__unopened-gift-badge__bow"
|
||||
src="images/gift-bow.svg"
|
||||
alt=""
|
||||
aria-hidden
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__unopened-gift-badge__text',
|
||||
`module-message__unopened-gift-badge__text--${direction}`
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__text',
|
||||
`module-message__text--${direction}`
|
||||
)}
|
||||
dir={isRTL ? 'rtl' : undefined}
|
||||
>
|
||||
{description}
|
||||
{this.getMetadataPlacement() ===
|
||||
MetadataPlacement.InlineWithText && (
|
||||
<MessageTextMetadataSpacer metadataWidth={metadataWidth} />
|
||||
)}
|
||||
</div>
|
||||
{this.renderMetadata()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (giftBadge.state === GiftBadgeStates.Redeemed) {
|
||||
const badgeId = `BOOST-${giftBadge.level}`;
|
||||
const badgeSize = 64;
|
||||
const badge = getPreferredBadge([{ id: badgeId }]);
|
||||
const badgeImagePath = getBadgeImageFileLocalPath(
|
||||
badge,
|
||||
badgeSize,
|
||||
BadgeImageTheme.Transparent
|
||||
);
|
||||
|
||||
let remaining: string;
|
||||
const duration = giftBadge.expiration - Date.now();
|
||||
|
||||
const remainingDays = Math.floor(duration / DAY);
|
||||
const remainingHours = Math.floor(duration / HOUR);
|
||||
const remainingMinutes = Math.floor(duration / MINUTE);
|
||||
|
||||
if (remainingDays > 1) {
|
||||
remaining = i18n('message--giftBadge--remaining--days', {
|
||||
days: remainingDays,
|
||||
});
|
||||
} else if (remainingHours > 1) {
|
||||
remaining = i18n('message--giftBadge--remaining--hours', {
|
||||
hours: remainingHours,
|
||||
});
|
||||
} else if (remainingMinutes > 1) {
|
||||
remaining = i18n('message--giftBadge--remaining--minutes', {
|
||||
minutes: remainingMinutes,
|
||||
});
|
||||
} else if (remainingMinutes === 1) {
|
||||
remaining = i18n('message--giftBadge--remaining--one-minute');
|
||||
} else {
|
||||
remaining = i18n('message--giftBadge--expired');
|
||||
}
|
||||
|
||||
const wasSent = direction === 'outgoing';
|
||||
const buttonContents = wasSent ? (
|
||||
i18n('message--giftBadge--view')
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
'module-message__redeemed-gift-badge__icon-check',
|
||||
`module-message__redeemed-gift-badge__icon-check--${direction}`
|
||||
)}
|
||||
/>{' '}
|
||||
{i18n('message--giftBadge--redeemed')}
|
||||
</>
|
||||
);
|
||||
|
||||
const badgeElement = badge ? (
|
||||
<img
|
||||
className="module-message__redeemed-gift-badge__badge"
|
||||
src={badgeImagePath}
|
||||
alt={badge.name}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__redeemed-gift-badge__badge',
|
||||
`module-message__redeemed-gift-badge__badge--missing-${direction}`
|
||||
)}
|
||||
aria-label={i18n('giftBadge--missing')}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="module-message__redeemed-gift-badge__container">
|
||||
<div className="module-message__redeemed-gift-badge">
|
||||
{badgeElement}
|
||||
<div className="module-message__redeemed-gift-badge__text">
|
||||
<div className="module-message__redeemed-gift-badge__title">
|
||||
{i18n('message--giftBadge')}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__redeemed-gift-badge__remaining',
|
||||
`module-message__redeemed-gift-badge__remaining--${direction}`
|
||||
)}
|
||||
>
|
||||
{remaining}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={classNames(
|
||||
'module-message__redeemed-gift-badge__button',
|
||||
`module-message__redeemed-gift-badge__button--${direction}`
|
||||
)}
|
||||
disabled={!wasSent}
|
||||
onClick={
|
||||
wasSent
|
||||
? () => this.setState({ showOutgoingGiftBadgeModal: true })
|
||||
: undefined
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<div className="module-message__redeemed-gift-badge__button__text">
|
||||
{buttonContents}
|
||||
</div>
|
||||
</button>
|
||||
{this.renderMetadata()}
|
||||
{showOutgoingGiftBadgeModal ? (
|
||||
<OutgoingGiftBadgeModal
|
||||
i18n={i18n}
|
||||
recipientTitle={conversationTitle}
|
||||
badgeId={badgeId}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
hideOutgoingGiftBadgeModal={() =>
|
||||
this.setState({ showOutgoingGiftBadgeModal: false })
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
throw missingCaseError(giftBadge.state);
|
||||
}
|
||||
|
||||
public renderQuote(): JSX.Element | null {
|
||||
const {
|
||||
conversationColor,
|
||||
|
@ -1216,14 +1460,13 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
id,
|
||||
quote,
|
||||
scrollToQuotedMessage,
|
||||
shouldCollapseAbove,
|
||||
} = this.props;
|
||||
|
||||
if (!quote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isViewOnce, referencedMessageNotFound } = quote;
|
||||
const { isGiftBadge, isViewOnce, referencedMessageNotFound } = quote;
|
||||
|
||||
const clickHandler = disableScroll
|
||||
? undefined
|
||||
|
@ -1236,19 +1479,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const isIncoming = direction === 'incoming';
|
||||
|
||||
let curveTopLeft: boolean;
|
||||
let curveTopRight: boolean;
|
||||
if (this.shouldRenderAuthor()) {
|
||||
curveTopLeft = false;
|
||||
curveTopRight = false;
|
||||
} else if (isIncoming) {
|
||||
curveTopLeft = !shouldCollapseAbove;
|
||||
curveTopRight = true;
|
||||
} else {
|
||||
curveTopLeft = true;
|
||||
curveTopRight = !shouldCollapseAbove;
|
||||
}
|
||||
|
||||
return (
|
||||
<Quote
|
||||
i18n={i18n}
|
||||
|
@ -1260,9 +1490,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
bodyRanges={quote.bodyRanges}
|
||||
conversationColor={conversationColor}
|
||||
customColor={customColor}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveTopRight={curveTopRight}
|
||||
isViewOnce={isViewOnce}
|
||||
isGiftBadge={isGiftBadge}
|
||||
referencedMessageNotFound={referencedMessageNotFound}
|
||||
isFromMe={quote.isFromMe}
|
||||
doubleCheckMissingQuoteReference={() =>
|
||||
|
@ -1279,7 +1508,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
direction,
|
||||
i18n,
|
||||
storyReplyContext,
|
||||
shouldCollapseAbove,
|
||||
} = this.props;
|
||||
|
||||
if (!storyReplyContext) {
|
||||
|
@ -1288,19 +1516,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const isIncoming = direction === 'incoming';
|
||||
|
||||
let curveTopLeft: boolean;
|
||||
let curveTopRight: boolean;
|
||||
if (this.shouldRenderAuthor()) {
|
||||
curveTopLeft = false;
|
||||
curveTopRight = false;
|
||||
} else if (isIncoming) {
|
||||
curveTopLeft = !shouldCollapseAbove;
|
||||
curveTopRight = true;
|
||||
} else {
|
||||
curveTopLeft = true;
|
||||
curveTopRight = !shouldCollapseAbove;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{storyReplyContext.emoji && (
|
||||
|
@ -1311,11 +1526,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
<Quote
|
||||
authorTitle={storyReplyContext.authorTitle}
|
||||
conversationColor={conversationColor}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveTopRight={curveTopRight}
|
||||
customColor={customColor}
|
||||
i18n={i18n}
|
||||
isFromMe={storyReplyContext.isFromMe}
|
||||
isGiftBadge={false}
|
||||
isIncoming={isIncoming}
|
||||
isStoryReply
|
||||
isViewOnce={false}
|
||||
|
@ -1757,6 +1971,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
deletedForEveryone,
|
||||
giftBadge,
|
||||
i18n,
|
||||
id,
|
||||
isSticker,
|
||||
|
@ -1769,7 +1984,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
text,
|
||||
} = this.props;
|
||||
|
||||
const canForward = !isTapToView && !deletedForEveryone && !contact;
|
||||
const canForward =
|
||||
!isTapToView && !deletedForEveryone && !giftBadge && !contact;
|
||||
const multipleAttachments = attachments && attachments.length > 1;
|
||||
|
||||
const shouldShowAdditional =
|
||||
|
@ -1934,7 +2150,11 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public getWidth(): number | undefined {
|
||||
const { attachments, isSticker, previews } = this.props;
|
||||
const { attachments, giftBadge, isSticker, previews } = this.props;
|
||||
|
||||
if (giftBadge) {
|
||||
return 240;
|
||||
}
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
if (isGIF(attachments)) {
|
||||
|
@ -2370,7 +2590,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public renderContents(): JSX.Element | null {
|
||||
const { isTapToView, deletedForEveryone } = this.props;
|
||||
const { giftBadge, isTapToView, deletedForEveryone } = this.props;
|
||||
|
||||
if (deletedForEveryone) {
|
||||
return (
|
||||
|
@ -2381,6 +2601,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
if (giftBadge) {
|
||||
return this.renderGiftBadge();
|
||||
}
|
||||
|
||||
if (isTapToView) {
|
||||
return (
|
||||
<>
|
||||
|
@ -2412,11 +2636,13 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
contact,
|
||||
displayTapToViewMessage,
|
||||
direction,
|
||||
giftBadge,
|
||||
id,
|
||||
isTapToView,
|
||||
isTapToViewExpired,
|
||||
kickOffAttachmentDownload,
|
||||
openConversation,
|
||||
openGiftBadge,
|
||||
showContactDetail,
|
||||
showVisualAttachment,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
|
@ -2426,6 +2652,11 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const isAttachmentPending = this.isAttachmentPending();
|
||||
|
||||
if (giftBadge && giftBadge.state === GiftBadgeStates.Unopened) {
|
||||
openGiftBadge(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTapToView) {
|
||||
if (isAttachmentPending) {
|
||||
log.info(
|
||||
|
@ -2621,6 +2852,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
customColor,
|
||||
deletedForEveryone,
|
||||
direction,
|
||||
giftBadge,
|
||||
isSticker,
|
||||
isTapToView,
|
||||
isTapToViewExpired,
|
||||
|
@ -2632,7 +2864,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const isAttachmentPending = this.isAttachmentPending();
|
||||
|
||||
const width = this.getWidth();
|
||||
const isShowingImage = this.isShowingImage();
|
||||
const shouldUseWidth = Boolean(giftBadge || this.isShowingImage());
|
||||
|
||||
const isEmojiOnly = this.canRenderStickerLikeEmoji();
|
||||
const isStickerLike = isSticker || isEmojiOnly;
|
||||
|
@ -2673,7 +2905,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
: null
|
||||
);
|
||||
const containerStyles = {
|
||||
width: isShowingImage ? width : undefined,
|
||||
width: shouldUseWidth ? width : undefined,
|
||||
};
|
||||
if (!isStickerLike && !deletedForEveryone && direction === 'outgoing') {
|
||||
Object.assign(containerStyles, getCustomColorStyle(customColor));
|
||||
|
|
|
@ -36,6 +36,7 @@ const defaultMessage: MessageDataPropsType = {
|
|||
canDownload: true,
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'my-convo',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'direct',
|
||||
direction: 'incoming',
|
||||
id: 'my-message',
|
||||
|
@ -81,6 +82,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
markViewed: action('markViewed'),
|
||||
openConversation: action('openConversation'),
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
openLink: action('openLink'),
|
||||
reactToMessage: action('reactToMessage'),
|
||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||
|
|
|
@ -73,6 +73,7 @@ export type PropsBackboneActions = Pick<
|
|||
| 'markAttachmentAsCorrupted'
|
||||
| 'markViewed'
|
||||
| 'openConversation'
|
||||
| 'openGiftBadge'
|
||||
| 'openLink'
|
||||
| 'reactToMessage'
|
||||
| 'renderAudioAttachment'
|
||||
|
@ -284,6 +285,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
markAttachmentAsCorrupted,
|
||||
markViewed,
|
||||
openConversation,
|
||||
openGiftBadge,
|
||||
openLink,
|
||||
reactToMessage,
|
||||
renderAudioAttachment,
|
||||
|
@ -339,6 +341,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
markViewed={markViewed}
|
||||
messageExpanded={noop}
|
||||
openConversation={openConversation}
|
||||
openGiftBadge={openGiftBadge}
|
||||
openLink={openLink}
|
||||
reactToMessage={reactToMessage}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
|
|
|
@ -49,6 +49,7 @@ const defaultMessageProps: MessagesProps = {
|
|||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'conversationId',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'direct', // override
|
||||
deleteMessage: action('default--deleteMessage'),
|
||||
deleteMessageForEveryone: action('default--deleteMessageForEveryone'),
|
||||
|
@ -70,6 +71,7 @@ const defaultMessageProps: MessagesProps = {
|
|||
markViewed: action('default--markViewed'),
|
||||
messageExpanded: action('default--message-expanded'),
|
||||
openConversation: action('default--openConversation'),
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
openLink: action('default--openLink'),
|
||||
previews: [],
|
||||
reactToMessage: action('default--reactToMessage'),
|
||||
|
@ -110,6 +112,7 @@ const renderInMessage = ({
|
|||
isFromMe,
|
||||
rawAttachment,
|
||||
isViewOnce,
|
||||
isGiftBadge,
|
||||
referencedMessageNotFound,
|
||||
text: quoteText,
|
||||
}: Props) => {
|
||||
|
@ -123,6 +126,7 @@ const renderInMessage = ({
|
|||
isFromMe,
|
||||
rawAttachment,
|
||||
isViewOnce,
|
||||
isGiftBadge,
|
||||
referencedMessageNotFound,
|
||||
sentAt: Date.now() - 30 * 1000,
|
||||
text: quoteText,
|
||||
|
@ -139,7 +143,10 @@ const renderInMessage = ({
|
|||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
|
||||
authorTitle: text(
|
||||
'authorTitle',
|
||||
overrideProps.authorTitle || 'Default Sender'
|
||||
),
|
||||
conversationColor: overrideProps.conversationColor || 'forest',
|
||||
doubleCheckMissingQuoteReference:
|
||||
overrideProps.doubleCheckMissingQuoteReference ||
|
||||
|
@ -154,6 +161,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
'referencedMessageNotFound',
|
||||
overrideProps.referencedMessageNotFound || false
|
||||
),
|
||||
isGiftBadge: boolean('isGiftBadge', overrideProps.isGiftBadge || false),
|
||||
isViewOnce: boolean('isViewOnce', overrideProps.isViewOnce || false),
|
||||
text: text(
|
||||
'text',
|
||||
|
@ -338,6 +346,15 @@ story.add('Video Tap-to-View', () => {
|
|||
return <Quote {...props} />;
|
||||
});
|
||||
|
||||
story.add('Gift Badge', () => {
|
||||
const props = createProps({
|
||||
text: '',
|
||||
isGiftBadge: true,
|
||||
});
|
||||
|
||||
return renderInMessage(props);
|
||||
});
|
||||
|
||||
story.add('Audio Only', () => {
|
||||
const props = createProps({
|
||||
rawAttachment: {
|
||||
|
|
|
@ -26,8 +26,6 @@ import { getCustomColorStyle } from '../../util/getCustomColorStyle';
|
|||
export type Props = {
|
||||
authorTitle: string;
|
||||
conversationColor: ConversationColorType;
|
||||
curveTopLeft?: boolean;
|
||||
curveTopRight?: boolean;
|
||||
customColor?: CustomColorType;
|
||||
bodyRanges?: BodyRangesType;
|
||||
i18n: LocalizerType;
|
||||
|
@ -39,6 +37,7 @@ export type Props = {
|
|||
onClose?: () => void;
|
||||
text: string;
|
||||
rawAttachment?: QuotedAttachmentType;
|
||||
isGiftBadge: boolean;
|
||||
isViewOnce: boolean;
|
||||
reactionEmoji?: string;
|
||||
referencedMessageNotFound: boolean;
|
||||
|
@ -62,6 +61,10 @@ function validateQuote(quote: Props): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (quote.isGiftBadge) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (quote.text) {
|
||||
return true;
|
||||
}
|
||||
|
@ -178,7 +181,12 @@ export class Quote extends React.Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
public renderImage(url: string, icon?: string): JSX.Element {
|
||||
public renderImage(
|
||||
url: string,
|
||||
icon: string | undefined,
|
||||
isGiftBadge?: boolean
|
||||
): JSX.Element {
|
||||
const { isIncoming } = this.props;
|
||||
const iconElement = icon ? (
|
||||
<div className={this.getClassName('__icon-container__inner')}>
|
||||
<div
|
||||
|
@ -196,7 +204,12 @@ export class Quote extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<ThumbnailImage
|
||||
className={this.getClassName('__icon-container')}
|
||||
className={classNames(
|
||||
this.getClassName('__icon-container'),
|
||||
isIncoming === false &&
|
||||
isGiftBadge &&
|
||||
this.getClassName('__icon-container__outgoing-gift-badge')
|
||||
)}
|
||||
src={url}
|
||||
onError={this.handleImageError}
|
||||
>
|
||||
|
@ -261,10 +274,14 @@ export class Quote extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public renderIconContainer(): JSX.Element | null {
|
||||
const { rawAttachment, isViewOnce, i18n } = this.props;
|
||||
const { isGiftBadge, isViewOnce, i18n, rawAttachment } = this.props;
|
||||
const { imageBroken } = this.state;
|
||||
const attachment = getAttachment(rawAttachment);
|
||||
|
||||
if (isGiftBadge) {
|
||||
return this.renderImage('images/gift-thumbnail.svg', undefined, true);
|
||||
}
|
||||
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
|
@ -295,7 +312,7 @@ export class Quote extends React.Component<Props, State> {
|
|||
}
|
||||
if (GoogleChrome.isImageTypeSupported(contentType)) {
|
||||
return url && !imageBroken
|
||||
? this.renderImage(url)
|
||||
? this.renderImage(url, undefined)
|
||||
: this.renderIcon('image');
|
||||
}
|
||||
if (MIME.isAudio(contentType)) {
|
||||
|
@ -306,8 +323,15 @@ export class Quote extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public renderText(): JSX.Element | null {
|
||||
const { bodyRanges, i18n, text, rawAttachment, isIncoming, isViewOnce } =
|
||||
this.props;
|
||||
const {
|
||||
bodyRanges,
|
||||
isGiftBadge,
|
||||
i18n,
|
||||
text,
|
||||
rawAttachment,
|
||||
isIncoming,
|
||||
isViewOnce,
|
||||
} = this.props;
|
||||
|
||||
if (text) {
|
||||
const quoteText = bodyRanges
|
||||
|
@ -334,18 +358,22 @@ export class Quote extends React.Component<Props, State> {
|
|||
|
||||
const attachment = getAttachment(rawAttachment);
|
||||
|
||||
if (!attachment) {
|
||||
let typeLabel;
|
||||
|
||||
if (isGiftBadge) {
|
||||
typeLabel = i18n('quote--giftBadge');
|
||||
} else if (attachment) {
|
||||
const { contentType, isVoiceMessage } = attachment;
|
||||
typeLabel = getTypeLabel({
|
||||
i18n,
|
||||
isViewOnce,
|
||||
contentType,
|
||||
isVoiceMessage,
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { contentType, isVoiceMessage } = attachment;
|
||||
|
||||
const typeLabel = getTypeLabel({
|
||||
i18n,
|
||||
isViewOnce,
|
||||
contentType,
|
||||
isVoiceMessage,
|
||||
});
|
||||
if (typeLabel) {
|
||||
return (
|
||||
<div
|
||||
|
@ -476,8 +504,6 @@ export class Quote extends React.Component<Props, State> {
|
|||
public override render(): JSX.Element | null {
|
||||
const {
|
||||
conversationColor,
|
||||
curveTopLeft,
|
||||
curveTopRight,
|
||||
customColor,
|
||||
isIncoming,
|
||||
onClick,
|
||||
|
@ -506,9 +532,7 @@ export class Quote extends React.Component<Props, State> {
|
|||
: this.getClassName(`--outgoing-${conversationColor}`),
|
||||
!onClick && this.getClassName('--no-click'),
|
||||
referencedMessageNotFound &&
|
||||
this.getClassName('--with-reference-warning'),
|
||||
curveTopLeft && this.getClassName('--curve-top-left'),
|
||||
curveTopRight && this.getClassName('--curve-top-right')
|
||||
this.getClassName('--with-reference-warning')
|
||||
)}
|
||||
style={{ ...getCustomColorStyle(customColor, true) }}
|
||||
>
|
||||
|
|
|
@ -55,6 +55,7 @@ const items: Record<string, TimelineItemType> = {
|
|||
canRetryDeleteForEveryone: true,
|
||||
conversationColor: 'forest',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'group',
|
||||
direction: 'incoming',
|
||||
id: 'id-1',
|
||||
|
@ -80,6 +81,7 @@ const items: Record<string, TimelineItemType> = {
|
|||
canRetryDeleteForEveryone: true,
|
||||
conversationColor: 'forest',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'group',
|
||||
direction: 'incoming',
|
||||
id: 'id-2',
|
||||
|
@ -119,6 +121,7 @@ const items: Record<string, TimelineItemType> = {
|
|||
canRetryDeleteForEveryone: true,
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'group',
|
||||
direction: 'incoming',
|
||||
id: 'id-3',
|
||||
|
@ -219,6 +222,7 @@ const items: Record<string, TimelineItemType> = {
|
|||
canRetryDeleteForEveryone: true,
|
||||
conversationColor: 'plum',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'group',
|
||||
direction: 'outgoing',
|
||||
id: 'id-6',
|
||||
|
@ -245,6 +249,7 @@ const items: Record<string, TimelineItemType> = {
|
|||
canRetryDeleteForEveryone: true,
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'group',
|
||||
direction: 'outgoing',
|
||||
id: 'id-7',
|
||||
|
@ -271,6 +276,7 @@ const items: Record<string, TimelineItemType> = {
|
|||
canRetryDeleteForEveryone: true,
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'group',
|
||||
direction: 'outgoing',
|
||||
id: 'id-8',
|
||||
|
@ -297,6 +303,7 @@ const items: Record<string, TimelineItemType> = {
|
|||
canRetryDeleteForEveryone: true,
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'group',
|
||||
direction: 'outgoing',
|
||||
id: 'id-9',
|
||||
|
@ -323,6 +330,7 @@ const items: Record<string, TimelineItemType> = {
|
|||
canRetryDeleteForEveryone: true,
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'Conversation Title',
|
||||
conversationType: 'group',
|
||||
direction: 'outgoing',
|
||||
id: 'id-10',
|
||||
|
@ -379,6 +387,7 @@ const actions = () => ({
|
|||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
|
||||
openLink: action('openLink'),
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
|
|
|
@ -248,6 +248,7 @@ const getActions = createSelector(
|
|||
'deleteMessageForEveryone',
|
||||
'showMessageDetail',
|
||||
'openConversation',
|
||||
'openGiftBadge',
|
||||
'showContactDetail',
|
||||
'showContactModal',
|
||||
'kickOffAttachmentDownload',
|
||||
|
|
|
@ -75,6 +75,7 @@ const getDefaultProps = () => ({
|
|||
messageExpanded: action('messageExpanded'),
|
||||
showMessageDetail: action('showMessageDetail'),
|
||||
openConversation: action('openConversation'),
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showForwardMessageModal: action('showForwardMessageModal'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue