mattermost-mobile/app/screens/post_options/post_options.js
Anurag Shivarathri fc929891ea
MM-37485 MM-37602 MM-37610 MM-37684 MM-37719 MM-37522 MM-37720 App crash, Sync issue fixes (#5612)
Summary
Fixes app crash on opening channel header
Fixes app crash on opening permalink from permalink screen
Fixes showing duplicates channels while searching in "Jump to..."
Fixes auto following the thread on opening the thread view when there are unreads
Fixes updation of threads state (Read/Unread/Deleted) of already loaded threads
Fixes counter showing as blank due to threads sync issue
Doesn't show Follow Thread/Message when CRT is not enabled

Ticket Link
https://mattermost.atlassian.net/browse/MM-37485
https://mattermost.atlassian.net/browse/MM-37602
https://mattermost.atlassian.net/browse/MM-37610
https://mattermost.atlassian.net/browse/MM-37684
https://mattermost.atlassian.net/browse/MM-37719
https://mattermost.atlassian.net/browse/MM-37522
https://mattermost.atlassian.net/browse/MM-37720

Device Information
This PR was tested on: iOS 14.5
2021-08-11 15:56:38 +05:30

494 lines
15 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Clipboard from '@react-native-community/clipboard';
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
import {intlShape} from 'react-intl';
import {Alert, StyleSheet, View} from 'react-native';
import {showModal, dismissModal} from '@actions/navigation';
import CompassIcon from '@components/compass_icon';
import ReactionPicker from '@components/reaction_picker';
import SlideUpPanel from '@components/slide_up_panel';
import {BOTTOM_MARGIN} from '@components/slide_up_panel/slide_up_panel';
import {REACTION_PICKER_HEIGHT} from '@constants/reaction_picker';
import {CHANNEL} from '@constants/screen';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {isSystemMessage} from '@mm-redux/utils/post_utils';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
import Bindings from './bindings';
import PostOption from './post_option';
import {OPTION_HEIGHT, getInitialPosition} from './post_options_utils';
export default class PostOptions extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
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,
setThreadFollow: PropTypes.func.isRequired,
setUnreadPost: PropTypes.func.isRequired,
}).isRequired,
canAddReaction: PropTypes.bool,
canReply: PropTypes.bool,
canCopyPermalink: PropTypes.bool,
canCopyText: PropTypes.bool,
canDelete: PropTypes.bool,
canFlag: PropTypes.bool,
canPin: PropTypes.bool,
canEdit: PropTypes.bool,
canMarkAsUnread: PropTypes.bool,
canEditUntil: PropTypes.number.isRequired,
currentTeamUrl: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
deviceHeight: PropTypes.number.isRequired,
isFlagged: PropTypes.bool,
location: PropTypes.string,
post: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
thread: PropTypes.object,
};
static contextTypes = {
intl: intlShape.isRequired,
};
close = async (cb) => {
await dismissModal();
if (typeof cb === 'function') {
requestAnimationFrame(cb);
}
};
closeWithAnimation = (cb) => {
if (this.slideUpPanel) {
this.slideUpPanel.closeWithAnimation(cb);
} else {
this.close(cb);
}
};
getOption = (key, icon, message, onPress, destructive = false) => {
const {formatMessage} = this.context.intl;
const {theme} = this.props;
const testID = `post.options.${key}.action`;
return (
<PostOption
testID={testID}
key={key}
icon={icon}
text={formatMessage(message)}
onPress={onPress}
destructive={destructive}
theme={theme}
/>
);
}
getReplyOption = () => {
const {canReply} = this.props;
if (canReply) {
const key = 'reply';
const icon = 'reply-outline';
const message = {id: t('mobile.post_info.reply'), defaultMessage: 'Reply'};
const onPress = this.handleReply;
return this.getOption(key, icon, message, onPress);
}
return null;
}
getFollowThreadOption = () => {
const {location, thread} = this.props;
if (location !== CHANNEL || !thread) {
return null;
}
const key = 'follow';
let icon;
let message;
if (thread.is_following) {
icon = 'message-minus-outline';
if (thread?.participants?.length) {
message = {id: t('threads.unfollowThread'), defaultMessage: 'Unfollow Thread'};
} else {
message = {id: t('threads.unfollowMessage'), defaultMessage: 'Unfollow Message'};
}
} else {
icon = 'message-plus-outline';
if (thread?.participants?.length) {
message = {id: t('threads.followThread'), defaultMessage: 'Follow Thread'};
} else {
message = {id: t('threads.followMessage'), defaultMessage: 'Follow Message'};
}
}
const onPress = this.handleToggleFollow;
return this.getOption(key, icon, message, onPress);
}
getCopyPermalink = () => {
const {canCopyPermalink} = this.props;
if (canCopyPermalink) {
const key = 'permalink';
const icon = 'link-variant';
const message = {id: t('get_post_link_modal.title'), defaultMessage: 'Copy Link'};
const onPress = this.handleCopyPermalink;
return this.getOption(key, icon, message, onPress);
}
return null;
};
getCopyText = () => {
const {canCopyText} = this.props;
if (canCopyText) {
const key = 'copy';
const icon = 'content-copy';
const message = {id: t('mobile.post_info.copy_text'), defaultMessage: 'Copy Text'};
const onPress = this.handleCopyText;
return this.getOption(key, icon, message, onPress);
}
return null;
};
getDeleteOption = () => {
const {canDelete} = this.props;
if (canDelete) {
const key = 'delete';
const icon = 'trash-can-outline';
const message = {id: t('post_info.del'), defaultMessage: 'Delete'};
const onPress = this.handlePostDelete;
const destructive = true;
return this.getOption(key, icon, message, onPress, destructive);
}
return null;
};
getEditOption = () => {
const {canEdit, canEditUntil} = this.props;
if (canEdit && (canEditUntil === -1 || canEditUntil > Date.now())) {
const key = 'edit';
const icon = 'pencil-outline';
const message = {id: t('post_info.edit'), defaultMessage: 'Edit'};
const onPress = this.handlePostEdit;
return this.getOption(key, icon, message, onPress);
}
return null;
};
getFlagOption = () => {
const {canFlag, isFlagged} = this.props;
if (!canFlag) {
return null;
}
let key;
let message;
let onPress;
const icon = 'bookmark-outline';
if (isFlagged) {
key = 'unflag';
message = {id: t('mobile.post_info.unflag'), defaultMessage: 'Unsave'};
onPress = this.handleUnflagPost;
} else {
key = 'flagged';
message = {id: t('mobile.post_info.flag'), defaultMessage: 'Save'};
onPress = this.handleFlagPost;
}
return this.getOption(key, icon, message, onPress);
};
getPinOption = () => {
const {canPin, post} = this.props;
if (!canPin) {
return null;
}
let key;
let message;
let onPress;
const icon = 'pin-outline';
if (post.is_pinned) {
key = 'unpin';
message = {id: t('mobile.post_info.unpin'), defaultMessage: 'Unpin from Channel'};
onPress = this.handleUnpinPost;
} else {
key = 'pin';
message = {id: t('mobile.post_info.pin'), defaultMessage: 'Pin to Channel'};
onPress = this.handlePinPost;
}
return this.getOption(key, icon, message, onPress);
};
getMarkAsUnreadOption = () => {
const {post, theme} = this.props;
const {formatMessage} = this.context.intl;
if (!isSystemMessage(post) && this.props.canMarkAsUnread) {
return (
<PostOption
testID='post.options.markUnread.action'
key='markUnread'
icon='mark-as-unread'
text={formatMessage({id: 'mobile.post_info.mark_unread', defaultMessage: 'Mark as Unread'})}
onPress={this.handleMarkUnread}
theme={theme}
/>
);
}
return null;
};
getAppsOptions = () => {
const {post} = this.props;
return (
<Bindings
key='bindings'
post={post}
closeWithAnimation={this.closeWithAnimation}
/>
);
}
getPostOptions = () => {
const actions = [
this.getReplyOption(),
this.getFollowThreadOption(),
this.getMarkAsUnreadOption(),
this.getCopyPermalink(),
this.getFlagOption(),
this.getCopyText(),
this.getPinOption(),
this.getEditOption(),
this.getDeleteOption(),
this.getAppsOptions(),
];
return actions.filter((a) => a !== null);
};
handleAddReactionScreen = () => {
const {theme} = this.props;
const {formatMessage} = this.context.intl;
CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor).then((source) => {
const screen = 'AddReaction';
const title = formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'});
const passProps = {
closeButton: source,
onEmojiPress: this.handleAddReactionToPost,
};
this.closeWithAnimation(() => showModal(screen, title, passProps));
});
};
handleReply = () => {
const {post} = this.props;
this.closeWithAnimation(() => {
EventEmitter.emit('goToThread', post);
});
};
handleToggleFollow = () => {
const {actions, currentUserId, thread} = this.props;
actions.setThreadFollow(currentUserId, thread.id, !thread.is_following);
this.closeWithAnimation();
};
handleAddReaction = preventDoubleTap((emoji) => {
this.handleAddReactionToPost(emoji);
this.closeWithAnimation();
}, 500);
handleAddReactionToPost = (emoji) => {
const {actions, post} = this.props;
actions.addReaction(post.id, emoji);
};
handleCopyPermalink = () => {
const {currentTeamUrl, post} = this.props;
const permalink = `${currentTeamUrl}/pl/${post.id}`;
Clipboard.setString(permalink);
this.closeWithAnimation();
};
handleCopyText = () => {
const {message} = this.props.post;
Clipboard.setString(message);
this.closeWithAnimation();
};
handleFlagPost = () => {
const {actions, post} = this.props;
this.closeWithAnimation();
requestAnimationFrame(() => {
actions.flagPost(post.id);
});
};
handlePinPost = () => {
const {actions, post} = this.props;
this.closeWithAnimation();
requestAnimationFrame(() => {
actions.pinPost(post.id);
});
};
handleMarkUnread = () => {
const {actions, currentUserId, location, post} = this.props;
this.closeWithAnimation();
requestAnimationFrame(() => {
actions.setUnreadPost(currentUserId, post.id, location);
});
}
handlePostDelete = () => {
const {formatMessage} = this.context.intl;
const {actions, post} = this.props;
Alert.alert(
formatMessage({id: 'mobile.post.delete_title', defaultMessage: 'Delete Post'}),
formatMessage({
id: 'mobile.post.delete_question',
defaultMessage: 'Are you sure you want to delete this post?',
}),
[{
text: formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
style: 'cancel',
}, {
text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}),
style: 'destructive',
onPress: () => {
this.closeWithAnimation(() => {
actions.deletePost(post);
actions.removePost(post);
});
},
}],
);
};
handlePostEdit = () => {
const {theme, post} = this.props;
const {intl} = this.context;
CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor).then((source) => {
const screen = 'EditPost';
const title = intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'});
const passProps = {
post,
closeButton: source,
};
this.closeWithAnimation(() => showModal(screen, title, passProps, {modal: {swipeToDismiss: false}}));
});
};
handleUnflagPost = () => {
const {actions, post} = this.props;
this.closeWithAnimation();
requestAnimationFrame(() => {
actions.unflagPost(post.id);
});
};
handleUnpinPost = () => {
const {actions, post} = this.props;
this.closeWithAnimation();
requestAnimationFrame(() => {
actions.unpinPost(post.id);
});
};
refSlideUpPanel = (r) => {
this.slideUpPanel = r;
};
render() {
const {deviceHeight, theme, canAddReaction} = this.props;
const options = this.getPostOptions();
let reactionHeight = 0;
let reactionPicker;
if (!options || !options.length) {
return null;
}
if (canAddReaction) {
reactionHeight = REACTION_PICKER_HEIGHT;
reactionPicker = (
<ReactionPicker
testID='post_options.reaction_picker.action'
addReaction={this.handleAddReaction}
openReactionScreen={this.handleAddReactionScreen}
/>
);
}
const marginFromTop = deviceHeight - BOTTOM_MARGIN - ((options.length + 1) * OPTION_HEIGHT) - reactionHeight;
const initialPosition = getInitialPosition(deviceHeight, marginFromTop);
return (
<View
testID='post.options'
style={style.container}
>
<SlideUpPanel
allowStayMiddle={false}
ref={this.refSlideUpPanel}
marginFromTop={marginFromTop > 0 ? marginFromTop : 0}
onRequestClose={this.close}
initialPosition={initialPosition}
key={marginFromTop}
theme={theme}
>
{reactionPicker}
{options}
</SlideUpPanel>
</View>
);
}
}
const style = StyleSheet.create({
container: {
flex: 1,
},
});