diff --git a/app/actions/remote/thread.ts b/app/actions/remote/thread.ts
index 78a61abcc..9025c1cf2 100644
--- a/app/actions/remote/thread.ts
+++ b/app/actions/remote/thread.ts
@@ -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});
diff --git a/app/client/rest/channels.ts b/app/client/rest/channels.ts
index ab9bb0389..fcc0d7c14 100644
--- a/app/client/rest/channels.ts
+++ b/app/client/rest/channels.ts
@@ -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},
diff --git a/app/screens/post_options/options/base_option.tsx b/app/components/common_post_options/base_option/index.tsx
similarity index 100%
rename from app/screens/post_options/options/base_option.tsx
rename to app/components/common_post_options/base_option/index.tsx
diff --git a/app/screens/post_options/options/copy_permalink_option/copy_permalink_option.tsx b/app/components/common_post_options/copy_permalink_option/copy_permalink_option.tsx
similarity index 95%
rename from app/screens/post_options/options/copy_permalink_option/copy_permalink_option.tsx
rename to app/components/common_post_options/copy_permalink_option/copy_permalink_option.tsx
index 8528709df..0bce40d69 100644
--- a/app/screens/post_options/options/copy_permalink_option/copy_permalink_option.tsx
+++ b/app/components/common_post_options/copy_permalink_option/copy_permalink_option.tsx
@@ -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 = {
diff --git a/app/screens/post_options/options/copy_permalink_option/index.tsx b/app/components/common_post_options/copy_permalink_option/index.tsx
similarity index 100%
rename from app/screens/post_options/options/copy_permalink_option/index.tsx
rename to app/components/common_post_options/copy_permalink_option/index.tsx
diff --git a/app/screens/post_options/options/follow_option.tsx b/app/components/common_post_options/follow_thread_option/follow_thread_option.tsx
similarity index 61%
rename from app/screens/post_options/options/follow_option.tsx
rename to app/components/common_post_options/follow_thread_option/follow_thread_option.tsx
index 024f20226..b6b32a946 100644
--- a/app/screens/post_options/options/follow_option.tsx
+++ b/app/components/common_post_options/follow_thread_option/follow_thread_option.tsx
@@ -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 (
diff --git a/app/components/common_post_options/follow_thread_option/index.ts b/app/components/common_post_options/follow_thread_option/index.ts
new file mode 100644
index 000000000..0b27c98cb
--- /dev/null
+++ b/app/components/common_post_options/follow_thread_option/index.ts
@@ -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);
diff --git a/app/components/common_post_options/index.ts b/app/components/common_post_options/index.ts
new file mode 100644
index 000000000..743001197
--- /dev/null
+++ b/app/components/common_post_options/index.ts
@@ -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';
diff --git a/app/screens/post_options/options/reply_option.tsx b/app/components/common_post_options/reply_option.tsx
similarity index 81%
rename from app/screens/post_options/options/reply_option.tsx
rename to app/components/common_post_options/reply_option.tsx
index ce89af3c7..4d941396a 100644
--- a/app/screens/post_options/options/reply_option.tsx
+++ b/app/components/common_post_options/reply_option.tsx
@@ -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]);
diff --git a/app/screens/post_options/options/save_option.tsx b/app/components/common_post_options/save_option.tsx
similarity index 95%
rename from app/screens/post_options/options/save_option.tsx
rename to app/components/common_post_options/save_option.tsx
index 9b917ab97..2083efb5e 100644
--- a/app/screens/post_options/options/save_option.tsx
+++ b/app/components/common_post_options/save_option.tsx
@@ -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;
diff --git a/app/components/post_list/index.tsx b/app/components/post_list/index.tsx
index a4fc270af..8920caeab 100644
--- a/app/components/post_list/index.tsx
+++ b/app/components/post_list/index.tsx
@@ -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 (
);
- }, [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({
diff --git a/app/components/post_list/post/footer/footer.tsx b/app/components/post_list/post/footer/footer.tsx
new file mode 100644
index 000000000..5dc3d62ac
--- /dev/null
+++ b/app/components/post_list/post/footer/footer.tsx
@@ -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 = (
+ <>
+
+
+
+
+ >
+ );
+ }
+ if (thread.isFollowing) {
+ followButton = (
+
+
+
+ );
+ } else {
+ followButton = (
+ <>
+
+
+
+
+ >
+ );
+ }
+
+ const participantsList = useMemo(() => {
+ if (participants?.length) {
+ const orderedParticipantsList = [...participants].reverse();
+ return orderedParticipantsList;
+ }
+ return [];
+ }, [participants.length]);
+
+ let userAvatarsStack;
+ if (participantsList.length) {
+ userAvatarsStack = (
+
+ );
+ }
+
+ return (
+
+ {userAvatarsStack}
+ {repliesComponent}
+ {followButton}
+
+ );
+};
+
+export default Footer;
diff --git a/app/components/post_list/post/footer/index.ts b/app/components/post_list/post/footer/index.ts
new file mode 100644
index 000000000..80fdf7551
--- /dev/null
+++ b/app/components/post_list/post/footer/index.ts
@@ -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));
diff --git a/app/components/post_list/post/header/header.tsx b/app/components/post_list/post/header/header.tsx
index 637eb114d..1e8360b95 100644
--- a/app/components/post_list/post/header/header.tsx
+++ b/app/components/post_list/post/header/header.tsx
@@ -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 &&
-
+ {!isCRTEnabled && showReply && commentCount > 0 &&
+
}
diff --git a/app/components/post_list/post/header/index.ts b/app/components/post_list/post/header/index.ts
index 0178f49a5..181e75b61 100644
--- a/app/components/post_list/post/header/index.ts
+++ b/app/components/post_list/post/header/index.ts
@@ -53,8 +53,8 @@ const withHeaderProps = withObservables(
isCustomStatusEnabled,
isMilitaryTime,
isTimezoneEnabled,
- teammateNameDisplay,
rootPostAuthor,
+ teammateNameDisplay,
};
});
diff --git a/app/components/post_list/post/index.ts b/app/components/post_list/post/index.ts
index ea44f23d4..78c802cb0 100644
--- a/app/components/post_list/post/index.ts
+++ b/app/components/post_list/post/index.ts
@@ -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(),
};
});
diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx
index 2e982728c..8a89f60c1 100644
--- a/app/components/post_list/post/post.tsx
+++ b/app/components/post_list/post/post.tsx
@@ -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;
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 = (
+
+ );
+ }
+ if (thread.unreadMentions || thread.unreadReplies) {
+ unreadDot = (
+
+ );
+ }
+ }
+
return (
{header}
{body}
+ {footer}
+ {unreadDot}
>
diff --git a/app/components/post_list/post/unread_dot.tsx b/app/components/post_list/post/unread_dot.tsx
new file mode 100644
index 000000000..0b6e73165
--- /dev/null
+++ b/app/components/post_list/post/unread_dot.tsx
@@ -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 (
+
+
+
+ );
+};
+
+export default UnreadDot;
diff --git a/app/queries/servers/post.ts b/app/queries/servers/post.ts
index 5dc0e0c4e..f80ac9752 100644
--- a/app/queries/servers/post.ts
+++ b/app/queries/servers/post.ts
@@ -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(POSTS_IN_CHANNEL).query(
Q.where('channel_id', channelId),
diff --git a/app/queries/servers/thread.ts b/app/queries/servers/thread.ts
index a6e85f612..cbe46a64a 100644
--- a/app/queries/servers/thread.ts
+++ b/app/queries/servers/thread.ts
@@ -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']).
diff --git a/app/screens/channel/channel_post_list/channel_post_list.tsx b/app/screens/channel/channel_post_list/channel_post_list.tsx
index d7865236c..151b63815 100644
--- a/app/screens/channel/channel_post_list/channel_post_list.tsx
+++ b/app/screens/channel/channel_post_list/channel_post_list.tsx
@@ -22,6 +22,7 @@ type Props = {
contentContainerStyle?: StyleProp;
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;
-
diff --git a/app/screens/post_options/index.ts b/app/screens/post_options/index.ts
index a65e20ee6..ac68bee7d 100644
--- a/app/screens/post_options/index.ts
+++ b/app/screens/post_options/index.ts
@@ -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,
};
});
diff --git a/app/screens/post_options/options/copy_text_option.tsx b/app/screens/post_options/options/copy_text_option.tsx
index ade8fb847..27eefd4c6 100644
--- a/app/screens/post_options/options/copy_text_option.tsx
+++ b/app/screens/post_options/options/copy_text_option.tsx
@@ -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;
}
diff --git a/app/screens/post_options/options/delete_post_option.tsx b/app/screens/post_options/options/delete_post_option.tsx
index 43d2c8c81..ae5314ceb 100644
--- a/app/screens/post_options/options/delete_post_option.tsx
+++ b/app/screens/post_options/options/delete_post_option.tsx
@@ -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 = {
diff --git a/app/screens/post_options/options/edit_option.tsx b/app/screens/post_options/options/edit_option.tsx
index a442cdfcd..810b47b79 100644
--- a/app/screens/post_options/options/edit_option.tsx
+++ b/app/screens/post_options/options/edit_option.tsx
@@ -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 = {
diff --git a/app/screens/post_options/options/mark_unread_option.tsx b/app/screens/post_options/options/mark_unread_option.tsx
index b7e2e12a4..60ccf90ec 100644
--- a/app/screens/post_options/options/mark_unread_option.tsx
+++ b/app/screens/post_options/options/mark_unread_option.tsx
@@ -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;
}
diff --git a/app/screens/post_options/options/pin_channel_option.tsx b/app/screens/post_options/options/pin_channel_option.tsx
index 807805421..555585a7d 100644
--- a/app/screens/post_options/options/pin_channel_option.tsx
+++ b/app/screens/post_options/options/pin_channel_option.tsx
@@ -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;
diff --git a/app/screens/post_options/post_options.tsx b/app/screens/post_options/post_options.tsx
index 48c5749ac..265d43335 100644
--- a/app/screens/post_options/post_options.tsx
+++ b/app/screens/post_options/post_options.tsx
@@ -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;
+ thread?: ThreadModel;
componentId: string;
};
@@ -93,15 +91,12 @@ const PostOptions = ({
{canAddReaction && }
{canReply && }
{shouldRenderFollow &&
-
+
}
{canMarkAsUnread && !isSystemPost &&
}
- {canCopyPermalink && }
+ {canCopyPermalink && }
{!isSystemPost &&
{
},
text: {
color: theme.sidebarHeaderTextColor,
- ...typography('Heading', 75, 'SemiBold'),
+ ...typography('Heading', 75),
},
};
});
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 80dafecaa..8272f4029 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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}.",