Virtualize search results - only render what's visible
This commit is contained in:
parent
9d4f2afa5a
commit
6292019d30
19 changed files with 1633 additions and 438 deletions
68
app/sql.js
68
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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -71,7 +71,6 @@
|
|||
position: relative;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.timeline-wrapper {
|
||||
-webkit-padding-start: 0px;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1584,7 +1584,7 @@ body.dark-theme {
|
|||
}
|
||||
|
||||
.module-message-search-result__body {
|
||||
color: $color-gray-05;
|
||||
color: $color-gray-15;
|
||||
}
|
||||
|
||||
// Module: Left Pane
|
||||
|
|
|
@ -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: '<<left>>Everyone<<right>>! 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 <<left>>everyone<<right>> 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, <<left>>everyone<<right>>! 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, <<left>>everyone<<right>>, 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 => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||
<LeftPane
|
||||
searchResults={searchResults}
|
||||
startNewConversation={(query, options) =>
|
||||
console.log('startNewConversation', query, options)
|
||||
}
|
||||
openConversationInternal={(id, messageId) =>
|
||||
console.log('openConversation', id, messageId)
|
||||
}
|
||||
showArchivedConversations={() => console.log('showArchivedConversations')}
|
||||
showInbox={() => console.log('showInbox')}
|
||||
renderMainHeader={() => (
|
||||
<MainHeader
|
||||
searchTerm="Hi there!"
|
||||
search={result => console.log('search', result)}
|
||||
updateSearch={result => console.log('updateSearch', result)}
|
||||
clearSearch={result => console.log('clearSearch', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
)}
|
||||
renderMessageSearchResult={id => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### 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: '<<left>>Everyone<<right>>! 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 <<left>>everyone<<right>> 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, <<left>>everyone<<right>>! 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, <<left>>everyone<<right>>, happy new year!',
|
||||
receivedAt: Date.now() - 24 * 60 * 1000,
|
||||
},
|
||||
];
|
||||
|
||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||
<LeftPane
|
||||
searchResults={window.searchResults}
|
||||
conversations={conversations}
|
||||
archivedConversations={[]}
|
||||
startNewConversation={(query, options) =>
|
||||
console.log('startNewConversation', query, options)
|
||||
}
|
||||
|
@ -151,42 +264,61 @@ window.searchResults.messages = [
|
|||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### With just conversations
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||
<LeftPane
|
||||
conversations={window.searchResults.conversations}
|
||||
archivedConversations={[]}
|
||||
startNewConversation={(query, options) =>
|
||||
console.log('startNewConversation', query, options)
|
||||
}
|
||||
openConversationInternal={(id, messageId) =>
|
||||
console.log('openConversation', id, messageId)
|
||||
}
|
||||
showArchivedConversations={() => console.log('showArchivedConversations')}
|
||||
showInbox={() => console.log('showInbox')}
|
||||
renderMainHeader={() => (
|
||||
<MainHeader
|
||||
searchTerm="Hi there!"
|
||||
search={result => console.log('search', result)}
|
||||
updateSearch={result => console.log('updateSearch', result)}
|
||||
clearSearch={result => console.log('clearSearch', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### 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!',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||
<LeftPane
|
||||
conversations={window.searchResults.conversations.slice(0, 2)}
|
||||
archivedConversations={window.searchResults.conversations.slice(2)}
|
||||
conversations={conversations.slice(0, 2)}
|
||||
archivedConversations={conversations.slice(2)}
|
||||
startNewConversation={(query, options) =>
|
||||
console.log('startNewConversation', query, options)
|
||||
}
|
||||
|
@ -206,16 +338,64 @@ window.searchResults.messages = [
|
|||
)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### 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!',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||
<LeftPane
|
||||
conversations={window.searchResults.conversations.slice(0, 2)}
|
||||
archivedConversations={window.searchResults.conversations.slice(2)}
|
||||
conversations={conversations.slice(0, 2)}
|
||||
archivedConversations={conversations.slice(2)}
|
||||
showArchived={true}
|
||||
startNewConversation={(query, options) =>
|
||||
console.log('startNewConversation', query, options)
|
||||
|
@ -236,5 +416,5 @@ window.searchResults.messages = [
|
|||
)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
|
|
@ -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<ConversationListItemPropsType>;
|
||||
archivedConversations?: Array<ConversationListItemPropsType>;
|
||||
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<Props> {
|
||||
export class LeftPane extends React.Component<PropsType> {
|
||||
public renderRow = ({
|
||||
index,
|
||||
key,
|
||||
|
@ -125,6 +126,7 @@ export class LeftPane extends React.Component<Props> {
|
|||
i18n,
|
||||
conversations,
|
||||
openConversationInternal,
|
||||
renderMessageSearchResult,
|
||||
startNewConversation,
|
||||
searchResults,
|
||||
showArchived,
|
||||
|
@ -134,8 +136,9 @@ export class LeftPane extends React.Component<Props> {
|
|||
return (
|
||||
<SearchResults
|
||||
{...searchResults}
|
||||
openConversation={openConversationInternal}
|
||||
openConversationInternal={openConversationInternal}
|
||||
startNewConversation={startNewConversation}
|
||||
renderMessageSearchResult={renderMessageSearchResult}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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<Props> {
|
||||
export class MessageSearchResult extends React.PureComponent<PropsType> {
|
||||
public renderFromName() {
|
||||
const { from, i18n, to } = this.props;
|
||||
|
||||
|
@ -123,7 +126,7 @@ export class MessageSearchResult extends React.PureComponent<Props> {
|
|||
id,
|
||||
isSelected,
|
||||
conversationId,
|
||||
onClick,
|
||||
openConversationInternal,
|
||||
receivedAt,
|
||||
snippet,
|
||||
to,
|
||||
|
@ -137,8 +140,8 @@ export class MessageSearchResult extends React.PureComponent<Props> {
|
|||
<div
|
||||
role="button"
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick(conversationId, id);
|
||||
if (openConversationInternal) {
|
||||
openConversationInternal(conversationId, id);
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
|
|
|
@ -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: '<<left>>Everyone<<right>>! 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 <<left>>everyone<<right>> 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, <<left>>everyone<<right>>! 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, <<left>>everyone<<right>>, happy new year!',
|
||||
receivedAt: Date.now() - 24 * 60 * 1000,
|
||||
onClick: () => console.log('onClick'),
|
||||
conversationOpenInternal: () => console.log('onClick'),
|
||||
},
|
||||
];
|
||||
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
const messageLookup = util._.fromPairs(
|
||||
util._.map(messages, message => [message.id, message])
|
||||
);
|
||||
messages.forEach(message => {
|
||||
items.push({
|
||||
type: 'message',
|
||||
data: message.id,
|
||||
});
|
||||
});
|
||||
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
conversations={window.searchResults.conversations}
|
||||
contacts={window.searchResults.contacts}
|
||||
messages={window.searchResults.messages}
|
||||
items={items}
|
||||
i18n={util.i18n}
|
||||
onClickMessage={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 => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
@ -123,82 +167,562 @@ window.searchResults.messages = [
|
|||
#### With 'start new conversation'
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
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: '<<left>>Everyone<<right>>! 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 <<left>>everyone<<right>> 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, <<left>>everyone<<right>>! 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, <<left>>everyone<<right>>, 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,
|
||||
});
|
||||
});
|
||||
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
conversations={window.searchResults.conversations}
|
||||
contacts={window.searchResults.contacts}
|
||||
messages={window.searchResults.messages}
|
||||
showStartNewConversation={true}
|
||||
searchTerm="(555) 100-2000"
|
||||
items={items}
|
||||
i18n={util.i18n}
|
||||
onClickMessage={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 => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### With no conversations
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
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: '<<left>>Everyone<<right>>! 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 <<left>>everyone<<right>> 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, <<left>>everyone<<right>>! 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, <<left>>everyone<<right>>, 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,
|
||||
});
|
||||
});
|
||||
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
conversations={null}
|
||||
contacts={window.searchResults.contacts}
|
||||
messages={window.searchResults.messages}
|
||||
items={items}
|
||||
i18n={util.i18n}
|
||||
onClickMessage={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 => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### With no contacts
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
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: '<<left>>Everyone<<right>>! 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 <<left>>everyone<<right>> 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, <<left>>everyone<<right>>! 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, <<left>>everyone<<right>>, 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,
|
||||
});
|
||||
});
|
||||
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
conversations={window.searchResults.conversations}
|
||||
contacts={null}
|
||||
messages={window.searchResults.messages}
|
||||
items={items}
|
||||
i18n={util.i18n}
|
||||
onClickMessage={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 => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### With no messages
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
conversations={window.searchResults.conversations}
|
||||
contacts={window.searchResults.contacts}
|
||||
messages={null}
|
||||
items={items}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
startNewConversation={(...args) =>
|
||||
console.log('startNewConversation', args)
|
||||
}
|
||||
onStartNewConversation={(...args) =>
|
||||
console.log('onStartNewConversation', args)
|
||||
}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### With no results at all
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
conversations={null}
|
||||
contacts={null}
|
||||
messages={null}
|
||||
searchTerm="dinner plans"
|
||||
items={[]}
|
||||
noResults={true}
|
||||
searchTerm="something"
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
startNewConversation={(...args) =>
|
||||
console.log('startNewConversation', args)
|
||||
}
|
||||
onStartNewConversation={(...args) =>
|
||||
console.log('onStartNewConversation', args)
|
||||
}
|
||||
renderMessageSearchResult={id => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
@ -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} <<left>>Everyone<<right>>! Get in!`,
|
||||
onClick: data => console.log('onClick', data),
|
||||
conversationOpenInternal: data => console.log('onClick', data),
|
||||
});
|
||||
}
|
||||
|
||||
<util.LeftPaneContext style={{ height: '500px' }} theme={util.theme}>
|
||||
const messageLookup = util._.fromPairs(
|
||||
util._.map(messages, message => [message.id, message])
|
||||
);
|
||||
messages.forEach(message => {
|
||||
items.push({
|
||||
type: 'message',
|
||||
data: message.id,
|
||||
});
|
||||
});
|
||||
|
||||
<util.LeftPaneContext
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
theme={util.theme}
|
||||
>
|
||||
<SearchResults
|
||||
conversations={null}
|
||||
contacts={null}
|
||||
messages={messages}
|
||||
items={items}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
startNewConversation={(...args) =>
|
||||
console.log('startNewConversation', args)
|
||||
}
|
||||
onStartNewConversation={(...args) =>
|
||||
console.log('onStartNewConversation', args)
|
||||
}
|
||||
renderMessageSearchResult={id => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
@ -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} <<left>>Everyone<<right>>! Get in!`,
|
||||
onClick: data => console.log('onClick', data),
|
||||
conversationOpenInternal: data => console.log('onClick', data),
|
||||
});
|
||||
}
|
||||
|
||||
<util.LeftPaneContext style={{ height: '500px' }} theme={util.theme}>
|
||||
const messageLookup = util._.fromPairs(
|
||||
util._.map(messages, message => [message.id, message])
|
||||
);
|
||||
messages.forEach(message => {
|
||||
items.push({
|
||||
type: 'message',
|
||||
data: message.id,
|
||||
});
|
||||
});
|
||||
|
||||
<util.LeftPaneContext
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
theme={util.theme}
|
||||
>
|
||||
<SearchResults
|
||||
hideMessagesHeader={true}
|
||||
messages={messages}
|
||||
items={items}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
startNewConversation={(...args) =>
|
||||
console.log('startNewConversation', args)
|
||||
}
|
||||
onStartNewConversation={(...args) =>
|
||||
console.log('onStartNewConversation', args)
|
||||
}
|
||||
renderMessageSearchResult={id => (
|
||||
<MessageSearchResult
|
||||
{...messageLookup[id]}
|
||||
i18n={util.i18n}
|
||||
openConversationInternal={(...args) =>
|
||||
console.log('openConversationInternal', args)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
|
|
@ -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<ConversationListItemPropsType>;
|
||||
conversations: Array<ConversationListItemPropsType>;
|
||||
hideMessagesHeader: boolean;
|
||||
messages: Array<MessageSearchResultPropsType>;
|
||||
export type PropsDataType = {
|
||||
items: Array<SearchResultRowType>;
|
||||
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<PropsType> {
|
||||
public mostRecentWidth = 0;
|
||||
public mostRecentHeight = 0;
|
||||
public cellSizeCache = new CellMeasurerCache({
|
||||
defaultHeight: 36,
|
||||
fixedWidth: true,
|
||||
});
|
||||
public listRef = React.createRef<any>();
|
||||
|
||||
export class SearchResults extends React.Component<Props> {
|
||||
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 (
|
||||
<StartNewConversation
|
||||
phoneNumber={searchTerm}
|
||||
i18n={i18n}
|
||||
onClick={this.handleStartNewConversation}
|
||||
/>
|
||||
);
|
||||
} else if (row.type === 'conversations-header') {
|
||||
return (
|
||||
<div className="module-search-results__conversations-header">
|
||||
{i18n('conversationsHeader')}
|
||||
</div>
|
||||
);
|
||||
} else if (row.type === 'conversation') {
|
||||
const { data } = row;
|
||||
|
||||
return (
|
||||
<ConversationListItem
|
||||
key={data.phoneNumber}
|
||||
{...data}
|
||||
onClick={openConversationInternal}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
} else if (row.type === 'contacts-header') {
|
||||
return (
|
||||
<div className="module-search-results__contacts-header">
|
||||
{i18n('contactsHeader')}
|
||||
</div>
|
||||
);
|
||||
} else if (row.type === 'contact') {
|
||||
const { data } = row;
|
||||
|
||||
return (
|
||||
<ConversationListItem
|
||||
key={data.phoneNumber}
|
||||
{...data}
|
||||
onClick={openConversationInternal}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
} else if (row.type === 'messages-header') {
|
||||
return (
|
||||
<div className="module-search-results__messages-header">
|
||||
{i18n('messagesHeader')}
|
||||
</div>
|
||||
);
|
||||
} 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 (
|
||||
<div className="module-search-results">
|
||||
{noResults ? (
|
||||
<div key={key} style={style}>
|
||||
<CellMeasurer
|
||||
cache={this.cellSizeCache}
|
||||
columnIndex={0}
|
||||
key={key}
|
||||
parent={parent}
|
||||
rowIndex={index}
|
||||
width={this.mostRecentWidth}
|
||||
>
|
||||
{this.renderRowContents(row)}
|
||||
</CellMeasurer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="module-search-results">
|
||||
<div className="module-search-results__no-results">
|
||||
{i18n('noSearchResults', [searchTerm])}
|
||||
</div>
|
||||
) : null}
|
||||
{showStartNewConversation ? (
|
||||
<StartNewConversation
|
||||
phoneNumber={searchTerm}
|
||||
i18n={i18n}
|
||||
onClick={this.handleStartNewConversation}
|
||||
/>
|
||||
) : null}
|
||||
{haveConversations ? (
|
||||
<div className="module-search-results__conversations">
|
||||
<div className="module-search-results__conversations-header">
|
||||
{i18n('conversationsHeader')}
|
||||
</div>
|
||||
{conversations.map(conversation => (
|
||||
<ConversationListItem
|
||||
key={conversation.phoneNumber}
|
||||
{...conversation}
|
||||
onClick={openConversation}
|
||||
i18n={i18n}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-search-results" key={searchTerm}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
this.mostRecentWidth = width;
|
||||
this.mostRecentHeight = height;
|
||||
|
||||
return (
|
||||
<List
|
||||
className="module-search-results__virtual-list"
|
||||
deferredMeasurementCache={this.cellSizeCache}
|
||||
height={height}
|
||||
items={items}
|
||||
overscanRowCount={5}
|
||||
ref={this.listRef}
|
||||
rowCount={this.getRowCount()}
|
||||
rowHeight={this.cellSizeCache.rowHeight}
|
||||
rowRenderer={this.renderRow}
|
||||
width={width}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{haveContacts ? (
|
||||
<div className="module-search-results__contacts">
|
||||
<div className="module-search-results__contacts-header">
|
||||
{i18n('contactsHeader')}
|
||||
</div>
|
||||
{contacts.map(contact => (
|
||||
<ConversationListItem
|
||||
key={contact.phoneNumber}
|
||||
{...contact}
|
||||
onClick={openConversation}
|
||||
i18n={i18n}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{haveMessages ? (
|
||||
<div className="module-search-results__messages">
|
||||
{hideMessagesHeader ? null : (
|
||||
<div className="module-search-results__messages-header">
|
||||
{i18n('messagesHeader')}
|
||||
</div>
|
||||
)}
|
||||
{messages.map(message => (
|
||||
<MessageSearchResult
|
||||
key={message.id}
|
||||
{...message}
|
||||
onClick={openConversation}
|
||||
i18n={i18n}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<string>;
|
||||
conversations: Array<string>;
|
||||
query: string;
|
||||
normalizedPhoneNumber?: string;
|
||||
// We need to store messages here, because they aren't anywhere else in state
|
||||
messages: Array<MessageSearchResultType>;
|
||||
messageIds: Array<string>;
|
||||
// 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<string>;
|
||||
contacts: Array<string>;
|
||||
};
|
||||
|
||||
// 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']),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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<SearchResultRowType> = [];
|
||||
|
||||
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
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 <FilteredSmartMainHeader />;
|
||||
}
|
||||
function renderMessageSearchResult(id: string): JSX.Element {
|
||||
return <FilteredSmartMessageSearchResult id={id} />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const showSearch = isSearching(state);
|
||||
|
@ -30,6 +35,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
showArchived: getShowArchived(state),
|
||||
i18n: getIntl(state),
|
||||
renderMainHeader,
|
||||
renderMessageSearchResult,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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<Props> {
|
||||
public render() {
|
||||
const { style, theme } = this.props;
|
||||
const { gutterStyle, style, theme } = this.props;
|
||||
|
||||
return (
|
||||
<div style={style} className={classNames(theme || 'light-theme')}>
|
||||
<div className="gutter">{this.props.children}</div>
|
||||
<div className="gutter" style={gutterStyle}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
Loading…
Reference in a new issue