In-Channel experience (#6141)

* User avatar stack

* In-Channel experience

* Misc Fixes

* Fixed fetchPostThread & added observer

* Reusing the user component

* Refactor fix

* Moved some post options to common post options

* Combined follow/unfollow functions

* Feedback fixes

* Feedback fixes

* teamId fix

* Fixed teamId again

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Anurag Shivarathri 2022-04-19 21:12:20 +05:30 committed by GitHub
parent 1996224a4c
commit 313fe9c469
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 406 additions and 62 deletions

View file

@ -8,7 +8,7 @@ import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
import {getPostById} from '@queries/servers/post';
import {getCommonSystemValues} from '@queries/servers/system';
import {getCommonSystemValues, getCurrentTeamId} from '@queries/servers/system';
import {getIsCRTEnabled, getNewestThreadInTeam, getThreadById} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
@ -180,6 +180,12 @@ export const updateThreadRead = async (serverUrl: string, teamId: string, thread
};
export const updateThreadFollowing = async (serverUrl: string, teamId: string, threadId: string, state: boolean) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
@ -187,8 +193,14 @@ export const updateThreadFollowing = async (serverUrl: string, teamId: string, t
return {error};
}
// DM/GM doesn't have a teamId, so we pass the current team id
let threadTeamId = teamId;
if (!threadTeamId) {
threadTeamId = await getCurrentTeamId(database);
}
try {
const data = await client.updateThreadFollow('me', teamId, threadId, state);
const data = await client.updateThreadFollow('me', threadTeamId, threadId, state);
// Update locally
await updateThread(serverUrl, threadId, {is_following: state});

View file

@ -278,7 +278,8 @@ const ClientChannels = (superclass: any) => class extends superclass {
};
viewMyChannel = async (channelId: string, prevChannelId?: string) => {
const data = {channel_id: channelId, prev_channel_id: prevChannelId};
// collapsed_threads_supported is not based on user preferences but to know if "CLIENT" supports CRT
const data = {channel_id: channelId, prev_channel_id: prevChannelId, collapsed_threads_supported: true};
return this.doFetch(
`${this.getChannelsRoute()}/members/me/view`,
{method: 'post', body: data},

View file

@ -4,13 +4,12 @@
import Clipboard from '@react-native-community/clipboard';
import React, {useCallback} from 'react';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import BaseOption from '../base_option';
import type PostModel from '@typings/database/models/servers/post';
type Props = {

View file

@ -3,26 +3,28 @@
import React from 'react';
import {updateThreadFollowing} from '@actions/remote/thread';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import BaseOption from './base_option';
import type ThreadModel from '@typings/database/models/servers/thread';
type FollowThreadOptionProps = {
thread?: any;
location?: typeof Screens[keyof typeof Screens];
thread: ThreadModel;
teamId?: string;
};
//todo: to implement CRT follow thread
const FollowThreadOption = ({thread}: FollowThreadOptionProps) => {
const FollowThreadOption = ({thread, teamId}: FollowThreadOptionProps) => {
let id: string;
let defaultMessage: string;
let icon: string;
if (thread.is_following) {
if (thread.isFollowing) {
icon = 'message-minus-outline';
if (thread?.participants?.length) {
if (thread.replyCount) {
id = t('threads.unfollowThread');
defaultMessage = 'Unfollow Thread';
} else {
@ -31,7 +33,7 @@ const FollowThreadOption = ({thread}: FollowThreadOptionProps) => {
}
} else {
icon = 'message-plus-outline';
if (thread?.participants?.length) {
if (thread.replyCount) {
id = t('threads.followThread');
defaultMessage = 'Follow Thread';
} else {
@ -40,8 +42,14 @@ const FollowThreadOption = ({thread}: FollowThreadOptionProps) => {
}
}
const serverUrl = useServerUrl();
const handleToggleFollow = () => {
//todo:
if (teamId == null) {
return;
}
updateThreadFollowing(serverUrl, teamId, thread.id, !thread.isFollowing);
dismissBottomSheet(Screens.POST_OPTIONS);
};
return (

View file

@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import withObservables from '@nozbe/with-observables';
import {observeTeamIdByThread} from '@queries/servers/thread';
import FollowThreadOption from './follow_thread_option';
import type ThreadModel from '@typings/database/models/servers/thread';
const enhanced = withObservables(['thread'], ({thread}: { thread: ThreadModel }) => {
return {
teamId: observeTeamIdByThread(thread),
};
});
export default enhanced(FollowThreadOption);

View file

@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default as BaseOption} from './base_option';
export {default as CopyPermalinkOption} from './copy_permalink_option';
export {default as FollowThreadOption} from './follow_thread_option';
export {default as ReplyOption} from './reply_option';
export {default as SaveOption} from './save_option';

View file

@ -4,24 +4,24 @@
import React, {useCallback} from 'react';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import BaseOption from './base_option';
import type PostModel from '@typings/database/models/servers/post';
type Props = {
post: PostModel;
location?: typeof Screens[keyof typeof Screens];
}
const ReplyOption = ({post}: Props) => {
const ReplyOption = ({post, location}: Props) => {
const serverUrl = useServerUrl();
const handleReply = useCallback(async () => {
const rootId = post.rootId || post.id;
await dismissBottomSheet(Screens.POST_OPTIONS);
await dismissBottomSheet(location || Screens.POST_OPTIONS);
fetchAndSwitchToThread(serverUrl, rootId);
}, [post, serverUrl]);

View file

@ -4,13 +4,12 @@
import React, {useCallback} from 'react';
import {deleteSavedPost, savePostPreference} from '@actions/remote/preference';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import BaseOption from './base_option';
type CopyTextProps = {
isSaved: boolean;
postId: string;

View file

@ -31,6 +31,7 @@ type Props = {
currentUsername: string;
highlightedId?: PostModel['id'];
highlightPinnedOrSaved?: boolean;
isCRTEnabled?: boolean;
isTimezoneEnabled: boolean;
lastViewedAt: number;
location: string;
@ -82,6 +83,7 @@ const PostList = ({
footer,
highlightedId,
highlightPinnedOrSaved = true,
isCRTEnabled,
isTimezoneEnabled,
lastViewedAt,
location,
@ -300,6 +302,7 @@ const PostList = ({
return (
<Post
isCRTEnabled={isCRTEnabled}
key={item.id}
post={item}
style={styles.scale}
@ -307,7 +310,7 @@ const PostList = ({
{...postProps}
/>
);
}, [currentTimezone, highlightPinnedOrSaved, isTimezoneEnabled, orderedPosts, shouldRenderReplyButton, theme]);
}, [currentTimezone, highlightPinnedOrSaved, isCRTEnabled, isTimezoneEnabled, orderedPosts, shouldRenderReplyButton, theme]);
const scrollToIndex = useCallback((index: number, animated = true, applyOffset = true) => {
listRef.current?.scrollToIndex({

View file

@ -0,0 +1,175 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {TouchableOpacity, View} from 'react-native';
import {updateThreadFollowing} from '@actions/remote/thread';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import UserAvatarsStack from '@components/user_avatars_stack';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type ThreadModel from '@typings/database/models/servers/thread';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
participants: UserModel[];
teamId?: string;
testID: string;
thread: ThreadModel;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const followingButtonContainerBase = {
justifyContent: 'center',
height: 32,
paddingHorizontal: 12,
};
return {
container: {
flexDirection: 'row',
alignItems: 'center',
minHeight: 40,
},
avatarsContainer: {
marginRight: 12,
paddingVertical: 8,
},
replyIconContainer: {
top: -1,
marginRight: 5,
},
replies: {
alignSelf: 'center',
color: changeOpacity(theme.centerChannelColor, 0.64),
marginRight: 12,
...typography('Heading', 75),
},
notFollowingButtonContainer: {
...followingButtonContainerBase,
paddingLeft: 0,
},
notFollowing: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Heading', 75),
},
followingButtonContainer: {
...followingButtonContainerBase,
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
borderRadius: 4,
},
following: {
color: theme.buttonBg,
...typography('Heading', 75),
},
followSeparator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.16),
height: 16,
marginRight: 12,
width: 1,
},
};
});
const Footer = ({participants, teamId, testID, thread}: Props) => {
const serverUrl = useServerUrl();
const theme = useTheme();
const styles = getStyleSheet(theme);
const toggleFollow = useCallback(preventDoubleTap(() => {
if (teamId == null) {
return;
}
updateThreadFollowing(serverUrl, teamId, thread.id, !thread.isFollowing);
}), [thread.isFollowing]);
let repliesComponent;
let followButton;
if (thread.replyCount) {
repliesComponent = (
<>
<View style={styles.replyIconContainer}>
<CompassIcon
name='reply-outline'
size={18}
color={changeOpacity(theme.centerChannelColor, 0.64)}
/>
</View>
<FormattedText
style={styles.replies}
testID={`${testID}.reply_count`}
id='threads.replies'
defaultMessage='{count} {count, plural, one {reply} other {replies}}'
values={{
count: thread.replyCount,
}}
/>
</>
);
}
if (thread.isFollowing) {
followButton = (
<TouchableOpacity
onPress={toggleFollow}
style={styles.followingButtonContainer}
testID={`${testID}.following`}
>
<FormattedText
id='threads.following'
defaultMessage='Following'
style={styles.following}
/>
</TouchableOpacity>
);
} else {
followButton = (
<>
<View style={styles.followSeparator}/>
<TouchableOpacity
onPress={toggleFollow}
style={styles.notFollowingButtonContainer}
testID={`${testID}.follow`}
>
<FormattedText
id='threads.follow'
defaultMessage='Follow'
style={styles.notFollowing}
/>
</TouchableOpacity>
</>
);
}
const participantsList = useMemo(() => {
if (participants?.length) {
const orderedParticipantsList = [...participants].reverse();
return orderedParticipantsList;
}
return [];
}, [participants.length]);
let userAvatarsStack;
if (participantsList.length) {
userAvatarsStack = (
<UserAvatarsStack
style={styles.avatarsContainer}
users={participantsList}
/>
);
}
return (
<View style={styles.container}>
{userAvatarsStack}
{repliesComponent}
{followButton}
</View>
);
};
export default Footer;

View file

@ -0,0 +1,24 @@
// 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 {observeTeamIdByThread, queryThreadParticipants} from '@queries/servers/thread';
import Footer from './footer';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ThreadModel from '@typings/database/models/servers/thread';
const enhanced = withObservables(
['thread'],
({database, thread}: WithDatabaseArgs & {thread: ThreadModel}) => {
return {
participants: queryThreadParticipants(database, thread.id).observe(),
teamId: observeTeamIdByThread(thread),
};
},
);
export default withDatabase(enhanced(Footer));

View file

@ -27,6 +27,7 @@ type HeaderProps = {
currentUser: UserModel;
enablePostUsernameOverride: boolean;
isAutoResponse: boolean;
isCRTEnabled?: boolean;
isCustomStatusEnabled: boolean;
isEphemeral: boolean;
isMilitaryTime: boolean;
@ -71,7 +72,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const Header = (props: HeaderProps) => {
const {
author, commentCount = 0, currentUser, enablePostUsernameOverride, isAutoResponse, isCustomStatusEnabled,
author, commentCount = 0, currentUser, enablePostUsernameOverride, isAutoResponse, isCRTEnabled, isCustomStatusEnabled,
isEphemeral, isMilitaryTime, isPendingOrFailed, isSystemPost, isTimezoneEnabled, isWebHook,
location, post, rootPostAuthor, shouldRenderReplyButton, teammateNameDisplay,
} = props;
@ -124,13 +125,13 @@ const Header = (props: HeaderProps) => {
style={style.time}
testID='post_header.date_time'
/>
{showReply && commentCount > 0 &&
<HeaderReply
commentCount={commentCount}
location={location}
post={post}
theme={theme}
/>
{!isCRTEnabled && showReply && commentCount > 0 &&
<HeaderReply
commentCount={commentCount}
location={location}
post={post}
theme={theme}
/>
}
</View>
</View>

View file

@ -53,8 +53,8 @@ const withHeaderProps = withObservables(
isCustomStatusEnabled,
isMilitaryTime,
isTimezoneEnabled,
teammateNameDisplay,
rootPostAuthor,
teammateNameDisplay,
};
});

View file

@ -13,6 +13,7 @@ import {queryPostsBetween} from '@queries/servers/post';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeCanManageChannelMembers, observePermissionForPost} from '@queries/servers/role';
import {observeConfigBooleanValue} from '@queries/servers/system';
import {observeThreadById} from '@queries/servers/thread';
import {observeCurrentUser} from '@queries/servers/user';
import {hasJumboEmojiOnly} from '@utils/emoji/helpers';
import {areConsecutivePosts, isPostEphemeral} from '@utils/post';
@ -28,6 +29,7 @@ import type UserModel from '@typings/database/models/servers/user';
type PropsInput = WithDatabaseArgs & {
appsEnabled: boolean;
currentUser: UserModel;
isCRTEnabled?: boolean;
nextPost: PostModel | undefined;
post: PostModel;
previousPost: PostModel | undefined;
@ -92,8 +94,8 @@ const withSystem = withObservables([], ({database}: WithDatabaseArgs) => ({
}));
const withPost = withObservables(
['currentUser', 'post', 'previousPost', 'nextPost'],
({appsEnabled, currentUser, database, post, previousPost, nextPost}: PropsInput) => {
['currentUser', 'isCRTEnabled', 'post', 'previousPost', 'nextPost'],
({appsEnabled, currentUser, database, isCRTEnabled, post, previousPost, nextPost}: PropsInput) => {
let isJumboEmoji = of$(false);
let isLastReply = of$(true);
let isPostAddChannelMember = of$(false);
@ -150,6 +152,7 @@ const withPost = withObservables(
isLastReply,
isPostAddChannelMember,
post: post.observe(),
thread: isCRTEnabled ? observeThreadById(database, post.id) : of$(undefined),
reactionsCount: post.reactions.observeCount(),
};
});

View file

@ -21,11 +21,14 @@ 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';
type PostProps = {
@ -39,6 +42,7 @@ type PostProps = {
highlightPinnedOrSaved?: boolean;
highlightReplyBar: boolean;
isConsecutivePost?: boolean;
isCRTEnabled?: boolean;
isEphemeral: boolean;
isFirstReply?: boolean;
isSaved?: boolean;
@ -55,6 +59,7 @@ type PostProps = {
skipPinnedHeader?: boolean;
style?: StyleProp<ViewStyle>;
testID?: string;
thread?: ThreadModel;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -97,9 +102,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const Post = ({
appsEnabled, canDelete, currentUser, differentThreadSequence, filesCount, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
isConsecutivePost, isEphemeral, isFirstReply, isSaved, isJumboEmoji, isLastReply, isPostAddChannelMember,
isCRTEnabled, isConsecutivePost, isEphemeral, isFirstReply, isSaved, isJumboEmoji, isLastReply, isPostAddChannelMember,
location, post, reactionsCount, shouldRenderReplyButton, skipSavedHeader, skipPinnedHeader, showAddReaction = true, style,
testID, previousPost,
testID, thread, previousPost,
}: PostProps) => {
const pressDetected = useRef(false);
const intl = useIntl();
@ -223,6 +228,7 @@ const Post = ({
currentUser={currentUser}
differentThreadSequence={differentThreadSequence}
isAutoResponse={isAutoResponder}
isCRTEnabled={isCRTEnabled}
isEphemeral={isEphemeral}
isPendingOrFailed={isPendingOrFailed}
isSystemPost={isSystemPost}
@ -264,6 +270,24 @@ const Post = ({
);
}
let unreadDot;
let footer;
if (isCRTEnabled && thread) {
if (thread.replyCount > 0 || thread.isFollowing) {
footer = (
<Footer
testID={`${itemTestID}.footer`}
thread={thread}
/>
);
}
if (thread.unreadMentions || thread.unreadReplies) {
unreadDot = (
<UnreadDot testID={`${itemTestID}.badge`}/>
);
}
}
return (
<View
testID={testID}
@ -289,7 +313,9 @@ const Post = ({
<View style={rightColumnStyle}>
{header}
{body}
{footer}
</View>
{unreadDot}
</View>
</>
</TouchableHighlight>

View file

@ -0,0 +1,46 @@
// 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 {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
testID: string;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
badgeContainer: {
position: 'absolute',
left: 21,
bottom: 9,
},
unreadDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: theme.sidebarTextActiveBorder,
alignSelf: 'center',
top: -6,
left: 4,
},
};
});
const UnreadDot = ({testID}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<View
style={styles.badgeContainer}
testID={testID}
>
<View style={styles.unreadDot}/>
</View>
);
};
export default UnreadDot;

View file

@ -5,8 +5,11 @@ import {Database, Model, Q, Query} from '@nozbe/watermelondb';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {Preferences} from '@constants';
import {MM_TABLES} from '@constants/database';
import {queryPreferencesByCategoryAndName} from './preference';
import type PostModel from '@typings/database/models/servers/post';
import type PostInChannelModel from '@typings/database/models/servers/posts_in_channel';
import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread';
@ -51,6 +54,14 @@ export const observePost = (database: Database, postId: string) => {
);
};
export const observePostSaved = (database: Database, postId: string) => {
return queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, postId).observe().pipe(
switchMap(
(pref) => of$(Boolean(pref[0]?.value === 'true')),
),
);
};
export const queryPostsInChannel = (database: Database, channelId: string) => {
return database.get<PostInChannelModel>(POSTS_IN_CHANNEL).query(
Q.where('channel_id', channelId),

View file

@ -52,6 +52,19 @@ export const observeThreadById = (database: Database, threadId: string) => {
);
};
export const observeTeamIdByThread = (thread: ThreadModel) => {
return thread.post.observe().pipe(
switchMap((post) => {
if (!post) {
return of$(undefined);
}
return post.channel.observe().pipe(
switchMap((channel) => of$(channel?.teamId)),
);
}),
);
};
export const observeUnreadsAndMentionsInTeam = (database: Database, teamId?: string, includeDmGm?: boolean): Observable<{unreads: number; mentions: number}> => {
const observeThreads = () => queryThreads(database, teamId, true, includeDmGm).
observeWithColumns(['unread_replies', 'unread_mentions']).

View file

@ -22,6 +22,7 @@ type Props = {
contentContainerStyle?: StyleProp<ViewStyle>;
currentTimezone: string | null;
currentUsername: string;
isCRTEnabled: boolean;
isTimezoneEnabled: boolean;
lastViewedAt: number;
nativeID: string;
@ -36,7 +37,7 @@ const styles = StyleSheet.create({
const ChannelPostList = ({
channelId, contentContainerStyle, currentTimezone, currentUsername,
isTimezoneEnabled, lastViewedAt, nativeID, posts, shouldShowJoinLeaveMessages,
isCRTEnabled, isTimezoneEnabled, lastViewedAt, nativeID, posts, shouldShowJoinLeaveMessages,
}: Props) => {
const isTablet = useIsTablet();
const serverUrl = useServerUrl();
@ -61,6 +62,7 @@ const ChannelPostList = ({
contentContainerStyle={contentContainerStyle}
currentTimezone={currentTimezone}
currentUsername={currentUsername}
isCRTEnabled={isCRTEnabled}
isTimezoneEnabled={isTimezoneEnabled}
footer={intro}
lastViewedAt={lastViewedAt}
@ -89,4 +91,3 @@ const ChannelPostList = ({
};
export default ChannelPostList;

View file

@ -6,12 +6,12 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$, Observable} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {General, Permissions, Post, Preferences, Screens} from '@constants';
import {General, Permissions, Post, Screens} from '@constants';
import {MAX_ALLOWED_REACTIONS} from '@constants/emoji';
import {observePost} from '@queries/servers/post';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observePost, observePostSaved} from '@queries/servers/post';
import {observePermissionForChannel, observePermissionForPost} from '@queries/servers/role';
import {observeConfig, observeLicense} from '@queries/servers/system';
import {observeIsCRTEnabled, observeThreadById} from '@queries/servers/thread';
import {observeCurrentUser} from '@queries/servers/user';
import {isMinimumServerVersion} from '@utils/helpers';
import {isSystemMessage} from '@utils/post';
@ -112,7 +112,7 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, loca
return of$(!isSystemMessage(post) && !isArchived && !isReadOnly);
}));
const isSaved = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, post.id).observe().pipe(switchMap((pref) => of$(Boolean(pref[0]?.value === 'true'))));
const isSaved = observePostSaved(database, post.id);
const canEdit = combineLatest([postEditTimeLimit, isLicensed, channel, currentUser, channelIsArchived, channelIsReadOnly, canEditUntil, canPostPermission]).pipe(
switchMap(([lt, ls, c, u, isArchived, isReadOnly, until, canPost]) => {
@ -140,6 +140,10 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, loca
return of$(permission && !isArchived && !isReadOnly && canPost);
}));
const thread = observeIsCRTEnabled(database).pipe(
switchMap((enabled) => (enabled ? observeThreadById(database, post.id) : of$(undefined))),
);
return {
canMarkAsUnread,
canAddReaction,
@ -150,6 +154,7 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, loca
isSaved,
canEdit,
post,
thread,
};
});

View file

@ -4,12 +4,11 @@
import Clipboard from '@react-native-community/clipboard';
import React, {useCallback} from 'react';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import BaseOption from './base_option';
type Props = {
postMessage: string;
}

View file

@ -6,13 +6,12 @@ import {useIntl} from 'react-intl';
import {Alert} from 'react-native';
import {deletePost} from '@actions/remote/post';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import BaseOption from './base_option';
import type PostModel from '@typings/database/models/servers/post';
type Props = {

View file

@ -4,14 +4,13 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {BaseOption} from '@components/common_post_options';
import CompassIcon from '@components/compass_icon';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {dismissBottomSheet, showModal} from '@screens/navigation';
import BaseOption from './base_option';
import type PostModel from '@typings/database/models/servers/post';
type Props = {

View file

@ -4,13 +4,12 @@
import React, {useCallback} from 'react';
import {markPostAsUnread} from '@actions/remote/post';
import {BaseOption} from '@components/common_post_options';
import Screens from '@constants/screens';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import BaseOption from './base_option';
type Props = {
postId: string;
}

View file

@ -4,13 +4,12 @@
import React, {useCallback} from 'react';
import {togglePinPost} from '@actions/remote/post';
import {BaseOption} from '@components/common_post_options';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import BaseOption from './base_option';
type PinChannelProps = {
isPostPinned: boolean;
postId: string;

View file

@ -5,24 +5,22 @@ import {useManagedConfig} from '@mattermost/react-native-emm';
import React, {useEffect} from 'react';
import {Navigation} from 'react-native-navigation';
import {CopyPermalinkOption, FollowThreadOption, ReplyOption, SaveOption} from '@components/common_post_options';
import {ITEM_HEIGHT} from '@components/menu_item';
import {Screens} from '@constants';
import BottomSheet from '@screens/bottom_sheet';
import {dismissModal} from '@screens/navigation';
import {isSystemMessage} from '@utils/post';
import CopyLinkOption from './options/copy_permalink_option';
import CopyTextOption from './options/copy_text_option';
import DeletePostOption from './options/delete_post_option';
import EditOption from './options/edit_option';
import FollowThreadOption from './options/follow_option';
import MarkAsUnreadOption from './options/mark_unread_option';
import PinChannelOption from './options/pin_channel_option';
import ReplyOption from './options/reply_option';
import SaveOption from './options/save_option';
import ReactionBar from './reaction_bar';
import type PostModel from '@typings/database/models/servers/post';
import type ThreadModel from '@typings/database/models/servers/thread';
type PostOptionsProps = {
canAddReaction: boolean;
@ -35,7 +33,7 @@ type PostOptionsProps = {
isSaved: boolean;
location: typeof Screens[keyof typeof Screens];
post: PostModel;
thread: Partial<PostModel>;
thread?: ThreadModel;
componentId: string;
};
@ -93,15 +91,12 @@ const PostOptions = ({
{canAddReaction && <ReactionBar postId={post.id}/>}
{canReply && <ReplyOption post={post}/>}
{shouldRenderFollow &&
<FollowThreadOption
location={location}
thread={thread}
/>
<FollowThreadOption thread={thread}/>
}
{canMarkAsUnread && !isSystemPost &&
<MarkAsUnreadOption postId={post.id}/>
}
{canCopyPermalink && <CopyLinkOption post={post}/>}
{canCopyPermalink && <CopyPermalinkOption post={post}/>}
{!isSystemPost &&
<SaveOption
isSaved={isSaved}

View file

@ -44,7 +44,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
},
text: {
color: theme.sidebarHeaderTextColor,
...typography('Heading', 75, 'SemiBold'),
...typography('Heading', 75),
},
};
});

View file

@ -520,6 +520,7 @@
"threads.following": "Following",
"threads.followMessage": "Follow Message",
"threads.followThread": "Follow Thread",
"threads.newReplies": "{count} new {count, plural, one {reply} other {replies}}",
"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}.",