diff --git a/stylesheets/_mixins.scss b/stylesheets/_mixins.scss index 1c90794c4a99..5d7c346f1d9f 100644 --- a/stylesheets/_mixins.scss +++ b/stylesheets/_mixins.scss @@ -1,4 +1,4 @@ -// Copyright 2016-2020 Signal Messenger, LLC +// Copyright 2016-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // Fonts @@ -116,6 +116,42 @@ } } +// Search results loading + +@mixin search-results-loading-pulsating-background { + animation: search-results-loading-pulsating-background-animation 2s infinite; + + @media (prefers-reduced-motion) { + animation: none; + } + + @include light-theme { + background: $color-gray-05; + } + @include dark-theme { + background: $color-gray-65; + } +} + +@keyframes search-results-loading-pulsating-background-animation { + 0% { + opacity: 1; + } + 50% { + opacity: 0.55; + } + 100% { + opacity: 1; + } +} + +@mixin search-results-loading-box($width) { + width: $width; + height: 12px; + border-radius: 4px; + @include search-results-loading-pulsating-background; +} + // Icons @mixin color-svg($svg, $color, $stretch: true) { diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 816b3128b4f5..24cc5dd0954c 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -6911,7 +6911,11 @@ button.module-image__border-overlay:focus { // Module: conversation list .module-conversation-list { - @include smooth-scroll; + &--scroll-behavior { + &-default { + @include smooth-scroll; + } + } &__item { &--archive-button { diff --git a/stylesheets/components/SearchResultsLoadingFakeHeader.scss b/stylesheets/components/SearchResultsLoadingFakeHeader.scss new file mode 100644 index 000000000000..c1ba071c57eb --- /dev/null +++ b/stylesheets/components/SearchResultsLoadingFakeHeader.scss @@ -0,0 +1,16 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +// These styles should match the "real" header. +.module-SearchResultsLoadingFakeHeader { + display: flex; + flex-direction: column; + justify-content: center; + padding-left: 16px; + + &::before { + content: ''; + display: block; + @include search-results-loading-box(25%); + } +} diff --git a/stylesheets/components/SearchResultsLoadingFakeRow.scss b/stylesheets/components/SearchResultsLoadingFakeRow.scss new file mode 100644 index 000000000000..2a2203ee525b --- /dev/null +++ b/stylesheets/components/SearchResultsLoadingFakeRow.scss @@ -0,0 +1,35 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +// These styles should match the "real" contact/conversation row. +.module-SearchResultsLoadingFakeRow { + display: flex; + align-items: center; + justify-content: center; + padding-left: 16px; + padding-right: 16px; + + &__avatar { + width: 52px; + height: 52px; + border-radius: 100%; + @include search-results-loading-pulsating-background; + } + + &__content { + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: center; + margin-left: 12px; + + &__header { + @include search-results-loading-box(50%); + margin-bottom: 8px; + } + + &__message { + @include search-results-loading-box(90%); + } + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index a629149c0bae..efef193e5b11 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -38,3 +38,5 @@ @import './components/GroupDialog.scss'; @import './components/GroupTitleInput.scss'; @import './components/MessageAudio.scss'; +@import './components/SearchResultsLoadingFakeHeader.scss'; +@import './components/SearchResultsLoadingFakeRow.scss'; diff --git a/ts/components/ConversationList.stories.tsx b/ts/components/ConversationList.stories.tsx index 6b94a09606d4..b523f836fc28 100644 --- a/ts/components/ConversationList.stories.tsx +++ b/ts/components/ConversationList.stories.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import { omit } from 'lodash'; +import { times, omit } from 'lodash'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; @@ -413,7 +413,7 @@ Line 4, well.`, }); story.add('Conversations: Various Times', () => { - const times: Array<[number, string]> = [ + const pairs: Array<[number, string]> = [ [Date.now() - 5 * 60 * 60 * 1000, 'Five hours ago'], [Date.now() - 24 * 60 * 60 * 1000, 'One day ago'], [Date.now() - 7 * 24 * 60 * 60 * 1000, 'One week ago'], @@ -423,7 +423,7 @@ Line 4, well.`, return ( ({ + pairs.map(([lastUpdated, messageText]) => ({ type: RowType.Conversation, conversation: createConversation({ lastUpdated, @@ -510,6 +510,18 @@ story.add('Start new conversation', () => ( /> )); +story.add('Search results loading skeleton', () => ( + ({ + type: RowType.SearchResultsLoadingFakeRow as const, + })), + ])} + /> +)); + story.add('Kitchen sink', () => ( ( type: RowType.MessageSearchResult, messageId: '123', }, - { type: RowType.Spinner }, { type: RowType.ArchiveButton, archivedConversationsCount: 123, diff --git a/ts/components/ConversationList.tsx b/ts/components/ConversationList.tsx index 103186443166..09acd8f14b99 100644 --- a/ts/components/ConversationList.tsx +++ b/ts/components/ConversationList.tsx @@ -3,10 +3,11 @@ import React, { useRef, useEffect, useCallback, CSSProperties } from 'react'; import { List, ListRowRenderer } from 'react-virtualized'; +import classNames from 'classnames'; import { missingCaseError } from '../util/missingCaseError'; import { assert } from '../util/assert'; -import { LocalizerType } from '../types/Util'; +import { LocalizerType, ScrollBehavior } from '../types/Util'; import { ConversationListItem, @@ -21,8 +22,9 @@ import { ContactCheckboxDisabledReason, } from './conversationList/ContactCheckbox'; import { CreateNewGroupButton } from './conversationList/CreateNewGroupButton'; -import { Spinner as SpinnerComponent } from './Spinner'; import { StartNewConversation as StartNewConversationComponent } from './conversationList/StartNewConversation'; +import { SearchResultsLoadingFakeHeader as SearchResultsLoadingFakeHeaderComponent } from './conversationList/SearchResultsLoadingFakeHeader'; +import { SearchResultsLoadingFakeRow as SearchResultsLoadingFakeRowComponent } from './conversationList/SearchResultsLoadingFakeRow'; export enum RowType { ArchiveButton, @@ -33,7 +35,8 @@ export enum RowType { CreateNewGroup, Header, MessageSearchResult, - Spinner, + SearchResultsLoadingFakeHeader, + SearchResultsLoadingFakeRow, StartNewConversation, } @@ -76,7 +79,13 @@ type HeaderRowType = { i18nKey: string; }; -type SpinnerRowType = { type: RowType.Spinner }; +type SearchResultsLoadingFakeHeaderType = { + type: RowType.SearchResultsLoadingFakeHeader; +}; + +type SearchResultsLoadingFakeRowType = { + type: RowType.SearchResultsLoadingFakeRow; +}; type StartNewConversationRowType = { type: RowType.StartNewConversation; @@ -92,7 +101,8 @@ export type Row = | CreateNewGroupRowType | MessageRowType | HeaderRowType - | SpinnerRowType + | SearchResultsLoadingFakeHeaderType + | SearchResultsLoadingFakeRowType | StartNewConversationRowType; export type PropsType = { @@ -105,8 +115,10 @@ export type PropsType = { // this should only happen if there is a bug somewhere. For example, an inaccurate // `rowCount`. getRow: (index: number) => undefined | Row; + scrollBehavior?: ScrollBehavior; scrollToRowIndex?: number; shouldRecomputeRowHeights: boolean; + scrollable?: boolean; i18n: LocalizerType; @@ -121,6 +133,9 @@ export type PropsType = { startNewConversationFromPhoneNumber: (e164: string) => void; }; +const NORMAL_ROW_HEIGHT = 68; +const HEADER_ROW_HEIGHT = 40; + export const ConversationList: React.FC = ({ dimensions, getRow, @@ -130,7 +145,9 @@ export const ConversationList: React.FC = ({ onSelectConversation, renderMessageSearchResult, rowCount, + scrollBehavior = ScrollBehavior.Default, scrollToRowIndex, + scrollable = true, shouldRecomputeRowHeights, showChooseGroupMembers, startNewConversationFromPhoneNumber, @@ -149,9 +166,15 @@ export const ConversationList: React.FC = ({ const row = getRow(index); if (!row) { assert(false, `Expected a row at index ${index}`); - return 68; + return NORMAL_ROW_HEIGHT; + } + switch (row.type) { + case RowType.Header: + case RowType.SearchResultsLoadingFakeHeader: + return HEADER_ROW_HEIGHT; + default: + return NORMAL_ROW_HEIGHT; } - return row.type === RowType.Header ? 40 : 68; }, [getRow] ); @@ -235,22 +258,20 @@ export const ConversationList: React.FC = ({ {i18n(row.i18nKey)} ); - case RowType.Spinner: - return ( -
- -
- ); case RowType.MessageSearchResult: return ( {renderMessageSearchResult(row.messageId, style)} ); + case RowType.SearchResultsLoadingFakeHeader: + return ( + + ); + case RowType.SearchResultsLoadingFakeRow: + return ( + + ); case RowType.StartNewConversation: return ( = ({ return ( diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 79d6afa5be59..bc210d541614 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -36,7 +36,7 @@ import { } from './leftPane/LeftPaneSetGroupMetadataHelper'; import * as OS from '../OS'; -import { LocalizerType } from '../types/Util'; +import { LocalizerType, ScrollBehavior } from '../types/Util'; import { usePrevious } from '../util/hooks'; import { missingCaseError } from '../util/missingCaseError'; @@ -337,10 +337,21 @@ export const LeftPane: React.FC = ({ selectedConversationId, selectedConversationId ); - const rowIndexToScrollTo: undefined | number = - previousSelectedConversationId === selectedConversationId - ? undefined - : helper.getRowIndexToScrollTo(selectedConversationId); + + const isScrollable = helper.isScrollable(); + + let rowIndexToScrollTo: undefined | number; + let scrollBehavior: ScrollBehavior; + if (isScrollable) { + rowIndexToScrollTo = + previousSelectedConversationId === selectedConversationId + ? undefined + : helper.getRowIndexToScrollTo(selectedConversationId); + scrollBehavior = ScrollBehavior.Default; + } else { + rowIndexToScrollTo = 0; + scrollBehavior = ScrollBehavior.Hard; + } // We ensure that the listKey differs between some modes (e.g. inbox/archived), ensuring // that AutoSizer properly detects the new size of its slot in the flexbox. The @@ -436,7 +447,9 @@ export const LeftPane: React.FC = ({ }} renderMessageSearchResult={renderMessageSearchResult} rowCount={helper.getRowCount()} + scrollBehavior={scrollBehavior} scrollToRowIndex={rowIndexToScrollTo} + scrollable={isScrollable} shouldRecomputeRowHeights={shouldRecomputeRowHeights} showChooseGroupMembers={showChooseGroupMembers} startNewConversationFromPhoneNumber={ diff --git a/ts/components/conversationList/SearchResultsLoadingFakeHeader.tsx b/ts/components/conversationList/SearchResultsLoadingFakeHeader.tsx new file mode 100644 index 000000000000..8c60a9012f89 --- /dev/null +++ b/ts/components/conversationList/SearchResultsLoadingFakeHeader.tsx @@ -0,0 +1,12 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { CSSProperties, FunctionComponent } from 'react'; + +type PropsType = { + style: CSSProperties; +}; + +export const SearchResultsLoadingFakeHeader: FunctionComponent = ({ + style, +}) =>
; diff --git a/ts/components/conversationList/SearchResultsLoadingFakeRow.tsx b/ts/components/conversationList/SearchResultsLoadingFakeRow.tsx new file mode 100644 index 000000000000..d378ec4542b8 --- /dev/null +++ b/ts/components/conversationList/SearchResultsLoadingFakeRow.tsx @@ -0,0 +1,20 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { CSSProperties, FunctionComponent } from 'react'; + +type PropsType = { + style: CSSProperties; +}; + +export const SearchResultsLoadingFakeRow: FunctionComponent = ({ + style, +}) => ( +
+
+
+
+
+
+
+); diff --git a/ts/components/leftPane/LeftPaneHelper.tsx b/ts/components/leftPane/LeftPaneHelper.tsx index 763f1f2ddb94..e92f5adc2292 100644 --- a/ts/components/leftPane/LeftPaneHelper.tsx +++ b/ts/components/leftPane/LeftPaneHelper.tsx @@ -83,6 +83,10 @@ export abstract class LeftPaneHelper { return undefined; } + isScrollable(): boolean { + return true; + } + abstract getConversationAndMessageAtIndex( conversationIndex: number ): undefined | { conversationId: string; messageId?: string }; diff --git a/ts/components/leftPane/LeftPaneSearchHelper.tsx b/ts/components/leftPane/LeftPaneSearchHelper.tsx index ca4cffa29dbc..9a38fe8c4868 100644 --- a/ts/components/leftPane/LeftPaneSearchHelper.tsx +++ b/ts/components/leftPane/LeftPaneSearchHelper.tsx @@ -10,6 +10,14 @@ import { PropsData as ConversationListItemPropsType } from '../conversationList/ import { Intl } from '../Intl'; import { Emojify } from '../conversation/Emojify'; +import { assert } from '../../util/assert'; + +// The "correct" thing to do is to measure the size of the left pane and render enough +// search results for the container height. But (1) that's slow (2) the list is +// virtualized (3) 99 rows is over 6000px tall, taller than most monitors (4) it's fine +// if, in some extremely tall window, we have some empty space. So we just hard-code a +// fairly big number. +const SEARCH_RESULTS_FAKE_ROW_COUNT = 99; type MaybeLoadedSearchResultsType = | { isLoading: true } @@ -106,9 +114,14 @@ export class LeftPaneSearchHelper extends LeftPaneHelper< } getRowCount(): number { + if (this.isLoading()) { + // 1 for the header. + return 1 + SEARCH_RESULTS_FAKE_ROW_COUNT; + } + return this.allResults().reduce( (result: number, searchResults) => - result + getRowCountForSearchResult(searchResults), + result + getRowCountForLoadedSearchResults(searchResults), 0 ); } @@ -124,11 +137,21 @@ export class LeftPaneSearchHelper extends LeftPaneHelper< getRow(rowIndex: number): undefined | Row { const { conversationResults, contactResults, messageResults } = this; - const conversationRowCount = getRowCountForSearchResult( + if (this.isLoading()) { + if (rowIndex === 0) { + return { type: RowType.SearchResultsLoadingFakeHeader }; + } + if (rowIndex + 1 <= SEARCH_RESULTS_FAKE_ROW_COUNT) { + return { type: RowType.SearchResultsLoadingFakeRow }; + } + return undefined; + } + + const conversationRowCount = getRowCountForLoadedSearchResults( conversationResults ); - const contactRowCount = getRowCountForSearchResult(contactResults); - const messageRowCount = getRowCountForSearchResult(messageResults); + const contactRowCount = getRowCountForLoadedSearchResults(contactResults); + const messageRowCount = getRowCountForLoadedSearchResults(messageResults); if (rowIndex < conversationRowCount) { if (rowIndex === 0) { @@ -137,9 +160,10 @@ export class LeftPaneSearchHelper extends LeftPaneHelper< i18nKey: 'conversationsHeader', }; } - if (conversationResults.isLoading) { - return { type: RowType.Spinner }; - } + assert( + !conversationResults.isLoading, + "We shouldn't get here with conversation results still loading" + ); const conversation = conversationResults.results[rowIndex - 1]; return conversation ? { @@ -157,9 +181,10 @@ export class LeftPaneSearchHelper extends LeftPaneHelper< i18nKey: 'contactsHeader', }; } - if (contactResults.isLoading) { - return { type: RowType.Spinner }; - } + assert( + !contactResults.isLoading, + "We shouldn't get here with contact results still loading" + ); const conversation = contactResults.results[localIndex - 1]; return conversation ? { @@ -180,9 +205,10 @@ export class LeftPaneSearchHelper extends LeftPaneHelper< i18nKey: 'messagesHeader', }; } - if (messageResults.isLoading) { - return { type: RowType.Spinner }; - } + assert( + !messageResults.isLoading, + "We shouldn't get here with message results still loading" + ); const message = messageResults.results[localIndex - 1]; return message ? { @@ -192,11 +218,23 @@ export class LeftPaneSearchHelper extends LeftPaneHelper< : undefined; } + isScrollable(): boolean { + return !this.isLoading(); + } + shouldRecomputeRowHeights(old: Readonly): boolean { + const oldIsLoading = new LeftPaneSearchHelper(old).isLoading(); + const newIsLoading = this.isLoading(); + if (oldIsLoading && newIsLoading) { + return false; + } + if (oldIsLoading !== newIsLoading) { + return true; + } return searchResultKeys.some( key => - getRowCountForSearchResult(old[key]) !== - getRowCountForSearchResult(this[key]) + getRowCountForLoadedSearchResults(old[key]) !== + getRowCountForLoadedSearchResults(this[key]) ); } @@ -221,20 +259,27 @@ export class LeftPaneSearchHelper extends LeftPaneHelper< private allResults() { return [this.conversationResults, this.contactResults, this.messageResults]; } + + private isLoading(): boolean { + return this.allResults().some(results => results.isLoading); + } } -function getRowCountForSearchResult( +function getRowCountForLoadedSearchResults( searchResults: Readonly> ): number { - let hasHeader: boolean; - let resultRows: number; + // It's possible to call this helper with invalid results (e.g., ones that are loading). + // We could change the parameter of this function, but that adds a bunch of redundant + // checks that are, in the author's opinion, less clear. if (searchResults.isLoading) { - hasHeader = true; - resultRows = 1; // For the spinner. - } else { - const resultCount = searchResults.results.length; - hasHeader = Boolean(resultCount); - resultRows = resultCount; + assert( + false, + 'getRowCountForLoadedSearchResults: Expected this to be called with loaded search results. Returning 0' + ); + return 0; } + + const resultRows = searchResults.results.length; + const hasHeader = Boolean(resultRows); return (hasHeader ? 1 : 0) + resultRows; } diff --git a/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.ts b/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.ts index 3ee8aab1c572..37be98cf39a1 100644 --- a/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.ts +++ b/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.ts @@ -40,6 +40,39 @@ describe('LeftPaneSearchHelper', () => { }); describe('getRowCount', () => { + it('returns 100 if any results are loading', () => { + assert.strictEqual( + new LeftPaneSearchHelper({ + conversationResults: { isLoading: true }, + contactResults: { isLoading: true }, + messageResults: { isLoading: true }, + searchTerm: 'foo', + }).getRowCount(), + 100 + ); + assert.strictEqual( + new LeftPaneSearchHelper({ + conversationResults: { + isLoading: false, + results: [fakeConversation(), fakeConversation()], + }, + contactResults: { isLoading: true }, + messageResults: { isLoading: true }, + searchTerm: 'foo', + }).getRowCount(), + 100 + ); + assert.strictEqual( + new LeftPaneSearchHelper({ + conversationResults: { isLoading: true }, + contactResults: { isLoading: true }, + messageResults: { isLoading: false, results: [fakeMessage()] }, + searchTerm: 'foo', + }).getRowCount(), + 100 + ); + }); + it('returns 0 when there are no search results', () => { const helper = new LeftPaneSearchHelper({ conversationResults: { isLoading: false, results: [] }, @@ -51,17 +84,6 @@ describe('LeftPaneSearchHelper', () => { assert.strictEqual(helper.getRowCount(), 0); }); - it("returns 2 rows for each section of search results that's loading", () => { - const helper = new LeftPaneSearchHelper({ - conversationResults: { isLoading: true }, - contactResults: { isLoading: false, results: [] }, - messageResults: { isLoading: true }, - searchTerm: 'foo', - }); - - assert.strictEqual(helper.getRowCount(), 4); - }); - it('returns 1 + the number of results, dropping empty sections', () => { const helper = new LeftPaneSearchHelper({ conversationResults: { @@ -78,34 +100,41 @@ describe('LeftPaneSearchHelper', () => { }); describe('getRow', () => { - it('returns header + spinner for loading sections', () => { - const helper = new LeftPaneSearchHelper({ - conversationResults: { isLoading: true }, - contactResults: { isLoading: true }, - messageResults: { isLoading: true }, - searchTerm: 'foo', - }); + it('returns a "loading search results" row if any results are loading', () => { + const helpers = [ + new LeftPaneSearchHelper({ + conversationResults: { isLoading: true }, + contactResults: { isLoading: true }, + messageResults: { isLoading: true }, + searchTerm: 'foo', + }), + new LeftPaneSearchHelper({ + conversationResults: { + isLoading: false, + results: [fakeConversation(), fakeConversation()], + }, + contactResults: { isLoading: true }, + messageResults: { isLoading: true }, + searchTerm: 'foo', + }), + new LeftPaneSearchHelper({ + conversationResults: { isLoading: true }, + contactResults: { isLoading: true }, + messageResults: { isLoading: false, results: [fakeMessage()] }, + searchTerm: 'foo', + }), + ]; - assert.deepEqual(helper.getRow(0), { - type: RowType.Header, - i18nKey: 'conversationsHeader', - }); - assert.deepEqual(helper.getRow(1), { - type: RowType.Spinner, - }); - assert.deepEqual(helper.getRow(2), { - type: RowType.Header, - i18nKey: 'contactsHeader', - }); - assert.deepEqual(helper.getRow(3), { - type: RowType.Spinner, - }); - assert.deepEqual(helper.getRow(4), { - type: RowType.Header, - i18nKey: 'messagesHeader', - }); - assert.deepEqual(helper.getRow(5), { - type: RowType.Spinner, + helpers.forEach(helper => { + assert.deepEqual(helper.getRow(0), { + type: RowType.SearchResultsLoadingFakeHeader, + }); + for (let i = 1; i < 99; i += 1) { + assert.deepEqual(helper.getRow(i), { + type: RowType.SearchResultsLoadingFakeRow, + }); + } + assert.isUndefined(helper.getRow(100)); }); }); @@ -272,6 +301,54 @@ describe('LeftPaneSearchHelper', () => { assert.isUndefined(helper.getRow(5)); }); + describe('isScrollable', () => { + it('returns false if any results are loading', () => { + const helpers = [ + new LeftPaneSearchHelper({ + conversationResults: { isLoading: true }, + contactResults: { isLoading: true }, + messageResults: { isLoading: true }, + searchTerm: 'foo', + }), + new LeftPaneSearchHelper({ + conversationResults: { + isLoading: false, + results: [fakeConversation(), fakeConversation()], + }, + contactResults: { isLoading: true }, + messageResults: { isLoading: true }, + searchTerm: 'foo', + }), + new LeftPaneSearchHelper({ + conversationResults: { isLoading: true }, + contactResults: { isLoading: true }, + messageResults: { isLoading: false, results: [fakeMessage()] }, + searchTerm: 'foo', + }), + ]; + + helpers.forEach(helper => { + assert.isFalse(helper.isScrollable()); + }); + }); + + it('returns true if all results have loaded', () => { + const helper = new LeftPaneSearchHelper({ + conversationResults: { + isLoading: false, + results: [fakeConversation(), fakeConversation()], + }, + contactResults: { isLoading: false, results: [] }, + messageResults: { + isLoading: false, + results: [fakeMessage(), fakeMessage(), fakeMessage()], + }, + searchTerm: 'foo', + }); + assert.isTrue(helper.isScrollable()); + }); + }); + describe('shouldRecomputeRowHeights', () => { it("returns false if the number of results doesn't change", () => { const helper = new LeftPaneSearchHelper({ @@ -303,7 +380,7 @@ describe('LeftPaneSearchHelper', () => { ); }); - it('returns false when a section goes from loading to loaded with 1 result', () => { + it('returns false when a section completes loading, but not all sections are done (because the pane is still loading overall)', () => { const helper = new LeftPaneSearchHelper({ conversationResults: { isLoading: true }, contactResults: { isLoading: true }, @@ -324,6 +401,27 @@ describe('LeftPaneSearchHelper', () => { ); }); + it('returns true when all sections finish loading', () => { + const helper = new LeftPaneSearchHelper({ + conversationResults: { isLoading: true }, + contactResults: { isLoading: true }, + messageResults: { isLoading: false, results: [fakeMessage()] }, + searchTerm: 'foo', + }); + + assert.isTrue( + helper.shouldRecomputeRowHeights({ + conversationResults: { + isLoading: false, + results: [fakeConversation(), fakeConversation()], + }, + contactResults: { isLoading: false, results: [] }, + messageResults: { isLoading: false, results: [fakeMessage()] }, + searchTerm: 'foo', + }) + ); + }); + it('returns true if the number of results in a section changes', () => { const helper = new LeftPaneSearchHelper({ conversationResults: { diff --git a/ts/types/Util.ts b/ts/types/Util.ts index fc7b4875427d..3b7687c2c28e 100644 --- a/ts/types/Util.ts +++ b/ts/types/Util.ts @@ -29,3 +29,9 @@ export enum ThemeType { 'light' = 'light', 'dark' = 'dark', } + +// These are strings so they can be interpolated into class names. +export enum ScrollBehavior { + Default = 'default', + Hard = 'hard', +} diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 19cd3552469f..f5227a18db6e 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -14384,7 +14384,7 @@ "rule": "React-useRef", "path": "ts/components/ConversationList.js", "line": " const listRef = react_1.useRef(null);", - "lineNumber": 49, + "lineNumber": 58, "reasonCategory": "usageTrusted", "updated": "2021-02-12T16:25:08.285Z", "reasonDetail": "Used for scroll calculations"