Modern profile sharing in 1:1 and GroupV1 groups

This commit is contained in:
Scott Nonnenberg 2020-10-16 11:31:57 -07:00
parent 60f2422e2a
commit 04b7a29229
22 changed files with 371 additions and 115 deletions

View File

@ -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",

View File

@ -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();

View File

@ -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 {

View File

@ -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;

View File

@ -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(

View File

@ -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">

View File

@ -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>
);
});

View 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>
</>
);
};

View File

@ -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,

View File

@ -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 &&

View File

@ -17,6 +17,7 @@ const defaultMessage: MessageProps = {
authorTitle: 'Max',
canReply: true,
canDeleteForEveryone: true,
canDownload: true,
clearSelectedMessage: () => null,
conversationId: 'my-convo',
conversationType: 'direct',

View File

@ -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
View File

@ -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;

View File

@ -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 {

View File

@ -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;
}

View File

@ -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,

View File

@ -75,6 +75,8 @@ export type ConversationType = {
draftText?: string | null;
draftPreview?: string;
groupVersion?: 1 | 2;
isMissingMandatoryProfileSharing?: boolean;
messageRequestsEnabled?: boolean;
acceptedMessageRequest?: boolean;
};

View File

@ -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') {

View File

@ -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"
}
]
]

View File

@ -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';
}

View File

@ -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
View File

@ -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;