Do not populate left pane on initial link

This commit is contained in:
Josh Perez 2021-04-20 16:16:49 -07:00 committed by GitHub
parent f456bbd3db
commit 5e2d48cc2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 791 additions and 277 deletions

View File

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

View File

@ -111,7 +111,8 @@
},
render_attributes: {
welcomeToSignal: i18n('welcomeToSignal'),
selectAContact: i18n('selectAContact'),
// TODO DESKTOP-1451: add back the selectAContact message
selectAContact: '',
},
events: {
click: 'onClick',

View File

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

View File

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

View File

@ -2398,13 +2398,6 @@ export async function startApp(): Promise<void> {
});
// 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<void> {
conversation.set({
name: details.name,
color: details.color,
active_at: activeAt,
inbox_position: details.inboxPosition,
});
@ -2480,8 +2472,7 @@ export async function startApp(): Promise<void> {
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<void> {
} 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<void> {
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<void> {
// 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,

View File

@ -35,6 +35,25 @@ const defaultConversations: Array<ConversationListItemPropsType> = [
},
];
const defaultGroups: Array<ConversationListItemPropsType> = [
{
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<ConversationListItemPropsType> = [
{
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', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
composeContacts: [],
composeGroups: [],
regionCode: 'US',
searchTerm: '',
},
@ -365,12 +385,13 @@ story.add('Compose: no contacts', () => (
/>
));
story.add('Compose: some contacts, no search term', () => (
story.add('Compose: some contacts, no groups, no search term', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
composeContacts: defaultConversations,
composeGroups: [],
regionCode: 'US',
searchTerm: '',
},
@ -378,14 +399,71 @@ story.add('Compose: some contacts, no search term', () => (
/>
));
story.add('Compose: some contacts with a search term', () => (
story.add('Compose: some contacts, no groups, with a search term', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
composeContacts: defaultConversations,
composeGroups: [],
regionCode: 'US',
searchTerm: 'foo bar',
searchTerm: 'ar',
},
})}
/>
));
story.add('Compose: some groups, no contacts, no search term', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
composeContacts: [],
composeGroups: defaultGroups,
regionCode: 'US',
searchTerm: '',
},
})}
/>
));
story.add('Compose: some groups, no contacts, with search term', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
composeContacts: [],
composeGroups: defaultGroups,
regionCode: 'US',
searchTerm: 'ar',
},
})}
/>
));
story.add('Compose: some contacts, some groups, no search term', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
composeContacts: defaultConversations,
composeGroups: defaultGroups,
regionCode: 'US',
searchTerm: '',
},
})}
/>
));
story.add('Compose: some contacts, some groups, with a search term', () => (
<LeftPane
{...createProps({
modeSpecificProps: {
mode: LeftPaneMode.Compose,
composeContacts: defaultConversations,
composeGroups: defaultGroups,
regionCode: 'US',
searchTerm: 'ar',
},
})}
/>

View File

@ -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<PropsType> = ({
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 () => {

View File

@ -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<ContactListItemPropsType>;
composeGroups: ReadonlyArray<ContactListItemPropsType>;
regionCode: string;
searchTerm: string;
};
@ -35,20 +37,24 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<
> {
private readonly composeContacts: ReadonlyArray<ContactListItemPropsType>;
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
private readonly searchTerm: string;
private readonly phoneNumber: undefined | PhoneNumber;
constructor({
composeContacts,
composeGroups,
regionCode,
searchTerm,
}: Readonly<LeftPaneComposePropsType>) {
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 : (
<div className="module-left-pane__compose-no-contacts">
{i18n('noContactsFound')}
{i18n('noConversationsFound')}
</div>
)}
</>
@ -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<LeftPaneComposePropsType>): boolean {
shouldRecomputeRowHeights(
exProps: Readonly<LeftPaneComposePropsType>
): 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,
};
}
}

View File

@ -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<ConversationListItemPropsType>;
@ -56,6 +59,33 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<
);
}
getPreRowsNode({
i18n,
}: Readonly<{ i18n: LocalizerType }>): null | ReactChild {
if (this.getRowCount() === 0) {
return (
<div className="module-left-pane__empty">
<div>
<Intl
i18n={i18n}
id="emptyInboxMessage"
components={[
<span>
<strong>{i18n('composeIcon')}</strong>
<span className="module-left-pane__empty--composer_icon">
<i className="module-left-pane__empty--composer_icon--icon" />
</span>
</span>,
]}
/>
</div>
</div>
);
}
return null;
}
getRow(rowIndex: number): undefined | Row {
const { conversations, archivedConversations, pinnedConversations } = this;

View File

@ -2598,7 +2598,8 @@ type MaybeUpdatePropsType = {
};
export async function waitThenMaybeUpdateGroup(
options: MaybeUpdatePropsType
options: MaybeUpdatePropsType,
{ viaSync = false } = {}
): Promise<void> {
// 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<void> {
export async function maybeUpdateGroup(
{
conversation,
dropInitialJoinMessage,
groupChangeBase64,
newRevision,
receivedAt,
sentAt,
}: MaybeUpdatePropsType,
{ viaSync = false } = {}
): Promise<void> {
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<void> {
async function updateGroup(
{
conversation,
receivedAt,
sentAt,
updates,
}: {
conversation: ConversationModel;
receivedAt?: number;
sentAt?: number;
updates: UpdatesResultType;
},
{ viaSync = false } = {}
): Promise<void> {
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,

View File

@ -1035,6 +1035,7 @@ async function sync(): Promise<ManifestRecordClass | undefined> {
);
}
window.Signal.Util.postLinkExperience.stop();
window.log.info('storageService.sync: complete');
return manifest;
}

View File

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

View File

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

View File

@ -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<ConversationType> =>
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<ConversationType> =>
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<ConversationType> => {
const result: Array<ConversationType> = filterAndSortContacts(
const result: Array<ConversationType> = filterAndSortConversations(
contacts,
searchTerm
);
@ -393,10 +407,21 @@ export const getComposeContacts = createSelector(
}
);
export const getComposeGroups = createSelector(
getNormalizedComposerConversationSearchTerm,
getComposableGroups,
(
searchTerm: string,
groups: Array<ConversationType>
): Array<ConversationType> => {
return filterAndSortConversations(groups, searchTerm);
}
);
export const getCandidateContactsForNewGroup = createSelector(
getContacts,
getNormalizedComposerContactSearchTerm,
filterAndSortContacts
getComposableContacts,
getNormalizedComposerConversationSearchTerm,
filterAndSortConversations
);
export const getCantAddContactForModal = createSelector(

View File

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

View File

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

View File

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

View File

@ -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']);

View File

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

View File

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

View File

@ -28,17 +28,17 @@ const FUSE_OPTIONS: FuseOptions<ConversationType> = {
const collator = new Intl.Collator();
export function filterAndSortContacts(
contacts: ReadonlyArray<ConversationType>,
export function filterAndSortConversations(
conversations: ReadonlyArray<ConversationType>,
searchTerm: string
): Array<ConversationType> {
if (searchTerm.length) {
return new Fuse<ConversationType>(contacts, FUSE_OPTIONS).search(
return new Fuse<ConversationType>(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);

View File

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

View File

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

View File

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