Modern profile sharing in 1:1 and GroupV1 groups
This commit is contained in:
parent
60f2422e2a
commit
04b7a29229
22 changed files with 371 additions and 115 deletions
|
@ -2727,6 +2727,38 @@
|
|||
"message": "Accept",
|
||||
"description": "Shown as a button to let the user accept a message request"
|
||||
},
|
||||
"MessageRequests--continue": {
|
||||
"message": "Continue",
|
||||
"description": "Shown as a button to share your profile, necessary to continue messaging in a conversation"
|
||||
},
|
||||
"MessageRequests--profile-sharing--group": {
|
||||
"message": "Continue your conversation with this group and share your name and photo with its members? $learnMore$",
|
||||
"description": "Shown when user hasn't shared their profile in a group yet",
|
||||
"placeholders": {
|
||||
"learnMore": {
|
||||
"content": "$1",
|
||||
"example": "Learn More."
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessageRequests--profile-sharing--direct": {
|
||||
"message": "Continue this conversation with $firstName$ and share your name and photo with them? $learnMore$",
|
||||
"description": "Shown when user hasn't shared their profile in a 1:1 conversation yet",
|
||||
"placeholders": {
|
||||
"firstName": {
|
||||
"content": "$1",
|
||||
"example": "Alice"
|
||||
},
|
||||
"learnMore": {
|
||||
"content": "$2",
|
||||
"example": "Learn More."
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessageRequests--learn-more": {
|
||||
"message": "Learn more.",
|
||||
"description": "Shown at the end of profile sharing messages as a link."
|
||||
},
|
||||
"ConversationHero--members": {
|
||||
"message": "$count$ members",
|
||||
"description": "Specifies the number of members in a group conversation",
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
Component: window.Signal.Components.SafetyNumberChangeDialog,
|
||||
props: {
|
||||
confirmText: options.confirmText,
|
||||
contacts: options.contacts.map(contact => contact.cachedProps),
|
||||
contacts: options.contacts.map(contact => contact.format()),
|
||||
i18n: window.i18n,
|
||||
onCancel: () => {
|
||||
dialog.remove();
|
||||
|
|
|
@ -3597,6 +3597,10 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
|||
&__name {
|
||||
@include font-body-2-bold;
|
||||
}
|
||||
|
||||
&__learn-more {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
|
|
|
@ -2,12 +2,13 @@ import { get, throttle } from 'lodash';
|
|||
import { WebAPIType } from './textsecure/WebAPI';
|
||||
|
||||
type ConfigKeyType =
|
||||
| 'desktop.messageRequests'
|
||||
| 'desktop.gv2'
|
||||
| 'desktop.cds'
|
||||
| 'desktop.clientExpiration'
|
||||
| 'desktop.gv2'
|
||||
| 'desktop.mandatoryProfileSharing'
|
||||
| 'desktop.messageRequests'
|
||||
| 'desktop.storage'
|
||||
| 'desktop.storageWrite'
|
||||
| 'desktop.clientExpiration';
|
||||
| 'desktop.storageWrite';
|
||||
type ConfigValueType = {
|
||||
name: ConfigKeyType;
|
||||
enabled: boolean;
|
||||
|
|
|
@ -652,12 +652,13 @@ type WhatIsThis = typeof window.WhatIsThis;
|
|||
function initializeRedux() {
|
||||
// Here we set up a full redux store with initial state for our LeftPane Root
|
||||
const convoCollection = window.getConversations();
|
||||
const conversations = convoCollection.map(
|
||||
conversation => conversation.cachedProps
|
||||
const conversations = convoCollection.map(conversation =>
|
||||
conversation.format()
|
||||
);
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
const ourConversationId = window.ConversationController.getOurConversationId();
|
||||
|
||||
const initialState = {
|
||||
conversations: {
|
||||
conversationLookup: window.Signal.Util.makeLookup(conversations, 'id'),
|
||||
|
@ -1580,7 +1581,7 @@ type WhatIsThis = typeof window.WhatIsThis;
|
|||
'desktop.clientExpiration',
|
||||
({ value }) => {
|
||||
const remoteBuildExpirationTimestamp = window.Signal.Util.parseRemoteClientExpiration(
|
||||
value
|
||||
value as string
|
||||
);
|
||||
if (remoteBuildExpirationTimestamp) {
|
||||
window.storage.put(
|
||||
|
|
|
@ -16,12 +16,15 @@ import {
|
|||
MessageRequestActions,
|
||||
Props as MessageRequestActionsProps,
|
||||
} from './conversation/MessageRequestActions';
|
||||
import { MandatoryProfileSharingActions } from './conversation/MandatoryProfileSharingActions';
|
||||
import { countStickers } from './stickers/lib';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly groupVersion?: 1 | 2;
|
||||
readonly isMissingMandatoryProfileSharing?: boolean;
|
||||
readonly messageRequestsEnabled?: boolean;
|
||||
readonly acceptedMessageRequest?: boolean;
|
||||
readonly compositionApi?: React.MutableRefObject<{
|
||||
|
@ -113,7 +116,9 @@ export const CompositionArea = ({
|
|||
// Message Requests
|
||||
acceptedMessageRequest,
|
||||
conversationType,
|
||||
groupVersion,
|
||||
isBlocked,
|
||||
isMissingMandatoryProfileSharing,
|
||||
messageRequestsEnabled,
|
||||
name,
|
||||
onAccept,
|
||||
|
@ -326,7 +331,7 @@ export const CompositionArea = ({
|
|||
};
|
||||
}, [setLarge]);
|
||||
|
||||
if ((!acceptedMessageRequest || isBlocked) && messageRequestsEnabled) {
|
||||
if (messageRequestsEnabled && (!acceptedMessageRequest || isBlocked)) {
|
||||
return (
|
||||
<MessageRequestActions
|
||||
i18n={i18n}
|
||||
|
@ -345,6 +350,28 @@ export const CompositionArea = ({
|
|||
);
|
||||
}
|
||||
|
||||
// If no message request, but we haven't shared profile yet, we show profile-sharing UI
|
||||
if (
|
||||
(conversationType === 'direct' ||
|
||||
(conversationType === 'group' && groupVersion === 1)) &&
|
||||
isMissingMandatoryProfileSharing
|
||||
) {
|
||||
return (
|
||||
<MandatoryProfileSharingActions
|
||||
i18n={i18n}
|
||||
conversationType={conversationType}
|
||||
onBlock={onBlock}
|
||||
onBlockAndDelete={onBlockAndDelete}
|
||||
onDelete={onDelete}
|
||||
onAccept={onAccept}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-composition-area">
|
||||
<div className="module-composition-area__toggle-large">
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import {
|
||||
MandatoryProfileSharingActions,
|
||||
Props as MandatoryProfileSharingActionsProps,
|
||||
} from './MandatoryProfileSharingActions';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const getBaseProps = (
|
||||
isGroup = false
|
||||
): MandatoryProfileSharingActionsProps => ({
|
||||
i18n,
|
||||
conversationType: isGroup ? 'group' : 'direct',
|
||||
firstName: text('firstName', 'Cayce'),
|
||||
title: isGroup
|
||||
? text('title', 'NYC Rock Climbers')
|
||||
: text('title', 'Cayce Bollard'),
|
||||
name: isGroup
|
||||
? text('name', 'NYC Rock Climbers')
|
||||
: text('name', 'Cayce Bollard'),
|
||||
onBlock: action('block'),
|
||||
onBlockAndDelete: action('onBlockAndDelete'),
|
||||
onDelete: action('delete'),
|
||||
onAccept: action('accept'),
|
||||
});
|
||||
|
||||
storiesOf('Components/Conversation/MandatoryProfileSharingActions', module)
|
||||
.add('Direct', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MandatoryProfileSharingActions {...getBaseProps()} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Group', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MandatoryProfileSharingActions {...getBaseProps(true)} />
|
||||
</div>
|
||||
);
|
||||
});
|
134
ts/components/conversation/MandatoryProfileSharingActions.tsx
Normal file
134
ts/components/conversation/MandatoryProfileSharingActions.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ContactName, PropsType as ContactNameProps } from './ContactName';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
Props as MessageRequestActionsConfirmationProps,
|
||||
} from './MessageRequestActionsConfirmation';
|
||||
import { Intl } from '../Intl';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
firstName?: string;
|
||||
onAccept(): unknown;
|
||||
} & Omit<ContactNameProps, 'module' | 'i18n'> &
|
||||
Pick<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
'conversationType' | 'onBlock' | 'onBlockAndDelete' | 'onDelete'
|
||||
>;
|
||||
|
||||
export const MandatoryProfileSharingActions = ({
|
||||
conversationType,
|
||||
firstName,
|
||||
i18n,
|
||||
name,
|
||||
onAccept,
|
||||
onBlock,
|
||||
onBlockAndDelete,
|
||||
onDelete,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
}: Props): JSX.Element => {
|
||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mrState !== MessageRequestState.default ? (
|
||||
<MessageRequestActionsConfirmation
|
||||
i18n={i18n}
|
||||
onBlock={onBlock}
|
||||
onBlockAndDelete={onBlockAndDelete}
|
||||
onUnblock={() => {
|
||||
throw new Error(
|
||||
'Should not be able to unblock from MandatoryProfileSharingActions'
|
||||
);
|
||||
}}
|
||||
onDelete={onDelete}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
title={title}
|
||||
conversationType={conversationType}
|
||||
state={mrState}
|
||||
onChangeState={setMrState}
|
||||
/>
|
||||
) : null}
|
||||
<div className="module-message-request-actions">
|
||||
<p className="module-message-request-actions__message">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={`MessageRequests--profile-sharing--${conversationType}`}
|
||||
components={{
|
||||
firstName: (
|
||||
<strong
|
||||
key="name"
|
||||
className="module-message-request-actions__message__name"
|
||||
>
|
||||
<ContactName
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
title={firstName || title}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
learnMore: (
|
||||
<a
|
||||
href="https://support.signal.org/hc/articles/360007459591"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="module-message-request-actions__message__learn-more"
|
||||
>
|
||||
{i18n('MessageRequests--learn-more')}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div className="module-message-request-actions__buttons">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.blocking);
|
||||
}}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-message-request-actions__buttons__button',
|
||||
'module-message-request-actions__buttons__button--deny'
|
||||
)}
|
||||
>
|
||||
{i18n('MessageRequests--block')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.deleting);
|
||||
}}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-message-request-actions__buttons__button',
|
||||
'module-message-request-actions__buttons__button--deny'
|
||||
)}
|
||||
>
|
||||
{i18n('MessageRequests--delete')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAccept}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-message-request-actions__buttons__button',
|
||||
'module-message-request-actions__buttons__button--accept'
|
||||
)}
|
||||
>
|
||||
{i18n('MessageRequests--continue')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -45,6 +45,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
|
||||
bodyRanges: overrideProps.bodyRanges,
|
||||
canReply: true,
|
||||
canDownload: true,
|
||||
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
collapseMetadata: overrideProps.collapseMetadata,
|
||||
|
|
|
@ -137,6 +137,7 @@ export type PropsData = {
|
|||
deletedForEveryone?: boolean;
|
||||
|
||||
canReply: boolean;
|
||||
canDownload: boolean;
|
||||
canDeleteForEveryone: boolean;
|
||||
bodyRanges?: BodyRangesType;
|
||||
};
|
||||
|
@ -1159,6 +1160,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
): JSX.Element | null {
|
||||
const {
|
||||
attachments,
|
||||
canDownload,
|
||||
canReply,
|
||||
direction,
|
||||
disableMenu,
|
||||
|
@ -1294,7 +1296,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
)}
|
||||
>
|
||||
{canReply ? reactButton : null}
|
||||
{canReply ? downloadButton : null}
|
||||
{canDownload ? downloadButton : null}
|
||||
{canReply ? replyButton : null}
|
||||
{menuButton}
|
||||
</div>
|
||||
|
@ -1328,6 +1330,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
public renderContextMenu(triggerId: string): JSX.Element {
|
||||
const {
|
||||
attachments,
|
||||
canDownload,
|
||||
canReply,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
|
@ -1349,7 +1352,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const menu = (
|
||||
<ContextMenu id={triggerId}>
|
||||
{!isSticker &&
|
||||
{canDownload &&
|
||||
!isSticker &&
|
||||
!multipleAttachments &&
|
||||
!isTapToView &&
|
||||
attachments &&
|
||||
|
|
|
@ -17,6 +17,7 @@ const defaultMessage: MessageProps = {
|
|||
authorTitle: 'Max',
|
||||
canReply: true,
|
||||
canDeleteForEveryone: true,
|
||||
canDownload: true,
|
||||
clearSelectedMessage: () => null,
|
||||
conversationId: 'my-convo',
|
||||
conversationType: 'direct',
|
||||
|
|
|
@ -20,6 +20,7 @@ const defaultMessageProps: MessagesProps = {
|
|||
authorTitle: 'Person X',
|
||||
canReply: true,
|
||||
canDeleteForEveryone: true,
|
||||
canDownload: true,
|
||||
clearSelectedMessage: () => null,
|
||||
conversationId: 'conversationId',
|
||||
conversationType: 'direct', // override
|
||||
|
|
4
ts/model-types.d.ts
vendored
4
ts/model-types.d.ts
vendored
|
@ -84,7 +84,7 @@ export type MessageAttributesType = {
|
|||
referencedMessageNotFound: boolean;
|
||||
text: string;
|
||||
} | null;
|
||||
reactions: Array<{ fromId: string; emoji: unknown; timestamp: unknown }>;
|
||||
reactions: Array<{ fromId: string; emoji: string; timestamp: number }>;
|
||||
read_by: Array<string | null>;
|
||||
requiredProtocolVersion: number;
|
||||
sent: boolean;
|
||||
|
@ -141,7 +141,7 @@ export type ConversationAttributesType = {
|
|||
accessKey: string | null;
|
||||
addedBy?: string;
|
||||
capabilities: { uuid: string };
|
||||
color?: ColorType;
|
||||
color?: string;
|
||||
discoveredUnregisteredAt: number;
|
||||
draftAttachments: Array<unknown>;
|
||||
draftTimestamp: number | null;
|
||||
|
|
|
@ -239,9 +239,16 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
|
||||
// Keep props ready
|
||||
this.generateProps = () => {
|
||||
// This is to prevent race conditions on startup; Conversation models are created
|
||||
// but the full window.ConversationController.load() sequence isn't complete.
|
||||
if (!window.ConversationController.isFetchComplete()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cachedProps = this.getProps();
|
||||
};
|
||||
this.on('change', this.generateProps);
|
||||
|
||||
this.generateProps();
|
||||
}
|
||||
|
||||
|
@ -1027,19 +1034,13 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
});
|
||||
}
|
||||
|
||||
format(): ConversationType | null | undefined {
|
||||
format(): ConversationType {
|
||||
this.cachedProps = this.cachedProps || this.getProps();
|
||||
|
||||
return this.cachedProps;
|
||||
}
|
||||
|
||||
getProps(): ConversationType | null {
|
||||
// This is to prevent race conditions on startup; Conversation models are created
|
||||
// but the full window.ConversationController.load() sequence isn't complete. So, we
|
||||
// don't cache props on create, but we do later when load() calls generateProps()
|
||||
// for us.
|
||||
if (!window.ConversationController.isFetchComplete()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getProps(): ConversationType {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const color = this.getColor()!;
|
||||
|
||||
|
@ -1064,6 +1065,13 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
'desktop.messageRequests'
|
||||
);
|
||||
|
||||
let groupVersion: undefined | 1 | 2;
|
||||
if (this.isGroupV1()) {
|
||||
groupVersion = 1;
|
||||
} else if (this.isGroupV2()) {
|
||||
groupVersion = 2;
|
||||
}
|
||||
|
||||
// TODO: DESKTOP-720
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
const result = {
|
||||
|
@ -1078,12 +1086,14 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
draftPreview,
|
||||
draftText,
|
||||
firstName: this.get('profileName')!,
|
||||
groupVersion,
|
||||
inboxPosition,
|
||||
isAccepted: this.getAccepted(),
|
||||
isArchived: this.get('isArchived')!,
|
||||
isBlocked: this.isBlocked(),
|
||||
isMe: this.isMe(),
|
||||
isPinned: this.get('isPinned'),
|
||||
isMissingMandatoryProfileSharing: this.isMissingRequiredProfileSharing(),
|
||||
isVerified: this.isVerified(),
|
||||
lastMessage: {
|
||||
status: this.get('lastMessageStatus')!,
|
||||
|
@ -1091,6 +1101,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
deletedForEveryone: this.get('lastMessageDeletedForEveryone')!,
|
||||
},
|
||||
lastUpdated: this.get('timestamp')!,
|
||||
|
||||
membersCount: this.isPrivate()
|
||||
? undefined
|
||||
: (this.get('membersV2')! || this.get('members')! || []).length,
|
||||
|
@ -1693,6 +1704,18 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return this.get('messageRequestResponseType') || 0;
|
||||
}
|
||||
|
||||
isMissingRequiredProfileSharing(): boolean {
|
||||
const mandatoryProfileSharingEnabled = window.Signal.RemoteConfig.isEnabled(
|
||||
'desktop.mandatoryProfileSharing'
|
||||
);
|
||||
|
||||
if (!mandatoryProfileSharingEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this.get('profileSharing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this conversation should be considered "accepted" in terms
|
||||
* of message requests
|
||||
|
@ -3864,20 +3887,19 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
}
|
||||
|
||||
const { migrateColor } = Util;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return migrateColor(this.get('color')!);
|
||||
return migrateColor(this.get('color'));
|
||||
}
|
||||
|
||||
getAvatarPath(): string | null {
|
||||
getAvatarPath(): string | undefined {
|
||||
const avatar = this.isMe()
|
||||
? this.get('profileAvatar') || this.get('avatar')
|
||||
: this.get('avatar') || this.get('profileAvatar');
|
||||
|
||||
if (avatar && avatar.path) {
|
||||
return getAbsoluteAttachmentPath(avatar.path);
|
||||
if (!avatar || !avatar.path) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return null;
|
||||
return getAbsoluteAttachmentPath(avatar.path);
|
||||
}
|
||||
|
||||
canChangeTimer(): boolean {
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
LastMessageStatus,
|
||||
ConversationType,
|
||||
} from '../state/ducks/conversations';
|
||||
import { PropsData } from '../components/conversation/Message';
|
||||
import { CallbackResultType } from '../textsecure/SendMessage';
|
||||
import { BodyRangesType } from '../types/Util';
|
||||
import { PropsDataType as GroupsV2Props } from '../components/conversation/GroupV2Change';
|
||||
|
@ -59,7 +60,9 @@ const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
|
|||
|
||||
const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
|
||||
const { bytesFromString } = window.Signal.Crypto;
|
||||
const PLACEHOLDER_CONTACT = {
|
||||
const PLACEHOLDER_CONTACT: Pick<ConversationType, 'title' | 'type' | 'id'> = {
|
||||
id: 'placeholder-contact',
|
||||
type: 'direct',
|
||||
title: window.i18n('unknownContact'),
|
||||
};
|
||||
|
||||
|
@ -694,26 +697,26 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
.map(attachment => this.getPropsForAttachment(attachment));
|
||||
}
|
||||
|
||||
getPropsForMessage(): WhatIsThis {
|
||||
// Note: interactionMode is mixed in via selectors/conversations._messageSelector
|
||||
getPropsForMessage(): Omit<PropsData, 'interactionMode'> {
|
||||
const sourceId = this.getContactId();
|
||||
const contact = this.findAndFormatContact(sourceId);
|
||||
const contactModel = this.findContact(sourceId);
|
||||
|
||||
const authorColor = contactModel ? contactModel.getColor() : null;
|
||||
const authorAvatarPath = contactModel ? contactModel.getAvatarPath() : null;
|
||||
const authorColor = contactModel ? contactModel.getColor() : undefined;
|
||||
const authorAvatarPath = contactModel
|
||||
? contactModel.getAvatarPath()
|
||||
: undefined;
|
||||
|
||||
const expirationLength = this.get('expireTimer') * 1000;
|
||||
const expireTimerStart = this.get('expirationStartTimestamp');
|
||||
const expirationTimestamp =
|
||||
expirationLength && expireTimerStart
|
||||
? expireTimerStart + expirationLength
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
const conversation = this.getConversation();
|
||||
const isGroup = conversation && !conversation.isPrivate();
|
||||
const conversationAccepted = Boolean(
|
||||
conversation && conversation.getAccepted()
|
||||
);
|
||||
const sticker = this.get('sticker');
|
||||
|
||||
const isTapToView = this.isTapToView();
|
||||
|
@ -739,7 +742,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
textPending: this.get('bodyPending'),
|
||||
id: this.id,
|
||||
conversationId: this.get('conversationId'),
|
||||
conversationAccepted,
|
||||
isSticker: Boolean(sticker),
|
||||
direction: this.isIncoming() ? 'incoming' : 'outgoing',
|
||||
timestamp: this.get('sent_at'),
|
||||
|
@ -747,6 +749,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
contact: this.getPropsForEmbeddedContact(),
|
||||
canReply: this.canReply(),
|
||||
canDeleteForEveryone: this.canDeleteForEveryone(),
|
||||
canDownload: this.canDownload(),
|
||||
authorTitle: contact.title,
|
||||
authorColor,
|
||||
authorName: contact.name,
|
||||
|
@ -801,7 +804,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
// Dependencies of prop-generation functions
|
||||
findAndFormatContact(
|
||||
identifier?: string
|
||||
): Partial<ConversationType> & Pick<ConversationType, 'title'> {
|
||||
): Partial<ConversationType> &
|
||||
Pick<ConversationType, 'title' | 'id' | 'type'> {
|
||||
if (!identifier) {
|
||||
return PLACEHOLDER_CONTACT;
|
||||
}
|
||||
|
@ -824,6 +828,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
});
|
||||
|
||||
return {
|
||||
id: 'phone-only',
|
||||
type: 'direct',
|
||||
title: phoneNumber,
|
||||
phoneNumber,
|
||||
};
|
||||
|
@ -839,9 +845,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
createNonBreakingLastSeparator(text: string): string | null {
|
||||
createNonBreakingLastSeparator(text: string): string | undefined {
|
||||
if (!text) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nbsp = '\xa0';
|
||||
|
@ -859,7 +865,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return this.get('type') === 'incoming';
|
||||
}
|
||||
|
||||
getMessagePropStatus(): LastMessageStatus | null {
|
||||
getMessagePropStatus(): LastMessageStatus | undefined {
|
||||
const sent = this.get('sent');
|
||||
const sentTo = this.get('sent_to') || [];
|
||||
|
||||
|
@ -870,7 +876,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return 'error';
|
||||
}
|
||||
if (!this.isOutgoing()) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const readBy = this.get('read_by') || [];
|
||||
|
@ -2010,34 +2016,50 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return true;
|
||||
}
|
||||
|
||||
canDownload(): boolean {
|
||||
const conversation = this.getConversation();
|
||||
const isAccepted = Boolean(conversation && conversation.getAccepted());
|
||||
|
||||
if (this.isOutgoing()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isAccepted;
|
||||
}
|
||||
|
||||
canReply(): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const isAccepted = this.getConversation()!.getAccepted();
|
||||
const conversation = this.getConversation();
|
||||
const errors = this.get('errors');
|
||||
const isOutgoing = this.get('type') === 'outgoing';
|
||||
const numDelivered = this.get('delivered');
|
||||
|
||||
// Case 1: We cannot reply if we have accepted the message request
|
||||
if (!isAccepted) {
|
||||
// Case 1: If mandatory profile sharing is enabled, and we haven't shared yet, then
|
||||
// we can't reply.
|
||||
if (conversation?.isMissingRequiredProfileSharing()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 2: We cannot reply if this message is deleted for everyone
|
||||
// Case 2: We cannot reply if we have accepted the message request
|
||||
if (!conversation?.getAccepted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 3: We cannot reply if this message is deleted for everyone
|
||||
if (this.get('deletedForEveryone')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 3: We can reply if this is outgoing and delievered to at least one recipient
|
||||
// Case 4: We can reply if this is outgoing and delievered to at least one recipient
|
||||
if (isOutgoing && numDelivered > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Case 4: We can reply if there are no errors
|
||||
// Case 5: We can reply if there are no errors
|
||||
if (!errors || (errors && errors.length === 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Case 5: default
|
||||
// Case 6: default
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -117,14 +117,7 @@ export class CallingClass {
|
|||
return;
|
||||
}
|
||||
|
||||
const conversationProps = conversation.cachedProps;
|
||||
|
||||
if (!conversationProps) {
|
||||
window.log.error(
|
||||
'CallingClass.startCallingLobby(): No conversation props?'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const conversationProps = conversation.format();
|
||||
|
||||
window.log.info('CallingClass.startCallingLobby(): Starting lobby');
|
||||
this.uxActions.showCallLobby({
|
||||
|
@ -829,10 +822,7 @@ export class CallingClass {
|
|||
conversation: ConversationModel,
|
||||
call: Call
|
||||
): CallDetailsType {
|
||||
const conversationProps = conversation.cachedProps;
|
||||
if (!conversationProps) {
|
||||
throw new Error('getAcceptedCallDetails: No conversation props?');
|
||||
}
|
||||
const conversationProps = conversation.format();
|
||||
|
||||
return {
|
||||
...conversationProps,
|
||||
|
|
|
@ -75,6 +75,8 @@ export type ConversationType = {
|
|||
draftText?: string | null;
|
||||
draftPreview?: string;
|
||||
|
||||
groupVersion?: 1 | 2;
|
||||
isMissingMandatoryProfileSharing?: boolean;
|
||||
messageRequestsEnabled?: boolean;
|
||||
acceptedMessageRequest?: boolean;
|
||||
};
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import { LocalizerType } from '../types/Util';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export type ProfileNameChangeType = {
|
||||
type: 'name';
|
||||
oldName: string;
|
||||
newName: string;
|
||||
};
|
||||
type ContactType = {
|
||||
title: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export function getStringForProfileChange(
|
||||
change: ProfileNameChangeType,
|
||||
changedContact: ConversationType,
|
||||
changedContact: ContactType,
|
||||
i18n: LocalizerType
|
||||
): string {
|
||||
if (change.type === 'name') {
|
||||
|
|
|
@ -13120,7 +13120,7 @@
|
|||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/components/CompositionArea.js",
|
||||
"line": " el.innerHTML = '';",
|
||||
"lineNumber": 24,
|
||||
"lineNumber": 25,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||
|
@ -13129,7 +13129,7 @@
|
|||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/components/CompositionArea.tsx",
|
||||
"line": " el.innerHTML = '';",
|
||||
"lineNumber": 78,
|
||||
"lineNumber": 81,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-03T19:23:21.195Z",
|
||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||
|
@ -13305,7 +13305,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
||||
"lineNumber": 217,
|
||||
"lineNumber": 218,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-09-08T20:19:01.913Z"
|
||||
},
|
||||
|
@ -13313,7 +13313,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||
"lineNumber": 219,
|
||||
"lineNumber": 220,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-09-08T20:19:01.913Z"
|
||||
},
|
||||
|
@ -13321,7 +13321,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " > = React.createRef();",
|
||||
"lineNumber": 223,
|
||||
"lineNumber": 224,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-08-28T19:36:40.817Z"
|
||||
},
|
||||
|
@ -13548,4 +13548,4 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-09-08T23:07:22.682Z"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { ColorType } from '../types/Colors';
|
||||
|
||||
// import { missingCaseError } from './missingCaseError';
|
||||
|
||||
type OldColorType =
|
||||
| 'amber'
|
||||
| 'blue'
|
||||
|
@ -22,9 +20,11 @@ type OldColorType =
|
|||
| 'red'
|
||||
| 'teal'
|
||||
| 'yellow'
|
||||
| 'ultramarine';
|
||||
| 'ultramarine'
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
export function migrateColor(color: OldColorType): ColorType {
|
||||
export function migrateColor(color?: OldColorType): ColorType {
|
||||
switch (color) {
|
||||
// These colors no longer exist
|
||||
case 'orange':
|
||||
|
@ -62,10 +62,6 @@ export function migrateColor(color: OldColorType): ColorType {
|
|||
case 'ultramarine':
|
||||
return color;
|
||||
|
||||
// Can uncomment this to ensure that we've covered all potential cases
|
||||
// default:
|
||||
// throw missingCaseError(color);
|
||||
|
||||
default:
|
||||
return 'grey';
|
||||
}
|
||||
|
|
|
@ -436,11 +436,12 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
: null;
|
||||
|
||||
return {
|
||||
...this.model.cachedProps,
|
||||
...this.model.format(),
|
||||
|
||||
leftGroup: this.model.get('left'),
|
||||
|
||||
disableTimerChanges:
|
||||
this.model.isMissingRequiredProfileSharing() ||
|
||||
this.model.get('left') ||
|
||||
!this.model.getAccepted() ||
|
||||
!this.model.canChangeTimer(),
|
||||
|
|
37
ts/window.d.ts
vendored
37
ts/window.d.ts
vendored
|
@ -3,6 +3,7 @@
|
|||
import * as Backbone from 'backbone';
|
||||
import * as Underscore from 'underscore';
|
||||
import { Ref } from 'react';
|
||||
import * as Util from './util';
|
||||
import {
|
||||
ConversationModelCollectionType,
|
||||
MessageModelCollectionType,
|
||||
|
@ -356,41 +357,7 @@ declare global {
|
|||
};
|
||||
VisualAttachment: any;
|
||||
};
|
||||
Util: {
|
||||
isFileDangerous: any;
|
||||
GoogleChrome: {
|
||||
isImageTypeSupported: (contentType: string) => unknown;
|
||||
isVideoTypeSupported: (contentType: string) => unknown;
|
||||
};
|
||||
downloadAttachment: (attachment: WhatIsThis) => WhatIsThis;
|
||||
getStringForProfileChange: (
|
||||
change: unknown,
|
||||
changedContact: unknown,
|
||||
i18n: unknown
|
||||
) => string;
|
||||
getTextWithMentions: (
|
||||
bodyRanges: BodyRangesType,
|
||||
text: string
|
||||
) => string;
|
||||
deleteForEveryone: (
|
||||
message: unknown,
|
||||
del: unknown,
|
||||
bool: boolean
|
||||
) => void;
|
||||
zkgroup: typeof zkgroup;
|
||||
combineNames: typeof combineNames;
|
||||
migrateColor: (color: string) => ColorType;
|
||||
createBatcher: (options: WhatIsThis) => WhatIsThis;
|
||||
Registration: {
|
||||
everDone: () => boolean;
|
||||
markDone: () => void;
|
||||
markEverDone: () => void;
|
||||
remove: () => void;
|
||||
};
|
||||
hasExpired: () => boolean;
|
||||
makeLookup: (conversations: WhatIsThis, key: string) => void;
|
||||
parseRemoteClientExpiration: (value: WhatIsThis) => WhatIsThis;
|
||||
};
|
||||
Util: typeof Util;
|
||||
LinkPreviews: {
|
||||
isMediaLinkInWhitelist: any;
|
||||
getTitleMetaTag: any;
|
||||
|
|
Loading…
Reference in a new issue