diff --git a/app/components/channel_loader/index.js b/app/components/channel_loader/index.js
index d99f1e453..a4ed5d89d 100644
--- a/app/components/channel_loader/index.js
+++ b/app/components/channel_loader/index.js
@@ -6,10 +6,10 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelLoader from './channel_loader';
-function mapStateToProps(state) {
+function mapStateToProps(state, ownProps) {
const {deviceWidth} = state.device.dimension;
return {
- channelIsLoading: state.views.channel.loading,
+ channelIsLoading: ownProps.channelIsLoading || state.views.channel.loading,
deviceWidth,
theme: getTheme(state),
};
diff --git a/app/components/failed_network_action/cloud.js b/app/components/failed_network_action/cloud.js
new file mode 100644
index 000000000..529c87f86
--- /dev/null
+++ b/app/components/failed_network_action/cloud.js
@@ -0,0 +1,35 @@
+// 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 {View} from 'react-native';
+import Svg, {Path} from 'react-native-svg';
+
+export default class CloudSvg extends PureComponent {
+ static propTypes = {
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+ color: PropTypes.string.isRequired,
+ };
+
+ render() {
+ const {color, height, width} = this.props;
+ return (
+
+
+
+ );
+ }
+}
diff --git a/app/components/failed_network_action/index.js b/app/components/failed_network_action/index.js
new file mode 100644
index 000000000..f8165a9bf
--- /dev/null
+++ b/app/components/failed_network_action/index.js
@@ -0,0 +1,85 @@
+// 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 {TouchableOpacity, View} from 'react-native';
+
+import FormattedText from 'app/components/formatted_text';
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+import Cloud from './cloud';
+
+export default class FailedNetworkAction extends PureComponent {
+ static propTypes = {
+ onRetry: PropTypes.func,
+ theme: PropTypes.object.isRequired,
+ };
+
+ render() {
+ const {theme, onRetry} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ return (
+
+
+
+
+ {onRetry &&
+
+
+
+ }
+
+ );
+ }
+}
+
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ paddingHorizontal: 15,
+ },
+ title: {
+ color: changeOpacity(theme.centerChannelColor, 0.8),
+ fontSize: 20,
+ fontWeight: '600',
+ marginBottom: 15,
+ },
+ description: {
+ color: changeOpacity(theme.centerChannelColor, 0.4),
+ fontSize: 17,
+ textAlign: 'center',
+ },
+ retryContainer: {
+ marginTop: 30,
+ },
+ retry: {
+ color: changeOpacity(theme.centerChannelColor, 0.7),
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ };
+});
diff --git a/app/components/no_results.js b/app/components/no_results.js
new file mode 100644
index 000000000..b255917cc
--- /dev/null
+++ b/app/components/no_results.js
@@ -0,0 +1,71 @@
+// 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 {Text, View} from 'react-native';
+import IonIcon from 'react-native-vector-icons/Ionicons';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+export default class NoResults extends PureComponent {
+ static propTypes = {
+ description: PropTypes.string,
+ iconName: PropTypes.string,
+ theme: PropTypes.object.isRequired,
+ title: PropTypes.string.isRequired,
+ };
+
+ render() {
+ const {
+ description,
+ iconName,
+ theme,
+ title,
+ } = this.props;
+ const style = getStyleFromTheme(theme);
+
+ let icon;
+ if (iconName) {
+ icon = (
+
+ );
+ }
+
+ return (
+
+ {icon}
+ {title}
+ {description &&
+ {description}
+ }
+
+ );
+ }
+}
+
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ paddingHorizontal: 15,
+ },
+ title: {
+ color: changeOpacity(theme.centerChannelColor, 0.4),
+ fontSize: 20,
+ fontWeight: '600',
+ marginBottom: 15,
+ },
+ description: {
+ color: changeOpacity(theme.centerChannelColor, 0.4),
+ fontSize: 17,
+ textAlign: 'center',
+ },
+ };
+});
diff --git a/app/components/post_header/post_header.js b/app/components/post_header/post_header.js
index 797139f8a..e14cd0eb5 100644
--- a/app/components/post_header/post_header.js
+++ b/app/components/post_header/post_header.js
@@ -266,7 +266,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
- height: 30,
+ height: 35,
minWidth: 40,
paddingVertical: 10,
},
diff --git a/app/components/post_separator.js b/app/components/post_separator.js
new file mode 100644
index 000000000..bd9c7c1a2
--- /dev/null
+++ b/app/components/post_separator.js
@@ -0,0 +1,44 @@
+// 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 {View} from 'react-native';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+
+const SEPARATOR_HEIGHT = 3;
+
+export default class PostSeparator extends PureComponent {
+ static propTypes = {
+ theme: PropTypes.object.isRequired,
+ };
+
+ render() {
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ return (
+
+
+
+ );
+ }
+}
+
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ separatorContainer: {
+ justifyContent: 'center',
+ flex: 1,
+ height: SEPARATOR_HEIGHT,
+ },
+ postsSeparator: {
+ height: 15,
+ },
+ separator: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
+ height: 1,
+ },
+ };
+});
diff --git a/app/components/settings_drawer/settings_drawer.js b/app/components/settings_drawer/settings_drawer.js
index f9cce998b..b0401f55c 100644
--- a/app/components/settings_drawer/settings_drawer.js
+++ b/app/components/settings_drawer/settings_drawer.js
@@ -180,58 +180,45 @@ export default class SettingsDrawer extends PureComponent {
});
goToEditProfile = preventDoubleTap(() => {
- const {currentUser, navigator, theme} = this.props;
+ const {currentUser} = this.props;
const {formatMessage} = this.context.intl;
this.closeSettingsDrawer();
- navigator.showModal({
- screen: 'EditProfile',
- title: formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
- animationType: 'slide-up',
- animated: true,
- backButtonTitle: '',
- navigatorStyle: {
- navBarTextColor: theme.sidebarHeaderTextColor,
- navBarBackgroundColor: theme.sidebarHeaderBg,
- navBarButtonColor: theme.sidebarHeaderTextColor,
- screenBackgroundColor: theme.centerChannelBg,
- },
- navigatorButtons: {
- leftButtons: [{
- id: 'close-settings',
- icon: this.closeButton,
- }],
- },
- passProps: {
- currentUser,
- },
- });
+ this.openModal(
+ 'EditProfile',
+ formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
+ {currentUser}
+ );
+ });
+
+ goToFlagged = preventDoubleTap(() => {
+ const {formatMessage} = this.context.intl;
+
+ this.closeSettingsDrawer();
+ this.openModal(
+ 'FlaggedPosts',
+ formatMessage({id: 'search_header.title3', defaultMessage: 'Flagged Posts'}),
+ );
+ });
+
+ goToMentions = preventDoubleTap(() => {
+ const {intl} = this.context;
+
+ this.closeSettingsDrawer();
+ this.openModal(
+ 'RecentMentions',
+ intl.formatMessage({id: 'search_header.title2', defaultMessage: 'Recent Mentions'}),
+ );
});
goToSettings = preventDoubleTap(() => {
const {intl} = this.context;
- const {navigator, theme} = this.props;
this.closeSettingsDrawer();
- navigator.showModal({
- screen: 'Settings',
- title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
- animationType: 'slide-up',
- animated: true,
- backButtonTitle: '',
- navigatorStyle: {
- navBarTextColor: theme.sidebarHeaderTextColor,
- navBarBackgroundColor: theme.sidebarHeaderBg,
- navBarButtonColor: theme.sidebarHeaderTextColor,
- screenBackgroundColor: theme.centerChannelBg,
- },
- navigatorButtons: {
- leftButtons: [{
- id: 'close-settings',
- icon: this.closeButton,
- }],
- },
- });
+ this.openModal(
+ 'Settings',
+ intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
+ );
});
logout = preventDoubleTap(() => {
@@ -240,6 +227,32 @@ export default class SettingsDrawer extends PureComponent {
InteractionManager.runAfterInteractions(logout);
});
+ openModal = (screen, title, passProps) => {
+ const {navigator, theme} = this.props;
+
+ this.closeSettingsDrawer();
+ navigator.showModal({
+ screen,
+ title,
+ animationType: 'slide-up',
+ animated: true,
+ backButtonTitle: '',
+ navigatorStyle: {
+ navBarTextColor: theme.sidebarHeaderTextColor,
+ navBarBackgroundColor: theme.sidebarHeaderBg,
+ navBarButtonColor: theme.sidebarHeaderTextColor,
+ screenBackgroundColor: theme.centerChannelBg,
+ },
+ navigatorButtons: {
+ leftButtons: [{
+ id: 'close-settings',
+ icon: this.closeButton,
+ }],
+ },
+ passProps,
+ });
+ };
+
renderUserStatusIcon = (userId) => {
return (
+
+
+
+
+
+
+
+
{
+ const {actions, navigator, theme} = this.props;
+ const channelId = post.channel_id;
+ const rootId = (post.root_id || post.id);
+
+ Keyboard.dismiss();
+ 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);
+ };
+
+ handleClosePermalink = () => {
+ const {actions} = this.props;
+ actions.selectFocusedPostId('');
+ this.showingPermalink = false;
+ };
+
+ handlePermalinkPress = (postId, teamName) => {
+ this.props.actions.loadChannelsByTeamName(teamName);
+ this.showPermalinkView(postId, true);
+ };
+
+ keyExtractor = (item) => item;
+
+ onNavigatorEvent = (event) => {
+ if (event.type === 'NavBarButtonPress') {
+ if (event.id === 'close-settings') {
+ this.props.navigator.dismissModal({
+ animationType: 'slide-down',
+ });
+ }
+ }
+ };
+
+ previewPost = (post) => {
+ Keyboard.dismiss();
+
+ this.showPermalinkView(post.id, false);
+ };
+
+ renderEmpty = () => {
+ const {formatMessage} = this.context.intl;
+ const {theme} = this.props;
+
+ return (
+
+ );
+ };
+
+ renderPost = ({item, index}) => {
+ const {postIds, theme} = this.props;
+ const {managedConfig} = this.state;
+ if (item.indexOf(DATE_LINE) === 0) {
+ const date = new Date(item.substring(DATE_LINE.length));
+
+ return (
+
+ );
+ }
+
+ let separator;
+ const nextPost = postIds[index + 1];
+ if (nextPost && nextPost.indexOf(DATE_LINE) === -1) {
+ separator = ;
+ }
+
+ return (
+
+
+
+ {separator}
+
+ );
+ };
+
+ setManagedConfig = async (config) => {
+ let nextConfig = config;
+ if (!nextConfig) {
+ nextConfig = await mattermostManaged.getLocalConfig();
+ }
+
+ this.setState({
+ managedConfig: nextConfig,
+ });
+ };
+
+ showPermalinkView = (postId, isPermalink) => {
+ const {actions, navigator} = this.props;
+
+ actions.selectFocusedPostId(postId);
+
+ if (!this.showingPermalink) {
+ const options = {
+ screen: 'Permalink',
+ animationType: 'none',
+ backButtonTitle: '',
+ overrideBackPress: true,
+ navigatorStyle: {
+ navBarHidden: true,
+ screenBackgroundColor: changeOpacity('#000', 0.2),
+ modalPresentationStyle: 'overCurrentContext',
+ },
+ passProps: {
+ isPermalink,
+ onClose: this.handleClosePermalink,
+ onPermalinkPress: this.handlePermalinkPress,
+ },
+ };
+
+ this.showingPermalink = true;
+ navigator.showModal(options);
+ }
+ };
+
+ retry = () => {
+ this.props.actions.getFlaggedPosts();
+ };
+
+ render() {
+ const {didFail, isLoading, postIds, theme} = this.props;
+
+ let component;
+ if (didFail) {
+ component = (
+
+ );
+ } else if (isLoading) {
+ component = (
+
+ );
+ } else if (postIds.length) {
+ component = (
+
+ );
+ } else {
+ component = this.renderEmpty();
+ }
+
+ return (
+
+
+
+ {component}
+
+
+ );
+ }
+}
+
+const style = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
diff --git a/app/screens/flagged_posts/index.js b/app/screens/flagged_posts/index.js
new file mode 100644
index 000000000..1a26d557c
--- /dev/null
+++ b/app/screens/flagged_posts/index.js
@@ -0,0 +1,47 @@
+// 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 {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts';
+import {clearSearch, getFlaggedPosts} from 'mattermost-redux/actions/search';
+import {RequestStatus} from 'mattermost-redux/constants';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel';
+import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list';
+
+import FlaggedPosts from './flagged_posts';
+
+function makeMapStateToProps() {
+ const preparePostIds = makePreparePostIdsForSearchPosts();
+ return (state) => {
+ const postIds = preparePostIds(state, state.entities.search.flagged);
+ const {flaggedPosts: flaggedPostsRequest} = state.requests.search;
+ const isLoading = flaggedPostsRequest.status === RequestStatus.STARTED ||
+ flaggedPostsRequest.status === RequestStatus.NOT_STARTED;
+
+ return {
+ postIds,
+ isLoading,
+ didFail: flaggedPostsRequest.status === RequestStatus.FAILURE,
+ theme: getTheme(state),
+ };
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ clearSearch,
+ loadChannelsByTeamName,
+ loadThreadIfNecessary,
+ getFlaggedPosts,
+ selectFocusedPostId,
+ selectPost,
+ }, dispatch),
+ };
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(FlaggedPosts);
diff --git a/app/screens/index.js b/app/screens/index.js
index ec68aaf5b..d04315c30 100644
--- a/app/screens/index.js
+++ b/app/screens/index.js
@@ -21,6 +21,7 @@ import DisplaySettings from 'app/screens/settings/display_settings';
import EditChannel from 'app/screens/edit_channel';
import EditPost from 'app/screens/edit_post';
import EditProfile from 'app/screens/edit_profile';
+import FlaggedPosts from 'app/screens/flagged_posts';
import ImagePreview from 'app/screens/image_preview';
import TextPreview from 'app/screens/text_preview';
import Login from 'app/screens/login';
@@ -37,6 +38,7 @@ import NotificationSettingsMentionsKeywords from 'app/screens/settings/notificat
import NotificationSettingsMobile from 'app/screens/settings/notification_settings_mobile';
import OptionsModal from 'app/screens/options_modal';
import Permalink from 'app/screens/permalink';
+import RecentMentions from 'app/screens/recent_mentions';
import Root from 'app/screens/root';
import SSO from 'app/screens/sso';
import Search from 'app/screens/search';
@@ -81,6 +83,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(EditChannel), store, Provider);
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider);
Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(EditProfile), store, Provider);
+ Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(FlaggedPosts), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider);
Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(TextPreview), store, Provider);
Navigation.registerComponent('Login', () => wrapWithContextProvider(Login), store, Provider);
@@ -97,6 +100,7 @@ export function registerScreens(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('RecentMentions', () => wrapWithContextProvider(RecentMentions), 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/recent_mentions/index.js b/app/screens/recent_mentions/index.js
new file mode 100644
index 000000000..516742034
--- /dev/null
+++ b/app/screens/recent_mentions/index.js
@@ -0,0 +1,47 @@
+// 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 {selectFocusedPostId, selectPost} from 'mattermost-redux/actions/posts';
+import {clearSearch, getRecentMentions} from 'mattermost-redux/actions/search';
+import {RequestStatus} from 'mattermost-redux/constants';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel';
+import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list';
+
+import RecentMentions from './recent_mentions';
+
+function makeMapStateToProps() {
+ const preparePostIds = makePreparePostIdsForSearchPosts();
+ return (state) => {
+ const postIds = preparePostIds(state, state.entities.search.results);
+ const {recentMentions: recentMentionsRequest} = state.requests.search;
+ const isLoading = recentMentionsRequest.status === RequestStatus.STARTED ||
+ recentMentionsRequest.status === RequestStatus.NOT_STARTED;
+
+ return {
+ postIds,
+ isLoading,
+ didFail: recentMentionsRequest.status === RequestStatus.FAILURE,
+ theme: getTheme(state),
+ };
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ clearSearch,
+ loadChannelsByTeamName,
+ loadThreadIfNecessary,
+ getRecentMentions,
+ selectFocusedPostId,
+ selectPost,
+ }, dispatch),
+ };
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(RecentMentions);
diff --git a/app/screens/recent_mentions/recent_mentions.js b/app/screens/recent_mentions/recent_mentions.js
new file mode 100644
index 000000000..ce945ec10
--- /dev/null
+++ b/app/screens/recent_mentions/recent_mentions.js
@@ -0,0 +1,276 @@
+// 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 {intlShape} from 'react-intl';
+import {
+ Keyboard,
+ FlatList,
+ SafeAreaView,
+ StyleSheet,
+ View,
+} from 'react-native';
+
+import ChannelLoader from 'app/components/channel_loader';
+import DateHeader from 'app/components/post_list/date_header';
+import FailedNetworkAction from 'app/components/failed_network_action';
+import NoResults from 'app/components/no_results';
+import PostSeparator from 'app/components/post_separator';
+import StatusBar from 'app/components/status_bar';
+import mattermostManaged from 'app/mattermost_managed';
+import SearchResultPost from 'app/screens/search/search_result_post';
+import ChannelDisplayName from 'app/screens/search/channel_display_name';
+import {DATE_LINE} from 'app/selectors/post_list';
+import {changeOpacity} from 'app/utils/theme';
+
+export default class RecentMentions extends PureComponent {
+ static propTypes = {
+ actions: PropTypes.shape({
+ clearSearch: PropTypes.func.isRequired,
+ loadChannelsByTeamName: PropTypes.func.isRequired,
+ loadThreadIfNecessary: PropTypes.func.isRequired,
+ getRecentMentions: PropTypes.func.isRequired,
+ selectFocusedPostId: PropTypes.func.isRequired,
+ selectPost: PropTypes.func.isRequired,
+ }).isRequired,
+ didFail: PropTypes.bool,
+ isLoading: PropTypes.bool,
+ navigator: PropTypes.object,
+ postIds: PropTypes.array,
+ theme: PropTypes.object.isRequired,
+ };
+
+ static defaultProps = {
+ postIds: [],
+ };
+
+ static contextTypes = {
+ intl: intlShape.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+
+ props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
+ props.actions.clearSearch();
+ props.actions.getRecentMentions();
+
+ this.state = {
+ managedConfig: {},
+ };
+ }
+
+ componentWillMount() {
+ this.listenerId = mattermostManaged.addEventListener('change', this.setManagedConfig);
+ }
+
+ componentDidMount() {
+ this.setManagedConfig();
+ }
+
+ componentWillUnmount() {
+ mattermostManaged.removeEventListener(this.listenerId);
+ }
+
+ goToThread = (post) => {
+ const {actions, navigator, theme} = this.props;
+ const channelId = post.channel_id;
+ const rootId = (post.root_id || post.id);
+
+ Keyboard.dismiss();
+ 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);
+ };
+
+ handleClosePermalink = () => {
+ const {actions} = this.props;
+ actions.selectFocusedPostId('');
+ this.showingPermalink = false;
+ };
+
+ handlePermalinkPress = (postId, teamName) => {
+ this.props.actions.loadChannelsByTeamName(teamName);
+ this.showPermalinkView(postId, true);
+ };
+
+ keyExtractor = (item) => item;
+
+ onNavigatorEvent = (event) => {
+ if (event.type === 'NavBarButtonPress') {
+ if (event.id === 'close-settings') {
+ this.props.navigator.dismissModal({
+ animationType: 'slide-down',
+ });
+ }
+ }
+ };
+
+ previewPost = (post) => {
+ Keyboard.dismiss();
+
+ this.showPermalinkView(post.id, false);
+ };
+
+ renderEmpty = () => {
+ const {formatMessage} = this.context.intl;
+ const {theme} = this.props;
+
+ return (
+
+ );
+ };
+
+ renderPost = ({item, index}) => {
+ const {postIds, theme} = this.props;
+ const {managedConfig} = this.state;
+ if (item.indexOf(DATE_LINE) === 0) {
+ const date = new Date(item.substring(DATE_LINE.length));
+
+ return (
+
+ );
+ }
+
+ let separator;
+ const nextPost = postIds[index + 1];
+ if (nextPost && nextPost.indexOf(DATE_LINE) === -1) {
+ separator = ;
+ }
+
+ return (
+
+
+
+ {separator}
+
+ );
+ };
+
+ setManagedConfig = async (config) => {
+ let nextConfig = config;
+ if (!nextConfig) {
+ nextConfig = await mattermostManaged.getLocalConfig();
+ }
+
+ this.setState({
+ managedConfig: nextConfig,
+ });
+ };
+
+ showPermalinkView = (postId, isPermalink) => {
+ const {actions, navigator} = this.props;
+
+ actions.selectFocusedPostId(postId);
+
+ if (!this.showingPermalink) {
+ const options = {
+ screen: 'Permalink',
+ animationType: 'none',
+ backButtonTitle: '',
+ overrideBackPress: true,
+ navigatorStyle: {
+ navBarHidden: true,
+ screenBackgroundColor: changeOpacity('#000', 0.2),
+ modalPresentationStyle: 'overCurrentContext',
+ },
+ passProps: {
+ isPermalink,
+ onClose: this.handleClosePermalink,
+ onPermalinkPress: this.handlePermalinkPress,
+ },
+ };
+
+ this.showingPermalink = true;
+ navigator.showModal(options);
+ }
+ };
+
+ retry = () => {
+ this.props.actions.getRecentMentions();
+ };
+
+ render() {
+ const {didFail, isLoading, postIds, theme} = this.props;
+
+ let component;
+ if (didFail) {
+ component = (
+
+ );
+ } else if (isLoading) {
+ component = (
+
+ );
+ } else if (postIds.length) {
+ component = (
+
+ );
+ } else {
+ component = this.renderEmpty();
+ }
+
+ return (
+
+
+
+ {component}
+
+
+ );
+ }
+}
+
+const style = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
diff --git a/app/screens/search/index.js b/app/screens/search/index.js
index d96ea926c..2fbc724bc 100644
--- a/app/screens/search/index.js
+++ b/app/screens/search/index.js
@@ -12,24 +12,30 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {loadChannelsByTeamName, loadThreadIfNecessary} from 'app/actions/views/channel';
import {isLandscape} from 'app/selectors/device';
+import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list';
import {handleSearchDraftChanged} from 'app/actions/views/search';
import Search from './search';
-function mapStateToProps(state) {
- const currentTeamId = getCurrentTeamId(state);
- const currentChannelId = getCurrentChannelId(state);
- const {recent} = state.entities.search;
- const {searchPosts: searchRequest} = state.requests.search;
+function makeMapStateToProps() {
+ const preparePostIds = makePreparePostIdsForSearchPosts();
- return {
- currentTeamId,
- currentChannelId,
- isLandscape: isLandscape(state),
- postIds: state.entities.search.results,
- recent: recent[currentTeamId],
- searchingStatus: searchRequest.status,
- theme: getTheme(state),
+ return (state) => {
+ const postIds = preparePostIds(state, state.entities.search.results);
+ const currentTeamId = getCurrentTeamId(state);
+ const currentChannelId = getCurrentChannelId(state);
+ const {recent} = state.entities.search;
+ const {searchPosts: searchRequest} = state.requests.search;
+
+ return {
+ currentTeamId,
+ currentChannelId,
+ isLandscape: isLandscape(state),
+ postIds,
+ recent: recent[currentTeamId],
+ searchingStatus: searchRequest.status,
+ theme: getTheme(state),
+ };
};
}
@@ -48,4 +54,4 @@ function mapDispatchToProps(dispatch) {
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(Search);
+export default connect(makeMapStateToProps, mapDispatchToProps)(Search);
diff --git a/app/screens/search/search.js b/app/screens/search/search.js
index 875170376..f20717a42 100644
--- a/app/screens/search/search.js
+++ b/app/screens/search/search.js
@@ -20,6 +20,7 @@ import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import {RequestStatus} from 'mattermost-redux/constants';
import Autocomplete from 'app/components/autocomplete';
+import DateHeader from 'app/components/post_list/date_header';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import PostListRetry from 'app/components/post_list_retry';
@@ -27,6 +28,7 @@ import SafeAreaView from 'app/components/safe_area_view';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import mattermostManaged from 'app/mattermost_managed';
+import {DATE_LINE} from 'app/selectors/post_list';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -259,6 +261,15 @@ export default class Search extends PureComponent {
actions.removeSearchTerms(currentTeamId, item.terms);
});
+ renderDateHeader = (date, index) => {
+ return (
+
+ );
+ };
+
renderModifiers = ({item}) => {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
@@ -306,8 +317,14 @@ export default class Search extends PureComponent {
);
}
+ if (item.indexOf(DATE_LINE) === 0) {
+ const date = item.substring(DATE_LINE.length);
+ return this.renderDateHeader(new Date(date), index);
+ }
+
let separator;
- if (index === postIds.length - 1) {
+ const nextPost = postIds[index + 1];
+ if (nextPost && nextPost.indexOf(DATE_LINE) === -1) {
separator = this.renderPostSeparator();
}
@@ -569,7 +586,6 @@ export default class Search extends PureComponent {
title: intl.formatMessage({id: 'search_header.results', defaultMessage: 'Search Results'}),
renderItem: this.renderPost,
keyExtractor: this.keyPostExtractor,
- ItemSeparatorComponent: this.renderPostSeparator,
});
}
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 d934fcd87..93a5c86fa 100644
--- a/app/screens/search/search_result_post/search_result_post.js
+++ b/app/screens/search/search_result_post/search_result_post.js
@@ -15,6 +15,11 @@ export default class SearchResultPost extends PureComponent {
onPermalinkPress: PropTypes.func.isRequired,
postId: PropTypes.string.isRequired,
previewPost: PropTypes.func.isRequired,
+ showFullDate: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ showFullDate: false,
};
render() {
@@ -35,7 +40,7 @@ export default class SearchResultPost extends PureComponent {
postId={this.props.postId}
{...postComponentProps}
isSearchResult={true}
- showFullDate={true}
+ showFullDate={this.props.showFullDate}
navigator={this.props.navigator}
/>
);
diff --git a/app/selectors/post_list.js b/app/selectors/post_list.js
index 002118e88..33c9ddc4a 100644
--- a/app/selectors/post_list.js
+++ b/app/selectors/post_list.js
@@ -80,3 +80,44 @@ export function makePreparePostIdsForPostList() {
}
);
}
+
+export function makePreparePostIdsForSearchPosts() {
+ const getMyPosts = makeGetPostsForIds();
+
+ return createIdsSelector(
+ (state, postIds) => getMyPosts(state, postIds),
+ getCurrentUser,
+ (posts, currentUser) => {
+ if (posts.length === 0 || !currentUser) {
+ return [];
+ }
+
+ const out = [];
+ let lastDate = null;
+ for (let i = 0; i < posts.length; i++) {
+ const post = posts[i];
+
+ // give chance for the post to be loaded
+ if (!post) {
+ continue;
+ }
+
+ if (post.state === Posts.POST_DELETED && post.user_id === currentUser.id) {
+ continue;
+ }
+
+ // Push on a date header if the last post was on a different day than the current one
+ const postDate = new Date(post.create_at);
+
+ if (!lastDate || lastDate.toDateString() !== postDate.toDateString()) {
+ out.push(DATE_LINE + postDate.toString());
+ lastDate = postDate;
+ }
+ out.push(post.id);
+ }
+
+ // Flip it back to newest to oldest
+ return out;
+ }
+ );
+}
diff --git a/app/store/middleware.js b/app/store/middleware.js
index ccd7e7fa8..204383dd6 100644
--- a/app/store/middleware.js
+++ b/app/store/middleware.js
@@ -224,10 +224,19 @@ function cleanupState(action, keepCurrent = false) {
}, []);
let searchResults = [];
- if (payload.entities.search && payload.entities.search.results.length) {
- const {results} = payload.entities.search;
- searchResults = results;
- postIdsToKeep.push(...results);
+ let flaggedPosts = [];
+ if (payload.entities.search) {
+ if (payload.entities.search.results.length) {
+ const {results} = payload.entities.search;
+ searchResults = results;
+ postIdsToKeep.push(...results);
+ }
+
+ if (payload.entities.search.flagged.length) {
+ const {flagged} = payload.entities.search;
+ flaggedPosts = flagged;
+ postIdsToKeep.push(...flagged);
+ }
}
postIdsToKeep.forEach((postId) => {
@@ -314,6 +323,7 @@ function cleanupState(action, keepCurrent = false) {
search: {
...resetPayload.entities.search,
results: searchResults,
+ flagged: flaggedPosts,
},
teams: resetPayload.entities.teams,
users: payload.entities.users,
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 1a36ba8e3..3f2850958 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -2108,11 +2108,16 @@
"mobile.extension.permission": "Mattermost needs access to the device storage to share files.",
"mobile.extension.posting": "Posting...",
"mobile.extension.title": "Share in Mattermost",
+ "mobile.failed_network_action.description": "There seems to be a problem with your internet connection. Make sure you have an active connection and try again.",
+ "mobile.failed_network_action.title": "No internet connection",
+ "mobile.failed_network_action.retry": "Try Again",
"mobile.file_upload.camera": "Take Photo or Video",
"mobile.file_upload.library": "Photo Library",
"mobile.file_upload.max_warning": "Uploads limited to 5 files maximum.",
"mobile.file_upload.more": "More",
"mobile.file_upload.video": "Video Library",
+ "mobile.flagged_posts.empty_title": "No Flagged Posts",
+ "mobile.flagged_posts.empty_description": "Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.",
"mobile.help.title": "Help",
"mobile.image_preview.deleted_post_message": "This post and its files have been deleted. The previewer will now be closed.",
"mobile.image_preview.deleted_post_title": "Post Deleted",
@@ -2199,6 +2204,8 @@
"mobile.post_textbox.uploadFailedDesc": "Some attachments failed to upload to the server, Are you sure you want to post the message?",
"mobile.post_textbox.uploadFailedTitle": "Attachment failure",
"mobile.posts_view.moreMsg": "More New Messages Above",
+ "mobile.recent_mentions.empty_title": "No Recent Mentions",
+ "mobile.recent_mentions.empty_description": "Messages containing your username and other words that trigger mentions will appear here.",
"mobile.rename_channel.display_name_maxLength": "Channel name must be less than {maxLength, number} characters",
"mobile.rename_channel.display_name_minLength": "Channel name must be {minLength, number} or more characters",
"mobile.rename_channel.display_name_required": "Channel name is required",
diff --git a/yarn.lock b/yarn.lock
index dc4d57bf6..d38e8e142 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2175,7 +2175,7 @@ commonmark-react-renderer@mattermost/commonmark-react-renderer:
pascalcase "^0.1.1"
xss-filters "^1.2.6"
-"commonmark@mattermost/commonmark.js":
+commonmark@mattermost/commonmark.js:
version "0.28.0"
resolved "https://codeload.github.com/mattermost/commonmark.js/tar.gz/1496b5d11f245e00aae51f8fa1b2c4f12b4ddd7b"
dependencies:
@@ -3655,10 +3655,16 @@ iconv-lite@0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
-iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
+iconv-lite@0.4.19, iconv-lite@^0.4.17:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+iconv-lite@~0.4.13:
+ version "0.4.21"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798"
+ dependencies:
+ safer-buffer "^2.1.0"
+
ignore@^3.3.3:
version "3.3.5"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6"
@@ -4637,7 +4643,7 @@ map-visit@^1.0.0:
mattermost-redux@mattermost/mattermost-redux:
version "1.2.0"
- resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/10d691f386e1dbb7669ae549bbe81b855ef4c505"
+ resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/3d058a0d98095885e4712ad6c0aadf7e3913ad1c"
dependencies:
deep-equal "1.0.1"
form-data "2.3.1"
@@ -6702,6 +6708,10 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
+safer-buffer@^2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
sane@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/sane/-/sane-2.0.0.tgz#99cb79f21f4a53a69d4d0cd957c2db04024b8eb2"