{fromName} {i18n('to')}{' '}
diff --git a/ts/components/SearchResults.md b/ts/components/SearchResults.md
index 4b1804b9d6..da055bc552 100644
--- a/ts/components/SearchResults.md
+++ b/ts/components/SearchResults.md
@@ -727,6 +727,76 @@ const items = [
```
+#### With no results at all, searching in conversation
+
+```jsx
+
+
+ console.log('openConversationInternal', args)
+ }
+ startNewConversation={(...args) =>
+ console.log('startNewConversation', args)
+ }
+ onStartNewConversation={(...args) =>
+ console.log('onStartNewConversation', args)
+ }
+ renderMessageSearchResult={id => (
+
+ console.log('openConversationInternal', args)
+ }
+ />
+ )}
+ />
+
+```
+
+#### Searching in conversation but no search term
+
+```jsx
+
+
+ console.log('openConversationInternal', args)
+ }
+ startNewConversation={(...args) =>
+ console.log('startNewConversation', args)
+ }
+ onStartNewConversation={(...args) =>
+ console.log('onStartNewConversation', args)
+ }
+ renderMessageSearchResult={id => (
+
+ console.log('openConversationInternal', args)
+ }
+ />
+ )}
+ />
+
+```
+
#### With a lot of results
```jsx
diff --git a/ts/components/SearchResults.tsx b/ts/components/SearchResults.tsx
index 5637f6c4e1..94b5319a08 100644
--- a/ts/components/SearchResults.tsx
+++ b/ts/components/SearchResults.tsx
@@ -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
{
}
public render() {
- const { items, i18n, noResults, searchTerm } = this.props;
+ const {
+ i18n,
+ items,
+ noResults,
+ searchConversationName,
+ searchTerm,
+ } = this.props;
if (noResults) {
return (
-
- {i18n('noSearchResults', [searchTerm])}
-
+ {!searchConversationName || searchTerm ? (
+
+ {searchConversationName ? (
+ ,
+ ]}
+ />
+ ) : (
+ i18n('noSearchResults', [searchTerm])
+ )}
+
+ ) : null}
);
}
diff --git a/ts/components/conversation/ConversationHeader.md b/ts/components/conversation/ConversationHeader.md
index 7f3b4b24f2..fcb6fba4e7 100644
--- a/ts/components/conversation/ConversationHeader.md
+++ b/ts/components/conversation/ConversationHeader.md
@@ -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')}
/>
```
diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx
index 68d8202dbe..e443808d5e 100644
--- a/ts/components/conversation/ConversationHeader.tsx
+++ b/ts/components/conversation/ConversationHeader.tsx
@@ -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 {
}
public renderExpirationLength() {
- const { expirationSettingName } = this.props;
+ const { expirationSettingName, showBackButton } = this.props;
if (!expirationSettingName) {
return null;
}
return (
-
+
{expirationSettingName}
@@ -168,7 +176,7 @@ export class ConversationHeader extends React.Component
{
);
}
- public renderGear(triggerId: string) {
+ public renderMoreButton(triggerId: string) {
const { showBackButton } = this.props;
return (
@@ -176,10 +184,10 @@ export class ConversationHeader extends React.Component {
@@ -187,6 +195,23 @@ export class ConversationHeader extends React.Component {
);
}
+ public renderSearchButton() {
+ const { onSearchInConversation, showBackButton } = this.props;
+
+ return (
+
+ );
+ }
+
public renderMenu(triggerId: string) {
const {
i18n,
@@ -260,7 +285,8 @@ export class ConversationHeader extends React.Component {
{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"