From 091bd8301bc7829daa462f153e24072d7b4818ef Mon Sep 17 00:00:00 2001 From: Avinash Lingaloo Date: Tue, 15 Feb 2022 00:26:16 +0400 Subject: [PATCH] MM-41602 Gekidou long press menu UI only (#5950) * skeleton in place * fix ts error * creating base option component * Added all options except reaction * moved options under /component/options * added destructive styling * skeleton - need polishing now * default emojis for quick reaction * rename files and small refactor * Properly close bottom sheet * redid reaction component * canSave, isSaved * canAddReaction condition * fix aligment * code clean up * fix opening on tablet * undo comment on local reaction action * undo needless formatting * clean up comment * shows selected reaction * fix marginTop and added title for Tablet * code clean up * investigating navigation * fixed navigation * Post options bottomSheet and renamed DrawerItem to MenuItem * renamed optionType to testID * update navigation_close_modal to close_bottom * removed context in favor of Pressable * Apply suggestions from code review Co-authored-by: Elias Nahum * removed theme prop from PickReaction * en.json and code fixes * removed post_options from screen/index * removed post_options from screens constant * Revert "removed post_options from screen/index" This reverts commit 24caa9773fef7f5355e8a3231f4b7e7afef2aa1d. * Revert "removed post_options from screens constant" This reverts commit 863e2faaf79819974dbb264d137fdcecc8066ec3. * fix theme import * remove useless margin * disabled post_options * refactored render method for post_options * fixing issue on iOS * corrections from PR review * fix for background on mobile * Fix stack navigation styles * i18n-extract output * Feedback review Co-authored-by: Elias Nahum --- .../__snapshots__/index.test.tsx.snap | 36 +++--- .../{drawer_item => menu_item}/index.test.tsx | 6 +- .../{drawer_item => menu_item}/index.tsx | 101 ++++++++-------- app/components/post_list/post/post.tsx | 24 ++-- app/constants/screens.ts | 2 + app/context/theme/index.tsx | 10 +- .../options/custom_status/index.tsx | 4 +- .../components/options/logout/index.tsx | 4 +- .../options/saved_messages/index.tsx | 4 +- .../components/options/settings/index.tsx | 4 +- .../options/user_presence/index.tsx | 4 +- .../components/options/your_profile/index.tsx | 4 +- app/screens/index.tsx | 8 +- app/screens/navigation.ts | 46 ++++---- .../components/options/base_option.tsx | 73 ++++++++++++ .../components/options/copy_link_option.tsx | 25 ++++ .../components/options/copy_text_option.tsx | 25 ++++ .../components/options/delete_post_option.tsx | 26 +++++ .../components/options/edit_option.tsx | 26 +++++ .../components/options/follow_option.tsx | 58 ++++++++++ .../components/options/mark_unread_option.tsx | 27 +++++ .../components/options/pin_channel_option.tsx | 48 ++++++++ .../components/options/reply_option.tsx | 25 ++++ .../components/options/save_option.tsx | 38 ++++++ .../reaction_bar/components/pick_reaction.tsx | 50 ++++++++ .../reaction_bar/components/reaction.tsx | 70 ++++++++++++ .../components/reaction_bar/index.ts | 41 +++++++ .../components/reaction_bar/reaction_bar.tsx | 98 ++++++++++++++++ app/screens/post_options/index.ts | 6 + app/screens/post_options/post_options.tsx | 108 ++++++++++++++++++ app/utils/theme/index.ts | 4 +- assets/base/i18n/en.json | 14 +++ package-lock.json | 1 + 33 files changed, 897 insertions(+), 123 deletions(-) rename app/components/{drawer_item => menu_item}/__snapshots__/index.test.tsx.snap (90%) rename app/components/{drawer_item => menu_item}/index.test.tsx (87%) rename app/components/{drawer_item => menu_item}/index.tsx (89%) create mode 100644 app/screens/post_options/components/options/base_option.tsx create mode 100644 app/screens/post_options/components/options/copy_link_option.tsx create mode 100644 app/screens/post_options/components/options/copy_text_option.tsx create mode 100644 app/screens/post_options/components/options/delete_post_option.tsx create mode 100644 app/screens/post_options/components/options/edit_option.tsx create mode 100644 app/screens/post_options/components/options/follow_option.tsx create mode 100644 app/screens/post_options/components/options/mark_unread_option.tsx create mode 100644 app/screens/post_options/components/options/pin_channel_option.tsx create mode 100644 app/screens/post_options/components/options/reply_option.tsx create mode 100644 app/screens/post_options/components/options/save_option.tsx create mode 100644 app/screens/post_options/components/reaction_bar/components/pick_reaction.tsx create mode 100644 app/screens/post_options/components/reaction_bar/components/reaction.tsx create mode 100644 app/screens/post_options/components/reaction_bar/index.ts create mode 100644 app/screens/post_options/components/reaction_bar/reaction_bar.tsx create mode 100644 app/screens/post_options/index.ts create mode 100644 app/screens/post_options/post_options.tsx diff --git a/app/components/drawer_item/__snapshots__/index.test.tsx.snap b/app/components/menu_item/__snapshots__/index.test.tsx.snap similarity index 90% rename from app/components/drawer_item/__snapshots__/index.test.tsx.snap rename to app/components/menu_item/__snapshots__/index.test.tsx.snap index ca7c2cce06..7f502a1373 100644 --- a/app/components/drawer_item/__snapshots__/index.test.tsx.snap +++ b/app/components/menu_item/__snapshots__/index.test.tsx.snap @@ -30,7 +30,6 @@ exports[`DrawerItem should match snapshot 1`] = ` { const baseProps = { @@ -22,7 +22,7 @@ describe('DrawerItem', () => { }; test('should match snapshot', () => { - const wrapper = renderWithIntl(); + const wrapper = renderWithIntl(); expect(wrapper.toJSON()).toMatchSnapshot(); }); @@ -34,7 +34,7 @@ describe('DrawerItem', () => { separator: false, }; const wrapper = renderWithIntl( - , + , ); expect(wrapper.toJSON()).toMatchSnapshot(); diff --git a/app/components/drawer_item/index.tsx b/app/components/menu_item/index.tsx similarity index 89% rename from app/components/drawer_item/index.tsx rename to app/components/menu_item/index.tsx index e3086b7d97..49aaab63a0 100644 --- a/app/components/drawer_item/index.tsx +++ b/app/components/menu_item/index.tsx @@ -2,17 +2,20 @@ // See LICENSE.txt for license information. import React, {ReactNode} from 'react'; -import {Platform, View} from 'react-native'; +import {Platform, StyleProp, View, ViewStyle} from 'react-native'; import CompassIcon from '@components/compass_icon'; import FormattedText from '@components/formatted_text'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -type DrawerItemProps = { +export const ITEM_HEIGHT = 50; + +type MenuItemProps = { centered?: boolean; defaultMessage?: string; i18nId?: string; + iconContainerStyle?: StyleProp; iconName?: string; isDestructor?: boolean; labelComponent?: ReactNode; @@ -23,11 +26,55 @@ type DrawerItemProps = { theme: Theme; }; -const DrawerItem = (props: DrawerItemProps) => { +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + flexDirection: 'row', + minHeight: ITEM_HEIGHT, + }, + iconContainer: { + width: 45, + height: ITEM_HEIGHT, + alignItems: 'center', + justifyContent: 'center', + marginLeft: 5, + }, + icon: { + color: changeOpacity(theme.centerChannelColor, 0.64), + fontSize: 24, + }, + wrapper: { + flex: 1, + }, + labelContainer: { + flex: 1, + justifyContent: 'center', + paddingTop: 14, + paddingBottom: 14, + }, + centerLabel: { + textAlign: 'center', + textAlignVertical: 'center', + }, + label: { + color: changeOpacity(theme.centerChannelColor, 0.5), + fontSize: 17, + textAlignVertical: 'center', + includeFontPadding: false, + }, + divider: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), + height: 1, + }, + }; +}); + +const MenuItem = (props: MenuItemProps) => { const { centered, defaultMessage = '', i18nId, + iconContainerStyle, iconName, isDestructor = false, labelComponent, @@ -87,7 +134,7 @@ const DrawerItem = (props: DrawerItemProps) => { > {icon && ( - + {icon} )} @@ -102,48 +149,4 @@ const DrawerItem = (props: DrawerItemProps) => { ); }; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - backgroundColor: theme.centerChannelBg, - flexDirection: 'row', - minHeight: 50, - }, - iconContainer: { - width: 45, - height: 50, - alignItems: 'center', - justifyContent: 'center', - marginLeft: 5, - }, - icon: { - color: changeOpacity(theme.centerChannelColor, 0.64), - fontSize: 24, - }, - wrapper: { - flex: 1, - }, - labelContainer: { - flex: 1, - justifyContent: 'center', - paddingTop: 14, - paddingBottom: 14, - }, - centerLabel: { - textAlign: 'center', - textAlignVertical: 'center', - }, - label: { - color: changeOpacity(theme.centerChannelColor, 0.5), - fontSize: 17, - textAlignVertical: 'center', - includeFontPadding: false, - }, - divider: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.2), - height: 1, - }, - }; -}); - -export default DrawerItem; +export default MenuItem; diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx index 7de035ad38..fc0cf921e6 100644 --- a/app/components/post_list/post/post.tsx +++ b/app/components/post_list/post/post.tsx @@ -13,7 +13,8 @@ import TouchableWithFeedback from '@components/touchable_with_feedback'; import * as Screens from '@constants/screens'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; -import {showModalOverCurrentContext} from '@screens/navigation'; +import {useIsTablet} from '@hooks/device'; +import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation'; import {fromAutoResponder, isFromWebhook, isPostPendingOrFailed, isSystemMessage} from '@utils/post'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -104,6 +105,7 @@ const Post = ({ const intl = useIntl(); const serverUrl = useServerUrl(); const theme = useTheme(); + const isTablet = useIsTablet(); const styles = getStyleSheet(theme); const isAutoResponder = fromAutoResponder(post); const isPendingOrFailed = isPostPendingOrFailed(post); @@ -162,18 +164,16 @@ const Post = ({ return; } - const screen = 'PostOptions'; - const passProps = { - location, - post, - showAddReaction, - }; - Keyboard.dismiss(); - const postOptionsRequest = requestAnimationFrame(() => { - showModalOverCurrentContext(screen, passProps); - cancelAnimationFrame(postOptionsRequest); - }); + + const passProps = {location, post}; + const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : ''; + + if (isTablet) { + showModal(Screens.POST_OPTIONS, title, passProps, bottomSheetModalOptions(theme, 'close-post-options')); + } else { + showModalOverCurrentContext(Screens.POST_OPTIONS, passProps); + } }; const highlightFlagged = isFlagged && !skipFlaggedHeader; diff --git a/app/constants/screens.ts b/app/constants/screens.ts index 596def55c8..553bf17e29 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -28,6 +28,7 @@ export const SETTINGS_SIDEBAR = 'SettingsSidebar'; export const SSO = 'SSO'; export const THREAD = 'Thread'; export const USER_PROFILE = 'UserProfile'; +export const POST_OPTIONS = 'PostOptions'; export default { ABOUT, @@ -57,4 +58,5 @@ export default { SSO, THREAD, USER_PROFILE, + POST_OPTIONS, }; diff --git a/app/context/theme/index.tsx b/app/context/theme/index.tsx index b225c2d997..c0cdf7dafe 100644 --- a/app/context/theme/index.tsx +++ b/app/context/theme/index.tsx @@ -45,10 +45,12 @@ const ThemeProvider = ({currentTeamId, children, themes}: Props) => { if (teamTheme?.value) { try { const theme = setThemeDefaults(JSON.parse(teamTheme.value) as Theme); - EphemeralStore.theme = theme; - requestAnimationFrame(() => { - setNavigationStackStyles(theme); - }); + if (theme !== EphemeralStore.theme) { + EphemeralStore.theme = theme; + requestAnimationFrame(() => { + setNavigationStackStyles(theme); + }); + } return theme; } catch { // no theme change diff --git a/app/screens/home/account/components/options/custom_status/index.tsx b/app/screens/home/account/components/options/custom_status/index.tsx index ab2d98455d..2df8d338f8 100644 --- a/app/screens/home/account/components/options/custom_status/index.tsx +++ b/app/screens/home/account/components/options/custom_status/index.tsx @@ -7,7 +7,7 @@ import {DeviceEventEmitter} from 'react-native'; import {updateLocalCustomStatus} from '@actions/local/user'; import {unsetCustomStatus} from '@actions/remote/user'; -import DrawerItem from '@components/drawer_item'; +import MenuItem from '@components/menu_item'; import {Events, Screens} from '@constants'; import {SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status'; import {useServerUrl} from '@context/server'; @@ -68,7 +68,7 @@ const CustomStatus = ({isCustomStatusExpirySupported, isTablet, currentUser}: Cu }), [isTablet]); return ( - { }), [serverDisplayName, serverUrl, intl]); return ( - diff --git a/app/screens/home/account/components/options/saved_messages/index.tsx b/app/screens/home/account/components/options/saved_messages/index.tsx index dcd197aabe..8ec189345a 100644 --- a/app/screens/home/account/components/options/saved_messages/index.tsx +++ b/app/screens/home/account/components/options/saved_messages/index.tsx @@ -4,8 +4,8 @@ import React, {useCallback} from 'react'; import {TextStyle} from 'react-native'; -import DrawerItem from '@components/drawer_item'; import FormattedText from '@components/formatted_text'; +import MenuItem from '@components/menu_item'; import {preventDoubleTap} from '@utils/tap'; type Props = { @@ -20,7 +20,7 @@ const SavedMessages = ({isTablet, style, theme}: Props) => { }), [isTablet]); return ( - { }), [isTablet]); return ( - { }, []); return ( - { }), [isTablet, theme]); return ( - ) if (Platform.OS === 'android') { return ( - + ) } @@ -186,9 +186,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { // case 'PinnedPosts': // screen = require('@screens/pinned_posts').default; // break; - // case 'PostOptions': - // screen = require('@screens/post_options').default; - // break; + case Screens.POST_OPTIONS: + screen = withServerDatabase(require('@screens/post_options').default); + break; // case 'ReactionList': // screen = require('@screens/reaction_list').default; // break; diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 1983d2919c..0ab7bbf778 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -75,6 +75,30 @@ export const loginAnimationOptions = () => { }; }; +export const bottomSheetModalOptions = (theme: Theme, closeButtonId: string) => { + const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.centerChannelColor); + return { + modalPresentationStyle: OptionsModalPresentationStyle.formSheet, + modal: { + swipeToDismiss: true, + }, + topBar: { + leftButtons: [{ + id: closeButtonId, + icon: closeButton, + testID: closeButtonId, + }], + leftButtonColor: changeOpacity(theme.centerChannelColor, 0.56), + background: { + color: theme.centerChannelBg, + }, + title: { + color: theme.centerChannelColor, + }, + }, + }; +}; + Navigation.setDefaultOptions({ layout: { @@ -565,32 +589,12 @@ export async function bottomSheet({title, renderContent, snapPoints, initialSnap const isTablet = Device.IS_TABLET && !isSplitView; if (isTablet) { - const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.centerChannelColor); showModal(Screens.BOTTOM_SHEET, title, { closeButtonId, initialSnapIndex, renderContent, snapPoints, - }, { - modalPresentationStyle: OptionsModalPresentationStyle.formSheet, - modal: { - swipeToDismiss: true, - }, - topBar: { - leftButtons: [{ - id: closeButtonId, - icon: closeButton, - testID: closeButtonId, - }], - leftButtonColor: changeOpacity(theme.centerChannelColor, 0.56), - background: { - color: theme.centerChannelBg, - }, - title: { - color: theme.centerChannelColor, - }, - }, - }); + }, bottomSheetModalOptions(theme, closeButtonId)); } else { showModalOverCurrentContext(Screens.BOTTOM_SHEET, { initialSnapIndex, diff --git a/app/screens/post_options/components/options/base_option.tsx b/app/screens/post_options/components/options/base_option.tsx new file mode 100644 index 0000000000..9c4bbe9a72 --- /dev/null +++ b/app/screens/post_options/components/options/base_option.tsx @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo} from 'react'; + +import FormattedText from '@components/formatted_text'; +import MenuItem from '@components/menu_item'; +import {useTheme} from '@context/theme'; +import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ + destructive: { + color: theme.dndIndicator, + }, + label: { + color: theme.centerChannelColor, + ...typography('Body', 200), + }, + iconContainerStyle: { + marginLeft: 0, + }, +})); + +type BaseOptionType = { + i18nId: string; + defaultMessage: string; + iconName: string; + onPress: () => void; + testID: string; + isDestructive?: boolean; +} + +const BaseOption = ({ + i18nId, + defaultMessage, + iconName, + onPress, + testID, + isDestructive = false, +}: BaseOptionType) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + + const labelStyles = useMemo(() => { + if (isDestructive) { + return [styles.label, styles.destructive]; + } + return styles.label; + }, [isDestructive, styles.label, styles.destructive]); + + const label = useMemo(() => ( + + ), [i18nId, defaultMessage, labelStyles]); + + return ( + + ); +}; +export default BaseOption; diff --git a/app/screens/post_options/components/options/copy_link_option.tsx b/app/screens/post_options/components/options/copy_link_option.tsx new file mode 100644 index 0000000000..c4cc538e77 --- /dev/null +++ b/app/screens/post_options/components/options/copy_link_option.tsx @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +const CopyPermalinkOption = () => { + const handleCopyLink = () => { + //todo: + }; + return ( + + ); +}; + +export default CopyPermalinkOption; diff --git a/app/screens/post_options/components/options/copy_text_option.tsx b/app/screens/post_options/components/options/copy_text_option.tsx new file mode 100644 index 0000000000..b9db26ad10 --- /dev/null +++ b/app/screens/post_options/components/options/copy_text_option.tsx @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +const CopyTextOption = () => { + const handleCopyText = () => { + //todo: + }; + return ( + + ); +}; + +export default CopyTextOption; diff --git a/app/screens/post_options/components/options/delete_post_option.tsx b/app/screens/post_options/components/options/delete_post_option.tsx new file mode 100644 index 0000000000..73782d0855 --- /dev/null +++ b/app/screens/post_options/components/options/delete_post_option.tsx @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +//fixme: wire up handleDeletePost +const DeletePostOption = () => { + const handleDeletePost = () => null; + + return ( + + ); +}; + +export default DeletePostOption; diff --git a/app/screens/post_options/components/options/edit_option.tsx b/app/screens/post_options/components/options/edit_option.tsx new file mode 100644 index 0000000000..6a3f2236d8 --- /dev/null +++ b/app/screens/post_options/components/options/edit_option.tsx @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +const EditOption = () => { + const handleEdit = () => { + //todo: + }; + + return ( + + ); +}; + +export default EditOption; diff --git a/app/screens/post_options/components/options/follow_option.tsx b/app/screens/post_options/components/options/follow_option.tsx new file mode 100644 index 0000000000..135da6af01 --- /dev/null +++ b/app/screens/post_options/components/options/follow_option.tsx @@ -0,0 +1,58 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {Screens} from '@constants'; +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +type FollowThreadOptionProps = { + thread?: any; + location?: typeof Screens[keyof typeof Screens]; +}; + +//todo: to implement CRT follow thread + +const FollowThreadOption = ({thread}: FollowThreadOptionProps) => { + let id: string; + let defaultMessage: string; + let icon: string; + + if (thread.is_following) { + icon = 'message-minus-outline'; + if (thread?.participants?.length) { + id = t('threads.unfollowThread'); + defaultMessage = 'Unfollow Thread'; + } else { + id = t('threads.unfollowMessage'); + defaultMessage = 'Unfollow Message'; + } + } else { + icon = 'message-plus-outline'; + if (thread?.participants?.length) { + id = t('threads.followThread'); + defaultMessage = 'Follow Thread'; + } else { + id = t('threads.followMessage'); + defaultMessage = 'Follow Message'; + } + } + + const handleToggleFollow = () => { + //todo: + }; + + return ( + + ); +}; + +export default FollowThreadOption; diff --git a/app/screens/post_options/components/options/mark_unread_option.tsx b/app/screens/post_options/components/options/mark_unread_option.tsx new file mode 100644 index 0000000000..745f4da578 --- /dev/null +++ b/app/screens/post_options/components/options/mark_unread_option.tsx @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +//fixme: wire up canMarkAsUnread +const MarkAsUnreadOption = () => { + const handleMarkUnread = () => { + //todo: + }; + + return ( + + ); +}; + +export default MarkAsUnreadOption; diff --git a/app/screens/post_options/components/options/pin_channel_option.tsx b/app/screens/post_options/components/options/pin_channel_option.tsx new file mode 100644 index 0000000000..e1c2799f7f --- /dev/null +++ b/app/screens/post_options/components/options/pin_channel_option.tsx @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +type PinChannelProps = { + isPostPinned: boolean; +} + +//fixme: wire up handlePinChannel +const PinChannelOption = ({isPostPinned}: PinChannelProps) => { + //todo: add useCallback for the handler callbacks + const handlePinPost = () => null; + const handleUnpinPost = () => null; + + let defaultMessage; + let id; + let key; + let onPress; + + if (isPostPinned) { + defaultMessage = 'Unpin from Channel'; + id = t('mobile.post_info.unpin'); + key = 'unpin'; + onPress = handleUnpinPost; + } else { + defaultMessage = 'Pin to Channel'; + id = t('mobile.post_info.pin'); + key = 'pin'; + onPress = handlePinPost; + } + + return ( + + ); +}; + +export default PinChannelOption; diff --git a/app/screens/post_options/components/options/reply_option.tsx b/app/screens/post_options/components/options/reply_option.tsx new file mode 100644 index 0000000000..ca29647cac --- /dev/null +++ b/app/screens/post_options/components/options/reply_option.tsx @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +const ReplyOption = () => { + const handleReply = () => { + //todo: + }; + return ( + + ); +}; + +export default ReplyOption; diff --git a/app/screens/post_options/components/options/save_option.tsx b/app/screens/post_options/components/options/save_option.tsx new file mode 100644 index 0000000000..b60baa61e7 --- /dev/null +++ b/app/screens/post_options/components/options/save_option.tsx @@ -0,0 +1,38 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {t} from '@i18n'; + +import BaseOption from './base_option'; + +type CopyTextProps = { + isSaved: boolean; +} + +const SaveOption = ({isSaved}: CopyTextProps) => { + const handleUnsavePost = () => { + //todo: + }; + + const handleSavePost = () => { + //todo: + }; + + const id = isSaved ? t('mobile.post_info.unsave') : t('mobile.post_info.save'); + const defaultMessage = isSaved ? 'Unsave' : 'Save'; + const onPress = isSaved ? handleUnsavePost : handleSavePost; + + return ( + + ); +}; + +export default SaveOption; diff --git a/app/screens/post_options/components/reaction_bar/components/pick_reaction.tsx b/app/screens/post_options/components/reaction_bar/components/pick_reaction.tsx new file mode 100644 index 0000000000..96f81fab13 --- /dev/null +++ b/app/screens/post_options/components/reaction_bar/components/pick_reaction.tsx @@ -0,0 +1,50 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {View} from 'react-native'; + +import CompassIcon from '@components/compass_icon'; +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.04), + alignItems: 'center', + justifyContent: 'center', + borderRadius: 4, + }, + icon: { + ...typography('Body', 1000), + color: theme.centerChannelColor, + }, + }; +}); + +type PickReactionProps = { + openEmojiPicker: () => void; + width: number; + height: number; +} +const PickReaction = ({openEmojiPicker, width, height}: PickReactionProps) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + return ( + + + + ); +}; + +export default PickReaction; diff --git a/app/screens/post_options/components/reaction_bar/components/reaction.tsx b/app/screens/post_options/components/reaction_bar/components/reaction.tsx new file mode 100644 index 0000000000..02ea30f4e0 --- /dev/null +++ b/app/screens/post_options/components/reaction_bar/components/reaction.tsx @@ -0,0 +1,70 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useMemo} from 'react'; +import {Pressable, PressableStateCallbackType, View} from 'react-native'; + +import Emoji from '@components/emoji'; +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + emoji: { + color: '#000', + fontWeight: 'bold', + }, + highlight: { + backgroundColor: changeOpacity(theme.linkColor, 0.1), + }, + reactionContainer: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.04), + borderRadius: 4, + alignItems: 'center', + justifyContent: 'center', + }, + }; +}); + +type ReactionProps = { + onPressReaction: (emoji: string) => void; + emoji: string; + iconSize: number; + containerSize: number; +} +const Reaction = ({onPressReaction, emoji, iconSize, containerSize}: ReactionProps) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + const handleReactionPressed = useCallback(() => { + onPressReaction(emoji); + }, [onPressReaction, emoji]); + + const highlightedStyle = useCallback(({pressed}: PressableStateCallbackType) => pressed && styles.highlight, [styles.highlight]); + const reactionStyle = useMemo(() => [ + styles.reactionContainer, + { + width: containerSize, + height: containerSize, + }, + ], [containerSize]); + + return ( + + + + + + ); +}; + +export default Reaction; diff --git a/app/screens/post_options/components/reaction_bar/index.ts b/app/screens/post_options/components/reaction_bar/index.ts new file mode 100644 index 0000000000..8bc576604f --- /dev/null +++ b/app/screens/post_options/components/reaction_bar/index.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import {of as of$} from 'rxjs'; +import {catchError, switchMap} from 'rxjs/operators'; + +import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import {safeParseJSON} from '@utils/helpers'; + +import ReactionBar from './reaction_bar'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type SystemModel from '@typings/database/models/servers/system'; + +const DEFAULT_EMOJIS = [ + 'thumbsup', + 'smiley', + 'white_check_mark', + 'heart', + 'eyes', + 'raised_hands', +]; + +const mergeRecentWithDefault = (recentEmojis: string[]) => { + const filterUsed = DEFAULT_EMOJIS.filter((e) => !recentEmojis.includes(e)); + return recentEmojis.concat(filterUsed).splice(0, 6); +}; + +const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({ + recentEmojis: database. + get(MM_TABLES.SERVER.SYSTEM). + findAndObserve(SYSTEM_IDENTIFIERS.RECENT_REACTIONS). + pipe( + switchMap((recent) => of$(mergeRecentWithDefault(safeParseJSON(recent.value) as string[]))), + catchError(() => of$(mergeRecentWithDefault([]))), + ), +})); + +export default withDatabase(enhanced(ReactionBar)); diff --git a/app/screens/post_options/components/reaction_bar/reaction_bar.tsx b/app/screens/post_options/components/reaction_bar/reaction_bar.tsx new file mode 100644 index 0000000000..1950757d8b --- /dev/null +++ b/app/screens/post_options/components/reaction_bar/reaction_bar.tsx @@ -0,0 +1,98 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import {useIntl} from 'react-intl'; +import {DeviceEventEmitter, useWindowDimensions, View} from 'react-native'; + +import CompassIcon from '@components/compass_icon'; +import {Events, Screens} from '@constants'; +import { + LARGE_CONTAINER_SIZE, + LARGE_ICON_SIZE, + REACTION_PICKER_HEIGHT, + SMALL_CONTAINER_SIZE, + SMALL_ICON_BREAKPOINT, + SMALL_ICON_SIZE, +} from '@constants/reaction_picker'; +import {useTheme} from '@context/theme'; +import {showModal} from '@screens/navigation'; +import EphemeralStore from '@store/ephemeral_store'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +import PickReaction from './components/pick_reaction'; +import Reaction from './components/reaction'; + +type QuickReactionProps = { + recentEmojis: string[]; +}; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + backgroundColor: theme.centerChannelBg, + flexDirection: 'row', + alignItems: 'center', + height: REACTION_PICKER_HEIGHT, + justifyContent: 'space-between', + }, + }; +}); + +const ReactionBar = ({recentEmojis = []}: QuickReactionProps) => { + const theme = useTheme(); + const intl = useIntl(); + const {width} = useWindowDimensions(); + const isSmallDevice = width < SMALL_ICON_BREAKPOINT; + const styles = getStyleSheet(theme); + + const handleEmojiPress = useCallback((emoji: string) => { + // eslint-disable-next-line no-console + console.log('>>> selected this emoji', emoji); + }, []); + + const openEmojiPicker = useCallback(async () => { + DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET); + await EphemeralStore.waitUntilScreensIsRemoved(Screens.BOTTOM_SHEET); + + const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor); + const screen = Screens.EMOJI_PICKER; + const title = intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}); + const passProps = {closeButton, onEmojiPress: handleEmojiPress}; + + showModal(screen, title, passProps); + }, [intl, theme]); + + let containerSize = LARGE_CONTAINER_SIZE; + let iconSize = LARGE_ICON_SIZE; + + if (isSmallDevice) { + containerSize = SMALL_CONTAINER_SIZE; + iconSize = SMALL_ICON_SIZE; + } + + return ( + + { + recentEmojis.map((emoji) => { + return ( + + ); + }) + } + + + ); +}; + +export default ReactionBar; diff --git a/app/screens/post_options/index.ts b/app/screens/post_options/index.ts new file mode 100644 index 0000000000..67eb0166f2 --- /dev/null +++ b/app/screens/post_options/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import PostOptions from './post_options'; + +export default PostOptions; diff --git a/app/screens/post_options/post_options.tsx b/app/screens/post_options/post_options.tsx new file mode 100644 index 0000000000..0330cf5049 --- /dev/null +++ b/app/screens/post_options/post_options.tsx @@ -0,0 +1,108 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {ITEM_HEIGHT} from '@components/menu_item'; +import {Screens} from '@constants'; +import BottomSheet from '@screens/bottom_sheet'; +import {isSystemMessage} from '@utils/post'; + +import CopyLinkOption from './components/options/copy_link_option'; +import CopyTextOption from './components/options/copy_text_option'; +import DeletePostOption from './components/options/delete_post_option'; +import EditOption from './components/options/edit_option'; +import FollowThreadOption from './components/options/follow_option'; +import MarkAsUnreadOption from './components/options/mark_unread_option'; +import PinChannelOption from './components/options/pin_channel_option'; +import ReplyOption from './components/options/reply_option'; +import SaveOption from './components/options/save_option'; +import ReactionBar from './components/reaction_bar'; + +import type PostModel from '@typings/database/models/servers/post'; + +//fixme: some props are optional - review them + +type PostOptionsProps = { + canAddReaction?: boolean; + canCopyPermalink?: boolean; + canCopyText?: boolean; + canDelete?: boolean; + canEdit?: boolean; + canEditUntil?: number; + canMarkAsUnread?: boolean; + canPin?: boolean; + canSave?: boolean; + canReply?: boolean; + isSaved?: boolean; + location: typeof Screens[keyof typeof Screens]; + post: PostModel; + thread?: Partial; +}; + +const PostOptions = ({ + canAddReaction = true, + canCopyPermalink = true, + canCopyText = true, + canDelete = true, + canEdit = true, + canEditUntil = -1, + canMarkAsUnread = true, + canPin = true, + canReply = true, + canSave = true, + isSaved = true, + location, + post, + thread, +}: PostOptionsProps) => { + const shouldRenderEdit = canEdit && (canEditUntil === -1 || canEditUntil > Date.now()); + const shouldRenderFollow = !(location !== Screens.CHANNEL || !thread); + + const snapPoints = [ + canAddReaction, canCopyPermalink, canCopyText, + canDelete, shouldRenderEdit, shouldRenderFollow, + canMarkAsUnread, canPin, canReply, canSave, + ].reduce((acc, v) => { + return v ? acc + 1 : acc; + }, 0); + + const renderContent = () => { + return ( + <> + {canAddReaction && } + {canReply && } + {shouldRenderFollow && + + } + {canMarkAsUnread && !isSystemMessage(post) && ( + + )} + {canCopyPermalink && } + {canSave && + + } + {canCopyText && } + {canPin && } + {shouldRenderEdit && } + {canDelete && } + + ); + }; + + return ( + + ); +}; + +export default PostOptions; diff --git a/app/utils/theme/index.ts b/app/utils/theme/index.ts index 6db0ba1a00..4bdfdadc57 100644 --- a/app/utils/theme/index.ts +++ b/app/utils/theme/index.ts @@ -5,7 +5,7 @@ import merge from 'deepmerge'; import {StatusBar, StyleSheet} from 'react-native'; import tinyColor from 'tinycolor2'; -import {Preferences, Screens} from '@constants'; +import {Preferences} from '@constants'; import {appearanceControlledScreens, mergeNavigationOptions} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; @@ -129,7 +129,7 @@ export function setNavigatorStyles(componentId: string, theme: Theme, additional export function setNavigationStackStyles(theme: Theme) { EphemeralStore.allNavigationComponentIds.forEach((componentId) => { - if (componentId !== Screens.BOTTOM_SHEET && !appearanceControlledScreens.includes(componentId)) { + if (!appearanceControlledScreens.includes(componentId)) { setNavigatorStyles(componentId, theme); } }); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 6c154b1bd5..a5d889023b 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -120,6 +120,7 @@ "emoji_skin.medium_light_skin_tone": "medium light skin tone", "emoji_skin.medium_skin_tone": "medium skin tone", "file_upload.fileAbove": "Files must be less than {max}", + "get_post_link_modal.title": "Copy Link", "intro.add_people": "Add People", "intro.channel_details": "Details", "intro.created_by": "created by {creator} on {date}.", @@ -259,6 +260,13 @@ "mobile.permission_denied_dismiss": "Don't Allow", "mobile.permission_denied_retry": "Settings", "mobile.post_info.add_reaction": "Add Reaction", + "mobile.post_info.copy_text": "Copy Text", + "mobile.post_info.mark_unread": "Mark as Unread", + "mobile.post_info.pin": "Pin to Channel", + "mobile.post_info.reply": "Reply", + "mobile.post_info.save": "Save", + "mobile.post_info.unpin": "Unpin from Channel", + "mobile.post_info.unsave": "Unsave", "mobile.post_pre_header.flagged": "Saved", "mobile.post_pre_header.pinned": "Pinned", "mobile.post_pre_header.pinned_flagged": "Pinned and Saved", @@ -368,6 +376,8 @@ "post_body.deleted": "(message deleted)", "post_info.auto_responder": "AUTOMATIC REPLY", "post_info.bot": "BOT", + "post_info.del": "Delete", + "post_info.edit": "Edit", "post_info.guest": "GUEST", "post_info.system": "System", "post_message_view.edited": "(edited)", @@ -397,6 +407,10 @@ "status_dropdown.set_ooo": "Out Of Office", "team_list.no_other_teams.description": "To join another team, ask a Team Admin for an invitation, or create your own team.", "team_list.no_other_teams.title": "No additional teams to join", + "threads.followMessage": "Follow Message", + "threads.followThread": "Follow Thread", + "threads.unfollowMessage": "Unfollow Message", + "threads.unfollowThread": "Unfollow Thread", "user.edit_profile.email.auth_service": "Login occurs through {service}. Email cannot be updated. Email address used for notifications is {email}.", "user.edit_profile.email.web_client": "Email must be updated using a web client or desktop application.", "user.settings.general.email": "Email", diff --git a/package-lock.json b/package-lock.json index 5c5f84d6df..e07fa2df9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "mattermost-mobile", "version": "2.0.0", "hasInstallScript": true, "license": "Apache 2.0",