Init payments message types
This commit is contained in:
parent
0c4b52a125
commit
6198b02640
26 changed files with 741 additions and 185 deletions
|
@ -5483,6 +5483,46 @@
|
|||
"message": "Delete this story? It will also be deleted for everyone who received it.",
|
||||
"description": "Confirmation dialog description text for deleting a story"
|
||||
},
|
||||
"icu:payment-event-notification-message-you-label": {
|
||||
"messageformat": "You started a payment to {receiver}",
|
||||
"description": "Payment event notification from you message bubble label"
|
||||
},
|
||||
"icu:payment-event-notification-message-you-label-without-receiver": {
|
||||
"messageformat": "You started a payment",
|
||||
"description": "Payment event notification from you message bubble label"
|
||||
},
|
||||
"icu:payment-event-notification-message-label": {
|
||||
"messageformat": "{sender} started a payment to you",
|
||||
"description": "Payment event notification from contact message bubble label"
|
||||
},
|
||||
"icu:payment-event-activation-request-label": {
|
||||
"messageformat": "{sender} wants you to activate Payments. Only send payments to people you trust. Payments can be activated on your mobile device by going to Settings -> Payments.",
|
||||
"description": "Payment event activation request from contact label"
|
||||
},
|
||||
"icu:payment-event-activation-request-you-label": {
|
||||
"messageformat": "You sent {receiver} a request to activate Payments.",
|
||||
"description": "Payment event activation request from you label"
|
||||
},
|
||||
"icu:payment-event-activation-request-you-label-without-receiver": {
|
||||
"messageformat": "You sent a request to activate Payments.",
|
||||
"description": "Payment event activation request from you label"
|
||||
},
|
||||
"icu:payment-event-activated-label": {
|
||||
"messageformat": "{sender} can now accept Payments.",
|
||||
"description": "Payment event activation from contact label"
|
||||
},
|
||||
"icu:payment-event-activated-you-label": {
|
||||
"messageformat": "You activated Payments.",
|
||||
"description": "Payment event activation from you label"
|
||||
},
|
||||
"icu:payment-event-notification-label": {
|
||||
"messageformat": "Payment",
|
||||
"description": "Payment event notification label"
|
||||
},
|
||||
"icu:payment-event-notification-check-primary-device": {
|
||||
"messageformat": "Check your primary device for this payment’s status",
|
||||
"description": "Payment event notification check device label"
|
||||
},
|
||||
"SignalConnectionsModal__title": {
|
||||
"message": "Signal Connections",
|
||||
"description": "The phrase/term: 'Signal Connections'"
|
||||
|
|
5
images/icons/v2/credit-card-16.svg
Normal file
5
images/icons/v2/credit-card-16.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="14" viewBox="0 0 16 14" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13.35 1H2.65C2.07978 1 1.53292 1.24055 1.12972 1.66873C0.726517 2.09691 0.5 2.67765 0.5 3.28319V10.7168C0.5 11.3224 0.726517 11.9031 1.12972 12.3313C1.53292 12.7595 2.07978 13 2.65 13H13.35C13.9202 13 14.4671 12.7595 14.8703 12.3313C15.2735 11.9031 15.5 11.3224 15.5 10.7168V3.28319C15.5 2.67765 15.2735 2.09691 14.8703 1.66873C14.4671 1.24055 13.9202 1 13.35 1ZM1.3 4.82301V3.28319C1.29987 3.09734 1.33508 2.91337 1.40351 2.74232C1.47195 2.57127 1.5722 2.41665 1.69827 2.28774C1.82433 2.15883 1.9736 2.05827 2.13714 1.9921C2.30068 1.92593 2.47512 1.89551 2.65 1.90266H13.35C13.5249 1.89551 13.6993 1.92593 13.8629 1.9921C14.0264 2.05827 14.1757 2.15883 14.3017 2.28774C14.4278 2.41665 14.5281 2.57127 14.5965 2.74232C14.6649 2.91337 14.7001 3.09734 14.7 3.28319V4.82301H1.3ZM2.65 12.0973C2.47512 12.1045 2.30068 12.0741 2.13714 12.0079C1.9736 11.9417 1.82433 11.8412 1.69827 11.7123C1.5722 11.5833 1.47195 11.4287 1.40351 11.2577C1.33508 11.0866 1.29987 10.9027 1.3 10.7168V6.52212H14.7V10.7168C14.7001 10.9027 14.6649 11.0866 14.5965 11.2577C14.5281 11.4287 14.4278 11.5833 14.3017 11.7123C14.1757 11.8412 14.0264 11.9417 13.8629 12.0079C13.6993 12.0741 13.5249 12.1045 13.35 12.0973H2.65Z"
|
||||
fill="#5E5E5E" stroke="#5E5E5E" stroke-width="0.25" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -132,6 +132,62 @@ message DataMessage {
|
|||
PROFILE_KEY_UPDATE = 4;
|
||||
}
|
||||
|
||||
message Payment {
|
||||
message Amount {
|
||||
message MobileCoin {
|
||||
optional uint64 picoMob = 1; // 1,000,000,000,000 picoMob per Mob
|
||||
}
|
||||
|
||||
oneof Amount {
|
||||
MobileCoin mobileCoin = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message RequestId {
|
||||
optional string uuid = 1;
|
||||
}
|
||||
|
||||
message Request {
|
||||
optional RequestId requestId = 1;
|
||||
optional Amount amount = 2;
|
||||
optional string note = 3;
|
||||
}
|
||||
|
||||
message Notification {
|
||||
message MobileCoin {
|
||||
optional bytes receipt = 1;
|
||||
}
|
||||
|
||||
oneof Transaction {
|
||||
MobileCoin mobileCoin = 1;
|
||||
}
|
||||
|
||||
// Optional, Refers to the PaymentRequest message, if any.
|
||||
optional string note = 2;
|
||||
optional RequestId requestId = 1003;
|
||||
}
|
||||
|
||||
message Cancellation {
|
||||
optional RequestId requestId = 1;
|
||||
}
|
||||
|
||||
message Activation {
|
||||
enum Type {
|
||||
REQUEST = 0;
|
||||
ACTIVATED = 1;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
}
|
||||
|
||||
oneof Item {
|
||||
Notification notification = 1;
|
||||
Activation activation = 2;
|
||||
Request request = 1002;
|
||||
Cancellation cancellation = 1003;
|
||||
}
|
||||
}
|
||||
|
||||
message Quote {
|
||||
enum Type {
|
||||
NORMAL = 0;
|
||||
|
@ -302,7 +358,7 @@ message DataMessage {
|
|||
optional Delete delete = 17;
|
||||
repeated BodyRange bodyRanges = 18;
|
||||
optional GroupCallUpdate groupCallUpdate = 19;
|
||||
reserved /* Payment payment */ 20;
|
||||
optional Payment payment = 20;
|
||||
optional StoryContext storyContext = 21;
|
||||
optional GiftBadge giftBadge = 22;
|
||||
}
|
||||
|
|
|
@ -3359,6 +3359,72 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
.module-payment-notification {
|
||||
&__container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin: 0 0 7px;
|
||||
@include font-subtitle;
|
||||
|
||||
@include light-theme() {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
@include dark-theme() {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
&__check_device_box {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
align-items: center;
|
||||
@include font-body-2;
|
||||
padding: 22px 7px;
|
||||
padding-left: 12px;
|
||||
border-radius: 18px;
|
||||
margin: 0 -4px;
|
||||
|
||||
@include light-theme() {
|
||||
background: $color-white-alpha-60;
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme() {
|
||||
background: $color-white-alpha-20;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@include color-svg('../images/icons/v2/info-16.svg', currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
&__note {
|
||||
margin: 9px 0 0;
|
||||
@include font-body-1;
|
||||
}
|
||||
|
||||
&--outgoing &__label {
|
||||
@include light-theme() {
|
||||
color: $color-white-alpha-80;
|
||||
}
|
||||
@include dark-theme() {
|
||||
color: $color-white-alpha-80;
|
||||
}
|
||||
}
|
||||
|
||||
&--outgoing &__check_device_box {
|
||||
background: $color-white-alpha-20;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
// Module: Spinner
|
||||
|
||||
.module-spinner__container {
|
||||
|
|
|
@ -267,6 +267,13 @@
|
|||
'../images/icons/v2/error-outline-12.svg'
|
||||
);
|
||||
}
|
||||
|
||||
&--icon-payment-event::before {
|
||||
@include system-message-icon(
|
||||
'../images/icons/v2/credit-card-16.svg',
|
||||
'../images/icons/v2/credit-card-16.svg'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
|
|
|
@ -18,6 +18,8 @@ import { fakeDraftAttachment } from '../test-both/helpers/fakeAttachment';
|
|||
import { landscapeGreenUrl } from '../storybook/Fixtures';
|
||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
||||
import { ConversationColors } from '../types/Colors';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { PaymentEventKind } from '../types/Payment';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -211,6 +213,7 @@ export function Quote(): JSX.Element {
|
|||
quotedMessageProps: {
|
||||
text: 'something',
|
||||
conversationColor: ConversationColors[10],
|
||||
conversationTitle: getDefaultConversation().title,
|
||||
isGiftBadge: false,
|
||||
isViewOnce: false,
|
||||
referencedMessageNotFound: false,
|
||||
|
@ -221,3 +224,30 @@ export function Quote(): JSX.Element {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function QuoteWithPayment(): JSX.Element {
|
||||
return (
|
||||
<CompositionArea
|
||||
{...useProps({
|
||||
quotedMessageProps: {
|
||||
text: '',
|
||||
conversationColor: ConversationColors[10],
|
||||
conversationTitle: getDefaultConversation().title,
|
||||
isGiftBadge: false,
|
||||
isViewOnce: false,
|
||||
referencedMessageNotFound: false,
|
||||
authorTitle: 'Someone',
|
||||
isFromMe: false,
|
||||
payment: {
|
||||
kind: PaymentEventKind.Notification,
|
||||
note: 'Thanks',
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
QuoteWithPayment.story = {
|
||||
name: 'Quote with payment',
|
||||
};
|
||||
|
|
|
@ -906,6 +906,7 @@ export function StoryViewer({
|
|||
{(currentViewTarget === StoryViewTargetType.Replies ||
|
||||
currentViewTarget === StoryViewTargetType.Views) && (
|
||||
<StoryViewsNRepliesModal
|
||||
conversationTitle={group?.title ?? title}
|
||||
authorTitle={firstName || title}
|
||||
canReply={Boolean(canReply)}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
|
|
|
@ -82,6 +82,7 @@ export enum StoryViewsNRepliesTab {
|
|||
}
|
||||
|
||||
export type PropsType = {
|
||||
conversationTitle: string;
|
||||
authorTitle: string;
|
||||
canReply: boolean;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
|
@ -115,6 +116,7 @@ export type PropsType = {
|
|||
};
|
||||
|
||||
export function StoryViewsNRepliesModal({
|
||||
conversationTitle,
|
||||
authorTitle,
|
||||
canReply,
|
||||
getPreferredBadge,
|
||||
|
@ -223,6 +225,7 @@ export function StoryViewsNRepliesModal({
|
|||
<Quote
|
||||
authorTitle={authorTitle}
|
||||
conversationColor="ultramarine"
|
||||
conversationTitle={conversationTitle}
|
||||
i18n={i18n}
|
||||
isFromMe={false}
|
||||
isGiftBadge={false}
|
||||
|
|
|
@ -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()}
|
||||
|
|
32
ts/components/conversation/PaymentEventNotification.tsx
Normal file
32
ts/components/conversation/PaymentEventNotification.tsx
Normal 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} />} />
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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',
|
||||
data: {
|
||||
author: getDefaultConversation({}),
|
||||
canDeleteForEveryone: false,
|
||||
canDownload: true,
|
||||
canReact: true,
|
||||
canReply: true,
|
||||
canRetry: true,
|
||||
canRetryDeleteForEveryone: true,
|
||||
'id-10': mockMessageTimelineItem('id-10', {
|
||||
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(),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
'id-11': {
|
||||
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',
|
||||
}),
|
||||
'id-11': mockMessageTimelineItem('id-11', {
|
||||
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(),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
'id-12': {
|
||||
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',
|
||||
}),
|
||||
'id-12': mockMessageTimelineItem('id-12', {
|
||||
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(),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
'id-13': {
|
||||
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',
|
||||
}),
|
||||
'id-13': mockMessageTimelineItem('id-13', {
|
||||
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(),
|
||||
},
|
||||
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',
|
||||
}),
|
||||
'id-14': mockMessageTimelineItem('id-14', {
|
||||
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-15': {
|
||||
type: 'paymentEvent',
|
||||
data: {
|
||||
event: {
|
||||
kind: PaymentEventKind.ActivationRequest,
|
||||
},
|
||||
sender: getDefaultConversation(),
|
||||
conversation: getDefaultConversation(),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
'id-16': {
|
||||
type: 'paymentEvent',
|
||||
data: {
|
||||
event: {
|
||||
kind: PaymentEventKind.Activation,
|
||||
},
|
||||
sender: getDefaultConversation(),
|
||||
conversation: getDefaultConversation(),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
'id-17': {
|
||||
type: 'paymentEvent',
|
||||
data: {
|
||||
event: {
|
||||
kind: PaymentEventKind.ActivationRequest,
|
||||
},
|
||||
sender: getDefaultConversation({
|
||||
isMe: true,
|
||||
}),
|
||||
conversation: getDefaultConversation(),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
'id-18': {
|
||||
type: 'paymentEvent',
|
||||
data: {
|
||||
event: {
|
||||
kind: PaymentEventKind.Activation,
|
||||
},
|
||||
sender: getDefaultConversation({
|
||||
isMe: true,
|
||||
}),
|
||||
conversation: getDefaultConversation(),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
'id-19': mockMessageTimelineItem('id-19', {
|
||||
direction: 'outgoing',
|
||||
status: 'read',
|
||||
payment: {
|
||||
kind: PaymentEventKind.Notification,
|
||||
note: 'Thanks',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const actions = () => ({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,6 +9,10 @@ import type {
|
|||
QuotedMessageType,
|
||||
} from '../model-types.d';
|
||||
import type { UUIDStringType } from '../types/UUID';
|
||||
import { PaymentEventKind } from '../types/Payment';
|
||||
import type { AnyPaymentEvent } from '../types/Payment';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
export function isIncoming(
|
||||
message: Pick<MessageAttributesType, 'type'>
|
||||
|
@ -26,6 +30,84 @@ export function isStory(message: Pick<MessageAttributesType, 'type'>): boolean {
|
|||
return message.type === 'story';
|
||||
}
|
||||
|
||||
export type MessageAttributesWithPaymentEvent = MessageAttributesType & {
|
||||
payment: AnyPaymentEvent;
|
||||
};
|
||||
|
||||
export function messageHasPaymentEvent(
|
||||
message: MessageAttributesType
|
||||
): message is MessageAttributesWithPaymentEvent {
|
||||
return message.payment != null;
|
||||
}
|
||||
|
||||
export function getPaymentEventNotificationText(
|
||||
payment: AnyPaymentEvent,
|
||||
senderTitle: string,
|
||||
conversationTitle: string | null,
|
||||
senderIsMe: boolean,
|
||||
i18n: LocalizerType
|
||||
): string {
|
||||
if (payment.kind === PaymentEventKind.Notification) {
|
||||
return i18n('icu:payment-event-notification-label');
|
||||
}
|
||||
return getPaymentEventDescription(
|
||||
payment,
|
||||
senderTitle,
|
||||
conversationTitle,
|
||||
senderIsMe,
|
||||
i18n
|
||||
);
|
||||
}
|
||||
|
||||
export function getPaymentEventDescription(
|
||||
payment: AnyPaymentEvent,
|
||||
senderTitle: string,
|
||||
conversationTitle: string | null,
|
||||
senderIsMe: boolean,
|
||||
i18n: LocalizerType
|
||||
): string {
|
||||
const { kind } = payment;
|
||||
if (kind === PaymentEventKind.Notification) {
|
||||
if (senderIsMe) {
|
||||
if (conversationTitle != null) {
|
||||
return i18n('icu:payment-event-notification-message-you-label', {
|
||||
receiver: conversationTitle,
|
||||
});
|
||||
}
|
||||
return i18n(
|
||||
'icu:payment-event-notification-message-you-label-without-receiver'
|
||||
);
|
||||
}
|
||||
return i18n('icu:payment-event-notification-message-label', {
|
||||
sender: senderTitle,
|
||||
});
|
||||
}
|
||||
if (kind === PaymentEventKind.ActivationRequest) {
|
||||
if (senderIsMe) {
|
||||
if (conversationTitle != null) {
|
||||
return i18n('icu:payment-event-activation-request-you-label', {
|
||||
receiver: conversationTitle,
|
||||
});
|
||||
}
|
||||
return i18n(
|
||||
'icu:payment-event-activation-request-you-label-without-receiver'
|
||||
);
|
||||
}
|
||||
return i18n('icu:payment-event-activation-request-label', {
|
||||
sender: senderTitle,
|
||||
});
|
||||
}
|
||||
if (kind === PaymentEventKind.Activation) {
|
||||
if (senderIsMe) {
|
||||
return i18n('icu:payment-event-activated-you-label');
|
||||
}
|
||||
return i18n('icu:payment-event-activated-label', {
|
||||
sender: senderTitle,
|
||||
});
|
||||
}
|
||||
throw missingCaseError(kind);
|
||||
}
|
||||
|
||||
export function isQuoteAMatch(
|
||||
message: MessageAttributesType | null | undefined,
|
||||
conversationId: string,
|
||||
|
|
3
ts/model-types.d.ts
vendored
3
ts/model-types.d.ts
vendored
|
@ -33,6 +33,7 @@ import type { StickerType } from './types/Stickers';
|
|||
import type { StorySendMode } from './types/Stories';
|
||||
import type { MIMEType } from './types/MIME';
|
||||
import type { DurationInSeconds } from './util/durations';
|
||||
import type { AnyPaymentEvent } from './types/Payment';
|
||||
|
||||
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
|
||||
import MemberRoleEnum = Proto.Member.Role;
|
||||
|
@ -76,6 +77,7 @@ export type QuotedMessageType = {
|
|||
// TODO DESKTOP-3826
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
attachments: Array<any>;
|
||||
payment?: AnyPaymentEvent;
|
||||
// `author` is an old attribute that holds the author's E164. We shouldn't use it for
|
||||
// new messages, but old messages might have this attribute.
|
||||
author?: string;
|
||||
|
@ -145,6 +147,7 @@ export type MessageAttributesType = {
|
|||
message?: unknown;
|
||||
messageTimer?: unknown;
|
||||
profileChange?: ProfileNameChangeType;
|
||||
payment?: AnyPaymentEvent;
|
||||
quote?: QuotedMessageType;
|
||||
reactions?: ReadonlyArray<MessageReactionType>;
|
||||
requiredProtocolVersion?: number;
|
||||
|
|
|
@ -3787,6 +3787,7 @@ export class ConversationModel extends window.Backbone
|
|||
attachments: isTapToView(quotedMessage.attributes)
|
||||
? [{ contentType: IMAGE_JPEG, fileName: null }]
|
||||
: await this.getQuoteAttachment(attachments, preview, sticker),
|
||||
payment: quotedMessage.get('payment'),
|
||||
bodyRanges: quotedMessage.get('bodyRanges'),
|
||||
id: quotedMessage.get('sent_at'),
|
||||
isViewOnce: isTapToView(quotedMessage.attributes),
|
||||
|
|
|
@ -159,7 +159,9 @@ import {
|
|||
getSource,
|
||||
getSourceUuid,
|
||||
isCustomError,
|
||||
messageHasPaymentEvent,
|
||||
isQuoteAMatch,
|
||||
getPaymentEventNotificationText,
|
||||
} from '../messages/helpers';
|
||||
import type { ReplacementValuesType } from '../types/I18N';
|
||||
import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue';
|
||||
|
@ -696,6 +698,21 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return { text: changes.map(({ text }) => text).join(' ') };
|
||||
}
|
||||
|
||||
if (messageHasPaymentEvent(attributes)) {
|
||||
const sender = findAndFormatContact(attributes.sourceUuid);
|
||||
const conversation = findAndFormatContact(attributes.conversationId);
|
||||
return {
|
||||
text: getPaymentEventNotificationText(
|
||||
attributes.payment,
|
||||
sender.title,
|
||||
conversation.title,
|
||||
sender.isMe,
|
||||
window.i18n
|
||||
),
|
||||
emoji: '💳',
|
||||
};
|
||||
}
|
||||
|
||||
const attachments = this.get('attachments') || [];
|
||||
|
||||
if (isTapToView(attributes)) {
|
||||
|
@ -1306,6 +1323,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
const isUniversalTimerNotificationValue =
|
||||
isUniversalTimerNotification(attributes);
|
||||
|
||||
const isPayment = messageHasPaymentEvent(attributes);
|
||||
|
||||
// Note: not all of these message types go through message.handleDataMessage
|
||||
|
||||
const hasSomethingToDisplay =
|
||||
|
@ -1314,6 +1333,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
hasAttachment ||
|
||||
hasEmbeddedContact ||
|
||||
isSticker ||
|
||||
isPayment ||
|
||||
// Rendered sync messages
|
||||
isCallHistoryValue ||
|
||||
isChatSessionRefreshedValue ||
|
||||
|
@ -2094,6 +2114,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
const { attachments } = quote;
|
||||
const firstAttachment = attachments ? attachments[0] : undefined;
|
||||
|
||||
if (messageHasPaymentEvent(originalMessage.attributes)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
quote.payment = originalMessage.get('payment');
|
||||
}
|
||||
|
||||
if (isTapToView(originalMessage.attributes)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
quote.text = undefined;
|
||||
|
@ -2670,6 +2695,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
dataMessage.requiredProtocolVersion ||
|
||||
this.INITIAL_PROTOCOL_VERSION,
|
||||
supportedVersionAtReceive: this.CURRENT_PROTOCOL_VERSION,
|
||||
payment: dataMessage.payment,
|
||||
quote: dataMessage.quote,
|
||||
schemaVersion: dataMessage.schemaVersion,
|
||||
sticker: dataMessage.sticker,
|
||||
|
@ -2962,7 +2988,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
!isStory(message.attributes) &&
|
||||
!isGroupStoryReply &&
|
||||
(!conversationTimestamp ||
|
||||
message.get('sent_at') > conversationTimestamp)
|
||||
message.get('sent_at') > conversationTimestamp) &&
|
||||
messageHasPaymentEvent(message.attributes)
|
||||
) {
|
||||
conversation.set({
|
||||
lastMessage: message.getNotificationText(),
|
||||
|
|
|
@ -273,7 +273,7 @@ function setMediaQualitySetting(
|
|||
}
|
||||
|
||||
function setQuotedMessage(
|
||||
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote'>
|
||||
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote' | 'payment'>
|
||||
): SetQuotedMessageActionType {
|
||||
return {
|
||||
type: SET_QUOTED_MESSAGE,
|
||||
|
|
|
@ -25,6 +25,7 @@ import type { PropsData as VerificationNotificationProps } from '../../component
|
|||
import type { PropsDataType as GroupsV2Props } from '../../components/conversation/GroupV2Change';
|
||||
import type { PropsDataType as GroupV1MigrationPropsType } from '../../components/conversation/GroupV1Migration';
|
||||
import type { PropsDataType as DeliveryIssuePropsType } from '../../components/conversation/DeliveryIssueNotification';
|
||||
import type { PropsType as PaymentEventNotificationPropsType } from '../../components/conversation/PaymentEventNotification';
|
||||
import type {
|
||||
PropsData as GroupNotificationProps,
|
||||
ChangeType,
|
||||
|
@ -95,9 +96,18 @@ import * as log from '../../logging/log';
|
|||
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
||||
import { DAY, HOUR, DurationInSeconds } from '../../util/durations';
|
||||
import { getStoryReplyText } from '../../util/getStoryReplyText';
|
||||
import { isIncoming, isOutgoing, isStory } from '../../messages/helpers';
|
||||
import type { MessageAttributesWithPaymentEvent } from '../../messages/helpers';
|
||||
import {
|
||||
isIncoming,
|
||||
isOutgoing,
|
||||
messageHasPaymentEvent,
|
||||
isStory,
|
||||
} from '../../messages/helpers';
|
||||
|
||||
import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import type { AnyPaymentEvent } from '../../types/Payment';
|
||||
import { isPaymentNotificationEvent } from '../../types/Payment';
|
||||
|
||||
export { isIncoming, isOutgoing, isStory };
|
||||
|
||||
|
@ -494,7 +504,10 @@ export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
|
|||
identity,
|
||||
|
||||
(
|
||||
message: Pick<MessageWithUIFieldsType, 'conversationId' | 'quote'>,
|
||||
message: Pick<
|
||||
MessageWithUIFieldsType,
|
||||
'conversationId' | 'quote' | 'payment'
|
||||
>,
|
||||
{
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
|
@ -515,6 +528,7 @@ export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
|
|||
isViewOnce,
|
||||
isGiftBadge: isTargetGiftBadge,
|
||||
referencedMessageNotFound,
|
||||
payment,
|
||||
text = '',
|
||||
} = quote;
|
||||
|
||||
|
@ -541,11 +555,13 @@ export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
|
|||
authorTitle,
|
||||
bodyRanges: processBodyRanges(quote, { conversationSelector }),
|
||||
conversationColor,
|
||||
conversationTitle: conversation.title,
|
||||
customColor,
|
||||
isFromMe,
|
||||
rawAttachment: firstAttachment
|
||||
? processQuoteAttachment(firstAttachment)
|
||||
: undefined,
|
||||
payment,
|
||||
isGiftBadge: Boolean(isTargetGiftBadge),
|
||||
isViewOnce,
|
||||
referencedMessageNotFound,
|
||||
|
@ -709,6 +725,12 @@ function getTextAttachment(
|
|||
);
|
||||
}
|
||||
|
||||
function getPayment(
|
||||
message: MessageWithUIFieldsType
|
||||
): AnyPaymentEvent | undefined {
|
||||
return message.payment;
|
||||
}
|
||||
|
||||
export function cleanBodyForDirectionCheck(text: string): string {
|
||||
const MENTIONS_REGEX = getMentionsRegex();
|
||||
const EMOJI_REGEX = emojiRegex();
|
||||
|
@ -778,6 +800,7 @@ export const getPropsForMessage: (
|
|||
getPropsForQuote,
|
||||
getPropsForStoryReplyContext,
|
||||
getTextAttachment,
|
||||
getPayment,
|
||||
getShallowPropsForMessage,
|
||||
(
|
||||
_,
|
||||
|
@ -789,6 +812,7 @@ export const getPropsForMessage: (
|
|||
quote: PropsData['quote'],
|
||||
storyReplyContext: PropsData['storyReplyContext'],
|
||||
textAttachment: PropsData['textAttachment'],
|
||||
payment: PropsData['payment'],
|
||||
shallowProps: ShallowPropsType
|
||||
): Omit<PropsForMessage, 'renderingContext' | 'menu' | 'contextMenu'> => {
|
||||
return {
|
||||
|
@ -800,6 +824,7 @@ export const getPropsForMessage: (
|
|||
reactions,
|
||||
storyReplyContext,
|
||||
textAttachment,
|
||||
payment,
|
||||
...shallowProps,
|
||||
};
|
||||
}
|
||||
|
@ -966,9 +991,31 @@ export function getPropsForBubble(
|
|||
};
|
||||
}
|
||||
|
||||
if (
|
||||
messageHasPaymentEvent(message) &&
|
||||
!isPaymentNotificationEvent(message.payment)
|
||||
) {
|
||||
return {
|
||||
type: 'paymentEvent',
|
||||
data: getPropsForPaymentEvent(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
return getBubblePropsForMessage(message, options);
|
||||
}
|
||||
|
||||
function getPropsForPaymentEvent(
|
||||
message: MessageAttributesWithPaymentEvent,
|
||||
{ conversationSelector }: GetPropsForBubbleOptions
|
||||
): Omit<PaymentEventNotificationPropsType, 'i18n'> {
|
||||
return {
|
||||
sender: conversationSelector(message.sourceUuid),
|
||||
conversation: conversationSelector(message.conversationId),
|
||||
event: message.payment,
|
||||
};
|
||||
}
|
||||
|
||||
// Unsupported Message
|
||||
|
||||
export function isUnsupportedMessage(
|
||||
|
@ -1640,6 +1687,7 @@ function canReplyOrReact(
|
|||
| 'canReplyToStory'
|
||||
| 'deletedForEveryone'
|
||||
| 'sendStateByConversationId'
|
||||
| 'payment'
|
||||
| 'type'
|
||||
>,
|
||||
ourConversationId: string | undefined,
|
||||
|
|
2
ts/textsecure/Types.d.ts
vendored
2
ts/textsecure/Types.d.ts
vendored
|
@ -8,6 +8,7 @@ import type { TextAttachmentType } from '../types/Attachment';
|
|||
import type { GiftBadgeStates } from '../components/conversation/Message';
|
||||
import type { MIMEType } from '../types/MIME';
|
||||
import type { DurationInSeconds } from '../util/durations';
|
||||
import type { AnyPaymentEvent } from '../types/Payment';
|
||||
|
||||
export {
|
||||
IdentityKeyType,
|
||||
|
@ -211,6 +212,7 @@ export type ProcessedDataMessage = {
|
|||
expireTimer: DurationInSeconds;
|
||||
profileKey?: string;
|
||||
timestamp: number;
|
||||
payment?: AnyPaymentEvent;
|
||||
quote?: ProcessedQuote;
|
||||
contact?: ReadonlyArray<ProcessedContact>;
|
||||
preview?: ReadonlyArray<ProcessedPreview>;
|
||||
|
|
|
@ -29,6 +29,8 @@ import { WarnOnlyError } from './Errors';
|
|||
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME';
|
||||
import { SECOND, DurationInSeconds } from '../util/durations';
|
||||
import type { AnyPaymentEvent } from '../types/Payment';
|
||||
import { PaymentEventKind } from '../types/Payment';
|
||||
|
||||
const FLAGS = Proto.DataMessage.Flags;
|
||||
export const ATTACHMENT_MAX = 32;
|
||||
|
@ -123,6 +125,38 @@ export function processGroupV2Context(
|
|||
};
|
||||
}
|
||||
|
||||
export function processPayment(
|
||||
payment?: Proto.DataMessage.IPayment | null
|
||||
): AnyPaymentEvent | undefined {
|
||||
if (!payment) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (payment.notification != null) {
|
||||
return {
|
||||
kind: PaymentEventKind.Notification,
|
||||
note: payment.notification.note ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (payment.activation != null) {
|
||||
if (
|
||||
payment.activation.type ===
|
||||
Proto.DataMessage.Payment.Activation.Type.REQUEST
|
||||
) {
|
||||
return { kind: PaymentEventKind.ActivationRequest };
|
||||
}
|
||||
if (
|
||||
payment.activation.type ===
|
||||
Proto.DataMessage.Payment.Activation.Type.ACTIVATED
|
||||
) {
|
||||
return { kind: PaymentEventKind.Activation };
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function processQuote(
|
||||
quote?: Proto.DataMessage.IQuote | null
|
||||
): ProcessedQuote | undefined {
|
||||
|
@ -305,6 +339,7 @@ export function processDataMessage(
|
|||
? Bytes.toBase64(message.profileKey)
|
||||
: undefined,
|
||||
timestamp,
|
||||
payment: processPayment(message.payment),
|
||||
quote: processQuote(message.quote),
|
||||
contact: processContact(message.contact),
|
||||
preview: processPreview(message.preview),
|
||||
|
|
34
ts/types/Payment.ts
Normal file
34
ts/types/Payment.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum PaymentEventKind {
|
||||
Notification = 1,
|
||||
ActivationRequest = 2,
|
||||
Activation = 3,
|
||||
// Request = 4, -- disabled
|
||||
// Cancellation = 5, -- disabled
|
||||
}
|
||||
|
||||
export type PaymentNotificationEvent = {
|
||||
kind: PaymentEventKind.Notification;
|
||||
note: string | null;
|
||||
};
|
||||
|
||||
export type PaymentActivationRequestEvent = {
|
||||
kind: PaymentEventKind.ActivationRequest;
|
||||
};
|
||||
|
||||
export type PaymentActivatedEvent = {
|
||||
kind: PaymentEventKind.Activation;
|
||||
};
|
||||
|
||||
export type AnyPaymentEvent =
|
||||
| PaymentNotificationEvent
|
||||
| PaymentActivationRequestEvent
|
||||
| PaymentActivatedEvent;
|
||||
|
||||
export function isPaymentNotificationEvent(
|
||||
event: AnyPaymentEvent
|
||||
): event is PaymentNotificationEvent {
|
||||
return event.kind === PaymentEventKind.Notification;
|
||||
}
|
Loading…
Reference in a new issue