Add some improvements to thread handling (#8407)

* Add some improvements to thread handling

* Add missing changes

* Android changes

* iOS changes

* Avoid copys

* Refactor getReplyAt
This commit is contained in:
Daniel Espino García 2025-01-16 11:48:28 +01:00 committed by GitHub
parent 0ad2679595
commit 4b068889e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 104 additions and 33 deletions

View file

@ -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) }
}

View file

@ -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
@ -9,6 +10,21 @@ import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONObject
private fun getLastReplyAt(thread: ReadableMap): Double {
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")
}
}
return v
} catch (e: NoSuchKeyException) {
return 0.0
}
}
internal fun insertThread(db: WMDatabase, thread: ReadableMap) {
// These fields are not present when we extract threads from posts
try {
@ -17,7 +33,7 @@ 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 = getLastReplyAt(thread)
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
db.execute(
@ -44,7 +60,7 @@ 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 = getLastReplyAt(thread)
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
db.execute(
@ -231,8 +247,19 @@ fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) {
fun handleTeamThreadsSync(db: WMDatabase, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
val sortedList = threadList.filter{ it.getBoolean("is_following") }
.sortedBy { it.getDouble("last_reply_at") }
.map { it.getDouble("last_reply_at") }
.sortedBy {
var v = getLastReplyAt(it)
if (v == 0.0) {
Log.d("Database", "Trying to add a thread with no replies and no post")
}
v
}
.map {
getLastReplyAt(it)
}
if (sortedList.isEmpty()) {
return;
}
val earliest = sortedList.first()
val latest = sortedList.last()

View file

@ -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);
}

View file

@ -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[] = [];

View file

@ -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;

View file

@ -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);

View file

@ -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();
}),
),
};

View file

@ -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]);

View file

@ -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];

View file

@ -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
}