diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6d823ed972..fa3e7d0dd2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/app/sql.js b/app/sql.js index 0807856881..09dd8a1493 100644 --- a/app/sql.js +++ b/app/sql.js @@ -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( diff --git a/images/more-h.svg b/images/more-h-24.svg similarity index 100% rename from images/more-h.svg rename to images/more-h-24.svg diff --git a/images/profile-solid-16.svg b/images/profile-solid-16.svg new file mode 100644 index 0000000000..afc0df2686 --- /dev/null +++ b/images/profile-solid-16.svg @@ -0,0 +1 @@ +profile-solid-16 \ No newline at end of file diff --git a/images/search-24.svg b/images/search-24.svg new file mode 100644 index 0000000000..a486b9b43b --- /dev/null +++ b/images/search-24.svg @@ -0,0 +1 @@ +search-24 \ No newline at end of file diff --git a/images/timer.svg b/images/timer.svg index 88d7413aa9..d9fd6b29e6 100644 --- a/images/timer.svg +++ b/images/timer.svg @@ -1,14 +1,9 @@ - - - - - - - - - - - - - + + + + diff --git a/js/background.js b/js/background.js index 821c0e74ce..209576238f 100644 --- a/js/background.js +++ b/js/background.js @@ -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 diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index d711ae4d93..41e949111d 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -1,5 +1,9 @@ export function searchMessages(query: string): Promise>; export function searchConversations(query: string): Promise>; +export function searchMessagesInConversation( + query: string, + conversationId: string +): Promise>; export function updateStickerLastUsed( packId: string, diff --git a/js/modules/data.js b/js/modules/data.js index 0e8d2ebb29..355f997b6f 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -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( diff --git a/js/modules/signal.js b/js/modules/signal.js index 233b053b88..3255e4de1d 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -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 = { diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index a517d74369..6597d0058d 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -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) { diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index a80762fb88..1e3d9ddb3d 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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); } } } diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 072c6e3358..9fa78df7d4 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -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 diff --git a/ts/components/LeftPane.md b/ts/components/LeftPane.md index 79eff21381..96d6e67d4d 100644 --- a/ts/components/LeftPane.md +++ b/ts/components/LeftPane.md @@ -418,3 +418,38 @@ const conversations = [ /> ; ``` + +#### Searching in conversation + +```jsx + + + console.log('startNewConversation', query, options) + } + openConversationInternal={(id, messageId) => + console.log('openConversation', id, messageId) + } + showArchivedConversations={() => console.log('showArchivedConversations')} + showInbox={() => console.log('showInbox')} + renderMainHeader={() => ( + console.log('search', result)} + 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} + /> + +``` diff --git a/ts/components/MainHeader.md b/ts/components/MainHeader.md index 819c86d772..80032a17d3 100644 --- a/ts/components/MainHeader.md +++ b/ts/components/MainHeader.md @@ -63,3 +63,38 @@ if the parent of this component feeds the updated `searchTerm` back. /> ``` + +#### Searching within conversation + +```jsx + + console.log('search', args)} + updateSearchTerm={(...args) => console.log('updateSearchTerm', args)} + clearSearch={(...args) => console.log('clearSearch', args)} + i18n={util.i18n} + /> + +``` + +#### Searching within conversation, with search term + +```jsx + + console.log('search', args)} + updateSearchTerm={(...args) => console.log('updateSearchTerm', args)} + clearSearch={(...args) => console.log('clearSearch', args)} + i18n={util.i18n} + /> + +``` diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index ec833e9c5c..f9a1cc4976 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -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 { - private readonly updateSearchBound: ( - event: React.FormEvent - ) => void; - private readonly clearSearchBound: () => void; - private readonly handleKeyUpBound: ( - event: React.KeyboardEvent - ) => void; - private readonly setFocusBound: () => void; +export class MainHeader extends React.Component { private readonly inputRef: React.RefObject; - 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) { - const { updateSearchTerm, clearSearch } = this.props; + public updateSearch = (event: React.FormEvent) => { + 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 { 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) { - const { clearSearch } = this.props; + public clearConversationSearch = () => { + const { clearConversationSearch } = this.props; - if (event.key === 'Escape') { + clearConversationSearch(); + this.setFocus(); + }; + + public handleKeyUp = (event: React.KeyboardEvent) => { + 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 (
{ size={28} />
-
+ {searchConversationId ? ( +
+
+
+
+
+ ) : ( +
{this.renderExpirationLength()} - {this.renderGear(triggerId)} + {this.renderSearchButton()} + {this.renderMoreButton(triggerId)} {this.renderMenu(triggerId)}
); diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 8aa311defb..9b8e4ca659 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -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; conversations: Array; @@ -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 { - 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 { diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index b50d137328..e014a5691c 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -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 ); }; diff --git a/ts/state/smart/MainHeader.tsx b/ts/state/smart/MainHeader.tsx index e9db0c481c..5a1bef6d9d 100644 --- a/ts/state/smart/MainHeader.tsx +++ b/ts/state/smart/MainHeader.tsx @@ -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), diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 1243b72414..d3f120a264 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -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"