From d20da3520524f100189f4fe9cd58b0e993c94309 Mon Sep 17 00:00:00 2001 From: Anurag Shivarathri Date: Wed, 23 Nov 2022 14:52:05 +0530 Subject: [PATCH] [Gekidou MM-46585] Message Priority (Phase 2 - setting message priority) (#6656) --- app/components/option_item/index.tsx | 4 +- .../draft_handler/draft_handler.tsx | 3 + .../post_draft/draft_input/index.tsx | 24 ++++ app/components/post_draft/post_draft.tsx | 3 + .../post_draft/quick_actions/index.ts | 3 +- .../post_priority_action/index.tsx | 84 ++++++++++++ .../quick_actions/quick_actions.tsx | 17 +++ .../post_draft/send_handler/send_handler.tsx | 16 ++- .../post_list/post/header/header.tsx | 8 +- app/components/post_list/post/post.tsx | 11 +- .../post_priority/post_priority_label.tsx | 12 +- .../post_priority_picker/index.tsx | 124 ++++++++++++++++++ .../post_priority_picker_item.tsx | 34 +++++ app/constants/post.ts | 13 +- app/screens/channel/channel.tsx | 1 + assets/base/i18n/en.json | 5 + 16 files changed, 341 insertions(+), 21 deletions(-) create mode 100644 app/components/post_draft/quick_actions/post_priority_action/index.tsx create mode 100644 app/components/post_priority/post_priority_picker/index.tsx create mode 100644 app/components/post_priority/post_priority_picker/post_priority_picker_item.tsx diff --git a/app/components/option_item/index.tsx b/app/components/option_item/index.tsx index 5d91feed5..33a4ebd4d 100644 --- a/app/components/option_item/index.tsx +++ b/app/components/option_item/index.tsx @@ -103,6 +103,7 @@ export type OptionItemProps = { description?: string; destructive?: boolean; icon?: string; + iconColor?: string; info?: string; inline?: boolean; label: string; @@ -124,6 +125,7 @@ const OptionItem = ({ description, destructive, icon, + iconColor, info, inline = false, label, @@ -239,7 +241,7 @@ const OptionItem = ({ )} diff --git a/app/components/post_draft/draft_handler/draft_handler.tsx b/app/components/post_draft/draft_handler/draft_handler.tsx index 1ac994c29..5de98330c 100644 --- a/app/components/post_draft/draft_handler/draft_handler.tsx +++ b/app/components/post_draft/draft_handler/draft_handler.tsx @@ -16,6 +16,7 @@ type Props = { channelId: string; cursorPosition: number; rootId?: string; + canShowPostPriority?: boolean; files?: FileInfo[]; maxFileCount: number; maxFileSize: number; @@ -40,6 +41,7 @@ export default function DraftHandler(props: Props) { channelId, cursorPosition, rootId = '', + canShowPostPriority, files, maxFileCount, maxFileSize, @@ -135,6 +137,7 @@ export default function DraftHandler(props: Props) { testID={testID} channelId={channelId} rootId={rootId} + canShowPostPriority={canShowPostPriority} // From draft handler cursorPosition={cursorPosition} diff --git a/app/components/post_draft/draft_input/index.tsx b/app/components/post_draft/draft_input/index.tsx index a5f827de8..cfb4a276a 100644 --- a/app/components/post_draft/draft_input/index.tsx +++ b/app/components/post_draft/draft_input/index.tsx @@ -6,6 +6,7 @@ import React, {useCallback, useRef} from 'react'; import {LayoutChangeEvent, Platform, ScrollView, View} from 'react-native'; import {Edge, SafeAreaView} from 'react-native-safe-area-context'; +import PostPriorityLabel from '@components/post_priority/post_priority_label'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -20,6 +21,11 @@ type Props = { channelId: string; rootId?: string; currentUserId: string; + canShowPostPriority?: boolean; + + // Post Props + postProps: Post['props']; + updatePostProps: (postProps: Post['props']) => void; // Cursor Position Handler updateCursorPosition: React.Dispatch>; @@ -77,6 +83,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { borderTopLeftRadius: 12, borderTopRightRadius: 12, }, + postPriorityLabel: { + marginLeft: 12, + marginTop: Platform.select({ + ios: 3, + android: 10, + }), + }, }; }); @@ -84,6 +97,7 @@ export default function DraftInput({ testID, channelId, currentUserId, + canShowPostPriority, files, maxMessageLength, rootId = '', @@ -96,6 +110,8 @@ export default function DraftInput({ updateCursorPosition, cursorPosition, updatePostInputTop, + postProps, + updatePostProps, setIsFocused, }: Props) { const theme = useTheme(); @@ -139,6 +155,11 @@ export default function DraftInput({ overScrollMode={'never'} disableScrollViewPanResponder={true} > + {Boolean(postProps.priority) && ( + + + + )} ; containerHeight: number; isChannelScreen: boolean; + canShowPostPriority?: boolean; } const {KEYBOARD_TRACKING_OFFSET} = ViewConstants; @@ -54,6 +55,7 @@ function PostDraft({ keyboardTracker, containerHeight, isChannelScreen, + canShowPostPriority, }: Props) { const [value, setValue] = useState(message); const [cursorPosition, setCursorPosition] = useState(message.length); @@ -109,6 +111,7 @@ function PostDraft({ cursorPosition={cursorPosition} files={files} rootId={rootId} + canShowPostPriority={canShowPostPriority} updateCursorPosition={setCursorPosition} updatePostInputTop={setPostInputTop} updateValue={setValue} diff --git a/app/components/post_draft/quick_actions/index.ts b/app/components/post_draft/quick_actions/index.ts index b5c7111f0..415d6359e 100644 --- a/app/components/post_draft/quick_actions/index.ts +++ b/app/components/post_draft/quick_actions/index.ts @@ -5,7 +5,7 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; import React from 'react'; -import {observeCanUploadFiles, observeMaxFileCount} from '@queries/servers/system'; +import {observeCanUploadFiles, observeIsPostPriorityEnabled, observeMaxFileCount} from '@queries/servers/system'; import QuickActions from './quick_actions'; @@ -17,6 +17,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { return { canUploadFiles, + isPostPriorityEnabled: observeIsPostPriorityEnabled(database), maxFileCount, }; }); diff --git a/app/components/post_draft/quick_actions/post_priority_action/index.tsx b/app/components/post_draft/quick_actions/post_priority_action/index.tsx new file mode 100644 index 000000000..cc6452f23 --- /dev/null +++ b/app/components/post_draft/quick_actions/post_priority_action/index.tsx @@ -0,0 +1,84 @@ +// 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 {StyleSheet} from 'react-native'; + +import CompassIcon from '@components/compass_icon'; +import PostPriorityPicker, {PostPriorityData} from '@components/post_priority/post_priority_picker'; +import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {ICON_SIZE} from '@constants/post_draft'; +import {useTheme} from '@context/theme'; +import {bottomSheet, dismissBottomSheet} from '@screens/navigation'; +import {changeOpacity} from '@utils/theme'; + +type Props = { + testID?: string; + postProps: Post['props']; + updatePostProps: (postProps: Post['props']) => void; +} + +const style = StyleSheet.create({ + icon: { + alignItems: 'center', + justifyContent: 'center', + padding: 10, + }, +}); + +export default function PostPriorityAction({ + testID, + postProps, + updatePostProps, +}: Props) { + const intl = useIntl(); + const theme = useTheme(); + + const handlePostPriorityPicker = useCallback((postPriorityData: PostPriorityData) => { + updatePostProps((oldPostProps: Post['props']) => ({ + ...oldPostProps, + ...postPriorityData, + })); + dismissBottomSheet(); + }, [updatePostProps]); + + const renderContent = useCallback(() => { + return ( + + ); + }, [handlePostPriorityPicker, postProps]); + + const onPress = useCallback(() => { + bottomSheet({ + title: intl.formatMessage({id: 'post_priority.picker.title', defaultMessage: 'Message priority'}), + renderContent, + snapPoints: [275, 10], + theme, + closeButtonId: 'post-priority-close-id', + }); + }, [intl, renderContent, theme]); + + const iconName = 'alert-circle-outline'; + const iconColor = changeOpacity(theme.centerChannelColor, 0.64); + + return ( + + + + ); +} diff --git a/app/components/post_draft/quick_actions/quick_actions.tsx b/app/components/post_draft/quick_actions/quick_actions.tsx index 4d63402d3..f5d882425 100644 --- a/app/components/post_draft/quick_actions/quick_actions.tsx +++ b/app/components/post_draft/quick_actions/quick_actions.tsx @@ -8,17 +8,22 @@ import CameraAction from './camera_quick_action'; import FileAction from './file_quick_action'; import ImageAction from './image_quick_action'; import InputAction from './input_quick_action'; +import PostPriorityAction from './post_priority_action'; type Props = { testID?: string; canUploadFiles: boolean; fileCount: number; + isPostPriorityEnabled: boolean; + canShowPostPriority?: boolean; maxFileCount: number; // Draft Handler value: string; updateValue: (value: string) => void; addFiles: (file: FileInfo[]) => void; + postProps: Post['props']; + updatePostProps: (postProps: Post['props']) => void; focus: () => void; } @@ -45,9 +50,13 @@ export default function QuickActions({ canUploadFiles, value, fileCount, + isPostPriorityEnabled, + canShowPostPriority, maxFileCount, updateValue, addFiles, + postProps, + updatePostProps, focus, }: Props) { const atDisabled = value[value.length - 1] === '@'; @@ -58,6 +67,7 @@ export default function QuickActions({ const fileActionTestID = `${testID}.file_action`; const imageActionTestID = `${testID}.image_action`; const cameraActionTestID = `${testID}.camera_action`; + const postPriorityActionTestID = `${testID}.post_priority_action`; const uploadProps = { disabled: !canUploadFiles, @@ -98,6 +108,13 @@ export default function QuickActions({ testID={cameraActionTestID} {...uploadProps} /> + {isPostPriorityEnabled && canShowPostPriority && ( + + )} ); } diff --git a/app/components/post_draft/send_handler/send_handler.tsx b/app/components/post_draft/send_handler/send_handler.tsx index 5ad1c5c41..b17b6c721 100644 --- a/app/components/post_draft/send_handler/send_handler.tsx +++ b/app/components/post_draft/send_handler/send_handler.tsx @@ -29,6 +29,7 @@ type Props = { testID?: string; channelId: string; rootId: string; + canShowPostPriority?: boolean; setIsFocused: (isFocused: boolean) => void; // From database @@ -64,6 +65,7 @@ export default function SendHandler({ membersCount = 0, cursorPosition, rootId, + canShowPostPriority, useChannelMentions, userIsOutOfOffice, customEmojis, @@ -82,6 +84,8 @@ export default function SendHandler({ const [channelTimezoneCount, setChannelTimezoneCount] = useState(0); const [sendingMessage, setSendingMessage] = useState(false); + const [postProps, setPostProps] = useState({}); + const canSend = useCallback(() => { if (sendingMessage) { return false; @@ -114,14 +118,19 @@ export default function SendHandler({ channel_id: channelId, root_id: rootId, message: value, - }; + } as Post; + + if (Object.keys(postProps).length) { + post.props = postProps; + } createPost(serverUrl, post, postFiles); clearDraft(); setSendingMessage(false); + setPostProps({}); DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL); - }, [files, currentUserId, channelId, rootId, value, clearDraft]); + }, [files, currentUserId, channelId, rootId, value, clearDraft, postProps]); const showSendToAllOrChannelOrHereAlert = useCallback((calculatedMembersCount: number, atHere: boolean) => { const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(intl, calculatedMembersCount, Boolean(isTimezoneEnabled), channelTimezoneCount, atHere); @@ -281,6 +290,7 @@ export default function SendHandler({ channelId={channelId} currentUserId={currentUserId} rootId={rootId} + canShowPostPriority={canShowPostPriority} cursorPosition={cursorPosition} updateCursorPosition={updateCursorPosition} value={value} @@ -292,6 +302,8 @@ export default function SendHandler({ canSend={canSend()} maxMessageLength={maxMessageLength} updatePostInputTop={updatePostInputTop} + postProps={postProps} + updatePostProps={setPostProps} setIsFocused={setIsFocused} /> ); diff --git a/app/components/post_list/post/header/header.tsx b/app/components/post_list/post/header/header.tsx index 700cf1370..fafe6035b 100644 --- a/app/components/post_list/post/header/header.tsx +++ b/app/components/post_list/post/header/header.tsx @@ -33,13 +33,13 @@ type HeaderProps = { isEphemeral: boolean; isMilitaryTime: boolean; isPendingOrFailed: boolean; - isPostPriorityEnabled: boolean; isSystemPost: boolean; isTimezoneEnabled: boolean; isWebHook: boolean; location: string; post: PostModel; rootPostAuthor?: UserModel; + showPostPriority: boolean; shouldRenderReplyButton?: boolean; teammateNameDisplay: string; } @@ -78,8 +78,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { const Header = (props: HeaderProps) => { const { author, commentCount = 0, currentUser, enablePostUsernameOverride, isAutoResponse, isCRTEnabled, isCustomStatusEnabled, - isEphemeral, isMilitaryTime, isPendingOrFailed, isPostPriorityEnabled, isSystemPost, isTimezoneEnabled, isWebHook, - location, post, rootPostAuthor, shouldRenderReplyButton, teammateNameDisplay, + isEphemeral, isMilitaryTime, isPendingOrFailed, isSystemPost, isTimezoneEnabled, isWebHook, + location, post, rootPostAuthor, showPostPriority, shouldRenderReplyButton, teammateNameDisplay, } = props; const theme = useTheme(); const style = getStyleSheet(theme); @@ -132,7 +132,7 @@ const Header = (props: HeaderProps) => { style={style.time} testID='post_header.date_time' /> - {Boolean(isPostPriorityEnabled && post.props?.priority) && ( + {showPostPriority && ( ; - const isProrityPost = Boolean(isPostPriorityEnabled && post.props?.priority); + + // If the post is a priority post: + // 1. Show the priority label in channel screen + // 2. Show the priority label in thread screen for the root post + const showPostPriority = Boolean(isPostPriorityEnabled && post.props?.priority) && (location !== Screens.THREAD || !post.rootId); + const sameSequence = hasReplies ? (hasReplies && post.rootId) : !post.rootId; - if (!isProrityPost && hasSameRoot && isConsecutivePost && sameSequence) { + if (!showPostPriority && hasSameRoot && isConsecutivePost && sameSequence) { consecutiveStyle = styles.consective; postAvatar = ; } else { @@ -256,13 +261,13 @@ const Post = ({ differentThreadSequence={differentThreadSequence} isAutoResponse={isAutoResponder} isCRTEnabled={isCRTEnabled} - isPostPriorityEnabled={isPostPriorityEnabled} isEphemeral={isEphemeral} isPendingOrFailed={isPendingOrFailed} isSystemPost={isSystemPost} isWebHook={isWebHook} location={location} post={post} + showPostPriority={showPostPriority} shouldRenderReplyButton={shouldRenderReplyButton} /> ); diff --git a/app/components/post_priority/post_priority_label.tsx b/app/components/post_priority/post_priority_label.tsx index 8eb35505b..3432b6554 100644 --- a/app/components/post_priority/post_priority_label.tsx +++ b/app/components/post_priority/post_priority_label.tsx @@ -6,22 +6,22 @@ import {useIntl} from 'react-intl'; import {StyleProp, StyleSheet, Text, View, ViewStyle} from 'react-native'; import CompassIcon from '@components/compass_icon'; -import {PostPriorityTypes} from '@constants/post'; +import {PostPriorityColors, PostPriorityType} from '@constants/post'; import {typography} from '@utils/typography'; const style = StyleSheet.create({ container: { + alignSelf: 'flex-start', flexDirection: 'row', borderRadius: 4, alignItems: 'center', - height: 16, paddingHorizontal: 4, }, urgent: { - backgroundColor: '#D24B4E', + backgroundColor: PostPriorityColors.URGENT, }, important: { - backgroundColor: '#5D89EA', + backgroundColor: PostPriorityColors.IMPORTANT, }, label: { color: '#fff', @@ -35,7 +35,7 @@ const style = StyleSheet.create({ }); type Props = { - label: string; + label: PostPriorityType; }; const PostPriorityLabel = ({label}: Props) => { @@ -44,7 +44,7 @@ const PostPriorityLabel = ({label}: Props) => { const containerStyle: StyleProp = [style.container]; let iconName = ''; let labelText = ''; - if (label === PostPriorityTypes.URGENT) { + if (label === PostPriorityType.URGENT) { containerStyle.push(style.urgent); iconName = 'alert-outline'; labelText = intl.formatMessage({id: 'post_priority.label.urgent', defaultMessage: 'URGENT'}); diff --git a/app/components/post_priority/post_priority_picker/index.tsx b/app/components/post_priority/post_priority_picker/index.tsx new file mode 100644 index 000000000..9de0b58d0 --- /dev/null +++ b/app/components/post_priority/post_priority_picker/index.tsx @@ -0,0 +1,124 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {useIntl} from 'react-intl'; +import {View} from 'react-native'; + +import FormattedText from '@components/formatted_text'; +import {PostPriorityColors, PostPriorityType} from '@constants/post'; +import {useTheme} from '@context/theme'; +import {useIsTablet} from '@hooks/device'; +import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +import PostPriorityPickerItem from './post_priority_picker_item'; + +export type PostPriorityData = { + priority: PostPriorityType; +}; + +type Props = { + data: PostPriorityData; + onSubmit: (data: PostPriorityData) => void; +}; + +const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({ + container: { + backgroundColor: theme.centerChannelBg, + height: 200, + }, + titleContainer: { + alignItems: 'center', + flexDirection: 'row', + }, + title: { + color: theme.centerChannelColor, + ...typography('Body', 600, 'SemiBold'), + }, + betaContainer: { + backgroundColor: PostPriorityColors.IMPORTANT, + borderRadius: 4, + paddingHorizontal: 4, + marginLeft: 8, + }, + beta: { + color: '#fff', + ...typography('Body', 25, 'SemiBold'), + }, + + optionsContainer: { + paddingVertical: 12, + }, +})); + +const PostPriorityPicker = ({data, onSubmit}: Props) => { + const intl = useIntl(); + const theme = useTheme(); + const isTablet = useIsTablet(); + const style = getStyle(theme); + + // For now, we just have one option but the spec suggest we have more in the next phase + // const [data, setData] = React.useState(defaultData); + + const handleUpdatePriority = React.useCallback((priority: PostPriorityType) => { + onSubmit({priority}); + }, [onSubmit]); + + return ( + + {!isTablet && + + + + + + + } + + + + + + + ); +}; + +export default PostPriorityPicker; diff --git a/app/components/post_priority/post_priority_picker/post_priority_picker_item.tsx b/app/components/post_priority/post_priority_picker/post_priority_picker_item.tsx new file mode 100644 index 000000000..7c613ae29 --- /dev/null +++ b/app/components/post_priority/post_priority_picker/post_priority_picker_item.tsx @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import OptionItem, {OptionItemProps} from '@components/option_item'; +import {useTheme} from '@context/theme'; +import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({ + optionLabelTextStyle: { + color: theme.centerChannelColor, + ...typography('Body', 200, 'Regular'), + }, +})); + +const PostPriorityPickerItem = (props: Omit) => { + const theme = useTheme(); + const style = getStyle(theme); + + const testID = `post_priority_picker_item.${props.value || 'standard'}`; + + return ( + + ); +}; + +export default PostPriorityPickerItem; diff --git a/app/constants/post.ts b/app/constants/post.ts index 036336858..99a6083dc 100644 --- a/app/constants/post.ts +++ b/app/constants/post.ts @@ -34,17 +34,22 @@ export const PostTypes: Record = { CUSTOM_CALLS: 'custom_calls', }; -export const PostPriorityTypes: Record = { - URGENT: 'urgent', - IMPORTANT: 'important', +export const PostPriorityColors = { + URGENT: '#D24B4E', + IMPORTANT: '#5D89EA', }; +export enum PostPriorityType { + STANDARD = '', + URGENT = 'urgent', + IMPORTANT = 'important', +} + export const POST_TIME_TO_FAIL = 10000; export default { POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5, POST_TYPES: PostTypes, - POST_PRIORITY_TYPES: PostPriorityTypes, USER_ACTIVITY_POST_TYPES: [ PostTypes.ADD_TO_CHANNEL, PostTypes.JOIN_CHANNEL, diff --git a/app/screens/channel/channel.tsx b/app/screens/channel/channel.tsx index 9e04cbd81..05c8c12dc 100644 --- a/app/screens/channel/channel.tsx +++ b/app/screens/channel/channel.tsx @@ -166,6 +166,7 @@ const Channel = ({ testID='channel.post_draft' containerHeight={containerHeight} isChannelScreen={true} + canShowPostPriority={true} /> } diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index d7ae5cfcf..783196678 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -695,6 +695,11 @@ "post_message_view.edited": "(edited)", "post_priority.label.important": "IMPORTANT", "post_priority.label.urgent": "URGENT", + "post_priority.picker.beta": "BETA", + "post_priority.picker.label.important": "Important", + "post_priority.picker.label.standard": "Standard", + "post_priority.picker.label.urgent": "Urgent", + "post_priority.picker.title": "Message priority", "post.options.title": "Options", "post.reactions.title": "Reactions", "posts_view.newMsg": "New Messages",