Add pinned post support (#2364)
* Add pinned post * Update pinned posts assets
This commit is contained in:
parent
24db914fd4
commit
f42fedf889
18 changed files with 519 additions and 70 deletions
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text, View} from 'react-native';
|
||||
import {Image, Text, View} from 'react-native';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
|
@ -12,6 +12,7 @@ export default class NoResults extends PureComponent {
|
|||
static propTypes = {
|
||||
description: PropTypes.string,
|
||||
iconName: PropTypes.string,
|
||||
image: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
@ -20,16 +21,24 @@ export default class NoResults extends PureComponent {
|
|||
const {
|
||||
description,
|
||||
iconName,
|
||||
image,
|
||||
theme,
|
||||
title,
|
||||
} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
let icon;
|
||||
if (iconName) {
|
||||
if (image) {
|
||||
icon = (
|
||||
<Image
|
||||
source={image}
|
||||
style={{width: 37, height: 37, tintColor: changeOpacity(theme.centerChannelColor, 0.5)}}
|
||||
/>
|
||||
);
|
||||
} else if (iconName) {
|
||||
icon = (
|
||||
<IonIcon
|
||||
size={76}
|
||||
size={44}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.4)}
|
||||
name={iconName}
|
||||
/>
|
||||
|
|
@ -60,7 +69,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginBottom: 15,
|
||||
marginVertical: 15,
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default class PostListBase extends PureComponent {
|
|||
onPostPress: PropTypes.func,
|
||||
onRefresh: PropTypes.func,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
refreshing: PropTypes.bool.isRequired,
|
||||
refreshing: PropTypes.bool,
|
||||
renderFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
|
||||
renderReplies: PropTypes.bool,
|
||||
serverURL: PropTypes.string.isRequired,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const CONTAINER_MARGIN = TOP_MARGIN - 10;
|
|||
|
||||
export default class SlideUpPanel extends PureComponent {
|
||||
static propTypes = {
|
||||
allowStayMiddle: PropTypes.bool,
|
||||
alwaysCaptureContainerMove: PropTypes.bool,
|
||||
containerHeight: PropTypes.number,
|
||||
children: PropTypes.oneOfType([
|
||||
|
|
@ -36,6 +37,7 @@ export default class SlideUpPanel extends PureComponent {
|
|||
};
|
||||
|
||||
static defaultProps = {
|
||||
allowStayMiddle: true,
|
||||
headerHeight: 0,
|
||||
initialPosition: 0.5,
|
||||
marginFromTop: TOP_MARGIN,
|
||||
|
|
@ -156,14 +158,14 @@ export default class SlideUpPanel extends PureComponent {
|
|||
};
|
||||
|
||||
startAnimation = (initialY, positionY, isGoingDown, initial = false) => {
|
||||
const {containerHeight, onRequestClose} = this.props;
|
||||
const {allowStayMiddle, containerHeight, onRequestClose} = this.props;
|
||||
const {finalPosition, initialPosition} = this.state;
|
||||
const position = new Animated.Value(initial ? initialY : positionY);
|
||||
let endPosition = (!isGoingDown && !initial ? finalPosition : positionY);
|
||||
|
||||
position.removeAllListeners();
|
||||
if (isGoingDown) {
|
||||
if (positionY <= this.state.initialPosition) {
|
||||
if (positionY <= this.state.initialPosition && allowStayMiddle) {
|
||||
endPosition = initialPosition;
|
||||
} else {
|
||||
endPosition = containerHeight;
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ import {
|
|||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import pinIcon from 'assets/images/channel_info/pin.png';
|
||||
|
||||
import ChannelInfoHeader from './channel_info_header';
|
||||
import ChannelInfoRow from './channel_info_row';
|
||||
|
|
@ -141,6 +142,29 @@ export default class ChannelInfo extends PureComponent {
|
|||
});
|
||||
});
|
||||
|
||||
goToPinnedPosts = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {currentChannel, navigator, theme} = this.props;
|
||||
const id = t('channel_header.pinnedPosts');
|
||||
const defaultMessage = 'Pinned Posts';
|
||||
|
||||
navigator.push({
|
||||
backButtonTitle: '',
|
||||
screen: 'PinnedPosts',
|
||||
title: formatMessage({id, defaultMessage}),
|
||||
animated: true,
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
passProps: {
|
||||
currentChannelId: currentChannel.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
handleChannelEdit = preventDoubleTap(() => {
|
||||
const {intl} = this.context;
|
||||
const {navigator, theme} = this.props;
|
||||
|
|
@ -363,41 +387,50 @@ export default class ChannelInfo extends PureComponent {
|
|||
</View>);
|
||||
}
|
||||
|
||||
return (<View>
|
||||
<ChannelInfoRow
|
||||
action={this.handleFavorite}
|
||||
defaultMessage='Favorite'
|
||||
detail={this.state.isFavorite}
|
||||
icon='star-o'
|
||||
textId={t('mobile.routes.channelInfo.favorite')}
|
||||
togglable={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
action={this.handleMuteChannel}
|
||||
defaultMessage='Mute channel'
|
||||
detail={this.state.isMuted}
|
||||
icon='bell-slash-o'
|
||||
textId={t('channel_notifications.muteChannel.settings')}
|
||||
togglable={true}
|
||||
theme={theme}
|
||||
/>
|
||||
{
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ChannelInfoRow
|
||||
action={this.handleFavorite}
|
||||
defaultMessage='Favorite'
|
||||
detail={this.state.isFavorite}
|
||||
icon='star-o'
|
||||
textId={t('mobile.routes.channelInfo.favorite')}
|
||||
togglable={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
action={this.handleMuteChannel}
|
||||
defaultMessage='Mute channel'
|
||||
detail={this.state.isMuted}
|
||||
icon='bell-slash-o'
|
||||
textId={t('channel_notifications.muteChannel.settings')}
|
||||
togglable={true}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
action={this.goToPinnedPosts}
|
||||
defaultMessage='Pinned Posts'
|
||||
image={pinIcon}
|
||||
textId={t('channel_header.pinnedPosts')}
|
||||
theme={theme}
|
||||
/>
|
||||
{
|
||||
|
||||
/**
|
||||
<ChannelInfoRow
|
||||
action={() => true}
|
||||
defaultMessage='Notification Preferences'
|
||||
icon='bell-o'
|
||||
textId='channel_header.notificationPreferences'
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.separator}/>
|
||||
**/
|
||||
}
|
||||
{this.renderViewOrManageMembersRow() &&
|
||||
<View>
|
||||
/**
|
||||
<ChannelInfoRow
|
||||
action={() => true}
|
||||
defaultMessage='Notification Preferences'
|
||||
icon='bell-o'
|
||||
textId='channel_header.notificationPreferences'
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={style.separator}/>
|
||||
**/
|
||||
}
|
||||
{this.renderViewOrManageMembersRow() &&
|
||||
<React.Fragment>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
action={this.goToChannelMembers}
|
||||
|
|
@ -407,10 +440,10 @@ export default class ChannelInfo extends PureComponent {
|
|||
textId={canManageUsers ? t('channel_header.manageMembers') : t('channel_header.viewMembers')}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
{canManageUsers &&
|
||||
<View>
|
||||
</React.Fragment>
|
||||
}
|
||||
{canManageUsers &&
|
||||
<React.Fragment>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
action={this.goToChannelAddMembers}
|
||||
|
|
@ -419,22 +452,23 @@ export default class ChannelInfo extends PureComponent {
|
|||
textId={t('channel_header.addMembers')}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
{canEditChannel && (
|
||||
<View>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
action={this.handleChannelEdit}
|
||||
defaultMessage='Edit Channel'
|
||||
icon='edit'
|
||||
textId={t('mobile.channel_info.edit')}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<View style={style.separator}/>
|
||||
</View>);
|
||||
</React.Fragment>
|
||||
}
|
||||
{canEditChannel && (
|
||||
<React.Fragment>
|
||||
<View style={style.separator}/>
|
||||
<ChannelInfoRow
|
||||
action={this.handleChannelEdit}
|
||||
defaultMessage='Edit Channel'
|
||||
icon='edit'
|
||||
textId={t('mobile.channel_info.edit')}
|
||||
theme={theme}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<View style={style.separator}/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function channelInfoRow(props) {
|
|||
iconElement = (
|
||||
<Image
|
||||
source={image}
|
||||
style={{width: 15, height: 15, tintColor: imageTintColor || changeOpacity(theme.sidebarText, 0.5)}}
|
||||
style={{width: 15, height: 15, tintColor: imageTintColor || changeOpacity(theme.centerChannelColor, 0.5)}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export function registerScreens(store, Provider) {
|
|||
Navigation.registerComponent('NotificationSettingsMobile', () => wrapWithContextProvider(require('app/screens/settings/notification_settings_mobile').default), store, Provider);
|
||||
Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(require('app/screens/options_modal').default), store, Provider);
|
||||
Navigation.registerComponent('Permalink', () => wrapWithContextProvider(require('app/screens/permalink').default), store, Provider);
|
||||
Navigation.registerComponent('PinnedPosts', () => wrapWithContextProvider(require('app/screens/pinned_posts').default), store, Provider);
|
||||
Navigation.registerComponent('PostOptions', () => wrapWithContextProvider(require('app/screens/post_options').default), store, Provider);
|
||||
Navigation.registerComponent('ReactionList', () => wrapWithContextProvider(require('app/screens/reaction_list').default), store, Provider);
|
||||
Navigation.registerComponent('RecentMentions', () => wrapWithContextProvider(require('app/screens/recent_mentions').default), store, Provider);
|
||||
|
|
|
|||
51
app/screens/pinned_posts/index.js
Normal file
51
app/screens/pinned_posts/index.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// 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, getPinnedPosts} 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 {showSearchModal} from 'app/actions/views/search';
|
||||
import {makePreparePostIdsForSearchPosts} from 'app/selectors/post_list';
|
||||
|
||||
import PinnedPosts from './pinned_posts';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const preparePostIds = makePreparePostIdsForSearchPosts();
|
||||
return (state, ownProps) => {
|
||||
const {pinned} = state.entities.search;
|
||||
const channelPinnedPosts = pinned[ownProps.currentChannelId] || [];
|
||||
const postIds = preparePostIds(state, channelPinnedPosts);
|
||||
const {pinnedPosts: pinnedPostsRequest} = state.requests.search;
|
||||
const isLoading = pinnedPostsRequest.status === RequestStatus.STARTED ||
|
||||
pinnedPostsRequest.status === RequestStatus.NOT_STARTED;
|
||||
|
||||
return {
|
||||
postIds,
|
||||
isLoading,
|
||||
didFail: pinnedPostsRequest.status === RequestStatus.FAILURE,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
clearSearch,
|
||||
loadChannelsByTeamName,
|
||||
loadThreadIfNecessary,
|
||||
getPinnedPosts,
|
||||
selectFocusedPostId,
|
||||
selectPost,
|
||||
showSearchModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(PinnedPosts);
|
||||
288
app/screens/pinned_posts/pinned_posts.js
Normal file
288
app/screens/pinned_posts/pinned_posts.js
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
// 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 {isDateLine} from 'app/components/post_list/date_header/utils';
|
||||
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 {changeOpacity} from 'app/utils/theme';
|
||||
import noResultsImage from 'assets/images/no_results/pin.png';
|
||||
|
||||
export default class PinnedPosts extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
clearSearch: PropTypes.func.isRequired,
|
||||
loadChannelsByTeamName: PropTypes.func.isRequired,
|
||||
loadThreadIfNecessary: PropTypes.func.isRequired,
|
||||
getPinnedPosts: PropTypes.func.isRequired,
|
||||
selectFocusedPostId: PropTypes.func.isRequired,
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
showSearchModal: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentChannelId: PropTypes.string.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);
|
||||
|
||||
this.state = {
|
||||
managedConfig: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.listenerId = mattermostManaged.addEventListener('change', this.setManagedConfig);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {actions, currentChannelId} = this.props;
|
||||
this.setManagedConfig();
|
||||
actions.clearSearch();
|
||||
actions.getPinnedPosts(currentChannelId);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
handleHashtagPress = async (hashtag) => {
|
||||
const {actions, navigator} = this.props;
|
||||
|
||||
await navigator.dismissModal();
|
||||
|
||||
actions.showSearchModal(navigator, '#' + hashtag);
|
||||
};
|
||||
|
||||
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.pinned_posts.empty_description',
|
||||
defaultMessage: 'Pin important items by holding down on any message and selecting "Pin to Channel".',
|
||||
})}
|
||||
image={noResultsImage}
|
||||
title={formatMessage({id: 'mobile.pinned_posts.empty_title', defaultMessage: 'No Pinned Posts'})}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderPost = ({item, index}) => {
|
||||
const {postIds, theme} = this.props;
|
||||
const {managedConfig} = this.state;
|
||||
if (isDateLine(item)) {
|
||||
return (
|
||||
<DateHeader
|
||||
dateLineString={item}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let separator;
|
||||
const nextPost = postIds[index + 1];
|
||||
if (nextPost && !isDateLine(nextPost)) {
|
||||
separator = <PostSeparator theme={theme}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SearchResultPost
|
||||
postId={item}
|
||||
previewPost={this.previewPost}
|
||||
highlightPinnedOrFlagged={true}
|
||||
goToThread={this.goToThread}
|
||||
navigator={this.props.navigator}
|
||||
onHashtagPress={this.handleHashtagPress}
|
||||
onPermalinkPress={this.handlePermalinkPress}
|
||||
managedConfig={managedConfig}
|
||||
showFullDate={false}
|
||||
skipFlaggedHeader={false}
|
||||
skipPinnedHeader={true}
|
||||
/>
|
||||
{separator}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
this.showingPermalink = true;
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
retry = () => {
|
||||
const {actions, currentChannelId} = this.props;
|
||||
actions.getPinnedPosts(currentChannelId);
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -4,7 +4,14 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {deletePost, flagPost, unflagPost, removePost} from 'mattermost-redux/actions/posts';
|
||||
import {
|
||||
deletePost,
|
||||
flagPost,
|
||||
pinPost,
|
||||
unflagPost,
|
||||
unpinPost,
|
||||
removePost,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import {General, Permissions} from 'mattermost-redux/constants';
|
||||
import {getChannel, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
|
@ -72,8 +79,10 @@ function mapDispatchToProps(dispatch) {
|
|||
addReaction,
|
||||
deletePost,
|
||||
flagPost,
|
||||
pinPost,
|
||||
removePost,
|
||||
unflagPost,
|
||||
unpinPost,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import edit from 'assets/images/post_menu/edit.png';
|
|||
import emoji from 'assets/images/post_menu/emoji.png';
|
||||
import flag from 'assets/images/post_menu/flag.png';
|
||||
import link from 'assets/images/post_menu/link.png';
|
||||
import pin from 'assets/images/post_menu/pin.png';
|
||||
import trash from 'assets/images/post_menu/trash.png';
|
||||
|
||||
const icons = {
|
||||
|
|
@ -24,6 +25,7 @@ const icons = {
|
|||
emoji,
|
||||
flag,
|
||||
link,
|
||||
pin,
|
||||
trash,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ export default class PostOptions extends PureComponent {
|
|||
addReaction: PropTypes.func.isRequired,
|
||||
deletePost: PropTypes.func.isRequired,
|
||||
flagPost: PropTypes.func.isRequired,
|
||||
pinPost: PropTypes.func.isRequired,
|
||||
removePost: PropTypes.func.isRequired,
|
||||
unflagPost: PropTypes.func.isRequired,
|
||||
unpinPost: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
additionalOption: PropTypes.object,
|
||||
canAddReaction: PropTypes.bool,
|
||||
|
|
@ -159,7 +161,7 @@ export default class PostOptions extends PureComponent {
|
|||
<PostOption
|
||||
key='unflag'
|
||||
icon='flag'
|
||||
text={formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'})}
|
||||
text={formatMessage({id: 'mobile.post_info.unflag', defaultMessage: 'Unflag'})}
|
||||
onPress={this.handleUnflagPost}
|
||||
/>
|
||||
);
|
||||
|
|
@ -169,16 +171,46 @@ export default class PostOptions extends PureComponent {
|
|||
<PostOption
|
||||
key='flagged'
|
||||
icon='flag'
|
||||
text={formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'})}
|
||||
text={formatMessage({id: 'mobile.post_info.flag', defaultMessage: 'Flag'})}
|
||||
onPress={this.handleFlagPost}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getPinOption = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {channelIsReadOnly, post} = this.props;
|
||||
|
||||
if (channelIsReadOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (post.is_pinned) {
|
||||
return (
|
||||
<PostOption
|
||||
key='unpin'
|
||||
icon='pin'
|
||||
text={formatMessage({id: 'mobile.post_info.unpin', defaultMessage: 'Unpin from Channel'})}
|
||||
onPress={this.handleUnpinPost}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PostOption
|
||||
key='pin'
|
||||
icon='pin'
|
||||
text={formatMessage({id: 'mobile.post_info.pin', defaultMessage: 'Pin to Channel'})}
|
||||
onPress={this.handlePinPost}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getMyPostOptions = () => {
|
||||
const actions = [
|
||||
this.getEditOption(),
|
||||
this.getFlagOption(),
|
||||
this.getPinOption(),
|
||||
this.getAddReactionOption(),
|
||||
this.getCopyPermalink(),
|
||||
this.getCopyText(),
|
||||
|
|
@ -192,6 +224,7 @@ export default class PostOptions extends PureComponent {
|
|||
const actions = [
|
||||
this.getFlagOption(),
|
||||
this.getAddReactionOption(),
|
||||
this.getPinOption(),
|
||||
this.getCopyPermalink(),
|
||||
this.getCopyText(),
|
||||
this.getEditOption(),
|
||||
|
|
@ -265,6 +298,13 @@ export default class PostOptions extends PureComponent {
|
|||
this.closeWithAnimation();
|
||||
};
|
||||
|
||||
handlePinPost = () => {
|
||||
const {actions, post} = this.props;
|
||||
|
||||
actions.pinPost(post.id);
|
||||
this.closeWithAnimation();
|
||||
};
|
||||
|
||||
handlePostDelete = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {actions, isMyPost, post} = this.props;
|
||||
|
|
@ -325,6 +365,13 @@ export default class PostOptions extends PureComponent {
|
|||
this.closeWithAnimation();
|
||||
};
|
||||
|
||||
handleUnpinPost = () => {
|
||||
const {actions, post} = this.props;
|
||||
|
||||
actions.unpinPost(post.id);
|
||||
this.closeWithAnimation();
|
||||
};
|
||||
|
||||
refSlideUpPanel = (r) => {
|
||||
this.slideUpPanel = r;
|
||||
};
|
||||
|
|
@ -338,6 +385,7 @@ export default class PostOptions extends PureComponent {
|
|||
return (
|
||||
<View style={style.flex}>
|
||||
<SlideUpPanel
|
||||
allowStayMiddle={false}
|
||||
alwaysCaptureContainerMove={true}
|
||||
ref={this.refSlideUpPanel}
|
||||
marginFromTop={marginFromTop}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"center_panel.archived.closeChannel": "Close Channel",
|
||||
"channel_header.addMembers": "Add Members",
|
||||
"channel_header.directchannel.you": "{displayname} (you) ",
|
||||
"channel_header.pinnedPosts": "Pinned Posts",
|
||||
"channel_header.manageMembers": "Manage Members",
|
||||
"channel_header.viewMembers": "View Members",
|
||||
"channel_info.header": "Header:",
|
||||
|
|
@ -329,8 +330,14 @@
|
|||
"mobile.open_dm.error": "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
|
||||
"mobile.open_gm.error": "We couldn't open a group message with those users. Please check your connection and try again.",
|
||||
"mobile.open_unknown_channel.error": "Unable to join the channel. Please reset the cache and try again.",
|
||||
"mobile.pinned_posts.empty_description": "Pin important items by holding down on any message and selecting \"Pin to Channel\".",
|
||||
"mobile.pinned_posts.empty_title": "No Pinned Posts",
|
||||
"mobile.post_info.add_reaction": "Add Reaction",
|
||||
"mobile.post_info.copy_text": "Copy Text",
|
||||
"mobile.post_info.flag": "Flag",
|
||||
"mobile.post_info.unflag": "Unflag",
|
||||
"mobile.post_info.pin": "Pin to Channel",
|
||||
"mobile.post_info.unpin": "Unpin from Channel",
|
||||
"mobile.post_pre_header.flagged": "Flagged",
|
||||
"mobile.post_pre_header.pinned_flagged": "Pinned and Flagged",
|
||||
"mobile.post_pre_header.pinned": "Pinned",
|
||||
|
|
@ -467,8 +474,6 @@
|
|||
"post_info.edit": "Edit",
|
||||
"post_info.message.show_less": "Show less",
|
||||
"post_info.message.show_more": "Show more",
|
||||
"post_info.mobile.flag": "Flag",
|
||||
"post_info.mobile.unflag": "Unflag",
|
||||
"post_info.system": "System",
|
||||
"post_message_view.edited": "(edited)",
|
||||
"posts_view.newMsg": "New Messages",
|
||||
|
|
|
|||
BIN
assets/base/images/channel_info/pin.png
Normal file
BIN
assets/base/images/channel_info/pin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 328 B |
BIN
assets/base/images/channel_info/pin@2x.png
Normal file
BIN
assets/base/images/channel_info/pin@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 900 B |
BIN
assets/base/images/channel_info/pin@3x.png
Normal file
BIN
assets/base/images/channel_info/pin@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/base/images/no_results/pin.png
Normal file
BIN
assets/base/images/no_results/pin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 907 B |
BIN
assets/base/images/no_results/pin@2x.png
Normal file
BIN
assets/base/images/no_results/pin@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/base/images/no_results/pin@3x.png
Normal file
BIN
assets/base/images/no_results/pin@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Loading…
Reference in a new issue