diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7f699102a3a1..68ccc0bef2ff 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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", diff --git a/js/views/safety_number_change_dialog_view.js b/js/views/safety_number_change_dialog_view.js index 6ad41c71e453..f27920901539 100644 --- a/js/views/safety_number_change_dialog_view.js +++ b/js/views/safety_number_change_dialog_view.js @@ -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(); diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 93189ddd9f4d..9556d6f851e3 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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 { diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 595a602bb7b5..952199520715 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -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; diff --git a/ts/background.ts b/ts/background.ts index 5021b0cf880e..d0e0834dca2a 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -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( diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index f853cba7c842..d4a9a167136a 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -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 ( + ); + } + return (
diff --git a/ts/components/conversation/MandatoryProfileSharingActions.stories.tsx b/ts/components/conversation/MandatoryProfileSharingActions.stories.tsx new file mode 100644 index 000000000000..6464ac5a1fea --- /dev/null +++ b/ts/components/conversation/MandatoryProfileSharingActions.stories.tsx @@ -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 ( +
+ +
+ ); + }) + .add('Group', () => { + return ( +
+ +
+ ); + }); diff --git a/ts/components/conversation/MandatoryProfileSharingActions.tsx b/ts/components/conversation/MandatoryProfileSharingActions.tsx new file mode 100644 index 000000000000..f5966c1982e5 --- /dev/null +++ b/ts/components/conversation/MandatoryProfileSharingActions.tsx @@ -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 & + 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 ? ( + { + 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} +
+

+ + + + ), + learnMore: ( + + {i18n('MessageRequests--learn-more')} + + ), + }} + /> +

+
+ + + +
+
+ + ); +}; diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index 077e6e8b9bad..fe015136c68d 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -45,6 +45,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ authorTitle: text('authorTitle', overrideProps.authorTitle || ''), bodyRanges: overrideProps.bodyRanges, canReply: true, + canDownload: true, canDeleteForEveryone: overrideProps.canDeleteForEveryone || false, clearSelectedMessage: action('clearSelectedMessage'), collapseMetadata: overrideProps.collapseMetadata, diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 4da81c99b5c7..0a4f39620dad 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -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 { ): JSX.Element | null { const { attachments, + canDownload, canReply, direction, disableMenu, @@ -1294,7 +1296,7 @@ export class Message extends React.PureComponent { )} > {canReply ? reactButton : null} - {canReply ? downloadButton : null} + {canDownload ? downloadButton : null} {canReply ? replyButton : null} {menuButton}
@@ -1328,6 +1330,7 @@ export class Message extends React.PureComponent { public renderContextMenu(triggerId: string): JSX.Element { const { attachments, + canDownload, canReply, deleteMessage, deleteMessageForEveryone, @@ -1349,7 +1352,8 @@ export class Message extends React.PureComponent { const menu = ( - {!isSticker && + {canDownload && + !isSticker && !multipleAttachments && !isTapToView && attachments && diff --git a/ts/components/conversation/MessageDetail.stories.tsx b/ts/components/conversation/MessageDetail.stories.tsx index 4e98224cffb2..9cca2eb99f6e 100644 --- a/ts/components/conversation/MessageDetail.stories.tsx +++ b/ts/components/conversation/MessageDetail.stories.tsx @@ -17,6 +17,7 @@ const defaultMessage: MessageProps = { authorTitle: 'Max', canReply: true, canDeleteForEveryone: true, + canDownload: true, clearSelectedMessage: () => null, conversationId: 'my-convo', conversationType: 'direct', diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index fd8c919c22f4..950a13ac43e7 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -20,6 +20,7 @@ const defaultMessageProps: MessagesProps = { authorTitle: 'Person X', canReply: true, canDeleteForEveryone: true, + canDownload: true, clearSelectedMessage: () => null, conversationId: 'conversationId', conversationType: 'direct', // override diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 5ab5a0afd278..71bbfeb5aeeb 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -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; 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; draftTimestamp: number | null; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index f31298ecd740..2febf59d6b67 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -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 { diff --git a/ts/models/messages.ts b/ts/models/messages.ts index d829ac7f3200..95ef6cb2bf00 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -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 = { + id: 'placeholder-contact', + type: 'direct', title: window.i18n('unknownContact'), }; @@ -694,26 +697,26 @@ export class MessageModel extends window.Backbone.Model { .map(attachment => this.getPropsForAttachment(attachment)); } - getPropsForMessage(): WhatIsThis { + // Note: interactionMode is mixed in via selectors/conversations._messageSelector + getPropsForMessage(): Omit { 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 { 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 { 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 { // Dependencies of prop-generation functions findAndFormatContact( identifier?: string - ): Partial & Pick { + ): Partial & + Pick { if (!identifier) { return PLACEHOLDER_CONTACT; } @@ -824,6 +828,8 @@ export class MessageModel extends window.Backbone.Model { }); return { + id: 'phone-only', + type: 'direct', title: phoneNumber, phoneNumber, }; @@ -839,9 +845,9 @@ export class MessageModel extends window.Backbone.Model { } // 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 { 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 { 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 { 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; } diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 2f4b6faeef39..ba8a2f299943 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -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, diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 169dd858469f..08fb2870a2d6 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -75,6 +75,8 @@ export type ConversationType = { draftText?: string | null; draftPreview?: string; + groupVersion?: 1 | 2; + isMissingMandatoryProfileSharing?: boolean; messageRequestsEnabled?: boolean; acceptedMessageRequest?: boolean; }; diff --git a/ts/util/getStringForProfileChange.ts b/ts/util/getStringForProfileChange.ts index 62f2b0338bce..ad864c4bb3d3 100644 --- a/ts/util/getStringForProfileChange.ts +++ b/ts/util/getStringForProfileChange.ts @@ -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') { diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 473ee30880c6..4fb52ec0c92e 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -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 = 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 = 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" } -] \ No newline at end of file +] diff --git a/ts/util/migrateColor.ts b/ts/util/migrateColor.ts index 75ef94729b85..83af4c1cea28 100644 --- a/ts/util/migrateColor.ts +++ b/ts/util/migrateColor.ts @@ -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'; } diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index c55496c9786e..77ac8cdf3f69 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -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(), diff --git a/ts/window.d.ts b/ts/window.d.ts index 7da4eb3e3e68..d18c75eb1914 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -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;