diff --git a/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt b/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt index 437e13072d6..b5f83233af2 100644 --- a/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt +++ b/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt @@ -55,26 +55,33 @@ class PushNotificationDataRunnable { val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty() val notificationData = Arguments.createMap() + var channel: ReadableMap? = null + var myTeam: ReadableMap? = null + if (!teamId.isNullOrEmpty()) { val res = fetchTeamIfNeeded(db, serverUrl, teamId) res.first?.let { notificationData.putMap("team", it) } - res.second?.let { notificationData.putMap("myTeam", it) } + + myTeam = res.second + myTeam?.let { notificationData.putMap("myTeam", it) } } if (channelId != null && postId != null) { val channelRes = fetchMyChannel(db, serverUrl, channelId, isCRTEnabled) - channelRes.first?.let { notificationData.putMap("channel", it) } + + channel = channelRes.first + channel?.let { notificationData.putMap("channel", it) } channelRes.second?.let { notificationData.putMap("myChannel", it) } val loadedProfiles = channelRes.third // Fetch categories if needed - if (!teamId.isNullOrEmpty() && notificationData.getMap("myTeam") != null) { + if (!teamId.isNullOrEmpty() && myTeam != null) { // should load all categories val res = fetchMyTeamCategories(db, serverUrl, teamId) res?.let { notificationData.putMap("categories", it) } - } else if (notificationData.getMap("channel") != null) { + } else if (channel != null) { // check if the channel is in the category for the team - val res = addToDefaultCategoryIfNeeded(db, notificationData.getMap("channel")!!) + val res = addToDefaultCategoryIfNeeded(db, channel) res?.let { notificationData.putArray("categoryChannels", it) } } diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/Thread.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/Thread.kt index efdbebb77ca..98427b89522 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/Thread.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/Thread.kt @@ -1,5 +1,6 @@ package com.mattermost.helpers.database_extension +import android.util.Log import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.NoSuchKeyException import com.facebook.react.bridge.ReadableArray @@ -17,7 +18,16 @@ internal fun insertThread(db: WMDatabase, thread: ReadableMap) { val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 } val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 } val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 } - val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 } + val lastReplyAt = try { + var v = thread.getDouble("last_reply_at") + if (v == 0.0) { + val post = thread.getMap("post") + if (post != null) { + v = post.getDouble("create_at") + } + } + v + } catch (e: NoSuchKeyException) { 0 } val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 } db.execute( @@ -44,7 +54,16 @@ internal fun updateThread(db: WMDatabase, thread: ReadableMap, existingRecord: R val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") } val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") } val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") } - val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 } + val lastReplyAt = try { + var v = thread.getDouble("last_reply_at") + if (v == 0.0) { + val post = thread.getMap("post") + if (post != null) { + v = post.getDouble("create_at") + } + } + v + } catch (e: NoSuchKeyException) { 0 } val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 } db.execute( @@ -231,8 +250,31 @@ fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) { fun handleTeamThreadsSync(db: WMDatabase, threadList: ArrayList, teamIds: ArrayList) { val sortedList = threadList.filter{ it.getBoolean("is_following") } - .sortedBy { it.getDouble("last_reply_at") } - .map { it.getDouble("last_reply_at") } + .sortedBy { + var v = it.getDouble("last_reply_at") + if (v == 0.0) { + val post = it.getMap("post"); + if (post != null) { + v = post.getDouble("create_at") + } else { + Log.d("Database", "Trying to add a thread with no replies and no post") + } + } + v + } + .map { + var v = it.getDouble("last_reply_at") + if (v == 0.0) { + val post = it.getMap("post") + if (post != null) { + v = post.getDouble("create_at") + } + } + v + } + if (sortedList.isEmpty()) { + return; + } val earliest = sortedList.first() val latest = sortedList.last() diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index d8e7c18547f..a73bab82c56 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -394,7 +394,7 @@ async function restDeferredAppEntryActions( setTimeout(async () => { if (chData?.channels?.length && chData.memberships?.length && initialTeamId) { if (isCRTEnabled && initialTeamId) { - await syncTeamThreads(serverUrl, initialTeamId, false, false, groupLabel); + await syncTeamThreads(serverUrl, initialTeamId, {groupLabel}); } fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, false, groupLabel); } diff --git a/app/actions/remote/thread.ts b/app/actions/remote/thread.ts index 84270d35b6b..5418c9a08ff 100644 --- a/app/actions/remote/thread.ts +++ b/app/actions/remote/thread.ts @@ -300,7 +300,7 @@ export const syncThreadsIfNeeded = async ( if (teams?.length) { for (const team of teams) { - promises.push(syncTeamThreads(serverUrl, team.id, true, true, groupLabel)); + promises.push(syncTeamThreads(serverUrl, team.id, {excludeDirect: true, fetchOnly: true, groupLabel})); } } @@ -325,9 +325,22 @@ export const syncThreadsIfNeeded = async ( } }; +type SyncThreadOptions = { + excludeDirect?: boolean; + fetchOnly?: boolean; + refresh?: boolean; + groupLabel?: string; +} + export const syncTeamThreads = async ( - serverUrl: string, teamId: string, - excludeDirect = false, fetchOnly = false, groupLabel?: string, + serverUrl: string, + teamId: string, + { + excludeDirect = false, + fetchOnly = false, + refresh = false, + groupLabel, + }: SyncThreadOptions = {}, ) => { try { const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); @@ -368,39 +381,56 @@ export const syncTeamThreads = async ( return {error: allUnreadThreads.error || latestThreads.error}; } - const dedupe = new Set(latestThreads.threads?.map((t) => t.id)); - if (latestThreads.threads?.length) { // We are fetching the threads for the first time. We get "latest" and "earliest" values. + // At this point we may receive threads without replies, so we also check the post.create_at timestamp. const {earliestThread, latestThread} = getThreadsListEdges(latestThreads.threads); - syncDataUpdate.latest = latestThread.last_reply_at; - syncDataUpdate.earliest = earliestThread.last_reply_at; + syncDataUpdate.latest = latestThread.last_reply_at || latestThread.post.create_at; + syncDataUpdate.earliest = earliestThread.last_reply_at || earliestThread.post.create_at; threads.push(...latestThreads.threads); } if (allUnreadThreads.threads?.length) { + const dedupe = new Set(latestThreads.threads?.map((t) => t.id)); const unread = allUnreadThreads.threads.filter((u) => !dedupe.has(u.id)); threads.push(...unread); } } else { - const allNewThreads = await fetchThreads( - serverUrl, - teamId, - {deleted: true, since: syncData.latest + 1, excludeDirect}, - undefined, - undefined, - groupLabel, - ); + const [allUnreadThreads, allNewThreads] = await Promise.all([ + fetchThreads( + serverUrl, + teamId, + {unread: true, excludeDirect}, + Direction.Down, + undefined, + groupLabel, + ), + fetchThreads( + serverUrl, + teamId, + {deleted: true, since: refresh ? undefined : syncData.latest + 1, excludeDirect}, + undefined, + 1, + groupLabel, + ), + ]); + if (allNewThreads.error) { return {error: allNewThreads.error}; } if (allNewThreads.threads?.length) { // As we are syncing, we get all new threads and we will update the "latest" value. const {latestThread} = getThreadsListEdges(allNewThreads.threads); - syncDataUpdate.latest = latestThread.last_reply_at; + const latestDate = latestThread.last_reply_at || latestThread.post.create_at; + syncDataUpdate.latest = Math.max(syncData.latest, latestDate); threads.push(...allNewThreads.threads); } + if (allUnreadThreads.threads?.length) { + const dedupe = new Set(allNewThreads.threads?.map((t) => t.id)); + const unread = allUnreadThreads.threads.filter((u) => !dedupe.has(u.id)); + threads.push(...unread); + } } const models: Model[] = []; diff --git a/app/database/operator/server_data_operator/transformers/thread.ts b/app/database/operator/server_data_operator/transformers/thread.ts index 19d170504d8..7846c98cefd 100644 --- a/app/database/operator/server_data_operator/transformers/thread.ts +++ b/app/database/operator/server_data_operator/transformers/thread.ts @@ -33,8 +33,9 @@ export const transformThreadRecord = ({action, database, value}: TransformerArgs const fieldsMapper = (thread: ThreadModel) => { thread._raw.id = isCreateAction ? (raw?.id ?? thread.id) : record.id; - // When post is individually fetched, we get last_reply_at as 0, so we use the record's value - thread.lastReplyAt = raw.last_reply_at || record?.lastReplyAt; + // When post is individually fetched, we get last_reply_at as 0, so we use the record's value. + // If there is no reply at, we default to the post's create_at + thread.lastReplyAt = raw.last_reply_at || record?.lastReplyAt || raw.post.create_at; thread.lastViewedAt = raw.last_viewed_at ?? record?.lastViewedAt ?? 0; thread.replyCount = raw.reply_count; diff --git a/app/queries/servers/thread.ts b/app/queries/servers/thread.ts index 287d72209b0..d011171af7e 100644 --- a/app/queries/servers/thread.ts +++ b/app/queries/servers/thread.ts @@ -123,7 +123,7 @@ export const prepareThreadsFromReceivedPosts = async (operator: ServerDataOperat id: post.id, participants: post.participants, reply_count: post.reply_count, - last_reply_at: post.last_reply_at, + last_reply_at: post.last_reply_at || post.create_at, is_following: post.is_following, lastFetchedAt: post.create_at, } as ThreadWithLastFetchedAt); diff --git a/app/screens/global_threads/threads_list/index.ts b/app/screens/global_threads/threads_list/index.ts index 93efdac8d22..cd52f200e7e 100644 --- a/app/screens/global_threads/threads_list/index.ts +++ b/app/screens/global_threads/threads_list/index.ts @@ -32,7 +32,7 @@ const enhanced = withObservables(['tab', 'teamId'], ({database, tab, teamId}: Pr threads: teamThreadsSyncObserver.pipe( switchMap((teamThreadsSync) => { const earliest = tab === 'all' ? teamThreadsSync?.[0]?.earliest : 0; - return queryThreadsInTeam(database, teamId, getOnlyUnreads, false, true, true, earliest).observe(); + return queryThreadsInTeam(database, teamId, getOnlyUnreads, true, true, true, earliest).observe(); }), ), }; diff --git a/app/screens/global_threads/threads_list/threads_list.tsx b/app/screens/global_threads/threads_list/threads_list.tsx index 665d1e74979..c5b74890ddd 100644 --- a/app/screens/global_threads/threads_list/threads_list.tsx +++ b/app/screens/global_threads/threads_list/threads_list.tsx @@ -126,7 +126,7 @@ const ThreadsList = ({ const handleRefresh = useCallback(() => { setRefreshing(true); - syncTeamThreads(serverUrl, teamId).finally(() => { + syncTeamThreads(serverUrl, teamId, {refresh: true}).finally(() => { setRefreshing(false); }); }, [serverUrl, teamId]); diff --git a/app/utils/thread/index.ts b/app/utils/thread/index.ts index 099411b1943..33bf1c34101 100644 --- a/app/utils/thread/index.ts +++ b/app/utils/thread/index.ts @@ -35,7 +35,9 @@ export function processIsCRTEnabled(preferences: PreferenceModel[]|PreferenceTyp export const getThreadsListEdges = (threads: Thread[]) => { // Sort a clone of 'threads' array by last_reply_at const sortedThreads = [...threads].sort((a, b) => { - return a.last_reply_at - b.last_reply_at; + const aDate = a.last_reply_at || a.post.create_at; + const bDate = b.last_reply_at || b.post.create_at; + return aDate - bDate; }); const earliestThread = sortedThreads[0]; diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+FetchData.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+FetchData.swift index 4a2fb1735c3..0a9a330354b 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+FetchData.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+FetchData.swift @@ -114,7 +114,11 @@ extension PushNotification { var copy = threads[index] copy.unreadMentions = thread.unreadMentions copy.unreadReplies = thread.unreadReplies - copy.lastReplyAt = thread.lastReplyAt + if (thread.lastReplyAt == 0) { + copy.lastReplyAt = thread.post?.createAt ?? 0 + } else { + copy.lastReplyAt = thread.lastReplyAt + } copy.lastViewedAt = thread.lastViewedAt notificationData.threads?[index] = copy }