diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 3f791a6ffc..74901cafb0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -761,6 +761,10 @@ "message": "Contacts", "description": "Shown to separate the types of search results" }, + "groupsHeader": { + "message": "Groups", + "description": "Shown to separate the types of search results" + }, "messagesHeader": { "message": "Messages", "description": "Shown to separate the types of search results" @@ -1905,6 +1909,10 @@ "message": "No contacts found", "description": "Label shown when there are no contacts to compose to" }, + "noConversationsFound": { + "message": "No conversations found", + "description": "Label shown when there are no conversations to compose to" + }, "chooseGroupMembers__title": { "message": "Choose members", "description": "The title for the 'choose group members' left pane screen" @@ -5130,5 +5138,19 @@ "MessageAudio--slider": { "message": "Playback time of audio attachment", "description": "Aria label for audio attachment's playback time slider" + }, + "emptyInboxMessage": { + "message": "Click the $composeIcon$ above and search for your contacts or groups to message.", + "description": "Shown in the left-pane when the inbox is empty", + "placeholders": { + "composeIcon": { + "content": "$1", + "example": "compose button" + } + } + }, + "composeIcon": { + "message": "compose button", + "description": "Shown in the left-pane when the inbox is empty. Describes the button that composes a new message." } } diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index f6db2621e6..050485643e 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -111,7 +111,8 @@ }, render_attributes: { welcomeToSignal: i18n('welcomeToSignal'), - selectAContact: i18n('selectAContact'), + // TODO DESKTOP-1451: add back the selectAContact message + selectAContact: '', }, events: { click: 'onClick', diff --git a/js/views/install_view.js b/js/views/install_view.js index 58c4d796d5..8317bb5a5c 100644 --- a/js/views/install_view.js +++ b/js/views/install_view.js @@ -35,7 +35,6 @@ initialize(options = {}) { window.readyForUpdates(); - this.didLink = false; this.selectStep(Steps.SCAN_QR_CODE); this.connect(); this.on('disconnected', this.reconnect); @@ -197,7 +196,7 @@ this.selectStep(Steps.PROGRESS_BAR); const finish = () => { - this.didLink = true; + window.Signal.Util.postLinkExperience.start(); return resolve(name); }; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index f53297cb13..a1db4661c7 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -7401,6 +7401,36 @@ button.module-image__border-overlay:focus { position: relative; } +.module-left-pane__empty { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + padding: 0 32px; + text-align: center; + + &--composer_icon { + align-items: center; + background-color: $color-gray-05; + border-radius: 100%; + display: inline-flex; + height: 28px; + justify-content: center; + margin-bottom: -2px; + margin-left: 4px; + vertical-align: bottom; + width: 28px; + + &--icon { + $icon: '../images/icons/v2/compose-outline-24.svg'; + @include color-svg($icon, $color-gray-90); + display: inline-block; + height: 16px; + width: 16px; + } + } +} + .module-left-pane__header { flex-grow: 0; flex-shrink: 0; diff --git a/ts/background.ts b/ts/background.ts index b7f700d976..b1611ae1d5 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -2398,13 +2398,6 @@ export async function startApp(): Promise { }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const conversation = window.ConversationController.get(detailsId)!; - let activeAt = conversation.get('active_at'); - - // The idea is to make any new contact show up in the left pane. If - // activeAt is null, then this contact has been purposefully hidden. - if (activeAt !== null) { - activeAt = activeAt || Date.now(); - } if (details.profileKey) { const profileKey = window.Signal.Crypto.arrayBufferToBase64( @@ -2424,7 +2417,6 @@ export async function startApp(): Promise { conversation.set({ name: details.name, color: details.color, - active_at: activeAt, inbox_position: details.inboxPosition, }); @@ -2480,8 +2472,7 @@ export async function startApp(): Promise { await onVerified(verifiedEvent); } - const { appView } = window.owsDesktopApp; - if (appView && appView.installView && appView.installView.didLink) { + if (window.Signal.Util.postLinkExperience.isActive()) { window.log.info( 'onContactReceived: Adding the message history disclaimer on link' ); @@ -2538,13 +2529,6 @@ export async function startApp(): Promise { } as WhatIsThis; if (details.active) { - const activeAt = conversation.get('active_at'); - - // The idea is to make any new group show up in the left pane. If - // activeAt is null, then this group has been purposefully hidden. - if (activeAt !== null) { - updates.active_at = activeAt || Date.now(); - } updates.left = false; } else { updates.left = true; @@ -2575,8 +2559,7 @@ export async function startApp(): Promise { window.Signal.Data.updateConversation(conversation.attributes); - const { appView } = window.owsDesktopApp; - if (appView && appView.installView && appView.installView.didLink) { + if (window.Signal.Util.postLinkExperience.isActive()) { window.log.info( 'onGroupReceived: Adding the message history disclaimer on link' ); @@ -2846,9 +2829,6 @@ export async function startApp(): Promise { // Finally create the V2 group normally const conversationId = window.ConversationController.ensureGroup(id, { - // Note: We don't set active_at, because we don't want the group to show until - // we have information about it beyond these initial details. - // see maybeUpdateGroup(). groupVersion: 2, masterKey: message.groupV2.masterKey, secretParams: message.groupV2.secretParams, diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index 1eaf1084a2..797960b691 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -35,6 +35,25 @@ const defaultConversations: Array = [ }, ]; +const defaultGroups: Array = [ + { + id: 'biking-group', + isSelected: false, + lastUpdated: Date.now(), + markedUnread: false, + title: 'Mtn Biking Arizona 🚵☀️⛰', + type: 'group', + }, + { + id: 'dance-group', + isSelected: false, + lastUpdated: Date.now(), + markedUnread: false, + title: 'Are we dancers? 💃', + type: 'group', + }, +]; + const defaultArchivedConversations: Array = [ { id: 'michelle-archive-convo', @@ -352,12 +371,13 @@ story.add('Archive: archived conversations', () => ( // Compose stories -story.add('Compose: no contacts', () => ( +story.add('Compose: no contacts or groups', () => ( ( /> )); -story.add('Compose: some contacts, no search term', () => ( +story.add('Compose: some contacts, no groups, no search term', () => ( ( /> )); -story.add('Compose: some contacts with a search term', () => ( +story.add('Compose: some contacts, no groups, with a search term', () => ( +)); + +story.add('Compose: some groups, no contacts, no search term', () => ( + +)); + +story.add('Compose: some groups, no contacts, with search term', () => ( + +)); + +story.add('Compose: some contacts, some groups, no search term', () => ( + +)); + +story.add('Compose: some contacts, some groups, with a search term', () => ( + diff --git a/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx b/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx index 96e94356e3..d6488180c4 100644 --- a/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx +++ b/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.tsx @@ -14,7 +14,7 @@ import { LocalizerType } from '../../../../types/Util'; import { assert } from '../../../../util/assert'; import { getOwn } from '../../../../util/getOwn'; import { missingCaseError } from '../../../../util/missingCaseError'; -import { filterAndSortContacts } from '../../../../util/filterAndSortContacts'; +import { filterAndSortConversations } from '../../../../util/filterAndSortConversations'; import { ConversationType } from '../../../../state/ducks/conversations'; import { ModalHost } from '../../../ModalHost'; import { ContactPills } from '../../../ContactPills'; @@ -72,13 +72,13 @@ export const ChooseGroupMembersModal: FunctionComponent = ({ const canContinue = Boolean(selectedContacts.length); const [filteredContacts, setFilteredContacts] = useState( - filterAndSortContacts(candidateContacts, '') + filterAndSortConversations(candidateContacts, '') ); const normalizedSearchTerm = searchTerm.trim(); useEffect(() => { const timeout = setTimeout(() => { setFilteredContacts( - filterAndSortContacts(candidateContacts, normalizedSearchTerm) + filterAndSortConversations(candidateContacts, normalizedSearchTerm) ); }, 200); return () => { diff --git a/ts/components/leftPane/LeftPaneComposeHelper.tsx b/ts/components/leftPane/LeftPaneComposeHelper.tsx index 82e56b32c5..72641cd57c 100644 --- a/ts/components/leftPane/LeftPaneComposeHelper.tsx +++ b/ts/components/leftPane/LeftPaneComposeHelper.tsx @@ -7,6 +7,7 @@ import { PhoneNumber } from 'google-libphonenumber'; import { LeftPaneHelper } from './LeftPaneHelper'; import { Row, RowType } from '../ConversationList'; import { PropsDataType as ContactListItemPropsType } from '../conversationList/ContactListItem'; +import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem'; import { LocalizerType } from '../../types/Util'; import { instance as phoneNumberInstance, @@ -18,6 +19,7 @@ import { isStorageWriteFeatureEnabled } from '../../storage/isFeatureEnabled'; export type LeftPaneComposePropsType = { composeContacts: ReadonlyArray; + composeGroups: ReadonlyArray; regionCode: string; searchTerm: string; }; @@ -35,20 +37,24 @@ export class LeftPaneComposeHelper extends LeftPaneHelper< > { private readonly composeContacts: ReadonlyArray; + private readonly composeGroups: ReadonlyArray; + private readonly searchTerm: string; private readonly phoneNumber: undefined | PhoneNumber; constructor({ composeContacts, + composeGroups, regionCode, searchTerm, }: Readonly) { super(); - this.composeContacts = composeContacts; this.searchTerm = searchTerm; this.phoneNumber = parsePhoneNumber(searchTerm, regionCode); + this.composeGroups = composeGroups; + this.composeContacts = composeContacts; } getHeaderContents({ @@ -103,7 +109,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper< {this.getRowCount() ? null : (
- {i18n('noContactsFound')} + {i18n('noConversationsFound')}
)} @@ -111,62 +117,89 @@ export class LeftPaneComposeHelper extends LeftPaneHelper< } getRowCount(): number { - let result = this.composeContacts.length; + let result = this.composeContacts.length + this.composeGroups.length; if (this.hasTopButton()) { result += 1; } if (this.hasContactsHeader()) { result += 1; } + if (this.hasGroupsHeader()) { + result += 1; + } + return result; } - getRow(rowIndex: number): undefined | Row { - if (rowIndex === 0) { - const topButton = this.getTopButton(); - switch (topButton) { - case TopButton.None: - break; - case TopButton.StartNewConversation: - assert( - this.phoneNumber, - 'LeftPaneComposeHelper: we should have a phone number if the top button is "Start new conversation"' - ); - return { - type: RowType.StartNewConversation, - phoneNumber: phoneNumberInstance.format( + getRow(actualRowIndex: number): undefined | Row { + let virtualRowIndex = actualRowIndex; + if (this.hasTopButton()) { + if (virtualRowIndex === 0) { + const topButton = this.getTopButton(); + switch (topButton) { + case TopButton.None: + break; + case TopButton.StartNewConversation: + assert( this.phoneNumber, - PhoneNumberFormat.E164 - ), - }; - case TopButton.CreateNewGroup: - return { type: RowType.CreateNewGroup }; - default: - throw missingCaseError(topButton); + 'LeftPaneComposeHelper: we should have a phone number if the top button is "Start new conversation"' + ); + return { + type: RowType.StartNewConversation, + phoneNumber: phoneNumberInstance.format( + this.phoneNumber, + PhoneNumberFormat.E164 + ), + }; + case TopButton.CreateNewGroup: + return { type: RowType.CreateNewGroup }; + default: + throw missingCaseError(topButton); + } } + + virtualRowIndex -= 1; } - if (rowIndex === 1 && this.hasContactsHeader()) { + if (this.hasContactsHeader()) { + if (virtualRowIndex === 0) { + return { + type: RowType.Header, + i18nKey: 'contactsHeader', + }; + } + + virtualRowIndex -= 1; + + const contact = this.composeContacts[virtualRowIndex]; + if (contact) { + return { + type: RowType.Contact, + contact, + }; + } + + virtualRowIndex -= this.composeContacts.length; + } + + if (this.hasGroupsHeader()) { + if (virtualRowIndex === 0) { + return { + type: RowType.Header, + i18nKey: 'groupsHeader', + }; + } + + virtualRowIndex -= 1; + + const group = this.composeGroups[virtualRowIndex]; return { - type: RowType.Header, - i18nKey: 'contactsHeader', + type: RowType.Conversation, + conversation: group, }; } - let contactIndex: number; - if (this.hasTopButton()) { - contactIndex = rowIndex - 2; - } else { - contactIndex = rowIndex; - } - - const contact = this.composeContacts[contactIndex]; - return contact - ? { - type: RowType.Contact, - contact, - } - : undefined; + return undefined; } // This is deliberately unimplemented because these keyboard shortcuts shouldn't work in @@ -183,10 +216,17 @@ export class LeftPaneComposeHelper extends LeftPaneHelper< return undefined; } - shouldRecomputeRowHeights(old: Readonly): boolean { + shouldRecomputeRowHeights( + exProps: Readonly + ): boolean { + const prev = new LeftPaneComposeHelper(exProps); + const currHeaderIndices = this.getHeaderIndices(); + const prevHeaderIndices = prev.getHeaderIndices(); + return ( - this.hasContactsHeader() !== - new LeftPaneComposeHelper(old).hasContactsHeader() + currHeaderIndices.top !== prevHeaderIndices.top || + currHeaderIndices.contact !== prevHeaderIndices.contact || + currHeaderIndices.group !== prevHeaderIndices.group ); } @@ -205,7 +245,39 @@ export class LeftPaneComposeHelper extends LeftPaneHelper< } private hasContactsHeader(): boolean { - return this.hasTopButton() && Boolean(this.composeContacts.length); + return Boolean(this.composeContacts.length); + } + + private hasGroupsHeader(): boolean { + return Boolean(this.composeGroups.length); + } + + private getHeaderIndices(): { + top?: number; + contact?: number; + group?: number; + } { + let top: number | undefined; + let contact: number | undefined; + let group: number | undefined; + let rowCount = 0; + if (this.hasTopButton()) { + top = 0; + rowCount += 1; + } + if (this.composeContacts.length) { + contact = rowCount; + rowCount += this.composeContacts.length; + } + if (this.composeGroups.length) { + group = rowCount; + } + + return { + top, + contact, + group, + }; } } diff --git a/ts/components/leftPane/LeftPaneInboxHelper.ts b/ts/components/leftPane/LeftPaneInboxHelper.tsx similarity index 87% rename from ts/components/leftPane/LeftPaneInboxHelper.ts rename to ts/components/leftPane/LeftPaneInboxHelper.tsx index b461aee0ef..ba4d3ffee6 100644 --- a/ts/components/leftPane/LeftPaneInboxHelper.ts +++ b/ts/components/leftPane/LeftPaneInboxHelper.tsx @@ -2,11 +2,14 @@ // SPDX-License-Identifier: AGPL-3.0-only import { last } from 'lodash'; +import React, { ReactChild } from 'react'; +import { Intl } from '../Intl'; import { LeftPaneHelper, ToFindType } from './LeftPaneHelper'; import { getConversationInDirection } from './getConversationInDirection'; import { Row, RowType } from '../ConversationList'; import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem'; +import { LocalizerType } from '../../types/Util'; export type LeftPaneInboxPropsType = { conversations: ReadonlyArray; @@ -56,6 +59,33 @@ export class LeftPaneInboxHelper extends LeftPaneHelper< ); } + getPreRowsNode({ + i18n, + }: Readonly<{ i18n: LocalizerType }>): null | ReactChild { + if (this.getRowCount() === 0) { + return ( +
+
+ + {i18n('composeIcon')} + + + + , + ]} + /> +
+
+ ); + } + + return null; + } + getRow(rowIndex: number): undefined | Row { const { conversations, archivedConversations, pinnedConversations } = this; diff --git a/ts/groups.ts b/ts/groups.ts index 4dfa4cd998..f199825074 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -2598,7 +2598,8 @@ type MaybeUpdatePropsType = { }; export async function waitThenMaybeUpdateGroup( - options: MaybeUpdatePropsType + options: MaybeUpdatePropsType, + { viaSync = false } = {} ): Promise { // First wait to process all incoming messages on the websocket await window.waitForEmptyEventQueue(); @@ -2609,7 +2610,7 @@ export async function waitThenMaybeUpdateGroup( await conversation.queueJob(async () => { try { // And finally try to update the group - await maybeUpdateGroup(options); + await maybeUpdateGroup(options, { viaSync }); } catch (error) { window.log.error( `waitThenMaybeUpdateGroup/${conversation.idForLogging()}: maybeUpdateGroup failure:`, @@ -2619,14 +2620,17 @@ export async function waitThenMaybeUpdateGroup( }); } -export async function maybeUpdateGroup({ - conversation, - dropInitialJoinMessage, - groupChangeBase64, - newRevision, - receivedAt, - sentAt, -}: MaybeUpdatePropsType): Promise { +export async function maybeUpdateGroup( + { + conversation, + dropInitialJoinMessage, + groupChangeBase64, + newRevision, + receivedAt, + sentAt, + }: MaybeUpdatePropsType, + { viaSync = false } = {} +): Promise { const logId = conversation.idForLogging(); try { @@ -2641,7 +2645,10 @@ export async function maybeUpdateGroup({ dropInitialJoinMessage, }); - await updateGroup({ conversation, receivedAt, sentAt, updates }); + await updateGroup( + { conversation, receivedAt, sentAt, updates }, + { viaSync } + ); } catch (error) { window.log.error( `maybeUpdateGroup/${logId}: Failed to update group:`, @@ -2651,17 +2658,20 @@ export async function maybeUpdateGroup({ } } -async function updateGroup({ - conversation, - receivedAt, - sentAt, - updates, -}: { - conversation: ConversationModel; - receivedAt?: number; - sentAt?: number; - updates: UpdatesResultType; -}): Promise { +async function updateGroup( + { + conversation, + receivedAt, + sentAt, + updates, + }: { + conversation: ConversationModel; + receivedAt?: number; + sentAt?: number; + updates: UpdatesResultType; + }, + { viaSync = false } = {} +): Promise { const { newAttributes, groupChangeMessages, members } = updates; const startingRevision = conversation.get('revision'); @@ -2684,15 +2694,21 @@ async function updateGroup({ const previousId = conversation.get('groupId'); const idChanged = previousId && previousId !== newAttributes.groupId; + // We force this conversation into the left pane if this is the first time we've + // fetched data about it, and we were able to fetch its name. Nobody likes to see + // Unknown Group in the left pane. + let activeAt = null; + if (viaSync) { + activeAt = null; + } else if ((isInitialDataFetch || justJoinedGroup) && newAttributes.name) { + activeAt = initialSentAt; + } else { + activeAt = newAttributes.active_at; + } + conversation.set({ ...newAttributes, - // We force this conversation into the left pane if this is the first time we've - // fetched data about it, and we were able to fetch its name. Nobody likes to see - // Unknown Group in the left pane. - active_at: - (isInitialDataFetch || justJoinedGroup) && newAttributes.name - ? initialSentAt - : newAttributes.active_at, + active_at: activeAt, temporaryMemberCount: isInGroup ? undefined : newAttributes.temporaryMemberCount, diff --git a/ts/services/storage.ts b/ts/services/storage.ts index ce39df57f8..8bbd831c5b 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -1035,6 +1035,7 @@ async function sync(): Promise { ); } + window.Signal.Util.postLinkExperience.stop(); window.log.info('storageService.sync: complete'); return manifest; } diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 0dfb43d989..b26fe8065f 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -685,10 +685,13 @@ export async function mergeGroupV2Record( // We don't await this because this could take a very long time, waiting for queues to // empty, etc. - waitThenMaybeUpdateGroup({ - conversation, - dropInitialJoinMessage, - }); + waitThenMaybeUpdateGroup( + { + conversation, + dropInitialJoinMessage, + }, + { viaSync: true } + ); } return hasPendingChanges; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 1a562ba171..915430f59b 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -259,11 +259,11 @@ type ComposerGroupCreationState = { type ComposerStateType = | { step: ComposerStep.StartDirectConversation; - contactSearchTerm: string; + searchTerm: string; } | ({ step: ComposerStep.ChooseGroupMembers; - contactSearchTerm: string; + searchTerm: string; cantAddContactIdForModal: undefined | string; } & ComposerGroupCreationState) | ({ @@ -529,7 +529,7 @@ type SetComposeGroupNameActionType = { }; type SetComposeSearchTermActionType = { type: 'SET_COMPOSE_SEARCH_TERM'; - payload: { contactSearchTerm: string }; + payload: { searchTerm: string }; }; type SetRecentMediaItemsActionType = { type: 'SET_RECENT_MEDIA_ITEMS'; @@ -1012,11 +1012,11 @@ function setComposeGroupName(groupName: string): SetComposeGroupNameActionType { } function setComposeSearchTerm( - contactSearchTerm: string + searchTerm: string ): SetComposeSearchTermActionType { return { type: 'SET_COMPOSE_SEARCH_TERM', - payload: { contactSearchTerm }, + payload: { searchTerm }, }; } @@ -2117,7 +2117,7 @@ export function reducer( showArchived: false, composer: { step: ComposerStep.StartDirectConversation, - contactSearchTerm: '', + searchTerm: '', }, }; } @@ -2154,7 +2154,7 @@ export function reducer( showArchived: false, composer: { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds, cantAddContactIdForModal: undefined, recommendedGroupSizeModalState, @@ -2253,7 +2253,7 @@ export function reducer( ...state, composer: { ...composer, - contactSearchTerm: action.payload.contactSearchTerm, + searchTerm: action.payload.searchTerm, }, }; } diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 05b7e46d32..6b053e1f9a 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -28,7 +28,7 @@ import { PropsDataType as TimelinePropsType } from '../../components/conversatio import { TimelineItemType } from '../../components/conversation/TimelineItem'; import { assert } from '../../util/assert'; import { isConversationUnregistered } from '../../util/isConversationUnregistered'; -import { filterAndSortContacts } from '../../util/filterAndSortContacts'; +import { filterAndSortConversations } from '../../util/filterAndSortConversations'; import { getInteractionMode, @@ -323,24 +323,33 @@ export const getMe = createSelector( } ); -export const getComposerContactSearchTerm = createSelector( +export const getComposerConversationSearchTerm = createSelector( getComposerState, (composer): string => { if (!composer) { - assert(false, 'getComposerContactSearchTerm: composer is not open'); + assert(false, 'getComposerConversationSearchTerm: composer is not open'); return ''; } if (composer.step === ComposerStep.SetGroupMetadata) { assert( false, - 'getComposerContactSearchTerm: composer does not have a search term' + 'getComposerConversationSearchTerm: composer does not have a search term' ); return ''; } - return composer.contactSearchTerm; + return composer.searchTerm; } ); +function canComposeConversation(conversation: ConversationType): boolean { + return Boolean( + !conversation.isMe && + !conversation.isBlocked && + !isConversationUnregistered(conversation) && + (isString(conversation.name) || conversation.profileSharing) + ); +} + /** * This returns contacts for the composer and group members, which isn't just your primary * system contacts. It may include false positives, which is better than missing contacts. @@ -349,21 +358,26 @@ export const getComposerContactSearchTerm = createSelector( * current time, it's possible for this to return stale contacts that have unregistered * if no other conversations change. This should be a rare false positive. */ -export const getContacts = createSelector( +export const getComposableContacts = createSelector( getConversationLookup, (conversationLookup: ConversationLookupType): Array => Object.values(conversationLookup).filter( - contact => - contact.type === 'direct' && - !contact.isMe && - !contact.isBlocked && - !isConversationUnregistered(contact) && - (isString(contact.name) || contact.profileSharing) + conversation => + conversation.type === 'direct' && canComposeConversation(conversation) ) ); -const getNormalizedComposerContactSearchTerm = createSelector( - getComposerContactSearchTerm, +export const getComposableGroups = createSelector( + getConversationLookup, + (conversationLookup: ConversationLookupType): Array => + Object.values(conversationLookup).filter( + conversation => + conversation.type === 'group' && canComposeConversation(conversation) + ) +); + +const getNormalizedComposerConversationSearchTerm = createSelector( + getComposerConversationSearchTerm, (searchTerm: string): string => searchTerm.trim() ); @@ -372,8 +386,8 @@ const getNoteToSelfTitle = createSelector(getIntl, (i18n: LocalizerType) => ); export const getComposeContacts = createSelector( - getNormalizedComposerContactSearchTerm, - getContacts, + getNormalizedComposerConversationSearchTerm, + getComposableContacts, getMe, getNoteToSelfTitle, ( @@ -382,7 +396,7 @@ export const getComposeContacts = createSelector( noteToSelf: ConversationType, noteToSelfTitle: string ): Array => { - const result: Array = filterAndSortContacts( + const result: Array = filterAndSortConversations( contacts, searchTerm ); @@ -393,10 +407,21 @@ export const getComposeContacts = createSelector( } ); +export const getComposeGroups = createSelector( + getNormalizedComposerConversationSearchTerm, + getComposableGroups, + ( + searchTerm: string, + groups: Array + ): Array => { + return filterAndSortConversations(groups, searchTerm); + } +); + export const getCandidateContactsForNewGroup = createSelector( - getContacts, - getNormalizedComposerContactSearchTerm, - filterAndSortContacts + getComposableContacts, + getNormalizedComposerConversationSearchTerm, + filterAndSortConversations ); export const getCantAddContactForModal = createSelector( diff --git a/ts/state/smart/ConversationDetails.tsx b/ts/state/smart/ConversationDetails.tsx index e04bbbb369..eba4a45392 100644 --- a/ts/state/smart/ConversationDetails.tsx +++ b/ts/state/smart/ConversationDetails.tsx @@ -9,7 +9,7 @@ import { StateProps, } from '../../components/conversation/conversation-details/ConversationDetails'; import { - getContacts, + getComposableContacts, getConversationSelector, } from '../selectors/conversations'; import { getIntl } from '../selectors/user'; @@ -50,7 +50,7 @@ const mapStateToProps = ( ? conversation.canEditGroupInfo : false; const isAdmin = Boolean(conversation?.areWeAdmin); - const candidateContactsToAdd = getContacts(state); + const candidateContactsToAdd = getComposableContacts(state); return { ...props, diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx index d39abba6ba..a4b1af24f3 100644 --- a/ts/state/smart/LeftPane.tsx +++ b/ts/state/smart/LeftPane.tsx @@ -19,10 +19,11 @@ import { getCandidateContactsForNewGroup, getCantAddContactForModal, getComposeContacts, + getComposeGroups, getComposeGroupAvatar, getComposeGroupName, getComposeSelectedContacts, - getComposerContactSearchTerm, + getComposerConversationSearchTerm, getComposerStep, getLeftPaneLists, getMaximumGroupSizeModalState, @@ -96,8 +97,9 @@ const getModeSpecificProps = ( return { mode: LeftPaneMode.Compose, composeContacts: getComposeContacts(state), + composeGroups: getComposeGroups(state), regionCode: getRegionCode(state), - searchTerm: getComposerContactSearchTerm(state), + searchTerm: getComposerConversationSearchTerm(state), }; case ComposerStep.ChooseGroupMembers: return { @@ -109,7 +111,7 @@ const getModeSpecificProps = ( OneTimeModalState.Showing, isShowingMaximumGroupSizeModal: getMaximumGroupSizeModalState(state) === OneTimeModalState.Showing, - searchTerm: getComposerContactSearchTerm(state), + searchTerm: getComposerConversationSearchTerm(state), selectedContacts: getComposeSelectedContacts(state), }; case ComposerStep.SetGroupMetadata: diff --git a/ts/test-both/state/selectors/conversations_test.ts b/ts/test-both/state/selectors/conversations_test.ts index b14abde894..4165344ca5 100644 --- a/ts/test-both/state/selectors/conversations_test.ts +++ b/ts/test-both/state/selectors/conversations_test.ts @@ -16,10 +16,11 @@ import { getCandidateContactsForNewGroup, getCantAddContactForModal, getComposeContacts, + getComposeGroups, getComposeGroupAvatar, getComposeGroupName, getComposeSelectedContacts, - getComposerContactSearchTerm, + getComposerConversationSearchTerm, getComposerStep, getConversationSelector, getInvitedContactsForNewlyCreatedGroup, @@ -271,7 +272,7 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation as const, - contactSearchTerm: 'foo', + searchTerm: 'foo', }, }, }; @@ -287,7 +288,7 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: 'foo', + searchTerm: 'foo', selectedConversationIds: ['abc'], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -337,7 +338,7 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation, - contactSearchTerm: '', + searchTerm: '', }, }, }) @@ -398,7 +399,7 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation, - contactSearchTerm: '', + searchTerm: '', }, }, }) @@ -449,7 +450,7 @@ describe('both/state/selectors/conversations', () => { }); describe('#getComposeContacts', () => { - const getRootState = (contactSearchTerm = ''): StateType => { + const getRootState = (searchTerm = ''): StateType => { const rootState = getEmptyRootState(); return { ...rootState, @@ -463,7 +464,7 @@ describe('both/state/selectors/conversations', () => { }, composer: { step: ComposerStep.StartDirectConversation, - contactSearchTerm, + searchTerm, }, }, user: { @@ -474,10 +475,8 @@ describe('both/state/selectors/conversations', () => { }; }; - const getRootStateWithConverastions = ( - contactSearchTerm = '' - ): StateType => { - const result = getRootState(contactSearchTerm); + const getRootStateWithConversations = (searchTerm = ''): StateType => { + const result = getRootState(searchTerm); Object.assign(result.conversations.conversationLookup, { 'convo-1': { ...getDefaultConversation('convo-1'), @@ -534,7 +533,7 @@ describe('both/state/selectors/conversations', () => { }); it('returns contacts with Note to Self at the end when there is no search term', () => { - const state = getRootStateWithConverastions(); + const state = getRootStateWithConversations(); const result = getComposeContacts(state); const ids = result.map(contact => contact.id); @@ -547,7 +546,7 @@ describe('both/state/selectors/conversations', () => { }); it('can search for contacts', () => { - const state = getRootStateWithConverastions('in system'); + const state = getRootStateWithConversations('in system'); const result = getComposeContacts(state); const ids = result.map(contact => contact.id); @@ -556,8 +555,90 @@ describe('both/state/selectors/conversations', () => { }); }); + describe('#getComposeGroups', () => { + const getState = (searchTerm = ''): StateType => { + const rootState = getEmptyRootState(); + return { + ...rootState, + conversations: { + ...getEmptyState(), + conversationLookup: { + 'our-conversation-id': { + ...getDefaultConversation('our-conversation-id'), + isMe: true, + }, + 'convo-1': { + ...getDefaultConversation('convo-1'), + name: 'In System Contacts', + title: 'Should be dropped (contact)', + }, + 'convo-2': { + ...getDefaultConversation('convo-2'), + title: 'Should be dropped (contact)', + }, + 'convo-3': { + ...getDefaultConversation('convo-3'), + type: 'group', + name: 'Hello World', + title: 'Hello World', + }, + 'convo-4': { + ...getDefaultConversation('convo-4'), + type: 'group', + isBlocked: true, + title: 'Should be dropped (blocked)', + }, + 'convo-5': { + ...getDefaultConversation('convo-5'), + type: 'group', + title: 'Unknown Group', + }, + 'convo-6': { + ...getDefaultConversation('convo-6'), + type: 'group', + name: 'Signal', + title: 'Signal', + }, + 'convo-7': { + ...getDefaultConversation('convo-7'), + profileSharing: false, + type: 'group', + name: 'Signal Fake', + title: 'Signal Fake', + }, + }, + composer: { + step: ComposerStep.StartDirectConversation, + searchTerm, + }, + }, + user: { + ...rootState.user, + ourConversationId: 'our-conversation-id', + i18n, + }, + }; + }; + + it('can search for groups', () => { + const state = getState('hello'); + const result = getComposeGroups(state); + + const ids = result.map(group => group.id); + assert.deepEqual(ids, ['convo-3']); + }); + + it('does not return unknown groups when getting all groups (no search term)', () => { + const state = getState(); + const result = getComposeGroups(state); + + const ids = result.map(group => group.id); + assert.deepEqual(ids, ['convo-3', 'convo-6', 'convo-7']); + }); + }); + describe('#getCandidateContactsForNewGroup', () => { - const getRootState = (contactSearchTerm = ''): StateType => { + const getRootState = (searchTerm = ''): StateType => { const rootState = getEmptyRootState(); return { ...rootState, @@ -603,7 +684,7 @@ describe('both/state/selectors/conversations', () => { }, composer: { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm, + searchTerm, selectedConversationIds: ['abc'], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -648,7 +729,7 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation, - contactSearchTerm: '', + searchTerm: '', }, }, }) @@ -663,7 +744,7 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: undefined, - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -687,7 +768,7 @@ describe('both/state/selectors/conversations', () => { conversationLookup: { abc123: conversation }, composer: { cantAddContactIdForModal: 'abc123', - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -702,16 +783,16 @@ describe('both/state/selectors/conversations', () => { }); }); - describe('#getComposerContactSearchTerm', () => { + describe('#getComposerConversationSearchTerm', () => { it("returns the composer's contact search term", () => { assert.strictEqual( - getComposerContactSearchTerm({ + getComposerConversationSearchTerm({ ...getEmptyRootState(), conversations: { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation, - contactSearchTerm: 'foo bar', + searchTerm: 'foo bar', }, }, }), @@ -966,7 +1047,7 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: undefined, - contactSearchTerm: 'to be cleared', + searchTerm: 'to be cleared', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.Showing, @@ -991,7 +1072,7 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: undefined, - contactSearchTerm: 'to be cleared', + searchTerm: 'to be cleared', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, diff --git a/ts/test-both/util/filterAndSortContacts_test.ts b/ts/test-both/util/filterAndSortConversations_test.ts similarity index 80% rename from ts/test-both/util/filterAndSortContacts_test.ts rename to ts/test-both/util/filterAndSortConversations_test.ts index dbd7bcb208..857bd4f4fa 100644 --- a/ts/test-both/util/filterAndSortContacts_test.ts +++ b/ts/test-both/util/filterAndSortConversations_test.ts @@ -4,9 +4,9 @@ import { assert } from 'chai'; import { getDefaultConversation } from '../helpers/getDefaultConversation'; -import { filterAndSortContacts } from '../../util/filterAndSortContacts'; +import { filterAndSortConversations } from '../../util/filterAndSortConversations'; -describe('filterAndSortContacts', () => { +describe('filterAndSortConversations', () => { const conversations = [ getDefaultConversation({ title: '+16505551234', @@ -34,7 +34,7 @@ describe('filterAndSortContacts', () => { ]; it('without a search term, sorts conversations by title (but puts no-name contacts at the bottom)', () => { - const titles = filterAndSortContacts(conversations, '').map( + const titles = filterAndSortConversations(conversations, '').map( contact => contact.title ); assert.deepEqual(titles, [ @@ -47,14 +47,14 @@ describe('filterAndSortContacts', () => { }); it('can search for contacts by title', () => { - const titles = filterAndSortContacts(conversations, 'belind').map( + const titles = filterAndSortConversations(conversations, 'belind').map( contact => contact.title ); assert.sameMembers(titles, ['Belinda Beetle', 'Belinda Zephyr']); }); it('can search for contacts by phone number (and puts no-name contacts at the bottom)', () => { - const titles = filterAndSortContacts(conversations, '650555').map( + const titles = filterAndSortConversations(conversations, '650555').map( contact => contact.title ); assert.sameMembers(titles, ['Carlos Santana', '+16505551234']); diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index dc8a81c086..43795aa8ba 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -467,7 +467,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: undefined, - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -530,7 +530,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: 'abc123', - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -556,7 +556,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: 'abc123', - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.Showing, @@ -581,7 +581,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: 'abc123', - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -601,7 +601,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: 'abc123', - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.Shown, @@ -623,7 +623,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: 'abc123', - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -648,7 +648,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: 'abc123', - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -668,7 +668,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: 'abc123', - contactSearchTerm: '', + searchTerm: '', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1232,7 +1232,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation as const, - contactSearchTerm: '', + searchTerm: '', }, }; const action = setComposeSearchTerm('foo bar'); @@ -1240,7 +1240,7 @@ describe('both/state/ducks/conversations', () => { assert.deepEqual(result.composer, { step: ComposerStep.StartDirectConversation, - contactSearchTerm: 'foo bar', + searchTerm: 'foo bar', }); }); }); @@ -1306,7 +1306,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation as const, - contactSearchTerm: '', + searchTerm: '', }, }; const action = showArchivedConversations(); @@ -1344,7 +1344,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation as const, - contactSearchTerm: '', + searchTerm: '', }, }; const action = showInbox(); @@ -1361,7 +1361,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation as const, - contactSearchTerm: 'foo bar', + searchTerm: 'foo bar', }, }; const action = startComposing(); @@ -1370,7 +1370,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.StartDirectConversation, - contactSearchTerm: 'foo bar', + searchTerm: 'foo bar', }); }); @@ -1379,7 +1379,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { cantAddContactIdForModal: undefined, - contactSearchTerm: 'to be cleared', + searchTerm: 'to be cleared', groupAvatar: undefined, groupName: '', maximumGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1394,7 +1394,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.StartDirectConversation, - contactSearchTerm: '', + searchTerm: '', }); }); @@ -1418,7 +1418,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.StartDirectConversation, - contactSearchTerm: '', + searchTerm: '', }); }); @@ -1430,7 +1430,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.StartDirectConversation, - contactSearchTerm: '', + searchTerm: '', }); }); @@ -1445,7 +1445,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.StartDirectConversation, - contactSearchTerm: '', + searchTerm: '', }); }); }); @@ -1456,7 +1456,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.StartDirectConversation as const, - contactSearchTerm: 'to be cleared', + searchTerm: 'to be cleared', }, }; const action = showChooseGroupMembers(); @@ -1465,7 +1465,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1480,7 +1480,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: 'foo bar', + searchTerm: 'foo bar', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1516,7 +1516,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1534,7 +1534,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1555,7 +1555,7 @@ describe('both/state/ducks/conversations', () => { assert.isFalse(result.showArchived); assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1572,7 +1572,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: 'foo bar', + searchTerm: 'foo bar', selectedConversationIds: ['abc', 'def'], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1601,7 +1601,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: 'foo bar', + searchTerm: 'foo bar', selectedConversationIds: ['abc', 'def'], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1681,7 +1681,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1695,7 +1695,7 @@ describe('both/state/ducks/conversations', () => { assert.deepEqual(two.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: ['abc', 'def'], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1710,7 +1710,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: ['abc', 'def'], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1724,7 +1724,7 @@ describe('both/state/ducks/conversations', () => { assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: ['def'], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1742,7 +1742,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: oldSelectedConversationIds, cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1756,7 +1756,7 @@ describe('both/state/ducks/conversations', () => { assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [...oldSelectedConversationIds, newUuid], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.Showing, @@ -1774,7 +1774,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: oldSelectedConversationIds, cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.Shown, @@ -1788,7 +1788,7 @@ describe('both/state/ducks/conversations', () => { assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [...oldSelectedConversationIds, newUuid], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.Shown, @@ -1808,7 +1808,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1831,7 +1831,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: oldSelectedConversationIds, cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.Shown, @@ -1845,7 +1845,7 @@ describe('both/state/ducks/conversations', () => { assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [...oldSelectedConversationIds, newUuid], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.Shown, @@ -1863,7 +1863,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: oldSelectedConversationIds, cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.Shown, @@ -1877,7 +1877,7 @@ describe('both/state/ducks/conversations', () => { assert.deepEqual(result.composer, { step: ComposerStep.ChooseGroupMembers, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [...oldSelectedConversationIds, newUuid], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.Shown, @@ -1892,7 +1892,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: times(1000, () => uuid()), cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1919,7 +1919,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, @@ -1945,7 +1945,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { step: ComposerStep.ChooseGroupMembers as const, - contactSearchTerm: '', + searchTerm: '', selectedConversationIds: [], cantAddContactIdForModal: undefined, recommendedGroupSizeModalState: OneTimeModalState.NeverShown, diff --git a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts index 405988b9dd..5156d4cda8 100644 --- a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts +++ b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts @@ -11,7 +11,7 @@ import * as remoteConfig from '../../../RemoteConfig'; import { LeftPaneComposeHelper } from '../../../components/leftPane/LeftPaneComposeHelper'; describe('LeftPaneComposeHelper', () => { - const fakeContact = () => ({ + const fakeConvo = () => ({ id: uuid(), title: uuid(), type: 'direct' as const, @@ -40,6 +40,7 @@ describe('LeftPaneComposeHelper', () => { const showInbox = sinon.fake(); const helper = new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: '', }); @@ -53,6 +54,7 @@ describe('LeftPaneComposeHelper', () => { assert.strictEqual( new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: '', }).getRowCount(), @@ -63,7 +65,8 @@ describe('LeftPaneComposeHelper', () => { it('returns the number of contacts + 2 (for the "new group" button and header) if not searching', () => { assert.strictEqual( new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: '', }).getRowCount(), @@ -71,10 +74,23 @@ describe('LeftPaneComposeHelper', () => { ); }); - it('returns the number of contacts if searching, but not for a phone number', () => { + it('returns the number of contacts + number of groups + 3 (for the "new group" button and the headers) if not searching', () => { + assert.strictEqual( + new LeftPaneComposeHelper({ + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [fakeConvo(), fakeConvo()], + regionCode: 'US', + searchTerm: '', + }).getRowCount(), + 7 + ); + }); + + it('returns the number of conversations + the headers, but not for a phone number', () => { assert.strictEqual( new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }).getRowCount(), @@ -82,11 +98,21 @@ describe('LeftPaneComposeHelper', () => { ); assert.strictEqual( new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }).getRowCount(), - 2 + 3 + ); + assert.strictEqual( + new LeftPaneComposeHelper({ + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [fakeConvo()], + regionCode: 'US', + searchTerm: 'foo bar', + }).getRowCount(), + 5 ); }); @@ -94,6 +120,7 @@ describe('LeftPaneComposeHelper', () => { assert.strictEqual( new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: '+16505551234', }).getRowCount(), @@ -104,7 +131,8 @@ describe('LeftPaneComposeHelper', () => { it('returns the number of contacts + 2 (for the "Start new conversation" button and header) if searching for a phone number', () => { assert.strictEqual( new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: '+16505551234', }).getRowCount(), @@ -117,6 +145,7 @@ describe('LeftPaneComposeHelper', () => { it('returns a "new group" button if not searching and there are no contacts', () => { const helper = new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: '', }); @@ -128,9 +157,10 @@ describe('LeftPaneComposeHelper', () => { }); it('returns a "new group" button, a header, and contacts if not searching', () => { - const composeContacts = [fakeContact(), fakeContact()]; + const composeContacts = [fakeConvo(), fakeConvo()]; const helper = new LeftPaneComposeHelper({ composeContacts, + composeGroups: [], regionCode: 'US', searchTerm: '', }); @@ -152,6 +182,45 @@ describe('LeftPaneComposeHelper', () => { }); }); + it('returns a "new group" button, a header, contacts, groups header, and groups -- if not searching', () => { + const composeContacts = [fakeConvo(), fakeConvo()]; + const composeGroups = [fakeConvo(), fakeConvo()]; + const helper = new LeftPaneComposeHelper({ + composeContacts, + composeGroups, + regionCode: 'US', + searchTerm: '', + }); + + assert.deepEqual(helper.getRow(0), { + type: RowType.CreateNewGroup, + }); + assert.deepEqual(helper.getRow(1), { + type: RowType.Header, + i18nKey: 'contactsHeader', + }); + assert.deepEqual(helper.getRow(2), { + type: RowType.Contact, + contact: composeContacts[0], + }); + assert.deepEqual(helper.getRow(3), { + type: RowType.Contact, + contact: composeContacts[1], + }); + assert.deepEqual(helper.getRow(4), { + type: RowType.Header, + i18nKey: 'groupsHeader', + }); + assert.deepEqual(helper.getRow(5), { + type: RowType.Conversation, + conversation: composeGroups[0], + }); + assert.deepEqual(helper.getRow(6), { + type: RowType.Conversation, + conversation: composeGroups[1], + }); + }); + it("doesn't let you create new groups if storage service write is disabled", () => { remoteConfigStub .withArgs('desktop.storage') @@ -162,6 +231,7 @@ describe('LeftPaneComposeHelper', () => { assert.isUndefined( new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: '', }).getRow(0) @@ -176,6 +246,7 @@ describe('LeftPaneComposeHelper', () => { assert.isUndefined( new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: '', }).getRow(0) @@ -185,6 +256,7 @@ describe('LeftPaneComposeHelper', () => { it('returns no rows if searching and there are no results', () => { const helper = new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }); @@ -194,18 +266,19 @@ describe('LeftPaneComposeHelper', () => { }); it('returns one row per contact if searching', () => { - const composeContacts = [fakeContact(), fakeContact()]; + const composeContacts = [fakeConvo(), fakeConvo()]; const helper = new LeftPaneComposeHelper({ composeContacts, + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }); - assert.deepEqual(helper.getRow(0), { + assert.deepEqual(helper.getRow(1), { type: RowType.Contact, contact: composeContacts[0], }); - assert.deepEqual(helper.getRow(1), { + assert.deepEqual(helper.getRow(2), { type: RowType.Contact, contact: composeContacts[1], }); @@ -214,6 +287,7 @@ describe('LeftPaneComposeHelper', () => { it('returns a "start new conversation" row if searching for a phone number and there are no results', () => { const helper = new LeftPaneComposeHelper({ composeContacts: [], + composeGroups: [], regionCode: 'US', searchTerm: '+16505551234', }); @@ -226,9 +300,10 @@ describe('LeftPaneComposeHelper', () => { }); it('returns a "start new conversation" row, a header, and contacts if searching for a phone number', () => { - const composeContacts = [fakeContact(), fakeContact()]; + const composeContacts = [fakeConvo(), fakeConvo()]; const helper = new LeftPaneComposeHelper({ composeContacts, + composeGroups: [], regionCode: 'US', searchTerm: '+16505551234', }); @@ -255,7 +330,8 @@ describe('LeftPaneComposeHelper', () => { describe('getConversationAndMessageAtIndex', () => { it('returns undefined because keyboard shortcuts are not supported', () => { const helper = new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }); @@ -267,7 +343,8 @@ describe('LeftPaneComposeHelper', () => { describe('getConversationAndMessageInDirection', () => { it('returns undefined because keyboard shortcuts are not supported', () => { const helper = new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }); @@ -285,21 +362,24 @@ describe('LeftPaneComposeHelper', () => { describe('shouldRecomputeRowHeights', () => { it('returns false if going from "no header" to "no header"', () => { const helper = new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }); assert.isFalse( helper.shouldRecomputeRowHeights({ - composeContacts: [fakeContact()], + composeContacts: [fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }) ); assert.isFalse( helper.shouldRecomputeRowHeights({ - composeContacts: [fakeContact(), fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: 'bing bong', }) @@ -308,21 +388,24 @@ describe('LeftPaneComposeHelper', () => { it('returns false if going from "has header" to "has header"', () => { const helper = new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: '', }); assert.isFalse( helper.shouldRecomputeRowHeights({ - composeContacts: [fakeContact()], + composeContacts: [fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: '', }) ); assert.isFalse( helper.shouldRecomputeRowHeights({ - composeContacts: [fakeContact()], + composeContacts: [fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: '+16505559876', }) @@ -331,21 +414,24 @@ describe('LeftPaneComposeHelper', () => { it('returns true if going from "no header" to "has header"', () => { const helper = new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }); assert.isTrue( helper.shouldRecomputeRowHeights({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: '', }) ); assert.isTrue( helper.shouldRecomputeRowHeights({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: '+16505551234', }) @@ -354,18 +440,72 @@ describe('LeftPaneComposeHelper', () => { it('returns true if going from "has header" to "no header"', () => { const helper = new LeftPaneComposeHelper({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: '', }); assert.isTrue( helper.shouldRecomputeRowHeights({ - composeContacts: [fakeContact(), fakeContact()], + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], regionCode: 'US', searchTerm: 'foo bar', }) ); }); + + it('should be true if going from contact to group or vice versa', () => { + const helperContacts = new LeftPaneComposeHelper({ + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], + regionCode: 'US', + searchTerm: 'foo bar', + }); + + assert.isTrue( + helperContacts.shouldRecomputeRowHeights({ + composeContacts: [], + composeGroups: [fakeConvo(), fakeConvo()], + regionCode: 'US', + searchTerm: 'foo bar', + }) + ); + + const helperGroups = new LeftPaneComposeHelper({ + composeContacts: [], + composeGroups: [fakeConvo(), fakeConvo()], + regionCode: 'US', + searchTerm: 'foo bar', + }); + + assert.isTrue( + helperGroups.shouldRecomputeRowHeights({ + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [], + regionCode: 'US', + searchTerm: 'foo bar', + }) + ); + }); + + it('should be true if the headers are in different row indices as before', () => { + const helperContacts = new LeftPaneComposeHelper({ + composeContacts: [fakeConvo(), fakeConvo()], + composeGroups: [fakeConvo()], + regionCode: 'US', + searchTerm: 'soup', + }); + + assert.isTrue( + helperContacts.shouldRecomputeRowHeights({ + composeContacts: [fakeConvo()], + composeGroups: [fakeConvo(), fakeConvo()], + regionCode: 'US', + searchTerm: 'sandwich', + }) + ); + }); }); }); diff --git a/ts/test-node/components/leftPane/LeftPaneInboxHelper_test.ts b/ts/test-node/components/leftPane/LeftPaneInboxHelper_test.tsx similarity index 100% rename from ts/test-node/components/leftPane/LeftPaneInboxHelper_test.ts rename to ts/test-node/components/leftPane/LeftPaneInboxHelper_test.tsx diff --git a/ts/util/filterAndSortContacts.ts b/ts/util/filterAndSortConversations.ts similarity index 82% rename from ts/util/filterAndSortContacts.ts rename to ts/util/filterAndSortConversations.ts index 1fbfd8d414..328677d031 100644 --- a/ts/util/filterAndSortContacts.ts +++ b/ts/util/filterAndSortConversations.ts @@ -28,17 +28,17 @@ const FUSE_OPTIONS: FuseOptions = { const collator = new Intl.Collator(); -export function filterAndSortContacts( - contacts: ReadonlyArray, +export function filterAndSortConversations( + conversations: ReadonlyArray, searchTerm: string ): Array { if (searchTerm.length) { - return new Fuse(contacts, FUSE_OPTIONS).search( + return new Fuse(conversations, FUSE_OPTIONS).search( searchTerm ); } - return contacts.concat().sort((a, b) => { + return conversations.concat().sort((a, b) => { const aHasName = hasName(a); const bHasName = hasName(b); diff --git a/ts/util/index.ts b/ts/util/index.ts index ca775d6c68..8027b7115e 100644 --- a/ts/util/index.ts +++ b/ts/util/index.ts @@ -34,6 +34,7 @@ import { } from './sessionTranslation'; import * as zkgroup from './zkgroup'; import { StartupQueue } from './StartupQueue'; +import { postLinkExperience } from './postLinkExperience'; export { GoogleChrome, @@ -58,6 +59,7 @@ export { mapToSupportLocale, missingCaseError, parseRemoteClientExpiration, + postLinkExperience, queueUpdateMessage, saveNewMessageBatcher, setBatchingStrategy, diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 155582486a..74d72b4aa1 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -645,7 +645,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);", - "lineNumber": 127, + "lineNumber": 128, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Adding sub-view to DOM" @@ -654,7 +654,7 @@ "rule": "jQuery-append(", "path": "js/views/inbox_view.js", "line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);", - "lineNumber": 127, + "lineNumber": 128, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Adding sub-view to DOM" @@ -663,7 +663,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);", - "lineNumber": 138, + "lineNumber": 139, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Adding sub-view to DOM" @@ -672,7 +672,7 @@ "rule": "jQuery-append(", "path": "js/views/inbox_view.js", "line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);", - "lineNumber": 138, + "lineNumber": 139, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Adding sub-view to DOM" @@ -681,7 +681,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " if (e && this.$(e.target).closest('.placeholder').length) {", - "lineNumber": 191, + "lineNumber": 192, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Static selector, read-only access" @@ -690,15 +690,6 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('#header, .gutter').addClass('inactive');", - "lineNumber": 195, - "reasonCategory": "usageTrusted", - "updated": "2021-02-26T18:44:56.450Z", - "reasonDetail": "Static selector, adding or removing classes" - }, - { - "rule": "jQuery-$(", - "path": "js/views/inbox_view.js", - "line": " this.$('.conversation-stack').removeClass('inactive');", "lineNumber": 196, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", @@ -707,8 +698,8 @@ { "rule": "jQuery-$(", "path": "js/views/inbox_view.js", - "line": " this.$('.conversation-stack').addClass('inactive');", - "lineNumber": 199, + "line": " this.$('.conversation-stack').removeClass('inactive');", + "lineNumber": 197, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Static selector, adding or removing classes" @@ -716,7 +707,7 @@ { "rule": "jQuery-$(", "path": "js/views/inbox_view.js", - "line": " this.$('#header, .gutter').removeClass('inactive');", + "line": " this.$('.conversation-stack').addClass('inactive');", "lineNumber": 200, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", @@ -725,17 +716,26 @@ { "rule": "jQuery-$(", "path": "js/views/inbox_view.js", - "line": " this.$('.conversation:first .menu').trigger('close');", + "line": " this.$('#header, .gutter').removeClass('inactive');", "lineNumber": 201, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", + "reasonDetail": "Static selector, adding or removing classes" + }, + { + "rule": "jQuery-$(", + "path": "js/views/inbox_view.js", + "line": " this.$('.conversation:first .menu').trigger('close');", + "lineNumber": 202, + "reasonCategory": "usageTrusted", + "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Static selector, trigging DOM event" }, { "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {", - "lineNumber": 223, + "lineNumber": 224, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Static selector, read-only access" @@ -744,7 +744,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.conversation:first .recorder').trigger('close');", - "lineNumber": 226, + "lineNumber": 227, "reasonCategory": "usageTrusted", "updated": "2021-02-26T18:44:56.450Z", "reasonDetail": "Static selector, triggering DOM event" @@ -771,7 +771,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#qr img').remove();", - "lineNumber": 161, + "lineNumber": 160, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -780,7 +780,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#qr canvas').remove();", - "lineNumber": 162, + "lineNumber": 161, "reasonCategory": "usageTrusted", "updated": "2020-09-11T17:24:56.124Z", "reasonDetail": "Static selector argument" @@ -789,7 +789,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#qr .container').show();", - "lineNumber": 163, + "lineNumber": 162, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -798,7 +798,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#qr').removeClass('ready');", - "lineNumber": 164, + "lineNumber": 163, "reasonCategory": "usageTrusted", "updated": "2020-09-11T17:24:56.124Z", "reasonDetail": "Static selector argument" @@ -807,7 +807,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " if ($('#qr').length === 0) {", - "lineNumber": 167, + "lineNumber": 166, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -816,7 +816,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#qr .container').hide();", - "lineNumber": 173, + "lineNumber": 172, "reasonCategory": "usageTrusted", "updated": "2020-03-24T19:03:04.861Z", "reasonDetail": "Protected from arbitrary input" @@ -825,7 +825,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.qr = new QRCode(this.$('#qr')[0]).makeCode(url);", - "lineNumber": 174, + "lineNumber": 173, "reasonCategory": "usageTrusted", "updated": "2020-03-24T19:03:04.861Z", "reasonDetail": "Protected from arbitrary input" @@ -834,7 +834,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#qr').removeAttr('title');", - "lineNumber": 175, + "lineNumber": 174, "reasonCategory": "usageTrusted", "updated": "2020-09-11T17:24:56.124Z", "reasonDetail": "Static selector argument" @@ -843,7 +843,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#qr').addClass('ready');", - "lineNumber": 176, + "lineNumber": 175, "reasonCategory": "usageTrusted", "updated": "2020-03-24T19:03:04.861Z", "reasonDetail": "Protected from arbitrary input" @@ -852,7 +852,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());", - "lineNumber": 181, + "lineNumber": 180, "reasonCategory": "usageTrusted", "updated": "2020-03-24T19:03:04.861Z", "reasonDetail": "Protected from arbitrary input" @@ -861,7 +861,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$(DEVICE_NAME_SELECTOR).focus();", - "lineNumber": 182, + "lineNumber": 181, "reasonCategory": "usageTrusted", "updated": "2020-09-11T17:24:56.124Z", "reasonDetail": "Static selector argument" @@ -870,7 +870,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#link-phone').submit();", - "lineNumber": 186, + "lineNumber": 185, "reasonCategory": "usageTrusted", "updated": "2020-03-24T19:03:04.861Z", "reasonDetail": "Protected from arbitrary input" @@ -879,7 +879,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$('#link-phone').submit(e => {", - "lineNumber": 228, + "lineNumber": 227, "reasonCategory": "usageTrusted", "updated": "2020-03-24T19:03:04.861Z", "reasonDetail": "Protected from arbitrary input" @@ -888,7 +888,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " let name = this.$(DEVICE_NAME_SELECTOR).val();", - "lineNumber": 232, + "lineNumber": 231, "reasonCategory": "usageTrusted", "updated": "2020-03-24T19:03:04.861Z", "reasonDetail": "Protected from arbitrary input" @@ -897,7 +897,7 @@ "rule": "jQuery-$(", "path": "js/views/install_view.js", "line": " this.$(DEVICE_NAME_SELECTOR).focus();", - "lineNumber": 235, + "lineNumber": 234, "reasonCategory": "usageTrusted", "updated": "2020-03-24T19:03:04.861Z", "reasonDetail": "Protected from arbitrary input" diff --git a/ts/util/postLinkExperience.ts b/ts/util/postLinkExperience.ts new file mode 100644 index 0000000000..dde4157570 --- /dev/null +++ b/ts/util/postLinkExperience.ts @@ -0,0 +1,32 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { onTimeout } from '../services/timers'; + +class PostLinkExperience { + private hasNotFinishedSync: boolean; + + constructor() { + this.hasNotFinishedSync = false; + } + + start() { + this.hasNotFinishedSync = true; + + // timeout "post link" after 10 minutes in case the syncs don't complete + // in time or are never called. + onTimeout(Date.now() + 60 * 60 * 10 * 1000, () => { + this.stop(); + }); + } + + stop() { + this.hasNotFinishedSync = false; + } + + isActive(): boolean { + return this.hasNotFinishedSync === true; + } +} + +export const postLinkExperience = new PostLinkExperience();