Full-text search within conversation
This commit is contained in:
parent
6292019d30
commit
c39d5a811a
26 changed files with 697 additions and 134 deletions
|
@ -735,6 +735,17 @@
|
|||
"message": "Search",
|
||||
"description": "Placeholder text in the search input"
|
||||
},
|
||||
"searchIn": {
|
||||
"message": "Search in $conversationName$",
|
||||
"description":
|
||||
"Shown in the search box before text is entered when searching in a specific conversation",
|
||||
"placeholders": {
|
||||
"conversationName": {
|
||||
"content": "$1",
|
||||
"example": "Friends"
|
||||
}
|
||||
}
|
||||
},
|
||||
"noSearchResults": {
|
||||
"message": "No results for \"$searchTerm$\"",
|
||||
"description": "Shown in the search left pane when no results were found",
|
||||
|
@ -745,6 +756,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"noSearchResultsInConversation": {
|
||||
"message": "No results for \"$searchTerm$\" in $conversationName$",
|
||||
"description": "Shown in the search left pane when no results were found",
|
||||
"placeholders": {
|
||||
"searchTerm": {
|
||||
"content": "$1",
|
||||
"example": "dog"
|
||||
},
|
||||
"searchTerm": {
|
||||
"content": "$2",
|
||||
"example": "Friends"
|
||||
}
|
||||
}
|
||||
},
|
||||
"conversationsHeader": {
|
||||
"message": "Conversations",
|
||||
"description": "Shown to separate the types of search results"
|
||||
|
|
|
@ -1641,7 +1641,7 @@ async function searchMessages(query, { limit } = {}) {
|
|||
);
|
||||
|
||||
return map(rows, row => ({
|
||||
...jsonToObject(row.json),
|
||||
json: row.json,
|
||||
snippet: row.snippet,
|
||||
}));
|
||||
}
|
||||
|
@ -1670,7 +1670,7 @@ async function searchMessagesInConversation(
|
|||
);
|
||||
|
||||
return map(rows, row => ({
|
||||
...jsonToObject(row.json),
|
||||
json: row.json,
|
||||
snippet: row.snippet,
|
||||
}));
|
||||
}
|
||||
|
@ -1925,7 +1925,7 @@ async function getOlderMessagesByConversation(
|
|||
}
|
||||
);
|
||||
|
||||
return map(rows.reverse(), row => jsonToObject(row.json));
|
||||
return rows.reverse();
|
||||
}
|
||||
|
||||
async function getNewerMessagesByConversation(
|
||||
|
@ -1945,7 +1945,7 @@ async function getNewerMessagesByConversation(
|
|||
}
|
||||
);
|
||||
|
||||
return map(rows, row => jsonToObject(row.json));
|
||||
return rows;
|
||||
}
|
||||
async function getOldestMessageForConversation(conversationId) {
|
||||
const row = await db.get(
|
||||
|
|
Before Width: | Height: | Size: 456 B After Width: | Height: | Size: 456 B |
1
images/profile-solid-16.svg
Normal file
1
images/profile-solid-16.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><title>profile-solid-16</title><path d="M10.5,6.324C10.5,7.98,9.381,9.5,8,9.5S5.5,7.98,5.5,6.324A2.617,2.617,0,0,1,8,3.5,2.617,2.617,0,0,1,10.5,6.324Z"/><path d="M10,10.5H6a3.975,3.975,0,0,0-3.108,1.511,6.486,6.486,0,0,0,10.216,0A3.975,3.975,0,0,0,10,10.5Z"/></svg>
|
After Width: | Height: | Size: 348 B |
1
images/search-24.svg
Normal file
1
images/search-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>search-24</title><path d="M17.161,16.1a3.979,3.979,0,0,1-1.287-.683,8.02,8.02,0,1,0-.457.457,3.959,3.959,0,0,1,.684,1.286L20.47,21.53l1.06-1.06ZM10,16.5A6.5,6.5,0,1,1,16.5,10,6.508,6.508,0,0,1,10,16.5Z"/></svg>
|
After Width: | Height: | Size: 300 B |
|
@ -1,14 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
|
||||
<defs>
|
||||
<path id="a" d="M10 1.5c-.338 0-.672.02-1 .058V.05C9.329.017 9.663 0 10 0c5.523 0 10 4.477 10 10s-4.477 10-10 10S0 15.523 0 10A10 10 0 0 1 5.658.99l.487.843.005-.002 4.5 7.794-1.3.75-4.233-7.333A8.5 8.5 0 1 0 10 1.5z"/>
|
||||
<path id="c" d="M0 0h40v40H0z"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<mask id="b" fill="#fff">
|
||||
<use xlink:href="#a"/>
|
||||
</mask>
|
||||
<g mask="url(#b)">
|
||||
<use fill="#62656A" xlink:href="#c"/>
|
||||
</g>
|
||||
</g>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
|
||||
<path d="M16.1,7.2c0.6-0.1,1.1-0.6,1.1-1.2c0-0.7-0.6-1.2-1.2-1.2c-0.6,0-1.1,0.4-1.2,1c-1.1-1-2.6-1.6-4.1-1.8l0.8-3h-3l0.7,3l0,0
|
||||
c-1.5,0.2-2.9,0.7-4,1.7c-0.1-0.6-0.6-1-1.2-1c-0.7,0-1.3,0.5-1.3,1.2c0,0,0,0.1,0,0.1c0,0.6,0.5,1.2,1.1,1.2
|
||||
c-2.4,3.4-1.5,8.1,1.9,10.4s8.1,1.5,10.4-1.9c0.9-1.3,1.4-2.8,1.3-4.3C17.5,10,17,8.5,16.1,7.2z M10,17.5c-3.3,0-6-2.7-6-6
|
||||
s2.7-6,6-6s6,2.7,6,6S13.3,17.5,10,17.5z M10.9,11.5c0,0.5-0.4,0.9-0.9,0.9S9.1,12,9.1,11.5l0.5-5h0.8L10.9,11.5z"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 650 B After Width: | Height: | Size: 820 B |
|
@ -520,6 +520,10 @@
|
|||
Signal.State.Ducks.user.actions,
|
||||
store.dispatch
|
||||
);
|
||||
actions.search = Signal.State.bindActionCreators(
|
||||
Signal.State.Ducks.search.actions,
|
||||
store.dispatch
|
||||
);
|
||||
actions.stickers = Signal.State.bindActionCreators(
|
||||
Signal.State.Ducks.stickers.actions,
|
||||
store.dispatch
|
||||
|
|
4
js/modules/data.d.ts
vendored
4
js/modules/data.d.ts
vendored
|
@ -1,5 +1,9 @@
|
|||
export function searchMessages(query: string): Promise<Array<any>>;
|
||||
export function searchConversations(query: string): Promise<Array<any>>;
|
||||
export function searchMessagesInConversation(
|
||||
query: string,
|
||||
conversationId: string
|
||||
): Promise<Array<any>>;
|
||||
|
||||
export function updateStickerLastUsed(
|
||||
packId: string,
|
||||
|
|
|
@ -655,9 +655,16 @@ async function searchConversations(query) {
|
|||
return conversations;
|
||||
}
|
||||
|
||||
function handleSearchMessageJSON(messages) {
|
||||
return messages.map(message => ({
|
||||
...JSON.parse(message.json),
|
||||
snippet: message.snippet,
|
||||
}));
|
||||
}
|
||||
|
||||
async function searchMessages(query, { limit } = {}) {
|
||||
const messages = await channels.searchMessages(query, { limit });
|
||||
return messages;
|
||||
return handleSearchMessageJSON(messages);
|
||||
}
|
||||
|
||||
async function searchMessagesInConversation(
|
||||
|
@ -670,7 +677,7 @@ async function searchMessagesInConversation(
|
|||
conversationId,
|
||||
{ limit }
|
||||
);
|
||||
return messages;
|
||||
return handleSearchMessageJSON(messages);
|
||||
}
|
||||
|
||||
// Message
|
||||
|
@ -784,6 +791,10 @@ async function getUnreadByConversation(conversationId, { MessageCollection }) {
|
|||
return new MessageCollection(messages);
|
||||
}
|
||||
|
||||
function handleMessageJSON(messages) {
|
||||
return messages.map(message => JSON.parse(message.json));
|
||||
}
|
||||
|
||||
async function getOlderMessagesByConversation(
|
||||
conversationId,
|
||||
{ limit = 100, receivedAt = Number.MAX_VALUE, MessageCollection }
|
||||
|
@ -796,7 +807,7 @@ async function getOlderMessagesByConversation(
|
|||
}
|
||||
);
|
||||
|
||||
return new MessageCollection(messages);
|
||||
return new MessageCollection(handleMessageJSON(messages));
|
||||
}
|
||||
async function getNewerMessagesByConversation(
|
||||
conversationId,
|
||||
|
@ -810,7 +821,7 @@ async function getNewerMessagesByConversation(
|
|||
}
|
||||
);
|
||||
|
||||
return new MessageCollection(messages);
|
||||
return new MessageCollection(handleMessageJSON(messages));
|
||||
}
|
||||
async function getMessageMetricsForConversation(conversationId) {
|
||||
const result = await channels.getMessageMetricsForConversation(
|
||||
|
|
|
@ -62,6 +62,7 @@ const { createStore } = require('../../ts/state/createStore');
|
|||
const conversationsDuck = require('../../ts/state/ducks/conversations');
|
||||
const emojisDuck = require('../../ts/state/ducks/emojis');
|
||||
const itemsDuck = require('../../ts/state/ducks/items');
|
||||
const searchDuck = require('../../ts/state/ducks/search');
|
||||
const stickersDuck = require('../../ts/state/ducks/stickers');
|
||||
const userDuck = require('../../ts/state/ducks/user');
|
||||
|
||||
|
@ -274,6 +275,7 @@ exports.setup = (options = {}) => {
|
|||
emojis: emojisDuck,
|
||||
items: itemsDuck,
|
||||
user: userDuck,
|
||||
search: searchDuck,
|
||||
stickers: stickersDuck,
|
||||
};
|
||||
const State = {
|
||||
|
|
|
@ -257,6 +257,13 @@
|
|||
this.setDisappearingMessages(seconds),
|
||||
onDeleteMessages: () => this.destroyMessages(),
|
||||
onResetSession: () => this.endSession(),
|
||||
onSearchInConversation: () => {
|
||||
const { searchInConversation } = window.reduxActions.search;
|
||||
const name = this.model.isMe()
|
||||
? i18n('noteToSelf')
|
||||
: this.model.getTitle();
|
||||
searchInConversation(this.model.id, name);
|
||||
},
|
||||
|
||||
// These are view only and don't update the Conversation model, so they
|
||||
// need a manual update call.
|
||||
|
@ -1490,8 +1497,6 @@
|
|||
|
||||
this.focusMessageField();
|
||||
|
||||
this.model.updateLastMessage();
|
||||
|
||||
const statusPromise = this.throttledGetProfiles();
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.statusFetch = statusPromise.then(() =>
|
||||
|
@ -1522,6 +1527,8 @@
|
|||
if (quotedMessageId) {
|
||||
this.setQuoteMessage(quotedMessageId);
|
||||
}
|
||||
|
||||
this.model.updateLastMessage();
|
||||
},
|
||||
|
||||
async retrySend(messageId) {
|
||||
|
|
|
@ -1644,12 +1644,17 @@
|
|||
align-items: center;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
transition: opacity 250ms ease-out;
|
||||
|
||||
&--hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.module-conversation-header__expiration__clock-icon {
|
||||
@include color-svg('../images/timer.svg', $color-gray-60);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
@ -1658,11 +1663,29 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.module-conversation-header__gear-icon {
|
||||
@include color-svg('../images/gear.svg', $color-gray-60);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-left: 4px;
|
||||
.module-conversation-header__more-button {
|
||||
@include color-svg('../images/more-h-24.svg', $color-gray-75);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-left: 12px;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms ease-out;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&--show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.module-conversation-header__search-button {
|
||||
@include color-svg('../images/search-24.svg', $color-gray-75);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-left: 12px;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms ease-out;
|
||||
|
@ -2402,6 +2425,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.module-main-header__search__input--in-conversation {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.module-main-header__search__icon {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
|
@ -2413,6 +2440,41 @@
|
|||
@include color-svg('../images/search.svg', $color-gray-60);
|
||||
}
|
||||
|
||||
.module-main-header__search__in-conversation-pill {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
bottom: 3px;
|
||||
|
||||
border-radius: 14px;
|
||||
width: 42px;
|
||||
background-color: $color-gray-05;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.module-main-header__search__in-conversation-pill__avatar-container {
|
||||
margin-left: 4px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
background-color: $color-signal-blue;
|
||||
}
|
||||
.module-main-header__search__in-conversation-pill__avatar {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
@include color-svg('../images/profile-solid-16.svg', $color-white);
|
||||
}
|
||||
.module-main-header__search__in-conversation-pill__x-button {
|
||||
margin-left: 2px;
|
||||
@include color-svg('../images/x.svg', $color-gray-60);
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.module-main-header__search__cancel-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
|
@ -3142,8 +3204,24 @@
|
|||
|
||||
.module-search-results__no-results {
|
||||
margin-top: 27px;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
animation: delayed-fade-in 2s;
|
||||
}
|
||||
|
||||
@keyframes delayed-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.module-search-results__contacts-header {
|
||||
|
@ -3872,10 +3950,10 @@
|
|||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
@include light-theme {
|
||||
@include color-svg('../images/more-h.svg', $color-gray-60);
|
||||
@include color-svg('../images/more-h-24.svg', $color-gray-60);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/more-h.svg', $color-gray-25);
|
||||
@include color-svg('../images/more-h-24.svg', $color-gray-25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1148,7 +1148,7 @@ body.dark-theme {
|
|||
}
|
||||
|
||||
.module-conversation-header__back-icon {
|
||||
@include color-svg('../images/back.svg', $color-dark-05);
|
||||
background-color: $color-dark-05;
|
||||
}
|
||||
|
||||
.module-conversation-header__title {
|
||||
|
@ -1160,15 +1160,19 @@ body.dark-theme {
|
|||
}
|
||||
|
||||
.module-conversation-header__title__verified-icon {
|
||||
@include color-svg('../images/verified-check.svg', $color-dark-05);
|
||||
background-color: $color-dark-05;
|
||||
}
|
||||
|
||||
.module-conversation-header__expiration__clock-icon {
|
||||
@include color-svg('../images/timer.svg', $color-dark-30);
|
||||
background-color: $color-gray-25;
|
||||
}
|
||||
|
||||
.module-conversation-header__gear-icon {
|
||||
@include color-svg('../images/gear.svg', $color-dark-30);
|
||||
.module-conversation-header__more-button {
|
||||
background-color: $color-gray-15;
|
||||
}
|
||||
|
||||
.module-conversation-header__search-button {
|
||||
background-color: $color-gray-15;
|
||||
}
|
||||
|
||||
// Module: Message Detail
|
||||
|
@ -1398,11 +1402,24 @@ body.dark-theme {
|
|||
}
|
||||
|
||||
.module-main-header__search__icon {
|
||||
@include color-svg('../images/search.svg', $color-gray-25);
|
||||
background-color: $color-gray-25;
|
||||
}
|
||||
|
||||
.module-main-header__search__cancel-icon {
|
||||
@include color-svg('../images/x-16.svg', $color-gray-25);
|
||||
background-color: $color-gray-25;
|
||||
}
|
||||
|
||||
.module-main-header__search__in-conversation-pill {
|
||||
background-color: $color-gray-75;
|
||||
}
|
||||
.module-main-header__search__in-conversation-pill__avatar-container {
|
||||
background-color: $color-signal-blue;
|
||||
}
|
||||
.module-main-header__search__in-conversation-pill__avatar {
|
||||
background-color: $color-gray-05;
|
||||
}
|
||||
.module-main-header__search__in-conversation-pill__x-button {
|
||||
background-color: $color-gray-25;
|
||||
}
|
||||
|
||||
// Module: Image
|
||||
|
|
|
@ -418,3 +418,38 @@ const conversations = [
|
|||
/>
|
||||
</util.LeftPaneContext>;
|
||||
```
|
||||
|
||||
#### Searching in conversation
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||
<LeftPane
|
||||
searchResults={{
|
||||
searchConversationName: "Y'all 🌆",
|
||||
}}
|
||||
conversations={[]}
|
||||
archivedConversations={[]}
|
||||
showArchived={false}
|
||||
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=""
|
||||
search={result => console.log('search', result)}
|
||||
searchConversationName="Y'all 🌆"
|
||||
searchConversationId="group-id-1"
|
||||
updateSearch={result => console.log('updateSearch', result)}
|
||||
clearSearch={result => console.log('clearSearch', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
|
|
@ -63,3 +63,38 @@ if the parent of this component feeds the updated `searchTerm` back.
|
|||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Searching within conversation
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
<MainHeader
|
||||
name="John Smith"
|
||||
color="purple"
|
||||
searchConversationId="group-id-1"
|
||||
searchConversationName="Everyone 🔥"
|
||||
search={(...args) => console.log('search', args)}
|
||||
updateSearchTerm={(...args) => console.log('updateSearchTerm', args)}
|
||||
clearSearch={(...args) => console.log('clearSearch', args)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Searching within conversation, with search term
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
<MainHeader
|
||||
name="John Smith"
|
||||
color="purple"
|
||||
searchConversationId="group-id-1"
|
||||
searchConversationName="Everyone 🔥"
|
||||
searchTerm="address"
|
||||
search={(...args) => console.log('search', args)}
|
||||
updateSearchTerm={(...args) => console.log('updateSearchTerm', args)}
|
||||
clearSearch={(...args) => console.log('clearSearch', args)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { Avatar } from './Avatar';
|
||||
|
||||
import { cleanSearchTerm } from '../util/cleanSearchTerm';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export interface Props {
|
||||
export interface PropsType {
|
||||
searchTerm: string;
|
||||
searchConversationName?: string;
|
||||
searchConversationId?: string;
|
||||
|
||||
// To be used as an ID
|
||||
ourNumber: string;
|
||||
|
@ -27,55 +28,73 @@ export interface Props {
|
|||
search: (
|
||||
query: string,
|
||||
options: {
|
||||
searchConversationId?: string;
|
||||
regionCode: string;
|
||||
ourNumber: string;
|
||||
noteToSelf: string;
|
||||
}
|
||||
) => void;
|
||||
|
||||
clearConversationSearch: () => void;
|
||||
clearSearch: () => void;
|
||||
}
|
||||
|
||||
export class MainHeader extends React.Component<Props> {
|
||||
private readonly updateSearchBound: (
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) => void;
|
||||
private readonly clearSearchBound: () => void;
|
||||
private readonly handleKeyUpBound: (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => void;
|
||||
private readonly setFocusBound: () => void;
|
||||
export class MainHeader extends React.Component<PropsType> {
|
||||
private readonly inputRef: React.RefObject<HTMLInputElement>;
|
||||
private readonly debouncedSearch: (searchTerm: string) => void;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
this.updateSearchBound = this.updateSearch.bind(this);
|
||||
this.clearSearchBound = this.clearSearch.bind(this);
|
||||
this.handleKeyUpBound = this.handleKeyUp.bind(this);
|
||||
this.setFocusBound = this.setFocus.bind(this);
|
||||
this.inputRef = React.createRef();
|
||||
|
||||
this.debouncedSearch = debounce(this.search.bind(this), 20);
|
||||
}
|
||||
|
||||
public search() {
|
||||
const { searchTerm, search, i18n, ourNumber, regionCode } = this.props;
|
||||
public componentDidUpdate(prevProps: PropsType) {
|
||||
const { searchConversationId } = this.props;
|
||||
|
||||
// When user chooses to search in a given conversation we focus the field for them
|
||||
if (
|
||||
searchConversationId &&
|
||||
searchConversationId !== prevProps.searchConversationId
|
||||
) {
|
||||
this.setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line member-ordering
|
||||
public search = debounce((searchTerm: string) => {
|
||||
const {
|
||||
i18n,
|
||||
ourNumber,
|
||||
regionCode,
|
||||
search,
|
||||
searchConversationId,
|
||||
} = this.props;
|
||||
|
||||
if (search) {
|
||||
search(searchTerm, {
|
||||
searchConversationId,
|
||||
noteToSelf: i18n('noteToSelf').toLowerCase(),
|
||||
ourNumber,
|
||||
regionCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
|
||||
public updateSearch(event: React.FormEvent<HTMLInputElement>) {
|
||||
const { updateSearchTerm, clearSearch } = this.props;
|
||||
public updateSearch = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const {
|
||||
updateSearchTerm,
|
||||
clearConversationSearch,
|
||||
clearSearch,
|
||||
searchConversationId,
|
||||
} = this.props;
|
||||
const searchTerm = event.currentTarget.value;
|
||||
|
||||
if (!searchTerm) {
|
||||
clearSearch();
|
||||
if (searchConversationId) {
|
||||
clearConversationSearch();
|
||||
} else {
|
||||
clearSearch();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -88,47 +107,82 @@ export class MainHeader extends React.Component<Props> {
|
|||
return;
|
||||
}
|
||||
|
||||
const cleanedTerm = cleanSearchTerm(searchTerm);
|
||||
if (!cleanedTerm) {
|
||||
return;
|
||||
}
|
||||
this.search(searchTerm);
|
||||
};
|
||||
|
||||
this.debouncedSearch(cleanedTerm);
|
||||
}
|
||||
|
||||
public clearSearch() {
|
||||
public clearSearch = () => {
|
||||
const { clearSearch } = this.props;
|
||||
|
||||
clearSearch();
|
||||
this.setFocus();
|
||||
}
|
||||
};
|
||||
|
||||
public handleKeyUp(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
const { clearSearch } = this.props;
|
||||
public clearConversationSearch = () => {
|
||||
const { clearConversationSearch } = this.props;
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
clearConversationSearch();
|
||||
this.setFocus();
|
||||
};
|
||||
|
||||
public handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const {
|
||||
clearConversationSearch,
|
||||
clearSearch,
|
||||
searchConversationId,
|
||||
searchTerm,
|
||||
} = this.props;
|
||||
|
||||
if (event.key !== 'Escape') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchConversationId && searchTerm) {
|
||||
clearConversationSearch();
|
||||
} else {
|
||||
clearSearch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public setFocus() {
|
||||
public handleXButton = () => {
|
||||
const {
|
||||
searchConversationId,
|
||||
clearConversationSearch,
|
||||
clearSearch,
|
||||
} = this.props;
|
||||
|
||||
if (searchConversationId) {
|
||||
clearConversationSearch();
|
||||
} else {
|
||||
clearSearch();
|
||||
}
|
||||
|
||||
this.setFocus();
|
||||
};
|
||||
|
||||
public setFocus = () => {
|
||||
if (this.inputRef.current) {
|
||||
// @ts-ignore
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
searchTerm,
|
||||
avatarPath,
|
||||
i18n,
|
||||
color,
|
||||
i18n,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
searchTerm,
|
||||
} = this.props;
|
||||
|
||||
const placeholder = searchConversationName
|
||||
? i18n('searchIn', [searchConversationName])
|
||||
: i18n('search');
|
||||
|
||||
return (
|
||||
<div className="module-main-header">
|
||||
<Avatar
|
||||
|
@ -142,26 +196,42 @@ export class MainHeader extends React.Component<Props> {
|
|||
size={28}
|
||||
/>
|
||||
<div className="module-main-header__search">
|
||||
<div
|
||||
role="button"
|
||||
className="module-main-header__search__icon"
|
||||
onClick={this.setFocusBound}
|
||||
/>
|
||||
{searchConversationId ? (
|
||||
<div className="module-main-header__search__in-conversation-pill">
|
||||
<div className="module-main-header__search__in-conversation-pill__avatar-container">
|
||||
<div className="module-main-header__search__in-conversation-pill__avatar" />
|
||||
</div>
|
||||
<button
|
||||
className="module-main-header__search__in-conversation-pill__x-button"
|
||||
onClick={this.clearSearch}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="module-main-header__search__icon"
|
||||
onClick={this.setFocus}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
ref={this.inputRef}
|
||||
className="module-main-header__search__input"
|
||||
placeholder={i18n('search')}
|
||||
className={classNames(
|
||||
'module-main-header__search__input',
|
||||
searchConversationId
|
||||
? 'module-main-header__search__input--in-conversation'
|
||||
: null
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
dir="auto"
|
||||
onKeyUp={this.handleKeyUpBound}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
value={searchTerm}
|
||||
onChange={this.updateSearchBound}
|
||||
onChange={this.updateSearch}
|
||||
/>
|
||||
{searchTerm ? (
|
||||
<div
|
||||
role="button"
|
||||
className="module-main-header__search__cancel-icon"
|
||||
onClick={this.clearSearchBound}
|
||||
onClick={this.handleXButton}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
@ -82,6 +82,47 @@
|
|||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Searching within conversation
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
<MessageSearchResult
|
||||
isSearchingInConversation={true}
|
||||
from={{
|
||||
name: 'Someone 🔥',
|
||||
phoneNumber: '(202) 555-0011',
|
||||
avatarPath: util.gifObjectUrl,
|
||||
}}
|
||||
to={{
|
||||
name: 'Everyone 🔥',
|
||||
}}
|
||||
snippet="What's <<left>>going<<right>> on?"
|
||||
id="messageId1"
|
||||
conversationId="conversationId1"
|
||||
receivedAt={Date.now() - 3 * 60 * 1000}
|
||||
onClick={result => console.log('onClick', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<MessageSearchResult
|
||||
isSearchingInConversation={true}
|
||||
from={{
|
||||
name: 'Someone 🔥',
|
||||
phoneNumber: '(202) 555-0011',
|
||||
avatarPath: util.gifObjectUrl,
|
||||
}}
|
||||
to={{
|
||||
name: 'Everyone 🔥',
|
||||
}}
|
||||
snippet="How is everyone? <<left>>Going<<right>> well?"
|
||||
id="messageId2"
|
||||
conversationId="conversationId2"
|
||||
receivedAt={Date.now() - 27 * 60 * 1000}
|
||||
onClick={result => console.log('onClick', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### From you and to you
|
||||
|
||||
```jsx
|
||||
|
|
|
@ -10,6 +10,7 @@ import { LocalizerType } from '../types/Util';
|
|||
|
||||
export type PropsDataType = {
|
||||
isSelected?: boolean;
|
||||
isSearchingInConversation?: boolean;
|
||||
|
||||
id: string;
|
||||
conversationId: string;
|
||||
|
@ -75,10 +76,10 @@ export class MessageSearchResult extends React.PureComponent<PropsType> {
|
|||
}
|
||||
|
||||
public renderFrom() {
|
||||
const { i18n, to } = this.props;
|
||||
const { i18n, to, isSearchingInConversation } = this.props;
|
||||
const fromName = this.renderFromName();
|
||||
|
||||
if (!to.isMe) {
|
||||
if (!to.isMe && !isSearchingInConversation) {
|
||||
return (
|
||||
<div className="module-message-search-result__header__from">
|
||||
{fromName} {i18n('to')}{' '}
|
||||
|
|
|
@ -727,6 +727,76 @@ const items = [
|
|||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### With no results at all, searching in conversation
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
items={[]}
|
||||
noResults={true}
|
||||
searchTerm="something"
|
||||
searchInConversationName="Everyone 🔥"
|
||||
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>
|
||||
```
|
||||
|
||||
#### Searching in conversation but no search term
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext
|
||||
theme={util.theme}
|
||||
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
|
||||
>
|
||||
<SearchResults
|
||||
items={[]}
|
||||
noResults={true}
|
||||
searchTerm=""
|
||||
searchInConversationName="Everyone 🔥"
|
||||
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>
|
||||
```
|
||||
|
||||
#### With a lot of results
|
||||
|
||||
```jsx
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
List,
|
||||
} from 'react-virtualized';
|
||||
|
||||
import { Intl } from './Intl';
|
||||
import { Emojify } from './conversation/Emojify';
|
||||
import {
|
||||
ConversationListItem,
|
||||
PropsData as ConversationListItemPropsType,
|
||||
|
@ -19,6 +21,7 @@ export type PropsDataType = {
|
|||
noResults: boolean;
|
||||
regionCode: string;
|
||||
searchTerm: string;
|
||||
searchConversationName?: string;
|
||||
};
|
||||
|
||||
type StartNewConversationType = {
|
||||
|
@ -237,14 +240,33 @@ export class SearchResults extends React.Component<PropsType> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { items, i18n, noResults, searchTerm } = this.props;
|
||||
const {
|
||||
i18n,
|
||||
items,
|
||||
noResults,
|
||||
searchConversationName,
|
||||
searchTerm,
|
||||
} = this.props;
|
||||
|
||||
if (noResults) {
|
||||
return (
|
||||
<div className="module-search-results">
|
||||
<div className="module-search-results__no-results">
|
||||
{i18n('noSearchResults', [searchTerm])}
|
||||
</div>
|
||||
{!searchConversationName || searchTerm ? (
|
||||
<div className="module-search-results__no-results" key={searchTerm}>
|
||||
{searchConversationName ? (
|
||||
<Intl
|
||||
id="noSearchResultsInConversation"
|
||||
i18n={i18n}
|
||||
components={[
|
||||
searchTerm,
|
||||
<Emojify key="item-1" text={searchConversationName} />,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
i18n('noSearchResults', [searchTerm])
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
### Name variations, 1:1 conversation
|
||||
|
||||
Note the five items in gear menu, and the second-level menu with disappearing messages options. Disappearing message set to 'off'.
|
||||
Note the five items in menu, and the second-level menu with disappearing messages options. Disappearing message set to 'off'.
|
||||
|
||||
#### With name and profile, verified
|
||||
|
||||
|
@ -24,6 +24,7 @@ Note the five items in gear menu, and the second-level menu with disappearing me
|
|||
onShowAllMedia={() => console.log('onShowAllMedia')}
|
||||
onShowGroupMembers={() => console.log('onShowGroupMembers')}
|
||||
onGoBack={() => console.log('onGoBack')}
|
||||
onSearchInConversation={() => console.log('onSearchInConversation')}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
|
|
@ -37,6 +37,7 @@ interface Props {
|
|||
onSetDisappearingMessages: (seconds: number) => void;
|
||||
onDeleteMessages: () => void;
|
||||
onResetSession: () => void;
|
||||
onSearchInConversation: () => void;
|
||||
|
||||
onShowSafetyNumber: () => void;
|
||||
onShowAllMedia: () => void;
|
||||
|
@ -152,14 +153,21 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public renderExpirationLength() {
|
||||
const { expirationSettingName } = this.props;
|
||||
const { expirationSettingName, showBackButton } = this.props;
|
||||
|
||||
if (!expirationSettingName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-conversation-header__expiration">
|
||||
<div
|
||||
className={classNames(
|
||||
'module-conversation-header__expiration',
|
||||
showBackButton
|
||||
? 'module-conversation-header__expiration--hidden'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
<div className="module-conversation-header__expiration__clock-icon" />
|
||||
<div className="module-conversation-header__expiration__setting">
|
||||
{expirationSettingName}
|
||||
|
@ -168,7 +176,7 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderGear(triggerId: string) {
|
||||
public renderMoreButton(triggerId: string) {
|
||||
const { showBackButton } = this.props;
|
||||
|
||||
return (
|
||||
|
@ -176,10 +184,10 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
<button
|
||||
onClick={this.showMenuBound}
|
||||
className={classNames(
|
||||
'module-conversation-header__gear-icon',
|
||||
'module-conversation-header__more-button',
|
||||
showBackButton
|
||||
? null
|
||||
: 'module-conversation-header__gear-icon--show'
|
||||
: 'module-conversation-header__more-button--show'
|
||||
)}
|
||||
disabled={showBackButton}
|
||||
/>
|
||||
|
@ -187,6 +195,23 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderSearchButton() {
|
||||
const { onSearchInConversation, showBackButton } = this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onSearchInConversation}
|
||||
className={classNames(
|
||||
'module-conversation-header__search-button',
|
||||
showBackButton
|
||||
? null
|
||||
: 'module-conversation-header__search-button--show'
|
||||
)}
|
||||
disabled={showBackButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMenu(triggerId: string) {
|
||||
const {
|
||||
i18n,
|
||||
|
@ -260,7 +285,8 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
</div>
|
||||
</div>
|
||||
{this.renderExpirationLength()}
|
||||
{this.renderGear(triggerId)}
|
||||
{this.renderSearchButton()}
|
||||
{this.renderMoreButton(triggerId)}
|
||||
{this.renderMenu(triggerId)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,11 @@ import { omit, reject } from 'lodash';
|
|||
import { normalize } from '../../types/PhoneNumber';
|
||||
import { trigger } from '../../shims/events';
|
||||
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||
import { searchConversations, searchMessages } from '../../../js/modules/data';
|
||||
import {
|
||||
searchConversations,
|
||||
searchMessages,
|
||||
searchMessagesInConversation,
|
||||
} from '../../../js/modules/data';
|
||||
import { makeLookup } from '../../util/makeLookup';
|
||||
|
||||
import {
|
||||
|
@ -25,6 +29,8 @@ export type MessageSearchResultLookupType = {
|
|||
};
|
||||
|
||||
export type SearchStateType = {
|
||||
searchConversationId?: string;
|
||||
searchConversationName?: string;
|
||||
// We store just ids of conversations, since that data is always cached in memory
|
||||
contacts: Array<string>;
|
||||
conversations: Array<string>;
|
||||
|
@ -64,11 +70,24 @@ type ClearSearchActionType = {
|
|||
type: 'SEARCH_CLEAR';
|
||||
payload: null;
|
||||
};
|
||||
type ClearConversationSearchActionType = {
|
||||
type: 'CLEAR_CONVERSATION_SEARCH';
|
||||
payload: null;
|
||||
};
|
||||
type SearchInConversationActionType = {
|
||||
type: 'SEARCH_IN_CONVERSATION';
|
||||
payload: {
|
||||
searchConversationId: string;
|
||||
searchConversationName: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type SEARCH_TYPES =
|
||||
| SearchResultsFulfilledActionType
|
||||
| UpdateSearchTermActionType
|
||||
| ClearSearchActionType
|
||||
| ClearConversationSearchActionType
|
||||
| SearchInConversationActionType
|
||||
| MessageDeletedActionType
|
||||
| RemoveAllConversationsActionType
|
||||
| SelectedConversationChangedActionType;
|
||||
|
@ -78,13 +97,20 @@ export type SEARCH_TYPES =
|
|||
export const actions = {
|
||||
search,
|
||||
clearSearch,
|
||||
clearConversationSearch,
|
||||
searchInConversation,
|
||||
updateSearchTerm,
|
||||
startNewConversation,
|
||||
};
|
||||
|
||||
function search(
|
||||
query: string,
|
||||
options: { regionCode: string; ourNumber: string; noteToSelf: string }
|
||||
options: {
|
||||
searchConversationId?: string;
|
||||
regionCode: string;
|
||||
ourNumber: string;
|
||||
noteToSelf: string;
|
||||
}
|
||||
): SearchResultsKickoffActionType {
|
||||
return {
|
||||
type: 'SEARCH_RESULTS',
|
||||
|
@ -95,26 +121,40 @@ function search(
|
|||
async function doSearch(
|
||||
query: string,
|
||||
options: {
|
||||
searchConversationId?: string;
|
||||
regionCode: string;
|
||||
ourNumber: string;
|
||||
noteToSelf: string;
|
||||
}
|
||||
): Promise<SearchResultsPayloadType> {
|
||||
const { regionCode, ourNumber, noteToSelf } = options;
|
||||
const { regionCode, ourNumber, noteToSelf, searchConversationId } = options;
|
||||
const normalizedPhoneNumber = normalize(query, { regionCode });
|
||||
|
||||
const [discussions, messages] = await Promise.all([
|
||||
queryConversationsAndContacts(query, { ourNumber, noteToSelf }),
|
||||
queryMessages(query),
|
||||
]);
|
||||
const { conversations, contacts } = discussions;
|
||||
if (searchConversationId) {
|
||||
const messages = await queryMessages(query, searchConversationId);
|
||||
|
||||
return {
|
||||
query,
|
||||
normalizedPhoneNumber: normalize(query, { regionCode }),
|
||||
conversations,
|
||||
contacts,
|
||||
messages,
|
||||
};
|
||||
return {
|
||||
contacts: [],
|
||||
conversations: [],
|
||||
messages,
|
||||
normalizedPhoneNumber,
|
||||
query,
|
||||
};
|
||||
} else {
|
||||
const [discussions, messages] = await Promise.all([
|
||||
queryConversationsAndContacts(query, { ourNumber, noteToSelf }),
|
||||
queryMessages(query),
|
||||
]);
|
||||
const { conversations, contacts } = discussions;
|
||||
|
||||
return {
|
||||
contacts,
|
||||
conversations,
|
||||
messages,
|
||||
normalizedPhoneNumber,
|
||||
query,
|
||||
};
|
||||
}
|
||||
}
|
||||
function clearSearch(): ClearSearchActionType {
|
||||
return {
|
||||
|
@ -122,6 +162,25 @@ function clearSearch(): ClearSearchActionType {
|
|||
payload: null,
|
||||
};
|
||||
}
|
||||
function clearConversationSearch(): ClearConversationSearchActionType {
|
||||
return {
|
||||
type: 'CLEAR_CONVERSATION_SEARCH',
|
||||
payload: null,
|
||||
};
|
||||
}
|
||||
function searchInConversation(
|
||||
searchConversationId: string,
|
||||
searchConversationName: string
|
||||
): SearchInConversationActionType {
|
||||
return {
|
||||
type: 'SEARCH_IN_CONVERSATION',
|
||||
payload: {
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function updateSearchTerm(query: string): UpdateSearchTermActionType {
|
||||
return {
|
||||
type: 'SEARCH_UPDATE',
|
||||
|
@ -147,10 +206,14 @@ function startNewConversation(
|
|||
};
|
||||
}
|
||||
|
||||
async function queryMessages(query: string) {
|
||||
async function queryMessages(query: string, searchConversationId?: string) {
|
||||
try {
|
||||
const normalized = cleanSearchTerm(query);
|
||||
|
||||
if (searchConversationId) {
|
||||
return searchMessagesInConversation(normalized, searchConversationId);
|
||||
}
|
||||
|
||||
return searchMessages(normalized);
|
||||
} catch (e) {
|
||||
return [];
|
||||
|
@ -206,6 +269,7 @@ function getEmptyState(): SearchStateType {
|
|||
};
|
||||
}
|
||||
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
export function reducer(
|
||||
state: SearchStateType = getEmptyState(),
|
||||
action: SEARCH_TYPES
|
||||
|
@ -224,6 +288,30 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === 'SEARCH_IN_CONVERSATION') {
|
||||
const { payload } = action;
|
||||
const { searchConversationId, searchConversationName } = payload;
|
||||
|
||||
if (searchConversationId === state.searchConversationId) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...getEmptyState(),
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
};
|
||||
}
|
||||
if (action.type === 'CLEAR_CONVERSATION_SEARCH') {
|
||||
const { searchConversationId, searchConversationName } = state;
|
||||
|
||||
return {
|
||||
...getEmptyState(),
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'SEARCH_RESULTS_FULFILLED') {
|
||||
const { payload } = action;
|
||||
const {
|
||||
|
@ -258,10 +346,11 @@ export function reducer(
|
|||
|
||||
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
|
||||
const { payload } = action;
|
||||
const { messageId } = payload;
|
||||
const { id, messageId } = payload;
|
||||
const { searchConversationId } = state;
|
||||
|
||||
if (!messageId) {
|
||||
return state;
|
||||
if (searchConversationId && searchConversationId !== id) {
|
||||
return getEmptyState();
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -40,12 +40,22 @@ export const getSelectedMessage = createSelector(
|
|||
(state: SearchStateType): string | undefined => state.selectedMessage
|
||||
);
|
||||
|
||||
export const getSearchConversationId = createSelector(
|
||||
getSearch,
|
||||
(state: SearchStateType): string | undefined => state.searchConversationId
|
||||
);
|
||||
|
||||
export const getSearchConversationName = createSelector(
|
||||
getSearch,
|
||||
(state: SearchStateType): string | undefined => state.searchConversationName
|
||||
);
|
||||
|
||||
export const isSearching = createSelector(
|
||||
getSearch,
|
||||
(state: SearchStateType) => {
|
||||
const { query } = state;
|
||||
const { query, searchConversationId } = state;
|
||||
|
||||
return query && query.trim().length > 1;
|
||||
return (query && query.trim().length > 1) || searchConversationId;
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -62,7 +72,12 @@ export const getSearchResults = createSelector(
|
|||
lookup: ConversationLookupType,
|
||||
selectedConversation?: string
|
||||
): SearchResultsPropsType => {
|
||||
const { conversations, contacts, messageIds } = state;
|
||||
const {
|
||||
contacts,
|
||||
conversations,
|
||||
messageIds,
|
||||
searchConversationName,
|
||||
} = state;
|
||||
|
||||
const showStartNewConversation = Boolean(
|
||||
state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber]
|
||||
|
@ -136,6 +151,7 @@ export const getSearchResults = createSelector(
|
|||
items,
|
||||
noResults,
|
||||
regionCode: regionCode,
|
||||
searchConversationName,
|
||||
searchTerm: state.query,
|
||||
};
|
||||
}
|
||||
|
@ -151,6 +167,7 @@ export function _messageSearchResultSelector(
|
|||
sender?: ConversationType,
|
||||
// @ts-ignore
|
||||
recipient?: ConversationType,
|
||||
searchConversationId?: string,
|
||||
selectedMessageId?: string
|
||||
): MessageSearchResultPropsDataType {
|
||||
// Note: We don't use all of those parameters here, but the shim we call does.
|
||||
|
@ -158,6 +175,7 @@ export function _messageSearchResultSelector(
|
|||
return {
|
||||
...getSearchResultsProps(message),
|
||||
isSelected: message.id === selectedMessageId,
|
||||
isSearchingInConversation: Boolean(searchConversationId),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -169,6 +187,7 @@ type CachedMessageSearchResultSelectorType = (
|
|||
regionCode: string,
|
||||
sender?: ConversationType,
|
||||
recipient?: ConversationType,
|
||||
searchConversationId?: string,
|
||||
selectedMessageId?: string
|
||||
) => MessageSearchResultPropsDataType;
|
||||
export const getCachedSelectorForMessageSearchResult = createSelector(
|
||||
|
@ -189,6 +208,7 @@ export const getMessageSearchResultSelector = createSelector(
|
|||
getMessageSearchResultLookup,
|
||||
getSelectedMessage,
|
||||
getConversationSelector,
|
||||
getSearchConversationId,
|
||||
getRegionCode,
|
||||
getUserNumber,
|
||||
(
|
||||
|
@ -196,6 +216,7 @@ export const getMessageSearchResultSelector = createSelector(
|
|||
messageSearchResultLookup: MessageSearchResultLookupType,
|
||||
selectedMessage: string | undefined,
|
||||
conversationSelector: GetConversationByIdType,
|
||||
searchConversationId: string | undefined,
|
||||
regionCode: string,
|
||||
ourNumber: string
|
||||
): GetMessageSearchResultByIdType => {
|
||||
|
@ -223,6 +244,7 @@ export const getMessageSearchResultSelector = createSelector(
|
|||
regionCode,
|
||||
sender,
|
||||
recipient,
|
||||
searchConversationId,
|
||||
selectedMessage
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,13 +4,19 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { MainHeader } from '../../components/MainHeader';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getQuery } from '../selectors/search';
|
||||
import {
|
||||
getQuery,
|
||||
getSearchConversationId,
|
||||
getSearchConversationName,
|
||||
} from '../selectors/search';
|
||||
import { getIntl, getRegionCode, getUserNumber } from '../selectors/user';
|
||||
import { getMe } from '../selectors/conversations';
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
return {
|
||||
searchTerm: getQuery(state),
|
||||
searchConversationId: getSearchConversationId(state),
|
||||
searchConversationName: getSearchConversationName(state),
|
||||
regionCode: getRegionCode(state),
|
||||
ourNumber: getUserNumber(state),
|
||||
...getMe(state),
|
||||
|
|
|
@ -7810,25 +7810,25 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/MainHeader.js",
|
||||
"line": " this.inputRef = react_1.default.createRef();",
|
||||
"lineNumber": 17,
|
||||
"lineNumber": 83,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-09T00:08:44.242Z",
|
||||
"updated": "2019-08-09T21:17:57.798Z",
|
||||
"reasonDetail": "Used only to set focus"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/MainHeader.tsx",
|
||||
"line": " this.inputRef = React.createRef();",
|
||||
"lineNumber": 57,
|
||||
"lineNumber": 48,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-09T00:08:44.242Z",
|
||||
"updated": "2019-08-09T21:17:57.798Z",
|
||||
"reasonDetail": "Used only to set focus"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/SearchResults.js",
|
||||
"line": " this.listRef = react_1.default.createRef();",
|
||||
"lineNumber": 19,
|
||||
"lineNumber": 21,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-08-09T00:44:31.008Z",
|
||||
"reasonDetail": "SearchResults needs to interact with its child List directly"
|
||||
|
@ -7846,7 +7846,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||
"line": " this.menuTriggerRef = React.createRef();",
|
||||
"lineNumber": 59,
|
||||
"lineNumber": 60,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Used to reference popup menu"
|
||||
|
|
Loading…
Add table
Reference in a new issue