Flagged Posts and Recent Mentions (#1584)

* Fix reply count in post header

* Make channel loader to trigger manually

* post list selector for search

* Include date separators in search results

* Flagged posts

* Recent Mentions

* Retry option for flagged posts and recent mentions

* feedback review

* Update mattermost-redux
This commit is contained in:
Elias Nahum
2018-04-13 11:26:37 -03:00
committed by Harrison Healey
parent d6ee97c0c7
commit 484a5a11f9
19 changed files with 1088 additions and 71 deletions

View File

@@ -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),
};

View File

@@ -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 (
<View style={{height, width, alignItems: 'flex-start'}}>
<Svg
width={width}
height={height}
viewBox='0 0 72 47'
>
<Path
d='M58.464,19.072c0,-5.181 -1.773,-9.599 -5.316,-13.249c-3.545,-3.649 -7.854,-5.474 -12.932,-5.474c-3.597,0 -6.902,0.979 -9.917,2.935c-3.014,1.959 -5.263,4.523 -6.743,7.696c-1.483,-0.739 -2.856,-1.111 -4.126,-1.111c-2.328,0 -4.363,0.769 -6.109,2.301c-1.745,1.535 -2.831,3.466 -3.252,5.792c-2.856,0.952 -5.185,2.672 -6.982,5.156c-1.8,2.487 -2.697,5.316 -2.697,8.489c0,3.915 1.4,7.299 4.204,10.155c2.802,2.857 6.161,4.285 10.076,4.285l43.794,0c3.595,0 6.664,-1.295 9.203,-3.888c2.538,-2.591 3.808,-5.685 3.808,-9.282c0,-3.702 -1.27,-6.848 -3.808,-9.441c-2.539,-2.591 -5.608,-3.888 -9.203,-3.888l0,-0.476Zm-31.294,16.424l17.17,0c-0.842,-1.62 -2.02,-2.92 -3.535,-3.898c-1.515,-0.977 -3.198,-1.467 -5.05,-1.467c-1.852,0 -3.535,0.49 -5.05,1.467c-1.515,0.978 -2.693,2.278 -3.535,3.898l0,0Zm17.338,-12.407c0,-0.782 -0.252,-1.411 -0.757,-1.886c-0.505,-0.474 -1.124,-0.713 -1.852,-0.713c-0.73,0 -1.347,0.239 -1.852,0.713c-0.505,0.475 -0.757,1.104 -0.757,1.886c0,0.783 0.252,1.412 0.757,1.886c0.505,0.476 1.122,0.713 1.852,0.713c0.728,0 1.347,-0.237 1.852,-0.713c0.505,-0.474 0.757,-1.103 0.757,-1.886Zm-12.288,0c0,-0.782 -0.253,-1.411 -0.758,-1.886c-0.505,-0.474 -1.123,-0.713 -1.851,-0.713c-0.73,0 -1.347,0.239 -1.852,0.713c-0.505,0.475 -0.757,1.104 -0.757,1.886c0,0.783 0.252,1.412 0.757,1.886c0.505,0.476 1.122,0.713 1.852,0.713c0.728,0 1.346,-0.237 1.851,-0.713c0.505,-0.474 0.758,-1.103 0.758,-1.886Z'
fillRule='evenodd'
strokeLinejoin='round'
fill={color}
/>
</Svg>
</View>
);
}
}

View File

@@ -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 (
<View style={style.container}>
<Cloud
color={changeOpacity(theme.centerChannelColor, 0.15)}
height={76}
width={76}
/>
<FormattedText
id='mobile.failed_network_action.title'
defaultMessage='No internet connection'
style={style.title}
/>
<FormattedText
id='mobile.failed_network_action.description'
defaultMessage='There seems to be a problem with your internet connection. Make sure you have an active connection and try again.'
style={style.description}
/>
{onRetry &&
<TouchableOpacity
onPress={onRetry}
style={style.retryContainer}
>
<FormattedText
id='mobile.failed_network_action.retry'
defaultMessage='Try Again'
style={style.retry}
/>
</TouchableOpacity>
}
</View>
);
}
}
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',
},
};
});

View File

@@ -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 = (
<IonIcon
size={76}
color={changeOpacity(theme.centerChannelColor, 0.4)}
name={iconName}
/>
);
}
return (
<View style={style.container}>
{icon}
<Text style={style.title}>{title}</Text>
{description &&
<Text style={style.description}>{description}</Text>
}
</View>
);
}
}
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',
},
};
});

View File

@@ -266,7 +266,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
height: 30,
height: 35,
minWidth: 40,
paddingVertical: 10,
},

View File

@@ -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 (
<View style={[style.separatorContainer, style.postsSeparator]}>
<View style={style.separator}/>
</View>
);
}
}
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,
},
};
});

View File

@@ -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 (
<UserStatus
@@ -282,10 +295,34 @@ export default class SettingsDrawer extends PureComponent {
<DrawerItem
labelComponent={this.renderUserStatusLabel(currentUser.id)}
leftComponent={this.renderUserStatusIcon(currentUser.id)}
separator={true}
separator={false}
onPress={this.handleSetStatus}
theme={theme}
/>
</View>
<View style={style.separator}/>
<View style={style.block}>
<DrawerItem
defaultMessage='Recent Mentions'
i18nId='search_header.title2'
iconName='ios-at-outline'
iconType='ion'
onPress={this.goToMentions}
separator={true}
theme={theme}
/>
<DrawerItem
defaultMessage='Flagged Posts'
i18nId='search_header.title3'
iconName='ios-flag-outline'
iconType='ion'
onPress={this.goToFlagged}
separator={false}
theme={theme}
/>
</View>
<View style={style.separator}/>
<View style={style.block}>
<DrawerItem
defaultMessage='Settings'
i18nId='mobile.routes.settings'

View File

@@ -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,
StyleSheet,
SafeAreaView,
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 FlaggedPosts extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
clearSearch: PropTypes.func.isRequired,
loadChannelsByTeamName: PropTypes.func.isRequired,
loadThreadIfNecessary: PropTypes.func.isRequired,
getFlaggedPosts: 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.getFlaggedPosts();
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 (
<NoResults
description={formatMessage({
id: 'mobile.flagged_posts.empty_description',
defaultMessage: 'Flags are a way to mark messages for follow up. Your flags are personal, and cannot be seen by other users.',
})}
iconName='ios-flag-outline'
title={formatMessage({id: 'mobile.flagged_posts.empty_title', defaultMessage: 'No Flagged Posts'})}
theme={theme}
/>
);
};
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 (
<DateHeader
date={date}
index={index}
/>
);
}
let separator;
const nextPost = postIds[index + 1];
if (nextPost && nextPost.indexOf(DATE_LINE) === -1) {
separator = <PostSeparator theme={theme}/>;
}
return (
<View>
<ChannelDisplayName postId={item}/>
<SearchResultPost
postId={item}
previewPost={this.previewPost}
goToThread={this.goToThread}
navigator={this.props.navigator}
onPermalinkPress={this.handlePermalinkPress}
managedConfig={managedConfig}
showFullDate={false}
/>
{separator}
</View>
);
};
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 = (
<FailedNetworkAction
onRetry={this.retry}
theme={theme}
/>
);
} else if (isLoading) {
component = (
<ChannelLoader channelIsLoading={true}/>
);
} else if (postIds.length) {
component = (
<FlatList
ref='list'
contentContainerStyle={style.sectionList}
data={postIds}
keyExtractor={this.keyExtractor}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
renderItem={this.renderPost}
/>
);
} else {
component = this.renderEmpty();
}
return (
<SafeAreaView style={style.container}>
<View style={style.container}>
<StatusBar/>
{component}
</View>
</SafeAreaView>
);
}
}
const style = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 (
<NoResults
description={formatMessage({
id: 'mobile.recent_mentions.empty_description',
defaultMessage: 'Messages containing your username and other words that trigger mentions will appear here.',
})}
iconName='ios-at-outline'
title={formatMessage({id: 'mobile.recent_mentions.empty_title', defaultMessage: 'No Recent Mentions'})}
theme={theme}
/>
);
};
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 (
<DateHeader
date={date}
index={index}
/>
);
}
let separator;
const nextPost = postIds[index + 1];
if (nextPost && nextPost.indexOf(DATE_LINE) === -1) {
separator = <PostSeparator theme={theme}/>;
}
return (
<View>
<ChannelDisplayName postId={item}/>
<SearchResultPost
postId={item}
previewPost={this.previewPost}
goToThread={this.goToThread}
navigator={this.props.navigator}
onPermalinkPress={this.handlePermalinkPress}
managedConfig={managedConfig}
showFullDate={false}
/>
{separator}
</View>
);
};
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 = (
<FailedNetworkAction
onRetry={this.retry}
theme={theme}
/>
);
} else if (isLoading) {
component = (
<ChannelLoader channelIsLoading={true}/>
);
} else if (postIds.length) {
component = (
<FlatList
ref='list'
contentContainerStyle={style.sectionList}
data={postIds}
keyExtractor={this.keyExtractor}
keyboardShouldPersistTaps='always'
keyboardDismissMode='interactive'
renderItem={this.renderPost}
/>
);
} else {
component = this.renderEmpty();
}
return (
<SafeAreaView style={style.container}>
<View style={style.container}>
<StatusBar/>
{component}
</View>
</SafeAreaView>
);
}
}
const style = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -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);

View File

@@ -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 (
<DateHeader
date={date}
index={index}
/>
);
};
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,
});
}

View File

@@ -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}
/>
);

View File

@@ -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;
}
);
}

View File

@@ -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,

View File

@@ -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",

View File

@@ -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"