diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
index 043f9efea..576e80c70 100644
--- a/app/actions/views/channel.js
+++ b/app/actions/views/channel.js
@@ -19,6 +19,7 @@ import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
+import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {
getChannelByName,
@@ -40,6 +41,18 @@ export function loadChannelsIfNecessary(teamId) {
};
}
+export function loadChannelsByTeamName(teamName) {
+ return async (dispatch, getState) => {
+ const state = getState();
+ const {currentTeamId} = state.entities.teams;
+ const team = getTeamByName(state, teamName);
+
+ if (team && team.id !== currentTeamId) {
+ await dispatch(fetchMyChannelsAndMembers(team.id));
+ }
+ };
+}
+
export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
return async (dispatch, getState) => {
const state = getState();
diff --git a/app/components/at_mention/at_mention.js b/app/components/at_mention/at_mention.js
index 2bccf5f6c..5665e05df 100644
--- a/app/components/at_mention/at_mention.js
+++ b/app/components/at_mention/at_mention.js
@@ -3,7 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {Clipboard, Text} from 'react-native';
+import {Clipboard, Platform, Text} from 'react-native';
import {intlShape} from 'react-intl';
import CustomPropTypes from 'app/constants/custom_prop_types';
@@ -49,8 +49,7 @@ export default class AtMention extends React.PureComponent {
goToUserProfile = () => {
const {navigator, theme} = this.props;
const {intl} = this.context;
-
- navigator.push({
+ const options = {
screen: 'UserProfile',
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
animated: true,
@@ -64,7 +63,13 @@ export default class AtMention extends React.PureComponent {
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
- });
+ };
+
+ if (Platform.OS === 'ios') {
+ navigator.push(options);
+ } else {
+ navigator.showModal(options);
+ }
};
getUserDetailsFromMentionName(props) {
diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js
index a0eb1ee53..9b4ffad74 100644
--- a/app/components/channel_intro/channel_intro.js
+++ b/app/components/channel_intro/channel_intro.js
@@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
+ Platform,
Text,
TouchableOpacity,
View,
@@ -30,8 +31,7 @@ class ChannelIntro extends PureComponent {
goToUserProfile = (userId) => {
const {intl, navigator, theme} = this.props;
-
- navigator.push({
+ const options = {
screen: 'UserProfile',
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
animated: true,
@@ -45,7 +45,13 @@ class ChannelIntro extends PureComponent {
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
- });
+ };
+
+ if (Platform.OS === 'ios') {
+ navigator.push(options);
+ } else {
+ navigator.showModal(options);
+ }
};
getDisplayName = (member) => {
diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js
index c413debce..4789d98f4 100644
--- a/app/components/markdown/markdown.js
+++ b/app/components/markdown/markdown.js
@@ -38,6 +38,7 @@ export default class Markdown extends PureComponent {
isSearchResult: PropTypes.bool,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func,
+ onPermalinkPress: PropTypes.func,
onPostPress: PropTypes.func,
textStyles: PropTypes.object,
theme: PropTypes.object.isRequired,
@@ -320,6 +321,7 @@ export default class Markdown extends PureComponent {
{children}
diff --git a/app/components/markdown/markdown_link/index.js b/app/components/markdown/markdown_link/index.js
new file mode 100644
index 000000000..411e09118
--- /dev/null
+++ b/app/components/markdown/markdown_link/index.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
+
+import MarkdownLink from './markdown_link';
+
+function mapStateToProps(state) {
+ return {
+ serverURL: getCurrentUrl(state),
+ siteURL: getConfig(state).SiteURL,
+ };
+}
+
+export default connect(mapStateToProps)(MarkdownLink);
diff --git a/app/components/markdown/markdown_link.js b/app/components/markdown/markdown_link/markdown_link.js
similarity index 73%
rename from app/components/markdown/markdown_link.js
rename to app/components/markdown/markdown_link/markdown_link.js
index 072b08ea2..1f027a92e 100644
--- a/app/components/markdown/markdown_link.js
+++ b/app/components/markdown/markdown_link/markdown_link.js
@@ -12,6 +12,8 @@ import mattermostManaged from 'app/mattermost_managed';
import Config from 'assets/config';
+import {escapeRegex} from 'app/utils/markdown';
+import {preventDoubleTap} from 'app/utils/tap';
import {normalizeProtocol} from 'app/utils/url';
export default class MarkdownLink extends PureComponent {
@@ -19,29 +21,45 @@ export default class MarkdownLink extends PureComponent {
children: CustomPropTypes.Children.isRequired,
href: PropTypes.string.isRequired,
onLongPress: PropTypes.func,
+ onPermalinkPress: PropTypes.func,
+ serverURL: PropTypes.string.isRequired,
+ siteURL: PropTypes.string.isRequired,
};
static defaultProps = {
onLongPress: () => true,
+ onPermalinkPress: () => true,
};
static contextTypes = {
intl: intlShape.isRequired,
};
- handlePress = () => {
- const url = normalizeProtocol(this.props.href);
+ handlePress = preventDoubleTap(() => {
+ const {href, onPermalinkPress, serverURL, siteURL} = this.props;
+ const url = normalizeProtocol(href);
if (!url) {
return;
}
- Linking.canOpenURL(url).then((supported) => {
- if (supported) {
- Linking.openURL(url);
- }
- });
- };
+ const pattern = new RegExp('^' + escapeRegex(serverURL) + '\\/([^\\/]+)\\/pl\\/(\\w+)');
+ const sitePattern = new RegExp('^' + escapeRegex(siteURL) + '\\/([^\\/]+)\\/pl\\/(\\w+)');
+ const match = url.match(pattern) || url.match(sitePattern);
+
+ if (!match) {
+ Linking.canOpenURL(url).then((supported) => {
+ if (supported) {
+ Linking.openURL(url);
+ }
+ });
+ return;
+ }
+
+ const teamName = match[1];
+ const postId = match[2];
+ onPermalinkPress(postId, teamName);
+ });
parseLinkLiteral = (literal) => {
let nextLiteral = literal;
@@ -54,7 +72,7 @@ export default class MarkdownLink extends PureComponent {
const parsed = urlParse(nextLiteral, {});
return parsed.href;
- }
+ };
parseChildren = () => {
return Children.map(this.props.children, (child) => {
diff --git a/app/components/post/post.js b/app/components/post/post.js
index 2d472b118..750e1a0b1 100644
--- a/app/components/post/post.js
+++ b/app/components/post/post.js
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import {
Alert,
Clipboard,
+ Platform,
View,
ViewPropTypes,
} from 'react-native';
@@ -55,6 +56,7 @@ class Post extends PureComponent {
license: PropTypes.object.isRequired,
managedConfig: PropTypes.object.isRequired,
navigator: PropTypes.object,
+ onPermalinkPress: PropTypes.func,
roles: PropTypes.string,
shouldRenderReplyButton: PropTypes.bool,
showFullDate: PropTypes.bool,
@@ -107,7 +109,7 @@ class Post extends PureComponent {
goToUserProfile = () => {
const {intl, navigator, post, theme} = this.props;
- navigator.push({
+ const options = {
screen: 'UserProfile',
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
animated: true,
@@ -121,7 +123,13 @@ class Post extends PureComponent {
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
- });
+ };
+
+ if (Platform.OS === 'ios') {
+ navigator.push(options);
+ } else {
+ navigator.showModal(options);
+ }
};
autofillUserMention = (username) => {
@@ -260,7 +268,6 @@ class Post extends PureComponent {
handlePress = preventDoubleTap(() => {
const {
- isSearchResult,
onPress,
post,
} = this.props;
@@ -268,7 +275,7 @@ class Post extends PureComponent {
if (!getToolTipVisible()) {
if (onPress && post.state !== Posts.POST_DELETED && !isSystemMessage(post) && !isPostPendingOrFailed(post)) {
onPress(post);
- } else if (!isSearchResult && isPostEphemeral(post)) {
+ } else if (isPostEphemeral(post) || post.state === Posts.POST_DELETED) {
this.onRemovePost(post);
}
}
@@ -320,9 +327,7 @@ class Post extends PureComponent {
};
viewUserProfile = preventDoubleTap(() => {
- const {isSearchResult} = this.props;
-
- if (!isSearchResult && !getToolTipVisible()) {
+ if (!getToolTipVisible()) {
this.goToUserProfile();
}
});
@@ -355,6 +360,7 @@ class Post extends PureComponent {
highlight,
isLastReply,
isSearchResult,
+ onPermalinkPress,
post,
renderReplies,
shouldRenderReplyButton,
@@ -412,6 +418,7 @@ class Post extends PureComponent {
onCopyPermalink={this.handleCopyPermalink}
onCopyText={this.handleCopyText}
onFailedPostPress={this.handleFailedPostPress}
+ onPermalinkPress={onPermalinkPress}
onPostDelete={this.handlePostDelete}
onPostEdit={this.handlePostEdit}
onPress={this.handlePress}
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index 646098a37..bd01e7d18 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -51,6 +51,7 @@ class PostBody extends PureComponent {
onCopyPermalink: PropTypes.func,
onCopyText: PropTypes.func,
onFailedPostPress: PropTypes.func,
+ onPermalinkPress: PropTypes.func,
onPostDelete: PropTypes.func,
onPostEdit: PropTypes.func,
onPress: PropTypes.func,
@@ -151,6 +152,7 @@ class PostBody extends PureComponent {
message,
navigator,
onFailedPostPress,
+ onPermalinkPress,
onPostDelete,
onPostEdit,
onPress,
@@ -169,7 +171,7 @@ class PostBody extends PureComponent {
const isPendingOrFailedPost = isPending || isFailed;
// we should check for the user roles and permissions
- if (!isPendingOrFailedPost && !isSearchResult && !isSystemMessage && !isPostEphemeral) {
+ if (!isPendingOrFailedPost && !isSystemMessage && !isPostEphemeral) {
actions.push({
text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
onPress: this.props.onAddReaction,
@@ -240,6 +242,7 @@ class PostBody extends PureComponent {
isSearchResult={isSearchResult}
navigator={navigator}
onLongPress={this.showOptionsContext}
+ onPermalinkPress={onPermalinkPress}
onPostPress={onPress}
textStyles={textStyles}
value={message}
@@ -250,61 +253,36 @@ class PostBody extends PureComponent {
}
if (!hasBeenDeleted) {
- if (isSearchResult) {
- body = (
-
-
- {messageComponent}
-
- {this.renderFileAttachments()}
-
-
- );
- } else {
- body = (
-
- {messageComponent}
-
- {this.renderFileAttachments()}
- {hasReactions &&
-
- }
-
- );
- }
+ body = (
+
+ {messageComponent}
+
+ {this.renderFileAttachments()}
+ {!isSearchResult && hasReactions &&
+
+ }
+
+ );
}
return (
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 cf6ce711a..85fea118a 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
@@ -37,6 +37,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
message: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func,
+ onPermalinkPress: PropTypes.func,
openGraphData: PropTypes.object,
postId: PropTypes.string.isRequired,
postProps: PropTypes.object.isRequired,
@@ -160,6 +161,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
baseTextStyle,
blockStyles,
navigator,
+ onPermalinkPress,
textStyles,
theme,
} = this.props;
@@ -176,6 +178,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
textStyles={textStyles}
theme={theme}
onLongPress={this.props.onLongPress}
+ onPermalinkPress={onPermalinkPress}
/>
);
}
diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js
index 28192a9fd..8163f9f05 100644
--- a/app/components/post_list/index.js
+++ b/app/components/post_list/index.js
@@ -4,11 +4,12 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
-import {refreshChannelWithRetry} from 'app/actions/views/channel';
-import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
-
+import {selectFocusedPostId} from 'mattermost-redux/actions/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+import {loadChannelsByTeamName, refreshChannelWithRetry} from 'app/actions/views/channel';
+import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
+
import PostList from './post_list';
function makeMapStateToProps() {
@@ -31,7 +32,9 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
+ loadChannelsByTeamName,
refreshChannelWithRetry,
+ selectFocusedPostId,
}, dispatch),
};
}
diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js
index 7dc38dad2..4cb765ce8 100644
--- a/app/components/post_list/post_list.js
+++ b/app/components/post_list/post_list.js
@@ -4,10 +4,10 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
+ FlatList,
InteractionManager,
Platform,
StyleSheet,
- FlatList,
} from 'react-native';
import ChannelIntro from 'app/components/channel_intro';
@@ -15,6 +15,7 @@ import Post from 'app/components/post';
import {DATE_LINE, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
import mattermostManaged from 'app/mattermost_managed';
import {makeExtraData} from 'app/utils/list_view';
+import {changeOpacity} from 'app/utils/theme';
import DateHeader from './date_header';
import LoadMorePosts from './load_more_posts';
@@ -30,7 +31,9 @@ const DATE_HEADER_HEIGHT = 28;
export default class PostList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
+ loadChannelsByTeamName: PropTypes.func.isRequired,
refreshChannelWithRetry: PropTypes.func.isRequired,
+ selectFocusedPostId: PropTypes.func.isRequired,
}).isRequired,
channelId: PropTypes.string,
currentUserId: PropTypes.string,
@@ -42,6 +45,7 @@ export default class PostList extends PureComponent {
loadMore: PropTypes.func,
measureCellLayout: PropTypes.bool,
navigator: PropTypes.object,
+ onPermalinkPress: PropTypes.func,
onPostPress: PropTypes.func,
onRefresh: PropTypes.func,
postIds: PropTypes.array.isRequired,
@@ -94,13 +98,51 @@ export default class PostList extends PureComponent {
mattermostManaged.removeEventListener(this.listenerId);
}
+ handleClosePermalink = () => {
+ const {actions} = this.props;
+ actions.selectFocusedPostId('');
+ this.showingPermalink = false;
+ };
+
+ handlePermalinkPress = (postId, teamName) => {
+ this.props.actions.loadChannelsByTeamName(teamName);
+ this.showPermalinkView(postId);
+ };
+
+ showPermalinkView = (postId) => {
+ const {actions, navigator} = this.props;
+
+ actions.selectFocusedPostId(postId);
+
+ if (!this.showingPermalink) {
+ const options = {
+ screen: 'Permalink',
+ animationType: 'none',
+ backButtonTitle: '',
+ navigatorStyle: {
+ navBarHidden: true,
+ screenBackgroundColor: changeOpacity('#000', 0.2),
+ modalPresentationStyle: 'overCurrentContext',
+ },
+ passProps: {
+ isPermalink: true,
+ onClose: this.handleClosePermalink,
+ onPermalinkPress: this.handlePermalinkPress,
+ },
+ };
+
+ this.showingPermalink = true;
+ navigator.showModal(options);
+ }
+ };
+
scrollToBottomOffset = () => {
InteractionManager.runAfterInteractions(() => {
if (this.refs.list) {
this.refs.list.scrollToOffset({offset: 0, animated: false});
}
});
- }
+ };
getMeasurementOffset = (index) => {
const orderedKeys = Object.keys(this.itemMeasurements).sort((a, b) => {
@@ -117,13 +159,12 @@ export default class PostList extends PureComponent {
}).slice(0, index);
return orderedKeys.map((i) => this.itemMeasurements[i]).reduce((a, b) => a + b, 0);
- }
+ };
scrollListToMessageOffset = () => {
const index = this.moreNewMessages ? this.props.postIds.length - 1 : this.newMessagesIndex;
-
if (index !== -1) {
- let offset = this.getMeasurementOffset(index);
+ let offset = this.getMeasurementOffset(index) - (3 * this.itemMeasurements[index]);
const windowHeight = this.state.postListHeight;
if (offset < windowHeight) {
@@ -148,7 +189,7 @@ export default class PostList extends PureComponent {
}
});
}
- }
+ };
setManagedConfig = async (config) => {
let nextConfig = config;
@@ -191,7 +232,7 @@ export default class PostList extends PureComponent {
});
}
}
- }
+ };
renderItem = ({item, index}) => {
if (item === START_OF_NEW_MESSAGES) {
@@ -260,6 +301,7 @@ export default class PostList extends PureComponent {
renderReplies={renderReplies}
isSearchResult={isSearchResult}
shouldRenderReplyButton={shouldRenderReplyButton}
+ onPermalinkPress={this.handlePermalinkPress}
onPress={onPostPress}
navigator={navigator}
managedConfig={managedConfig}
@@ -296,7 +338,7 @@ export default class PostList extends PureComponent {
this.setState({
postListHeight: height,
});
- }
+ };
render() {
const {
diff --git a/app/components/post_textbox/index.js b/app/components/post_textbox/index.js
index a4454efd9..08ba0a39c 100644
--- a/app/components/post_textbox/index.js
+++ b/app/components/post_textbox/index.js
@@ -28,7 +28,7 @@ function mapStateToProps(state, ownProps) {
const currentChannel = getCurrentChannel(state);
let deactivatedChannel = false;
- if (currentChannel.type === General.DM_CHANNEL) {
+ if (currentChannel && currentChannel.type === General.DM_CHANNEL) {
const teammate = getChannelMembersForDm(state, currentChannel);
if (teammate.length && teammate[0].delete_at) {
deactivatedChannel = true;
@@ -36,7 +36,7 @@ function mapStateToProps(state, ownProps) {
}
return {
- channelId: ownProps.channelId || currentChannel.id,
+ channelId: ownProps.channelId || (currentChannel ? currentChannel.id : ''),
canUploadFiles: canUploadFilesOnMobile(state),
channelIsLoading: state.views.channel.loading,
currentUserId: getCurrentUserId(state),
diff --git a/app/components/search_preview/index.js b/app/components/search_preview/index.js
deleted file mode 100644
index 3fb34d422..000000000
--- a/app/components/search_preview/index.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import {bindActionCreators} from 'redux';
-import {connect} from 'react-redux';
-
-import {makeGetPostIdsAroundPost} from 'mattermost-redux/selectors/entities/posts';
-import {getPostsAfter, getPostsBefore, getPostThread} from 'mattermost-redux/actions/posts';
-import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
-import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
-
-import SearchPreview from './search_preview';
-
-function makeMapStateToProps() {
- const getPostIdsAroundPost = makeGetPostIdsAroundPost();
- const getChannel = makeGetChannel();
-
- return function mapStateToProps(state, ownProps) {
- const channel = getChannel(state, {id: ownProps.channelId});
- const postIds = getPostIdsAroundPost(state, ownProps.focusedPostId, ownProps.channelId, {postsBeforeCount: 5, postsAfterCount: 5});
-
- return {
- channelName: channel ? channel.display_name : '',
- currentUserId: getCurrentUserId(state),
- postIds,
- };
- };
-}
-
-function mapDispatchToProps(dispatch) {
- return {
- actions: bindActionCreators({
- getPostsAfter,
- getPostsBefore,
- getPostThread,
- }, dispatch),
- };
-}
-
-export default connect(makeMapStateToProps, mapDispatchToProps, null, {withRef: true})(SearchPreview);
diff --git a/app/components/search_preview/search_preview.js b/app/components/search_preview/search_preview.js
deleted file mode 100644
index ecd519938..000000000
--- a/app/components/search_preview/search_preview.js
+++ /dev/null
@@ -1,287 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import React, {PureComponent} from 'react';
-import PropTypes from 'prop-types';
-import {
- Platform,
- Text,
- TouchableOpacity,
- View,
-} from 'react-native';
-import * as Animatable from 'react-native-animatable';
-import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
-
-import FormattedText from 'app/components/formatted_text';
-import Loading from 'app/components/loading';
-import PostList from 'app/components/post_list';
-import PostListRetry from 'app/components/post_list_retry';
-import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
-
-Animatable.initializeRegistryWithDefinitions({
- growOut: {
- from: {
- opacity: 1,
- scale: 1,
- },
- 0.5: {
- opacity: 1,
- scale: 3,
- },
- to: {
- opacity: 0,
- scale: 5,
- },
- },
-});
-
-export default class SearchPreview extends PureComponent {
- static propTypes = {
- actions: PropTypes.shape({
- getPostsAfter: PropTypes.func.isRequired,
- getPostsBefore: PropTypes.func.isRequired,
- getPostThread: PropTypes.func.isRequired,
- }).isRequired,
- channelId: PropTypes.string,
- channelName: PropTypes.string,
- currentUserId: PropTypes.string.isRequired,
- focusedPostId: PropTypes.string.isRequired,
- navigator: PropTypes.object,
- onClose: PropTypes.func,
- onPress: PropTypes.func,
- postIds: PropTypes.array,
- theme: PropTypes.object.isRequired,
- };
-
- static defaultProps = {
- postIds: [],
- };
-
- constructor(props) {
- super(props);
-
- const {postIds} = props;
- let show = false;
- if (postIds && postIds.length >= 10) {
- show = true;
- }
-
- this.state = {
- show,
- error: false,
- };
- }
-
- componentDidMount() {
- if (!this.state.show) {
- this.loadPosts();
- }
- }
-
- handleClose = () => {
- if (this.refs.view) {
- this.refs.view.zoomOut().then(() => {
- if (this.props.onClose) {
- this.props.onClose();
- }
- });
- }
- };
-
- handlePress = () => {
- const {channelId, channelName, onPress} = this.props;
-
- if (this.refs.view) {
- this.refs.view.growOut().then(() => {
- if (onPress) {
- onPress(channelId, channelName);
- }
- });
- }
- };
-
- loadPosts = async () => {
- const {actions, channelId, focusedPostId} = this.props;
-
- const result = await Promise.all([
- actions.getPostThread(focusedPostId, false),
- actions.getPostsBefore(channelId, focusedPostId, 0, 5),
- actions.getPostsAfter(channelId, focusedPostId, 0, 5),
- ]);
-
- const error = result.some((res) => Boolean(res.error));
- this.setState({show: true, error});
- };
-
- retry = () => {
- this.setState({show: false, error: false});
- this.loadPosts();
- };
-
- render() {
- const {
- channelName,
- currentUserId,
- focusedPostId,
- navigator,
- postIds,
- theme,
- } = this.props;
- const style = getStyleSheet(theme);
-
- let postList;
- if (this.state.error) {
- postList = (
-
- );
- } else if (this.state.show) {
- postList = (
-
- );
- } else {
- postList = ;
- }
-
- return (
-
-
-
-
-
-
-
-
- {channelName}
-
-
-
-
- {postList}
-
-
-
-
-
-
- );
- }
-}
-
-const getStyleSheet = makeStyleSheetFromTheme((theme) => {
- return {
- container: {
- position: 'absolute',
- backgroundColor: changeOpacity('#000', 0.3),
- height: '100%',
- top: 0,
- left: 0,
- zIndex: 10,
- width: '100%',
- },
- wrapper: {
- flex: 1,
- marginBottom: 10,
- marginHorizontal: 10,
- opacity: 0,
- ...Platform.select({
- android: {
- marginTop: 10,
- },
- ios: {
- marginTop: 20,
- },
- }),
- },
- header: {
- alignItems: 'center',
- backgroundColor: theme.centerChannelBg,
- borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
- borderBottomWidth: 1,
- borderTopLeftRadius: 6,
- borderTopRightRadius: 6,
- flexDirection: 'row',
- height: 44,
- paddingRight: 16,
- width: '100%',
- },
- close: {
- justifyContent: 'center',
- height: 44,
- width: 40,
- paddingLeft: 7,
- },
- titleContainer: {
- alignItems: 'center',
- flex: 1,
- paddingRight: 40,
- },
- title: {
- color: theme.centerChannelColor,
- fontSize: 17,
- fontWeight: '600',
- },
- postList: {
- backgroundColor: theme.centerChannelBg,
- flex: 1,
- },
- footer: {
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: theme.buttonBg,
- borderBottomLeftRadius: 6,
- borderBottomRightRadius: 6,
- flexDirection: 'row',
- height: 44,
- paddingRight: 16,
- width: '100%',
- },
- jump: {
- color: theme.buttonColor,
- fontSize: 16,
- fontWeight: '600',
- textAlignVertical: 'center',
- },
- };
-});
diff --git a/app/components/slack_attachments/index.js b/app/components/slack_attachments/index.js
index 745000827..7a63401ec 100644
--- a/app/components/slack_attachments/index.js
+++ b/app/components/slack_attachments/index.js
@@ -17,12 +17,23 @@ export default class SlackAttachments extends PureComponent {
postId: PropTypes.string.isRequired,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func.isRequired,
+ onPermalinkPress: PropTypes.func,
theme: PropTypes.object,
textStyles: PropTypes.object,
};
render() {
- const {attachments, baseTextStyle, blockStyles, navigator, onLongPress, postId, theme, textStyles} = this.props;
+ const {
+ attachments,
+ baseTextStyle,
+ blockStyles,
+ navigator,
+ onLongPress,
+ onPermalinkPress,
+ postId,
+ theme,
+ textStyles,
+ } = this.props;
const content = [];
attachments.forEach((attachment, i) => {
@@ -34,6 +45,7 @@ export default class SlackAttachments extends PureComponent {
key={'att_' + i}
navigator={navigator}
onLongPress={onLongPress}
+ onPermalinkPress={onPermalinkPress}
postId={postId}
theme={theme}
textStyles={textStyles}
diff --git a/app/components/slack_attachments/slack_attachment.js b/app/components/slack_attachments/slack_attachment.js
index 738dcf4bf..7398eb4f8 100644
--- a/app/components/slack_attachments/slack_attachment.js
+++ b/app/components/slack_attachments/slack_attachment.js
@@ -31,6 +31,7 @@ export default class SlackAttachment extends PureComponent {
navigator: PropTypes.object.isRequired,
postId: PropTypes.string.isRequired,
onLongPress: PropTypes.func.isRequired,
+ onPermalinkPress: PropTypes.func,
theme: PropTypes.object,
textStyles: PropTypes.object,
};
@@ -103,6 +104,7 @@ export default class SlackAttachment extends PureComponent {
baseTextStyle,
blockStyles,
navigator,
+ onPermalinkPress,
textStyles,
} = this.props;
const fields = attachment.fields;
@@ -159,6 +161,7 @@ export default class SlackAttachment extends PureComponent {
value={(field.value || '')}
navigator={navigator}
onLongPress={this.props.onLongPress}
+ onPermalinkPress={onPermalinkPress}
/>
@@ -212,6 +215,7 @@ export default class SlackAttachment extends PureComponent {
blockStyles,
textStyles,
navigator,
+ onPermalinkPress,
theme,
} = this.props;
@@ -228,6 +232,7 @@ export default class SlackAttachment extends PureComponent {
value={attachment.pretext}
navigator={navigator}
onLongPress={this.props.onLongPress}
+ onPermalinkPress={onPermalinkPress}
/>
);
@@ -341,6 +346,7 @@ export default class SlackAttachment extends PureComponent {
value={this.state.text}
navigator={navigator}
onLongPress={this.props.onLongPress}
+ onPermalinkPress={onPermalinkPress}
/>
{moreLess}
diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js
index 5ff70f47a..ab007e59b 100644
--- a/app/screens/channel_info/channel_info.js
+++ b/app/screens/channel_info/channel_info.js
@@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {injectIntl, intlShape} from 'react-intl';
+import {intlShape} from 'react-intl';
import {
Alert,
Platform,
@@ -22,9 +22,20 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ChannelInfoHeader from './channel_info_header';
import ChannelInfoRow from './channel_info_row';
-class ChannelInfo extends PureComponent {
+export default class ChannelInfo extends PureComponent {
static propTypes = {
- intl: intlShape.isRequired,
+ actions: PropTypes.shape({
+ closeDMChannel: PropTypes.func.isRequired,
+ closeGMChannel: PropTypes.func.isRequired,
+ deleteChannel: PropTypes.func.isRequired,
+ getChannelStats: PropTypes.func.isRequired,
+ leaveChannel: PropTypes.func.isRequired,
+ loadChannelsByTeamName: PropTypes.func.isRequired,
+ favoriteChannel: PropTypes.func.isRequired,
+ unfavoriteChannel: PropTypes.func.isRequired,
+ getCustomEmojisInText: PropTypes.func.isRequired,
+ selectFocusedPostId: PropTypes.func.isRequired,
+ }),
canDeleteChannel: PropTypes.bool.isRequired,
currentChannel: PropTypes.object.isRequired,
currentChannelCreatorName: PropTypes.string,
@@ -36,23 +47,17 @@ class ChannelInfo extends PureComponent {
isFavorite: PropTypes.bool.isRequired,
canManageUsers: PropTypes.bool.isRequired,
canEditChannel: PropTypes.bool.isRequired,
- actions: PropTypes.shape({
- closeDMChannel: PropTypes.func.isRequired,
- closeGMChannel: PropTypes.func.isRequired,
- deleteChannel: PropTypes.func.isRequired,
- getChannelStats: PropTypes.func.isRequired,
- leaveChannel: PropTypes.func.isRequired,
- favoriteChannel: PropTypes.func.isRequired,
- unfavoriteChannel: PropTypes.func.isRequired,
- getCustomEmojisInText: PropTypes.func.isRequired,
- }),
+ };
+
+ static contextTypes = {
+ intl: intlShape.isRequired,
};
constructor(props) {
super(props);
this.state = {
- isFavorite: this.props.isFavorite,
+ isFavorite: props.isFavorite,
};
}
@@ -82,7 +87,8 @@ class ChannelInfo extends PureComponent {
};
goToChannelAddMembers = preventDoubleTap(() => {
- const {intl, navigator, theme} = this.props;
+ const {intl} = this.context;
+ const {navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'ChannelAddMembers',
@@ -98,7 +104,8 @@ class ChannelInfo extends PureComponent {
});
goToChannelMembers = preventDoubleTap(() => {
- const {canManageUsers, intl, navigator, theme} = this.props;
+ const {intl} = this.context;
+ const {canManageUsers, navigator, theme} = this.props;
const id = canManageUsers ? 'channel_header.manageMembers' : 'channel_header.viewMembers';
const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members';
@@ -117,7 +124,8 @@ class ChannelInfo extends PureComponent {
});
handleChannelEdit = preventDoubleTap(() => {
- const {intl, navigator, theme} = this.props;
+ const {intl} = this.context;
+ const {navigator, theme} = this.props;
const id = 'mobile.channel_info.edit';
const defaultMessage = 'Edit Channel';
@@ -144,7 +152,7 @@ class ChannelInfo extends PureComponent {
};
handleDeleteOrLeave = preventDoubleTap((eventType) => {
- const {formatMessage} = this.props.intl;
+ const {formatMessage} = this.context.intl;
const channel = this.props.currentChannel;
const term = channel.type === General.OPEN_CHANNEL ?
formatMessage({id: 'mobile.channel_info.publicChannel', defaultMessage: 'Public Channel'}) :
@@ -173,7 +181,7 @@ class ChannelInfo extends PureComponent {
const result = await this.props.actions.deleteChannel(channel.id);
if (result.error) {
alertErrorWithFallback(
- this.props.intl,
+ this.context.intl,
result.error,
{
id: 'mobile.channel_info.delete_failed',
@@ -234,6 +242,44 @@ class ChannelInfo extends PureComponent {
toggleFavorite(currentChannel.id);
};
+ handleClosePermalink = () => {
+ const {actions} = this.props;
+ actions.selectFocusedPostId('');
+ this.showingPermalink = false;
+ };
+
+ handlePermalinkPress = (postId, teamName) => {
+ this.props.actions.loadChannelsByTeamName(teamName);
+ this.showPermalinkView(postId);
+ };
+
+ showPermalinkView = (postId) => {
+ const {actions, navigator} = this.props;
+
+ actions.selectFocusedPostId(postId);
+
+ if (!this.showingPermalink) {
+ const options = {
+ screen: 'Permalink',
+ animationType: 'none',
+ backButtonTitle: '',
+ navigatorStyle: {
+ navBarHidden: true,
+ screenBackgroundColor: changeOpacity('#000', 0.2),
+ modalPresentationStyle: 'overCurrentContext',
+ },
+ passProps: {
+ isPermalink: true,
+ onClose: this.handleClosePermalink,
+ onPermalinkPress: this.handlePermalinkPress,
+ },
+ };
+
+ this.showingPermalink = true;
+ navigator.showModal(options);
+ }
+ };
+
renderViewOrManageMembersRow = () => {
const channel = this.props.currentChannel;
const isDirectMessage = channel.type === General.DM_CHANNEL;
@@ -301,6 +347,7 @@ class ChannelInfo extends PureComponent {
header={currentChannel.header}
memberCount={currentChannelMemberCount}
navigator={navigator}
+ onPermalinkPress={this.handlePermalinkPress}
purpose={currentChannel.purpose}
status={status}
theme={theme}
@@ -443,5 +490,3 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
};
});
-
-export default injectIntl(ChannelInfo);
diff --git a/app/screens/channel_info/channel_info_header.js b/app/screens/channel_info/channel_info_header.js
index 99d114ff4..6f119620e 100644
--- a/app/screens/channel_info/channel_info_header.js
+++ b/app/screens/channel_info/channel_info_header.js
@@ -23,6 +23,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
displayName: PropTypes.string.isRequired,
header: PropTypes.string,
navigator: PropTypes.object.isRequired,
+ onPermalinkPress: PropTypes.func,
purpose: PropTypes.string,
status: PropTypes.string,
theme: PropTypes.object.isRequired,
@@ -37,6 +38,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
header,
memberCount,
navigator,
+ onPermalinkPress,
purpose,
status,
theme,
@@ -75,6 +77,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
/>
wrapWithContextProvider(NotificationSettingsMentionsKeywords), store, Provider);
Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(NotificationSettingsMobile), store, Provider);
Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider);
+ Navigation.registerComponent('Permalink', () => wrapWithContextProvider(Permalink), store, Provider);
Navigation.registerComponent('Root', () => Root, store, Provider);
Navigation.registerComponent('Search', () => wrapWithContextProvider(Search), store, Provider);
Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider);
diff --git a/app/screens/permalink/index.js b/app/screens/permalink/index.js
new file mode 100644
index 000000000..6c7035c3b
--- /dev/null
+++ b/app/screens/permalink/index.js
@@ -0,0 +1,76 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {getChannel as getChannelAction, joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
+import {getPostsAfter, getPostsBefore, getPostThread, selectPost} from 'mattermost-redux/actions/posts';
+import {getMyChannelMemberships, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
+import {makeGetPostIdsAroundPost, getPost} from 'mattermost-redux/selectors/entities/posts';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
+import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
+
+import {
+ handleSelectChannel,
+ loadThreadIfNecessary,
+ setChannelDisplayName,
+ setChannelLoading,
+} from 'app/actions/views/channel';
+import {handleTeamChange} from 'app/actions/views/select_team';
+
+import Permalink from './permalink';
+
+function makeMapStateToProps() {
+ const getPostIdsAroundPost = makeGetPostIdsAroundPost();
+ const getChannel = makeGetChannel();
+
+ return function mapStateToProps(state) {
+ const {currentFocusedPostId} = state.entities.posts;
+ const post = getPost(state, currentFocusedPostId);
+ const channel = post ? getChannel(state, {id: post.channel_id}) : null;
+ let postIds;
+
+ if (channel && channel.id) {
+ postIds = getPostIdsAroundPost(state, currentFocusedPostId, channel.id, {
+ postsBeforeCount: 10,
+ postsAfterCount: 10,
+ });
+ }
+
+ return {
+ channelId: channel ? channel.id : '',
+ channelName: channel ? channel.display_name : '',
+ channelTeamId: channel ? channel.team_id : '',
+ currentTeamId: getCurrentTeamId(state),
+ currentUserId: getCurrentUserId(state),
+ focusedPostId: currentFocusedPostId,
+ myMembers: getMyChannelMemberships(state),
+ postIds,
+ theme: getTheme(state),
+ };
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ getPostsAfter,
+ getPostsBefore,
+ getPostThread,
+ getChannel: getChannelAction,
+ handleSelectChannel,
+ handleTeamChange,
+ joinChannel,
+ loadThreadIfNecessary,
+ markChannelAsRead,
+ markChannelAsViewed,
+ selectPost,
+ setChannelDisplayName,
+ setChannelLoading,
+ }, dispatch),
+ };
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps, null, {withRef: true})(Permalink);
diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js
new file mode 100644
index 000000000..ef22ed1fd
--- /dev/null
+++ b/app/screens/permalink/permalink.js
@@ -0,0 +1,455 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import {
+ InteractionManager,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import {intlShape} from 'react-intl';
+import * as Animatable from 'react-native-animatable';
+import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
+
+import {General} from 'mattermost-redux/constants';
+
+import FormattedText from 'app/components/formatted_text';
+import Loading from 'app/components/loading';
+import PostList from 'app/components/post_list';
+import PostListRetry from 'app/components/post_list_retry';
+import SafeAreaView from 'app/components/safe_area_view';
+import {preventDoubleTap} from 'app/utils/tap';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+Animatable.initializeRegistryWithDefinitions({
+ growOut: {
+ from: {
+ opacity: 1,
+ scale: 1,
+ },
+ 0.5: {
+ opacity: 1,
+ scale: 3,
+ },
+ to: {
+ opacity: 0,
+ scale: 5,
+ },
+ },
+});
+
+export default class Permalink extends PureComponent {
+ static propTypes = {
+ actions: PropTypes.shape({
+ getPostsAfter: PropTypes.func.isRequired,
+ getPostsBefore: PropTypes.func.isRequired,
+ getPostThread: PropTypes.func.isRequired,
+ getChannel: PropTypes.func.isRequired,
+ handleSelectChannel: PropTypes.func.isRequired,
+ handleTeamChange: PropTypes.func.isRequired,
+ joinChannel: PropTypes.func.isRequired,
+ loadThreadIfNecessary: PropTypes.func.isRequired,
+ markChannelAsRead: PropTypes.func.isRequired,
+ markChannelAsViewed: PropTypes.func.isRequired,
+ selectPost: PropTypes.func.isRequired,
+ setChannelDisplayName: PropTypes.func.isRequired,
+ setChannelLoading: PropTypes.func.isRequired,
+ }).isRequired,
+ channelId: PropTypes.string,
+ channelName: PropTypes.string,
+ channelTeamId: PropTypes.string,
+ currentTeamId: PropTypes.string.isRequired,
+ currentUserId: PropTypes.string.isRequired,
+ focusedPostId: PropTypes.string.isRequired,
+ isPermalink: PropTypes.bool,
+ myMembers: PropTypes.object.isRequired,
+ navigator: PropTypes.object,
+ onClose: PropTypes.func,
+ onPermalinkPress: PropTypes.func,
+ onPress: PropTypes.func,
+ postIds: PropTypes.array,
+ theme: PropTypes.object.isRequired,
+ };
+
+ static defaultProps = {
+ onPress: () => true,
+ postIds: [],
+ };
+
+ static contextTypes = {
+ intl: intlShape.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+
+ const {postIds, channelName} = props;
+ let loading = false;
+
+ if (postIds && postIds.length >= 10) {
+ loading = true;
+ }
+
+ this.state = {
+ title: channelName,
+ loading,
+ error: '',
+ retry: false,
+ };
+ }
+
+ componentWillMount() {
+ if (!this.state.loading) {
+ this.loadPosts(this.props);
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.channelName !== nextProps.channelName) {
+ this.setState({title: nextProps.channelName});
+ }
+
+ if (this.props.focusedPostId !== nextProps.focusedPostId) {
+ this.setState({loading: false});
+ if (nextProps.postIds && nextProps.postIds.length < 10) {
+ this.loadPosts(nextProps);
+ } else {
+ this.setState({loading: true});
+ }
+ }
+ }
+
+ goToThread = preventDoubleTap((post) => {
+ const {actions, navigator, theme} = this.props;
+ const channelId = post.channel_id;
+ const rootId = (post.root_id || post.id);
+
+ actions.loadThreadIfNecessary(rootId, channelId);
+ actions.selectPost(rootId);
+
+ const options = {
+ screen: 'Thread',
+ animated: true,
+ backButtonTitle: '',
+ navigatorStyle: {
+ navBarTextColor: theme.sidebarHeaderTextColor,
+ navBarBackgroundColor: theme.sidebarHeaderBg,
+ navBarButtonColor: theme.sidebarHeaderTextColor,
+ screenBackgroundColor: theme.centerChannelBg,
+ },
+ passProps: {
+ channelId,
+ rootId,
+ },
+ };
+
+ navigator.push(options);
+ });
+
+ handleClose = () => {
+ const {actions, navigator, onClose} = this.props;
+ if (this.refs.view) {
+ this.refs.view.zoomOut().then(() => {
+ actions.selectPost('');
+ navigator.dismissModal({animationType: 'none'});
+
+ if (onClose) {
+ onClose();
+ }
+ });
+ }
+ };
+
+ jumpToChannel = (channelId, channelDisplayName) => {
+ if (channelId) {
+ const {actions, channelTeamId, currentTeamId, navigator, onClose, theme} = this.props;
+ const currentChannelId = this.props.channelId;
+ const {
+ handleSelectChannel,
+ handleTeamChange,
+ markChannelAsRead,
+ setChannelLoading,
+ setChannelDisplayName,
+ markChannelAsViewed,
+ } = actions;
+
+ actions.selectPost('');
+
+ if (onClose) {
+ onClose();
+ }
+
+ navigator.resetTo({
+ screen: 'Channel',
+ animated: true,
+ animationType: 'fade',
+ navigatorStyle: {
+ navBarHidden: true,
+ statusBarHidden: false,
+ statusBarHideWithNavBar: false,
+ screenBackgroundColor: theme.centerChannelBg,
+ },
+ });
+
+ if (channelTeamId && currentTeamId !== channelTeamId) {
+ handleTeamChange(channelTeamId, false);
+ }
+
+ setChannelLoading(channelId !== currentChannelId);
+ setChannelDisplayName(channelDisplayName);
+ handleSelectChannel(channelId);
+
+ InteractionManager.runAfterInteractions(async () => {
+ markChannelAsRead(channelId, currentChannelId);
+ if (channelId !== currentChannelId) {
+ markChannelAsViewed(currentChannelId);
+ }
+ });
+ }
+ };
+
+ handlePress = () => {
+ const {channelId, channelName} = this.props;
+
+ if (this.refs.view) {
+ this.refs.view.growOut().then(() => {
+ this.jumpToChannel(channelId, channelName);
+ });
+ }
+ };
+
+ loadPosts = async (props) => {
+ const {intl} = this.context;
+ const {actions, channelId, currentUserId, focusedPostId, isPermalink, postIds} = props;
+ const {formatMessage} = intl;
+ let focusChannelId = channelId;
+
+ const post = await actions.getPostThread(focusedPostId, false);
+ if (post.error && (!postIds || !postIds.length)) {
+ if (isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
+ this.setState({
+ error: formatMessage({
+ id: 'permalink.error.access',
+ defaultMessage: 'Permalink belongs to a deleted message or to a channel to which you do not have access.',
+ }),
+ title: formatMessage({
+ id: 'mobile.search.no_results',
+ defaultMessage: 'No Results Found',
+ }),
+ });
+ } else {
+ this.setState({error: post.error.message, retry: true});
+ }
+
+ return;
+ }
+
+ if (!channelId) {
+ focusChannelId = post.data.posts[focusedPostId].channel_id;
+ if (!this.props.myMembers[focusChannelId]) {
+ const {data: channel} = await actions.getChannel(focusChannelId);
+ if (channel && channel.type === General.OPEN_CHANNEL) {
+ await actions.joinChannel(currentUserId, channel.team_id, channel.id);
+ }
+ }
+ }
+
+ await Promise.all([
+ actions.getPostsBefore(focusChannelId, focusedPostId, 0, 10),
+ actions.getPostsAfter(focusChannelId, focusedPostId, 0, 10),
+ ]);
+
+ this.setState({loading: true});
+ };
+
+ retry = () => {
+ this.setState({loading: false, error: null, retry: false});
+ this.loadPosts(this.props);
+ };
+
+ render() {
+ const {
+ currentUserId,
+ focusedPostId,
+ navigator,
+ onPermalinkPress,
+ postIds,
+ theme,
+ } = this.props;
+ const {error, retry, loading, title} = this.state;
+ const style = getStyleSheet(theme);
+
+ let postList;
+ if (retry) {
+ postList = (
+
+ );
+ } else if (error) {
+ postList = (
+
+
+ {error}
+
+
+ );
+ } else if (loading) {
+ postList = (
+
+ );
+ } else {
+ postList = ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+ {postList}
+
+ {!error && loading &&
+
+
+
+ }
+
+
+
+ );
+ }
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flex: 1,
+ marginTop: 20,
+ },
+ wrapper: {
+ flex: 1,
+ margin: 10,
+ opacity: 0,
+ },
+ header: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ borderBottomColor: changeOpacity(theme.centerChannelColor, 0.2),
+ borderBottomWidth: 1,
+ borderTopLeftRadius: 6,
+ borderTopRightRadius: 6,
+ flexDirection: 'row',
+ height: 44,
+ paddingRight: 16,
+ width: '100%',
+ },
+ close: {
+ justifyContent: 'center',
+ height: 44,
+ width: 40,
+ paddingLeft: 7,
+ },
+ titleContainer: {
+ alignItems: 'center',
+ flex: 1,
+ paddingRight: 40,
+ },
+ title: {
+ color: theme.centerChannelColor,
+ fontSize: 17,
+ fontWeight: '600',
+ },
+ postList: {
+ backgroundColor: theme.centerChannelBg,
+ flex: 1,
+ },
+ bottom: {
+ borderBottomLeftRadius: 6,
+ borderBottomRightRadius: 6,
+ },
+ footer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: theme.buttonBg,
+ flexDirection: 'row',
+ height: 43,
+ paddingRight: 16,
+ width: '100%',
+ },
+ jump: {
+ color: theme.buttonColor,
+ fontSize: 15,
+ fontWeight: '600',
+ textAlignVertical: 'center',
+ },
+ errorContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 15,
+ },
+ errorText: {
+ color: changeOpacity(theme.centerChannelColor, 0.4),
+ fontSize: 15,
+ },
+ };
+});
diff --git a/app/screens/search/index.js b/app/screens/search/index.js
index 66d14445b..d96ea926c 100644
--- a/app/screens/search/index.js
+++ b/app/screens/search/index.js
@@ -4,19 +4,13 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
-import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
-import {selectPost} from 'mattermost-redux/actions/posts';
+import {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts';
import {clearSearch, removeSearchTerms, searchPosts} from 'mattermost-redux/actions/search';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
-import {
- handleSelectChannel,
- loadThreadIfNecessary,
- setChannelDisplayName,
- setChannelLoading,
-} from 'app/actions/views/channel';
+import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
import {handleSearchDraftChanged} from 'app/actions/views/search';
@@ -44,15 +38,12 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
clearSearch,
handleSearchDraftChanged,
- handleSelectChannel,
+ loadChannelsByTeamName,
loadThreadIfNecessary,
- markChannelAsRead,
- markChannelAsViewed,
removeSearchTerms,
+ selectFocusedPostId,
searchPosts,
selectPost,
- setChannelDisplayName,
- setChannelLoading,
}, dispatch),
};
}
diff --git a/app/screens/search/search.js b/app/screens/search/search.js
index 4c55a435d..858cc7200 100644
--- a/app/screens/search/search.js
+++ b/app/screens/search/search.js
@@ -3,10 +3,9 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {injectIntl, intlShape} from 'react-intl';
+import {intlShape} from 'react-intl';
import {
Keyboard,
- InteractionManager,
Platform,
SectionList,
Text,
@@ -26,7 +25,6 @@ import Loading from 'app/components/loading';
import PostListRetry from 'app/components/post_list_retry';
import SafeAreaView from 'app/components/safe_area_view';
import SearchBar from 'app/components/search_bar';
-import SearchPreview from 'app/components/search_preview';
import StatusBar from 'app/components/status_bar';
import mattermostManaged from 'app/mattermost_managed';
import {preventDoubleTap} from 'app/utils/tap';
@@ -42,21 +40,20 @@ const MODIFIER_LABEL_HEIGHT = 58;
const SEARCHING = 'searching';
const NO_RESULTS = 'no results';
-class Search extends PureComponent {
+export default class Search extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
clearSearch: PropTypes.func.isRequired,
handleSearchDraftChanged: PropTypes.func.isRequired,
+ loadChannelsByTeamName: PropTypes.func.isRequired,
loadThreadIfNecessary: PropTypes.func.isRequired,
- markChannelAsRead: PropTypes.func.isRequired,
- markChannelAsViewed: PropTypes.func.isRequired,
removeSearchTerms: PropTypes.func.isRequired,
searchPosts: PropTypes.func.isRequired,
+ selectFocusedPostId: PropTypes.func.isRequired,
selectPost: PropTypes.func.isRequired,
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
- intl: intlShape.isRequired,
isLandscape: PropTypes.bool.isRequired,
navigator: PropTypes.object,
postIds: PropTypes.array,
@@ -70,6 +67,10 @@ class Search extends PureComponent {
recent: [],
};
+ static contextTypes = {
+ intl: intlShape.isRequired,
+ };
+
constructor(props) {
super(props);
@@ -77,9 +78,6 @@ class Search extends PureComponent {
this.isX = DeviceInfo.getModel() === 'iPhone X';
this.state = {
channelName: '',
- focusedChannelId: null,
- focusedPostId: null,
- preview: false,
value: '',
managedConfig: {},
};
@@ -160,6 +158,17 @@ class Search extends PureComponent {
navigator.push(options);
};
+ handleClosePermalink = () => {
+ const {actions} = this.props;
+ actions.selectFocusedPostId('');
+ this.showingPermalink = false;
+ };
+
+ handlePermalinkPress = (postId, teamName) => {
+ this.props.actions.loadChannelsByTeamName(teamName);
+ this.showPermalinkView(postId, true);
+ };
+
handleSelectionChange = (event) => {
if (this.autocomplete) {
this.autocomplete.getWrappedInstance().handleSelectionChange(event);
@@ -201,10 +210,6 @@ class Search extends PureComponent {
return item.id || item;
};
- onFocus = () => {
- this.scrollToTop();
- };
-
onNavigatorEvent = (event) => {
if (event.id === 'backPress') {
if (this.state.preview) {
@@ -218,11 +223,34 @@ class Search extends PureComponent {
previewPost = (post) => {
Keyboard.dismiss();
- this.setState({
- preview: true,
- focusedChannelId: post.channel_id,
- focusedPostId: post.id,
- });
+ this.showPermalinkView(post.id, false);
+ };
+
+ showPermalinkView = (postId, isPermalink) => {
+ const {actions, navigator} = this.props;
+
+ actions.selectFocusedPostId(postId);
+
+ if (!this.showingPermalink) {
+ const options = {
+ screen: 'Permalink',
+ animationType: 'none',
+ backButtonTitle: '',
+ navigatorStyle: {
+ navBarHidden: true,
+ screenBackgroundColor: changeOpacity('#000', 0.2),
+ modalPresentationStyle: 'overCurrentContext',
+ },
+ passProps: {
+ isPermalink,
+ onClose: this.handleClosePermalink,
+ onPermalinkPress: this.handlePermalinkPress,
+ },
+ };
+
+ this.showingPermalink = true;
+ navigator.showModal(options);
+ }
};
removeSearchTerms = preventDoubleTap((item) => {
@@ -290,6 +318,7 @@ class Search extends PureComponent {
previewPost={this.previewPost}
goToThread={this.goToThread}
navigator={this.props.navigator}
+ onPermalinkPress={this.handlePermalinkPress}
managedConfig={managedConfig}
/>
{separator}
@@ -440,60 +469,20 @@ class Search extends PureComponent {
const {terms, isOrSearch} = recent;
this.handleTextChanged(terms);
this.search(terms, isOrSearch);
+ Keyboard.dismiss();
});
- handleClosePreview = () => {
- this.setState({
- preview: false,
- focusedChannelId: null,
- focusedPostId: null,
- });
- };
-
- handleJumpToChannel = (channelId, channelDisplayName) => {
- if (channelId) {
- const {actions, currentChannelId} = this.props;
- const {
- handleSelectChannel,
- markChannelAsRead,
- setChannelLoading,
- setChannelDisplayName,
- markChannelAsViewed,
- } = actions;
-
- setChannelLoading(channelId !== currentChannelId);
- setChannelDisplayName(channelDisplayName);
-
- InteractionManager.runAfterInteractions(() => {
- handleSelectChannel(channelId);
- requestAnimationFrame(() => {
- // mark the channel as viewed after all the frame has flushed
- markChannelAsRead(channelId, currentChannelId);
- if (channelId !== currentChannelId) {
- markChannelAsViewed(currentChannelId);
- }
- });
-
- this.props.navigator.dismissModal({animationType: 'slide-down'});
- });
- }
- };
-
render() {
const {
- intl,
isLandscape,
- navigator,
postIds,
recent,
searchingStatus,
theme,
} = this.props;
- const {
- preview,
- value,
- } = this.state;
+ const {intl} = this.context;
+ const {value} = this.state;
const style = getStyleFromTheme(theme);
const sections = [{
data: [{
@@ -522,7 +511,7 @@ class Search extends PureComponent {
sections.push({
data: recent,
key: 'recent',
- title: intl.formatMessage({id: 'mobile.search.recentTitle', defaultMessage: 'Recent Searches'}),
+ title: intl.formatMessage({id: 'mobile.search.recent_title', defaultMessage: 'Recent Searches'}),
renderItem: this.renderRecentItem,
keyExtractor: this.keyRecentExtractor,
ItemSeparatorComponent: this.renderRecentSeparator,
@@ -583,23 +572,6 @@ class Search extends PureComponent {
});
}
- let previewComponent;
- if (preview) {
- const {focusedChannelId, focusedPostId} = this.state;
-
- previewComponent = (
-
- );
- }
-
const searchBarInput = {
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
@@ -651,7 +623,6 @@ class Search extends PureComponent {
isSearch={true}
value={value}
/>
- {previewComponent}
);
@@ -778,4 +749,3 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
};
});
-export default injectIntl(Search);
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 50ecfad23..d934fcd87 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,
+ onPermalinkPress: PropTypes.func.isRequired,
postId: PropTypes.string.isRequired,
previewPost: PropTypes.func.isRequired,
};
@@ -26,6 +27,7 @@ export default class SearchResultPost extends PureComponent {
postComponentProps.onReply = this.props.goToThread;
postComponentProps.shouldRenderReplyButton = true;
postComponentProps.managedConfig = this.props.managedConfig;
+ postComponentProps.onPermalinkPress = this.props.onPermalinkPress;
}
return (
diff --git a/app/screens/user_profile/user_profile.js b/app/screens/user_profile/user_profile.js
index 10dc20271..647e9cdb5 100644
--- a/app/screens/user_profile/user_profile.js
+++ b/app/screens/user_profile/user_profile.js
@@ -4,7 +4,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
- Platform,
ScrollView,
Text,
View,
@@ -47,17 +46,20 @@ class UserProfile extends PureComponent {
}
close = () => {
- const {navigator} = this.props;
+ const {navigator, theme} = this.props;
- navigator.popToRoot({
+ navigator.resetTo({
+ screen: 'Channel',
animated: true,
+ navigatorStyle: {
+ animated: true,
+ animationType: 'fade',
+ navBarHidden: true,
+ statusBarHidden: false,
+ statusBarHideWithNavBar: false,
+ screenBackgroundColor: theme.centerChannelBg,
+ },
});
-
- if (Platform.OS === 'android') {
- navigator.dismissModal({
- animationType: 'slide-down',
- });
- }
};
displaySendMessageOption = () => {
diff --git a/app/utils/markdown.js b/app/utils/markdown.js
index 5425e18c7..f1c8c2ae8 100644
--- a/app/utils/markdown.js
+++ b/app/utils/markdown.js
@@ -181,3 +181,7 @@ const languages = {
export function getDisplayNameForLanguage(language) {
return languages[language.toLowerCase()] || '';
}
+
+export function escapeRegex(text) {
+ return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
+}
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 141817748..a26207040 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -2166,8 +2166,9 @@
"mobile.search.from_modifier_title": "username",
"mobile.search.in_modifier_description": "to find posts in specific channels",
"mobile.search.in_modifier_title": "channel-name",
- "mobile.search.jump": "JUMP",
+ "mobile.search.jump": "Jump to recent messages",
"mobile.search.no_results": "No Results Found",
+ "mobile.search.recent_title": "Recent Searches",
"mobile.select_team.choose": "Your teams:",
"mobile.select_team.join_open": "Open teams you can join",
"mobile.select_team.no_teams": "There are no available teams for you to join.",