diff --git a/app/components/markdown/__snapshots__/hashtag.test.js.snap b/app/components/markdown/__snapshots__/hashtag.test.js.snap
new file mode 100644
index 000000000..30476ed77
--- /dev/null
+++ b/app/components/markdown/__snapshots__/hashtag.test.js.snap
@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Hashtag should match snapshot 1`] = `
+ShallowWrapper {
+ "length": 1,
+ Symbol(enzyme.__root__): [Circular],
+ Symbol(enzyme.__unrendered__): ,
+ Symbol(enzyme.__renderer__): Object {
+ "batchedUpdates": [Function],
+ "getNode": [Function],
+ "render": [Function],
+ "simulateEvent": [Function],
+ "unmount": [Function],
+ },
+ Symbol(enzyme.__node__): Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "accessible": true,
+ "allowFontScaling": true,
+ "children": "#test",
+ "ellipsizeMode": "tail",
+ "onPress": [Function],
+ "style": Object {
+ "color": "red",
+ },
+ },
+ "ref": null,
+ "rendered": "#test",
+ "type": [Function],
+ },
+ Symbol(enzyme.__nodes__): Array [
+ Object {
+ "instance": null,
+ "key": undefined,
+ "nodeType": "class",
+ "props": Object {
+ "accessible": true,
+ "allowFontScaling": true,
+ "children": "#test",
+ "ellipsizeMode": "tail",
+ "onPress": [Function],
+ "style": Object {
+ "color": "red",
+ },
+ },
+ "ref": null,
+ "rendered": "#test",
+ "type": [Function],
+ },
+ ],
+ Symbol(enzyme.__options__): Object {
+ "adapter": ReactSixteenAdapter {
+ "options": Object {
+ "enableComponentDidUpdateOnSetState": true,
+ },
+ },
+ },
+}
+`;
diff --git a/app/components/markdown/hashtag.js b/app/components/markdown/hashtag.js
new file mode 100644
index 000000000..424f58420
--- /dev/null
+++ b/app/components/markdown/hashtag.js
@@ -0,0 +1,60 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import {intlShape} from 'react-intl';
+import {Text} from 'react-native';
+
+import CustomPropTypes from 'app/constants/custom_prop_types';
+
+export default class Hashtag extends React.PureComponent {
+ static propTypes = {
+ hashtag: PropTypes.string.isRequired,
+ linkStyle: CustomPropTypes.Style.isRequired,
+ onHashtagPress: PropTypes.func,
+ navigator: PropTypes.object.isRequired,
+ theme: PropTypes.object.isRequired,
+ };
+
+ static contextTypes = {
+ intl: intlShape,
+ };
+
+ handlePress = () => {
+ if (this.props.onHashtagPress) {
+ this.props.onHashtagPress(this.props.hashtag);
+ return;
+ }
+
+ const options = {
+ screen: 'Search',
+ animated: true,
+ backButtonTitle: '',
+ passProps: {
+ initialValue: '#' + this.props.hashtag,
+ },
+ navigatorStyle: {
+ navBarHidden: true,
+ screenBackgroundColor: this.props.theme.centerChannelBg,
+ },
+ };
+
+ // Close thread view, permalink view, etc
+ this.props.navigator.dismissAllModals();
+ this.props.navigator.popToRoot();
+
+ this.props.navigator.showModal(options);
+ };
+
+ render() {
+ return (
+
+ {`#${this.props.hashtag}`}
+
+ );
+ }
+}
diff --git a/app/components/markdown/hashtag.test.js b/app/components/markdown/hashtag.test.js
new file mode 100644
index 000000000..4d3128891
--- /dev/null
+++ b/app/components/markdown/hashtag.test.js
@@ -0,0 +1,69 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {shallow} from 'enzyme';
+import React from 'react';
+import {Text} from 'react-native';
+
+import Hashtag from './hashtag';
+
+describe('Hashtag', () => {
+ const baseProps = {
+ hashtag: 'test',
+ linkStyle: {color: 'red'},
+ navigator: {},
+ theme: {},
+ };
+
+ test('should match snapshot', () => {
+ const wrapper = shallow();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should open hashtag search on click', () => {
+ const props = {
+ ...baseProps,
+ navigator: {
+ dismissAllModals: jest.fn(),
+ popToRoot: jest.fn(),
+ showModal: jest.fn(),
+ },
+ };
+
+ const wrapper = shallow();
+
+ wrapper.find(Text).simulate('press');
+
+ expect(props.navigator.dismissAllModals).toHaveBeenCalled();
+ expect(props.navigator.popToRoot).toHaveBeenCalled();
+ expect(props.navigator.showModal).toHaveBeenCalledWith(expect.objectContaining({
+ screen: 'Search',
+ passProps: {
+ initialValue: '#test',
+ },
+ }));
+ });
+
+ test('should call onHashtagPress if provided', () => {
+ const props = {
+ ...baseProps,
+ navigator: {
+ dismissAllModals: jest.fn(),
+ popToRoot: jest.fn(),
+ showModal: jest.fn(),
+ },
+ onHashtagPress: jest.fn(),
+ };
+
+ const wrapper = shallow();
+
+ wrapper.find(Text).simulate('press');
+
+ expect(props.navigator.dismissAllModals).not.toBeCalled();
+ expect(props.navigator.popToRoot).not.toBeCalled();
+ expect(props.navigator.showModal).not.toBeCalled();
+
+ expect(props.onHashtagPress).toBeCalled();
+ });
+});
diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js
index c638a79e6..93d7dab43 100644
--- a/app/components/markdown/markdown.js
+++ b/app/components/markdown/markdown.js
@@ -15,6 +15,7 @@ import AtMention from 'app/components/at_mention';
import ChannelLink from 'app/components/channel_link';
import Emoji from 'app/components/emoji';
import FormattedText from 'app/components/formatted_text';
+import Hashtag from 'app/components/hashtag';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {blendColors, concatStyles, makeStyleSheetFromTheme} from 'app/utils/theme';
import {getScheme} from 'app/utils/url';
@@ -41,6 +42,7 @@ export default class Markdown extends PureComponent {
isSearchResult: PropTypes.bool,
navigator: PropTypes.object.isRequired,
onChannelLinkPress: PropTypes.func,
+ onHashtagPress: PropTypes.func,
onLongPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
onPostPress: PropTypes.func,
@@ -88,6 +90,7 @@ export default class Markdown extends PureComponent {
atMention: this.renderAtMention,
channelLink: this.renderChannelLink,
emoji: this.renderEmoji,
+ hashtag: this.renderHashtag,
paragraph: this.renderParagraph,
heading: this.renderHeading,
@@ -213,6 +216,18 @@ export default class Markdown extends PureComponent {
);
}
+ renderHashtag = ({hashtag}) => {
+ return (
+
+ );
+ }
+
renderParagraph = ({children, first}) => {
if (!children || children.length === 0) {
return null;
diff --git a/app/components/message_attachments/index.js b/app/components/message_attachments/index.js
index 20cbf4eb4..857422ee4 100644
--- a/app/components/message_attachments/index.js
+++ b/app/components/message_attachments/index.js
@@ -16,6 +16,7 @@ export default class MessageAttachments extends PureComponent {
blockStyles: PropTypes.object,
postId: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired,
+ onHashtagPress: PropTypes.func,
onLongPress: PropTypes.func.isRequired,
onPermalinkPress: PropTypes.func,
theme: PropTypes.object,
@@ -28,6 +29,7 @@ export default class MessageAttachments extends PureComponent {
baseTextStyle,
blockStyles,
navigator,
+ onHashtagPress,
onLongPress,
onPermalinkPress,
postId,
@@ -44,6 +46,7 @@ export default class MessageAttachments extends PureComponent {
blockStyles={blockStyles}
key={'att_' + i}
navigator={navigator}
+ onHashtagPress={onHashtagPress}
onLongPress={onLongPress}
onPermalinkPress={onPermalinkPress}
postId={postId}
diff --git a/app/components/post/post.js b/app/components/post/post.js
index 185f911ab..b51f0c15f 100644
--- a/app/components/post/post.js
+++ b/app/components/post/post.js
@@ -58,6 +58,7 @@ export default class Post extends PureComponent {
canEdit: PropTypes.bool.isRequired,
canEditUntil: PropTypes.number.isRequired,
canDelete: PropTypes.bool.isRequired,
+ onHashtagPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
shouldRenderReplyButton: PropTypes.bool,
showAddReaction: PropTypes.bool,
@@ -349,6 +350,7 @@ export default class Post extends PureComponent {
highlight,
isLastReply,
isSearchResult,
+ onHashtagPress,
onPermalinkPress,
post,
renderReplies,
@@ -448,6 +450,7 @@ export default class Post extends PureComponent {
onCopyPermalink={this.handleCopyPermalink}
onCopyText={this.handleCopyText}
onFailedPostPress={this.handleFailedPostPress}
+ onHashtagPress={onHashtagPress}
onPermalinkPress={onPermalinkPress}
onPostDelete={this.handlePostDelete}
onPostEdit={this.handlePostEdit}
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index 59a9effbe..f309681d6 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -62,6 +62,7 @@ export default class PostBody extends PureComponent {
onCopyPermalink: PropTypes.func,
onCopyText: PropTypes.func,
onFailedPostPress: PropTypes.func,
+ onHashtagPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
onPostDelete: PropTypes.func,
onPostEdit: PropTypes.func,
@@ -201,7 +202,15 @@ export default class PostBody extends PureComponent {
};
openLongPost = preventDoubleTap(() => {
- const {managedConfig, navigator, onAddReaction, onPermalinkPress, postId} = this.props;
+ const {
+ managedConfig,
+ navigator,
+ onAddReaction,
+ onHashtagPress,
+ onPermalinkPress,
+ postId,
+ } = this.props;
+
const options = {
screen: 'LongPost',
animationType: 'none',
@@ -216,6 +225,7 @@ export default class PostBody extends PureComponent {
postId,
managedConfig,
onAddReaction,
+ onHashtagPress,
onPermalinkPress,
},
};
@@ -235,7 +245,7 @@ export default class PostBody extends PureComponent {
};
renderAddChannelMember = (style, messageStyle, textStyles) => {
- const {onPermalinkPress, onPress, postProps} = this.props;
+ const {onPress, postProps} = this.props;
if (!PostAddChannelMember) {
PostAddChannelMember = require('app/components/post_add_channel_member').default;
@@ -248,7 +258,6 @@ export default class PostBody extends PureComponent {
baseTextStyle={messageStyle}
navigator={navigator}
onLongPress={this.showOptionsContext}
- onPermalinkPress={onPermalinkPress}
onPostPress={onPress}
textStyles={textStyles}
postId={postProps.add_channel_member.post_id}
@@ -296,7 +305,7 @@ export default class PostBody extends PureComponent {
}
renderPostAdditionalContent = (blockStyles, messageStyle, textStyles) => {
- const {isReplyPost, message, navigator, onPermalinkPress, postId, postProps} = this.props;
+ const {isReplyPost, message, navigator, onHashtagPress, onPermalinkPress, postId, postProps} = this.props;
if (!PostBodyAdditionalContent) {
PostBodyAdditionalContent = require('app/components/post_body_additional_content').default;
@@ -311,8 +320,9 @@ export default class PostBody extends PureComponent {
postId={postId}
postProps={postProps}
textStyles={textStyles}
- onLongPress={this.showOptionsContext}
isReplyPost={isReplyPost}
+ onHashtagPress={onHashtagPress}
+ onLongPress={this.showOptionsContext}
onPermalinkPress={onPermalinkPress}
/>
);
@@ -352,6 +362,7 @@ export default class PostBody extends PureComponent {
message,
navigator,
onFailedPostPress,
+ onHashtagPress,
onPermalinkPress,
onPress,
postProps,
@@ -422,6 +433,7 @@ export default class PostBody extends PureComponent {
isReplyPost={isReplyPost}
isSearchResult={isSearchResult}
navigator={navigator}
+ onHashtagPress={onHashtagPress}
onLongPress={this.showOptionsContext}
onPermalinkPress={onPermalinkPress}
onPostPress={onPress}
diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js
index 0042265d8..55aa1afe6 100644
--- a/app/components/post_body_additional_content/post_body_additional_content.js
+++ b/app/components/post_body_additional_content/post_body_additional_content.js
@@ -44,6 +44,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
link: PropTypes.string,
message: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired,
+ onHashtagPress: PropTypes.func,
onLongPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
openGraphData: PropTypes.object,
@@ -291,6 +292,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
baseTextStyle,
blockStyles,
navigator,
+ onHashtagPress,
onPermalinkPress,
textStyles,
theme,
@@ -311,6 +313,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
postId={postId}
textStyles={textStyles}
theme={theme}
+ onHashtagPress={onHashtagPress}
onLongPress={this.props.onLongPress}
onPermalinkPress={onPermalinkPress}
/>
diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js
index c933abc5b..8803d4869 100644
--- a/app/components/post_list/post_list.js
+++ b/app/components/post_list/post_list.js
@@ -51,6 +51,7 @@ export default class PostList extends PureComponent {
navigator: PropTypes.object,
onContentSizeChange: PropTypes.func,
onEndReached: PropTypes.func,
+ onHashtagPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
onPostPress: PropTypes.func,
onRefresh: PropTypes.func,
@@ -146,7 +147,11 @@ export default class PostList extends PureComponent {
};
showPermalinkView = (postId) => {
- const {actions, navigator} = this.props;
+ const {
+ actions,
+ navigator,
+ onHashtagPress,
+ } = this.props;
actions.selectFocusedPostId(postId);
@@ -164,6 +169,7 @@ export default class PostList extends PureComponent {
passProps: {
isPermalink: true,
onClose: this.handleClosePermalink,
+ onHashtagPress,
onPermalinkPress: this.handlePermalinkPress,
},
};
@@ -314,6 +320,7 @@ export default class PostList extends PureComponent {
highlightPostId,
isSearchResult,
navigator,
+ onHashtagPress,
onPostPress,
renderReplies,
shouldRenderReplyButton,
@@ -335,6 +342,7 @@ export default class PostList extends PureComponent {
renderReplies={renderReplies}
isSearchResult={isSearchResult}
shouldRenderReplyButton={shouldRenderReplyButton}
+ onHashtagPress={onHashtagPress}
onPermalinkPress={this.handlePermalinkPress}
onPress={onPostPress}
navigator={navigator}
diff --git a/app/screens/long_post/long_post.js b/app/screens/long_post/long_post.js
index 973b25fbb..d90d477d2 100644
--- a/app/screens/long_post/long_post.js
+++ b/app/screens/long_post/long_post.js
@@ -52,6 +52,7 @@ export default class LongPost extends PureComponent {
managedConfig: PropTypes.object,
navigator: PropTypes.object,
onAddReaction: PropTypes.func,
+ onHashtagPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
postId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
@@ -178,6 +179,7 @@ export default class LongPost extends PureComponent {
hasReactions,
managedConfig,
navigator,
+ onHashtagPress,
onPermalinkPress,
postId,
theme,
@@ -239,6 +241,7 @@ export default class LongPost extends PureComponent {
onPress={this.handlePress}
isSearchResult={false}
showLongPost={true}
+ onHashtagPress={onHashtagPress}
onPermalinkPress={onPermalinkPress}
navigator={navigator}
managedConfig={managedConfig}
diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js
index c49e95c74..8badb1957 100644
--- a/app/screens/permalink/permalink.js
+++ b/app/screens/permalink/permalink.js
@@ -70,6 +70,7 @@ export default class Permalink extends PureComponent {
myMembers: PropTypes.object.isRequired,
navigator: PropTypes.object,
onClose: PropTypes.func,
+ onHashtagPress: PropTypes.func,
onPermalinkPress: PropTypes.func,
onPress: PropTypes.func,
postIds: PropTypes.array,
@@ -319,6 +320,7 @@ export default class Permalink extends PureComponent {
currentUserId,
focusedPostId,
navigator,
+ onHashtagPress,
onPermalinkPress,
postIds,
theme,
@@ -352,6 +354,7 @@ export default class Permalink extends PureComponent {
isSearchResult={false}
shouldRenderReplyButton={false}
renderReplies={true}
+ onHashtagPress={onHashtagPress}
onPermalinkPress={onPermalinkPress}
onPostPress={this.goToThread}
postIds={postIds}
diff --git a/app/screens/search/search.js b/app/screens/search/search.js
index 7d2534248..8a5ecabc6 100644
--- a/app/screens/search/search.js
+++ b/app/screens/search/search.js
@@ -57,6 +57,7 @@ export default class Search extends PureComponent {
selectPost: PropTypes.func.isRequired,
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
+ initialValue: PropTypes.string,
isLandscape: PropTypes.bool.isRequired,
navigator: PropTypes.object,
postIds: PropTypes.array,
@@ -69,6 +70,7 @@ export default class Search extends PureComponent {
};
static defaultProps = {
+ initialValue: '',
postIds: [],
recent: [],
archivedPostIds: [],
@@ -86,7 +88,7 @@ export default class Search extends PureComponent {
this.state = {
channelName: '',
cursorPosition: 0,
- value: '',
+ value: props.initialValue,
managedConfig: {},
};
}
@@ -97,9 +99,14 @@ export default class Search extends PureComponent {
componentDidMount() {
this.setManagedConfig();
- if (this.refs.searchBar) {
+
+ if (this.props.initialValue) {
+ this.search(this.props.initialValue);
+ } else {
setTimeout(() => {
- this.refs.searchBar.focus();
+ if (this.refs.searchBar) {
+ this.refs.searchBar.focus();
+ }
}, 150);
}
}
@@ -117,10 +124,12 @@ export default class Search extends PureComponent {
if (shouldScroll) {
setTimeout(() => {
const modifiersCount = enableDateSuggestion ? 5 : 2;
- this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
- animated: true,
- offset: SECTION_HEIGHT + (modifiersCount * MODIFIER_LABEL_HEIGHT) + (recentLength * RECENT_LABEL_HEIGHT) + ((recentLength + 1) * RECENT_SEPARATOR_HEIGHT),
- });
+ if (this.refs.list) {
+ this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
+ animated: true,
+ offset: SECTION_HEIGHT + (modifiersCount * MODIFIER_LABEL_HEIGHT) + (recentLength * RECENT_LABEL_HEIGHT) + ((recentLength + 1) * RECENT_SEPARATOR_HEIGHT),
+ });
+ }
}, 100);
}
}
@@ -163,6 +172,20 @@ export default class Search extends PureComponent {
navigator.push(options);
};
+ handleHashtagPress = (hashtag) => {
+ if (this.showingPermalink) {
+ this.props.navigator.dismissModal();
+ this.handleClosePermalink();
+ }
+
+ const terms = '#' + hashtag;
+
+ this.handleTextChanged(terms);
+ this.search(terms, false);
+
+ Keyboard.dismiss();
+ };
+
handleClosePermalink = () => {
const {actions} = this.props;
actions.selectFocusedPostId('');
@@ -251,6 +274,7 @@ export default class Search extends PureComponent {
passProps: {
isPermalink,
onClose: this.handleClosePermalink,
+ onHashtagPress: this.handleHashtagPress,
onPermalinkPress: this.handlePermalinkPress,
},
};
@@ -360,6 +384,7 @@ export default class Search extends PureComponent {
previewPost={this.previewPost}
goToThread={this.goToThread}
navigator={this.props.navigator}
+ onHashtagPress={this.handleHashtagPress}
onPermalinkPress={this.handlePermalinkPress}
managedConfig={managedConfig}
/>
diff --git a/app/screens/search/search_result_post/search_result_post.js b/app/screens/search/search_result_post/search_result_post.js
index c1a953855..be62a3132 100644
--- a/app/screens/search/search_result_post/search_result_post.js
+++ b/app/screens/search/search_result_post/search_result_post.js
@@ -12,6 +12,7 @@ export default class SearchResultPost extends PureComponent {
goToThread: PropTypes.func.isRequired,
managedConfig: PropTypes.object.isRequired,
navigator: PropTypes.object.isRequired,
+ onHashtagPress: PropTypes.func.isRequired,
onPermalinkPress: PropTypes.func.isRequired,
postId: PropTypes.string.isRequired,
previewPost: PropTypes.func.isRequired,
@@ -32,6 +33,7 @@ export default class SearchResultPost extends PureComponent {
postComponentProps.onReply = this.props.goToThread;
postComponentProps.shouldRenderReplyButton = true;
postComponentProps.managedConfig = this.props.managedConfig;
+ postComponentProps.onHashtagPress = this.props.onHashtagPress;
postComponentProps.onPermalinkPress = this.props.onPermalinkPress;
}
diff --git a/package-lock.json b/package-lock.json
index 3da995544..537ba26eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5325,8 +5325,8 @@
}
},
"commonmark-react-renderer": {
- "version": "github:mattermost/commonmark-react-renderer#86fa63f898802953842526c2030f3b63c5d1ae7a",
- "from": "commonmark-react-renderer@github:mattermost/commonmark-react-renderer#86fa63f898802953842526c2030f3b63c5d1ae7a",
+ "version": "github:mattermost/commonmark-react-renderer#b560513b93357ee5fd33489fe311bc65d4658870",
+ "from": "github:mattermost/commonmark-react-renderer#b560513b93357ee5fd33489fe311bc65d4658870",
"requires": {
"in-publish": "^2.0.0",
"lodash.assign": "^4.2.0",