mattermost-mobile/app/components/post_list/post/post.tsx
Kyriakos Z bf5783252e
MM-49219: fixes post priority (#6880)
We have changed how priority gets saved in the server, so now instead of
post.props we are using post.metadata.priority.
This commit adds the relevant changes for posts' priority to work in the
mobile app.
2022-12-20 21:54:25 +02:00

371 lines
13 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactNode, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, StyleProp, View, ViewStyle, TouchableHighlight} from 'react-native';
import {removePost} from '@actions/local/post';
import {showPermalink} from '@actions/remote/permalink';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import CallsCustomMessage from '@calls/components/calls_custom_message';
import {isCallsCustomMessage} from '@calls/utils';
import SystemAvatar from '@components/system_avatar';
import SystemHeader from '@components/system_header';
import {POST_TIME_TO_FAIL} from '@constants/post';
import * as Screens from '@constants/screens';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
import {fromAutoResponder, isFromWebhook, isPostFailed, isPostPendingOrFailed, isSystemMessage} from '@utils/post';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import Avatar from './avatar';
import Body from './body';
import Footer from './footer';
import Header from './header';
import PreHeader from './pre_header';
import SystemMessage from './system_message';
import UnreadDot from './unread_dot';
import type PostModel from '@typings/database/models/servers/post';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
import type {SearchPattern} from '@typings/global/markdown';
type PostProps = {
appsEnabled: boolean;
canDelete: boolean;
currentUser: UserModel;
differentThreadSequence: boolean;
hasFiles: boolean;
hasReplies: boolean;
highlight?: boolean;
highlightPinnedOrSaved?: boolean;
highlightReplyBar: boolean;
isConsecutivePost?: boolean;
isCRTEnabled?: boolean;
isEphemeral: boolean;
isFirstReply?: boolean;
isSaved?: boolean;
isJumboEmoji: boolean;
isLastReply?: boolean;
isPostAddChannelMember: boolean;
isPostPriorityEnabled: boolean;
location: string;
post: PostModel;
rootId?: string;
previousPost?: PostModel;
hasReactions: boolean;
searchPatterns?: SearchPattern[];
shouldRenderReplyButton?: boolean;
showAddReaction?: boolean;
skipSavedHeader?: boolean;
skipPinnedHeader?: boolean;
style?: StyleProp<ViewStyle>;
testID?: string;
thread?: ThreadModel;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
consecutive: {marginTop: 0},
consecutivePostContainer: {
marginBottom: 10,
marginRight: 10,
marginLeft: Platform.select({ios: 34, android: 33}),
marginTop: 10,
},
container: {flexDirection: 'row'},
highlight: {backgroundColor: changeOpacity(theme.mentionHighlightBg, 0.5)},
highlightBar: {
backgroundColor: theme.mentionHighlightBg,
opacity: 1,
},
highlightPinnedOrSaved: {
backgroundColor: changeOpacity(theme.mentionHighlightBg, 0.2),
},
pendingPost: {opacity: 0.5},
postContent: {paddingHorizontal: 16},
postStyle: {
overflow: 'hidden',
flex: 1,
},
profilePictureContainer: {
marginBottom: 5,
marginRight: 10,
marginTop: 10,
},
rightColumn: {
flex: 1,
flexDirection: 'column',
},
rightColumnPadding: {paddingBottom: 3},
};
});
const Post = ({
appsEnabled, canDelete, currentUser, differentThreadSequence, hasFiles, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
isCRTEnabled, isConsecutivePost, isEphemeral, isFirstReply, isSaved, isJumboEmoji, isLastReply, isPostAddChannelMember, isPostPriorityEnabled,
location, post, rootId, hasReactions, searchPatterns, shouldRenderReplyButton, skipSavedHeader, skipPinnedHeader, showAddReaction = true, style,
testID, thread, previousPost,
}: PostProps) => {
const pressDetected = useRef(false);
const intl = useIntl();
const serverUrl = useServerUrl();
const theme = useTheme();
const isTablet = useIsTablet();
const styles = getStyleSheet(theme);
const isAutoResponder = fromAutoResponder(post);
const isPendingOrFailed = isPostPendingOrFailed(post);
const isFailed = isPostFailed(post);
const isSystemPost = isSystemMessage(post);
const isCallsPost = isCallsCustomMessage(post);
const hasBeenDeleted = (post.deleteAt !== 0);
const isWebHook = isFromWebhook(post);
const hasSameRoot = useMemo(() => {
if (isFirstReply) {
return false;
} else if (!post.rootId && !previousPost?.rootId && isConsecutivePost) {
return true;
} else if (post.rootId) {
return true;
}
return false;
}, [isConsecutivePost, post, previousPost, isFirstReply]);
const handlePostPress = () => {
if ([Screens.SAVED_MESSAGES, Screens.MENTIONS, Screens.SEARCH, Screens.PINNED_MESSAGES].includes(location)) {
showPermalink(serverUrl, '', post.id);
return;
}
const isValidSystemMessage = isAutoResponder || !isSystemPost;
if (isEphemeral || hasBeenDeleted) {
removePost(serverUrl, post);
} else if (isValidSystemMessage && !hasBeenDeleted && !isPendingOrFailed) {
if ([Screens.CHANNEL, Screens.PERMALINK].includes(location)) {
const postRootId = post.rootId || post.id;
fetchAndSwitchToThread(serverUrl, postRootId);
}
}
setTimeout(() => {
pressDetected.current = false;
}, 300);
};
const handlePress = preventDoubleTap(() => {
pressDetected.current = true;
if (post) {
Keyboard.dismiss();
setTimeout(handlePostPress, 300);
}
});
const showPostOptions = () => {
if (!post) {
return;
}
if (isSystemPost && (!canDelete || hasBeenDeleted)) {
return;
}
if (isPendingOrFailed || isEphemeral) {
return;
}
Keyboard.dismiss();
const passProps = {sourceScreen: location, post, showAddReaction, serverUrl};
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, bottomSheetModalOptions(theme));
}
};
const [, rerender] = useState(false);
useEffect(() => {
let t: NodeJS.Timeout|undefined;
if (post.pendingPostId === post.id && !isFailed) {
t = setTimeout(() => rerender(true), POST_TIME_TO_FAIL - (Date.now() - post.updateAt));
}
return () => {
if (t) {
clearTimeout(t);
}
};
}, [post.id]);
const highlightSaved = isSaved && !skipSavedHeader;
const hightlightPinned = post.isPinned && !skipPinnedHeader;
const itemTestID = `${testID}.${post.id}`;
const rightColumnStyle: StyleProp<ViewStyle> = [styles.rightColumn, (Boolean(post.rootId) && isLastReply && styles.rightColumnPadding)];
const pendingPostStyle: StyleProp<ViewStyle> | undefined = isPendingOrFailed ? styles.pendingPost : undefined;
let highlightedStyle: StyleProp<ViewStyle>;
if (highlight) {
highlightedStyle = styles.highlight;
} else if ((highlightSaved || hightlightPinned) && highlightPinnedOrSaved) {
highlightedStyle = styles.highlightPinnedOrSaved;
}
let header: ReactNode;
let postAvatar: ReactNode;
let consecutiveStyle: StyleProp<ViewStyle>;
// 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.metadata?.priority?.priority) && (location !== Screens.THREAD || !post.rootId);
const sameSequence = hasReplies ? (hasReplies && post.rootId) : !post.rootId;
if (!showPostPriority && hasSameRoot && isConsecutivePost && sameSequence) {
consecutiveStyle = styles.consecutive;
postAvatar = <View style={styles.consecutivePostContainer}/>;
} else {
postAvatar = (
<View style={[styles.profilePictureContainer, pendingPostStyle]}>
{(isAutoResponder || isSystemPost) ? (
<SystemAvatar theme={theme}/>
) : (
<Avatar
isAutoReponse={isAutoResponder}
location={location}
post={post}
/>
)}
</View>
);
if (isSystemPost && !isAutoResponder) {
header = (
<SystemHeader
createAt={post.createAt}
theme={theme}
/>
);
} else {
header = (
<Header
currentUser={currentUser}
differentThreadSequence={differentThreadSequence}
isAutoResponse={isAutoResponder}
isCRTEnabled={isCRTEnabled}
isEphemeral={isEphemeral}
isPendingOrFailed={isPendingOrFailed}
isSystemPost={isSystemPost}
isWebHook={isWebHook}
location={location}
post={post}
showPostPriority={showPostPriority}
shouldRenderReplyButton={shouldRenderReplyButton}
/>
);
}
}
let body;
if (isSystemPost && !isEphemeral && !isAutoResponder) {
body = (
<SystemMessage
location={location}
post={post}
/>
);
} else if (isCallsPost && !hasBeenDeleted) {
body = (
<CallsCustomMessage
serverUrl={serverUrl}
post={post}
/>
);
} else {
body = (
<Body
appsEnabled={appsEnabled}
hasFiles={hasFiles}
hasReactions={hasReactions}
highlight={Boolean(highlightedStyle)}
highlightReplyBar={highlightReplyBar}
isCRTEnabled={isCRTEnabled}
isEphemeral={isEphemeral}
isFirstReply={isFirstReply}
isJumboEmoji={isJumboEmoji}
isLastReply={isLastReply}
isPendingOrFailed={isPendingOrFailed}
isPostAddChannelMember={isPostAddChannelMember}
location={location}
post={post}
searchPatterns={searchPatterns}
showAddReaction={showAddReaction}
theme={theme}
/>
);
}
let unreadDot;
let footer;
if (isCRTEnabled && thread && location !== Screens.THREAD && !(rootId && location === Screens.PERMALINK)) {
if (thread.replyCount > 0 || thread.isFollowing) {
footer = (
<Footer
channelId={post.channelId}
location={location}
thread={thread}
/>
);
}
if (thread.unreadMentions || thread.unreadReplies) {
unreadDot = (
<UnreadDot/>
);
}
}
return (
<View
testID={testID}
style={[styles.postStyle, style, highlightedStyle]}
>
<TouchableHighlight
testID={itemTestID}
onPress={handlePress}
onLongPress={showPostOptions}
delayLongPress={200}
underlayColor={changeOpacity(theme.centerChannelColor, 0.1)}
style={styles.postContent}
>
<>
<PreHeader
isConsecutivePost={isConsecutivePost}
isSaved={isSaved}
isPinned={post.isPinned}
skipSavedHeader={skipSavedHeader}
skipPinnedHeader={skipPinnedHeader}
/>
<View style={[styles.container, consecutiveStyle]}>
{postAvatar}
<View style={rightColumnStyle}>
{header}
{body}
{footer}
</View>
{unreadDot}
</View>
</>
</TouchableHighlight>
</View>
);
};
export default Post;