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 @@ -36,12 +36,13 @@ `.append(whereClause); 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, t.avatar, 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 + 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, t.avatar, 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 ` .append(whereClause) .append(SQL` ORDER BY m.user ASC`); @@ -74,6 +75,7 @@ members: [], roles: {}, repliesCount: threadsRow.replies_count, + pinnedCount: threadsRow.pinned_count, }; if (threadsRow.avatar) { threadInfos[threadID] = { @@ -160,6 +162,10 @@ viewer.platformDetails, 104, ); + const hasCodeVersionBelow207 = !hasMinCodeVersion( + viewer.platformDetails, + 207, + ); const threadInfos = {}; for (const threadID in serverResult.threadInfos) { const serverThreadInfo = serverResult.threadInfos[threadID]; @@ -172,6 +178,7 @@ shimThreadTypes: hasCodeVersionBelow87 ? shimCommunityRoot : null, hideThreadStructure: hasCodeVersionBelow102, filterDetailedThreadEditPermissions: hasCodeVersionBelow104, + excludePinnedCount: hasCodeVersionBelow207, }, ); 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; @@ -812,6 +815,12 @@ visibilityRules: rawThreadInfo.type, }; } + if (!excludePinnedCount) { + return { + ...rawThreadInfo, + pinnedCount: serverThreadInfo.pinnedCount, + }; + } return rawThreadInfo; } @@ -871,7 +880,7 @@ ...threadInfo, uiName: threadUIName(threadInfo), }; - const { sourceMessageID, avatar } = rawThreadInfo; + const { sourceMessageID, avatar, pinnedCount } = rawThreadInfo; if (sourceMessageID) { threadInfo = { ...threadInfo, sourceMessageID }; } @@ -887,6 +896,11 @@ avatar: getUserAvatarForThread(rawThreadInfo, viewerID, userInfos), }; } + + if (pinnedCount) { + threadInfo = { ...threadInfo, pinnedCount }; + } + return threadInfo; } diff --git a/lib/shared/unshim-utils.js b/lib/shared/unshim-utils.js --- a/lib/shared/unshim-utils.js +++ b/lib/shared/unshim-utils.js @@ -44,6 +44,7 @@ messageTypes.SIDEBAR_SOURCE, messageTypes.MULTIMEDIA, messageTypes.REACTION, + messageTypes.TOGGLE_PIN, ]); function unshimMessageInfos( messageInfos: $ReadOnlyArray, 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 @@ -217,6 +217,7 @@ +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount?: number, }; export type ThreadInfo = { @@ -236,6 +237,7 @@ +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount?: number, }; export type ResolvedThreadInfo = { @@ -255,6 +257,7 @@ +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount?: number, }; export type ServerMemberInfo = { @@ -282,6 +285,7 @@ +roles: { [id: string]: RoleInfo }, +sourceMessageID?: string, +repliesCount: number, + +pinnedCount: number, }; export type ThreadStore = { @@ -323,6 +327,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,10 @@ int repliesCount = std::lround(threadObj.getProperty(rt, "repliesCount").asNumber()); + + int pinnedCount = threadObj.hasProperty(rt, "pinnedCount") + ? std::lround(threadObj.getProperty(rt, "pinnedCount").asNumber()) + : 0; Thread thread{ threadID, type, @@ -651,7 +656,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 @@ -13,7 +13,10 @@ getContainingThreadID, getCommunity, } from 'lib/shared/thread-utils.js'; -import { DEPRECATED_unshimMessageStore } from 'lib/shared/unshim-utils.js'; +import { + DEPRECATED_unshimMessageStore, + unshimFunc, +} from 'lib/shared/unshim-utils.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { @@ -23,9 +26,20 @@ type ClientDBMessageStoreOperation, } from 'lib/types/message-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; -import { translateRawMessageInfoToClientDBMessageInfo } from 'lib/utils/message-ops-utils.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'; @@ -391,6 +405,85 @@ return stateSansThreadIDsToNotifIDs; }, [35]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), + [36]: (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` @@ -471,7 +564,7 @@ 'storeLoaded', ], debug: __DEV__, - version: 35, + version: 36, transforms: [messageStoreMessagesBlocklistTransform], migrate: (createMigrate(migrations, { debug: __DEV__ }): any), timeout: ((__DEV__ ? 0 : undefined): number | void),