forked from Ivasoft/mattermost-mobile
[MM-48375 Gekidou] Threads Sync Fix (#6788)
* Init * Test fix * New sync implementation * misc * Includes migration and other servers sync * Misc * Migration fix * Migration is done version 7 * Update app/queries/servers/thread.ts Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * Update app/database/operator/server_data_operator/handlers/team_threads_sync.ts Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * Feedback changes * Fixes when old thread gets a reply * Fix Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
committed by
GitHub
parent
f3f5cef8d1
commit
0e5d63a7c3
@@ -168,7 +168,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
|
|||||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
const models: Model[] = [];
|
const models: Model[] = [];
|
||||||
if (post.root_id) {
|
if (post.root_id) {
|
||||||
// Update the thread data: `reply_count`
|
// Update the thread data: `reply_count`
|
||||||
const {model: threadModel} = await updateThread(serverUrl, post.root_id, {reply_count: post.reply_count}, true);
|
const {model: threadModel} = await updateThread(serverUrl, post.root_id, {reply_count: post.reply_count}, true);
|
||||||
if (threadModel) {
|
if (threadModel) {
|
||||||
models.push(threadModel);
|
models.push(threadModel);
|
||||||
@@ -204,7 +204,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On receiving threads, Along with the "threads" & "thread participants", extract and save "posts" & "users"
|
// On receiving threads, Along with the "threads" & "thread participants", extract and save "posts" & "users"
|
||||||
export async function processReceivedThreads(serverUrl: string, threads: Thread[], teamId: string, loadedInGlobalThreads = false, prepareRecordsOnly = false) {
|
export async function processReceivedThreads(serverUrl: string, threads: Thread[], teamId: string, prepareRecordsOnly = false) {
|
||||||
try {
|
try {
|
||||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
const currentUserId = await getCurrentUserId(database);
|
const currentUserId = await getCurrentUserId(database);
|
||||||
@@ -236,7 +236,6 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[
|
|||||||
threads: threadsToHandle,
|
threads: threadsToHandle,
|
||||||
teamId,
|
teamId,
|
||||||
prepareRecordsOnly: true,
|
prepareRecordsOnly: true,
|
||||||
loadedInGlobalThreads,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const models = [...postModels, ...threadModels];
|
const models = [...postModels, ...threadModels];
|
||||||
@@ -328,3 +327,17 @@ export async function updateThread(serverUrl: string, threadId: string, updatedT
|
|||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateTeamThreadsSync(serverUrl: string, data: TeamThreadsSync, prepareRecordsOnly = false) {
|
||||||
|
try {
|
||||||
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||||
|
const models = await operator.handleTeamThreadsSync({data: [data], prepareRecordsOnly});
|
||||||
|
if (!prepareRecordsOnly) {
|
||||||
|
await operator.batchRecords(models);
|
||||||
|
}
|
||||||
|
return {models};
|
||||||
|
} catch (error) {
|
||||||
|
logError('Failed updateTeamThreadsSync', error);
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preferen
|
|||||||
import {fetchRoles} from '@actions/remote/role';
|
import {fetchRoles} from '@actions/remote/role';
|
||||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||||
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team';
|
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team';
|
||||||
import {fetchNewThreads} from '@actions/remote/thread';
|
import {syncTeamThreads} from '@actions/remote/thread';
|
||||||
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||||
import {gqlAllChannels} from '@client/graphQL/entry';
|
import {gqlAllChannels} from '@client/graphQL/entry';
|
||||||
import {General, Preferences, Screens} from '@constants';
|
import {General, Preferences, Screens} from '@constants';
|
||||||
@@ -28,6 +28,7 @@ import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
|||||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||||
import {getConfig, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system';
|
import {getConfig, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||||
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||||
|
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||||
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
||||||
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
|
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
|
||||||
import {logDebug} from '@utils/log';
|
import {logDebug} from '@utils/log';
|
||||||
@@ -327,14 +328,14 @@ export async function restDeferredAppEntryActions(
|
|||||||
|
|
||||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||||
if (initialTeamId) {
|
if (initialTeamId) {
|
||||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
await syncTeamThreads(serverUrl, initialTeamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teamData.teams?.length) {
|
if (teamData.teams?.length) {
|
||||||
for await (const team of teamData.teams) {
|
for await (const team of teamData.teams) {
|
||||||
if (team.id !== initialTeamId) {
|
if (team.id !== initialTeamId) {
|
||||||
// need to await here since GM/DM threads in different teams overlap
|
// need to await here since GM/DM threads in different teams overlap
|
||||||
await fetchNewThreads(serverUrl, team.id, false);
|
await syncTeamThreads(serverUrl, team.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,6 +407,8 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
|||||||
return 'Server database not found';
|
return 'Server database not found';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {database} = operator;
|
||||||
|
|
||||||
const response = await gqlAllChannels(serverUrl);
|
const response = await gqlAllChannels(serverUrl);
|
||||||
if ('error' in response) {
|
if ('error' in response) {
|
||||||
return response.error;
|
return response.error;
|
||||||
@@ -415,7 +418,7 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
|||||||
return response.errors[0].message;
|
return response.errors[0].message;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = await getCurrentUserId(operator.database);
|
const userId = await getCurrentUserId(database);
|
||||||
|
|
||||||
const channels = getMemberChannelsFromGQLQuery(response.data);
|
const channels = getMemberChannelsFromGQLQuery(response.data);
|
||||||
const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId));
|
const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId));
|
||||||
@@ -424,7 +427,16 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
|||||||
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
|
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
|
||||||
const models = (await Promise.all(modelPromises)).flat();
|
const models = (await Promise.all(modelPromises)).flat();
|
||||||
if (models.length) {
|
if (models.length) {
|
||||||
operator.batchRecords(models);
|
await operator.batchRecords(models);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||||
|
if (isCRTEnabled) {
|
||||||
|
const myTeams = await queryMyTeams(operator.database).fetch();
|
||||||
|
for await (const myTeam of myTeams) {
|
||||||
|
// need to await here since GM/DM threads in different teams overlap
|
||||||
|
await syncTeamThreads(serverUrl, myTeam.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,11 +457,12 @@ const restSyncAllChannelMembers = async (serverUrl: string) => {
|
|||||||
const config = await client.getClientConfigOld();
|
const config = await client.getClientConfigOld();
|
||||||
|
|
||||||
let excludeDirect = false;
|
let excludeDirect = false;
|
||||||
for (const myTeam of myTeams) {
|
for await (const myTeam of myTeams) {
|
||||||
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
|
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
|
||||||
excludeDirect = true;
|
excludeDirect = true;
|
||||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||||
fetchNewThreads(serverUrl, myTeam.id, false);
|
// need to await here since GM/DM threads in different teams overlap
|
||||||
|
await syncTeamThreads(serverUrl, myTeam.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {MyChannelsRequest} from '@actions/remote/channel';
|
|||||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||||
import {MyTeamsRequest} from '@actions/remote/team';
|
import {MyTeamsRequest} from '@actions/remote/team';
|
||||||
import {fetchNewThreads} from '@actions/remote/thread';
|
import {syncTeamThreads} from '@actions/remote/thread';
|
||||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||||
import {Preferences} from '@constants';
|
import {Preferences} from '@constants';
|
||||||
@@ -54,14 +54,14 @@ export async function deferredAppEntryGraphQLActions(
|
|||||||
|
|
||||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads)) {
|
||||||
if (initialTeamId) {
|
if (initialTeamId) {
|
||||||
await fetchNewThreads(serverUrl, initialTeamId, false);
|
await syncTeamThreads(serverUrl, initialTeamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teamData.teams?.length) {
|
if (teamData.teams?.length) {
|
||||||
for await (const team of teamData.teams) {
|
for await (const team of teamData.teams) {
|
||||||
if (team.id !== initialTeamId) {
|
if (team.id !== initialTeamId) {
|
||||||
// need to await here since GM/DM threads in different teams overlap
|
// need to await here since GM/DM threads in different teams overlap
|
||||||
await fetchNewThreads(serverUrl, team.id, false);
|
await syncTeamThreads(serverUrl, team.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -791,7 +791,7 @@ export async function fetchPostById(serverUrl: string, postId: string, fetchOnly
|
|||||||
if (authors?.length) {
|
if (authors?.length) {
|
||||||
const users = await operator.handleUsers({
|
const users = await operator.handleUsers({
|
||||||
users: authors,
|
users: authors,
|
||||||
prepareRecordsOnly: false,
|
prepareRecordsOnly: true,
|
||||||
});
|
});
|
||||||
models.push(...users);
|
models.push(...users);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switchToThread, updateThread} from '@actions/local/thread';
|
import Model from '@nozbe/watermelondb/Model';
|
||||||
|
|
||||||
|
import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switchToThread, updateTeamThreadsSync, updateThread} from '@actions/local/thread';
|
||||||
import {fetchPostThread} from '@actions/remote/post';
|
import {fetchPostThread} from '@actions/remote/post';
|
||||||
import {General} from '@constants';
|
import {General} from '@constants';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
@@ -10,19 +12,13 @@ import AppsManager from '@managers/apps_manager';
|
|||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
import {getPostById} from '@queries/servers/post';
|
import {getPostById} from '@queries/servers/post';
|
||||||
import {getConfigValue, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
import {getConfigValue, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system';
|
||||||
import {getIsCRTEnabled, getNewestThreadInTeam, getThreadById} from '@queries/servers/thread';
|
import {getIsCRTEnabled, getThreadById, getTeamThreadsSyncData} from '@queries/servers/thread';
|
||||||
import {getCurrentUser} from '@queries/servers/user';
|
import {getCurrentUser} from '@queries/servers/user';
|
||||||
|
import {getThreadsListEdges} from '@utils/thread';
|
||||||
|
|
||||||
import {forceLogoutIfNecessary} from './session';
|
import {forceLogoutIfNecessary} from './session';
|
||||||
|
|
||||||
import type {Client} from '@client/rest';
|
import type {Client} from '@client/rest';
|
||||||
import type {Model} from '@nozbe/watermelondb';
|
|
||||||
|
|
||||||
type FetchThreadsRequest = {
|
|
||||||
error?: unknown;
|
|
||||||
} | {
|
|
||||||
data: GetUserThreadsResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FetchThreadsOptions = {
|
type FetchThreadsOptions = {
|
||||||
before?: string;
|
before?: string;
|
||||||
@@ -34,6 +30,11 @@ type FetchThreadsOptions = {
|
|||||||
totalsOnly?: boolean;
|
totalsOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false) => {
|
export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false) => {
|
||||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||||
if (!database) {
|
if (!database) {
|
||||||
@@ -74,57 +75,6 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string,
|
|||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchThreads = async (
|
|
||||||
serverUrl: string,
|
|
||||||
teamId: string,
|
|
||||||
{
|
|
||||||
before,
|
|
||||||
after,
|
|
||||||
perPage = General.CRT_CHUNK_SIZE,
|
|
||||||
deleted = false,
|
|
||||||
unread = false,
|
|
||||||
since,
|
|
||||||
}: FetchThreadsOptions = {
|
|
||||||
perPage: General.CRT_CHUNK_SIZE,
|
|
||||||
deleted: false,
|
|
||||||
unread: false,
|
|
||||||
since: 0,
|
|
||||||
},
|
|
||||||
): Promise<FetchThreadsRequest> => {
|
|
||||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
|
||||||
if (!database) {
|
|
||||||
return {error: `${serverUrl} database not found`};
|
|
||||||
}
|
|
||||||
|
|
||||||
let client;
|
|
||||||
try {
|
|
||||||
client = NetworkManager.getClient(serverUrl);
|
|
||||||
} catch (error) {
|
|
||||||
return {error};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const version = await getConfigValue(database, 'Version');
|
|
||||||
const data = await client.getThreads('me', teamId, before, after, perPage, deleted, unread, since, false, version);
|
|
||||||
|
|
||||||
const {threads} = data;
|
|
||||||
|
|
||||||
if (threads.length) {
|
|
||||||
// Mark all fetched threads as following
|
|
||||||
threads.forEach((thread: Thread) => {
|
|
||||||
thread.is_following = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
await processReceivedThreads(serverUrl, threads, teamId, !unread, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {data};
|
|
||||||
} catch (error) {
|
|
||||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
|
||||||
return {error};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchThread = async (serverUrl: string, teamId: string, threadId: string, extended?: boolean) => {
|
export const fetchThread = async (serverUrl: string, teamId: string, threadId: string, extended?: boolean) => {
|
||||||
let client;
|
let client;
|
||||||
try {
|
try {
|
||||||
@@ -136,7 +86,7 @@ export const fetchThread = async (serverUrl: string, teamId: string, threadId: s
|
|||||||
try {
|
try {
|
||||||
const thread = await client.getThread('me', teamId, threadId, extended);
|
const thread = await client.getThread('me', teamId, threadId, extended);
|
||||||
|
|
||||||
await processReceivedThreads(serverUrl, [thread], teamId, false, false);
|
await processReceivedThreads(serverUrl, [thread], teamId);
|
||||||
|
|
||||||
return {data: thread};
|
return {data: thread};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -286,17 +236,13 @@ export const updateThreadFollowing = async (serverUrl: string, teamId: string, t
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
enum Direction {
|
export const fetchThreads = async (
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchBatchThreads(
|
|
||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
teamId: string,
|
teamId: string,
|
||||||
options: FetchThreadsOptions,
|
options: FetchThreadsOptions,
|
||||||
|
direction?: Direction,
|
||||||
pages?: number,
|
pages?: number,
|
||||||
): Promise<{error: unknown; data?: Thread[]}> {
|
) => {
|
||||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||||
|
|
||||||
if (!operator) {
|
if (!operator) {
|
||||||
@@ -310,12 +256,7 @@ async function fetchBatchThreads(
|
|||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we start from the begging of time (since = 0) we need to fetch threads from newest to oldest (Direction.Down)
|
const fetchDirection = direction ?? Direction.Up;
|
||||||
// if there is another point in time, we need to fetch threads from oldest to newest (Direction.Up)
|
|
||||||
let direction = Direction.Up;
|
|
||||||
if (options.since === 0) {
|
|
||||||
direction = Direction.Down;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentUser = await getCurrentUser(operator.database);
|
const currentUser = await getCurrentUser(operator.database);
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
@@ -323,34 +264,32 @@ async function fetchBatchThreads(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const version = await getConfigValue(operator.database, 'Version');
|
const version = await getConfigValue(operator.database, 'Version');
|
||||||
const data: Thread[] = [];
|
const threadsData: Thread[] = [];
|
||||||
|
|
||||||
|
let currentPage = 0;
|
||||||
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
|
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
|
||||||
let page = 0;
|
|
||||||
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since} = opts;
|
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since} = opts;
|
||||||
|
|
||||||
page += 1;
|
currentPage++;
|
||||||
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version);
|
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version);
|
||||||
if (threads.length) {
|
if (threads.length) {
|
||||||
// Mark all fetched threads as following
|
// Mark all fetched threads as following
|
||||||
for (const thread of threads) {
|
for (const thread of threads) {
|
||||||
thread.is_following = true;
|
thread.is_following = thread.is_following ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.push(...threads);
|
threadsData.push(...threads);
|
||||||
|
|
||||||
if (threads.length === perPage) {
|
if (threads.length === perPage && (pages == null || currentPage < pages!)) {
|
||||||
const newOptions: FetchThreadsOptions = {perPage, deleted, unread};
|
const newOptions: FetchThreadsOptions = {perPage, deleted, unread};
|
||||||
if (direction === Direction.Down) {
|
if (fetchDirection === Direction.Down) {
|
||||||
const last = threads[threads.length - 1];
|
const last = threads[threads.length - 1];
|
||||||
newOptions.before = last.id;
|
newOptions.before = last.id;
|
||||||
} else {
|
} else {
|
||||||
const first = threads[0];
|
const first = threads[0];
|
||||||
newOptions.after = first.id;
|
newOptions.after = first.id;
|
||||||
}
|
}
|
||||||
if (pages != null && page < pages) {
|
await fetchThreadsFunc(newOptions);
|
||||||
fetchThreadsFunc(newOptions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -361,140 +300,179 @@ async function fetchBatchThreads(
|
|||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
return {error};
|
||||||
return {error, data};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {error: false, data};
|
return {error: false, threads: threadsData};
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function fetchNewThreads(
|
|
||||||
serverUrl: string,
|
|
||||||
teamId: string,
|
|
||||||
prepareRecordsOnly = false,
|
|
||||||
): Promise<{error: unknown; models?: Model[]}> {
|
|
||||||
const options: FetchThreadsOptions = {
|
|
||||||
unread: false,
|
|
||||||
deleted: true,
|
|
||||||
perPage: 60,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export const syncTeamThreads = async (serverUrl: string, teamId: string, prepareRecordsOnly = false) => {
|
||||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||||
|
|
||||||
if (!operator) {
|
if (!operator) {
|
||||||
return {error: `${serverUrl} database not found`};
|
return {error: `${serverUrl} database not found`};
|
||||||
}
|
}
|
||||||
|
|
||||||
const newestThread = await getNewestThreadInTeam(operator.database, teamId, false);
|
try {
|
||||||
options.since = newestThread ? newestThread.lastReplyAt : 0;
|
const syncData = await getTeamThreadsSyncData(operator.database, teamId);
|
||||||
|
const syncDataUpdate = {
|
||||||
|
id: teamId,
|
||||||
|
} as TeamThreadsSync;
|
||||||
|
|
||||||
let response: {
|
const threads: Thread[] = [];
|
||||||
error: unknown;
|
|
||||||
data?: Thread[];
|
|
||||||
} = {
|
|
||||||
error: undefined,
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let loadedInGlobalThreads = true;
|
/**
|
||||||
|
* If Syncing for the first time,
|
||||||
// if we have no threads in the DB fetch all unread ones
|
* - Get all unread threads to show the right badges
|
||||||
if (options.since === 0) {
|
* - Get latest threads to show by default in the global threads screen
|
||||||
// options to fetch all unread threads
|
* Else
|
||||||
options.deleted = false;
|
* - Get all threads since last sync
|
||||||
options.unread = true;
|
*/
|
||||||
loadedInGlobalThreads = false;
|
if (!syncData || !syncData?.latest) {
|
||||||
}
|
const [allUnreadThreads, latestThreads] = await Promise.all([
|
||||||
|
fetchThreads(
|
||||||
response = await fetchBatchThreads(serverUrl, teamId, options);
|
serverUrl,
|
||||||
|
teamId,
|
||||||
const {error: nErr, data} = response;
|
{unread: true},
|
||||||
|
Direction.Down,
|
||||||
if (nErr) {
|
),
|
||||||
return {error: nErr};
|
fetchThreads(
|
||||||
}
|
serverUrl,
|
||||||
|
teamId,
|
||||||
if (!data?.length) {
|
{},
|
||||||
return {error: false, models: []};
|
undefined,
|
||||||
}
|
1,
|
||||||
|
),
|
||||||
const {error, models} = await processReceivedThreads(serverUrl, data, teamId, loadedInGlobalThreads, true);
|
]);
|
||||||
|
if (allUnreadThreads.error || latestThreads.error) {
|
||||||
if (!error && !prepareRecordsOnly && models?.length) {
|
return {error: allUnreadThreads.error || latestThreads.error};
|
||||||
try {
|
}
|
||||||
await operator.batchRecords(models);
|
if (latestThreads.threads?.length) {
|
||||||
} catch (err) {
|
// We are fetching the threads for the first time. We get "latest" and "earliest" values.
|
||||||
if (__DEV__) {
|
const {earliestThread, latestThread} = getThreadsListEdges(latestThreads.threads);
|
||||||
throw err;
|
syncDataUpdate.latest = latestThread.last_reply_at;
|
||||||
|
syncDataUpdate.earliest = earliestThread.last_reply_at;
|
||||||
|
|
||||||
|
threads.push(...latestThreads.threads);
|
||||||
|
}
|
||||||
|
if (allUnreadThreads.threads?.length) {
|
||||||
|
threads.push(...allUnreadThreads.threads);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const allNewThreads = await fetchThreads(
|
||||||
|
serverUrl,
|
||||||
|
teamId,
|
||||||
|
{deleted: true, since: syncData.latest},
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
|
||||||
|
threads.push(...allNewThreads.threads);
|
||||||
}
|
}
|
||||||
return {error: true};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const models: Model[] = [];
|
||||||
|
|
||||||
|
if (threads.length) {
|
||||||
|
const {error, models: threadModels = []} = await processReceivedThreads(serverUrl, threads, teamId, true);
|
||||||
|
if (error) {
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadModels?.length) {
|
||||||
|
models.push(...threadModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncDataUpdate.earliest || syncDataUpdate.latest) {
|
||||||
|
const {models: updateModels} = await updateTeamThreadsSync(serverUrl, syncDataUpdate, true);
|
||||||
|
if (updateModels?.length) {
|
||||||
|
models.push(...updateModels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prepareRecordsOnly && models?.length) {
|
||||||
|
try {
|
||||||
|
await operator.batchRecords(models);
|
||||||
|
} catch (err) {
|
||||||
|
if (__DEV__) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return {error: err};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {error: false, models};
|
||||||
|
} catch (error) {
|
||||||
|
return {error};
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {error: false, models};
|
export const loadEarlierThreads = async (serverUrl: string, teamId: string, lastThreadId: string, prepareRecordsOnly = false) => {
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchRefreshThreads(
|
|
||||||
serverUrl: string,
|
|
||||||
teamId: string,
|
|
||||||
unread = false,
|
|
||||||
prepareRecordsOnly = false,
|
|
||||||
): Promise<{error: unknown; models?: Model[]}> {
|
|
||||||
const options: FetchThreadsOptions = {
|
|
||||||
unread,
|
|
||||||
deleted: true,
|
|
||||||
perPage: 60,
|
|
||||||
};
|
|
||||||
|
|
||||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||||
|
|
||||||
if (!operator) {
|
if (!operator) {
|
||||||
return {error: `${serverUrl} database not found`};
|
return {error: `${serverUrl} database not found`};
|
||||||
}
|
}
|
||||||
|
|
||||||
const newestThread = await getNewestThreadInTeam(operator.database, teamId, unread);
|
try {
|
||||||
options.since = newestThread ? newestThread.lastReplyAt : 0;
|
/*
|
||||||
|
* - We will fetch one page of old threads
|
||||||
let response: {
|
* - Update the sync data with the earliest thread last_reply_at timestamp
|
||||||
error: unknown;
|
*/
|
||||||
data?: Thread[];
|
const fetchedThreads = await fetchThreads(
|
||||||
} = {
|
serverUrl,
|
||||||
error: undefined,
|
teamId,
|
||||||
data: [],
|
{
|
||||||
};
|
before: lastThreadId,
|
||||||
|
},
|
||||||
let pages;
|
undefined,
|
||||||
|
1,
|
||||||
// in the case of global threads: if we have no threads in the DB fetch just one page
|
);
|
||||||
if (options.since === 0 && !unread) {
|
if (fetchedThreads.error) {
|
||||||
pages = 1;
|
return {error: fetchedThreads.error};
|
||||||
}
|
|
||||||
|
|
||||||
response = await fetchBatchThreads(serverUrl, teamId, options, pages);
|
|
||||||
|
|
||||||
const {error: nErr, data} = response;
|
|
||||||
|
|
||||||
if (nErr) {
|
|
||||||
return {error: nErr};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data?.length) {
|
|
||||||
return {error: false, models: []};
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadedInGlobalThreads = !unread;
|
|
||||||
const {error, models} = await processReceivedThreads(serverUrl, data, teamId, loadedInGlobalThreads, true);
|
|
||||||
|
|
||||||
if (!error && !prepareRecordsOnly && models?.length) {
|
|
||||||
try {
|
|
||||||
await operator.batchRecords(models);
|
|
||||||
} catch (err) {
|
|
||||||
if (__DEV__) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return {error: true};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {error: false, models};
|
const models: Model[] = [];
|
||||||
}
|
const threads = fetchedThreads.threads || [];
|
||||||
|
|
||||||
|
if (threads?.length) {
|
||||||
|
const {error, models: threadModels = []} = await processReceivedThreads(serverUrl, threads, teamId, true);
|
||||||
|
if (error) {
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadModels?.length) {
|
||||||
|
models.push(...threadModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {earliestThread} = getThreadsListEdges(threads);
|
||||||
|
const syncDataUpdate = {
|
||||||
|
id: teamId,
|
||||||
|
earliest: earliestThread.last_reply_at,
|
||||||
|
} as TeamThreadsSync;
|
||||||
|
const {models: updateModels} = await updateTeamThreadsSync(serverUrl, syncDataUpdate, true);
|
||||||
|
if (updateModels?.length) {
|
||||||
|
models.push(...updateModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prepareRecordsOnly && models?.length) {
|
||||||
|
try {
|
||||||
|
await operator.batchRecords(models);
|
||||||
|
} catch (err) {
|
||||||
|
if (__DEV__) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return {error: err};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {error: false, models, threads};
|
||||||
|
} catch (error) {
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const MM_TABLES = {
|
|||||||
THREAD: 'Thread',
|
THREAD: 'Thread',
|
||||||
THREADS_IN_TEAM: 'ThreadsInTeam',
|
THREADS_IN_TEAM: 'ThreadsInTeam',
|
||||||
THREAD_PARTICIPANT: 'ThreadParticipant',
|
THREAD_PARTICIPANT: 'ThreadParticipant',
|
||||||
|
TEAM_THREADS_SYNC: 'TeamThreadsSync',
|
||||||
USER: 'User',
|
USER: 'User',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, Cha
|
|||||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||||
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel,
|
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
|
||||||
} from '@database/models/server';
|
} from '@database/models/server';
|
||||||
import AppDataOperator from '@database/operator/app_data_operator';
|
import AppDataOperator from '@database/operator/app_data_operator';
|
||||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
@@ -51,7 +51,7 @@ class DatabaseManager {
|
|||||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||||
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel,
|
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
|
||||||
];
|
];
|
||||||
this.databaseDirectory = '';
|
this.databaseDirectory = '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, Cha
|
|||||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||||
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel, ConfigModel,
|
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel, ConfigModel,
|
||||||
} from '@database/models/server';
|
} from '@database/models/server';
|
||||||
import AppDataOperator from '@database/operator/app_data_operator';
|
import AppDataOperator from '@database/operator/app_data_operator';
|
||||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
@@ -50,7 +50,7 @@ class DatabaseManager {
|
|||||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||||
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, UserModel,
|
ThreadModel, ThreadParticipantModel, ThreadInTeamModel, TeamThreadsSyncModel, UserModel,
|
||||||
];
|
];
|
||||||
|
|
||||||
this.databaseDirectory = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupDatabase : `${FileSystem.DocumentDirectoryPath}/databases/`;
|
this.databaseDirectory = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupDatabase : `${FileSystem.DocumentDirectoryPath}/databases/`;
|
||||||
|
|||||||
@@ -8,16 +8,46 @@ import {schemaMigrations, addColumns, createTable} from '@nozbe/watermelondb/Sch
|
|||||||
|
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
import {tableSchemaSpec as configSpec} from '@database/schema/server/table_schemas/config';
|
import {tableSchemaSpec as configSpec} from '@database/schema/server/table_schemas/config';
|
||||||
|
import {tableSchemaSpec as teamThreadsSyncSpec} from '@database/schema/server/table_schemas/team_threads_sync';
|
||||||
|
import {tableSchemaSpec as threadSpec} from '@database/schema/server/table_schemas/thread';
|
||||||
|
import {tableSchemaSpec as threadInTeamSpec} from '@database/schema/server/table_schemas/thread_in_team';
|
||||||
|
import {tableSchemaSpec as threadParticipantSpec} from '@database/schema/server/table_schemas/thread_participant';
|
||||||
|
|
||||||
const {SERVER: {
|
const {SERVER: {
|
||||||
GROUP,
|
GROUP,
|
||||||
MY_CHANNEL,
|
MY_CHANNEL,
|
||||||
TEAM,
|
TEAM,
|
||||||
THREAD,
|
THREAD,
|
||||||
|
THREAD_PARTICIPANT,
|
||||||
|
THREADS_IN_TEAM,
|
||||||
USER,
|
USER,
|
||||||
}} = MM_TABLES;
|
}} = MM_TABLES;
|
||||||
|
|
||||||
export default schemaMigrations({migrations: [
|
export default schemaMigrations({migrations: [
|
||||||
|
{
|
||||||
|
toVersion: 7,
|
||||||
|
steps: [
|
||||||
|
|
||||||
|
// Along with adding the new table - TeamThreadsSync,
|
||||||
|
// We need to clear the data in thread related tables (DROP & CREATE) to fetch the fresh data from the server
|
||||||
|
createTable({
|
||||||
|
...teamThreadsSyncSpec,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
unsafeSql: (baseSql) => {
|
||||||
|
return `
|
||||||
|
${baseSql}
|
||||||
|
DROP TABLE ${THREAD};
|
||||||
|
DROP TABLE ${THREADS_IN_TEAM};
|
||||||
|
DROP TABLE ${THREAD_PARTICIPANT};
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createTable(threadSpec),
|
||||||
|
createTable(threadInTeamSpec),
|
||||||
|
createTable(threadParticipantSpec),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
toVersion: 6,
|
toVersion: 6,
|
||||||
steps: [
|
steps: [
|
||||||
|
|||||||
@@ -31,4 +31,5 @@ export {default as TeamSearchHistoryModel} from './team_search_history';
|
|||||||
export {default as ThreadModel} from './thread';
|
export {default as ThreadModel} from './thread';
|
||||||
export {default as ThreadInTeamModel} from './thread_in_team';
|
export {default as ThreadInTeamModel} from './thread_in_team';
|
||||||
export {default as ThreadParticipantModel} from './thread_participant';
|
export {default as ThreadParticipantModel} from './thread_participant';
|
||||||
|
export {default as TeamThreadsSyncModel} from './team_threads_sync';
|
||||||
export {default as UserModel} from './user';
|
export {default as UserModel} from './user';
|
||||||
|
|||||||
@@ -110,10 +110,7 @@ export default class TeamModel extends Model implements TeamModelInterface {
|
|||||||
|
|
||||||
/** threads : Threads list belonging to a team */
|
/** threads : Threads list belonging to a team */
|
||||||
@lazy threadsList = this.collections.get<ThreadModel>(THREAD).query(
|
@lazy threadsList = this.collections.get<ThreadModel>(THREAD).query(
|
||||||
Q.on(THREADS_IN_TEAM, Q.and(
|
Q.on(THREADS_IN_TEAM, 'team_id', this.id),
|
||||||
Q.where('team_id', this.id),
|
|
||||||
Q.where('loaded_in_global_threads', true),
|
|
||||||
)),
|
|
||||||
Q.and(
|
Q.and(
|
||||||
Q.where('reply_count', Q.gt(0)),
|
Q.where('reply_count', Q.gt(0)),
|
||||||
Q.where('is_following', true),
|
Q.where('is_following', true),
|
||||||
|
|||||||
35
app/database/models/server/team_threads_sync.ts
Normal file
35
app/database/models/server/team_threads_sync.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Relation} from '@nozbe/watermelondb';
|
||||||
|
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
|
||||||
|
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||||
|
|
||||||
|
import {MM_TABLES} from '@constants/database';
|
||||||
|
|
||||||
|
import type TeamModel from '@typings/database/models/servers/team';
|
||||||
|
import type TeamThreadsSyncModelInterface from '@typings/database/models/servers/team_threads_sync';
|
||||||
|
|
||||||
|
const {TEAM, TEAM_THREADS_SYNC} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThreadInTeam model helps us to sync threads without creating any gaps between the threads
|
||||||
|
* by keeping track of the latest and earliest last_replied_at timestamps loaded for a team.
|
||||||
|
*/
|
||||||
|
export default class TeamThreadsSyncModel extends Model implements TeamThreadsSyncModelInterface {
|
||||||
|
/** table (name) : TeamThreadsSync */
|
||||||
|
static table = TEAM_THREADS_SYNC;
|
||||||
|
|
||||||
|
/** associations : Describes every relationship to this table. */
|
||||||
|
static associations: Associations = {
|
||||||
|
[TEAM]: {type: 'belongs_to', key: 'id'},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** earliest: Oldest last_replied_at loaded through infinite loading */
|
||||||
|
@field('earliest') earliest!: number;
|
||||||
|
|
||||||
|
/** latest: Newest last_replied_at loaded during app init / navigating to global threads / pull to refresh */
|
||||||
|
@field('latest') latest!: number;
|
||||||
|
|
||||||
|
@immutableRelation(TEAM, 'id') team!: Relation<TeamModel>;
|
||||||
|
}
|
||||||
@@ -37,9 +37,6 @@ export default class ThreadInTeamModel extends Model implements ThreadInTeamMode
|
|||||||
/** team_id: Associated team identifier */
|
/** team_id: Associated team identifier */
|
||||||
@field('team_id') teamId!: string;
|
@field('team_id') teamId!: string;
|
||||||
|
|
||||||
/** loaded_in_global_threads : Flag to differentiate the unread threads loaded for showing unread counts/mentions */
|
|
||||||
@field('loaded_in_global_threads') loadedInGlobalThreads!: boolean;
|
|
||||||
|
|
||||||
@immutableRelation(THREAD, 'thread_id') thread!: Relation<ThreadModel>;
|
@immutableRelation(THREAD, 'thread_id') thread!: Relation<ThreadModel>;
|
||||||
|
|
||||||
@immutableRelation(TEAM, 'team_id') team!: Relation<TeamModel>;
|
@immutableRelation(TEAM, 'team_id') team!: Relation<TeamModel>;
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Q, Database} from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
import {MM_TABLES} from '@constants/database';
|
||||||
|
import {transformTeamThreadsSyncRecord} from '@database/operator/server_data_operator/transformers/thread';
|
||||||
|
import {getRawRecordPairs, getUniqueRawsBy, getValidRecordsForUpdate} from '@database/operator/utils/general';
|
||||||
|
import {logWarning} from '@utils/log';
|
||||||
|
|
||||||
|
import type {HandleTeamThreadsSyncArgs, RecordPair} from '@typings/database/database';
|
||||||
|
import type TeamThreadsSyncModel from '@typings/database/models/servers/team_threads_sync';
|
||||||
|
|
||||||
|
export interface TeamThreadsSyncHandlerMix {
|
||||||
|
handleTeamThreadsSync: ({data, prepareRecordsOnly}: HandleTeamThreadsSyncArgs) => Promise<TeamThreadsSyncModel[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {TEAM_THREADS_SYNC} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
|
const TeamThreadsSyncHandler = (superclass: any) => class extends superclass {
|
||||||
|
handleTeamThreadsSync = async ({data, prepareRecordsOnly = false}: HandleTeamThreadsSyncArgs): Promise<TeamThreadsSyncModel[]> => {
|
||||||
|
if (!data || !data.length) {
|
||||||
|
logWarning(
|
||||||
|
'An empty or undefined "data" array has been passed to the handleTeamThreadsSync method',
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueRaws = getUniqueRawsBy({raws: data, key: 'id'}) as TeamThreadsSync[];
|
||||||
|
const ids = uniqueRaws.map((item) => item.id);
|
||||||
|
const chunks = await (this.database as Database).get<TeamThreadsSyncModel>(TEAM_THREADS_SYNC).query(
|
||||||
|
Q.where('id', Q.oneOf(ids)),
|
||||||
|
).fetch();
|
||||||
|
const chunksMap = chunks.reduce((result: Record<string, TeamThreadsSyncModel>, chunk) => {
|
||||||
|
result[chunk.id] = chunk;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const create: TeamThreadsSync[] = [];
|
||||||
|
const update: RecordPair[] = [];
|
||||||
|
|
||||||
|
for await (const item of uniqueRaws) {
|
||||||
|
const {id} = item;
|
||||||
|
const chunk = chunksMap[id];
|
||||||
|
if (chunk) {
|
||||||
|
update.push(getValidRecordsForUpdate({
|
||||||
|
tableName: TEAM_THREADS_SYNC,
|
||||||
|
newValue: item,
|
||||||
|
existingRecord: chunk,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
create.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = (await this.prepareRecords({
|
||||||
|
createRaws: getRawRecordPairs(create),
|
||||||
|
updateRaws: update,
|
||||||
|
transformer: transformTeamThreadsSyncRecord,
|
||||||
|
tableName: TEAM_THREADS_SYNC,
|
||||||
|
})) as TeamThreadsSyncModel[];
|
||||||
|
|
||||||
|
if (models?.length && !prepareRecordsOnly) {
|
||||||
|
await this.batchRecords(models);
|
||||||
|
}
|
||||||
|
|
||||||
|
return models;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamThreadsSyncHandler;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {transformThreadRecord, transformThreadParticipantRecord, transformThreadInTeamRecord} from '@database/operator/server_data_operator/transformers/thread';
|
import {transformThreadRecord, transformThreadParticipantRecord, transformThreadInTeamRecord, transformTeamThreadsSyncRecord} from '@database/operator/server_data_operator/transformers/thread';
|
||||||
|
|
||||||
import ServerDataOperator from '..';
|
import ServerDataOperator from '..';
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ describe('*** Operator: Thread Handlers tests ***', () => {
|
|||||||
] as ThreadWithLastFetchedAt[];
|
] as ThreadWithLastFetchedAt[];
|
||||||
|
|
||||||
const threadsMap = {team_id_1: threads};
|
const threadsMap = {team_id_1: threads};
|
||||||
await operator.handleThreads({threads, loadedInGlobalThreads: false, prepareRecordsOnly: false, teamId: 'team_id_1'});
|
await operator.handleThreads({threads, prepareRecordsOnly: false, teamId: 'team_id_1'});
|
||||||
|
|
||||||
expect(spyOnHandleRecords).toHaveBeenCalledWith({
|
expect(spyOnHandleRecords).toHaveBeenCalledWith({
|
||||||
fieldName: 'id',
|
fieldName: 'id',
|
||||||
@@ -76,7 +76,6 @@ describe('*** Operator: Thread Handlers tests ***', () => {
|
|||||||
expect(spyOnHandleThreadInTeam).toHaveBeenCalledWith({
|
expect(spyOnHandleThreadInTeam).toHaveBeenCalledWith({
|
||||||
threadsMap,
|
threadsMap,
|
||||||
prepareRecordsOnly: true,
|
prepareRecordsOnly: true,
|
||||||
loadedInGlobalThreads: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only one batch operation for both tables
|
// Only one batch operation for both tables
|
||||||
@@ -161,21 +160,77 @@ describe('*** Operator: Thread Handlers tests ***', () => {
|
|||||||
team_id_2: team2Threads,
|
team_id_2: team2Threads,
|
||||||
};
|
};
|
||||||
|
|
||||||
await operator.handleThreadInTeam({threadsMap, loadedInGlobalThreads: true, prepareRecordsOnly: false});
|
await operator.handleThreadInTeam({threadsMap, prepareRecordsOnly: false});
|
||||||
|
|
||||||
expect(spyOnPrepareRecords).toHaveBeenCalledWith({
|
expect(spyOnPrepareRecords).toHaveBeenCalledWith({
|
||||||
createRaws: [{
|
createRaws: [{
|
||||||
raw: {team_id: 'team_id_1', thread_id: 'thread-2', loaded_in_global_threads: true},
|
raw: {team_id: 'team_id_1', thread_id: 'thread-1'},
|
||||||
}, {
|
}, {
|
||||||
raw: {team_id: 'team_id_2', thread_id: 'thread-2', loaded_in_global_threads: true},
|
raw: {team_id: 'team_id_1', thread_id: 'thread-2'},
|
||||||
|
}, {
|
||||||
|
raw: {team_id: 'team_id_2', thread_id: 'thread-2'},
|
||||||
}],
|
}],
|
||||||
transformer: transformThreadInTeamRecord,
|
transformer: transformThreadInTeamRecord,
|
||||||
updateRaws: [
|
|
||||||
expect.objectContaining({
|
|
||||||
raw: {team_id: 'team_id_1', thread_id: 'thread-1', loaded_in_global_threads: true},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
tableName: 'ThreadsInTeam',
|
tableName: 'ThreadsInTeam',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('=> HandleTeamThreadsSync: should write to the the TeamThreadsSync table', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
id: 'team_id_1',
|
||||||
|
earliest: 100,
|
||||||
|
latest: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'team_id_2',
|
||||||
|
earliest: 100,
|
||||||
|
latest: 300,
|
||||||
|
},
|
||||||
|
] as TeamThreadsSync[];
|
||||||
|
|
||||||
|
await operator.handleTeamThreadsSync({data, prepareRecordsOnly: false});
|
||||||
|
|
||||||
|
expect(spyOnPrepareRecords).toHaveBeenCalledWith({
|
||||||
|
createRaws: [{
|
||||||
|
raw: {id: 'team_id_1', earliest: 100, latest: 200},
|
||||||
|
}, {
|
||||||
|
raw: {id: 'team_id_2', earliest: 100, latest: 300},
|
||||||
|
}],
|
||||||
|
updateRaws: [],
|
||||||
|
transformer: transformTeamThreadsSyncRecord,
|
||||||
|
tableName: 'TeamThreadsSync',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('=> HandleTeamThreadsSync: should update the record in TeamThreadsSync table', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const spyOnPrepareRecords = jest.spyOn(operator, 'prepareRecords');
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
id: 'team_id_1',
|
||||||
|
earliest: 100,
|
||||||
|
latest: 300,
|
||||||
|
},
|
||||||
|
] as TeamThreadsSync[];
|
||||||
|
|
||||||
|
await operator.handleTeamThreadsSync({data, prepareRecordsOnly: false});
|
||||||
|
|
||||||
|
expect(spyOnPrepareRecords).toHaveBeenCalledWith({
|
||||||
|
createRaws: [],
|
||||||
|
updateRaws: [
|
||||||
|
expect.objectContaining({
|
||||||
|
raw: {id: 'team_id_1', earliest: 100, latest: 300},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
transformer: transformTeamThreadsSyncRecord,
|
||||||
|
tableName: 'TeamThreadsSync',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const {
|
|||||||
} = MM_TABLES.SERVER;
|
} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
export interface ThreadHandlerMix {
|
export interface ThreadHandlerMix {
|
||||||
handleThreads: ({threads, teamId, prepareRecordsOnly, loadedInGlobalThreads}: HandleThreadsArgs) => Promise<Model[]>;
|
handleThreads: ({threads, teamId, prepareRecordsOnly}: HandleThreadsArgs) => Promise<Model[]>;
|
||||||
handleThreadParticipants: ({threadsParticipants, prepareRecordsOnly}: HandleThreadParticipantsArgs) => Promise<ThreadParticipantModel[]>;
|
handleThreadParticipants: ({threadsParticipants, prepareRecordsOnly}: HandleThreadParticipantsArgs) => Promise<ThreadParticipantModel[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ const ThreadHandler = (superclass: any) => class extends superclass {
|
|||||||
* @param {boolean | undefined} handleThreads.prepareRecordsOnly
|
* @param {boolean | undefined} handleThreads.prepareRecordsOnly
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
handleThreads = async ({threads, teamId, loadedInGlobalThreads, prepareRecordsOnly = false}: HandleThreadsArgs): Promise<Model[]> => {
|
handleThreads = async ({threads, teamId, prepareRecordsOnly = false}: HandleThreadsArgs): Promise<Model[]> => {
|
||||||
if (!threads?.length) {
|
if (!threads?.length) {
|
||||||
logWarning(
|
logWarning(
|
||||||
'An empty or undefined "threads" array has been passed to the handleThreads method',
|
'An empty or undefined "threads" array has been passed to the handleThreads method',
|
||||||
@@ -119,7 +119,6 @@ const ThreadHandler = (superclass: any) => class extends superclass {
|
|||||||
const threadsInTeam = await this.handleThreadInTeam({
|
const threadsInTeam = await this.handleThreadInTeam({
|
||||||
threadsMap: {[teamId]: threads},
|
threadsMap: {[teamId]: threads},
|
||||||
prepareRecordsOnly: true,
|
prepareRecordsOnly: true,
|
||||||
loadedInGlobalThreads,
|
|
||||||
}) as ThreadInTeamModel[];
|
}) as ThreadInTeamModel[];
|
||||||
batch.push(...threadsInTeam);
|
batch.push(...threadsInTeam);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ import {Q, Database} from '@nozbe/watermelondb';
|
|||||||
|
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
import {transformThreadInTeamRecord} from '@database/operator/server_data_operator/transformers/thread';
|
import {transformThreadInTeamRecord} from '@database/operator/server_data_operator/transformers/thread';
|
||||||
import {getRawRecordPairs, getValidRecordsForUpdate} from '@database/operator/utils/general';
|
import {getRawRecordPairs} from '@database/operator/utils/general';
|
||||||
import {logWarning} from '@utils/log';
|
import {logWarning} from '@utils/log';
|
||||||
|
|
||||||
import type {HandleThreadInTeamArgs, RecordPair} from '@typings/database/database';
|
import type {HandleThreadInTeamArgs} from '@typings/database/database';
|
||||||
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
|
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
|
||||||
|
|
||||||
export interface ThreadInTeamHandlerMix {
|
export interface ThreadInTeamHandlerMix {
|
||||||
handleThreadInTeam: ({threadsMap, loadedInGlobalThreads, prepareRecordsOnly}: HandleThreadInTeamArgs) => Promise<ThreadInTeamModel[]>;
|
handleThreadInTeam: ({threadsMap, prepareRecordsOnly}: HandleThreadInTeamArgs) => Promise<ThreadInTeamModel[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {THREADS_IN_TEAM} = MM_TABLES.SERVER;
|
const {THREADS_IN_TEAM} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
const ThreadInTeamHandler = (superclass: any) => class extends superclass {
|
const ThreadInTeamHandler = (superclass: any) => class extends superclass {
|
||||||
handleThreadInTeam = async ({threadsMap, loadedInGlobalThreads, prepareRecordsOnly = false}: HandleThreadInTeamArgs): Promise<ThreadInTeamModel[]> => {
|
handleThreadInTeam = async ({threadsMap, prepareRecordsOnly = false}: HandleThreadInTeamArgs): Promise<ThreadInTeamModel[]> => {
|
||||||
if (!threadsMap || !Object.keys(threadsMap).length) {
|
if (!threadsMap || !Object.keys(threadsMap).length) {
|
||||||
logWarning(
|
logWarning(
|
||||||
'An empty or undefined "threadsMap" object has been passed to the handleReceivedPostForChannel method',
|
'An empty or undefined "threadsMap" object has been passed to the handleReceivedPostForChannel method',
|
||||||
@@ -26,12 +26,13 @@ const ThreadInTeamHandler = (superclass: any) => class extends superclass {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const update: RecordPair[] = [];
|
|
||||||
const create: ThreadInTeam[] = [];
|
const create: ThreadInTeam[] = [];
|
||||||
const teamIds = Object.keys(threadsMap);
|
const teamIds = Object.keys(threadsMap);
|
||||||
for await (const teamId of teamIds) {
|
for await (const teamId of teamIds) {
|
||||||
|
const threadIds = threadsMap[teamId].map((thread) => thread.id);
|
||||||
const chunks = await (this.database as Database).get<ThreadInTeamModel>(THREADS_IN_TEAM).query(
|
const chunks = await (this.database as Database).get<ThreadInTeamModel>(THREADS_IN_TEAM).query(
|
||||||
Q.where('team_id', teamId),
|
Q.where('team_id', teamId),
|
||||||
|
Q.where('id', Q.oneOf(threadIds)),
|
||||||
).fetch();
|
).fetch();
|
||||||
const chunksMap = chunks.reduce((result: Record<string, ThreadInTeamModel>, chunk) => {
|
const chunksMap = chunks.reduce((result: Record<string, ThreadInTeamModel>, chunk) => {
|
||||||
result[chunk.threadId] = chunk;
|
result[chunk.threadId] = chunk;
|
||||||
@@ -41,29 +42,18 @@ const ThreadInTeamHandler = (superclass: any) => class extends superclass {
|
|||||||
for (const thread of threadsMap[teamId]) {
|
for (const thread of threadsMap[teamId]) {
|
||||||
const chunk = chunksMap[thread.id];
|
const chunk = chunksMap[thread.id];
|
||||||
|
|
||||||
const newValue = {
|
// Create if the chunk is not found
|
||||||
thread_id: thread.id,
|
if (!chunk) {
|
||||||
team_id: teamId,
|
create.push({
|
||||||
loaded_in_global_threads: Boolean(loadedInGlobalThreads),
|
thread_id: thread.id,
|
||||||
};
|
team_id: teamId,
|
||||||
|
});
|
||||||
// update record only if loaded_in_global_threads is true
|
|
||||||
if (chunk && loadedInGlobalThreads) {
|
|
||||||
update.push(getValidRecordsForUpdate({
|
|
||||||
tableName: THREADS_IN_TEAM,
|
|
||||||
newValue,
|
|
||||||
existingRecord: chunk,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// create chunk
|
|
||||||
create.push(newValue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const threadsInTeam = (await this.prepareRecords({
|
const threadsInTeam = (await this.prepareRecords({
|
||||||
createRaws: getRawRecordPairs(create),
|
createRaws: getRawRecordPairs(create),
|
||||||
updateRaws: update,
|
|
||||||
transformer: transformThreadInTeamRecord,
|
transformer: transformThreadInTeamRecord,
|
||||||
tableName: THREADS_IN_TEAM,
|
tableName: THREADS_IN_TEAM,
|
||||||
})) as ThreadInTeamModel[];
|
})) as ThreadInTeamModel[];
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import PostsInChannelHandler, {PostsInChannelHandlerMix} from '@database/operato
|
|||||||
import PostsInThreadHandler, {PostsInThreadHandlerMix} from '@database/operator/server_data_operator/handlers/posts_in_thread';
|
import PostsInThreadHandler, {PostsInThreadHandlerMix} from '@database/operator/server_data_operator/handlers/posts_in_thread';
|
||||||
import ReactionHander, {ReactionHandlerMix} from '@database/operator/server_data_operator/handlers/reaction';
|
import ReactionHander, {ReactionHandlerMix} from '@database/operator/server_data_operator/handlers/reaction';
|
||||||
import TeamHandler, {TeamHandlerMix} from '@database/operator/server_data_operator/handlers/team';
|
import TeamHandler, {TeamHandlerMix} from '@database/operator/server_data_operator/handlers/team';
|
||||||
|
import TeamThreadsSyncHandler, {TeamThreadsSyncHandlerMix} from '@database/operator/server_data_operator/handlers/team_threads_sync';
|
||||||
import ThreadHandler, {ThreadHandlerMix} from '@database/operator/server_data_operator/handlers/thread';
|
import ThreadHandler, {ThreadHandlerMix} from '@database/operator/server_data_operator/handlers/thread';
|
||||||
import ThreadInTeamHandler, {ThreadInTeamHandlerMix} from '@database/operator/server_data_operator/handlers/thread_in_team';
|
import ThreadInTeamHandler, {ThreadInTeamHandlerMix} from '@database/operator/server_data_operator/handlers/thread_in_team';
|
||||||
import UserHandler, {UserHandlerMix} from '@database/operator/server_data_operator/handlers/user';
|
import UserHandler, {UserHandlerMix} from '@database/operator/server_data_operator/handlers/user';
|
||||||
@@ -29,6 +30,7 @@ interface ServerDataOperator extends
|
|||||||
TeamHandlerMix,
|
TeamHandlerMix,
|
||||||
ThreadHandlerMix,
|
ThreadHandlerMix,
|
||||||
ThreadInTeamHandlerMix,
|
ThreadInTeamHandlerMix,
|
||||||
|
TeamThreadsSyncHandlerMix,
|
||||||
UserHandlerMix
|
UserHandlerMix
|
||||||
{}
|
{}
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ class ServerDataOperator extends mix(ServerDataOperatorBase).with(
|
|||||||
TeamHandler,
|
TeamHandler,
|
||||||
ThreadHandler,
|
ThreadHandler,
|
||||||
ThreadInTeamHandler,
|
ThreadInTeamHandler,
|
||||||
|
TeamThreadsSyncHandler,
|
||||||
UserHandler,
|
UserHandler,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line no-useless-constructor
|
// eslint-disable-next-line no-useless-constructor
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {MM_TABLES, OperationType} from '@constants/database';
|
|||||||
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
|
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
|
||||||
|
|
||||||
import type {TransformerArgs} from '@typings/database/database';
|
import type {TransformerArgs} from '@typings/database/database';
|
||||||
|
import type TeamThreadsSyncModel from '@typings/database/models/servers/team_threads_sync';
|
||||||
import type ThreadModel from '@typings/database/models/servers/thread';
|
import type ThreadModel from '@typings/database/models/servers/thread';
|
||||||
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
|
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
|
||||||
import type ThreadParticipantModel from '@typings/database/models/servers/thread_participant';
|
import type ThreadParticipantModel from '@typings/database/models/servers/thread_participant';
|
||||||
@@ -13,6 +14,7 @@ const {
|
|||||||
THREAD,
|
THREAD,
|
||||||
THREAD_PARTICIPANT,
|
THREAD_PARTICIPANT,
|
||||||
THREADS_IN_TEAM,
|
THREADS_IN_TEAM,
|
||||||
|
TEAM_THREADS_SYNC,
|
||||||
} = MM_TABLES.SERVER;
|
} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +32,10 @@ export const transformThreadRecord = ({action, database, value}: TransformerArgs
|
|||||||
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
|
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
|
||||||
const fieldsMapper = (thread: ThreadModel) => {
|
const fieldsMapper = (thread: ThreadModel) => {
|
||||||
thread._raw.id = isCreateAction ? (raw?.id ?? thread.id) : record.id;
|
thread._raw.id = isCreateAction ? (raw?.id ?? thread.id) : record.id;
|
||||||
thread.lastReplyAt = raw.last_reply_at;
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
thread.lastViewedAt = raw.last_viewed_at ?? record?.lastViewedAt ?? 0;
|
thread.lastViewedAt = raw.last_viewed_at ?? record?.lastViewedAt ?? 0;
|
||||||
thread.replyCount = raw.reply_count;
|
thread.replyCount = raw.reply_count;
|
||||||
thread.isFollowing = raw.is_following ?? record?.isFollowing;
|
thread.isFollowing = raw.is_following ?? record?.isFollowing;
|
||||||
@@ -76,14 +81,10 @@ export const transformThreadParticipantRecord = ({action, database, value}: Tran
|
|||||||
|
|
||||||
export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise<ThreadInTeamModel> => {
|
export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise<ThreadInTeamModel> => {
|
||||||
const raw = value.raw as ThreadInTeam;
|
const raw = value.raw as ThreadInTeam;
|
||||||
const record = value.record as ThreadInTeamModel;
|
|
||||||
|
|
||||||
const fieldsMapper = (threadInTeam: ThreadInTeamModel) => {
|
const fieldsMapper = (threadInTeam: ThreadInTeamModel) => {
|
||||||
threadInTeam.threadId = raw.thread_id;
|
threadInTeam.threadId = raw.thread_id;
|
||||||
threadInTeam.teamId = raw.team_id;
|
threadInTeam.teamId = raw.team_id;
|
||||||
|
|
||||||
// if it's already loaded don't change it
|
|
||||||
threadInTeam.loadedInGlobalThreads = record?.loadedInGlobalThreads || raw.loaded_in_global_threads;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return prepareBaseRecord({
|
return prepareBaseRecord({
|
||||||
@@ -94,3 +95,23 @@ export const transformThreadInTeamRecord = ({action, database, value}: Transform
|
|||||||
fieldsMapper,
|
fieldsMapper,
|
||||||
}) as Promise<ThreadInTeamModel>;
|
}) as Promise<ThreadInTeamModel>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const transformTeamThreadsSyncRecord = ({action, database, value}: TransformerArgs): Promise<TeamThreadsSyncModel> => {
|
||||||
|
const raw = value.raw as TeamThreadsSync;
|
||||||
|
const record = value.record as TeamThreadsSyncModel;
|
||||||
|
const isCreateAction = action === OperationType.CREATE;
|
||||||
|
|
||||||
|
const fieldsMapper = (teamThreadsSync: TeamThreadsSyncModel) => {
|
||||||
|
teamThreadsSync._raw.id = isCreateAction ? (raw?.id ?? teamThreadsSync.id) : record.id;
|
||||||
|
teamThreadsSync.earliest = raw.earliest || record?.earliest;
|
||||||
|
teamThreadsSync.latest = raw.latest || record?.latest;
|
||||||
|
};
|
||||||
|
|
||||||
|
return prepareBaseRecord({
|
||||||
|
action,
|
||||||
|
database,
|
||||||
|
tableName: TEAM_THREADS_SYNC,
|
||||||
|
value,
|
||||||
|
fieldsMapper,
|
||||||
|
}) as Promise<TeamThreadsSyncModel>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ import {
|
|||||||
ThreadSchema,
|
ThreadSchema,
|
||||||
ThreadInTeamSchema,
|
ThreadInTeamSchema,
|
||||||
ThreadParticipantSchema,
|
ThreadParticipantSchema,
|
||||||
|
TeamThreadsSyncSchema,
|
||||||
UserSchema,
|
UserSchema,
|
||||||
} from './table_schemas';
|
} from './table_schemas';
|
||||||
|
|
||||||
export const serverSchema: AppSchema = appSchema({
|
export const serverSchema: AppSchema = appSchema({
|
||||||
version: 6,
|
version: 7,
|
||||||
tables: [
|
tables: [
|
||||||
CategorySchema,
|
CategorySchema,
|
||||||
CategoryChannelSchema,
|
CategoryChannelSchema,
|
||||||
@@ -67,6 +68,7 @@ export const serverSchema: AppSchema = appSchema({
|
|||||||
TeamMembershipSchema,
|
TeamMembershipSchema,
|
||||||
TeamSchema,
|
TeamSchema,
|
||||||
TeamSearchHistorySchema,
|
TeamSearchHistorySchema,
|
||||||
|
TeamThreadsSyncSchema,
|
||||||
ThreadSchema,
|
ThreadSchema,
|
||||||
ThreadInTeamSchema,
|
ThreadInTeamSchema,
|
||||||
ThreadParticipantSchema,
|
ThreadParticipantSchema,
|
||||||
|
|||||||
@@ -30,5 +30,6 @@ export {default as TeamSearchHistorySchema} from './team_search_history';
|
|||||||
export {default as ThreadSchema} from './thread';
|
export {default as ThreadSchema} from './thread';
|
||||||
export {default as ThreadParticipantSchema} from './thread_participant';
|
export {default as ThreadParticipantSchema} from './thread_participant';
|
||||||
export {default as ThreadInTeamSchema} from './thread_in_team';
|
export {default as ThreadInTeamSchema} from './thread_in_team';
|
||||||
|
export {default as TeamThreadsSyncSchema} from './team_threads_sync';
|
||||||
export {default as UserSchema} from './user';
|
export {default as UserSchema} from './user';
|
||||||
export {default as ConfigSchema} from './config';
|
export {default as ConfigSchema} from './config';
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {tableSchema} from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
import {MM_TABLES} from '@constants/database';
|
||||||
|
|
||||||
|
import type {TableSchemaSpec} from '@nozbe/watermelondb/Schema';
|
||||||
|
|
||||||
|
const {TEAM_THREADS_SYNC} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
|
export const tableSchemaSpec: TableSchemaSpec = {
|
||||||
|
name: TEAM_THREADS_SYNC,
|
||||||
|
columns: [
|
||||||
|
{name: 'earliest', type: 'number'},
|
||||||
|
{name: 'latest', type: 'number'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tableSchema(tableSchemaSpec);
|
||||||
@@ -5,9 +5,11 @@ import {tableSchema} from '@nozbe/watermelondb';
|
|||||||
|
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
|
|
||||||
|
import type {TableSchemaSpec} from '@nozbe/watermelondb/Schema';
|
||||||
|
|
||||||
const {THREAD} = MM_TABLES.SERVER;
|
const {THREAD} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
export default tableSchema({
|
export const tableSchemaSpec: TableSchemaSpec = {
|
||||||
name: THREAD,
|
name: THREAD,
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'is_following', type: 'boolean'},
|
{name: 'is_following', type: 'boolean'},
|
||||||
@@ -19,5 +21,7 @@ export default tableSchema({
|
|||||||
{name: 'viewed_at', type: 'number'},
|
{name: 'viewed_at', type: 'number'},
|
||||||
{name: 'last_fetched_at', type: 'number', isIndexed: true},
|
{name: 'last_fetched_at', type: 'number', isIndexed: true},
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export default tableSchema(tableSchemaSpec);
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import {tableSchema} from '@nozbe/watermelondb';
|
|||||||
|
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
|
|
||||||
|
import type {TableSchemaSpec} from '@nozbe/watermelondb/Schema';
|
||||||
|
|
||||||
const {THREADS_IN_TEAM} = MM_TABLES.SERVER;
|
const {THREADS_IN_TEAM} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
export default tableSchema({
|
export const tableSchemaSpec: TableSchemaSpec = {
|
||||||
name: THREADS_IN_TEAM,
|
name: THREADS_IN_TEAM,
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'loaded_in_global_threads', type: 'boolean', isIndexed: true},
|
|
||||||
{name: 'team_id', type: 'string', isIndexed: true},
|
{name: 'team_id', type: 'string', isIndexed: true},
|
||||||
{name: 'thread_id', type: 'string', isIndexed: true},
|
{name: 'thread_id', type: 'string', isIndexed: true},
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export default tableSchema(tableSchemaSpec);
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import {tableSchema} from '@nozbe/watermelondb';
|
|||||||
|
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
|
|
||||||
|
import type {TableSchemaSpec} from '@nozbe/watermelondb/Schema';
|
||||||
|
|
||||||
const {THREAD_PARTICIPANT} = MM_TABLES.SERVER;
|
const {THREAD_PARTICIPANT} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
export default tableSchema({
|
export const tableSchemaSpec: TableSchemaSpec = {
|
||||||
name: THREAD_PARTICIPANT,
|
name: THREAD_PARTICIPANT,
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'thread_id', type: 'string', isIndexed: true},
|
{name: 'thread_id', type: 'string', isIndexed: true},
|
||||||
{name: 'user_id', type: 'string', isIndexed: true},
|
{name: 'user_id', type: 'string', isIndexed: true},
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export default tableSchema(tableSchemaSpec);
|
||||||
|
|||||||
@@ -38,13 +38,14 @@ const {
|
|||||||
THREAD,
|
THREAD,
|
||||||
THREAD_PARTICIPANT,
|
THREAD_PARTICIPANT,
|
||||||
THREADS_IN_TEAM,
|
THREADS_IN_TEAM,
|
||||||
|
TEAM_THREADS_SYNC,
|
||||||
USER,
|
USER,
|
||||||
} = MM_TABLES.SERVER;
|
} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
describe('*** Test schema for SERVER database ***', () => {
|
describe('*** Test schema for SERVER database ***', () => {
|
||||||
it('=> The SERVER SCHEMA should strictly match', () => {
|
it('=> The SERVER SCHEMA should strictly match', () => {
|
||||||
expect(serverSchema).toEqual({
|
expect(serverSchema).toEqual({
|
||||||
version: 6,
|
version: 7,
|
||||||
unsafeSql: undefined,
|
unsafeSql: undefined,
|
||||||
tables: {
|
tables: {
|
||||||
[CATEGORY]: {
|
[CATEGORY]: {
|
||||||
@@ -570,16 +571,26 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||||||
name: THREADS_IN_TEAM,
|
name: THREADS_IN_TEAM,
|
||||||
unsafeSql: undefined,
|
unsafeSql: undefined,
|
||||||
columns: {
|
columns: {
|
||||||
loaded_in_global_threads: {name: 'loaded_in_global_threads', type: 'boolean', isIndexed: true},
|
|
||||||
team_id: {name: 'team_id', type: 'string', isIndexed: true},
|
team_id: {name: 'team_id', type: 'string', isIndexed: true},
|
||||||
thread_id: {name: 'thread_id', type: 'string', isIndexed: true},
|
thread_id: {name: 'thread_id', type: 'string', isIndexed: true},
|
||||||
},
|
},
|
||||||
columnArray: [
|
columnArray: [
|
||||||
{name: 'loaded_in_global_threads', type: 'boolean', isIndexed: true},
|
|
||||||
{name: 'team_id', type: 'string', isIndexed: true},
|
{name: 'team_id', type: 'string', isIndexed: true},
|
||||||
{name: 'thread_id', type: 'string', isIndexed: true},
|
{name: 'thread_id', type: 'string', isIndexed: true},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
[TEAM_THREADS_SYNC]: {
|
||||||
|
name: TEAM_THREADS_SYNC,
|
||||||
|
unsafeSql: undefined,
|
||||||
|
columns: {
|
||||||
|
earliest: {name: 'earliest', type: 'number'},
|
||||||
|
latest: {name: 'latest', type: 'number'},
|
||||||
|
},
|
||||||
|
columnArray: [
|
||||||
|
{name: 'earliest', type: 'number'},
|
||||||
|
{name: 'latest', type: 'number'},
|
||||||
|
],
|
||||||
|
},
|
||||||
[USER]: {
|
[USER]: {
|
||||||
name: USER,
|
name: USER,
|
||||||
unsafeSql: undefined,
|
unsafeSql: undefined,
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import {getConfig, observeConfigValue} from './system';
|
|||||||
|
|
||||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||||
import type Model from '@nozbe/watermelondb/Model';
|
import type Model from '@nozbe/watermelondb/Model';
|
||||||
|
import type TeamThreadsSyncModel from '@typings/database/models/servers/team_threads_sync';
|
||||||
import type ThreadModel from '@typings/database/models/servers/thread';
|
import type ThreadModel from '@typings/database/models/servers/thread';
|
||||||
import type UserModel from '@typings/database/models/servers/user';
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
||||||
const {SERVER: {CHANNEL, POST, THREAD, THREADS_IN_TEAM, THREAD_PARTICIPANT, USER}} = MM_TABLES;
|
const {SERVER: {CHANNEL, POST, THREAD, THREADS_IN_TEAM, THREAD_PARTICIPANT, TEAM_THREADS_SYNC, USER}} = MM_TABLES;
|
||||||
|
|
||||||
export const getIsCRTEnabled = async (database: Database): Promise<boolean> => {
|
export const getIsCRTEnabled = async (database: Database): Promise<boolean> => {
|
||||||
const config = await getConfig(database);
|
const config = await getConfig(database);
|
||||||
@@ -34,6 +35,11 @@ export const getThreadById = async (database: Database, threadId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTeamThreadsSyncData = async (database: Database, teamId: string): Promise<TeamThreadsSyncModel | undefined> => {
|
||||||
|
const result = await queryTeamThreadsSync(database, teamId).fetch();
|
||||||
|
return result?.[0];
|
||||||
|
};
|
||||||
|
|
||||||
export const observeIsCRTEnabled = (database: Database) => {
|
export const observeIsCRTEnabled = (database: Database) => {
|
||||||
const cfgValue = observeConfigValue(database, 'CollapsedThreads');
|
const cfgValue = observeConfigValue(database, 'CollapsedThreads');
|
||||||
const featureFlag = observeConfigValue(database, 'FeatureFlagCollapsedThreads');
|
const featureFlag = observeConfigValue(database, 'FeatureFlagCollapsedThreads');
|
||||||
@@ -133,7 +139,7 @@ export const prepareThreadsFromReceivedPosts = async (operator: ServerDataOperat
|
|||||||
return models;
|
return models;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const queryThreadsInTeam = (database: Database, teamId: string, onlyUnreads?: boolean, hasReplies?: boolean, isFollowing?: boolean, sort?: boolean, limit?: number): Query<ThreadModel> => {
|
export const queryThreadsInTeam = (database: Database, teamId: string, onlyUnreads?: boolean, hasReplies?: boolean, isFollowing?: boolean, sort?: boolean, earliest?: number): Query<ThreadModel> => {
|
||||||
const query: Q.Clause[] = [];
|
const query: Q.Clause[] = [];
|
||||||
|
|
||||||
if (isFollowing) {
|
if (isFollowing) {
|
||||||
@@ -152,38 +158,22 @@ export const queryThreadsInTeam = (database: Database, teamId: string, onlyUnrea
|
|||||||
query.push(Q.sortBy('last_reply_at', Q.desc));
|
query.push(Q.sortBy('last_reply_at', Q.desc));
|
||||||
}
|
}
|
||||||
|
|
||||||
let joinCondition: Q.Condition = Q.where('team_id', teamId);
|
|
||||||
|
|
||||||
if (!onlyUnreads) {
|
|
||||||
joinCondition = Q.and(
|
|
||||||
Q.where('team_id', teamId),
|
|
||||||
Q.where('loaded_in_global_threads', true),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
query.push(
|
query.push(
|
||||||
Q.on(THREADS_IN_TEAM, joinCondition),
|
Q.on(THREADS_IN_TEAM, Q.where('team_id', teamId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (limit) {
|
if (earliest) {
|
||||||
query.push(Q.take(limit));
|
query.push(Q.where('last_reply_at', Q.gte(earliest)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return database.get<ThreadModel>(THREAD).query(...query);
|
return database.get<ThreadModel>(THREAD).query(...query);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getNewestThreadInTeam(
|
export const queryTeamThreadsSync = (database: Database, teamId: string) => {
|
||||||
database: Database,
|
return database.get<TeamThreadsSyncModel>(TEAM_THREADS_SYNC).query(
|
||||||
teamId: string,
|
Q.where('id', teamId),
|
||||||
unread: boolean,
|
);
|
||||||
): Promise<ThreadModel | undefined> {
|
};
|
||||||
try {
|
|
||||||
const threads = await queryThreadsInTeam(database, teamId, unread, true, true, true, 1).fetch();
|
|
||||||
return threads?.[0] || undefined;
|
|
||||||
} catch (e) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function observeThreadMentionCount(database: Database, teamId?: string, includeDmGm?: boolean): Observable<number> {
|
export function observeThreadMentionCount(database: Database, teamId?: string, includeDmGm?: boolean): Observable<number> {
|
||||||
return observeUnreadsAndMentionsInTeam(database, teamId, includeDmGm).pipe(
|
return observeUnreadsAndMentionsInTeam(database, teamId, includeDmGm).pipe(
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||||
import withObservables from '@nozbe/with-observables';
|
import withObservables from '@nozbe/with-observables';
|
||||||
import {AppStateStatus} from 'react-native';
|
import {AppStateStatus} from 'react-native';
|
||||||
|
import {switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||||
import {queryThreadsInTeam} from '@queries/servers/thread';
|
import {queryTeamThreadsSync, queryThreadsInTeam} from '@queries/servers/thread';
|
||||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||||
|
|
||||||
import ThreadsList from './threads_list';
|
import ThreadsList from './threads_list';
|
||||||
@@ -26,10 +27,17 @@ const withTeamId = withObservables([], ({database}: WithDatabaseArgs) => ({
|
|||||||
const enhanced = withObservables(['tab', 'teamId', 'forceQueryAfterAppState'], ({database, tab, teamId}: Props) => {
|
const enhanced = withObservables(['tab', 'teamId', 'forceQueryAfterAppState'], ({database, tab, teamId}: Props) => {
|
||||||
const getOnlyUnreads = tab !== 'all';
|
const getOnlyUnreads = tab !== 'all';
|
||||||
|
|
||||||
|
const teamThreadsSyncObserver = queryTeamThreadsSync(database, teamId).observeWithColumns(['earliest']);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
unreadsCount: queryThreadsInTeam(database, teamId, true).observeCount(false),
|
unreadsCount: queryThreadsInTeam(database, teamId, true).observeCount(false),
|
||||||
teammateNameDisplay: observeTeammateNameDisplay(database),
|
teammateNameDisplay: observeTeammateNameDisplay(database),
|
||||||
threads: queryThreadsInTeam(database, teamId, getOnlyUnreads, false, true, true).observe(),
|
threads: teamThreadsSyncObserver.pipe(
|
||||||
|
switchMap((teamThreadsSync) => {
|
||||||
|
const earliest = teamThreadsSync?.[0]?.earliest;
|
||||||
|
return queryThreadsInTeam(database, teamId, getOnlyUnreads, false, true, true, earliest).observe();
|
||||||
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import React, {useCallback, useEffect, useMemo, useState, useRef} from 'react';
|
import React, {useCallback, useEffect, useMemo, useState, useRef} from 'react';
|
||||||
import {FlatList, ListRenderItemInfo, StyleSheet} from 'react-native';
|
import {FlatList, ListRenderItemInfo, StyleSheet} from 'react-native';
|
||||||
|
|
||||||
import {fetchNewThreads, fetchRefreshThreads, fetchThreads} from '@actions/remote/thread';
|
import {loadEarlierThreads, syncTeamThreads} from '@actions/remote/thread';
|
||||||
import Loading from '@components/loading';
|
import Loading from '@components/loading';
|
||||||
import {General, Screens} from '@constants';
|
import {General, Screens} from '@constants';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
@@ -57,6 +57,7 @@ const ThreadsList = ({
|
|||||||
const serverUrl = useServerUrl();
|
const serverUrl = useServerUrl();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const flatListRef = useRef<FlatList<ThreadModel>>(null);
|
||||||
const hasFetchedOnce = useRef(false);
|
const hasFetchedOnce = useRef(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [endReached, setEndReached] = useState(false);
|
const [endReached, setEndReached] = useState(false);
|
||||||
@@ -66,22 +67,21 @@ const ThreadsList = ({
|
|||||||
const lastThread = threads?.length > 0 ? threads[threads.length - 1] : null;
|
const lastThread = threads?.length > 0 ? threads[threads.length - 1] : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This is to be called only when there are no threads
|
if (hasFetchedOnce.current || tab !== 'all') {
|
||||||
if (tab === 'all' && noThreads && !hasFetchedOnce.current) {
|
return;
|
||||||
setIsLoading(true);
|
|
||||||
fetchThreads(serverUrl, teamId).finally(() => {
|
|
||||||
hasFetchedOnce.current = true;
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [noThreads, serverUrl, tab]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Display loading only when there are no threads
|
||||||
// This is to be called when threads already exist locally and to fetch the latest threads
|
|
||||||
if (!noThreads) {
|
if (!noThreads) {
|
||||||
fetchNewThreads(serverUrl, teamId);
|
setIsLoading(true);
|
||||||
}
|
}
|
||||||
}, [noThreads, serverUrl, teamId]);
|
syncTeamThreads(serverUrl, teamId).then(() => {
|
||||||
|
hasFetchedOnce.current = true;
|
||||||
|
});
|
||||||
|
if (!noThreads) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [noThreads, serverUrl, tab, teamId]);
|
||||||
|
|
||||||
const listEmptyComponent = useMemo(() => {
|
const listEmptyComponent = useMemo(() => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -118,33 +118,33 @@ const ThreadsList = ({
|
|||||||
return null;
|
return null;
|
||||||
}, [isLoading, tab, theme, endReached]);
|
}, [isLoading, tab, theme, endReached]);
|
||||||
|
|
||||||
|
const handleTabChange = useCallback((value: GlobalThreadsTab) => {
|
||||||
|
setTab(value);
|
||||||
|
flatListRef.current?.scrollToOffset({animated: true, offset: 0});
|
||||||
|
}, [setTab]);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
|
|
||||||
fetchRefreshThreads(serverUrl, teamId, tab === 'unreads').finally(() => {
|
syncTeamThreads(serverUrl, teamId).finally(() => {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
});
|
});
|
||||||
}, [serverUrl, teamId]);
|
}, [serverUrl, teamId]);
|
||||||
|
|
||||||
const handleEndReached = useCallback(() => {
|
const handleEndReached = useCallback(() => {
|
||||||
if (!lastThread || tab === 'unreads' || endReached) {
|
if (tab === 'unreads' || endReached || !lastThread) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
|
||||||
before: lastThread.id,
|
|
||||||
perPage: General.CRT_CHUNK_SIZE,
|
|
||||||
};
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
fetchThreads(serverUrl, teamId, options).then((response) => {
|
loadEarlierThreads(serverUrl, teamId, lastThread.id).then((response) => {
|
||||||
if ('data' in response) {
|
if (response.threads) {
|
||||||
setEndReached(response.data.threads.length < General.CRT_CHUNK_SIZE);
|
setEndReached(response.threads.length < General.CRT_CHUNK_SIZE);
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, [endReached, lastThread?.id, serverUrl, tab, teamId]);
|
}, [endReached, serverUrl, tab, teamId, lastThread]);
|
||||||
|
|
||||||
const renderItem = useCallback(({item}: ListRenderItemInfo<ThreadModel>) => (
|
const renderItem = useCallback(({item}: ListRenderItemInfo<ThreadModel>) => (
|
||||||
<Thread
|
<Thread
|
||||||
@@ -158,7 +158,7 @@ const ThreadsList = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
<Header
|
||||||
setTab={setTab}
|
setTab={handleTabChange}
|
||||||
tab={tab}
|
tab={tab}
|
||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
testID={`${testID}.header`}
|
testID={`${testID}.header`}
|
||||||
@@ -172,6 +172,7 @@ const ThreadsList = ({
|
|||||||
maxToRenderPerBatch={10}
|
maxToRenderPerBatch={10}
|
||||||
onEndReached={handleEndReached}
|
onEndReached={handleEndReached}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
ref={flatListRef}
|
||||||
refreshing={isRefreshing}
|
refreshing={isRefreshing}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
|
|||||||
@@ -23,3 +23,15 @@ export function processIsCRTEnabled(preferences: PreferenceModel[]|PreferenceTyp
|
|||||||
configValue === Config.ALWAYS_ON
|
configValue === Config.ALWAYS_ON
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 earliestThread = sortedThreads[0];
|
||||||
|
const latestThread = sortedThreads[sortedThreads.length - 1];
|
||||||
|
|
||||||
|
return {earliestThread, latestThread};
|
||||||
|
};
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ export type HandleThreadsArgs = {
|
|||||||
threads?: ThreadWithLastFetchedAt[];
|
threads?: ThreadWithLastFetchedAt[];
|
||||||
prepareRecordsOnly?: boolean;
|
prepareRecordsOnly?: boolean;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
loadedInGlobalThreads?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HandleThreadParticipantsArgs = {
|
export type HandleThreadParticipantsArgs = {
|
||||||
@@ -102,7 +101,11 @@ export type HandleThreadParticipantsArgs = {
|
|||||||
export type HandleThreadInTeamArgs = {
|
export type HandleThreadInTeamArgs = {
|
||||||
threadsMap?: Record<string, Thread[]>;
|
threadsMap?: Record<string, Thread[]>;
|
||||||
prepareRecordsOnly?: boolean;
|
prepareRecordsOnly?: boolean;
|
||||||
loadedInGlobalThreads?: boolean;
|
};
|
||||||
|
|
||||||
|
export type HandleTeamThreadsSyncArgs = {
|
||||||
|
data: TeamThreadsSync[];
|
||||||
|
prepareRecordsOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SanitizeReactionsArgs = {
|
export type SanitizeReactionsArgs = {
|
||||||
|
|||||||
29
types/database/models/servers/team_threads_sync.ts
Normal file
29
types/database/models/servers/team_threads_sync.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type TeamModel from './team';
|
||||||
|
import type {Relation, Model} from '@nozbe/watermelondb';
|
||||||
|
import type {Associations} from '@nozbe/watermelondb/Model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThreadInTeam model helps us to sync threads without creating any gaps between the threads
|
||||||
|
* by keeping track of the latest and earliest last_replied_at timestamps loaded for a team.
|
||||||
|
*/
|
||||||
|
declare class TeamThreadsSyncModel extends Model {
|
||||||
|
/** table (name) : TeamThreadsSync */
|
||||||
|
static table: string;
|
||||||
|
|
||||||
|
/** associations : Describes every relationship to this table. */
|
||||||
|
static associations: Associations;
|
||||||
|
|
||||||
|
/** earliest: Oldest last_replied_at loaded through infinite loading */
|
||||||
|
earliest: number;
|
||||||
|
|
||||||
|
/** latest: Newest last_replied_at loaded during app init / navigating to global threads / pull to refresh */
|
||||||
|
latest: number;
|
||||||
|
|
||||||
|
/** team : The related record to the parent Team model */
|
||||||
|
team: Relation<TeamModel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TeamThreadsSyncModel;
|
||||||
@@ -24,9 +24,6 @@ declare class ThreadInTeamModel extends Model {
|
|||||||
/** teamId: Associated thread identifier */
|
/** teamId: Associated thread identifier */
|
||||||
teamId: string;
|
teamId: string;
|
||||||
|
|
||||||
/** loaded_in_global_threads : Flag to differentiate the unread threads loaded for showing unread counts/mentions */
|
|
||||||
loadedInGlobalThreads: boolean;
|
|
||||||
|
|
||||||
/** thread : The related record to the parent Thread model */
|
/** thread : The related record to the parent Thread model */
|
||||||
thread: Relation<ThreadModel>;
|
thread: Relation<ThreadModel>;
|
||||||
|
|
||||||
|
|||||||
8
types/database/raw_values.d.ts
vendored
8
types/database/raw_values.d.ts
vendored
@@ -91,7 +91,12 @@ type TermsOfService = {
|
|||||||
type ThreadInTeam = {
|
type ThreadInTeam = {
|
||||||
thread_id: string;
|
thread_id: string;
|
||||||
team_id: string;
|
team_id: string;
|
||||||
loaded_in_global_threads: boolean;
|
};
|
||||||
|
|
||||||
|
type TeamThreadsSync = {
|
||||||
|
id: string;
|
||||||
|
earliest: number;
|
||||||
|
latest: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RawValue =
|
type RawValue =
|
||||||
@@ -125,5 +130,6 @@ type RawValue =
|
|||||||
| ThreadWithLastFetchedAt
|
| ThreadWithLastFetchedAt
|
||||||
| ThreadInTeam
|
| ThreadInTeam
|
||||||
| ThreadParticipant
|
| ThreadParticipant
|
||||||
|
| TeamThreadsSync
|
||||||
| UserProfile
|
| UserProfile
|
||||||
| Pick<ChannelMembership, 'channel_id' | 'user_id'>
|
| Pick<ChannelMembership, 'channel_id' | 'user_id'>
|
||||||
|
|||||||
Reference in New Issue
Block a user