Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stream action buttons: Implement search messages in stream #5248

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/action-sheets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
navigateToEmojiPicker,
navigateToStream,
fetchSomeMessageIdForConversation,
navigateToSearch,
} from '../actions';
import {
navigateToMessageReactionScreen,
Expand Down Expand Up @@ -331,6 +332,14 @@ const unsubscribe = {
},
};

const searchMessage = {
title: 'Search in stream',
errorMessage: 'Failed to open search',
action: ({ streamId, streams }) => {
NavigationService.dispatch(navigateToSearch(streamId, streams.get(streamId)?.name));
},
};

const pinToTop = {
title: 'Pin to top',
errorMessage: 'Failed to pin to top',
Expand Down Expand Up @@ -471,6 +480,7 @@ export const constructStreamActionButtons = (args: {|
buttons.push(subscribe);
}
buttons.push(showStreamSettings);
buttons.push(searchMessage);
buttons.push(cancel);
return buttons;
};
Expand Down
4 changes: 3 additions & 1 deletion src/common/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type Props = $ReadOnly<{|
...React$ElementConfig<typeof TextInput>,
placeholder: LocalizableText,
onChangeText?: (text: string) => void,
prefixText?: string,

// We should replace the fixme with
// `React$ElementRef<typeof TextInput>` when we can. Currently, that
Expand Down Expand Up @@ -47,7 +48,7 @@ const componentStyles = createStyleSheet({
* See upstream: https://reactnative.dev/docs/textinput
*/
export default function Input(props: Props): Node {
const { style, placeholder, textInputRef, ...restProps } = props;
const { style, placeholder, textInputRef, prefixText, ...restProps } = props;

const [isFocused, setIsFocused] = useState<boolean>(false);

Expand Down Expand Up @@ -76,6 +77,7 @@ export default function Input(props: Props): Node {
onFocus={handleFocus}
onBlur={handleBlur}
ref={textInputRef}
value={prefixText}
{...restProps}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions src/common/InputWithClearButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ type Props = $ReadOnly<$Diff<InputProps, {| textInputRef: mixed, value: mixed, _
* All props are passed through to `Input`. See `Input` for descriptions.
*/
export default function InputWithClearButton(props: Props): Node {
const { onChangeText } = props;
const { onChangeText, prefixText } = props;

const [text, setText] = useState<string>('');
const [text, setText] = useState<string>(prefixText ?? '');

// We should replace the fixme with
// `React$ElementRef<typeof TextInput>` when we can. Currently, that
Expand Down
3 changes: 3 additions & 0 deletions src/common/Screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Props = $ReadOnly<{|
searchBarOnSubmit?: (e: EditingEvent) => void,
shouldShowLoadingBanner?: boolean,
searchPlaceholder?: LocalizableText,
searchPrefixText?: string,

canGoBack?: boolean,
title?: LocalizableReactText,
Expand Down Expand Up @@ -88,6 +89,7 @@ export default function Screen(props: Props): Node {
scrollEnabled = true,
search = false,
searchPlaceholder,
searchPrefixText,
searchBarOnChange = (text: string) => {},
style,
title = '',
Expand All @@ -108,6 +110,7 @@ export default function Screen(props: Props): Node {
searchBarOnChange={searchBarOnChange}
searchBarOnSubmit={searchBarOnSubmit}
placeholder={searchPlaceholder}
prefixText={searchPrefixText}
/>
) : (
<ModalNavBar canGoBack={canGoBack} title={title} />
Expand Down
10 changes: 9 additions & 1 deletion src/common/SearchInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Props = $ReadOnly<{|
onChangeText: (text: string) => void,
onSubmitEditing: (e: EditingEvent) => mixed,
placeholder?: LocalizableText,
prefixText?: string,
|}>;

/**
Expand All @@ -35,7 +36,13 @@ type Props = $ReadOnly<{|
* @prop onChangeText - Event called when search query is edited.
*/
export default function SearchInput(props: Props): Node {
const { autoFocus = true, onChangeText, onSubmitEditing, placeholder = 'Search' } = props;
const {
autoFocus = true,
onChangeText,
onSubmitEditing,
placeholder = 'Search',
prefixText,
} = props;

return (
<View style={styles.wrapper}>
Expand All @@ -51,6 +58,7 @@ export default function SearchInput(props: Props): Node {
onChangeText={onChangeText}
autoFocus={autoFocus}
onSubmitEditing={onSubmitEditing}
prefixText={prefixText}
/>
</View>
);
Expand Down
11 changes: 10 additions & 1 deletion src/nav/ModalSearchNavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Props = $ReadOnly<{|
searchBarOnChange: (text: string) => void,
searchBarOnSubmit: (e: EditingEvent) => void,
placeholder?: LocalizableText,
prefixText?: string,
canGoBack?: boolean,
|}>;

Expand All @@ -23,7 +24,14 @@ export default function ModalSearchNavBar(props: Props): Node {
//
// For details, see comment at ModalNavBar.

const { autoFocus, searchBarOnChange, canGoBack = true, searchBarOnSubmit, placeholder } = props;
const {
autoFocus,
searchBarOnChange,
canGoBack = true,
searchBarOnSubmit,
placeholder,
prefixText,
} = props;
const { backgroundColor } = useContext(ThemeContext);
return (
<SafeAreaView
Expand All @@ -50,6 +58,7 @@ export default function ModalSearchNavBar(props: Props): Node {
onChangeText={searchBarOnChange}
onSubmitEditing={searchBarOnSubmit}
placeholder={placeholder}
prefixText={prefixText}
/>
</SafeAreaView>
);
Expand Down
3 changes: 2 additions & 1 deletion src/nav/navActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export const replaceWithChat = (narrow: Narrow): GenericNavigationAction =>

export const navigateToUsersScreen = (): GenericNavigationAction => StackActions.push('users');

export const navigateToSearch = (): GenericNavigationAction => StackActions.push('search-messages');
export const navigateToSearch = (streamId?: number, streamName?: string): GenericNavigationAction =>
StackActions.push('search-messages', { streamId, streamName });

export const navigateToEmojiPicker = (
onPressEmoji: ({| +type: EmojiType, +code: string, +name: string |}) => void,
Expand Down
35 changes: 29 additions & 6 deletions src/search/SearchMessagesScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { fetchMessages } from '../message/fetchActions';
type OuterProps = $ReadOnly<{|
// These should be passed from React Navigation
navigation: AppNavigationProp<'search-messages'>,
route: RouteProp<'search-messages', void>,
route: RouteProp<'search-messages', {| streamId?: number, streamName?: string |}>,
|}>;

type SelectorProps = $ReadOnly<{|
Expand Down Expand Up @@ -63,9 +63,14 @@ class SearchMessagesScreenInner extends PureComponent<Props, State> {
* Stores the fetched messages in the Redux store. Does not read any
* of the component's data except `props.dispatch`.
*/
fetchSearchMessages = async (query: string): Promise<$ReadOnlyArray<Message>> => {
fetchSearchMessages = async (
query: string,
inStream: boolean,
): Promise<$ReadOnlyArray<Message>> => {
const fetchArgs = {
narrow: SEARCH_NARROW(query),
narrow: inStream
? SEARCH_NARROW(query, this.props.route.params.streamId)
: SEARCH_NARROW(query),
anchor: LAST_MESSAGE_ANCHOR,
numBefore: 20,
numAfter: 0,
Expand All @@ -90,7 +95,17 @@ class SearchMessagesScreenInner extends PureComponent<Props, State> {
// invalidate outstanding requests on change will require more work.

handleQuerySubmit = async (e: EditingEvent) => {
const query = e.nativeEvent.text;
const searchString = e.nativeEvent.text;
let query;
if (
this.props.route.params.streamName !== undefined
&& searchString.substring(0, 7) === 'stream:'
) {
const prefixLength = `stream:${this.props.route.params.streamName.replace(' ', '+')}`.length;
query = searchString.substring(prefixLength + 1);
} else {
query = searchString;
}
const id = ++this.lastIdSent;

if (query === '') {
Expand All @@ -103,7 +118,10 @@ class SearchMessagesScreenInner extends PureComponent<Props, State> {

this.setState({ isFetching: true });
try {
const messages = await this.fetchSearchMessages(query);
const messages = await this.fetchSearchMessages(
query,
searchString.substring(0, 7) === 'stream:',
);

// Update `state.messages` if this is our new latest result.
if (id > this.lastIdSuccess) {
Expand Down Expand Up @@ -133,18 +151,23 @@ class SearchMessagesScreenInner extends PureComponent<Props, State> {

render() {
const { messages, isFetching } = this.state;
const searchPrefix =
this.props.route.params.streamId != null
? `stream:${this.props.route.params.streamName?.replace(' ', '+') ?? ''} `
: '';

return (
<Screen
search
autoFocus
searchBarOnSubmit={this.handleQuerySubmitWrapper}
style={styles.flexed}
searchPrefixText={searchPrefix}
>
<SearchMessagesCard
messages={messages}
isFetching={isFetching}
narrow={SEARCH_NARROW(this.state.query)}
narrow={SEARCH_NARROW(this.state.query, this.props.route.params.streamId)}
/>
</Screen>
);
Expand Down
20 changes: 15 additions & 5 deletions src/utils/narrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export opaque type Narrow =
| {| type: 'stream', streamId: number |}
| {| type: 'topic', streamId: number, topic: string |}
| {| type: 'pm', userIds: PmKeyRecipients |}
| {| type: 'search', query: string |}
// The search narrow can behave in two ways,
// one where there is no filters, so the user can search a message in all the streams
// the second where the user wants to search in a particular stream
| {| type: 'search', query: string, streamId?: number |}
| {| type: 'all' | 'starred' | 'mentioned' | 'all-pm' |};

export const HOME_NARROW: Narrow = Object.freeze({ type: 'all' });
Expand Down Expand Up @@ -153,7 +156,8 @@ export const streamNarrow = (streamId: number): Narrow =>
export const topicNarrow = (streamId: number, topic: string): Narrow =>
Object.freeze({ type: 'topic', streamId, topic });

export const SEARCH_NARROW = (query: string): Narrow => Object.freeze({ type: 'search', query });
export const SEARCH_NARROW = (query: string, streamId?: number): Narrow =>
Object.freeze({ type: 'search', query, streamId });

type NarrowCases<T> = {|
home: () => T,
Expand All @@ -163,7 +167,7 @@ type NarrowCases<T> = {|
allPrivate: () => T,
stream: (streamId: number) => T,
topic: (streamId: number, topic: string) => T,
search: (query: string) => T,
search: (query: string, streamId?: number) => T,
|};

/* prettier-ignore */
Expand All @@ -176,7 +180,7 @@ export function caseNarrow<T>(narrow: Narrow, cases: NarrowCases<T>): T {
case 'stream': return cases.stream(narrow.streamId);
case 'topic': return cases.topic(narrow.streamId, narrow.topic);
case 'pm': return cases.pm(narrow.userIds);
case 'search': return cases.search(narrow.query);
case 'search': return cases.search(narrow.query, narrow.streamId);
case 'all': return cases.home();
case 'starred': return cases.starred();
case 'mentioned': return cases.mentioned();
Expand Down Expand Up @@ -437,7 +441,13 @@ export const apiNarrowOfNarrow = (
// TODO(server-2.1): just send IDs instead
return [{ operator: 'pm-with', operand: emails.join(',') }];
},
search: query => [{ operator: 'search', operand: query }],
search: (query, streamId) =>
streamId !== undefined
? [
{ operator: 'search', operand: query },
{ operator: 'stream', operand: get('stream', streamsById, streamId).name },
]
: [{ operator: 'search', operand: query }],
home: () => [],
starred: () => [{ operator: 'is', operand: 'starred' }],
mentioned: () => [{ operator: 'is', operand: 'mentioned' }],
Expand Down
2 changes: 2 additions & 0 deletions static/translations/messages_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@
"Failed to delete topic": "Failed to delete topic",
"Stream settings": "Stream settings",
"Failed to show stream settings": "Failed to show stream settings",
"Search in stream": "Search in stream",
"Failed to open search": "Failed to open search",
"show": "show",
"hide": "hide",
"Debug": "Debug",
Expand Down