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 = [
|
const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion1,
|
updateToSchemaVersion1,
|
||||||
updateToSchemaVersion2,
|
updateToSchemaVersion2,
|
||||||
|
@ -1035,6 +1098,7 @@ const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion15,
|
updateToSchemaVersion15,
|
||||||
updateToSchemaVersion16,
|
updateToSchemaVersion16,
|
||||||
updateToSchemaVersion17,
|
updateToSchemaVersion17,
|
||||||
|
updateToSchemaVersion18,
|
||||||
];
|
];
|
||||||
|
|
||||||
async function updateSchema(instance) {
|
async function updateSchema(instance) {
|
||||||
|
@ -1552,7 +1616,7 @@ async function searchConversations(query, { limit } = {}) {
|
||||||
$id: `%${query}%`,
|
$id: `%${query}%`,
|
||||||
$name: `%${query}%`,
|
$name: `%${query}%`,
|
||||||
$profileName: `%${query}%`,
|
$profileName: `%${query}%`,
|
||||||
$limit: limit || 50,
|
$limit: limit || 100,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1572,7 +1636,7 @@ async function searchMessages(query, { limit } = {}) {
|
||||||
LIMIT $limit;`,
|
LIMIT $limit;`,
|
||||||
{
|
{
|
||||||
$query: query,
|
$query: query,
|
||||||
$limit: limit || 100,
|
$limit: limit || 500,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,6 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
.timeline-wrapper {
|
.timeline-wrapper {
|
||||||
-webkit-padding-start: 0px;
|
-webkit-padding-start: 0px;
|
||||||
|
|
|
@ -3125,8 +3125,8 @@
|
||||||
// Module: Search Results
|
// Module: Search Results
|
||||||
|
|
||||||
.module-search-results {
|
.module-search-results {
|
||||||
overflow-y: scroll;
|
overflow: hidden;
|
||||||
max-height: 100%;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-search-results__conversations-header {
|
.module-search-results__conversations-header {
|
||||||
|
|
|
@ -1584,7 +1584,7 @@ body.dark-theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message-search-result__body {
|
.module-message-search-result__body {
|
||||||
color: $color-gray-05;
|
color: $color-gray-15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module: Left Pane
|
// Module: Left Pane
|
||||||
|
|
|
@ -1,8 +1,196 @@
|
||||||
#### With search results
|
#### With search results
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
window.searchResults = {};
|
const items = [
|
||||||
window.searchResults.conversations = [
|
{
|
||||||
|
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',
|
id: 'convo1',
|
||||||
name: 'Everyone 🌆',
|
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' }}>
|
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||||
<LeftPane
|
<LeftPane
|
||||||
searchResults={window.searchResults}
|
conversations={conversations}
|
||||||
|
archivedConversations={[]}
|
||||||
startNewConversation={(query, options) =>
|
startNewConversation={(query, options) =>
|
||||||
console.log('startNewConversation', query, options)
|
console.log('startNewConversation', query, options)
|
||||||
}
|
}
|
||||||
|
@ -151,42 +264,61 @@ window.searchResults.messages = [
|
||||||
</util.LeftPaneContext>;
|
</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
|
#### Showing inbox, with some archived
|
||||||
|
|
||||||
```jsx
|
```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' }}>
|
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||||
<LeftPane
|
<LeftPane
|
||||||
conversations={window.searchResults.conversations.slice(0, 2)}
|
conversations={conversations.slice(0, 2)}
|
||||||
archivedConversations={window.searchResults.conversations.slice(2)}
|
archivedConversations={conversations.slice(2)}
|
||||||
startNewConversation={(query, options) =>
|
startNewConversation={(query, options) =>
|
||||||
console.log('startNewConversation', query, options)
|
console.log('startNewConversation', query, options)
|
||||||
}
|
}
|
||||||
|
@ -206,16 +338,64 @@ window.searchResults.messages = [
|
||||||
)}
|
)}
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</util.LeftPaneContext>
|
</util.LeftPaneContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Showing archived conversations
|
#### Showing archived conversations
|
||||||
|
|
||||||
```jsx
|
```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' }}>
|
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||||
<LeftPane
|
<LeftPane
|
||||||
conversations={window.searchResults.conversations.slice(0, 2)}
|
conversations={conversations.slice(0, 2)}
|
||||||
archivedConversations={window.searchResults.conversations.slice(2)}
|
archivedConversations={conversations.slice(2)}
|
||||||
showArchived={true}
|
showArchived={true}
|
||||||
startNewConversation={(query, options) =>
|
startNewConversation={(query, options) =>
|
||||||
console.log('startNewConversation', query, options)
|
console.log('startNewConversation', query, options)
|
||||||
|
@ -236,5 +416,5 @@ window.searchResults.messages = [
|
||||||
)}
|
)}
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</util.LeftPaneContext>
|
</util.LeftPaneContext>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -6,12 +6,12 @@ import {
|
||||||
PropsData as ConversationListItemPropsType,
|
PropsData as ConversationListItemPropsType,
|
||||||
} from './ConversationListItem';
|
} from './ConversationListItem';
|
||||||
import {
|
import {
|
||||||
PropsData as SearchResultsProps,
|
PropsDataType as SearchResultsProps,
|
||||||
SearchResults,
|
SearchResults,
|
||||||
} from './SearchResults';
|
} from './SearchResults';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export interface Props {
|
export interface PropsType {
|
||||||
conversations?: Array<ConversationListItemPropsType>;
|
conversations?: Array<ConversationListItemPropsType>;
|
||||||
archivedConversations?: Array<ConversationListItemPropsType>;
|
archivedConversations?: Array<ConversationListItemPropsType>;
|
||||||
searchResults?: SearchResultsProps;
|
searchResults?: SearchResultsProps;
|
||||||
|
@ -30,6 +30,7 @@ export interface Props {
|
||||||
|
|
||||||
// Render Props
|
// Render Props
|
||||||
renderMainHeader: () => JSX.Element;
|
renderMainHeader: () => JSX.Element;
|
||||||
|
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
||||||
|
@ -42,7 +43,7 @@ type RowRendererParamsType = {
|
||||||
style: Object;
|
style: Object;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LeftPane extends React.Component<Props> {
|
export class LeftPane extends React.Component<PropsType> {
|
||||||
public renderRow = ({
|
public renderRow = ({
|
||||||
index,
|
index,
|
||||||
key,
|
key,
|
||||||
|
@ -125,6 +126,7 @@ export class LeftPane extends React.Component<Props> {
|
||||||
i18n,
|
i18n,
|
||||||
conversations,
|
conversations,
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
|
renderMessageSearchResult,
|
||||||
startNewConversation,
|
startNewConversation,
|
||||||
searchResults,
|
searchResults,
|
||||||
showArchived,
|
showArchived,
|
||||||
|
@ -134,8 +136,9 @@ export class LeftPane extends React.Component<Props> {
|
||||||
return (
|
return (
|
||||||
<SearchResults
|
<SearchResults
|
||||||
{...searchResults}
|
{...searchResults}
|
||||||
openConversation={openConversationInternal}
|
openConversationInternal={openConversationInternal}
|
||||||
startNewConversation={startNewConversation}
|
startNewConversation={startNewConversation}
|
||||||
|
renderMessageSearchResult={renderMessageSearchResult}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,9 @@ import { ContactName } from './conversation/ContactName';
|
||||||
|
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsDataType = {
|
||||||
|
isSelected?: boolean;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
receivedAt: number;
|
receivedAt: number;
|
||||||
|
@ -33,16 +35,17 @@ export type PropsData = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeepingType = {
|
||||||
isSelected?: boolean;
|
|
||||||
|
|
||||||
i18n: LocalizerType;
|
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() {
|
public renderFromName() {
|
||||||
const { from, i18n, to } = this.props;
|
const { from, i18n, to } = this.props;
|
||||||
|
|
||||||
|
@ -123,7 +126,7 @@ export class MessageSearchResult extends React.PureComponent<Props> {
|
||||||
id,
|
id,
|
||||||
isSelected,
|
isSelected,
|
||||||
conversationId,
|
conversationId,
|
||||||
onClick,
|
openConversationInternal,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
snippet,
|
snippet,
|
||||||
to,
|
to,
|
||||||
|
@ -137,8 +140,8 @@ export class MessageSearchResult extends React.PureComponent<Props> {
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onClick) {
|
if (openConversationInternal) {
|
||||||
onClick(conversationId, id);
|
openConversationInternal(conversationId, id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
@ -1,48 +1,68 @@
|
||||||
#### With all result types
|
#### With all result types
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
window.searchResults = {};
|
const items = [
|
||||||
window.searchResults.conversations = [
|
|
||||||
{
|
{
|
||||||
name: 'Everyone 🌆',
|
type: 'conversations-header',
|
||||||
conversationType: 'group',
|
data: undefined,
|
||||||
phoneNumber: '(202) 555-0011',
|
},
|
||||||
avatarPath: util.landscapeGreenObjectUrl,
|
{
|
||||||
lastUpdated: Date.now() - 5 * 60 * 1000,
|
type: 'conversation',
|
||||||
lastMessage: {
|
data: {
|
||||||
text: 'The rabbit hopped silently in the night.',
|
name: 'Everyone 🌆',
|
||||||
status: 'sent',
|
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 🔥',
|
type: 'conversation',
|
||||||
conversationType: 'direct',
|
data: {
|
||||||
phoneNumber: '(202) 555-0012',
|
name: 'Everyone Else 🔥',
|
||||||
avatarPath: util.landscapePurpleObjectUrl,
|
conversationType: 'direct',
|
||||||
lastUpdated: Date.now() - 5 * 60 * 1000,
|
phoneNumber: '(202) 555-0012',
|
||||||
lastMessage: {
|
avatarPath: util.landscapePurpleObjectUrl,
|
||||||
text: "What's going on?",
|
lastUpdated: Date.now() - 5 * 60 * 1000,
|
||||||
status: 'sent',
|
lastMessage: {
|
||||||
|
text: "What's going on?",
|
||||||
|
status: 'sent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
|
||||||
|
|
||||||
window.searchResults.contacts = [
|
|
||||||
{
|
{
|
||||||
name: 'The one Everyone',
|
type: 'contacts-header',
|
||||||
conversationType: 'direct',
|
data: undefined,
|
||||||
phoneNumber: '(202) 555-0013',
|
|
||||||
avatarPath: util.gifObjectUrl,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'No likey everyone',
|
type: 'contact',
|
||||||
conversationType: 'direct',
|
data: {
|
||||||
phoneNumber: '(202) 555-0014',
|
name: 'The one Everyone',
|
||||||
color: 'red',
|
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: {
|
from: {
|
||||||
isMe: true,
|
isMe: true,
|
||||||
|
@ -56,7 +76,7 @@ window.searchResults.messages = [
|
||||||
conversationId: '(202) 555-0015',
|
conversationId: '(202) 555-0015',
|
||||||
receivedAt: Date.now() - 5 * 60 * 1000,
|
receivedAt: Date.now() - 5 * 60 * 1000,
|
||||||
snippet: '<<left>>Everyone<<right>>! Get in!',
|
snippet: '<<left>>Everyone<<right>>! Get in!',
|
||||||
onClick: () => console.log('onClick'),
|
conversationOpenInternal: () => console.log('onClick'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: {
|
from: {
|
||||||
|
@ -71,7 +91,7 @@ window.searchResults.messages = [
|
||||||
conversationId: '(202) 555-0016',
|
conversationId: '(202) 555-0016',
|
||||||
snippet: 'Why is <<left>>everyone<<right>> so frustrated?',
|
snippet: 'Why is <<left>>everyone<<right>> so frustrated?',
|
||||||
receivedAt: Date.now() - 20 * 60 * 1000,
|
receivedAt: Date.now() - 20 * 60 * 1000,
|
||||||
onClick: () => console.log('onClick'),
|
conversationOpenInternal: () => console.log('onClick'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: {
|
from: {
|
||||||
|
@ -87,7 +107,7 @@ window.searchResults.messages = [
|
||||||
conversationId: 'EveryoneGroupID',
|
conversationId: 'EveryoneGroupID',
|
||||||
snippet: 'Hello, <<left>>everyone<<right>>! Woohooo!',
|
snippet: 'Hello, <<left>>everyone<<right>>! Woohooo!',
|
||||||
receivedAt: Date.now() - 24 * 60 * 1000,
|
receivedAt: Date.now() - 24 * 60 * 1000,
|
||||||
onClick: () => console.log('onClick'),
|
conversationOpenInternal: () => console.log('onClick'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: {
|
from: {
|
||||||
|
@ -101,21 +121,45 @@ window.searchResults.messages = [
|
||||||
conversationId: 'EveryoneGroupID',
|
conversationId: 'EveryoneGroupID',
|
||||||
snippet: 'Well, <<left>>everyone<<right>>, happy new year!',
|
snippet: 'Well, <<left>>everyone<<right>>, happy new year!',
|
||||||
receivedAt: Date.now() - 24 * 60 * 1000,
|
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
|
<SearchResults
|
||||||
conversations={window.searchResults.conversations}
|
items={items}
|
||||||
contacts={window.searchResults.contacts}
|
|
||||||
messages={window.searchResults.messages}
|
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
onClickMessage={id => console.log('onClickMessage', id)}
|
openConversationInternal={(...args) =>
|
||||||
onClickConversation={id => console.log('onClickConversation', id)}
|
console.log('openConversationInternal', args)
|
||||||
onStartNewConversation={(query, options) =>
|
|
||||||
console.log('onStartNewConversation', query, options)
|
|
||||||
}
|
}
|
||||||
|
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>;
|
||||||
```
|
```
|
||||||
|
@ -123,82 +167,562 @@ window.searchResults.messages = [
|
||||||
#### With 'start new conversation'
|
#### With 'start new conversation'
|
||||||
|
|
||||||
```jsx
|
```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
|
<SearchResults
|
||||||
conversations={window.searchResults.conversations}
|
items={items}
|
||||||
contacts={window.searchResults.contacts}
|
|
||||||
messages={window.searchResults.messages}
|
|
||||||
showStartNewConversation={true}
|
|
||||||
searchTerm="(555) 100-2000"
|
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
onClickMessage={id => console.log('onClickMessage', id)}
|
searchTerm="(202) 555-0015"
|
||||||
onClickConversation={id => console.log('onClickConversation', id)}
|
openConversationInternal={(...args) =>
|
||||||
onStartNewConversation={(query, options) =>
|
console.log('openConversationInternal', args)
|
||||||
console.log('onStartNewConversation', query, options)
|
|
||||||
}
|
}
|
||||||
|
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
|
#### With no conversations
|
||||||
|
|
||||||
```jsx
|
```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
|
<SearchResults
|
||||||
conversations={null}
|
items={items}
|
||||||
contacts={window.searchResults.contacts}
|
|
||||||
messages={window.searchResults.messages}
|
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
onClickMessage={id => console.log('onClickMessage', id)}
|
openConversationInternal={(...args) =>
|
||||||
onClickConversation={id => console.log('onClickConversation', id)}
|
console.log('openConversationInternal', args)
|
||||||
onStartNewConversation={(query, options) =>
|
|
||||||
console.log('onStartNewConversation', query, options)
|
|
||||||
}
|
}
|
||||||
|
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
|
#### With no contacts
|
||||||
|
|
||||||
```jsx
|
```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
|
<SearchResults
|
||||||
conversations={window.searchResults.conversations}
|
items={items}
|
||||||
contacts={null}
|
|
||||||
messages={window.searchResults.messages}
|
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
onClickMessage={id => console.log('onClickMessage', id)}
|
openConversationInternal={(...args) =>
|
||||||
onClickConversation={id => console.log('onClickConversation', id)}
|
console.log('openConversationInternal', args)
|
||||||
onStartNewConversation={(query, options) =>
|
|
||||||
console.log('onStartNewConversation', query, options)
|
|
||||||
}
|
}
|
||||||
|
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
|
#### With no messages
|
||||||
|
|
||||||
```jsx
|
```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
|
<SearchResults
|
||||||
conversations={window.searchResults.conversations}
|
items={items}
|
||||||
contacts={window.searchResults.contacts}
|
|
||||||
messages={null}
|
|
||||||
i18n={util.i18n}
|
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
|
#### With no results at all
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.LeftPaneContext theme={util.theme}>
|
<util.LeftPaneContext
|
||||||
|
theme={util.theme}
|
||||||
|
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||||
|
>
|
||||||
<SearchResults
|
<SearchResults
|
||||||
conversations={null}
|
items={[]}
|
||||||
contacts={null}
|
noResults={true}
|
||||||
messages={null}
|
searchTerm="something"
|
||||||
searchTerm="dinner plans"
|
|
||||||
i18n={util.i18n}
|
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>
|
</util.LeftPaneContext>
|
||||||
```
|
```
|
||||||
|
@ -206,6 +730,67 @@ window.searchResults.messages = [
|
||||||
#### With a lot of results
|
#### With a lot of results
|
||||||
|
|
||||||
```jsx
|
```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 = [];
|
const messages = [];
|
||||||
for (let i = 0; i < 100; i += 1) {
|
for (let i = 0; i < 100; i += 1) {
|
||||||
messages.push({
|
messages.push({
|
||||||
|
@ -221,16 +806,45 @@ for (let i = 0; i < 100; i += 1) {
|
||||||
conversationId: '(202) 555-0015',
|
conversationId: '(202) 555-0015',
|
||||||
receivedAt: Date.now() - 5 * 60 * 1000,
|
receivedAt: Date.now() - 5 * 60 * 1000,
|
||||||
snippet: `${i} <<left>>Everyone<<right>>! Get in!`,
|
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
|
<SearchResults
|
||||||
conversations={null}
|
items={items}
|
||||||
contacts={null}
|
|
||||||
messages={messages}
|
|
||||||
i18n={util.i18n}
|
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>;
|
</util.LeftPaneContext>;
|
||||||
```
|
```
|
||||||
|
@ -238,6 +852,8 @@ for (let i = 0; i < 100; i += 1) {
|
||||||
#### With just messages and no header
|
#### With just messages and no header
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
|
const items = [];
|
||||||
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
for (let i = 0; i < 10; i += 1) {
|
for (let i = 0; i < 10; i += 1) {
|
||||||
messages.push({
|
messages.push({
|
||||||
|
@ -253,15 +869,45 @@ for (let i = 0; i < 10; i += 1) {
|
||||||
conversationId: '(202) 555-0015',
|
conversationId: '(202) 555-0015',
|
||||||
receivedAt: Date.now() - 5 * 60 * 1000,
|
receivedAt: Date.now() - 5 * 60 * 1000,
|
||||||
snippet: `${i} <<left>>Everyone<<right>>! Get in!`,
|
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
|
<SearchResults
|
||||||
hideMessagesHeader={true}
|
items={items}
|
||||||
messages={messages}
|
|
||||||
i18n={util.i18n}
|
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>;
|
</util.LeftPaneContext>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,123 +1,277 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
AutoSizer,
|
||||||
|
CellMeasurer,
|
||||||
|
CellMeasurerCache,
|
||||||
|
List,
|
||||||
|
} from 'react-virtualized';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConversationListItem,
|
ConversationListItem,
|
||||||
PropsData as ConversationListItemPropsType,
|
PropsData as ConversationListItemPropsType,
|
||||||
} from './ConversationListItem';
|
} from './ConversationListItem';
|
||||||
import { MessageSearchResult } from './MessageSearchResult';
|
|
||||||
import { StartNewConversation } from './StartNewConversation';
|
import { StartNewConversation } from './StartNewConversation';
|
||||||
|
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsDataType = {
|
||||||
contacts: Array<ConversationListItemPropsType>;
|
items: Array<SearchResultRowType>;
|
||||||
conversations: Array<ConversationListItemPropsType>;
|
noResults: boolean;
|
||||||
hideMessagesHeader: boolean;
|
|
||||||
messages: Array<MessageSearchResultPropsType>;
|
|
||||||
regionCode: string;
|
regionCode: string;
|
||||||
searchTerm: 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;
|
i18n: LocalizerType;
|
||||||
openConversation: (id: string, messageId?: string) => void;
|
openConversationInternal: (id: string, messageId?: string) => void;
|
||||||
startNewConversation: (
|
startNewConversation: (
|
||||||
query: string,
|
query: string,
|
||||||
options: { regionCode: string }
|
options: { regionCode: string }
|
||||||
) => void;
|
) => 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 = () => {
|
public handleStartNewConversation = () => {
|
||||||
const { regionCode, searchTerm, startNewConversation } = this.props;
|
const { regionCode, searchTerm, startNewConversation } = this.props;
|
||||||
|
|
||||||
startNewConversation(searchTerm, { regionCode });
|
startNewConversation(searchTerm, { regionCode });
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public renderRowContents(row: SearchResultRowType) {
|
||||||
const {
|
const {
|
||||||
conversations,
|
|
||||||
contacts,
|
|
||||||
hideMessagesHeader,
|
|
||||||
i18n,
|
|
||||||
messages,
|
|
||||||
openConversation,
|
|
||||||
searchTerm,
|
searchTerm,
|
||||||
showStartNewConversation,
|
i18n,
|
||||||
|
openConversationInternal,
|
||||||
|
renderMessageSearchResult,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const haveConversations = conversations && conversations.length;
|
if (row.type === 'start-new-conversation') {
|
||||||
const haveContacts = contacts && contacts.length;
|
return (
|
||||||
const haveMessages = messages && messages.length;
|
<StartNewConversation
|
||||||
const noResults =
|
phoneNumber={searchTerm}
|
||||||
!showStartNewConversation &&
|
i18n={i18n}
|
||||||
!haveConversations &&
|
onClick={this.handleStartNewConversation}
|
||||||
!haveContacts &&
|
/>
|
||||||
!haveMessages;
|
);
|
||||||
|
} 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 (
|
return (
|
||||||
<div className="module-search-results">
|
<div key={key} style={style}>
|
||||||
{noResults ? (
|
<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">
|
<div className="module-search-results__no-results">
|
||||||
{i18n('noSearchResults', [searchTerm])}
|
{i18n('noSearchResults', [searchTerm])}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
{showStartNewConversation ? (
|
);
|
||||||
<StartNewConversation
|
}
|
||||||
phoneNumber={searchTerm}
|
|
||||||
i18n={i18n}
|
return (
|
||||||
onClick={this.handleStartNewConversation}
|
<div className="module-search-results" key={searchTerm}>
|
||||||
/>
|
<AutoSizer>
|
||||||
) : null}
|
{({ height, width }) => {
|
||||||
{haveConversations ? (
|
this.mostRecentWidth = width;
|
||||||
<div className="module-search-results__conversations">
|
this.mostRecentHeight = height;
|
||||||
<div className="module-search-results__conversations-header">
|
|
||||||
{i18n('conversationsHeader')}
|
return (
|
||||||
</div>
|
<List
|
||||||
{conversations.map(conversation => (
|
className="module-search-results__virtual-list"
|
||||||
<ConversationListItem
|
deferredMeasurementCache={this.cellSizeCache}
|
||||||
key={conversation.phoneNumber}
|
height={height}
|
||||||
{...conversation}
|
items={items}
|
||||||
onClick={openConversation}
|
overscanRowCount={5}
|
||||||
i18n={i18n}
|
ref={this.listRef}
|
||||||
|
rowCount={this.getRowCount()}
|
||||||
|
rowHeight={this.cellSizeCache.rowHeight}
|
||||||
|
rowRenderer={this.renderRow}
|
||||||
|
width={width}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
</div>
|
}}
|
||||||
) : null}
|
</AutoSizer>
|
||||||
{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}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,32 +14,6 @@ import { NoopActionType } from './noop';
|
||||||
|
|
||||||
// State
|
// 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 = {
|
export type ConversationType = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
|
@ -9,25 +9,31 @@ import { makeLookup } from '../../util/makeLookup';
|
||||||
import {
|
import {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
MessageDeletedActionType,
|
MessageDeletedActionType,
|
||||||
MessageSearchResultType,
|
MessageType,
|
||||||
RemoveAllConversationsActionType,
|
RemoveAllConversationsActionType,
|
||||||
SelectedConversationChangedActionType,
|
SelectedConversationChangedActionType,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
export type MessageSearchResultType = MessageType & {
|
||||||
|
snippet: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessageSearchResultLookupType = {
|
||||||
|
[id: string]: MessageSearchResultType;
|
||||||
|
};
|
||||||
|
|
||||||
export type SearchStateType = {
|
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;
|
query: string;
|
||||||
normalizedPhoneNumber?: string;
|
normalizedPhoneNumber?: string;
|
||||||
// We need to store messages here, because they aren't anywhere else in state
|
messageIds: Array<string>;
|
||||||
messages: Array<MessageSearchResultType>;
|
// We do store message data to pass through the selector
|
||||||
|
messageLookup: MessageSearchResultLookupType;
|
||||||
selectedMessage?: string;
|
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
|
// Actions
|
||||||
|
@ -193,7 +199,7 @@ async function queryConversationsAndContacts(
|
||||||
function getEmptyState(): SearchStateType {
|
function getEmptyState(): SearchStateType {
|
||||||
return {
|
return {
|
||||||
query: '',
|
query: '',
|
||||||
messages: [],
|
messageIds: [],
|
||||||
messageLookup: {},
|
messageLookup: {},
|
||||||
conversations: [],
|
conversations: [],
|
||||||
contacts: [],
|
contacts: [],
|
||||||
|
@ -220,16 +226,28 @@ export function reducer(
|
||||||
|
|
||||||
if (action.type === 'SEARCH_RESULTS_FULFILLED') {
|
if (action.type === 'SEARCH_RESULTS_FULFILLED') {
|
||||||
const { payload } = action;
|
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
|
// Reject if the associated query is not the most recent user-provided query
|
||||||
if (state.query !== query) {
|
if (state.query !== query) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageIds = messages.map(message => message.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...payload,
|
contacts,
|
||||||
|
conversations,
|
||||||
|
normalizedPhoneNumber,
|
||||||
|
query,
|
||||||
|
messageIds,
|
||||||
messageLookup: makeLookup(messages, 'id'),
|
messageLookup: makeLookup(messages, 'id'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -253,8 +271,8 @@ export function reducer(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'MESSAGE_DELETED') {
|
if (action.type === 'MESSAGE_DELETED') {
|
||||||
const { messages, messageLookup } = state;
|
const { messageIds, messageLookup } = state;
|
||||||
if (!messages.length) {
|
if (!messageIds || messageIds.length < 1) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +281,7 @@ export function reducer(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
messages: reject(messages, message => id === message.id),
|
messageIds: reject(messageIds, messageId => id === messageId),
|
||||||
messageLookup: omit(messageLookup, ['id']),
|
messageLookup: omit(messageLookup, ['id']),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,11 +207,13 @@ export const getCachedSelectorForConversation = createSelector(
|
||||||
(): CachedConversationSelectorType => {
|
(): CachedConversationSelectorType => {
|
||||||
// Note: memoizee will check all parameters provided, and only run our selector
|
// Note: memoizee will check all parameters provided, and only run our selector
|
||||||
// if any of them have changed.
|
// 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(
|
export const getConversationSelector = createSelector(
|
||||||
getCachedSelectorForConversation,
|
getCachedSelectorForConversation,
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
|
@ -287,7 +289,7 @@ export const getCachedSelectorForMessage = createSelector(
|
||||||
(): CachedMessageSelectorType => {
|
(): CachedMessageSelectorType => {
|
||||||
// Note: memoizee will check all parameters provided, and only run our selector
|
// Note: memoizee will check all parameters provided, and only run our selector
|
||||||
// if any of them have changed.
|
// 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 { createSelector } from 'reselect';
|
||||||
import { getSearchResultsProps } from '../../shims/Whisper';
|
import { getSearchResultsProps } from '../../shims/Whisper';
|
||||||
|
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { SearchStateType } from '../ducks/search';
|
|
||||||
import {
|
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,
|
getConversationLookup,
|
||||||
|
getConversationSelector,
|
||||||
getSelectedConversation,
|
getSelectedConversation,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import { ConversationLookupType } from '../ducks/conversations';
|
|
||||||
|
|
||||||
import { getRegionCode } from './user';
|
|
||||||
|
|
||||||
export const getSearch = (state: StateType): SearchStateType => state.search;
|
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(
|
export const getSearchResults = createSelector(
|
||||||
[
|
[getSearch, getRegionCode, getConversationLookup, getSelectedConversation],
|
||||||
getSearch,
|
|
||||||
getRegionCode,
|
|
||||||
getConversationLookup,
|
|
||||||
getSelectedConversation,
|
|
||||||
getSelectedMessage,
|
|
||||||
],
|
|
||||||
(
|
(
|
||||||
state: SearchStateType,
|
state: SearchStateType,
|
||||||
regionCode: string,
|
regionCode: string,
|
||||||
lookup: ConversationLookupType,
|
lookup: ConversationLookupType,
|
||||||
selectedConversation?: string,
|
selectedConversation?: string
|
||||||
selectedMessage?: 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 {
|
return {
|
||||||
contacts: compact(
|
items,
|
||||||
state.contacts.map(id => {
|
noResults,
|
||||||
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;
|
|
||||||
}),
|
|
||||||
regionCode: regionCode,
|
regionCode: regionCode,
|
||||||
searchTerm: state.query,
|
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 { getLeftPaneLists, getShowArchived } from '../selectors/conversations';
|
||||||
|
|
||||||
import { SmartMainHeader } from './MainHeader';
|
import { SmartMainHeader } from './MainHeader';
|
||||||
|
import { SmartMessageSearchResult } from './MessageSearchResult';
|
||||||
|
|
||||||
// Workaround: A react component's required properties are filtering up through connect()
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
const FilteredSmartMainHeader = SmartMainHeader as any;
|
const FilteredSmartMainHeader = SmartMainHeader as any;
|
||||||
|
const FilteredSmartMessageSearchResult = SmartMessageSearchResult as any;
|
||||||
|
|
||||||
function renderMainHeader(): JSX.Element {
|
function renderMainHeader(): JSX.Element {
|
||||||
return <FilteredSmartMainHeader />;
|
return <FilteredSmartMainHeader />;
|
||||||
}
|
}
|
||||||
|
function renderMessageSearchResult(id: string): JSX.Element {
|
||||||
|
return <FilteredSmartMessageSearchResult id={id} />;
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType) => {
|
const mapStateToProps = (state: StateType) => {
|
||||||
const showSearch = isSearching(state);
|
const showSearch = isSearching(state);
|
||||||
|
@ -30,6 +35,7 @@ const mapStateToProps = (state: StateType) => {
|
||||||
showArchived: getShowArchived(state),
|
showArchived: getShowArchived(state),
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
renderMainHeader,
|
renderMainHeader,
|
||||||
|
renderMessageSearchResult,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mapDispatchToProps } from '../actions';
|
|
||||||
|
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { MessageSearchResult } from '../../components/MessageSearchResult';
|
import { MessageSearchResult } from '../../components/MessageSearchResult';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import { getMessageSearchResultSelector } from '../selectors/search';
|
||||||
|
|
||||||
type SmartProps = {
|
type SmartProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -11,12 +13,13 @@ type SmartProps = {
|
||||||
|
|
||||||
function mapStateToProps(state: StateType, ourProps: SmartProps) {
|
function mapStateToProps(state: StateType, ourProps: SmartProps) {
|
||||||
const { id } = ourProps;
|
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);
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
|
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getMessageSelector } from '../selectors/conversations';
|
import { getMessageSelector } from '../selectors/conversations';
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ interface Props {
|
||||||
*/
|
*/
|
||||||
theme: 'light-theme' | 'dark-theme';
|
theme: 'light-theme' | 'dark-theme';
|
||||||
style: any;
|
style: any;
|
||||||
|
gutterStyle: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,11 +16,13 @@ interface Props {
|
||||||
*/
|
*/
|
||||||
export class LeftPaneContext extends React.Component<Props> {
|
export class LeftPaneContext extends React.Component<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
const { style, theme } = this.props;
|
const { gutterStyle, style, theme } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style} className={classNames(theme || 'light-theme')}>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ export const format = memoizee(_format, {
|
||||||
primitive: true,
|
primitive: true,
|
||||||
// Convert the arguments to a unique string, required for primitive mode.
|
// Convert the arguments to a unique string, required for primitive mode.
|
||||||
normalizer: (...args) => JSON.stringify(args),
|
normalizer: (...args) => JSON.stringify(args),
|
||||||
|
max: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function parse(
|
export function parse(
|
||||||
|
|
|
@ -7770,6 +7770,24 @@
|
||||||
"updated": "2019-03-09T00:08:44.242Z",
|
"updated": "2019-03-09T00:08:44.242Z",
|
||||||
"reasonDetail": "Used only to set focus"
|
"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",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/Lightbox.js",
|
"path": "ts/components/Lightbox.js",
|
||||||
|
@ -7806,6 +7824,15 @@
|
||||||
"updated": "2019-03-09T00:08:44.242Z",
|
"updated": "2019-03-09T00:08:44.242Z",
|
||||||
"reasonDetail": "Used only to set focus"
|
"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",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/ConversationHeader.js",
|
"path": "ts/components/conversation/ConversationHeader.js",
|
||||||
|
@ -7848,23 +7875,5 @@
|
||||||
"lineNumber": 60,
|
"lineNumber": 60,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2019-05-02T20:44:56.470Z"
|
"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…
Add table
Add a link
Reference in a new issue