[Gekidou MM-46585] Message Priority (Phase 2 - setting message priority) (#6656)

This commit is contained in:
Anurag Shivarathri
2022-11-23 14:52:05 +05:30
committed by GitHub
parent da31c9c3cf
commit d20da35205
16 changed files with 341 additions and 21 deletions

View File

@@ -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 = ({
<CompassIcon
name={icon!}
size={24}
color={destructive ? theme.dndIndicator : changeOpacity(theme.centerChannelColor, 0.64)}
color={iconColor || (destructive ? theme.dndIndicator : changeOpacity(theme.centerChannelColor, 0.64))}
/>
</View>
)}

View File

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

View File

@@ -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<React.SetStateAction<number>>;
@@ -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) && (
<View style={style.postPriorityLabel}>
<PostPriorityLabel label={postProps.priority}/>
</View>
)}
<PostInput
testID={postInputTestID}
channelId={channelId}
@@ -167,6 +188,9 @@ export default function DraftInput({
addFiles={addFiles}
updateValue={updateValue}
value={value}
postProps={postProps}
updatePostProps={updatePostProps}
canShowPostPriority={canShowPostPriority}
focus={focus}
/>
<SendAction

View File

@@ -34,6 +34,7 @@ type Props = {
keyboardTracker: RefObject<KeyboardTrackingViewRef>;
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}

View File

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

View File

@@ -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 (
<PostPriorityPicker
data={{
priority: postProps?.priority || '',
}}
onSubmit={handlePostPriorityPicker}
/>
);
}, [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 (
<TouchableWithFeedback
testID={testID}
onPress={onPress}
style={style.icon}
type={'opacity'}
>
<CompassIcon
name={iconName}
color={iconColor}
size={ICON_SIZE}
/>
</TouchableWithFeedback>
);
}

View File

@@ -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 && (
<PostPriorityAction
testID={postPriorityActionTestID}
postProps={postProps}
updatePostProps={updatePostProps}
/>
)}
</View>
);
}

View File

@@ -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<Post['props']>({});
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}
/>
);

View File

@@ -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 && (
<View style={style.postPriority}>
<PostPriorityLabel
label={post.props?.priority}

View File

@@ -222,9 +222,14 @@ const Post = ({
let header: ReactNode;
let postAvatar: ReactNode;
let consecutiveStyle: StyleProp<ViewStyle>;
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 = <View style={styles.consecutivePostContainer}/>;
} 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}
/>
);

View File

@@ -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<ViewStyle> = [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'});

View File

@@ -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<PostPriorityData>(defaultData);
const handleUpdatePriority = React.useCallback((priority: PostPriorityType) => {
onSubmit({priority});
}, [onSubmit]);
return (
<View style={style.container}>
{!isTablet &&
<View style={style.titleContainer}>
<FormattedText
id='post_priority.picker.title'
defaultMessage='Message priority'
style={style.title}
/>
<View style={style.betaContainer}>
<FormattedText
id='post_priority.picker.beta'
defaultMessage='BETA'
style={style.beta}
/>
</View>
</View>
}
<View style={style.optionsContainer}>
<PostPriorityPickerItem
action={handleUpdatePriority}
icon='message-text-outline'
label={intl.formatMessage({
id: 'post_priority.picker.label.standard',
defaultMessage: 'Standard',
})}
selected={data.priority === ''}
value={PostPriorityType.STANDARD}
/>
<PostPriorityPickerItem
action={handleUpdatePriority}
icon='alert-circle-outline'
iconColor={PostPriorityColors.IMPORTANT}
label={intl.formatMessage({
id: 'post_priority.picker.label.important',
defaultMessage: 'Important',
})}
selected={data.priority === PostPriorityType.IMPORTANT}
value={PostPriorityType.IMPORTANT}
/>
<PostPriorityPickerItem
action={handleUpdatePriority}
icon='alert-outline'
iconColor={PostPriorityColors.URGENT}
label={intl.formatMessage({
id: 'post_priority.picker.label.urgent',
defaultMessage: 'Urgent',
})}
selected={data.priority === PostPriorityType.URGENT}
value={PostPriorityType.URGENT}
/>
</View>
</View>
);
};
export default PostPriorityPicker;

View File

@@ -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<OptionItemProps, 'type'>) => {
const theme = useTheme();
const style = getStyle(theme);
const testID = `post_priority_picker_item.${props.value || 'standard'}`;
return (
<OptionItem
optionLabelTextStyle={style.optionLabelTextStyle}
testID={testID}
type='select'
{...props}
/>
);
};
export default PostPriorityPickerItem;