Init payments message types

This commit is contained in:
Jamie Kyle
2022-11-30 13:47:54 -08:00
committed by GitHub
parent 0c4b52a125
commit 6198b02640
26 changed files with 741 additions and 185 deletions

View File

@@ -83,6 +83,10 @@ import { DAY, HOUR, MINUTE, SECOND } from '../../util/durations';
import { BadgeImageTheme } from '../../badges/BadgeImageTheme';
import { getBadgeImageFileLocalPath } from '../../badges/getBadgeImageFileLocalPath';
import { handleOutsideClick } from '../../util/handleOutsideClick';
import { PaymentEventKind } from '../../types/Payment';
import type { AnyPaymentEvent } from '../../types/Payment';
import { Emojify } from './Emojify';
import { getPaymentEventDescription } from '../../messages/helpers';
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10;
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
@@ -216,11 +220,14 @@ export type PropsData = {
conversationType: ConversationTypeType;
attachments?: Array<AttachmentType>;
giftBadge?: GiftBadgeType;
payment?: AnyPaymentEvent;
quote?: {
conversationColor: ConversationColorType;
conversationTitle: string;
customColor?: CustomColorType;
text: string;
rawAttachment?: QuotedAttachmentType;
payment?: AnyPaymentEvent;
isFromMe: boolean;
sentAt: number;
authorId: string;
@@ -1405,9 +1412,52 @@ export class Message extends React.PureComponent<Props, State> {
throw missingCaseError(giftBadge.state);
}
public renderPayment(): JSX.Element | null {
const {
payment,
direction,
author,
conversationTitle,
conversationColor,
i18n,
} = this.props;
if (payment == null || payment.kind !== PaymentEventKind.Notification) {
return null;
}
return (
<div
className={`module-payment-notification__container ${
direction === 'outgoing'
? `module-payment-notification--outgoing module-payment-notification--outgoing-${conversationColor}`
: ''
}`}
>
<p className="module-payment-notification__label">
{getPaymentEventDescription(
payment,
author.title,
conversationTitle,
author.isMe,
i18n
)}
</p>
<p className="module-payment-notification__check_device_box">
{i18n('icu:payment-event-notification-check-primary-device')}
</p>
{payment.note != null && (
<p className="module-payment-notification__note">
<Emojify text={payment.note} />
</p>
)}
</div>
);
}
public renderQuote(): JSX.Element | null {
const {
conversationColor,
conversationTitle,
customColor,
direction,
disableScroll,
@@ -1441,10 +1491,12 @@ export class Message extends React.PureComponent<Props, State> {
onClick={clickHandler}
text={quote.text}
rawAttachment={quote.rawAttachment}
payment={quote.payment}
isIncoming={isIncoming}
authorTitle={quote.authorTitle}
bodyRanges={quote.bodyRanges}
conversationColor={conversationColor}
conversationTitle={conversationTitle}
customColor={customColor}
isViewOnce={isViewOnce}
isGiftBadge={isGiftBadge}
@@ -1459,6 +1511,7 @@ export class Message extends React.PureComponent<Props, State> {
public renderStoryReplyContext(): JSX.Element | null {
const {
conversationTitle,
conversationColor,
customColor,
direction,
@@ -1483,6 +1536,7 @@ export class Message extends React.PureComponent<Props, State> {
<Quote
authorTitle={storyReplyContext.authorTitle}
conversationColor={conversationColor}
conversationTitle={conversationTitle}
customColor={customColor}
i18n={i18n}
isFromMe={storyReplyContext.isFromMe}
@@ -2169,6 +2223,7 @@ export class Message extends React.PureComponent<Props, State> {
{this.renderStoryReplyContext()}
{this.renderAttachment()}
{this.renderPreview()}
{this.renderPayment()}
{this.renderEmbeddedContact()}
{this.renderText()}
{this.renderMetadata()}

View File

@@ -0,0 +1,32 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../../types/Util';
import type { ConversationType } from '../../state/ducks/conversations';
import { SystemMessage } from './SystemMessage';
import { Emojify } from './Emojify';
import type { AnyPaymentEvent } from '../../types/Payment';
import { getPaymentEventDescription } from '../../messages/helpers';
export type PropsType = {
event: AnyPaymentEvent;
sender: ConversationType;
conversation: ConversationType;
i18n: LocalizerType;
};
export function PaymentEventNotification(props: PropsType): JSX.Element {
const { event, sender, conversation, i18n } = props;
const message = getPaymentEventDescription(
event,
sender.title,
conversation.title,
sender.isMe,
i18n
);
return (
<SystemMessage icon="payment-event" contents={<Emojify text={message} />} />
);
}

View File

@@ -26,6 +26,7 @@ import enMessages from '../../../_locales/en/messages.json';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { WidthBreakpoint } from '../_util';
import { ThemeType } from '../../types/Util';
import { PaymentEventKind } from '../../types/Payment';
const i18n = setupI18n('en', enMessages);
@@ -166,6 +167,7 @@ const renderInMessage = ({
authorId: 'an-author',
authorTitle,
conversationColor,
conversationTitle: getDefaultConversation().title,
isFromMe,
rawAttachment,
isViewOnce,
@@ -567,3 +569,12 @@ IsStoryReplyEmoji.args = {
IsStoryReplyEmoji.story = {
name: 'isStoryReply emoji',
};
export const Payment = Template.bind({});
Payment.args = {
text: '',
payment: {
kind: PaymentEventKind.Notification,
note: null,
},
};

View File

@@ -22,10 +22,14 @@ import { TextAttachment } from '../TextAttachment';
import { getTextWithMentions } from '../../util/getTextWithMentions';
import { getClassNamesFor } from '../../util/getClassNamesFor';
import { getCustomColorStyle } from '../../util/getCustomColorStyle';
import type { AnyPaymentEvent } from '../../types/Payment';
import { PaymentEventKind } from '../../types/Payment';
import { getPaymentEventNotificationText } from '../../messages/helpers';
export type Props = {
authorTitle: string;
conversationColor: ConversationColorType;
conversationTitle: string;
customColor?: CustomColorType;
bodyRanges?: HydratedBodyRangesType;
i18n: LocalizerType;
@@ -38,6 +42,7 @@ export type Props = {
onClose?: () => void;
text: string;
rawAttachment?: QuotedAttachmentType;
payment?: AnyPaymentEvent;
isGiftBadge: boolean;
isViewOnce: boolean;
reactionEmoji?: string;
@@ -74,6 +79,10 @@ function validateQuote(quote: Props): boolean {
return true;
}
if (quote.payment?.kind === PaymentEventKind.Notification) {
return true;
}
return false;
}
@@ -274,6 +283,28 @@ export class Quote extends React.Component<Props, State> {
);
}
public renderPayment(): JSX.Element | null {
const { payment, authorTitle, conversationTitle, isFromMe, i18n } =
this.props;
if (payment == null) {
return null;
}
return (
<>
<Emojify text="💳" />
{getPaymentEventNotificationText(
payment,
authorTitle,
conversationTitle,
isFromMe,
i18n
)}
</>
);
}
public renderIconContainer(): JSX.Element | null {
const { isGiftBadge, isViewOnce, i18n, rawAttachment } = this.props;
const { imageBroken } = this.state;
@@ -550,6 +581,7 @@ export class Quote extends React.Component<Props, State> {
<div className={this.getClassName('__primary')}>
{this.renderAuthor()}
{this.renderGenericFile()}
{this.renderPayment()}
{this.renderText()}
</div>
{reactionEmoji && (

View File

@@ -26,6 +26,8 @@ import { ReadStatus } from '../../messages/MessageReadStatus';
import type { WidthBreakpoint } from '../_util';
import { ThemeType } from '../../types/Util';
import { TextDirection } from './Message';
import { PaymentEventKind } from '../../types/Payment';
import type { PropsData as TimelineMessageProps } from './TimelineMessage';
const i18n = setupI18n('en', enMessages);
@@ -36,61 +38,53 @@ export default {
// eslint-disable-next-line
const noop = () => {};
const items: Record<string, TimelineItemType> = {
'id-1': {
type: 'message',
data: {
author: getDefaultConversation({
phoneNumber: '(202) 555-2001',
}),
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
conversationColor: 'forest',
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
direction: 'incoming',
id: 'id-1',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
text: '🔥',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
},
'id-2': {
function mockMessageTimelineItem(
id: string,
data: Partial<TimelineMessageProps>
): TimelineItemType {
return {
type: 'message',
data: {
id,
author: getDefaultConversation({}),
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
conversationColor: 'forest',
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
conversationColor: 'crimson',
direction: 'incoming',
id: 'id-2',
status: 'sent',
text: 'Hello there from the new world!',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
text: 'Hello there from the new world! http://somewhere.com',
canRetryDeleteForEveryone: true,
textDirection: TextDirection.Default,
timestamp: Date.now(),
...data,
},
timestamp: Date.now(),
},
};
}
const items: Record<string, TimelineItemType> = {
'id-1': mockMessageTimelineItem('id-1', {
author: getDefaultConversation({
phoneNumber: '(202) 555-2001',
}),
conversationColor: 'forest',
text: '🔥',
}),
'id-2': mockMessageTimelineItem('id-2', {
conversationColor: 'forest',
direction: 'incoming',
text: 'Hello there from the new world! http://somewhere.com',
}),
'id-2.5': {
type: 'unsupportedMessage',
data: {
@@ -105,32 +99,7 @@ const items: Record<string, TimelineItemType> = {
},
timestamp: Date.now(),
},
'id-3': {
type: 'message',
data: {
author: getDefaultConversation({}),
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
conversationColor: 'crimson',
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
direction: 'incoming',
id: 'id-3',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
text: 'Hello there from the new world!',
textDirection: TextDirection.Default,
timestamp: Date.now(),
},
timestamp: Date.now(),
},
'id-3': mockMessageTimelineItem('id-3', {}),
'id-4': {
type: 'timerNotification',
data: {
@@ -206,141 +175,85 @@ const items: Record<string, TimelineItemType> = {
data: null,
timestamp: Date.now(),
},
'id-10': {
type: 'message',
'id-10': mockMessageTimelineItem('id-10', {
conversationColor: 'plum',
direction: 'outgoing',
text: '🔥',
}),
'id-11': mockMessageTimelineItem('id-11', {
direction: 'outgoing',
status: 'read',
text: 'Hello there from the new world! http://somewhere.com',
}),
'id-12': mockMessageTimelineItem('id-12', {
direction: 'outgoing',
text: 'Hello there from the new world! 🔥',
}),
'id-13': mockMessageTimelineItem('id-13', {
direction: 'outgoing',
text: 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
}),
'id-14': mockMessageTimelineItem('id-14', {
direction: 'outgoing',
status: 'read',
text: 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
}),
'id-15': {
type: 'paymentEvent',
data: {
author: getDefaultConversation({}),
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
conversationColor: 'plum',
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
direction: 'outgoing',
id: 'id-6',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
status: 'sent',
text: '🔥',
textDirection: TextDirection.Default,
timestamp: Date.now(),
event: {
kind: PaymentEventKind.ActivationRequest,
},
sender: getDefaultConversation(),
conversation: getDefaultConversation(),
},
timestamp: Date.now(),
},
'id-11': {
type: 'message',
'id-16': {
type: 'paymentEvent',
data: {
author: getDefaultConversation({}),
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
conversationColor: 'crimson',
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
direction: 'outgoing',
id: 'id-7',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
status: 'read',
text: 'Hello there from the new world! http://somewhere.com',
textDirection: TextDirection.Default,
timestamp: Date.now(),
event: {
kind: PaymentEventKind.Activation,
},
sender: getDefaultConversation(),
conversation: getDefaultConversation(),
},
timestamp: Date.now(),
},
'id-12': {
type: 'message',
'id-17': {
type: 'paymentEvent',
data: {
author: getDefaultConversation({}),
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
conversationColor: 'crimson',
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
direction: 'outgoing',
id: 'id-8',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
status: 'sent',
text: 'Hello there from the new world! 🔥',
textDirection: TextDirection.Default,
timestamp: Date.now(),
event: {
kind: PaymentEventKind.ActivationRequest,
},
sender: getDefaultConversation({
isMe: true,
}),
conversation: getDefaultConversation(),
},
timestamp: Date.now(),
},
'id-13': {
type: 'message',
'id-18': {
type: 'paymentEvent',
data: {
author: getDefaultConversation({}),
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
conversationColor: 'crimson',
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
direction: 'outgoing',
id: 'id-9',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
status: 'sent',
text: 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
textDirection: TextDirection.Default,
timestamp: Date.now(),
event: {
kind: PaymentEventKind.Activation,
},
sender: getDefaultConversation({
isMe: true,
}),
conversation: getDefaultConversation(),
},
timestamp: Date.now(),
},
'id-14': {
type: 'message',
data: {
author: getDefaultConversation({}),
canDeleteForEveryone: false,
canDownload: true,
canReact: true,
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
conversationColor: 'crimson',
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
direction: 'outgoing',
id: 'id-10',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],
readStatus: ReadStatus.Read,
status: 'read',
text: 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
textDirection: TextDirection.Default,
timestamp: Date.now(),
'id-19': mockMessageTimelineItem('id-19', {
direction: 'outgoing',
status: 'read',
payment: {
kind: PaymentEventKind.Notification,
note: 'Thanks',
},
timestamp: Date.now(),
},
}),
};
const actions = () => ({

View File

@@ -17,6 +17,7 @@ import { AvatarColors } from '../../types/Colors';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { WidthBreakpoint } from '../_util';
import { ThemeType } from '../../types/Util';
import { PaymentEventKind } from '../../types/Payment';
const i18n = setupI18n('en', enMessages);
@@ -437,6 +438,50 @@ export function Notification(): JSX.Element {
changedContact: getDefaultConversation(),
},
},
{
type: 'paymentEvent',
data: {
event: {
kind: PaymentEventKind.ActivationRequest,
},
sender: getDefaultConversation(),
conversation: getDefaultConversation(),
},
},
{
type: 'paymentEvent',
data: {
event: {
kind: PaymentEventKind.Activation,
},
sender: getDefaultConversation(),
conversation: getDefaultConversation(),
},
},
{
type: 'paymentEvent',
data: {
event: {
kind: PaymentEventKind.ActivationRequest,
},
sender: getDefaultConversation({
isMe: true,
}),
conversation: getDefaultConversation(),
},
},
{
type: 'paymentEvent',
data: {
event: {
kind: PaymentEventKind.Activation,
},
sender: getDefaultConversation({
isMe: true,
}),
conversation: getDefaultConversation(),
},
},
{
type: 'resetSessionNotification',
data: null,

View File

@@ -53,6 +53,8 @@ import type { SmartContactRendererType } from '../../groupChange';
import { ResetSessionNotification } from './ResetSessionNotification';
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
import { ProfileChangeNotification } from './ProfileChangeNotification';
import type { PropsType as PaymentEventNotificationPropsType } from './PaymentEventNotification';
import { PaymentEventNotification } from './PaymentEventNotification';
import type { FullJSXType } from '../Intl';
import { TimelineMessage } from './TimelineMessage';
@@ -116,6 +118,10 @@ type ProfileChangeNotificationType = {
type: 'profileChange';
data: ProfileChangeNotificationPropsType;
};
type PaymentEventType = {
type: 'paymentEvent';
data: Omit<PaymentEventNotificationPropsType, 'i18n'>;
};
export type TimelineItemType = (
| CallHistoryType
@@ -133,6 +139,7 @@ export type TimelineItemType = (
| ChangeNumberNotificationType
| UnsupportedMessageType
| VerificationNotificationType
| PaymentEventType
) & { timestamp: number };
type PropsLocalType = {
@@ -302,6 +309,14 @@ export class TimelineItem extends React.PureComponent<PropsType> {
i18n={i18n}
/>
);
} else if (item.type === 'paymentEvent') {
notification = (
<PaymentEventNotification
{...this.props}
{...item.data}
i18n={i18n}
/>
);
} else {
// Weird, yes, but the idea is to get a compile error when we aren't comprehensive
// with our if/else checks above, but also log out the type we don't understand

View File

@@ -44,6 +44,7 @@ import { getFakeBadge } from '../../test-both/helpers/getFakeBadge';
import { ThemeType } from '../../types/Util';
import { UUID } from '../../types/UUID';
import { BadgeCategory } from '../../badges/BadgeCategory';
import { PaymentEventKind } from '../../types/Payment';
const i18n = setupI18n('en', enMessages);
@@ -863,6 +864,7 @@ export const LinkPreviewWithQuote = Template.bind({});
LinkPreviewWithQuote.args = {
quote: {
conversationColor: ConversationColors[2],
conversationTitle: getDefaultConversation().title,
text: 'The quoted message',
isFromMe: false,
sentAt: Date.now(),
@@ -2069,3 +2071,13 @@ GiftBadgeMissingBadge.args = {
GiftBadgeMissingBadge.story = {
name: 'Gift Badge: Missing Badge',
};
export const PaymentNotification = Template.bind({});
PaymentNotification.args = {
canReply: false,
canReact: false,
payment: {
kind: PaymentEventKind.Notification,
note: 'Hello there',
},
};

View File

@@ -78,6 +78,7 @@ export function TimelineMessage(props: Props): JSX.Element {
canDeleteForEveryone,
canRetryDeleteForEveryone,
contact,
payment,
containerElementRef,
containerWidthBreakpoint,
deletedForEveryone,
@@ -210,7 +211,7 @@ export function TimelineMessage(props: Props): JSX.Element {
};
const canForward =
!isTapToView && !deletedForEveryone && !giftBadge && !contact;
!isTapToView && !deletedForEveryone && !giftBadge && !contact && !payment;
const shouldShowAdditional =
doesMessageBodyOverflow(text || '') || !isWindowWidthNotNarrow;