diff --git a/app/actions/local/thread.ts b/app/actions/local/thread.ts index 99e96c9c39..a06549f9f8 100644 --- a/app/actions/local/thread.ts +++ b/app/actions/local/thread.ts @@ -324,7 +324,7 @@ export async function updateThread(serverUrl: string, threadId: string, updatedT } return {model}; } catch (error) { - logError('Failed markTeamThreadsAsRead', error); + logError('Failed updateThread', error); return {error}; } } diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index 973ef0cd6a..9f059efcc2 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -570,7 +570,7 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn } }; -export async function fetchPostThread(serverUrl: string, postId: string, fetchOnly = false) { +export async function fetchPostThread(serverUrl: string, postId: string, options?: FetchPaginatedThreadOptions, fetchOnly = false) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; @@ -585,7 +585,13 @@ export async function fetchPostThread(serverUrl: string, postId: string, fetchOn try { const isCRTEnabled = await getIsCRTEnabled(operator.database); - const data = await client.getPostThread(postId, isCRTEnabled, isCRTEnabled); + + // Not doing any version check as server versions below 6.7 will ignore the additional params from the client. + const data = await client.getPostThread(postId, { + collapsedThreads: isCRTEnabled, + collapsedThreadsExtended: isCRTEnabled, + ...options, + }); const result = processPostsFetched(data); let posts: Model[] = []; if (!fetchOnly) { @@ -637,7 +643,11 @@ export async function fetchPostsAround(serverUrl: string, channelId: string, pos try { const [after, post, before] = await Promise.all([ client.getPostsAfter(channelId, postId, 0, perPage, isCRTEnabled, isCRTEnabled), - client.getPostThread(postId, isCRTEnabled, isCRTEnabled), + client.getPostThread(postId, { + collapsedThreads: isCRTEnabled, + collapsedThreadsExtended: isCRTEnabled, + fetchAll: true, + }), client.getPostsBefore(channelId, postId, 0, perPage, isCRTEnabled, isCRTEnabled), ]); diff --git a/app/actions/remote/thread.ts b/app/actions/remote/thread.ts index cb565ef8ef..f7a4a9fb6c 100644 --- a/app/actions/remote/thread.ts +++ b/app/actions/remote/thread.ts @@ -40,7 +40,6 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, } // Load thread before we open to the thread modal - // @Todo: https://mattermost.atlassian.net/browse/MM-42232 fetchPostThread(serverUrl, rootId); // Mark thread as read diff --git a/app/client/rest/posts.ts b/app/client/rest/posts.ts index 18b7f94d9e..6075925c78 100644 --- a/app/client/rest/posts.ts +++ b/app/client/rest/posts.ts @@ -11,7 +11,7 @@ export interface ClientPostsMix { getPost: (postId: string) => Promise; patchPost: (postPatch: Partial & {id: string}) => Promise; deletePost: (postId: string) => Promise; - getPostThread: (postId: string, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise; + getPostThread: (postId: string, options: FetchPaginatedThreadOptions) => Promise; getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise; getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise; getPostsBefore: (channelId: string, postId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise; @@ -79,9 +79,18 @@ const ClientPosts = (superclass: any) => class extends superclass { ); }; - getPostThread = async (postId: string, collapsedThreads = false, collapsedThreadsExtended = false) => { + getPostThread = (postId: string, options: FetchPaginatedThreadOptions): Promise => { + const { + fetchThreads = true, + collapsedThreads = false, + collapsedThreadsExtended = false, + direction = 'up', + fetchAll = false, + perPage = fetchAll ? undefined : PER_PAGE_DEFAULT, + ...rest + } = options; return this.doFetch( - `${this.getPostRoute(postId)}/thread${buildQueryString({collapsedThreads, collapsedThreadsExtended})}`, + `${this.getPostRoute(postId)}/thread${buildQueryString({skipFetchThreads: !fetchThreads, collapsedThreads, collapsedThreadsExtended, direction, perPage, ...rest})}`, {method: 'get'}, ); }; diff --git a/app/components/post_list/post_list.tsx b/app/components/post_list/post_list.tsx index ca624b5640..7decf1d690 100644 --- a/app/components/post_list/post_list.tsx +++ b/app/components/post_list/post_list.tsx @@ -149,10 +149,17 @@ const PostList = ({ if (location === Screens.CHANNEL && channelId) { await fetchPosts(serverUrl, channelId); } else if (location === Screens.THREAD && rootId) { - await fetchPostThread(serverUrl, rootId); + const options: FetchPaginatedThreadOptions = {}; + const lastPost = posts[0]; + if (lastPost) { + options.fromCreateAt = lastPost.createAt; + options.fromPost = lastPost.id; + options.direction = 'down'; + } + await fetchPostThread(serverUrl, rootId, options); } setRefreshing(false); - }, [channelId, location, rootId]); + }, [channelId, location, posts, rootId]); const onScroll = useCallback((event: NativeSyntheticEvent) => { if (Platform.OS === 'android') { diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index 58d1edc8e5..c73d6f9af4 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -25,7 +25,7 @@ import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n'; import NativeNotifications from '@notifications'; import {queryServerName} from '@queries/app/servers'; import {getCurrentChannelId} from '@queries/servers/system'; -import {getIsCRTEnabled} from '@queries/servers/thread'; +import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread'; import {showOverlay} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; @@ -139,7 +139,10 @@ class PushNotifications { if (database) { const isCRTEnabled = await getIsCRTEnabled(database); if (isCRTEnabled && payload.root_id) { - markThreadAsRead(serverUrl, payload.team_id, payload.post_id); + const thread = await getThreadById(database, payload.root_id); + if (thread?.isFollowing) { + markThreadAsRead(serverUrl, payload.team_id, payload.post_id); + } } else { markChannelAsViewed(serverUrl, payload.channel_id, false); } diff --git a/app/screens/permalink/permalink.tsx b/app/screens/permalink/permalink.tsx index eec81db921..2ed80ce3b7 100644 --- a/app/screens/permalink/permalink.tsx +++ b/app/screens/permalink/permalink.tsx @@ -155,7 +155,9 @@ function Permalink({ let data; const loadThreadPosts = isCRTEnabled && rootId; if (loadThreadPosts) { - data = await fetchPostThread(serverUrl, rootId); + data = await fetchPostThread(serverUrl, rootId, { + fetchAll: true, + }); } else { data = await fetchPostsAround(serverUrl, channelId, postId, POSTS_LIMIT, isCRTEnabled); } diff --git a/app/screens/thread/thread_post_list/index.ts b/app/screens/thread/thread_post_list/index.ts index 77e91d2049..cdf3401fbd 100644 --- a/app/screens/thread/thread_post_list/index.ts +++ b/app/screens/thread/thread_post_list/index.ts @@ -9,6 +9,7 @@ import {switchMap} from 'rxjs/operators'; import {observeMyChannel, observeChannel} from '@queries/servers/channel'; import {queryPostsChunk, queryPostsInThread} from '@queries/servers/post'; +import {observeConfigValue} from '@queries/servers/system'; import {observeIsCRTEnabled, observeThreadById} from '@queries/servers/thread'; import ThreadPostList from './thread_post_list'; @@ -41,6 +42,7 @@ const enhanced = withObservables(['forceQueryAfterAppState', 'rootPost'], ({data switchMap((channel) => of$(channel?.teamId)), ), thread: observeThreadById(database, rootPost.id), + version: observeConfigValue(database, 'Version'), }; }); diff --git a/app/screens/thread/thread_post_list/thread_post_list.tsx b/app/screens/thread/thread_post_list/thread_post_list.tsx index 6f6668ef70..86cdda67e4 100644 --- a/app/screens/thread/thread_post_list/thread_post_list.tsx +++ b/app/screens/thread/thread_post_list/thread_post_list.tsx @@ -1,15 +1,18 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {StyleSheet, View} from 'react-native'; import {Edge, SafeAreaView} from 'react-native-safe-area-context'; +import {fetchPostThread} from '@actions/remote/post'; import {markThreadAsRead} from '@actions/remote/thread'; import PostList from '@components/post_list'; import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; +import {debounce} from '@helpers/api/general'; import {useIsTablet} from '@hooks/device'; +import {isMinimumServerVersion} from '@utils/helpers'; import type PostModel from '@typings/database/models/servers/post'; import type ThreadModel from '@typings/database/models/servers/thread'; @@ -22,6 +25,7 @@ type Props = { rootPost: PostModel; teamId: string; thread?: ThreadModel; + version?: string; } const edges: Edge[] = ['bottom']; @@ -34,18 +38,35 @@ const styles = StyleSheet.create({ const ThreadPostList = ({ channelLastViewedAt, isCRTEnabled, - nativeID, posts, rootPost, teamId, thread, + nativeID, posts, rootPost, teamId, thread, version, }: Props) => { const isTablet = useIsTablet(); const serverUrl = useServerUrl(); + const canLoadPosts = useRef(true); + const fetchingPosts = useRef(false); + const onEndReached = useCallback(debounce(async () => { + if (isMinimumServerVersion(version || '', 6, 7) && !fetchingPosts.current && canLoadPosts.current && posts.length) { + fetchingPosts.current = true; + const options: FetchPaginatedThreadOptions = {}; + const lastPost = posts[posts.length - 1]; + if (lastPost) { + options.fromPost = lastPost.id; + options.fromCreateAt = lastPost.createAt; + } + const result = await fetchPostThread(serverUrl, rootPost.id, options); + fetchingPosts.current = false; + canLoadPosts.current = Boolean(result?.posts?.length); + } + }, 500), [rootPost, posts, version]); + const threadPosts = useMemo(() => { return [...posts, rootPost]; }, [posts, rootPost]); // If CRT is enabled, mark the thread as read on mount. useEffect(() => { - if (isCRTEnabled) { + if (isCRTEnabled && thread?.isFollowing) { markThreadAsRead(serverUrl, teamId, rootPost.id); } }, []); @@ -69,6 +90,7 @@ const ThreadPostList = ({ lastViewedAt={lastViewedAt} location={Screens.THREAD} nativeID={nativeID} + onEndReached={onEndReached} posts={threadPosts} rootId={rootPost.id} shouldShowJoinLeaveMessages={false} diff --git a/types/api/posts.d.ts b/types/api/posts.d.ts index 714916bfe6..0163bb4862 100644 --- a/types/api/posts.d.ts +++ b/types/api/posts.d.ts @@ -116,3 +116,14 @@ type PostSearchParams = { terms: string; is_or_search: boolean; }; + +type FetchPaginatedThreadOptions = { + fetchThreads?: boolean; + collapsedThreads?: boolean; + collapsedThreadsExtended?: boolean; + direction?: 'up'|'down'; + fetchAll?: boolean; + perPage?: number; + fromCreateAt?: number; + fromPost?: string; +}