From ae629a455ccfdc41b1ec44e649488163cab98806 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 14 Nov 2018 17:29:09 -0300 Subject: [PATCH] Update UX for post long press menu (#2325) * Update UX for post long press menu * Post menu feedback review --- NOTICE.txt | 35 -- app/components/at_mention/at_mention.js | 39 +- .../file_attachment_list/file_attachment.js | 14 +- .../file_attachment_document.js | 12 +- .../file_attachment_list.js | 28 +- app/components/formatted_markdown_text.js | 3 +- app/components/markdown/markdown.js | 57 ++- .../markdown_code_block.js | 23 +- .../markdown/markdown_image/markdown_image.js | 19 +- .../markdown/markdown_link/markdown_link.js | 31 +- app/components/message_attachments/index.js | 3 - .../message_attachments/message_attachment.js | 4 - app/components/options_context/index.js | 6 - .../options_context.android.js | 71 ---- .../options_context/options_context.ios.js | 108 ------ app/components/post/index.js | 41 +- app/components/post/post.js | 138 +------ .../post_add_channel_member.js | 5 +- app/components/post_body/index.js | 47 +-- app/components/post_body/post_body.js | 186 +++------ .../post_body_additional_content.js | 7 - app/components/reactions/reactions.js | 38 +- .../slide_up_panel/slide_up_panel.js | 21 +- app/screens/index.js | 1 + app/screens/long_post/long_post.js | 6 +- app/screens/post_options/index.js | 81 ++++ app/screens/post_options/post_option.js | 106 ++++++ app/screens/post_options/post_options.js | 358 ++++++++++++++++++ app/screens/reaction_list/reaction_list.js | 1 - .../bottom_sheet/bottom_sheet.android.js | 5 + app/utils/bottom_sheet/bottom_sheet.ios.js | 14 + app/utils/bottom_sheet/index.js | 5 + assets/base/i18n/en.json | 2 +- assets/base/images/post_menu/copy.png | Bin 0 -> 293 bytes assets/base/images/post_menu/copy@2x.png | Bin 0 -> 493 bytes assets/base/images/post_menu/copy@3x.png | Bin 0 -> 719 bytes assets/base/images/post_menu/edit.png | Bin 0 -> 399 bytes assets/base/images/post_menu/edit@2x.png | Bin 0 -> 851 bytes assets/base/images/post_menu/edit@3x.png | Bin 0 -> 1278 bytes assets/base/images/post_menu/emoji.png | Bin 0 -> 400 bytes assets/base/images/post_menu/emoji@2x.png | Bin 0 -> 883 bytes assets/base/images/post_menu/emoji@3x.png | Bin 0 -> 1558 bytes assets/base/images/post_menu/flag.png | Bin 0 -> 365 bytes assets/base/images/post_menu/flag@2x.png | Bin 0 -> 612 bytes assets/base/images/post_menu/flag@3x.png | Bin 0 -> 878 bytes assets/base/images/post_menu/link.png | Bin 0 -> 441 bytes assets/base/images/post_menu/link@2x.png | Bin 0 -> 945 bytes assets/base/images/post_menu/link@3x.png | Bin 0 -> 1471 bytes assets/base/images/post_menu/pin.png | Bin 0 -> 448 bytes assets/base/images/post_menu/pin@2x.png | Bin 0 -> 920 bytes assets/base/images/post_menu/pin@3x.png | Bin 0 -> 1414 bytes assets/base/images/post_menu/trash.png | Bin 0 -> 511 bytes assets/base/images/post_menu/trash@2x.png | Bin 0 -> 952 bytes assets/base/images/post_menu/trash@3x.png | Bin 0 -> 1435 bytes ios/Mattermost.xcodeproj/project.pbxproj | 94 ++--- package-lock.json | 5 - package.json | 1 - 57 files changed, 845 insertions(+), 770 deletions(-) delete mode 100644 app/components/options_context/index.js delete mode 100644 app/components/options_context/options_context.android.js delete mode 100644 app/components/options_context/options_context.ios.js create mode 100644 app/screens/post_options/index.js create mode 100644 app/screens/post_options/post_option.js create mode 100644 app/screens/post_options/post_options.js create mode 100644 app/utils/bottom_sheet/bottom_sheet.android.js create mode 100644 app/utils/bottom_sheet/bottom_sheet.ios.js create mode 100644 app/utils/bottom_sheet/index.js create mode 100755 assets/base/images/post_menu/copy.png create mode 100755 assets/base/images/post_menu/copy@2x.png create mode 100755 assets/base/images/post_menu/copy@3x.png create mode 100755 assets/base/images/post_menu/edit.png create mode 100755 assets/base/images/post_menu/edit@2x.png create mode 100755 assets/base/images/post_menu/edit@3x.png create mode 100755 assets/base/images/post_menu/emoji.png create mode 100755 assets/base/images/post_menu/emoji@2x.png create mode 100755 assets/base/images/post_menu/emoji@3x.png create mode 100755 assets/base/images/post_menu/flag.png create mode 100755 assets/base/images/post_menu/flag@2x.png create mode 100755 assets/base/images/post_menu/flag@3x.png create mode 100755 assets/base/images/post_menu/link.png create mode 100755 assets/base/images/post_menu/link@2x.png create mode 100755 assets/base/images/post_menu/link@3x.png create mode 100755 assets/base/images/post_menu/pin.png create mode 100755 assets/base/images/post_menu/pin@2x.png create mode 100755 assets/base/images/post_menu/pin@3x.png create mode 100755 assets/base/images/post_menu/trash.png create mode 100755 assets/base/images/post_menu/trash@2x.png create mode 100755 assets/base/images/post_menu/trash@3x.png diff --git a/NOTICE.txt b/NOTICE.txt index 766baf1a3..45ab02693 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -2076,41 +2076,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --- -## react-native-tooltip - -This product contains 'react-native-tooltip' by Chirag Jain. - -A react-native wrapper for showing tooltips - -* HOMEPAGE: - * https://github.com/chirag04/react-native-tooltip#readme - -* LICENSE: MIT - -The MIT License (MIT) - -Copyright (c) 2015-2016 Chirag Jain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ---- - ## react-native-vector-icons This product contains 'react-native-vector-icons' by Joel Arvidsson. diff --git a/app/components/at_mention/at_mention.js b/app/components/at_mention/at_mention.js index f800ef4a2..d39351fd4 100644 --- a/app/components/at_mention/at_mention.js +++ b/app/components/at_mention/at_mention.js @@ -8,10 +8,9 @@ import {intlShape} from 'react-intl'; import {displayUsername} from 'mattermost-redux/utils/user_utils'; -import {emptyFunction} from 'app/utils/general'; - import CustomPropTypes from 'app/constants/custom_prop_types'; import mattermostManaged from 'app/mattermost_managed'; +import BottomSheet from 'app/utils/bottom_sheet'; export default class AtMention extends React.PureComponent { static propTypes = { @@ -19,7 +18,6 @@ export default class AtMention extends React.PureComponent { mentionName: PropTypes.string.isRequired, mentionStyle: CustomPropTypes.Style, navigator: PropTypes.object.isRequired, - onLongPress: PropTypes.func.isRequired, onPostPress: PropTypes.func, textStyle: CustomPropTypes.Style, teammateNameDisplay: PropTypes.string, @@ -27,10 +25,6 @@ export default class AtMention extends React.PureComponent { usersByUsername: PropTypes.object.isRequired, }; - static defaultProps = { - onLongPress: emptyFunction, - }; - static contextTypes = { intl: intlShape, }; @@ -101,26 +95,33 @@ export default class AtMention extends React.PureComponent { } handleLongPress = async () => { - const {intl} = this.context; + const {formatMessage} = this.context.intl; const config = await mattermostManaged.getLocalConfig(); - let action; if (config.copyAndPasteProtection !== 'false') { - action = { - text: intl.formatMessage({ - id: 'mobile.mention.copy_mention', - defaultMessage: 'Copy Mention', - }), - onPress: this.handleCopyMention, - }; - } + const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}); + const actionText = formatMessage({id: 'mobile.mention.copy_mention', defaultMessage: 'Copy Mention'}); - this.props.onLongPress(action); + BottomSheet.showBottomSheetWithOptions({ + options: [actionText, cancelText], + cancelButtonIndex: 1, + }, (value) => { + if (value !== 1) { + this.handleCopyMention(); + } + }); + } }; handleCopyMention = () => { - const {username} = this.state; + const {user} = this.state; + const {mentionName} = this.props; + let username = mentionName; + if (user.username) { + username = user.username; + } + Clipboard.setString(`@${username}`); }; diff --git a/app/components/file_attachment_list/file_attachment.js b/app/components/file_attachment_list/file_attachment.js index 2b6ebcb45..628f3b231 100644 --- a/app/components/file_attachment_list/file_attachment.js +++ b/app/components/file_attachment_list/file_attachment.js @@ -25,6 +25,7 @@ export default class FileAttachment extends PureComponent { file: PropTypes.object.isRequired, index: PropTypes.number.isRequired, onCaptureRef: PropTypes.func, + onLongPress: PropTypes.func, onPreviewPress: PropTypes.func, theme: PropTypes.object.isRequired, navigator: PropTypes.object, @@ -92,6 +93,7 @@ export default class FileAttachment extends PureComponent { file, theme, navigator, + onLongPress, } = this.props; const {data} = file; const style = getStyleSheet(theme); @@ -99,7 +101,10 @@ export default class FileAttachment extends PureComponent { let fileAttachmentComponent; if ((data && data.has_preview_image) || file.loading || isGif(data)) { fileAttachmentComponent = ( - + ); } else { fileAttachmentComponent = ( - + {fileAttachmentComponent} diff --git a/app/components/file_attachment_list/file_attachment_document.js b/app/components/file_attachment_list/file_attachment_document.js index 6911bfa2f..69d7d0b0b 100644 --- a/app/components/file_attachment_list/file_attachment_document.js +++ b/app/components/file_attachment_list/file_attachment_document.js @@ -47,6 +47,7 @@ export default class FileAttachmentDocument extends PureComponent { file: PropTypes.object.isRequired, theme: PropTypes.object.isRequired, navigator: PropTypes.object, + onLongPress: PropTypes.func, wrapperHeight: PropTypes.number, wrapperWidth: PropTypes.number, }; @@ -349,7 +350,7 @@ export default class FileAttachmentDocument extends PureComponent { } render() { - const {theme, wrapperHeight} = this.props; + const {onLongPress, theme, wrapperHeight} = this.props; const {downloading, progress} = this.state; let fileAttachmentComponent; @@ -371,8 +372,11 @@ export default class FileAttachmentDocument extends PureComponent { } return ( - - + + {fileAttachmentComponent} @@ -389,7 +393,7 @@ const style = StyleSheet.create({ position: 'absolute', top: 0, }, - whiteBackgroud: { + whiteBackground: { backgroundColor: '#fff', }, }); diff --git a/app/components/file_attachment_list/file_attachment_list.js b/app/components/file_attachment_list/file_attachment_list.js index 8ac26bc22..96a4f42c0 100644 --- a/app/components/file_attachment_list/file_attachment_list.js +++ b/app/components/file_attachment_list/file_attachment_list.js @@ -8,7 +8,6 @@ import { Platform, ScrollView, StyleSheet, - TouchableOpacity, View, } from 'react-native'; @@ -30,7 +29,6 @@ export default class FileAttachmentList extends Component { deviceWidth: PropTypes.number.isRequired, fileIds: PropTypes.array.isRequired, files: PropTypes.array.isRequired, - hideOptionsContext: PropTypes.func.isRequired, isFailed: PropTypes.bool, navigator: PropTypes.object, onLongPress: PropTypes.func, @@ -121,7 +119,6 @@ export default class FileAttachmentList extends Component { }; handlePreviewPress = preventDoubleTap((idx) => { - this.props.hideOptionsContext(); Keyboard.dismiss(); previewImageAtIndex(this.props.navigator, this.items, idx, this.galleryFiles); }); @@ -157,23 +154,18 @@ export default class FileAttachmentList extends Component { }; return ( - - - + theme={this.props.theme} + /> ); }); }; diff --git a/app/components/formatted_markdown_text.js b/app/components/formatted_markdown_text.js index cb11833a2..965280d45 100644 --- a/app/components/formatted_markdown_text.js +++ b/app/components/formatted_markdown_text.js @@ -116,7 +116,6 @@ class FormattedMarkdownText extends React.PureComponent { mentionStyle={this.props.textStyles.mention} mentionName={mentionName} navigator={this.props.navigator} - onLongPress={this.props.onPostPress} onPostPress={this.props.onPostPress} textStyle={this.computeTextStyle(this.props.baseTextStyle, context)} /> @@ -145,4 +144,4 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); -export default injectIntl(FormattedMarkdownText); \ No newline at end of file +export default injectIntl(FormattedMarkdownText); diff --git a/app/components/markdown/markdown.js b/app/components/markdown/markdown.js index b37d68299..0c0c14e26 100644 --- a/app/components/markdown/markdown.js +++ b/app/components/markdown/markdown.js @@ -49,7 +49,6 @@ export default class Markdown extends PureComponent { navigator: PropTypes.object.isRequired, onChannelLinkPress: PropTypes.func, onHashtagPress: PropTypes.func, - onLongPress: PropTypes.func, onPermalinkPress: PropTypes.func, onPostPress: PropTypes.func, textStyles: PropTypes.object, @@ -80,13 +79,13 @@ export default class Markdown extends PureComponent { return new Parser({ urlFilter: this.urlFilter, }); - } + }; urlFilter = (url) => { const scheme = getScheme(url); return !scheme || this.props.autolinkedUrlSchemes.indexOf(scheme) !== -1; - } + }; createRenderer = () => { return new Renderer({ @@ -130,7 +129,7 @@ export default class Markdown extends PureComponent { renderParagraphsInLists: true, getExtraPropsForNode: this.getExtraPropsForNode, }); - } + }; getExtraPropsForNode = (node) => { const extraProps = { @@ -144,11 +143,11 @@ export default class Markdown extends PureComponent { } return extraProps; - } + }; computeTextStyle = (baseStyle, context) => { return concatStyles(baseStyle, context.map((type) => this.props.textStyles[type])); - } + }; renderText = ({context, literal}) => { if (context.indexOf('image') !== -1) { @@ -160,19 +159,17 @@ export default class Markdown extends PureComponent { const style = this.computeTextStyle(this.props.baseTextStyle, context); return {literal}; - } + }; renderCodeSpan = ({context, literal}) => { return {literal}; - } + }; renderImage = ({linkDestination, reactChildren, context, src}) => { if (context.indexOf('table') !== -1) { // We have enough problems rendering images as is, so just render a link inside of a table return ( {reactChildren} ); - } + }; renderAtMention = ({context, mentionName}) => { if (this.props.disableAtMentions) { @@ -207,12 +203,11 @@ export default class Markdown extends PureComponent { textStyle={this.computeTextStyle(this.props.baseTextStyle, context)} isSearchResult={this.props.isSearchResult} mentionName={mentionName} - onLongPress={this.props.onLongPress} onPostPress={this.props.onPostPress} navigator={this.props.navigator} /> ); - } + }; renderChannelLink = ({context, channelName}) => { if (this.props.disableChannelLink) { @@ -227,7 +222,7 @@ export default class Markdown extends PureComponent { channelName={channelName} /> ); - } + }; renderEmoji = ({context, emojiName, literal}) => { return ( @@ -237,7 +232,7 @@ export default class Markdown extends PureComponent { textStyle={this.computeTextStyle(this.props.baseTextStyle, context)} /> ); - } + }; renderHashtag = ({context, hashtag}) => { if (this.props.disableHashtags) { @@ -252,7 +247,7 @@ export default class Markdown extends PureComponent { navigator={this.props.navigator} /> ); - } + }; renderParagraph = ({children, first}) => { if (!children || children.length === 0) { @@ -271,7 +266,7 @@ export default class Markdown extends PureComponent { ); - } + }; renderHeading = ({children, level}) => { const containerStyle = [ @@ -286,7 +281,7 @@ export default class Markdown extends PureComponent { ); - } + }; renderCodeBlock = (props) => { // These sometimes include a trailing newline @@ -298,10 +293,9 @@ export default class Markdown extends PureComponent { content={content} language={props.language} textStyle={this.props.textStyles.codeBlock} - onLongPress={this.props.onLongPress} /> ); - } + }; renderBlockQuote = ({children, ...otherProps}) => { return ( @@ -312,7 +306,7 @@ export default class Markdown extends PureComponent { {children} ); - } + }; renderList = ({children, start, tight, type}) => { return ( @@ -324,7 +318,7 @@ export default class Markdown extends PureComponent { {children} ); - } + }; renderListItem = ({children, context, ...otherProps}) => { const level = context.filter((type) => type === 'list').length; @@ -338,19 +332,19 @@ export default class Markdown extends PureComponent { {children} ); - } + }; renderHardBreak = () => { return {'\n'}; - } + }; renderThematicBreak = () => { return ; - } + }; renderSoftBreak = () => { return {'\n'}; - } + }; renderHtml = (props) => { let rendered = this.renderText(props); @@ -366,7 +360,7 @@ export default class Markdown extends PureComponent { } return rendered; - } + }; renderTable = ({children}) => { return ( @@ -374,19 +368,18 @@ export default class Markdown extends PureComponent { {children} ); - } + }; renderLink = ({children, href}) => { return ( {children} ); - } + }; renderEditedIndicator = ({context}) => { let spacer = ''; @@ -411,7 +404,7 @@ export default class Markdown extends PureComponent { /> ); - } + }; render() { let ast = this.parser.parse(this.props.value); diff --git a/app/components/markdown/markdown_code_block/markdown_code_block.js b/app/components/markdown/markdown_code_block/markdown_code_block.js index f2341cf30..2cdace7ca 100644 --- a/app/components/markdown/markdown_code_block/markdown_code_block.js +++ b/app/components/markdown/markdown_code_block/markdown_code_block.js @@ -14,6 +14,7 @@ import { import CustomPropTypes from 'app/constants/custom_prop_types'; import FormattedText from 'app/components/formatted_text'; +import BottomSheet from 'app/utils/bottom_sheet'; import {getDisplayNameForLanguage} from 'app/utils/markdown'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -28,7 +29,6 @@ export default class MarkdownCodeBlock extends React.PureComponent { language: PropTypes.string, content: PropTypes.string.isRequired, textStyle: CustomPropTypes.Style, - onLongPress: PropTypes.func.isRequired, }; static defaultProps = { @@ -84,20 +84,23 @@ export default class MarkdownCodeBlock extends React.PureComponent { const config = await mattermostManaged.getLocalConfig(); - let action; if (config.copyAndPasteProtection !== 'true') { - action = { - text: formatMessage({id: 'mobile.markdown.code.copy_code', defaultMessage: 'Copy Code'}), - onPress: this.handleCopyCode, - }; + const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}); + const actionText = formatMessage({id: 'mobile.markdown.code.copy_code', defaultMessage: 'Copy Code'}); + BottomSheet.showBottomSheetWithOptions({ + options: [actionText, cancelText], + cancelButtonIndex: 1, + }, (value) => { + if (value !== 1) { + this.handleCopyCode(); + } + }); } - - this.props.onLongPress(action); - } + }; handleCopyCode = () => { Clipboard.setString(this.props.content); - } + }; trimContent = (content) => { const lines = content.split('\n'); diff --git a/app/components/markdown/markdown_image/markdown_image.js b/app/components/markdown/markdown_image/markdown_image.js index 489b89859..6ab72389e 100644 --- a/app/components/markdown/markdown_image/markdown_image.js +++ b/app/components/markdown/markdown_image/markdown_image.js @@ -19,6 +19,7 @@ import FormattedText from 'app/components/formatted_text'; import ProgressiveImage from 'app/components/progressive_image'; import CustomPropTypes from 'app/constants/custom_prop_types'; import mattermostManaged from 'app/mattermost_managed'; +import BottomSheet from 'app/utils/bottom_sheet'; import ImageCacheManager from 'app/utils/image_cache_manager'; import {previewImageAtIndex, calculateDimensions} from 'app/utils/images'; import {normalizeProtocol} from 'app/utils/url'; @@ -38,7 +39,6 @@ export default class MarkdownImage extends React.Component { linkDestination: PropTypes.string, isReplyPost: PropTypes.bool, navigator: PropTypes.object.isRequired, - onLongPress: PropTypes.func, serverURL: PropTypes.string.isRequired, source: PropTypes.string.isRequired, errorTextStyle: CustomPropTypes.Style, @@ -138,15 +138,18 @@ export default class MarkdownImage extends React.Component { const config = await mattermostManaged.getLocalConfig(); - let action; if (config.copyAndPasteProtection !== 'true') { - action = { - text: formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'}), - onPress: this.handleLinkCopy, - }; + const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}); + const actionText = formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'}); + BottomSheet.showBottomSheetWithOptions({ + options: [actionText, cancelText], + cancelButtonIndex: 1, + }, (value) => { + if (value !== 1) { + this.handleLinkCopy(); + } + }); } - - this.props.onLongPress(action); }; handleLinkCopy = () => { diff --git a/app/components/markdown/markdown_link/markdown_link.js b/app/components/markdown/markdown_link/markdown_link.js index 52867c020..3148eb094 100644 --- a/app/components/markdown/markdown_link/markdown_link.js +++ b/app/components/markdown/markdown_link/markdown_link.js @@ -9,24 +9,22 @@ import {intlShape} from 'react-intl'; import CustomPropTypes from 'app/constants/custom_prop_types'; import mattermostManaged from 'app/mattermost_managed'; - -import Config from 'assets/config'; - +import BottomSheet from 'app/utils/bottom_sheet'; import {preventDoubleTap} from 'app/utils/tap'; import {matchPermalink, normalizeProtocol} from 'app/utils/url'; +import Config from 'assets/config'; + export default class MarkdownLink extends PureComponent { static propTypes = { children: CustomPropTypes.Children.isRequired, href: PropTypes.string.isRequired, - onLongPress: PropTypes.func, onPermalinkPress: PropTypes.func, serverURL: PropTypes.string.isRequired, siteURL: PropTypes.string.isRequired, }; static defaultProps = { - onLongPress: () => true, onPermalinkPress: () => true, }; @@ -89,27 +87,30 @@ export default class MarkdownLink extends PureComponent { ...otherChildProps, }; }); - } + }; handleLongPress = async () => { const {formatMessage} = this.context.intl; const config = await mattermostManaged.getLocalConfig(); - let action; if (config.copyAndPasteProtection !== 'true') { - action = { - text: formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'}), - onPress: this.handleCopyURL, - }; + const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}); + const actionText = formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'}); + BottomSheet.showBottomSheetWithOptions({ + options: [actionText, cancelText], + cancelButtonIndex: 1, + }, (value) => { + if (value !== 1) { + this.handleLinkCopy(); + } + }); } - - this.props.onLongPress(action); - } + }; handleCopyURL = () => { Clipboard.setString(this.props.href); - } + }; render() { const children = Config.ExperimentalNormalizeMarkdownLinks ? this.parseChildren() : this.props.children; diff --git a/app/components/message_attachments/index.js b/app/components/message_attachments/index.js index 857422ee4..da6e1ddbe 100644 --- a/app/components/message_attachments/index.js +++ b/app/components/message_attachments/index.js @@ -17,7 +17,6 @@ export default class MessageAttachments extends PureComponent { postId: PropTypes.string.isRequired, navigator: PropTypes.object.isRequired, onHashtagPress: PropTypes.func, - onLongPress: PropTypes.func.isRequired, onPermalinkPress: PropTypes.func, theme: PropTypes.object, textStyles: PropTypes.object, @@ -30,7 +29,6 @@ export default class MessageAttachments extends PureComponent { blockStyles, navigator, onHashtagPress, - onLongPress, onPermalinkPress, postId, theme, @@ -47,7 +45,6 @@ export default class MessageAttachments extends PureComponent { key={'att_' + i} navigator={navigator} onHashtagPress={onHashtagPress} - onLongPress={onLongPress} onPermalinkPress={onPermalinkPress} postId={postId} theme={theme} diff --git a/app/components/message_attachments/message_attachment.js b/app/components/message_attachments/message_attachment.js index 7b8557fc5..7e6131dd0 100644 --- a/app/components/message_attachments/message_attachment.js +++ b/app/components/message_attachments/message_attachment.js @@ -40,7 +40,6 @@ export default class MessageAttachment extends PureComponent { blockStyles: PropTypes.object, navigator: PropTypes.object.isRequired, postId: PropTypes.string.isRequired, - onLongPress: PropTypes.func.isRequired, onPermalinkPress: PropTypes.func, theme: PropTypes.object, textStyles: PropTypes.object, @@ -194,7 +193,6 @@ export default class MessageAttachment extends PureComponent { blockStyles={blockStyles} value={(field.value || '')} navigator={navigator} - onLongPress={this.props.onLongPress} onPermalinkPress={onPermalinkPress} /> @@ -317,7 +315,6 @@ export default class MessageAttachment extends PureComponent { blockStyles={blockStyles} value={attachment.pretext} navigator={navigator} - onLongPress={this.props.onLongPress} onPermalinkPress={onPermalinkPress} /> @@ -415,7 +412,6 @@ export default class MessageAttachment extends PureComponent { blockStyles={blockStyles} value={attachment.text} navigator={navigator} - onLongPress={this.props.onLongPress} onPermalinkPress={onPermalinkPress} /> diff --git a/app/components/options_context/index.js b/app/components/options_context/index.js deleted file mode 100644 index 98c033d65..000000000 --- a/app/components/options_context/index.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Used to leverage the platform specific components -import OptionsContext from './options_context'; -export default OptionsContext; diff --git a/app/components/options_context/options_context.android.js b/app/components/options_context/options_context.android.js deleted file mode 100644 index f0249d87c..000000000 --- a/app/components/options_context/options_context.android.js +++ /dev/null @@ -1,71 +0,0 @@ -// 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 {TouchableHighlight, View} from 'react-native'; -import RNBottomSheet from 'react-native-bottom-sheet'; - -export default class OptionsContext extends PureComponent { - static propTypes = { - getPostActions: PropTypes.func, - cancelText: PropTypes.string, - children: PropTypes.node.isRequired, - onPress: PropTypes.func.isRequired, - toggleSelected: PropTypes.func.isRequired, - }; - - static defaultProps = { - getPostActions: () => [], - cancelText: 'Cancel', - }; - - show = (additionalAction) => { - const {getPostActions, cancelText} = this.props; - const nextActions = getPostActions(); - if (additionalAction && !additionalAction.nativeEvent && additionalAction.text) { - const copyPostIndex = nextActions.findIndex((action) => action.copyPost); - nextActions.splice(copyPostIndex + 1, 0, additionalAction); - } - - if (nextActions.length) { - const actionsText = nextActions.map((a) => a.text); - RNBottomSheet.showBottomSheetWithOptions({ - options: [...actionsText, cancelText], - cancelButtonIndex: nextActions.length, - }, (value) => { - if (value !== nextActions.length) { - const selectedOption = nextActions[value]; - if (selectedOption && selectedOption.onPress) { - selectedOption.onPress(); - } - } - }); - } - }; - - handleHideUnderlay = () => { - this.props.toggleSelected(false, this.props.getPostActions().length > 0); - }; - - handleShowUnderlay = () => { - this.props.toggleSelected(true, this.props.getPostActions().length > 0); - }; - - render() { - return ( - - - {this.props.children} - - - ); - } -} diff --git a/app/components/options_context/options_context.ios.js b/app/components/options_context/options_context.ios.js deleted file mode 100644 index 12713d6a6..000000000 --- a/app/components/options_context/options_context.ios.js +++ /dev/null @@ -1,108 +0,0 @@ -// 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 ToolTip from 'app/components/tooltip'; - -export default class OptionsContext extends PureComponent { - static propTypes = { - getPostActions: PropTypes.func, - children: PropTypes.node.isRequired, - onPress: PropTypes.func.isRequired, - toggleSelected: PropTypes.func.isRequired, - }; - - static defaultProps = { - getPostActions: () => [], - additionalActions: [], - }; - - constructor(props) { - super(props); - - this.state = { - actions: props.getPostActions(), - }; - } - - beforeShow = (additionalAction) => { - const nextActions = this.props.getPostActions(); - if (additionalAction && additionalAction.text && !additionalAction.nativeEvent) { - const copyPostIndex = nextActions.findIndex((action) => action.copyPost); - nextActions.splice(copyPostIndex + 1, 0, additionalAction); - } - - this.setState({ - actions: nextActions, - }); - }; - - handleHide = () => { - this.isShowing = false; - this.props.toggleSelected(false); - }; - - handleHideUnderlay = () => { - if (!this.isShowing) { - this.props.toggleSelected(false); - } - }; - - handleShow = () => { - this.isShowing = true; - this.props.toggleSelected(true); - } - - handleShowUnderlay = () => { - this.beforeShow(); - this.props.toggleSelected(true); - }; - - hide = () => { - this.setState({ - actions: this.props.getPostActions(), - }); - - if (this.refs.toolTip) { - this.refs.toolTip.hideMenu(); - } - - this.isShowing = false; - this.props.toggleSelected(false); - }; - - handlePress = () => { - this.props.toggleSelected(false); - this.props.onPress(); - }; - - show = (additionalAction) => { - this.props.toggleSelected(true); - this.beforeShow(additionalAction); - - if (this.refs.toolTip) { - this.refs.toolTip.showMenu(); - } - }; - - render() { - return ( - - {this.props.children} - - ); - } -} diff --git a/app/components/post/index.js b/app/components/post/index.js index 97d74b1cf..cda62802d 100644 --- a/app/components/post/index.js +++ b/app/components/post/index.js @@ -4,19 +4,15 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {createPost, deletePost, removePost} from 'mattermost-redux/actions/posts'; -import {General, Posts} from 'mattermost-redux/constants'; -import {getCurrentChannelId, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels'; +import {createPost, removePost} from 'mattermost-redux/actions/posts'; +import {Posts} from 'mattermost-redux/constants'; +import {isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels'; import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts'; -import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {canDeletePost, canEditPost, isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils'; -import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils'; import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel'; -import {addReaction} from 'app/actions/views/emoji'; import Post from './post'; @@ -45,13 +41,8 @@ function makeMapStateToProps() { const getCommentCountForPost = makeGetCommentCountForPost(); return function mapStateToProps(state, ownProps) { const post = getPost(state, ownProps.postId); - const config = getConfig(state); - const license = getLicense(state); - const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : ''; const myPreferences = getMyPreferences(state); const currentUserId = getCurrentUserId(state); - const currentTeamId = getCurrentTeamId(state); - const currentChannelId = getCurrentChannelId(state); let isFirstReply = true; let isLastReply = true; @@ -78,28 +69,8 @@ function makeMapStateToProps() { } } - const isAdmin = checkIsAdmin(roles); - const isSystemAdmin = checkIsSystemAdmin(roles); - - let canDelete = false; - let canEdit = false; - let canEditUntil = -1; - if (post) { - canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin); - canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post); - if (canEdit && license.IsLicensed === 'true' && - (config.AllowEditPost === General.ALLOW_EDIT_POST_TIME_LIMIT || (config.PostEditTimeLimit !== -1 && config.PostEditTimeLimit !== '-1')) - ) { - canEditUntil = post.create_at + (config.PostEditTimeLimit * 1000); - } - } - return { channelIsReadOnly: isCurrentChannelReadOnly(state), - canDelete, - canEdit, - canEditUntil, - currentTeamUrl: getCurrentTeamUrl(state), currentUserId, post, isFirstReply, @@ -116,9 +87,7 @@ function makeMapStateToProps() { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - addReaction, createPost, - deletePost, removePost, setPostTooltipVisible, insertToDraft, diff --git a/app/components/post/post.js b/app/components/post/post.js index d30ceddf4..60d1a1747 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -4,15 +4,12 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { - Alert, - Clipboard, Platform, TouchableHighlight, View, ViewPropTypes, } from 'react-native'; import {intlShape} from 'react-intl'; -import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import FlagIcon from 'app/components/flag_icon'; import PostBody from 'app/components/post_body'; @@ -22,7 +19,6 @@ import {NavigationTypes} from 'app/constants'; import {fromAutoResponder} from 'app/utils/general'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; -import {getToolTipVisible} from 'app/utils/tooltip'; import {t} from 'app/utils/i18n'; import {Posts} from 'mattermost-redux/constants'; @@ -34,14 +30,11 @@ import Config from 'assets/config'; export default class Post extends PureComponent { static propTypes = { actions: PropTypes.shape({ - addReaction: PropTypes.func.isRequired, createPost: PropTypes.func.isRequired, - deletePost: PropTypes.func.isRequired, insertToDraft: PropTypes.func.isRequired, removePost: PropTypes.func.isRequired, }).isRequired, channelIsReadOnly: PropTypes.bool, - currentTeamUrl: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, highlight: PropTypes.bool, style: ViewPropTypes.style, @@ -56,9 +49,6 @@ export default class Post extends PureComponent { commentedOnPost: PropTypes.object, managedConfig: PropTypes.object.isRequired, navigator: PropTypes.object, - canEdit: PropTypes.bool.isRequired, - canEditUntil: PropTypes.number.isRequired, - canDelete: PropTypes.bool.isRequired, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, shouldRenderReplyButton: PropTypes.bool, @@ -112,83 +102,6 @@ export default class Post extends PureComponent { this.props.actions.insertToDraft(`@${username} `); }; - handlePostDelete = () => { - const {formatMessage} = this.context.intl; - const {actions, currentUserId, 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: () => { - actions.deletePost(post); - if (post.user_id === currentUserId) { - actions.removePost(post); - } - }, - }] - ); - }; - - handlePostEdit = () => { - const {intl} = this.context; - const {navigator, post, theme} = this.props; - MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => { - navigator.showModal({ - screen: 'EditPost', - title: intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - post, - closeButton: source, - }, - }); - }); - }; - - handleAddReactionToPost = (emoji) => { - const {post} = this.props; - this.props.actions.addReaction(post.id, emoji); - }; - - handleAddReaction = preventDoubleTap(() => { - const {intl} = this.context; - const {navigator, post, theme} = this.props; - - MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => { - navigator.showModal({ - screen: 'AddReaction', - title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}), - animated: true, - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg, - }, - passProps: { - post, - closeButton: source, - onEmojiPress: this.handleAddReactionToPost, - }, - }); - }); - }); - handleFailedPostPress = () => { const options = { title: { @@ -246,22 +159,17 @@ export default class Post extends PureComponent { showLongPost, } = this.props; - if (!getToolTipVisible()) { - const isValidSystemMessage = fromAutoResponder(post) || !isSystemMessage(post); - if (onPress && post.state !== Posts.POST_DELETED && isValidSystemMessage && !isPostPendingOrFailed(post)) { - onPress(post); - } else if ((isPostEphemeral(post) || post.state === Posts.POST_DELETED) && !showLongPost) { - this.onRemovePost(post); - } - } else if (this.refs.postBody) { - this.refs.postBody.getWrappedInstance().hideOptionsContext(); - this.handleHideUnderlay(); + const isValidSystemMessage = fromAutoResponder(post) || !isSystemMessage(post); + if (onPress && post.state !== Posts.POST_DELETED && isValidSystemMessage && !isPostPendingOrFailed(post)) { + onPress(post); + } else if ((isPostEphemeral(post) || post.state === Posts.POST_DELETED) && !showLongPost) { + this.onRemovePost(post); } }); handleReply = preventDoubleTap(() => { const {post, onReply} = this.props; - if (!getToolTipVisible() && onReply) { + if (onReply) { return onReply(post); } @@ -305,31 +213,13 @@ export default class Post extends PureComponent { }; viewUserProfile = preventDoubleTap(() => { - if (!getToolTipVisible()) { - this.goToUserProfile(); - } + this.goToUserProfile(); }); toggleSelected = (selected) => { this.setState({selected}); }; - handleCopyText = (text) => { - let textToCopy = this.props.post.message; - if (typeof text === 'string') { - textToCopy = text; - } - - Clipboard.setString(textToCopy); - }; - - handleCopyPermalink = () => { - const {currentTeamUrl, postId} = this.props; - const permalink = `${currentTeamUrl}/pl/${postId}`; - - Clipboard.setString(permalink); - }; - handleHideUnderlay = () => { this.toggleSelected(false); }; @@ -338,9 +228,9 @@ export default class Post extends PureComponent { this.toggleSelected(true); }; - showOptionsContext = () => { + showPostOptions = () => { if (this.refs.postBody) { - this.refs.postBody.getWrappedInstance().showOptionsContext(); + this.refs.postBody.getWrappedInstance().showPostOptions(); } }; @@ -405,7 +295,7 @@ export default class Post extends PureComponent { onPress={this.handlePress} onHideUnderlay={this.handleHideUnderlay} onShowUnderlay={this.handleShowUnderlay} - onLongPress={this.showOptionsContext} + onLongPress={this.showPostOptions} underlayColor='transparent' > @@ -114,7 +112,6 @@ export default class PostAddChannelMember extends React.PureComponent { key={username} mentionStyle={this.props.textStyles.mention} mentionName={username} - onLongPress={this.props.onLongPress} onPostPress={this.props.onPostPress} navigator={this.props.navigator} /> @@ -192,4 +189,4 @@ export default class PostAddChannelMember extends React.PureComponent { ); } -} \ No newline at end of file +} diff --git a/app/components/post_body/index.js b/app/components/post_body/index.js index 82bc3ef58..62854803a 100644 --- a/app/components/post_body/index.js +++ b/app/components/post_body/index.js @@ -2,27 +2,19 @@ // See LICENSE.txt for license information. import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; -import {flagPost, unflagPost} from 'mattermost-redux/actions/posts'; -import { - General, - Posts, -} from 'mattermost-redux/constants'; +import {General, Posts} from 'mattermost-redux/constants'; import {getChannel, canManageChannelMembers, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {hasNewPermissions, getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; +import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import Permissions from 'mattermost-redux/constants/permissions'; import { isEdited, isPostEphemeral, isSystemMessage, - canEditPost, canDeletePost, } from 'mattermost-redux/utils/post_utils'; import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils'; @@ -34,19 +26,6 @@ const POST_TIMEOUT = 20000; function mapStateToProps(state, ownProps) { const post = getPost(state, ownProps.postId) || {}; const channel = getChannel(state, post.channel_id) || {}; - const channelIsArchived = channel ? channel.delete_at !== 0 : false; - const teamId = channel.team_id; - - let canAddReaction = true; - if (channelIsArchived) { - canAddReaction = false; - } else if (hasNewPermissions(state)) { - canAddReaction = haveIChannelPermission(state, { - team: teamId, - channel: post.channel_id, - permission: Permissions.ADD_REACTION, - }); - } let isFailed = post.failed; let isPending = post.id === post.pending_post_id; @@ -69,12 +48,9 @@ function mapStateToProps(state, ownProps) { const isAdmin = checkIsAdmin(roles); const isSystemAdmin = checkIsSystemAdmin(roles); let canDelete = false; - let canEdit = false; - if (post) { - if (!channelIsArchived) { - canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin); - canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post); - } + + if (post && !ownProps.channelIsArchived) { + canDelete = canDeletePost(state, config, license, currentTeamId, currentChannelId, currentUserId, post, isAdmin, isSystemAdmin); } let isPostAddChannelMember = false; @@ -103,19 +79,8 @@ function mapStateToProps(state, ownProps) { isSystemMessage: isSystemMessage(post), message: post.message, theme: getTheme(state), - canAddReaction, canDelete, - canEdit, }; } -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - flagPost, - unflagPost, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(PostBody); +export default connect(mapStateToProps, null, null, {withRef: true})(PostBody); diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js index 0138c6101..4eb82b440 100644 --- a/app/components/post_body/post_body.js +++ b/app/components/post_body/post_body.js @@ -5,7 +5,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { Dimensions, - Platform, TouchableHighlight, TouchableOpacity, View, @@ -18,7 +17,6 @@ import {Posts} from 'mattermost-redux/constants'; import CombinedSystemMessage from 'app/components/combined_system_message'; import FormattedText from 'app/components/formatted_text'; import Markdown from 'app/components/markdown'; -import OptionsContext from 'app/components/options_context'; import ShowMoreButton from 'app/components/show_more_button'; import {emptyFunction} from 'app/utils/general'; @@ -33,14 +31,7 @@ let Reactions; export default class PostBody extends PureComponent { static propTypes = { - actions: PropTypes.shape({ - flagPost: PropTypes.func.isRequired, - unflagPost: PropTypes.func.isRequired, - }).isRequired, - canAddReaction: PropTypes.bool, canDelete: PropTypes.bool, - canEdit: PropTypes.bool, - canEditUntil: PropTypes.number.isRequired, channelIsReadOnly: PropTypes.bool.isRequired, fileIds: PropTypes.array, hasBeenDeleted: PropTypes.bool, @@ -58,14 +49,9 @@ export default class PostBody extends PureComponent { managedConfig: PropTypes.object, message: PropTypes.string, navigator: PropTypes.object.isRequired, - onAddReaction: PropTypes.func, - onCopyPermalink: PropTypes.func, - onCopyText: PropTypes.func, onFailedPostPress: PropTypes.func, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, - onPostDelete: PropTypes.func, - onPostEdit: PropTypes.func, onPress: PropTypes.func, postId: PropTypes.string.isRequired, postProps: PropTypes.object, @@ -79,12 +65,7 @@ export default class PostBody extends PureComponent { static defaultProps = { fileIds: [], - onAddReaction: emptyFunction, - onCopyPermalink: emptyFunction, - onCopyText: emptyFunction, onFailedPostPress: emptyFunction, - onPostDelete: emptyFunction, - onPostEdit: emptyFunction, onPress: emptyFunction, replyBarStyle: [], toggleSelected: emptyFunction, @@ -98,11 +79,6 @@ export default class PostBody extends PureComponent { isLongPost: false, }; - flagPost = () => { - const {actions, postId} = this.props; - actions.flagPost(postId); - }; - handleHideUnderlay = () => { this.props.toggleSelected(false); }; @@ -111,84 +87,6 @@ export default class PostBody extends PureComponent { this.props.toggleSelected(true); }; - hideOptionsContext = () => { - if (Platform.OS === 'ios' && this.refs.options) { - this.refs.options.hide(); - } - }; - - getPostActions = () => { - const {formatMessage} = this.context.intl; - const { - canEdit, - canEditUntil, - canDelete, - canAddReaction, - channelIsReadOnly, - hasBeenDeleted, - isPending, - isFailed, - isFlagged, - isPostEphemeral, - isSystemMessage, - managedConfig, - message, - onCopyText, - onPostDelete, - onPostEdit, - showAddReaction, - } = this.props; - const actions = []; - const isPendingOrFailedPost = isPending || isFailed; - - // we should check for the user roles and permissions - if (!isPendingOrFailedPost && !isSystemMessage && !isPostEphemeral) { - if (showAddReaction && canAddReaction && !channelIsReadOnly) { - actions.push({ - text: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}), - onPress: this.props.onAddReaction, - }); - } - - if (managedConfig.copyAndPasteProtection !== 'true' && message) { - actions.push({ - text: formatMessage({id: 'mobile.post_info.copy_post', defaultMessage: 'Copy Post'}), - onPress: onCopyText, - copyPost: true, - }); - } - - if (!channelIsReadOnly) { - if (isFlagged) { - actions.push({ - text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}), - onPress: this.unflagPost, - }); - } else { - actions.push({ - text: formatMessage({id: 'post_info.mobile.flag', defaultMessage: 'Flag'}), - onPress: this.flagPost, - }); - } - } - - if (canEdit && (canEditUntil === -1 || canEditUntil > Date.now())) { - actions.push({text: formatMessage({id: 'post_info.edit', defaultMessage: 'Edit'}), onPress: onPostEdit}); - } - - actions.push({ - text: formatMessage({id: 'get_post_link_modal.title', defaultMessage: 'Copy Permalink'}), - onPress: this.props.onCopyPermalink, - }); - } - - if (!isPendingOrFailedPost && !isPostEphemeral && canDelete && !hasBeenDeleted) { - actions.push({text: formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), onPress: onPostDelete}); - } - - return actions; - }; - measurePost = (event) => { const {height} = event.nativeEvent.layout; const {height: deviceHeight} = Dimensions.get('window'); @@ -206,7 +104,6 @@ export default class PostBody extends PureComponent { const { managedConfig, navigator, - onAddReaction, onHashtagPress, onPermalinkPress, postId, @@ -225,7 +122,6 @@ export default class PostBody extends PureComponent { passProps: { postId, managedConfig, - onAddReaction, onHashtagPress, onPermalinkPress, }, @@ -234,15 +130,51 @@ export default class PostBody extends PureComponent { navigator.showModal(options); }); - unflagPost = () => { - const {actions, postId} = this.props; - actions.unflagPost(postId); - }; + showPostOptions = () => { + const { + canDelete, + channelIsReadOnly, + hasBeenDeleted, + isFailed, + isFlagged, + isPending, + isPostEphemeral, + isSystemMessage, + managedConfig, + navigator, + postId, + showAddReaction, + } = this.props; - showOptionsContext = (additionalAction) => { - if (this.refs.options) { - this.refs.options.show(additionalAction); + if (isSystemMessage && (!canDelete || hasBeenDeleted)) { + return; } + + if (isPending || isFailed || isPostEphemeral) { + return; + } + + const options = { + screen: 'PostOptions', + animationType: 'none', + backButtonTitle: '', + navigatorStyle: { + navBarHidden: true, + screenBackgroundColor: 'transparent', + modalPresentationStyle: 'overCurrentContext', + }, + passProps: { + canDelete, + channelIsReadOnly, + hasBeenDeleted, + isFlagged, + postId, + managedConfig, + showAddReaction, + }, + }; + + navigator.showModal(options); }; renderAddChannelMember = (style, messageStyle, textStyles) => { @@ -258,7 +190,6 @@ export default class PostBody extends PureComponent { ); @@ -334,7 +263,6 @@ export default class PostBody extends PureComponent { hasReactions, isSearchResult, navigator, - onAddReaction, postId, showLongPost, } = this.props; @@ -350,14 +278,12 @@ export default class PostBody extends PureComponent { return ( ); }; render() { - const {formatMessage} = this.context.intl; const { hasBeenDeleted, hasBeenEdited, @@ -378,7 +304,6 @@ export default class PostBody extends PureComponent { postType, replyBarStyle, theme, - toggleSelected, } = this.props; const {isLongPost, maxHeight} = this.state; const style = getStyleSheet(theme); @@ -412,7 +337,7 @@ export default class PostBody extends PureComponent { } else if (postType === Posts.POST_TYPES.COMBINED_USER_ACTIVITY) { const {allUserIds, allUsernames, messageData} = postProps.user_activity; messageComponent = ( - + - + + {messageComponent} {isLongPost && @@ -476,8 +400,8 @@ export default class PostBody extends PureComponent { {this.renderPostAdditionalContent(blockStyles, messageStyle, textStyles)} {this.renderFileAttachments()} {this.renderReactions()} - - + + ); } diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index addbadcbb..7ec33087f 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -19,7 +19,6 @@ import {intlShape} from 'react-intl'; import ProgressiveImage from 'app/components/progressive_image'; import CustomPropTypes from 'app/constants/custom_prop_types'; -import {emptyFunction} from 'app/utils/general'; import ImageCacheManager from 'app/utils/image_cache_manager'; import {previewImageAtIndex, calculateDimensions} from 'app/utils/images'; import {getYouTubeVideoId, isImageLink, isYoutubeLink} from 'app/utils/url'; @@ -45,7 +44,6 @@ export default class PostBodyAdditionalContent extends PureComponent { message: PropTypes.string.isRequired, navigator: PropTypes.object.isRequired, onHashtagPress: PropTypes.func, - onLongPress: PropTypes.func, onPermalinkPress: PropTypes.func, openGraphData: PropTypes.object, postId: PropTypes.string.isRequired, @@ -55,10 +53,6 @@ export default class PostBodyAdditionalContent extends PureComponent { theme: PropTypes.object.isRequired, }; - static defaultProps = { - onLongPress: emptyFunction, - }; - static contextTypes = { intl: intlShape.isRequired, }; @@ -311,7 +305,6 @@ export default class PostBodyAdditionalContent extends PureComponent { textStyles={textStyles} theme={theme} onHashtagPress={onHashtagPress} - onLongPress={this.props.onLongPress} onPermalinkPress={onPermalinkPress} /> ); diff --git a/app/components/reactions/reactions.js b/app/components/reactions/reactions.js index 602767870..bd6d69439 100644 --- a/app/components/reactions/reactions.js +++ b/app/components/reactions/reactions.js @@ -9,7 +9,10 @@ import { TouchableOpacity, View, } from 'react-native'; +import {intlShape} from 'react-intl'; +import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import addReactionIcon from 'assets/images/icons/reaction.png'; @@ -24,7 +27,6 @@ export default class Reactions extends PureComponent { }).isRequired, highlightedReactions: PropTypes.array.isRequired, navigator: PropTypes.object.isRequired, - onAddReaction: PropTypes.func.isRequired, position: PropTypes.oneOf(['right', 'left']), postId: PropTypes.string.isRequired, reactions: PropTypes.object.isRequired, @@ -37,11 +39,43 @@ export default class Reactions extends PureComponent { position: 'right', }; + static contextTypes = { + intl: intlShape.isRequired, + }; + componentDidMount() { const {actions, postId} = this.props; actions.getReactionsForPost(postId); } + handleAddReaction = preventDoubleTap(() => { + const {formatMessage} = this.context.intl; + const {navigator, theme} = this.props; + + MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => { + navigator.showModal({ + screen: 'AddReaction', + title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}), + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg, + }, + passProps: { + closeButton: source, + onEmojiPress: this.handleAddReactionToPost, + }, + }); + }); + }); + + handleAddReactionToPost = (emoji) => { + const {postId} = this.props; + this.props.actions.addReaction(postId, emoji); + }; + handleReactionPress = (emoji, remove) => { const {actions, postId} = this.props; if (remove && this.props.canRemoveReaction) { @@ -104,7 +138,7 @@ export default class Reactions extends PureComponent { addMoreReactions = ( { + onMoveShouldSetPanResponderCapture: (evt, gestureState) => { + if (this.props.alwaysCaptureContainerMove) { + return gestureState.dy !== 0; + } const isGoingDown = gestureState.y0 < gestureState.dy; return this.isAValidMovement(gestureState.dx, gestureState.dy, isGoingDown); }, onPanResponderMove: (evt, gestureState) => { + const isGoingDown = gestureState.dy > 0; + if (this.props.alwaysCaptureContainerMove && + !this.isAValidMovement(gestureState.dx, gestureState.dy, isGoingDown)) { + return; + } + this.moveStart(gestureState); }, onPanResponderRelease: (evt, gestureState) => { diff --git a/app/screens/index.js b/app/screens/index.js index cab6f1f7b..6bee81ef7 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -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('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); Navigation.registerComponent('Root', () => require('app/screens/root').default, store, Provider); diff --git a/app/screens/long_post/long_post.js b/app/screens/long_post/long_post.js index d90d477d2..56fb0b596 100644 --- a/app/screens/long_post/long_post.js +++ b/app/screens/long_post/long_post.js @@ -51,7 +51,6 @@ export default class LongPost extends PureComponent { inThreadView: PropTypes.bool, managedConfig: PropTypes.object, navigator: PropTypes.object, - onAddReaction: PropTypes.func, onHashtagPress: PropTypes.func, onPermalinkPress: PropTypes.func, postId: PropTypes.string.isRequired, @@ -141,7 +140,6 @@ export default class LongPost extends PureComponent { { - const {hasReactions, postId, onAddReaction} = this.props; + const {hasReactions, navigator, postId} = this.props; if (!hasReactions) { return null; @@ -164,9 +162,9 @@ export default class LongPost extends PureComponent { return ( ); diff --git a/app/screens/post_options/index.js b/app/screens/post_options/index.js new file mode 100644 index 000000000..3771540cf --- /dev/null +++ b/app/screens/post_options/index.js @@ -0,0 +1,81 @@ +// 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 {deletePost, flagPost, unflagPost, 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'; +import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selectors/entities/general'; +import {getPost} from 'mattermost-redux/selectors/entities/posts'; +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; +import {getCurrentTeamId, getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams'; +import {canEditPost} from 'mattermost-redux/utils/post_utils'; + +import {addReaction} from 'app/actions/views/emoji'; +import {getDimensions} from 'app/selectors/device'; + +import PostOptions from './post_options'; + +function mapStateToProps(state, ownProps) { + const post = getPost(state, ownProps.postId); + const channel = getChannel(state, post.channel_id); + const config = getConfig(state); + const license = getLicense(state); + const currentUserId = getCurrentUserId(state); + const currentTeamId = getCurrentTeamId(state); + const currentChannelId = getCurrentChannelId(state); + + const channelIsArchived = channel.delete_at !== 0; + + let canAddReaction = true; + let canEdit = false; + let canEditUntil = -1; + + if (hasNewPermissions(state)) { + canAddReaction = haveIChannelPermission(state, { + team: currentTeamId, + channel: post.channel_id, + permission: Permissions.ADD_REACTION, + }); + } + + if (channelIsArchived) { + canAddReaction = false; + } else { + canEdit = canEditPost(state, config, license, currentTeamId, currentChannelId, currentUserId, post); + if (canEdit && license.IsLicensed === 'true' && + (config.AllowEditPost === General.ALLOW_EDIT_POST_TIME_LIMIT || (config.PostEditTimeLimit !== -1 && config.PostEditTimeLimit !== '-1')) + ) { + canEditUntil = post.create_at + (config.PostEditTimeLimit * 1000); + } + } + + return { + ...getDimensions(state), + canAddReaction, + canEdit, + canEditUntil, + currentTeamUrl: getCurrentTeamUrl(state), + isMyPost: currentUserId === post.user_id, + post, + theme: getTheme(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + addReaction, + deletePost, + flagPost, + removePost, + unflagPost, + }, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(PostOptions); diff --git a/app/screens/post_options/post_option.js b/app/screens/post_options/post_option.js new file mode 100644 index 000000000..38f80c25f --- /dev/null +++ b/app/screens/post_options/post_option.js @@ -0,0 +1,106 @@ +// 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 { + Image, + StyleSheet, + Text, + TouchableHighlight, + View, +} from 'react-native'; + +import copy from 'assets/images/post_menu/copy.png'; +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 trash from 'assets/images/post_menu/trash.png'; + +const icons = { + copy, + edit, + emoji, + flag, + link, + trash, +}; + +export default class PostOption extends PureComponent { + static propTypes = { + destructive: PropTypes.bool, + icon: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired, + text: PropTypes.string.isRequired, + }; + + render() { + const {destructive, icon, onPress, text} = this.props; + const image = icons[icon]; + + return ( + + + + + + + + + {text} + + + + + + + ); + } +} + +const style = StyleSheet.create({ + container: { + height: 51, + width: '100%', + }, + flex: { + flex: 1, + }, + destructive: { + color: '#D0021B', + }, + row: { + flex: 1, + flexDirection: 'row', + }, + icon: { + alignItems: 'center', + height: 50, + justifyContent: 'center', + width: 60, + }, + textContainer: { + justifyContent: 'center', + flex: 1, + height: 50, + marginRight: 5, + }, + text: { + color: '#000000', + fontSize: 16, + lineHeight: 19, + opacity: 0.9, + letterSpacing: -0.45, + }, + footer: { + height: 1, + marginLeft: 60, + borderBottomWidth: 1, + borderBottomColor: 'rgba(0, 0, 0 ,0.2)', + }, +}); diff --git a/app/screens/post_options/post_options.js b/app/screens/post_options/post_options.js new file mode 100644 index 000000000..4c6526b89 --- /dev/null +++ b/app/screens/post_options/post_options.js @@ -0,0 +1,358 @@ +// 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 {Alert, Clipboard, StyleSheet, View} from 'react-native'; +import {intlShape} from 'react-intl'; +import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; + +import SlideUpPanel from 'app/components/slide_up_panel'; +import {BOTTOM_MARGIN} from 'app/components/slide_up_panel/slide_up_panel'; + +import PostOption from './post_option'; + +const OPTION_HEIGHT = 50; +const INITIAL_OPTION_COUNT = 6; + +export default class PostOptions extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + addReaction: PropTypes.func.isRequired, + deletePost: PropTypes.func.isRequired, + flagPost: PropTypes.func.isRequired, + removePost: PropTypes.func.isRequired, + unflagPost: PropTypes.func.isRequired, + }).isRequired, + additionalOption: PropTypes.object, + canAddReaction: PropTypes.bool, + canDelete: PropTypes.bool, + canEdit: PropTypes.bool, + canEditUntil: PropTypes.number.isRequired, + channelIsReadOnly: PropTypes.bool, + currentTeamUrl: PropTypes.string.isRequired, + deviceHeight: PropTypes.number.isRequired, + hasBeenDeleted: PropTypes.bool, + isFlagged: PropTypes.bool, + isMyPost: PropTypes.bool, + managedConfig: PropTypes.object.isRequired, + navigator: PropTypes.object.isRequired, + post: PropTypes.object.isRequired, + showAddReaction: PropTypes.bool, + theme: PropTypes.object.isRequired, + }; + + static contextTypes = { + intl: intlShape.isRequired, + }; + + close = () => { + this.props.navigator.dismissModal({ + animationType: 'none', + }); + }; + + closeWithAnimation = () => { + if (this.slideUpPanel) { + this.slideUpPanel.getWrappedInstance().handleTouchEnd(); + } else { + this.close(); + } + }; + + getAddReactionOption = () => { + const {formatMessage} = this.context.intl; + const {canAddReaction, channelIsReadOnly, showAddReaction} = this.props; + + if (showAddReaction && canAddReaction && !channelIsReadOnly) { + return ( + + ); + } + + return null; + }; + + getCopyPermalink = () => { + const {formatMessage} = this.context.intl; + + return ( + + ); + }; + + getCopyText = () => { + const {formatMessage} = this.context.intl; + const {managedConfig, post} = this.props; + + if (managedConfig.copyAndPasteProtection !== 'true' && post.message) { + return ( + + ); + } + + return null; + }; + + getDeleteOption = () => { + const {formatMessage} = this.context.intl; + const {canDelete, hasBeenDeleted} = this.props; + + if (canDelete && !hasBeenDeleted) { + return ( + + ); + } + + return null; + }; + + getEditOption = () => { + const {formatMessage} = this.context.intl; + const {canEdit, canEditUntil} = this.props; + + if (canEdit && (canEditUntil === -1 || canEditUntil > Date.now())) { + return ( + + ); + } + + return null; + }; + + getFlagOption = () => { + const {formatMessage} = this.context.intl; + const {channelIsReadOnly, isFlagged} = this.props; + + if (channelIsReadOnly) { + return null; + } + + if (isFlagged) { + return ( + + ); + } + + return ( + + ); + }; + + getMyPostOptions = () => { + const actions = [ + this.getEditOption(), + this.getFlagOption(), + this.getAddReactionOption(), + this.getCopyPermalink(), + this.getCopyText(), + this.getDeleteOption(), + ]; + + return actions.filter((a) => a !== null); + }; + + getOthersPostOptions = () => { + const actions = [ + this.getFlagOption(), + this.getAddReactionOption(), + this.getCopyPermalink(), + this.getCopyText(), + this.getEditOption(), + this.getDeleteOption(), + ]; + + return actions.filter((a) => a !== null); + }; + + getPostOptions = () => { + const {isMyPost} = this.props; + + if (isMyPost) { + return this.getMyPostOptions(); + } + + return this.getOthersPostOptions(); + }; + + handleAddReaction = () => { + const {formatMessage} = this.context.intl; + const {navigator, theme} = this.props; + + this.close(); + requestAnimationFrame(() => { + MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => { + navigator.showModal({ + screen: 'AddReaction', + title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}), + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg, + }, + passProps: { + closeButton: source, + onEmojiPress: this.handleAddReactionToPost, + }, + }); + }); + }); + }; + + 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; + + actions.flagPost(post.id); + this.closeWithAnimation(); + }; + + handlePostDelete = () => { + const {formatMessage} = this.context.intl; + const {actions, isMyPost, 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: () => { + actions.deletePost(post); + if (isMyPost) { + actions.removePost(post); + } + this.closeWithAnimation(); + }, + }] + ); + }; + + handlePostEdit = () => { + const {intl} = this.context; + const {navigator, post, theme} = this.props; + + this.close(); + requestAnimationFrame(() => { + MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).then((source) => { + navigator.showModal({ + screen: 'EditPost', + title: intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}), + animated: true, + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg, + }, + passProps: { + post, + closeButton: source, + }, + }); + }); + }); + }; + + handleUnflagPost = () => { + const {actions, post} = this.props; + + actions.unflagPost(post.id); + this.closeWithAnimation(); + }; + + refSlideUpPanel = (r) => { + this.slideUpPanel = r; + }; + + render() { + const {deviceHeight} = this.props; + const options = this.getPostOptions(); + const initialPosition = (INITIAL_OPTION_COUNT + 1) * OPTION_HEIGHT; + const marginFromTop = deviceHeight - BOTTOM_MARGIN - ((options.length + 1) * OPTION_HEIGHT); + + return ( + + + {options} + + + ); + } +} + +const style = StyleSheet.create({ + flex: { + flex: 1, + }, +}); diff --git a/app/screens/reaction_list/reaction_list.js b/app/screens/reaction_list/reaction_list.js index 64ead7543..af226b5e1 100644 --- a/app/screens/reaction_list/reaction_list.js +++ b/app/screens/reaction_list/reaction_list.js @@ -46,7 +46,6 @@ export default class ReactionList extends PureComponent { this.contentOffsetY = -1; this.state = { - canDrag: true, allUserIds: getUniqueUserIds(reactions), reactions, reactionsByName, diff --git a/app/utils/bottom_sheet/bottom_sheet.android.js b/app/utils/bottom_sheet/bottom_sheet.android.js new file mode 100644 index 000000000..890420ff0 --- /dev/null +++ b/app/utils/bottom_sheet/bottom_sheet.android.js @@ -0,0 +1,5 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import RNBottomSheet from 'react-native-bottom-sheet'; +export default RNBottomSheet; diff --git a/app/utils/bottom_sheet/bottom_sheet.ios.js b/app/utils/bottom_sheet/bottom_sheet.ios.js new file mode 100644 index 000000000..0ba3abc9e --- /dev/null +++ b/app/utils/bottom_sheet/bottom_sheet.ios.js @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ActionSheetIOS} from 'react-native'; + +export default { + showBottomSheetWithOptions: (options, callback) => { + return ActionSheetIOS.showActionSheetWithOptions(options, callback); + }, + + showShareBottomSheetWithOptions: (options, failureCallback, successCallback) => { + return ActionSheetIOS.showShareActionSheetWithOptions(options, failureCallback, successCallback); + }, +}; diff --git a/app/utils/bottom_sheet/index.js b/app/utils/bottom_sheet/index.js new file mode 100644 index 000000000..6f745c8c3 --- /dev/null +++ b/app/utils/bottom_sheet/index.js @@ -0,0 +1,5 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import BottomSheet from './bottom_sheet'; +export default BottomSheet; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 4e97e6c76..d16c2741f 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -330,7 +330,7 @@ "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.post_info.add_reaction": "Add Reaction", - "mobile.post_info.copy_post": "Copy Post", + "mobile.post_info.copy_text": "Copy Text", "mobile.post_textbox.empty.message": "You are trying to send an empty message.\nPlease make sure you have a message or at least one attached file.", "mobile.post_textbox.empty.ok": "OK", "mobile.post_textbox.empty.title": "Empty Message", diff --git a/assets/base/images/post_menu/copy.png b/assets/base/images/post_menu/copy.png new file mode 100755 index 0000000000000000000000000000000000000000..09ce290f9854024d083d88720cc0ae0b04c72d2c GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv_!3HFka9+I#q!^2X+?^QKos)S9Px$r%6OXR9Fe^STRn+Fcb|*E&y8)Q!l`f3sfv@Y#F!!TO}3*XXwPv&IP(L^#&1A zhD?m@gv6RCD7@#yk(ajl)J75VmwsY9|G)qA?6k3iV4`-ii&vVaNnO|HyPp4MJdWcg z&-3>&xv?3PWm!~~s^BVKf3e=VDgaz!t2&dNN>5BW{a!Jzm{b92 zykBE372EdUGieb)yvE*2cH(Ok;MoGV@ie_T?Rv*wNlIg{C2%}c`Go8LOjmc-P$xc8 jay%;9I3P$&Vk=TV=bD-TJ6?ow00000NkvXXu0mjfJ6YOq literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/copy@3x.png b/assets/base/images/post_menu/copy@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..e5a2e89f0fd389c6605960fbb8f165ad7424aedd GIT binary patch literal 719 zcmV;=0xPx%iAh93RA>e5TCr*yK@jy_5(thuL4xuH0vlma5pZSPq{t7XNGI8D{R#d6L+Ydq zDe?;;7*qxKR+Ar4RHRFaT!bXQXKt5Ol4IdU^394F7JpNL5?uS)@yL&RwNf11Ta+jF25#{QVsfzoDBnd*nw^act9$z8JC0Z)zxnx=P4gLuSZqOF~ z#65O+O-j#TUH+7TQL^VChO-!z3P9v#B2`P?sDal9C z3H%!V6TWQ7nVT4h#XlZbz6{_*9pe}0kMQFTxzI0PdZuTdH!OvyBKila0o<26cSgejSZ zPH~i`WV}Xcj4&nB&?%16l#JIXjS;3~8al;Mnv(Gvr7^<)TUWlao;ujlSJ-ZzZD_;S z0q+zMA{LL$u1Q78h@hB~qVg#UHYF8_A%bE`ipr-b*pyTxh6su&DJq|$U{g|&7$PWA zC4U9kxxGY-g;+w2weWF^m;2rJ2YqfYY1)W^SRAfvzyGWzGVWn}AH-_gtVC$10lr#Y z7U>G^s-~?aHmQ(zh3P0IN`l0;HecR3NuBy$U^`C002ovPDHLkV1jR< BKYRcH literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/edit.png b/assets/base/images/post_menu/edit.png new file mode 100755 index 0000000000000000000000000000000000000000..597016cd46a9f229337ca4560b2dcda8f103d167 GIT binary patch literal 399 zcmV;A0dW3_P)Px$Nl8ROR5%fx)V)hWQ5?qcYelHNSwmPvLw`YA{S^h#-c$qUwOCg7-z?_K&4C4w48HKgXkf~6w5%zDOTd%gY+99v>=oF#=1!-2+>K39U zXern54w(*}N+Bg&LKoRu=1jsNV~i@}0�X>%cDCU*uulp#=L6IEOQYKEV_AP{2E8 zFb+Lzr|9~ea6HhPJc}A8p?4?;J@N|rz%m+eR*D{HtzLs{H=$(~5rtQ2MkmlrbXI*^ t>Nv&@e&AhtU#i&00u=tQ^7;h0lACfoL1yspe%Am1002ovPDHLkV1j_ntZe`Q literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/edit@2x.png b/assets/base/images/post_menu/edit@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..8baf8cbd16fe3dce21b0aa5ecf05e499d7f7f758 GIT binary patch literal 851 zcmV-Z1FZasP)Px&4M{{nR9Fecm)WaLQ5?rzL!nSelJSNRnWrR4TuCJVfO*JJ9x46<;z9Bt#e)~+ z!Mh?{Bzg28^E}T)hR>(YFZ-_UIZkKqeSGWFT6?Xvf4}ctd#!cXY3b5jjUZhcEP?NE z1x~_SXsUXV?HK&-gh#Y3hNh_>nc5-DyrF&yPQ%Aa`%g+|u5@YUDukK6)VpW0wo<=Y z>3>BzEYnwbRWsKh%vfH6S&-FG9-e{yhqE0;*0VG7i@^6GjJcRMCCPSI`fnxeg*>Ka zOx^;^;VFc%ZPY`nso4HG8j314<5o7fGqWDD8pOjnu-}=Q4eBV^cEN|D)R#b6GwZ4Z z9zc7{lm-@oyG%oSpb%-M9SmylwNOhd>2$1osFc?!dq8ew&G?!AWhN447y3II$3{n< zGBs1}Go?8u(%`uhGc+e1(8zNiUbde$@W-?=6R^EG0S&Er-H<71PO{MiUI5V1w zT}HKWr8Bdk&QQ}B7iL^D>4{xun2y6zGaEa0SACj6&l_-N-oi}Ctwzn5Toann)O5(L z)J*!ns9FL)i=Rg`el9;-Zlz{6*BQ|T8)n?%Q|f%L=CIVvmYSpgcDj|_Mb3;{+^wEl zshO>HMx4vIF!O@?q&nZLIjm7L^N1S%o1RnmM9!^7&CDlr+>yNjPRQ+q0F9biz`LKo zHRIJ{JmgkaB6vSBsCXY92>;ST26)r+ohL933Xx|O5Rq>m z6^(dLIS6rv*meL?EvDfM*zb?MF;J-D_tT+DRDUDIEzqR9GTGXwe+Ang!e}T|gNf#y z#ssd3P!B3+!S)~E`dA9zz_tg_3Wdx2j0rsAyCu>4^Xx3xe*?nIJ?f*N6rCZe36EEa zzP-}#7`Y20p%nEbYMdb}k_pf{8*PuEBurN_TUJ!{-)O_ejFk-M-T{Eyug{+nDgh1}-; d+h0&-;y)h<1YMYsksSa4002ovPDHLkV1l-*l~w=% literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/edit@3x.png b/assets/base/images/post_menu/edit@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..268cf72ec59973c151c19aeddafd4d1f1b47096f GIT binary patch literal 1278 zcmVPx(xJg7oRA>e5np>zARTRfNDhIk>$eEvnKS$Q23+tzd-mFE z?fw7Hp4qc!zB6z_J!}w~w}VST18fK1fN#MLP&*rd@e@Eo{>#B~{HKE2*jSi;3i1Sb z|5oBA)6+riy7r%}AIiT-M zqJ9RR?03}9&0#tQ|GObxm+$DD_@214L`mR&@ESNhhm~gUW8hE5tvRvD-Fc!s+23Gw zO%Ag&@c#iU{_UK&r0xqDE#6aGGJGO^2+RUmwgRUKQ+`h^OOzhEy6kj`-3&GXBW$If zZPA{_VeKk=W?IsJ0-nq|>Zb)pS|DBxaa!814RNbn%)Zc;tePe2D@Jd%gLsJcBfw6? zuN&_vCRK~>hXMCl$WI#c4R2ee-uC2T%OW>tuK6!v{5 z>T5`~dQP=FG-lc7He(=#oZWr@-@smx5pJ)lI*PNs-~ zvP7-NANPLUaC(Md3GsNTn-0pxRw3%SAV}h#FH5wW{47!W7ExxVULfDkl8&G4hE&uZRwS=MQe4u@iqcAFVyw)1Q!c`{qns>J z8_3k_d{K+U9M8nfjOd19cbmMuw>SQj2N^5@i>PH$#8_P`@{& zsPh;siuw~@-cLE5a9d1}zx*WX?oJX%GUok2!U+9>Uk}YOT@JQ1^e0hwb=k*=gbxQI zCd|suX}3?ytp*&R@tx3@zPV3tl~h%m{@uU?sQd){9(?i-fy+RiNz{A5_a#x&@)D}{ z`jU5a<~b1?sEoco0pZJ^)_SA?kO;z?Ahb93g$H@HUo;KFWB)-%~%y1&!i( zS%{IS+gwcY!?PGsH?|Si>_s~f$7jG|RBCaMNz`)S+YVBn3VLeyFuEsoQ4Skjp4(ll zX5?b!-2C_O&289Brrn=}U7>z}`b};6NYbollNJLbyRd^%*SL7gQ~5Wz zp@aM+A=fdACwF<9QMK~T4ml=jN}JrI&2Kr9&76@X<|?oRYySv|z7u>1ZUlp%cD5GACQPfHwn&q}Xi#g@ ouObhE2|%mhaXWqndT<7g0`ROyLQ?R0S^xk507*qoM6N<$f^U9k8vpPx$N=ZaPR5%fxl)Fj;K^R6it|Hiqh21uyV5^-?*auL8o!Dw&VIIa;u=UdU5|#n6 z3Z@imK?(7k(b>$diERAf?96|g+1dG%PWD?lu0uG2U08=V7{DFe!x-l3fQZQta0)xH z@iS`cH*Vfco!}ioIWszS8aL1Unq1jHQS;6>%rn1wqa4FmEMXCuZ=Lm74?Jts9+PKi zzOJ@@<5AZ2mUwsz*U`&p(iU_RpHpr9=_1xypFM)N5VgwW7`)n4+WL)0S!aDV1h4Tw zvTo>cY^3I{Z5IrUxJ7|0iFk`BcGwsHIr|0000Px&ElET{R9Fekmpy9~Q51#~KM)iPK~WcU3sb43LQJ8J-4^zi7K#wCvQR-#e}tsf zB8ow=2bD9pGHR)r!!#DF zbxn+7I&uQZU~V%L}o4zn;jlJ9J*}! z=xdCzMNmskwcBtP)L6Dpyi5K1$aAm^_JyB!V~e1c{c5*R=c-|4^D%dILH&aI1aotn ziLx*Jyc=7%D6y-lPkjfM1{7}FMQ~egW;zUfm3i)N+NV#z*s`rXG#eH&ks@5lWw=bo z0(=jz!fatYcECP;^fkuVGK6NUhFQG)1sBV_x6rPp+Ov*GEeZK0-EM?t%|iYQSV+67 ze^60-HY{X46%fYltS2Q`wt*y*IM#9|sRf3K&GOn4)N>Rm5#+t(s zG-u@^@NL0qioX{onS*6V$yPwuMPFlL3e9ECLTtyM@J}e;hcmG3q*hp5kPx)&`Cr=RA>d&nahhDRT#xHz9tTtNDw45aUem$NCY8bR0MZf%}PNG2Hh!$k`WgY z@)rbx8;PrdEXP1xD6U-SMi5_%h~VhxPSgoV5@R%uNqn4NQ>Xd*rfyeNcTG>{z~MgX zdz@Q!@2y+i6I^y5jrU5r=rh9SBT|rr05eW8x8UYvPR9FAM&n(mb#F-QsNE0o7ru zVO@PE#pB{dal^2v$QQ(Thp(#rf%uY$*e1XpKm3UkaEbJT1m#|Ej$#?h)Gn zafnCUn1{UNZw0#)g{c!0e5m$Atwg^u2yuxY^N}ANLxF9UdGEm2RcEb?0^}i|bE5-2 z1A#jc2f`daI8da)oyZs0D0&({A?9eDT0f}0SKJJcm;8Q0=xVfv;$r8{Y}P=Ox&!jZ zCxpIQcvV)u3)fU1_0-CVL`Pghi#bF0^FoYwU{s6U`KwCw_(r1>y;+>l6&tfXjw8@z zjp!`}XQIDE40&;F)O$pshXT6%VnuK1S&l=VN4#7k-L!1p6u%Ka77vSq06+YRLp)lKQKJI5u|JM}OCeUj=m99{pQxDLkGmN{aVFJy*@Gr|ojMFXxo>V~2eia`B^W z)Y;WPA&WJUQL?aR#VWie1K*XjysOH&xQ<=w>o)PLfKG1&=w}?AUCTi}r=tdT%jQqf zelFjy;Tuh`x35d$?v6%%>gcs!=V?A4I^OEzm|l8H9lr(NPgI{tjVjvM<7Yv{BW_)w z*S^o)rpA2mY z#0+Q{a09*?FhJBh!)~@M1d10!_t;jW-fawKDoACz+)Rjirx*;sG#bp?n+Z|x6$Zm` zz^L<%vF$jNQSW62!*9Pvyy(7hE~DOa42B=Ejd;<0<6K6)t8C5u8gKB@==exP??l^-qGWR-Z;H3o;r!^n0dwkM0b|6X z?R+1ojCfp)bU}M|1OT->3RoMG?2YR#E zp(__ei#w9BJgk21itXAg+1hghL638x6TN*A7y5Ns9?V$t!ivAnJIOHR_lo&&CLcP` z;ZY>3_w@uNr9t44!d*#$C<+5qRMB)+{RFtb>{J7YCIz1 z%UNr?$kXF2$IHCNA+~K4u6yS^;x3ZVDt~&M%8)M;~=J262d&t~v6Su93zW z5(l=!xT`QI&PW{kIK|G2O@KXq_!B2S4f2q$Hu`MqmvWSDB;xO7rHF0$#21e}RD{Lj{vF@z_51iSr`w8D5A}B4@5YLb%707*qo IM6N<$f}zy-i2wiq literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/flag.png b/assets/base/images/post_menu/flag.png new file mode 100755 index 0000000000000000000000000000000000000000..a986428c6575e278d3ae06352eb35cc45c539412 GIT binary patch literal 365 zcmV-z0h0cSP)Px$CrLy>R5%f1WHv{=|(ZsNF(IbEhsLCIR?*Q>-AO;!k1H||MD4l`8M|77#gc%sx zfsD66yb*{G0`XlS{tU#}fD%v;l%4M4G7P2(lxH=8*baz6#S=CF%bVa_qzhC5Vh8}S z0hG-Pq{#xTKwYPS7-S#_uLokX0Ib*n8K?ooe}VWq5QCizq)7q*1m`o2yA9v>00000 LNkvXXu0mjf0H=mf literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/flag@2x.png b/assets/base/images/post_menu/flag@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..dab7db2afa5183d9f2ad03839ad254ea62675fc9 GIT binary patch literal 612 zcmV-q0-ODbP)Px%9!W$&R9Fe^m@!KnK@`VNP1Mv9qJm%{5aaNBAcB?=!4DwB&k?_YTG*Ii zBO$GojUZZB2$~{Q8qq@~Y62-@5{$q9W(UU8&CTrXor~;)-@V!0_ujmHGrKplxm-%6 z>7Km0I9CMe3-}2SD$nSB5w78H$Pe@zsJk>+JucCXN@AI7=ay|$v&4lR*o!joJ+EOi zm0F|y9QxoBnCkiHclfnJY%z08b80$_GKT&da)i#qcSl;VdkTY~#5SNRnZ?^wq?$lV z=0h0kH(K*HmOP2&744)a_$^F7cPEUC%g?2!EbOl;O?q>jFk*$=ho?gl(Px&C`m*?RA>e5T0LkKK@>iXXd(g8LfZ(6g@FiGHi0y0#2-wj&0!^0f`tfHf|Y_+ zVhRmHl1dO25z@qnjS7mTT@aKs{=^WC|9)TGxjf(K-tFGZoU_b>FY{)0=DqjLo7uVS zZmpK28Z)pehsJhX+Y9sopMdAUU0?x-Ub?~HJn$X(sqh+g5|3I)@EM@V>H4XdSnB0^ z0l}YafxuVv;bfE%u8=w40Wb#41HpuQP%hCAs2>G30^5M=fX-;U^_Q?Y3SuJtv-;8vkiy{YOXbPd>`+KX1#VgOf-^gW9DL*NbIRp>}&#;3DQ zFn(M4JLk)xoK0Uo{$}?r8jk?800~ogG>tE7#r`azJCC-{tqcp9NfXZ$6uElJ>LkdO z!EIp4YO5t|!$(K%xXSrAxSLp6?W05b$q_L-m=oXfv@@&n|JfmqbSM42xF{7D7P@?V2N=~hNL~| zWEy`PX*^VNXj7ZgJ{k|UiY`}ecmfTtbYoVpquiY7JF?Pep?&bpnI)|}L9b(!d`i8? zva-@JuKLzjE;-k7pP;v!$-HQ<;398SW*PB@#WzonfG>b6(eFZNcNVxuMT_f9(*7kt zWuETeG|mE){BrC#FfD!gCP}A!ZGH&g3sPUg<$nhl0Ju&55TAQe zI^vfk1iu{F0`NxRN}7H%5Q?NBG!KOcD!!L%P%{5=CP{u%o%k`K7Tu=D=>Gp*op&X9 zi$9CHB-Mxd6VZoncYA@Tq#x1pjFCk1^v6-L6Tx0=ktF{=Y`l{f%jg`g z^XFMKVt!ZZfVp%B0e%Z(j+yt+p8`|BJ0MA#fxt8H3-mb&bQR%yr2qf`07*qoM6N<$ Ef_-Y4%K!iX literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/link.png b/assets/base/images/post_menu/link.png new file mode 100755 index 0000000000000000000000000000000000000000..503a31d4c9288ade2bbd42a848299b0ab3498f42 GIT binary patch literal 441 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 Z{s-xNIDCto-0uJY002ovPDHLkV1jf3#s>fZ literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/pin.png b/assets/base/images/post_menu/pin.png new file mode 100755 index 0000000000000000000000000000000000000000..a07aa27d72415172df6f2df112452c7fe7ff6f73 GIT binary patch literal 448 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 UVlm*Hq)$ literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/trash.png b/assets/base/images/post_menu/trash.png new file mode 100755 index 0000000000000000000000000000000000000000..e8f443bbf17c7e5a2a2c17ecb8b1fbe0e17d6b6b GIT binary patch literal 511 zcmVPx$xk*GpR5%fpRJ%$;K@i>9y=sDq?_gevg=is)g?@mgSotd!f}Nd>AE2G!2Z)uW zA7B#`1XBp2;i`x)NMZun9cQlD^{Tl(wz#u1bN1|+ndO4$uu3YwY7n|^V;a6 zp)8D!RVuFzdn$9~lFF^PK*WYIggv)u*F*&HsZkl5h_XgN20{2XFYShExj__J>qpr( z#t}FLg1Sy5zbc^VWCW?T7dQoIBn9DDf&gRC5jX_^RD$?J35)@G^GyLi9Kb)6Kp48r zDR3ni)lCX8jYvI?Sh%?<86#B(g08+evwFqkWGdpkC~oibQ}zZ@A3bmiX710bbamBh z>c~;P=ioW~0v$p7R$rS|6W9|Ax0EmYK+AYZ3js=7* zHy8x0g=613e%3pY7OhtBCkg|Dz2{YDYL~a+_#1HNo6*Ot{jmT5002ovPDHLkV1gZ3 B;Y|Pl literal 0 HcmV?d00001 diff --git a/assets/base/images/post_menu/trash@2x.png b/assets/base/images/post_menu/trash@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..532dd292c9fb8dfc6e68f67dbcdc2b598200c980 GIT binary patch literal 952 zcmV;p14sOcP)Px&a!Eu%R9FecSlv$4Kop*tmSvYHAn`(B2`CVMVA+Hx@gXoF-pDif1Y%;sg%9Bi znD_)F8{EVM4IwP3@rML~Eo(c+Gt*9YmYzblU9=Z;dVaohdd@dzr*PKC^u5{-GsHU0wwiJDz*+ z6dzp9_%`0(#ERaaE}3kaM-E`2d^l&hBLK-HZ{)SKBcE* zf)f5qSzLkb%G@*y{2OV(gCH1V_}uP zcISy?Y!l1(gN2B zIiMVleu%<6urY+&#a8(0N^)RyEUEx1eO5RD8{^h(8Dodxi3AqEH&4{WQU(%&!aOh- zIQx<dlSUZ0?B)L<&uoLF@#Bfs-{sKoyHj04J6%6&J-@03zH0ct}ZYwigN(lsjFyB*of!h zjU=Hd7ar3lcIM}Ydp!Dh*qxm%D-7kri7~9?WCHdy^jm&z9(Z5b9Th}fuGWPHtJH9_rrY63V7qq60%0B$~AJsD%!p~H1* z0yqr~2XeOnIiP*HnXJk9-4BzCx*^AT=LZsSkSyW1W8!!GqCNGDzGbxqg)82(4rO6| zno;V8TipN&&rlbCK~Z*>$b2sN>$=}sPqV=UFTd2kX3jgXe7O~^9V$7E$7g%w0HQtq agY+*)&MxJOyGN%00000Px)RY^oaRA>e5T1#&nMG&6qnT(x>*GcSyA&X^OQNY^B8m)$O^cei`0$J;Z#JD$Nv%)v8T-SvG{ zT~%E@yN2KtuATX{wO+6PW&pSu1aK}J68{bmcH%htbhFcHWzWqYh5RYmGwy|}cK`>U zV-Q@-o@tK~aT_6g{nlXjD|=+RPlX)VS$Juw*M9a81H3vN?PxXxa1dX-bYt`AyZu@8 zl}YdoVC7<0- z_eA|qGQT#QVPUv+TqD9Y;3r3(f>Z|H7n^HgcmwE(1s%{Z|K9yS?|@YU&AIyZ{s5ly zT~9#VK&nV}L4e_Q^unuNx>ANZhX7#1TaUn_5T7)u=*p~N@NveDc&ZfoK|AU>wX(&ZD+;GIm;qmCSQRoRo@ z>b)8SITyhvTB(XV_VI0$@*TxRMjk;N-crvC^WtFruyv@O)nyg&cwjiVEv`MLLK`<5 z8EF~kn0D>?Ppkr_Ru=e7TtyvoSTOQST` zV3rh8XAbVGlu%hAr@2HlLk(bU(Q-gwP+0`hTnj5=`V?yX-h9Si4-sW$5#U^ObogVv z)k?LmBsZKjpr+6f*+hXFTV)a8T+{=ctFLU#bbx@2GH)zOvCPZj0=}J}WX*JdfJ_AF za8X$VI2YMq7V)I{IT4^7GRpMgRha`87q|;pIg}Xzl*xgPim5UOI2UAe;amrJGo#Kp z*R0iN`UY-N=v^|!p}6$8WLcnP{GJ&B?toJ2cgk;OX$}wtia6bkg)Bdf(i~AVXX=oz zynfD%K)M@CEdRZLbI=Zi!N&Ptw6T-!nGUFKOdv;Ekve4Qh0HmqiS0L<&7S6;bkB5P ztYAV2qWrB?KoVuzjSTlBAP4vay&2I?LD?^McluL2`uAvPpMO z2kaf8-9G&no8$Jtsx1(wSOYSQ?Qlw2pR^i9Wb`AuEG8xW99_Pp8pK zYBs$ob!l_$#K9g!tGA^b+>`;tTwe59 pP)Z6BKDgQ6{iZ0^|N2#~z&|b^y51&f@M{17002ovPDHLkV1hg