Add pinned post support (#2364)

* Add pinned post

* Update pinned posts assets
This commit is contained in:
Elias Nahum 2018-11-23 12:57:28 -03:00 committed by GitHub
parent 24db914fd4
commit f42fedf889
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 519 additions and 70 deletions

View file

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

View file

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

View file

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

View file

@ -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() {

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB