diff --git a/app/sql.js b/app/sql.js index 82c59ee773..0807856881 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1017,6 +1017,69 @@ async function updateToSchemaVersion17(currentVersion, instance) { } } +async function updateToSchemaVersion18(currentVersion, instance) { + if (currentVersion >= 18) { + return; + } + + console.log('updateToSchemaVersion18: starting...'); + await instance.run('BEGIN TRANSACTION;'); + + try { + // Delete and rebuild full-text search index to capture everything + + await instance.run('DELETE FROM messages_fts;'); + await instance.run( + "INSERT INTO messages_fts(messages_fts) VALUES('rebuild');" + ); + + await instance.run(` + INSERT INTO messages_fts(id, body) + SELECT id, body FROM messages WHERE isViewOnce IS NULL OR isViewOnce != 1; + `); + + // Fixing full-text triggers + + await instance.run('DROP TRIGGER messages_on_insert;'); + await instance.run('DROP TRIGGER messages_on_update;'); + + await instance.run(` + CREATE TRIGGER messages_on_insert AFTER INSERT ON messages + WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 + BEGIN + INSERT INTO messages_fts ( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + `); + await instance.run(` + CREATE TRIGGER messages_on_update AFTER UPDATE ON messages + WHEN new.isViewOnce IS NULL OR new.isViewOnce != 1 + BEGIN + DELETE FROM messages_fts WHERE id = old.id; + INSERT INTO messages_fts( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + `); + + await instance.run('PRAGMA schema_version = 18;'); + await instance.run('COMMIT TRANSACTION;'); + console.log('updateToSchemaVersion18: success!'); + } catch (error) { + await instance.run('ROLLBACK;'); + throw error; + } +} + const SCHEMA_VERSIONS = [ updateToSchemaVersion1, updateToSchemaVersion2, @@ -1035,6 +1098,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion15, updateToSchemaVersion16, updateToSchemaVersion17, + updateToSchemaVersion18, ]; async function updateSchema(instance) { @@ -1552,7 +1616,7 @@ async function searchConversations(query, { limit } = {}) { $id: `%${query}%`, $name: `%${query}%`, $profileName: `%${query}%`, - $limit: limit || 50, + $limit: limit || 100, } ); @@ -1572,7 +1636,7 @@ async function searchMessages(query, { limit } = {}) { LIMIT $limit;`, { $query: query, - $limit: limit || 100, + $limit: limit || 500, } ); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 0b60d9c818..48e31b2a81 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -71,7 +71,6 @@ position: relative; max-width: 100%; margin: 0; - margin-bottom: 10px; .timeline-wrapper { -webkit-padding-start: 0px; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 72c4d7aa15..a80762fb88 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -3125,8 +3125,8 @@ // Module: Search Results .module-search-results { - overflow-y: scroll; - max-height: 100%; + overflow: hidden; + flex-grow: 1; } .module-search-results__conversations-header { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 688de2351f..072c6e3358 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -1584,7 +1584,7 @@ body.dark-theme { } .module-message-search-result__body { - color: $color-gray-05; + color: $color-gray-15; } // Module: Left Pane diff --git a/ts/components/LeftPane.md b/ts/components/LeftPane.md index 11fb6c9787..79eff21381 100644 --- a/ts/components/LeftPane.md +++ b/ts/components/LeftPane.md @@ -1,8 +1,196 @@ #### With search results ```jsx -window.searchResults = {}; -window.searchResults.conversations = [ +const items = [ + { + type: 'conversations-header', + data: undefined, + }, + { + type: 'conversation', + data: { + name: 'Everyone 🌆', + conversationType: 'group', + phoneNumber: '(202) 555-0011', + avatarPath: util.landscapeGreenObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: 'The rabbit hopped silently in the night.', + status: 'sent', + }, + }, + }, + { + type: 'conversation', + data: { + name: 'Everyone Else 🔥', + conversationType: 'direct', + phoneNumber: '(202) 555-0012', + avatarPath: util.landscapePurpleObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: "What's going on?", + status: 'sent', + }, + }, + }, + { + type: 'contacts-header', + data: undefined, + }, + { + type: 'contact', + data: { + name: 'The one Everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0013', + avatarPath: util.gifObjectUrl, + }, + }, + { + type: 'contact', + data: { + name: 'No likey everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0014', + color: 'red', + }, + }, + { + type: 'messages-header', + data: undefined, + }, +]; + +const messages = [ + { + from: { + isMe: true, + avatarPath: util.gifObjectUrl, + }, + to: { + name: 'Mr. Fire 🔥', + phoneNumber: '(202) 555-0015', + }, + id: '1-guid-guid-guid-guid-guid', + conversationId: '(202) 555-0015', + receivedAt: Date.now() - 5 * 60 * 1000, + snippet: '<>Everyone<>! Get in!', + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + name: 'Jon ❄️', + phoneNumber: '(202) 555-0016', + color: 'green', + }, + to: { + isMe: true, + }, + id: '2-guid-guid-guid-guid-guid', + conversationId: '(202) 555-0016', + snippet: 'Why is <>everyone<> so frustrated?', + receivedAt: Date.now() - 20 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + name: 'Someone', + phoneNumber: '(202) 555-0011', + color: 'green', + avatarPath: util.pngObjectUrl, + }, + to: { + name: "Y'all 🌆", + }, + id: '3-guid-guid-guid-guid-guid', + conversationId: 'EveryoneGroupID', + snippet: 'Hello, <>everyone<>! Woohooo!', + receivedAt: Date.now() - 24 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + isMe: true, + avatarPath: util.gifObjectUrl, + }, + to: { + name: "Y'all 🌆", + }, + id: '4-guid-guid-guid-guid-guid', + conversationId: 'EveryoneGroupID', + snippet: 'Well, <>everyone<>, happy new year!', + receivedAt: Date.now() - 24 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, +]; + +const messageLookup = util._.fromPairs( + util._.map(messages, message => [message.id, message]) +); +messages.forEach(message => { + items.push({ + type: 'message', + data: message.id, + }); +}); + +const searchResults = { + items, + openConversationInternal: (...args) => + console.log('openConversationInternal', args), + startNewConversation: (...args) => console.log('startNewConversation', args), + onStartNewConversation: (...args) => { + console.log('onStartNewConversation', args); + }, + renderMessageSearchResult: id => ( + + console.log('openConversationInternal', args) + } + /> + ), +}; + + + + console.log('startNewConversation', query, options) + } + openConversationInternal={(id, messageId) => + console.log('openConversation', id, messageId) + } + showArchivedConversations={() => console.log('showArchivedConversations')} + showInbox={() => console.log('showInbox')} + renderMainHeader={() => ( + console.log('search', result)} + updateSearch={result => console.log('updateSearch', result)} + clearSearch={result => console.log('clearSearch', result)} + i18n={util.i18n} + /> + )} + renderMessageSearchResult={id => ( + + console.log('openConversationInternal', args) + } + /> + )} + i18n={util.i18n} + /> +; +``` + +#### With just conversations + +```jsx +const conversations = [ { id: 'convo1', name: 'Everyone 🌆', @@ -50,85 +238,10 @@ window.searchResults.conversations = [ }, ]; -window.searchResults.contacts = [ - { - id: 'contact1', - name: 'The one Everyone', - conversationType: 'direct', - phoneNumber: '(202) 555-0013', - avatarPath: util.gifObjectUrl, - }, - { - id: 'contact2', - e: 'No likey everyone', - conversationType: 'direct', - phoneNumber: '(202) 555-0014', - color: 'red', - }, -]; - -window.searchResults.messages = [ - { - from: { - isMe: true, - avatarPath: util.gifObjectUrl, - }, - to: { - name: 'Mr. Fire 🔥', - phoneNumber: '(202) 555-0015', - }, - id: '1-guid-guid-guid-guid-guid', - conversationId: '(202) 555-0015', - receivedAt: Date.now() - 5 * 60 * 1000, - snippet: '<>Everyone<>! Get in!', - }, - { - from: { - name: 'Jon ❄️', - phoneNumber: '(202) 555-0016', - color: 'green', - }, - to: { - isMe: true, - }, - id: '2-guid-guid-guid-guid-guid', - conversationId: '(202) 555-0016', - snippet: 'Why is <>everyone<> so frustrated?', - receivedAt: Date.now() - 20 * 60 * 1000, - }, - { - from: { - name: 'Someone', - phoneNumber: '(202) 555-0011', - color: 'green', - avatarPath: util.pngObjectUrl, - }, - to: { - name: "Y'all 🌆", - }, - id: '3-guid-guid-guid-guid-guid', - conversationId: 'EveryoneGroupID', - snippet: 'Hello, <>everyone<>! Woohooo!', - receivedAt: Date.now() - 24 * 60 * 1000, - }, - { - from: { - isMe: true, - avatarPath: util.gifObjectUrl, - }, - to: { - name: "Y'all 🌆", - }, - id: '4-guid-guid-guid-guid-guid', - conversationId: 'EveryoneGroupID', - snippet: 'Well, <>everyone<>, happy new year!', - receivedAt: Date.now() - 24 * 60 * 1000, - }, -]; - console.log('startNewConversation', query, options) } @@ -151,42 +264,61 @@ window.searchResults.messages = [ ; ``` -#### With just conversations - -```jsx - - - console.log('startNewConversation', query, options) - } - openConversationInternal={(id, messageId) => - console.log('openConversation', id, messageId) - } - showArchivedConversations={() => console.log('showArchivedConversations')} - showInbox={() => console.log('showInbox')} - renderMainHeader={() => ( - console.log('search', result)} - updateSearch={result => console.log('updateSearch', result)} - clearSearch={result => console.log('clearSearch', result)} - i18n={util.i18n} - /> - )} - i18n={util.i18n} - /> - -``` - #### Showing inbox, with some archived ```jsx +const conversations = [ + { + id: 'convo1', + name: 'Everyone 🌆', + conversationType: 'group', + phoneNumber: '(202) 555-0011', + avatarPath: util.landscapeGreenObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: 'The rabbit hopped silently in the night.', + status: 'sent', + }, + }, + { + id: 'convo2', + name: 'Everyone Else 🔥', + conversationType: 'direct', + phoneNumber: '(202) 555-0012', + avatarPath: util.landscapePurpleObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: "What's going on?", + status: 'error', + }, + }, + { + id: 'convo3', + name: 'John the Turtle', + conversationType: 'direct', + phoneNumber: '(202) 555-0021', + lastUpdated: Date.now() - 24 * 60 * 60 * 1000, + lastMessage: { + text: 'I dunno', + }, + }, + { + id: 'convo4', + name: 'The Fly', + conversationType: 'direct', + phoneNumber: '(202) 555-0022', + avatarPath: util.pngObjectUrl, + lastUpdated: Date.now(), + lastMessage: { + text: 'Gimme!', + }, + }, +]; + console.log('startNewConversation', query, options) } @@ -206,16 +338,64 @@ window.searchResults.messages = [ )} i18n={util.i18n} /> - +; ``` #### Showing archived conversations ```jsx +const conversations = [ + { + id: 'convo1', + name: 'Everyone 🌆', + conversationType: 'group', + phoneNumber: '(202) 555-0011', + avatarPath: util.landscapeGreenObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: 'The rabbit hopped silently in the night.', + status: 'sent', + }, + }, + { + id: 'convo2', + name: 'Everyone Else 🔥', + conversationType: 'direct', + phoneNumber: '(202) 555-0012', + avatarPath: util.landscapePurpleObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: "What's going on?", + status: 'error', + }, + }, + { + id: 'convo3', + name: 'John the Turtle', + conversationType: 'direct', + phoneNumber: '(202) 555-0021', + lastUpdated: Date.now() - 24 * 60 * 60 * 1000, + lastMessage: { + text: 'I dunno', + }, + }, + { + id: 'convo4', + name: 'The Fly', + conversationType: 'direct', + phoneNumber: '(202) 555-0022', + avatarPath: util.pngObjectUrl, + lastUpdated: Date.now(), + lastMessage: { + text: 'Gimme!', + }, + }, +]; + console.log('startNewConversation', query, options) @@ -236,5 +416,5 @@ window.searchResults.messages = [ )} i18n={util.i18n} /> - +; ``` diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index ee68d4968d..cce19383f9 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -6,12 +6,12 @@ import { PropsData as ConversationListItemPropsType, } from './ConversationListItem'; import { - PropsData as SearchResultsProps, + PropsDataType as SearchResultsProps, SearchResults, } from './SearchResults'; import { LocalizerType } from '../types/Util'; -export interface Props { +export interface PropsType { conversations?: Array; archivedConversations?: Array; searchResults?: SearchResultsProps; @@ -30,6 +30,7 @@ export interface Props { // Render Props renderMainHeader: () => JSX.Element; + renderMessageSearchResult: (id: string) => JSX.Element; } // from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5 @@ -42,7 +43,7 @@ type RowRendererParamsType = { style: Object; }; -export class LeftPane extends React.Component { +export class LeftPane extends React.Component { public renderRow = ({ index, key, @@ -125,6 +126,7 @@ export class LeftPane extends React.Component { i18n, conversations, openConversationInternal, + renderMessageSearchResult, startNewConversation, searchResults, showArchived, @@ -134,8 +136,9 @@ export class LeftPane extends React.Component { return ( ); diff --git a/ts/components/MessageSearchResult.tsx b/ts/components/MessageSearchResult.tsx index e67d02bad5..3f01c9a6cb 100644 --- a/ts/components/MessageSearchResult.tsx +++ b/ts/components/MessageSearchResult.tsx @@ -8,7 +8,9 @@ import { ContactName } from './conversation/ContactName'; import { LocalizerType } from '../types/Util'; -export type PropsData = { +export type PropsDataType = { + isSelected?: boolean; + id: string; conversationId: string; receivedAt: number; @@ -33,16 +35,17 @@ export type PropsData = { }; }; -type PropsHousekeeping = { - isSelected?: boolean; - +type PropsHousekeepingType = { i18n: LocalizerType; - onClick: (conversationId: string, messageId?: string) => void; + openConversationInternal: ( + conversationId: string, + messageId?: string + ) => void; }; -type Props = PropsData & PropsHousekeeping; +type PropsType = PropsDataType & PropsHousekeepingType; -export class MessageSearchResult extends React.PureComponent { +export class MessageSearchResult extends React.PureComponent { public renderFromName() { const { from, i18n, to } = this.props; @@ -123,7 +126,7 @@ export class MessageSearchResult extends React.PureComponent { id, isSelected, conversationId, - onClick, + openConversationInternal, receivedAt, snippet, to, @@ -137,8 +140,8 @@ export class MessageSearchResult extends React.PureComponent {
{ - if (onClick) { - onClick(conversationId, id); + if (openConversationInternal) { + openConversationInternal(conversationId, id); } }} className={classNames( diff --git a/ts/components/SearchResults.md b/ts/components/SearchResults.md index e1c8e1ae5a..4b1804b9d6 100644 --- a/ts/components/SearchResults.md +++ b/ts/components/SearchResults.md @@ -1,48 +1,68 @@ #### With all result types ```jsx -window.searchResults = {}; -window.searchResults.conversations = [ +const items = [ { - name: 'Everyone 🌆', - conversationType: 'group', - phoneNumber: '(202) 555-0011', - avatarPath: util.landscapeGreenObjectUrl, - lastUpdated: Date.now() - 5 * 60 * 1000, - lastMessage: { - text: 'The rabbit hopped silently in the night.', - status: 'sent', + type: 'conversations-header', + data: undefined, + }, + { + type: 'conversation', + data: { + name: 'Everyone 🌆', + conversationType: 'group', + phoneNumber: '(202) 555-0011', + avatarPath: util.landscapeGreenObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: 'The rabbit hopped silently in the night.', + status: 'sent', + }, }, }, { - name: 'Everyone Else 🔥', - conversationType: 'direct', - phoneNumber: '(202) 555-0012', - avatarPath: util.landscapePurpleObjectUrl, - lastUpdated: Date.now() - 5 * 60 * 1000, - lastMessage: { - text: "What's going on?", - status: 'sent', + type: 'conversation', + data: { + name: 'Everyone Else 🔥', + conversationType: 'direct', + phoneNumber: '(202) 555-0012', + avatarPath: util.landscapePurpleObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: "What's going on?", + status: 'sent', + }, }, }, -]; - -window.searchResults.contacts = [ { - name: 'The one Everyone', - conversationType: 'direct', - phoneNumber: '(202) 555-0013', - avatarPath: util.gifObjectUrl, + type: 'contacts-header', + data: undefined, }, { - name: 'No likey everyone', - conversationType: 'direct', - phoneNumber: '(202) 555-0014', - color: 'red', + type: 'contact', + data: { + name: 'The one Everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0013', + avatarPath: util.gifObjectUrl, + }, + }, + { + type: 'contact', + data: { + name: 'No likey everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0014', + color: 'red', + }, + }, + { + type: 'messages-header', + data: undefined, }, ]; -window.searchResults.messages = [ +const messages = [ { from: { isMe: true, @@ -56,7 +76,7 @@ window.searchResults.messages = [ conversationId: '(202) 555-0015', receivedAt: Date.now() - 5 * 60 * 1000, snippet: '<>Everyone<>! Get in!', - onClick: () => console.log('onClick'), + conversationOpenInternal: () => console.log('onClick'), }, { from: { @@ -71,7 +91,7 @@ window.searchResults.messages = [ conversationId: '(202) 555-0016', snippet: 'Why is <>everyone<> so frustrated?', receivedAt: Date.now() - 20 * 60 * 1000, - onClick: () => console.log('onClick'), + conversationOpenInternal: () => console.log('onClick'), }, { from: { @@ -87,7 +107,7 @@ window.searchResults.messages = [ conversationId: 'EveryoneGroupID', snippet: 'Hello, <>everyone<>! Woohooo!', receivedAt: Date.now() - 24 * 60 * 1000, - onClick: () => console.log('onClick'), + conversationOpenInternal: () => console.log('onClick'), }, { from: { @@ -101,21 +121,45 @@ window.searchResults.messages = [ conversationId: 'EveryoneGroupID', snippet: 'Well, <>everyone<>, happy new year!', receivedAt: Date.now() - 24 * 60 * 1000, - onClick: () => console.log('onClick'), + conversationOpenInternal: () => console.log('onClick'), }, ]; - +const messageLookup = util._.fromPairs( + util._.map(messages, message => [message.id, message]) +); +messages.forEach(message => { + items.push({ + type: 'message', + data: message.id, + }); +}); + + console.log('onClickMessage', id)} - onClickConversation={id => console.log('onClickConversation', id)} - onStartNewConversation={(query, options) => - console.log('onStartNewConversation', query, options) + openConversationInternal={(...args) => + console.log('openConversationInternal', args) } + startNewConversation={(...args) => + console.log('startNewConversation', args) + } + onStartNewConversation={(...args) => + console.log('onStartNewConversation', args) + } + renderMessageSearchResult={id => ( + + console.log('openConversationInternal', args) + } + /> + )} /> ; ``` @@ -123,82 +167,562 @@ window.searchResults.messages = [ #### With 'start new conversation' ```jsx - +const items = [ + { + type: 'start-new-conversation', + data: undefined, + }, + { + type: 'conversations-header', + data: undefined, + }, + { + type: 'conversation', + data: { + name: 'Everyone 🌆', + conversationType: 'group', + phoneNumber: '(202) 555-0011', + avatarPath: util.landscapeGreenObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: 'The rabbit hopped silently in the night.', + status: 'sent', + }, + }, + }, + { + type: 'conversation', + data: { + name: 'Everyone Else 🔥', + conversationType: 'direct', + phoneNumber: '(202) 555-0012', + avatarPath: util.landscapePurpleObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: "What's going on?", + status: 'sent', + }, + }, + }, + { + type: 'contacts-header', + data: undefined, + }, + { + type: 'contact', + data: { + name: 'The one Everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0013', + avatarPath: util.gifObjectUrl, + }, + }, + { + type: 'contact', + data: { + name: 'No likey everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0014', + color: 'red', + }, + }, + { + type: 'messages-header', + data: undefined, + }, +]; + +const messages = [ + { + from: { + isMe: true, + avatarPath: util.gifObjectUrl, + }, + to: { + name: 'Mr. Fire 🔥', + phoneNumber: '(202) 555-0015', + }, + id: '1-guid-guid-guid-guid-guid', + conversationId: '(202) 555-0015', + receivedAt: Date.now() - 5 * 60 * 1000, + snippet: '<>Everyone<>! Get in!', + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + name: 'Jon ❄️', + phoneNumber: '(202) 555-0016', + color: 'green', + }, + to: { + isMe: true, + }, + id: '2-guid-guid-guid-guid-guid', + conversationId: '(202) 555-0016', + snippet: 'Why is <>everyone<> so frustrated?', + receivedAt: Date.now() - 20 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + name: 'Someone', + phoneNumber: '(202) 555-0011', + color: 'green', + avatarPath: util.pngObjectUrl, + }, + to: { + name: "Y'all 🌆", + }, + id: '3-guid-guid-guid-guid-guid', + conversationId: 'EveryoneGroupID', + snippet: 'Hello, <>everyone<>! Woohooo!', + receivedAt: Date.now() - 24 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + isMe: true, + avatarPath: util.gifObjectUrl, + }, + to: { + name: "Y'all 🌆", + }, + id: '4-guid-guid-guid-guid-guid', + conversationId: 'EveryoneGroupID', + snippet: 'Well, <>everyone<>, happy new year!', + receivedAt: Date.now() - 24 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, +]; + +const messageLookup = util._.fromPairs( + util._.map(messages, message => [message.id, message]) +); +messages.forEach(message => { + items.push({ + type: 'message', + data: message.id, + }); +}); + + console.log('onClickMessage', id)} - onClickConversation={id => console.log('onClickConversation', id)} - onStartNewConversation={(query, options) => - console.log('onStartNewConversation', query, options) + searchTerm="(202) 555-0015" + openConversationInternal={(...args) => + console.log('openConversationInternal', args) } + startNewConversation={(...args) => + console.log('startNewConversation', args) + } + onStartNewConversation={(...args) => + console.log('onStartNewConversation', args) + } + renderMessageSearchResult={id => ( + + console.log('openConversationInternal', args) + } + /> + )} /> - +; ``` #### With no conversations ```jsx - +const items = [ + { + type: 'contacts-header', + data: undefined, + }, + { + type: 'contact', + data: { + name: 'The one Everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0013', + avatarPath: util.gifObjectUrl, + }, + }, + { + type: 'contact', + data: { + name: 'No likey everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0014', + color: 'red', + }, + }, + { + type: 'messages-header', + data: undefined, + }, +]; + +const messages = [ + { + from: { + isMe: true, + avatarPath: util.gifObjectUrl, + }, + to: { + name: 'Mr. Fire 🔥', + phoneNumber: '(202) 555-0015', + }, + id: '1-guid-guid-guid-guid-guid', + conversationId: '(202) 555-0015', + receivedAt: Date.now() - 5 * 60 * 1000, + snippet: '<>Everyone<>! Get in!', + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + name: 'Jon ❄️', + phoneNumber: '(202) 555-0016', + color: 'green', + }, + to: { + isMe: true, + }, + id: '2-guid-guid-guid-guid-guid', + conversationId: '(202) 555-0016', + snippet: 'Why is <>everyone<> so frustrated?', + receivedAt: Date.now() - 20 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + name: 'Someone', + phoneNumber: '(202) 555-0011', + color: 'green', + avatarPath: util.pngObjectUrl, + }, + to: { + name: "Y'all 🌆", + }, + id: '3-guid-guid-guid-guid-guid', + conversationId: 'EveryoneGroupID', + snippet: 'Hello, <>everyone<>! Woohooo!', + receivedAt: Date.now() - 24 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + isMe: true, + avatarPath: util.gifObjectUrl, + }, + to: { + name: "Y'all 🌆", + }, + id: '4-guid-guid-guid-guid-guid', + conversationId: 'EveryoneGroupID', + snippet: 'Well, <>everyone<>, happy new year!', + receivedAt: Date.now() - 24 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, +]; + +const messageLookup = util._.fromPairs( + util._.map(messages, message => [message.id, message]) +); +messages.forEach(message => { + items.push({ + type: 'message', + data: message.id, + }); +}); + + console.log('onClickMessage', id)} - onClickConversation={id => console.log('onClickConversation', id)} - onStartNewConversation={(query, options) => - console.log('onStartNewConversation', query, options) + openConversationInternal={(...args) => + console.log('openConversationInternal', args) } + startNewConversation={(...args) => + console.log('startNewConversation', args) + } + onStartNewConversation={(...args) => + console.log('onStartNewConversation', args) + } + renderMessageSearchResult={id => ( + + console.log('openConversationInternal', args) + } + /> + )} /> - +; ``` #### With no contacts ```jsx - +const items = [ + { + type: 'conversations-header', + data: undefined, + }, + { + type: 'conversation', + data: { + name: 'Everyone 🌆', + conversationType: 'group', + phoneNumber: '(202) 555-0011', + avatarPath: util.landscapeGreenObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: 'The rabbit hopped silently in the night.', + status: 'sent', + }, + }, + }, + { + type: 'conversation', + data: { + name: 'Everyone Else 🔥', + conversationType: 'direct', + phoneNumber: '(202) 555-0012', + avatarPath: util.landscapePurpleObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: "What's going on?", + status: 'sent', + }, + }, + }, + { + type: 'messages-header', + data: undefined, + }, +]; + +const messages = [ + { + from: { + isMe: true, + avatarPath: util.gifObjectUrl, + }, + to: { + name: 'Mr. Fire 🔥', + phoneNumber: '(202) 555-0015', + }, + id: '1-guid-guid-guid-guid-guid', + conversationId: '(202) 555-0015', + receivedAt: Date.now() - 5 * 60 * 1000, + snippet: '<>Everyone<>! Get in!', + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + name: 'Jon ❄️', + phoneNumber: '(202) 555-0016', + color: 'green', + }, + to: { + isMe: true, + }, + id: '2-guid-guid-guid-guid-guid', + conversationId: '(202) 555-0016', + snippet: 'Why is <>everyone<> so frustrated?', + receivedAt: Date.now() - 20 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + name: 'Someone', + phoneNumber: '(202) 555-0011', + color: 'green', + avatarPath: util.pngObjectUrl, + }, + to: { + name: "Y'all 🌆", + }, + id: '3-guid-guid-guid-guid-guid', + conversationId: 'EveryoneGroupID', + snippet: 'Hello, <>everyone<>! Woohooo!', + receivedAt: Date.now() - 24 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, + { + from: { + isMe: true, + avatarPath: util.gifObjectUrl, + }, + to: { + name: "Y'all 🌆", + }, + id: '4-guid-guid-guid-guid-guid', + conversationId: 'EveryoneGroupID', + snippet: 'Well, <>everyone<>, happy new year!', + receivedAt: Date.now() - 24 * 60 * 1000, + conversationOpenInternal: () => console.log('onClick'), + }, +]; + +const messageLookup = util._.fromPairs( + util._.map(messages, message => [message.id, message]) +); +messages.forEach(message => { + items.push({ + type: 'message', + data: message.id, + }); +}); + + console.log('onClickMessage', id)} - onClickConversation={id => console.log('onClickConversation', id)} - onStartNewConversation={(query, options) => - console.log('onStartNewConversation', query, options) + openConversationInternal={(...args) => + console.log('openConversationInternal', args) } + startNewConversation={(...args) => + console.log('startNewConversation', args) + } + onStartNewConversation={(...args) => + console.log('onStartNewConversation', args) + } + renderMessageSearchResult={id => ( + + console.log('openConversationInternal', args) + } + /> + )} /> - +; ``` #### With no messages ```jsx - +const items = [ + { + type: 'conversations-header', + data: undefined, + }, + { + type: 'conversation', + data: { + name: 'Everyone 🌆', + conversationType: 'group', + phoneNumber: '(202) 555-0011', + avatarPath: util.landscapeGreenObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: 'The rabbit hopped silently in the night.', + status: 'sent', + }, + }, + }, + { + type: 'conversation', + data: { + name: 'Everyone Else 🔥', + conversationType: 'direct', + phoneNumber: '(202) 555-0012', + avatarPath: util.landscapePurpleObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: "What's going on?", + status: 'sent', + }, + }, + }, + { + type: 'contacts-header', + data: undefined, + }, + { + type: 'contact', + data: { + name: 'The one Everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0013', + avatarPath: util.gifObjectUrl, + }, + }, + { + type: 'contact', + data: { + name: 'No likey everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0014', + color: 'red', + }, + }, +]; + + + console.log('openConversationInternal', args) + } + startNewConversation={(...args) => + console.log('startNewConversation', args) + } + onStartNewConversation={(...args) => + console.log('onStartNewConversation', args) + } /> - +; ``` #### With no results at all ```jsx - + + console.log('openConversationInternal', args) + } + startNewConversation={(...args) => + console.log('startNewConversation', args) + } + onStartNewConversation={(...args) => + console.log('onStartNewConversation', args) + } + renderMessageSearchResult={id => ( + + console.log('openConversationInternal', args) + } + /> + )} /> ``` @@ -206,6 +730,67 @@ window.searchResults.messages = [ #### With a lot of results ```jsx +const items = [ + { + type: 'conversations-header', + data: undefined, + }, + { + type: 'conversation', + data: { + name: 'Everyone 🌆', + conversationType: 'group', + phoneNumber: '(202) 555-0011', + avatarPath: util.landscapeGreenObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: 'The rabbit hopped silently in the night.', + status: 'sent', + }, + }, + }, + { + type: 'conversation', + data: { + name: 'Everyone Else 🔥', + conversationType: 'direct', + phoneNumber: '(202) 555-0012', + avatarPath: util.landscapePurpleObjectUrl, + lastUpdated: Date.now() - 5 * 60 * 1000, + lastMessage: { + text: "What's going on?", + status: 'sent', + }, + }, + }, + { + type: 'contacts-header', + data: undefined, + }, + { + type: 'contact', + data: { + name: 'The one Everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0013', + avatarPath: util.gifObjectUrl, + }, + }, + { + type: 'contact', + data: { + name: 'No likey everyone', + conversationType: 'direct', + phoneNumber: '(202) 555-0014', + color: 'red', + }, + }, + { + type: 'messages-header', + data: undefined, + }, +]; + const messages = []; for (let i = 0; i < 100; i += 1) { messages.push({ @@ -221,16 +806,45 @@ for (let i = 0; i < 100; i += 1) { conversationId: '(202) 555-0015', receivedAt: Date.now() - 5 * 60 * 1000, snippet: `${i} <>Everyone<>! Get in!`, - onClick: data => console.log('onClick', data), + conversationOpenInternal: data => console.log('onClick', data), }); } - +const messageLookup = util._.fromPairs( + util._.map(messages, message => [message.id, message]) +); +messages.forEach(message => { + items.push({ + type: 'message', + data: message.id, + }); +}); + + + console.log('openConversationInternal', args) + } + startNewConversation={(...args) => + console.log('startNewConversation', args) + } + onStartNewConversation={(...args) => + console.log('onStartNewConversation', args) + } + renderMessageSearchResult={id => ( + + console.log('openConversationInternal', args) + } + /> + )} /> ; ``` @@ -238,6 +852,8 @@ for (let i = 0; i < 100; i += 1) { #### With just messages and no header ```jsx +const items = []; + const messages = []; for (let i = 0; i < 10; i += 1) { messages.push({ @@ -253,15 +869,45 @@ for (let i = 0; i < 10; i += 1) { conversationId: '(202) 555-0015', receivedAt: Date.now() - 5 * 60 * 1000, snippet: `${i} <>Everyone<>! Get in!`, - onClick: data => console.log('onClick', data), + conversationOpenInternal: data => console.log('onClick', data), }); } - +const messageLookup = util._.fromPairs( + util._.map(messages, message => [message.id, message]) +); +messages.forEach(message => { + items.push({ + type: 'message', + data: message.id, + }); +}); + + + console.log('openConversationInternal', args) + } + startNewConversation={(...args) => + console.log('startNewConversation', args) + } + onStartNewConversation={(...args) => + console.log('onStartNewConversation', args) + } + renderMessageSearchResult={id => ( + + console.log('openConversationInternal', args) + } + /> + )} /> ; ``` diff --git a/ts/components/SearchResults.tsx b/ts/components/SearchResults.tsx index f645700ed9..5637f6c4e1 100644 --- a/ts/components/SearchResults.tsx +++ b/ts/components/SearchResults.tsx @@ -1,123 +1,277 @@ import React from 'react'; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from 'react-virtualized'; + import { ConversationListItem, PropsData as ConversationListItemPropsType, } from './ConversationListItem'; -import { MessageSearchResult } from './MessageSearchResult'; import { StartNewConversation } from './StartNewConversation'; import { LocalizerType } from '../types/Util'; -export type PropsData = { - contacts: Array; - conversations: Array; - hideMessagesHeader: boolean; - messages: Array; +export type PropsDataType = { + items: Array; + noResults: boolean; regionCode: string; searchTerm: string; - showStartNewConversation: boolean; }; -type PropsHousekeeping = { +type StartNewConversationType = { + type: 'start-new-conversation'; + data: undefined; +}; +type ConversationHeaderType = { + type: 'conversations-header'; + data: undefined; +}; +type ContactsHeaderType = { + type: 'contacts-header'; + data: undefined; +}; +type MessagesHeaderType = { + type: 'messages-header'; + data: undefined; +}; +type ConversationType = { + type: 'conversation'; + data: ConversationListItemPropsType; +}; +type ContactsType = { + type: 'contact'; + data: ConversationListItemPropsType; +}; +type MessageType = { + type: 'message'; + data: string; +}; + +export type SearchResultRowType = + | StartNewConversationType + | ConversationHeaderType + | ContactsHeaderType + | MessagesHeaderType + | ConversationType + | ContactsType + | MessageType; + +type PropsHousekeepingType = { i18n: LocalizerType; - openConversation: (id: string, messageId?: string) => void; + openConversationInternal: (id: string, messageId?: string) => void; startNewConversation: ( query: string, options: { regionCode: string } ) => void; + + renderMessageSearchResult: (id: string) => JSX.Element; }; -type Props = PropsData & PropsHousekeeping; +type PropsType = PropsDataType & PropsHousekeepingType; + +// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5 +type RowRendererParamsType = { + index: number; + isScrolling: boolean; + isVisible: boolean; + key: string; + parent: Object; + style: Object; +}; + +export class SearchResults extends React.Component { + public mostRecentWidth = 0; + public mostRecentHeight = 0; + public cellSizeCache = new CellMeasurerCache({ + defaultHeight: 36, + fixedWidth: true, + }); + public listRef = React.createRef(); -export class SearchResults extends React.Component { public handleStartNewConversation = () => { const { regionCode, searchTerm, startNewConversation } = this.props; startNewConversation(searchTerm, { regionCode }); }; - public render() { + public renderRowContents(row: SearchResultRowType) { const { - conversations, - contacts, - hideMessagesHeader, - i18n, - messages, - openConversation, searchTerm, - showStartNewConversation, + i18n, + openConversationInternal, + renderMessageSearchResult, } = this.props; - const haveConversations = conversations && conversations.length; - const haveContacts = contacts && contacts.length; - const haveMessages = messages && messages.length; - const noResults = - !showStartNewConversation && - !haveConversations && - !haveContacts && - !haveMessages; + if (row.type === 'start-new-conversation') { + return ( + + ); + } else if (row.type === 'conversations-header') { + return ( +
+ {i18n('conversationsHeader')} +
+ ); + } else if (row.type === 'conversation') { + const { data } = row; + + return ( + + ); + } else if (row.type === 'contacts-header') { + return ( +
+ {i18n('contactsHeader')} +
+ ); + } else if (row.type === 'contact') { + const { data } = row; + + return ( + + ); + } else if (row.type === 'messages-header') { + return ( +
+ {i18n('messagesHeader')} +
+ ); + } else if (row.type === 'message') { + const { data } = row; + + return renderMessageSearchResult(data); + } else { + throw new Error( + 'SearchResults.renderRowContents: Encountered unknown row type' + ); + } + } + + public renderRow = ({ + index, + key, + parent, + style, + }: RowRendererParamsType): JSX.Element => { + const { items } = this.props; + + const row = items[index]; return ( -
- {noResults ? ( +
+ + {this.renderRowContents(row)} + +
+ ); + }; + + public componentDidUpdate(prevProps: PropsType) { + const { items } = this.props; + + if ( + items && + items.length > 0 && + prevProps.items && + prevProps.items.length > 0 && + items !== prevProps.items + ) { + this.resizeAll(); + } + } + + public getList = () => { + if (!this.listRef) { + return; + } + + const { current } = this.listRef; + + return current; + }; + + public recomputeRowHeights = (row?: number) => { + const list = this.getList(); + if (!list) { + return; + } + + list.recomputeRowHeights(row); + }; + + public resizeAll = () => { + this.cellSizeCache.clearAll(); + + const rowCount = this.getRowCount(); + this.recomputeRowHeights(rowCount - 1); + }; + + public getRowCount() { + const { items } = this.props; + + return items ? items.length : 0; + } + + public render() { + const { items, i18n, noResults, searchTerm } = this.props; + + if (noResults) { + return ( +
{i18n('noSearchResults', [searchTerm])}
- ) : null} - {showStartNewConversation ? ( - - ) : null} - {haveConversations ? ( -
-
- {i18n('conversationsHeader')} -
- {conversations.map(conversation => ( - + ); + } + + return ( +
+ + {({ height, width }) => { + this.mostRecentWidth = width; + this.mostRecentHeight = height; + + return ( + - ))} -
- ) : null} - {haveContacts ? ( -
-
- {i18n('contactsHeader')} -
- {contacts.map(contact => ( - - ))} -
- ) : null} - {haveMessages ? ( -
- {hideMessagesHeader ? null : ( -
- {i18n('messagesHeader')} -
- )} - {messages.map(message => ( - - ))} -
- ) : null} + ); + }} +
); } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 3799cbb066..bf367875f8 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -14,32 +14,6 @@ import { NoopActionType } from './noop'; // State -export type MessageSearchResultType = { - id: string; - conversationId: string; - receivedAt: number; - - snippet: string; - - from: { - phoneNumber: string; - isMe?: boolean; - name?: string; - color?: string; - profileName?: string; - avatarPath?: string; - }; - - to: { - groupName?: string; - phoneNumber: string; - isMe?: boolean; - name?: string; - profileName?: string; - }; - - isSelected?: boolean; -}; export type ConversationType = { id: string; name?: string; diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 4230d687a4..8aa311defb 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -9,25 +9,31 @@ import { makeLookup } from '../../util/makeLookup'; import { ConversationType, MessageDeletedActionType, - MessageSearchResultType, + MessageType, RemoveAllConversationsActionType, SelectedConversationChangedActionType, } from './conversations'; // State +export type MessageSearchResultType = MessageType & { + snippet: string; +}; + +export type MessageSearchResultLookupType = { + [id: string]: MessageSearchResultType; +}; + export type SearchStateType = { + // We store just ids of conversations, since that data is always cached in memory + contacts: Array; + conversations: Array; query: string; normalizedPhoneNumber?: string; - // We need to store messages here, because they aren't anywhere else in state - messages: Array; + messageIds: Array; + // We do store message data to pass through the selector + messageLookup: MessageSearchResultLookupType; selectedMessage?: string; - messageLookup: { - [key: string]: MessageSearchResultType; - }; - // For conversations we store just the id, and pull conversation props in the selector - conversations: Array; - contacts: Array; }; // Actions @@ -193,7 +199,7 @@ async function queryConversationsAndContacts( function getEmptyState(): SearchStateType { return { query: '', - messages: [], + messageIds: [], messageLookup: {}, conversations: [], contacts: [], @@ -220,16 +226,28 @@ export function reducer( if (action.type === 'SEARCH_RESULTS_FULFILLED') { const { payload } = action; - const { query, messages } = payload; + const { + contacts, + conversations, + messages, + normalizedPhoneNumber, + query, + } = payload; // Reject if the associated query is not the most recent user-provided query if (state.query !== query) { return state; } + const messageIds = messages.map(message => message.id); + return { ...state, - ...payload, + contacts, + conversations, + normalizedPhoneNumber, + query, + messageIds, messageLookup: makeLookup(messages, 'id'), }; } @@ -253,8 +271,8 @@ export function reducer( } if (action.type === 'MESSAGE_DELETED') { - const { messages, messageLookup } = state; - if (!messages.length) { + const { messageIds, messageLookup } = state; + if (!messageIds || messageIds.length < 1) { return state; } @@ -263,7 +281,7 @@ export function reducer( return { ...state, - messages: reject(messages, message => id === message.id), + messageIds: reject(messageIds, messageId => id === messageId), messageLookup: omit(messageLookup, ['id']), }; } diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index d2a4869cb2..26904d304c 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -207,11 +207,13 @@ export const getCachedSelectorForConversation = createSelector( (): CachedConversationSelectorType => { // Note: memoizee will check all parameters provided, and only run our selector // if any of them have changed. - return memoizee(_conversationSelector, { max: 100 }); + return memoizee(_conversationSelector, { max: 2000 }); } ); -type GetConversationByIdType = (id: string) => ConversationType | undefined; +export type GetConversationByIdType = ( + id: string +) => ConversationType | undefined; export const getConversationSelector = createSelector( getCachedSelectorForConversation, getConversationLookup, @@ -287,7 +289,7 @@ export const getCachedSelectorForMessage = createSelector( (): CachedMessageSelectorType => { // Note: memoizee will check all parameters provided, and only run our selector // if any of them have changed. - return memoizee(_messageSelector, { max: 500 }); + return memoizee(_messageSelector, { max: 2000 }); } ); diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index 6d0a09415d..b50d137328 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -1,17 +1,32 @@ -import { compact } from 'lodash'; +import memoizee from 'memoizee'; import { createSelector } from 'reselect'; import { getSearchResultsProps } from '../../shims/Whisper'; import { StateType } from '../reducer'; -import { SearchStateType } from '../ducks/search'; import { + MessageSearchResultLookupType, + MessageSearchResultType, + SearchStateType, +} from '../ducks/search'; +import { + ConversationLookupType, + ConversationType, +} from '../ducks/conversations'; + +import { + PropsDataType as SearchResultsPropsType, + SearchResultRowType, +} from '../../components/SearchResults'; +import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/MessageSearchResult'; + +import { getRegionCode, getUserNumber } from './user'; +import { + GetConversationByIdType, getConversationLookup, + getConversationSelector, getSelectedConversation, } from './conversations'; -import { ConversationLookupType } from '../ducks/conversations'; - -import { getRegionCode } from './user'; export const getSearch = (state: StateType): SearchStateType => state.search; @@ -34,68 +49,182 @@ export const isSearching = createSelector( } ); +export const getMessageSearchResultLookup = createSelector( + getSearch, + (state: SearchStateType) => state.messageLookup +); + export const getSearchResults = createSelector( - [ - getSearch, - getRegionCode, - getConversationLookup, - getSelectedConversation, - getSelectedMessage, - ], + [getSearch, getRegionCode, getConversationLookup, getSelectedConversation], ( state: SearchStateType, regionCode: string, lookup: ConversationLookupType, - selectedConversation?: string, - selectedMessage?: string - ) => { + selectedConversation?: string + ): SearchResultsPropsType => { + const { conversations, contacts, messageIds } = state; + + const showStartNewConversation = Boolean( + state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber] + ); + const haveConversations = conversations && conversations.length; + const haveContacts = contacts && contacts.length; + const haveMessages = messageIds && messageIds.length; + const noResults = + !showStartNewConversation && + !haveConversations && + !haveContacts && + !haveMessages; + + const items: Array = []; + + if (showStartNewConversation) { + items.push({ + type: 'start-new-conversation', + data: undefined, + }); + } + + if (haveConversations) { + items.push({ + type: 'conversations-header', + data: undefined, + }); + conversations.forEach(id => { + const data = lookup[id]; + items.push({ + type: 'conversation', + data: { + ...data, + isSelected: Boolean(data && id === selectedConversation), + }, + }); + }); + } + + if (haveContacts) { + items.push({ + type: 'contacts-header', + data: undefined, + }); + contacts.forEach(id => { + const data = lookup[id]; + items.push({ + type: 'contact', + data: { + ...data, + isSelected: Boolean(data && id === selectedConversation), + }, + }); + }); + } + + if (haveMessages) { + items.push({ + type: 'messages-header', + data: undefined, + }); + messageIds.forEach(messageId => { + items.push({ + type: 'message', + data: messageId, + }); + }); + } + return { - contacts: compact( - state.contacts.map(id => { - const value = lookup[id]; - - if (value && id === selectedConversation) { - return { - ...value, - isSelected: true, - }; - } - - return value; - }) - ), - conversations: compact( - state.conversations.map(id => { - const value = lookup[id]; - - if (value && id === selectedConversation) { - return { - ...value, - isSelected: true, - }; - } - - return value; - }) - ), - hideMessagesHeader: false, - messages: state.messages.map(message => { - const props = getSearchResultsProps(message); - - if (message.id === selectedMessage) { - return { - ...props, - isSelected: true, - }; - } - - return props; - }), + items, + noResults, regionCode: regionCode, searchTerm: state.query, - showStartNewConversation: Boolean( - state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber] - ), + }; + } +); + +export function _messageSearchResultSelector( + message: MessageSearchResultType, + // @ts-ignore + ourNumber: string, + // @ts-ignore + regionCode: string, + // @ts-ignore + sender?: ConversationType, + // @ts-ignore + recipient?: ConversationType, + selectedMessageId?: string +): MessageSearchResultPropsDataType { + // Note: We don't use all of those parameters here, but the shim we call does. + // We want to call this function again if any of those parameters change. + return { + ...getSearchResultsProps(message), + isSelected: message.id === selectedMessageId, + }; +} + +// A little optimization to reset our selector cache whenever high-level application data +// changes: regionCode and userNumber. +type CachedMessageSearchResultSelectorType = ( + message: MessageSearchResultType, + ourNumber: string, + regionCode: string, + sender?: ConversationType, + recipient?: ConversationType, + selectedMessageId?: string +) => MessageSearchResultPropsDataType; +export const getCachedSelectorForMessageSearchResult = createSelector( + getRegionCode, + getUserNumber, + (): CachedMessageSearchResultSelectorType => { + // Note: memoizee will check all parameters provided, and only run our selector + // if any of them have changed. + return memoizee(_messageSearchResultSelector, { max: 500 }); + } +); + +type GetMessageSearchResultByIdType = ( + id: string +) => MessageSearchResultPropsDataType | undefined; +export const getMessageSearchResultSelector = createSelector( + getCachedSelectorForMessageSearchResult, + getMessageSearchResultLookup, + getSelectedMessage, + getConversationSelector, + getRegionCode, + getUserNumber, + ( + messageSearchResultSelector: CachedMessageSearchResultSelectorType, + messageSearchResultLookup: MessageSearchResultLookupType, + selectedMessage: string | undefined, + conversationSelector: GetConversationByIdType, + regionCode: string, + ourNumber: string + ): GetMessageSearchResultByIdType => { + return (id: string) => { + const message = messageSearchResultLookup[id]; + if (!message) { + return; + } + + const { conversationId, source, type } = message; + let sender: ConversationType | undefined; + let recipient: ConversationType | undefined; + + if (type === 'incoming') { + sender = conversationSelector(source); + recipient = conversationSelector(ourNumber); + } else if (type === 'outgoing') { + sender = conversationSelector(ourNumber); + recipient = conversationSelector(conversationId); + } + + return messageSearchResultSelector( + message, + ourNumber, + regionCode, + sender, + recipient, + selectedMessage + ); }; } ); diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx index 86c7c48751..5a9abcb8b7 100644 --- a/ts/state/smart/LeftPane.tsx +++ b/ts/state/smart/LeftPane.tsx @@ -9,14 +9,19 @@ import { getIntl } from '../selectors/user'; import { getLeftPaneLists, getShowArchived } from '../selectors/conversations'; import { SmartMainHeader } from './MainHeader'; +import { SmartMessageSearchResult } from './MessageSearchResult'; // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 const FilteredSmartMainHeader = SmartMainHeader as any; +const FilteredSmartMessageSearchResult = SmartMessageSearchResult as any; function renderMainHeader(): JSX.Element { return ; } +function renderMessageSearchResult(id: string): JSX.Element { + return ; +} const mapStateToProps = (state: StateType) => { const showSearch = isSearching(state); @@ -30,6 +35,7 @@ const mapStateToProps = (state: StateType) => { showArchived: getShowArchived(state), i18n: getIntl(state), renderMainHeader, + renderMessageSearchResult, }; }; diff --git a/ts/state/smart/MessageSearchResult.tsx b/ts/state/smart/MessageSearchResult.tsx index 3a45803b07..3222536a36 100644 --- a/ts/state/smart/MessageSearchResult.tsx +++ b/ts/state/smart/MessageSearchResult.tsx @@ -1,9 +1,11 @@ import { connect } from 'react-redux'; -import { mapDispatchToProps } from '../actions'; +import { mapDispatchToProps } from '../actions'; import { StateType } from '../reducer'; import { MessageSearchResult } from '../../components/MessageSearchResult'; +import { getIntl } from '../selectors/user'; +import { getMessageSearchResultSelector } from '../selectors/search'; type SmartProps = { id: string; @@ -11,12 +13,13 @@ type SmartProps = { function mapStateToProps(state: StateType, ourProps: SmartProps) { const { id } = ourProps; - const lookup = state.search && state.search.messageLookup; - if (!lookup) { - return null; - } - return lookup[id]; + const props = getMessageSearchResultSelector(state)(id); + + return { + ...props, + i18n: getIntl(state), + }; } const smart = connect(mapStateToProps, mapDispatchToProps); diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index 089f229c55..094197fa09 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -1,8 +1,9 @@ import { connect } from 'react-redux'; + import { mapDispatchToProps } from '../actions'; -import { TimelineItem } from '../../components/conversation/TimelineItem'; import { StateType } from '../reducer'; +import { TimelineItem } from '../../components/conversation/TimelineItem'; import { getIntl } from '../selectors/user'; import { getMessageSelector } from '../selectors/conversations'; diff --git a/ts/styleguide/LeftPaneContext.tsx b/ts/styleguide/LeftPaneContext.tsx index db054e72a1..2776ddb70e 100644 --- a/ts/styleguide/LeftPaneContext.tsx +++ b/ts/styleguide/LeftPaneContext.tsx @@ -7,6 +7,7 @@ interface Props { */ theme: 'light-theme' | 'dark-theme'; style: any; + gutterStyle: any; } /** @@ -15,11 +16,13 @@ interface Props { */ export class LeftPaneContext extends React.Component { public render() { - const { style, theme } = this.props; + const { gutterStyle, style, theme } = this.props; return (
-
{this.props.children}
+
+ {this.props.children} +
); } diff --git a/ts/types/PhoneNumber.ts b/ts/types/PhoneNumber.ts index 1c6c2ff087..48fd5b83a2 100644 --- a/ts/types/PhoneNumber.ts +++ b/ts/types/PhoneNumber.ts @@ -26,6 +26,7 @@ export const format = memoizee(_format, { primitive: true, // Convert the arguments to a unique string, required for primitive mode. normalizer: (...args) => JSON.stringify(args), + max: 5000, }); export function parse( diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 1e6f02222a..1243b72414 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -7770,6 +7770,24 @@ "updated": "2019-03-09T00:08:44.242Z", "reasonDetail": "Used only to set focus" }, + { + "rule": "DOM-innerHTML", + "path": "ts/components/CompositionArea.js", + "line": " el.innerHTML = '';", + "lineNumber": 22, + "reasonCategory": "usageTrusted", + "updated": "2019-08-01T14:10:37.481Z", + "reasonDetail": "Our code, no user input, only clearing out the dom" + }, + { + "rule": "DOM-innerHTML", + "path": "ts/components/CompositionArea.tsx", + "line": " el.innerHTML = '';", + "lineNumber": 65, + "reasonCategory": "usageTrusted", + "updated": "2019-08-01T14:10:37.481Z", + "reasonDetail": "Our code, no user input, only clearing out the dom" + }, { "rule": "React-createRef", "path": "ts/components/Lightbox.js", @@ -7806,6 +7824,15 @@ "updated": "2019-03-09T00:08:44.242Z", "reasonDetail": "Used only to set focus" }, + { + "rule": "React-createRef", + "path": "ts/components/SearchResults.js", + "line": " this.listRef = react_1.default.createRef();", + "lineNumber": 19, + "reasonCategory": "usageTrusted", + "updated": "2019-08-09T00:44:31.008Z", + "reasonDetail": "SearchResults needs to interact with its child List directly" + }, { "rule": "React-createRef", "path": "ts/components/conversation/ConversationHeader.js", @@ -7848,23 +7875,5 @@ "lineNumber": 60, "reasonCategory": "falseMatch", "updated": "2019-05-02T20:44:56.470Z" - }, - { - "rule": "DOM-innerHTML", - "path": "ts/components/CompositionArea.js", - "line": " el.innerHTML = '';", - "lineNumber": 22, - "reasonCategory": "usageTrusted", - "updated": "2019-08-01T14:10:37.481Z", - "reasonDetail": "Our code, no user input, only clearing out the dom" - }, - { - "rule": "DOM-innerHTML", - "path": "ts/components/CompositionArea.tsx", - "line": " el.innerHTML = '';", - "lineNumber": 65, - "reasonCategory": "usageTrusted", - "updated": "2019-08-01T14:10:37.481Z", - "reasonDetail": "Our code, no user input, only clearing out the dom" } -] +] \ No newline at end of file