diff --git a/keyserver/src/fetchers/thread-fetchers.js b/keyserver/src/fetchers/thread-fetchers.js --- a/keyserver/src/fetchers/thread-fetchers.js +++ b/keyserver/src/fetchers/thread-fetchers.js @@ -38,8 +38,9 @@ const threadsQuery = SQL` SELECT t.id, t.name, t.parent_thread_id, t.containing_thread_id, t.community, t.depth, t.color, t.description, t.type, t.creation_time, - t.source_message, t.replies_count, m.user, m.role, m.permissions, - m.subscription, m.last_read_message < m.last_message AS unread, m.sender + t.source_message, t.replies_count, t.pinned_count, m.user, + m.role, m.permissions, m.subscription, + m.last_read_message < m.last_message AS unread, m.sender FROM threads t LEFT JOIN memberships m ON m.thread = t.id AND m.role >= 0 ` @@ -74,6 +75,7 @@ members: [], roles: {}, repliesCount: threadsRow.replies_count, + pinnedCount: threadsRow.pinned_count, }; } const sourceMessageID = threadsRow.source_message?.toString(); @@ -154,6 +156,10 @@ viewer.platformDetails, 104, ); + const hasCodeVersionBelow205 = !hasMinCodeVersion( + viewer.platformDetails, + 205, + ); const threadInfos = {}; for (const threadID in serverResult.threadInfos) { const serverThreadInfo = serverResult.threadInfos[threadID]; @@ -166,6 +172,7 @@ shimThreadTypes: hasCodeVersionBelow87 ? shimCommunityRoot : null, hideThreadStructure: hasCodeVersionBelow102, filterDetailedThreadEditPermissions: hasCodeVersionBelow104, + excludePinnedCount: hasCodeVersionBelow205, }, ); if (threadInfo) { diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js --- a/lib/shared/thread-utils.js +++ b/lib/shared/thread-utils.js @@ -374,6 +374,7 @@ }, repliesCount: 0, sourceMessageID, + pinnedCount: 0, }; const userInfos = {}; @@ -697,6 +698,7 @@ +[inType: ThreadType]: ThreadType, }, +filterDetailedThreadEditPermissions?: boolean, + +excludePinnedCount?: boolean, }; function rawThreadInfoFromServerThreadInfo( serverThreadInfo: ServerThreadInfo, @@ -709,6 +711,7 @@ const shimThreadTypes = options?.shimThreadTypes; const filterDetailedThreadEditPermissions = options?.filterDetailedThreadEditPermissions; + const excludePinnedCount = options?.excludePinnedCount; const members = []; let currentUser; @@ -809,6 +812,12 @@ visibilityRules: rawThreadInfo.type, }; } + if (!excludePinnedCount) { + return { + ...rawThreadInfo, + pinnedCount: serverThreadInfo.pinnedCount, + }; + } return rawThreadInfo; } @@ -868,7 +877,7 @@ ...threadInfo, uiName: threadUIName(threadInfo), }; - const { sourceMessageID, avatar } = rawThreadInfo; + const { sourceMessageID, avatar, pinnedCount } = rawThreadInfo; if (sourceMessageID) { threadInfo = { ...threadInfo, sourceMessageID }; } @@ -884,6 +893,11 @@ avatar: getUserAvatarForThread(rawThreadInfo, viewerID, userInfos), }; } + + if (pinnedCount) { + threadInfo = { ...threadInfo, pinnedCount }; + } + return threadInfo; } @@ -973,6 +987,7 @@ roles: threadInfo.roles, currentUser: threadInfo.currentUser, repliesCount: threadInfo.repliesCount, + pinnedCount: threadInfo.pinnedCount, }; const { sourceMessageID } = threadInfo; if (sourceMessageID) { diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -211,6 +211,7 @@ +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount?: number, }; export type ThreadInfo = { @@ -230,6 +231,7 @@ +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount?: number, }; export type ResolvedThreadInfo = { @@ -249,6 +251,7 @@ +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount?: number, }; export type ServerMemberInfo = { @@ -275,6 +278,7 @@ +roles: { [id: string]: RoleInfo }, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount: number, }; export type ThreadStore = { @@ -316,6 +320,7 @@ +currentUser: string, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount?: number, }; export type ClientDBReplaceThreadOperation = { diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp --- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp +++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp @@ -255,6 +255,7 @@ ? jsi::String::createFromUtf8(rt, *thread.source_message_id) : jsi::Value::null()); jsiThread.setProperty(rt, "repliesCount", thread.replies_count); + jsiThread.setProperty(rt, "pinnedCount", thread.pinned_count); if (thread.avatar) { auto avatar = jsi::String::createFromUtf8(rt, *thread.avatar); @@ -637,6 +638,8 @@ int repliesCount = std::lround(threadObj.getProperty(rt, "repliesCount").asNumber()); + int pinnedCount = + std::lround(threadObj.getProperty(rt, "pinnedCount").asNumber()); Thread thread{ threadID, type, @@ -651,7 +654,9 @@ roles, currentUser, std::move(sourceMessageID), - repliesCount}; + repliesCount, + NULL, + pinnedCount}; threadStoreOps.push_back( std::make_unique(std::move(thread))); diff --git a/native/redux/persist.js b/native/redux/persist.js --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -24,12 +24,20 @@ type ClientDBMessageInfo, } from 'lib/types/message-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; +import type { + ClientDBThreadStoreOperation, + ClientDBThreadInfo, +} from 'lib/types/thread-types.js'; import { translateClientDBMessageInfoToRawMessageInfo, translateRawMessageInfoToClientDBMessageInfo, } from 'lib/utils/message-ops-utils.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; -import { convertThreadStoreOperationsToClientDBOperations } from 'lib/utils/thread-ops-utils.js'; +import { + convertClientDBThreadInfoToRawThreadInfo, + convertRawThreadInfoToClientDBThreadInfo, + convertThreadStoreOperationsToClientDBOperations, +} from 'lib/utils/thread-ops-utils.js'; import { migrateThreadStoreForEditThreadPermissions } from './edit-thread-permission-migration.js'; import type { AppState } from './state-types.js'; @@ -473,6 +481,85 @@ const { threadIDsToNotifIDs, ...stateSansThreadIDsToNotifIDs } = state; return stateSansThreadIDsToNotifIDs; }, + [35]: (state: AppState) => { + // 1. Get threads and messages from SQLite `threads` and `messages` tables. + const clientDBThreadInfos = commCoreModule.getAllThreadsSync(); + const clientDBMessageInfos = commCoreModule.getAllMessagesSync(); + + // 2. Translate `ClientDBThreadInfo`s to `RawThreadInfo`s and + // `ClientDBMessageInfo`s to `RawMessageInfo`s. + const rawThreadInfos = clientDBThreadInfos.map( + convertClientDBThreadInfoToRawThreadInfo, + ); + const rawMessageInfos = clientDBMessageInfos.map( + translateClientDBMessageInfoToRawMessageInfo, + ); + + // 3. Unshim translated `RawMessageInfos` to get the TOGGLE_PIN messages + const unshimmedRawMessageInfos = rawMessageInfos.map(messageInfo => + unshimFunc(messageInfo, new Set([messageTypes.TOGGLE_PIN])), + ); + + // 4. Filter out non-TOGGLE_PIN messages + const filteredRawMessageInfos = unshimmedRawMessageInfos.filter( + messageInfo => messageInfo.type === messageTypes.TOGGLE_PIN, + ); + + // 5. We want only the last TOGGLE_PIN message for each message ID, + // so 'pin', 'unpin', 'pin' don't count as 3 pins, but only 1. + const lastMessageIDToRawMessageInfoMap = new Map(); + for (const messageInfo of filteredRawMessageInfos) { + const { targetMessageID } = messageInfo; + lastMessageIDToRawMessageInfoMap.set(targetMessageID, messageInfo); + } + const lastMessageIDToRawMessageInfos = Array.from( + lastMessageIDToRawMessageInfoMap.values(), + ); + + // 6. Create a Map of threadIDs to pinnedCount + const threadIDsToPinnedCount = new Map(); + for (const messageInfo of lastMessageIDToRawMessageInfos) { + const { threadID, type } = messageInfo; + if (type === messageTypes.TOGGLE_PIN) { + const pinnedCount = threadIDsToPinnedCount.get(threadID) || 0; + threadIDsToPinnedCount.set(threadID, pinnedCount + 1); + } + } + + // 7. Include a pinnedCount for each rawThreadInfo + const rawThreadInfosWithPinnedCount = rawThreadInfos.map(threadInfo => ({ + ...threadInfo, + pinnedCount: threadIDsToPinnedCount.get(threadInfo.id) || 0, + })); + + // 8. Translate `RawThreadInfo`s to `ClientDBThreadInfo`s. + const convertedClientDBThreadInfos = rawThreadInfosWithPinnedCount.map( + convertRawThreadInfoToClientDBThreadInfo, + ); + + // 9. Construct `ClientDBThreadStoreOperation`s to clear SQLite `threads` + // table and repopulate with `ClientDBThreadInfo`s. + const operations: $ReadOnlyArray = [ + { + type: 'remove_all', + }, + ...convertedClientDBThreadInfos.map((thread: ClientDBThreadInfo) => ({ + type: 'replace', + payload: thread, + })), + ]; + + // 10. Try processing `ClientDBThreadStoreOperation`s and log out if + // `processThreadStoreOperationsSync(...)` throws an exception. + try { + commCoreModule.processThreadStoreOperationsSync(operations); + } catch (exception) { + console.log(exception); + return { ...state, cookie: null }; + } + + return state; + }, }; // After migration 31, we'll no longer want to persist `messageStore.messages` @@ -553,7 +640,7 @@ 'storeLoaded', ], debug: __DEV__, - version: 34, + version: 35, transforms: [messageStoreMessagesBlocklistTransform], migrate: (createMigrate(migrations, { debug: __DEV__ }): any), timeout: ((__DEV__ ? 0 : undefined): number | void),