[Gekidou[ [MM-43969, MM-44037] global threads crash, mark as read (#6253)
* Preparing thread data as well with prepareDeletePost * observing the post directly to prevent the crash until the data issue is found * mark as read, delete threeds fix * Update thread.ts Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
parent
591cfa1ae4
commit
ba976dadc2
10 changed files with 104 additions and 28 deletions
|
|
@ -52,7 +52,7 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string)
|
|||
if (thread?.unreadReplies || thread?.unreadMentions) {
|
||||
const channel = await getChannelById(database, post.channelId);
|
||||
if (channel) {
|
||||
updateThreadRead(serverUrl, channel.teamId, thread.id, Date.now());
|
||||
markThreadAsRead(serverUrl, channel.teamId, thread.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +156,13 @@ export const updateTeamThreadsAsRead = async (serverUrl: string, teamId: string)
|
|||
}
|
||||
};
|
||||
|
||||
export const updateThreadRead = async (serverUrl: string, teamId: string, threadId: string, timestamp: number) => {
|
||||
export const markThreadAsRead = async (serverUrl: string, teamId: string, threadId: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -165,11 +171,20 @@ export const updateThreadRead = async (serverUrl: string, teamId: string, thread
|
|||
}
|
||||
|
||||
try {
|
||||
const data = await client.updateThreadRead('me', teamId, threadId, timestamp);
|
||||
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,
|
||||
});
|
||||
|
||||
return {data};
|
||||
|
|
@ -194,7 +209,13 @@ export const markThreadAsUnread = async (serverUrl: string, teamId: string, thre
|
|||
}
|
||||
|
||||
try {
|
||||
const data = await client.markThreadAsUnread('me', teamId, threadId, postId);
|
||||
// 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, threadId);
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import {PER_PAGE_DEFAULT} from './constants';
|
|||
export interface ClientThreadsMix {
|
||||
getThreads: (userId: string, teamId: string, before?: string, after?: string, pageSize?: number, deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string) => Promise<GetUserThreadsResponse>;
|
||||
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
|
||||
markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>;
|
||||
markThreadAsUnread: (userId: string, teamId: string, threadId: string, postId: string) => Promise<any>;
|
||||
updateTeamThreadsAsRead: (userId: string, teamId: string) => Promise<any>;
|
||||
updateThreadRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>;
|
||||
updateThreadFollow: (userId: string, teamId: string, threadId: string, state: boolean) => Promise<any>;
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +43,14 @@ const ClientThreads = (superclass: any) => class extends superclass {
|
|||
);
|
||||
};
|
||||
|
||||
markThreadAsRead = (userId: string, teamId: string, threadId: string, timestamp: number) => {
|
||||
const url = `${this.getThreadRoute(userId, teamId, threadId)}/read/${timestamp}`;
|
||||
return this.doFetch(
|
||||
url,
|
||||
{method: 'put'},
|
||||
);
|
||||
};
|
||||
|
||||
markThreadAsUnread = (userId: string, teamId: string, threadId: string, postId: string) => {
|
||||
const url = `${this.getThreadRoute(userId, teamId, threadId)}/set_unread/${postId}`;
|
||||
return this.doFetch(
|
||||
|
|
@ -59,14 +67,6 @@ const ClientThreads = (superclass: any) => class extends superclass {
|
|||
);
|
||||
};
|
||||
|
||||
updateThreadRead = (userId: string, teamId: string, threadId: string, timestamp: number) => {
|
||||
const url = `${this.getThreadRoute(userId, teamId, threadId)}/read/${timestamp}`;
|
||||
return this.doFetch(
|
||||
url,
|
||||
{method: 'put'},
|
||||
);
|
||||
};
|
||||
|
||||
updateThreadFollow = (userId: string, teamId: string, threadId: string, state: boolean) => {
|
||||
const url = this.getThreadRoute(userId, teamId, threadId) + '/following';
|
||||
return this.doFetch(
|
||||
|
|
|
|||
|
|
@ -131,7 +131,12 @@ export default class PostModel extends Model implements PostModelInterface {
|
|||
Q.where('root_id', this.id),
|
||||
).destroyAllPermanently();
|
||||
try {
|
||||
(await this.thread.fetch())?.destroyPermanently();
|
||||
const thread = await this.thread.fetch();
|
||||
if (thread) {
|
||||
await thread.destroyPermanently();
|
||||
await thread.participants.destroyAllPermanently();
|
||||
await thread.threadsInTeam.destroyAllPermanently();
|
||||
}
|
||||
} catch {
|
||||
// there is no thread record for this post
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,10 +63,4 @@ export default class ThreadModel extends Model implements ThreadModelInterface {
|
|||
|
||||
/** post : The root post of this thread */
|
||||
@immutableRelation(POST, 'id') post!: Relation<PostModel>;
|
||||
|
||||
async destroyPermanently() {
|
||||
await this.participants.destroyAllPermanently();
|
||||
await this.threadsInTeam.destroyAllPermanently();
|
||||
super.destroyPermanently();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {Database} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {
|
||||
transformThreadRecord,
|
||||
transformThreadParticipantRecord,
|
||||
|
|
@ -11,6 +12,7 @@ import {
|
|||
import {getUniqueRawsBy} from '@database/operator/utils/general';
|
||||
import {sanitizeThreadParticipants} from '@database/operator/utils/thread';
|
||||
|
||||
import type Database from '@nozbe/watermelondb/Database';
|
||||
import type {HandleThreadsArgs, HandleThreadParticipantsArgs} from '@typings/database/database';
|
||||
import type ThreadModel from '@typings/database/models/servers/thread';
|
||||
import type ThreadInTeamModel from '@typings/database/models/servers/thread_in_team';
|
||||
|
|
@ -19,7 +21,7 @@ import type ThreadParticipantModel from '@typings/database/models/servers/thread
|
|||
const {
|
||||
THREAD,
|
||||
THREAD_PARTICIPANT,
|
||||
} = Database.MM_TABLES.SERVER;
|
||||
} = MM_TABLES.SERVER;
|
||||
|
||||
export interface ThreadHandlerMix {
|
||||
handleThreads: ({threads, teamId, prepareRecordsOnly, loadedInGlobalThreads}: HandleThreadsArgs) => Promise<Model[]>;
|
||||
|
|
@ -49,10 +51,42 @@ const ThreadHandler = (superclass: any) => class extends superclass {
|
|||
key: 'id',
|
||||
}) as Thread[];
|
||||
|
||||
// Seperate threads to be deleted & created/updated
|
||||
const deletedThreadIds: string[] = [];
|
||||
const createOrUpdateThreads: Thread[] = [];
|
||||
uniqueThreads.forEach((thread) => {
|
||||
if (thread.delete_at > 0) {
|
||||
deletedThreadIds.push(thread.id);
|
||||
} else {
|
||||
createOrUpdateThreads.push(thread);
|
||||
}
|
||||
});
|
||||
|
||||
if (deletedThreadIds.length) {
|
||||
const database: Database = this.database;
|
||||
const threadsToDelete = await database.get<ThreadModel>(THREAD).query(Q.where('id', Q.oneOf(deletedThreadIds))).fetch();
|
||||
if (threadsToDelete.length) {
|
||||
await database.write(async () => {
|
||||
const promises: Array<Promise<void>> = [];
|
||||
threadsToDelete.forEach((thread) => {
|
||||
promises.push(thread.destroyPermanently());
|
||||
promises.push(thread.threadsInTeam.destroyAllPermanently());
|
||||
promises.push(thread.participants.destroyAllPermanently());
|
||||
});
|
||||
await Promise.all(promises);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// As there are no threads to be created or updated
|
||||
if (!createOrUpdateThreads.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const threadsParticipants: ParticipantsPerThread[] = [];
|
||||
|
||||
// Let's process the thread data
|
||||
for (const thread of uniqueThreads) {
|
||||
for (const thread of createOrUpdateThreads) {
|
||||
// Avoid participants field set as "null" from overriding the existing ones
|
||||
if (Array.isArray(thread.participants)) {
|
||||
threadsParticipants.push({
|
||||
|
|
@ -70,7 +104,7 @@ const ThreadHandler = (superclass: any) => class extends superclass {
|
|||
fieldName: 'id',
|
||||
transformer: transformThreadRecord,
|
||||
prepareRecordsOnly: true,
|
||||
createOrUpdateRawValues: uniqueThreads,
|
||||
createOrUpdateRawValues: createOrUpdateThreads,
|
||||
tableName: THREAD,
|
||||
}) as ThreadModel[];
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,24 @@ export const prepareDeletePost = async (post: PostModel): Promise<Model[]> => {
|
|||
models?.forEach((model) => preparedModels.push(model.prepareDestroyPermanently()));
|
||||
}));
|
||||
|
||||
// If thread exists, delete thread, participants and threadsInTeam
|
||||
try {
|
||||
const thread = await post.thread.fetch();
|
||||
if (thread) {
|
||||
const participants = await thread.participants.fetch();
|
||||
if (participants.length) {
|
||||
preparedModels.push(...participants.map((p) => p.prepareDestroyPermanently()));
|
||||
}
|
||||
const threadsInTeam = await thread.threadsInTeam.fetch();
|
||||
if (threadsInTeam.length) {
|
||||
preparedModels.push(...threadsInTeam.map((t) => t.prepareDestroyPermanently()));
|
||||
}
|
||||
preparedModels.push(thread.prepareDestroyPermanently());
|
||||
}
|
||||
} catch {
|
||||
// Thread not found, do nothing
|
||||
}
|
||||
|
||||
return preparedModels;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import React, {useEffect, useMemo, useRef} from 'react';
|
|||
import {StyleSheet, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {updateThreadRead} from '@actions/remote/thread';
|
||||
import {markThreadAsRead} from '@actions/remote/thread';
|
||||
import PostList from '@components/post_list';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
|
@ -46,7 +46,7 @@ const ThreadPostList = ({
|
|||
useEffect(() => {
|
||||
if (isCRTEnabled && oldPostsCount.current < posts.length) {
|
||||
oldPostsCount.current = posts.length;
|
||||
updateThreadRead(serverUrl, teamId, rootPost.id, Date.now());
|
||||
markThreadAsRead(serverUrl, teamId, rootPost.id);
|
||||
}
|
||||
}, [isCRTEnabled, posts, rootPost, serverUrl, teamId]);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
import {markThreadAsUnread, updateThreadRead} from '@actions/remote/thread';
|
||||
import {markThreadAsRead, markThreadAsUnread} from '@actions/remote/thread';
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
|
@ -22,7 +22,7 @@ const MarkAsUnreadOption = ({teamId, thread}: Props) => {
|
|||
const onHandlePress = useCallback(async () => {
|
||||
await dismissBottomSheet(Screens.THREAD_OPTIONS);
|
||||
if (thread.unreadReplies) {
|
||||
updateThreadRead(serverUrl, teamId, thread.id, Date.now());
|
||||
markThreadAsRead(serverUrl, teamId, thread.id);
|
||||
} else {
|
||||
markThreadAsUnread(serverUrl, teamId, thread.id, thread.id);
|
||||
}
|
||||
|
|
|
|||
1
types/api/threads.d.ts
vendored
1
types/api/threads.d.ts
vendored
|
|
@ -11,6 +11,7 @@ type Thread = {
|
|||
is_following?: boolean;
|
||||
unread_replies: number;
|
||||
unread_mentions: number;
|
||||
delete_at: number;
|
||||
};
|
||||
|
||||
type ThreadParticipant = {
|
||||
|
|
|
|||
3
types/database/models/servers/thread.d.ts
vendored
3
types/database/models/servers/thread.d.ts
vendored
|
|
@ -41,6 +41,9 @@ export default class ThreadModel extends Model {
|
|||
/** participants: All the participants of the thread */
|
||||
participants: Query<ThreadParticipantsModel>;
|
||||
|
||||
/** threadsInTeam : All the threadsInTeam associated with this Thread */
|
||||
threadsInTeam: Query<ThreadInTeamModel>;
|
||||
|
||||
/** post : Query returning the post data for the current thread */
|
||||
post: Relation<PostModel>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue