forked from Ivasoft/mattermost-mobile
487 lines
14 KiB
TypeScript
487 lines
14 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switchToThread, updateThread} from '@actions/local/thread';
|
|
import {fetchPostThread} from '@actions/remote/post';
|
|
import {General} from '@constants';
|
|
import DatabaseManager from '@database/manager';
|
|
import PushNotifications from '@init/push_notifications';
|
|
import NetworkManager from '@managers/network_manager';
|
|
import {getPostById} from '@queries/servers/post';
|
|
import {getCommonSystemValues, getCurrentTeamId} from '@queries/servers/system';
|
|
import {getIsCRTEnabled, getNewestThreadInTeam, getThreadById} from '@queries/servers/thread';
|
|
import {getCurrentUser} from '@queries/servers/user';
|
|
|
|
import {forceLogoutIfNecessary} from './session';
|
|
|
|
import type {Client} from '@client/rest';
|
|
import type {Model} from '@nozbe/watermelondb';
|
|
|
|
type FetchThreadsRequest = {
|
|
error?: unknown;
|
|
} | {
|
|
data: GetUserThreadsResponse;
|
|
};
|
|
|
|
type FetchThreadsOptions = {
|
|
before?: string;
|
|
after?: string;
|
|
perPage?: number;
|
|
deleted?: boolean;
|
|
unread?: boolean;
|
|
since?: number;
|
|
totalsOnly?: boolean;
|
|
};
|
|
|
|
export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false) => {
|
|
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
|
if (!database) {
|
|
return {error: `${serverUrl} database not found`};
|
|
}
|
|
|
|
// Load thread before we open to the thread modal
|
|
fetchPostThread(serverUrl, rootId);
|
|
|
|
// Mark thread as read
|
|
const isCRTEnabled = await getIsCRTEnabled(database);
|
|
if (isCRTEnabled) {
|
|
const post = await getPostById(database, rootId);
|
|
if (post) {
|
|
const thread = await getThreadById(database, rootId);
|
|
if (thread?.isFollowing) {
|
|
markThreadAsViewed(serverUrl, thread.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
await switchToThread(serverUrl, rootId, isFromNotification);
|
|
|
|
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 {config} = await getCommonSystemValues(database);
|
|
|
|
const data = await client.getThreads('me', teamId, before, after, perPage, deleted, unread, since, false, config.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) => {
|
|
let client;
|
|
try {
|
|
client = NetworkManager.getClient(serverUrl);
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
|
|
try {
|
|
const thread = await client.getThread('me', teamId, threadId, extended);
|
|
|
|
await processReceivedThreads(serverUrl, [thread], teamId, false, false);
|
|
|
|
return {data: thread};
|
|
} catch (error) {
|
|
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
export const updateTeamThreadsAsRead = async (serverUrl: string, teamId: string) => {
|
|
let client;
|
|
try {
|
|
client = NetworkManager.getClient(serverUrl);
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
|
|
try {
|
|
const data = await client.updateTeamThreadsAsRead('me', teamId);
|
|
|
|
// Update locally
|
|
await markTeamThreadsAsRead(serverUrl, teamId);
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
export const markThreadAsRead = async (serverUrl: string, teamId: string | undefined, threadId: string) => {
|
|
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 timestamp = Date.now();
|
|
|
|
// DM/GM doesn't have a teamId, so we pass the current team id
|
|
let threadTeamId = teamId;
|
|
if (!threadTeamId) {
|
|
threadTeamId = await getCurrentTeamId(database);
|
|
}
|
|
const data = await client.markThreadAsRead('me', threadTeamId, threadId, timestamp);
|
|
|
|
// Update locally
|
|
await updateThread(serverUrl, threadId, {
|
|
last_viewed_at: timestamp,
|
|
unread_replies: 0,
|
|
unread_mentions: 0,
|
|
});
|
|
|
|
const isCRTEnabled = await getIsCRTEnabled(database);
|
|
const post = await getPostById(database, threadId);
|
|
if (post) {
|
|
if (isCRTEnabled) {
|
|
PushNotifications.removeThreadNotifications(serverUrl, threadId);
|
|
} else {
|
|
PushNotifications.removeChannelNotifications(serverUrl, post.channelId);
|
|
}
|
|
}
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
export const markThreadAsUnread = async (serverUrl: string, teamId: string, threadId: string, postId: string) => {
|
|
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 {
|
|
// DM/GM doesn't have a teamId, so we pass the current team id
|
|
let threadTeamId = teamId;
|
|
if (!threadTeamId) {
|
|
threadTeamId = await getCurrentTeamId(database);
|
|
}
|
|
|
|
const data = await client.markThreadAsUnread('me', threadTeamId, threadId, postId);
|
|
|
|
// Update locally
|
|
const post = await getPostById(database, postId);
|
|
if (post) {
|
|
await updateThread(serverUrl, threadId, {
|
|
last_viewed_at: post.createAt - 1,
|
|
viewed_at: post.createAt - 1,
|
|
});
|
|
}
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
export const updateThreadFollowing = async (serverUrl: string, teamId: string, threadId: string, state: boolean) => {
|
|
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};
|
|
}
|
|
|
|
// DM/GM doesn't have a teamId, so we pass the current team id
|
|
let threadTeamId = teamId;
|
|
if (!threadTeamId) {
|
|
threadTeamId = await getCurrentTeamId(database);
|
|
}
|
|
|
|
try {
|
|
const data = await client.updateThreadFollow('me', threadTeamId, threadId, state);
|
|
|
|
// Update locally
|
|
await updateThread(serverUrl, threadId, {is_following: state});
|
|
|
|
return {data};
|
|
} catch (error) {
|
|
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
enum Direction {
|
|
Up,
|
|
Down,
|
|
}
|
|
|
|
async function fetchBatchThreads(
|
|
serverUrl: string,
|
|
teamId: string,
|
|
options: FetchThreadsOptions,
|
|
pages?: number,
|
|
): Promise<{error: unknown; data?: Thread[]}> {
|
|
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
|
|
|
if (!operator) {
|
|
return {error: `${serverUrl} database not found`};
|
|
}
|
|
|
|
let client: Client;
|
|
try {
|
|
client = NetworkManager.getClient(serverUrl);
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
|
|
// if we start from the begging of time (since = 0) we need to fetch threads from newest to oldest (Direction.Down)
|
|
// 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);
|
|
if (!currentUser) {
|
|
return {error: 'currentUser not found'};
|
|
}
|
|
|
|
const {config} = await getCommonSystemValues(operator.database);
|
|
const data: Thread[] = [];
|
|
|
|
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
|
|
let page = 0;
|
|
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since} = opts;
|
|
|
|
page += 1;
|
|
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, config.Version);
|
|
if (threads.length) {
|
|
// Mark all fetched threads as following
|
|
for (const thread of threads) {
|
|
thread.is_following = true;
|
|
}
|
|
|
|
data.push(...threads);
|
|
|
|
if (threads.length === perPage) {
|
|
const newOptions: FetchThreadsOptions = {perPage, deleted, unread};
|
|
if (direction === Direction.Down) {
|
|
const last = threads[threads.length - 1];
|
|
newOptions.before = last.id;
|
|
} else {
|
|
const first = threads[0];
|
|
newOptions.after = first.id;
|
|
}
|
|
if (pages != null && page < pages) {
|
|
fetchThreadsFunc(newOptions);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
await fetchThreadsFunc(options);
|
|
} catch (error) {
|
|
if (__DEV__) {
|
|
throw error;
|
|
}
|
|
|
|
return {error, data};
|
|
}
|
|
|
|
return {error: false, data};
|
|
}
|
|
|
|
export async function fetchNewThreads(
|
|
serverUrl: string,
|
|
teamId: string,
|
|
prepareRecordsOnly = false,
|
|
): Promise<{error: unknown; models?: Model[]}> {
|
|
const options: FetchThreadsOptions = {
|
|
unread: false,
|
|
deleted: true,
|
|
perPage: 60,
|
|
};
|
|
|
|
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
|
|
|
if (!operator) {
|
|
return {error: `${serverUrl} database not found`};
|
|
}
|
|
|
|
const newestThread = await getNewestThreadInTeam(operator.database, teamId, false);
|
|
options.since = newestThread ? newestThread.lastReplyAt : 0;
|
|
|
|
let response: {
|
|
error: unknown;
|
|
data?: Thread[];
|
|
} = {
|
|
error: undefined,
|
|
data: [],
|
|
};
|
|
|
|
let loadedInGlobalThreads = true;
|
|
|
|
// if we have no threads in the DB fetch all unread ones
|
|
if (options.since === 0) {
|
|
// options to fetch all unread threads
|
|
options.deleted = false;
|
|
options.unread = true;
|
|
loadedInGlobalThreads = false;
|
|
}
|
|
|
|
response = await fetchBatchThreads(serverUrl, teamId, options);
|
|
|
|
const {error: nErr, data} = response;
|
|
|
|
if (nErr) {
|
|
return {error: nErr};
|
|
}
|
|
|
|
if (!data?.length) {
|
|
return {error: false, models: []};
|
|
}
|
|
|
|
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};
|
|
}
|
|
|
|
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;
|
|
|
|
if (!operator) {
|
|
return {error: `${serverUrl} database not found`};
|
|
}
|
|
|
|
const newestThread = await getNewestThreadInTeam(operator.database, teamId, unread);
|
|
options.since = newestThread ? newestThread.lastReplyAt : 0;
|
|
|
|
let response: {
|
|
error: unknown;
|
|
data?: Thread[];
|
|
} = {
|
|
error: undefined,
|
|
data: [],
|
|
};
|
|
|
|
let pages;
|
|
|
|
// in the case of global threads: if we have no threads in the DB fetch just one page
|
|
if (options.since === 0 && !unread) {
|
|
pages = 1;
|
|
}
|
|
|
|
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};
|
|
}
|