From ba976dadc2eb42a5dc72150fb9ae8f0b56d720d8 Mon Sep 17 00:00:00 2001 From: Anurag Shivarathri Date: Tue, 10 May 2022 15:59:06 +0530 Subject: [PATCH] [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 --- app/actions/remote/thread.ts | 29 +++++++++++-- app/client/rest/threads.ts | 18 ++++---- app/database/models/server/post.ts | 7 +++- app/database/models/server/thread.ts | 6 --- .../server_data_operator/handlers/thread.ts | 42 +++++++++++++++++-- app/queries/servers/post.ts | 18 ++++++++ .../thread_post_list/thread_post_list.tsx | 4 +- .../options/mark_as_unread_option.tsx | 4 +- types/api/threads.d.ts | 1 + types/database/models/servers/thread.d.ts | 3 ++ 10 files changed, 104 insertions(+), 28 deletions(-) diff --git a/app/actions/remote/thread.ts b/app/actions/remote/thread.ts index 279c5deb6..7c7037004 100644 --- a/app/actions/remote/thread.ts +++ b/app/actions/remote/thread.ts @@ -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); diff --git a/app/client/rest/threads.ts b/app/client/rest/threads.ts index 6859912a9..2df61f53c 100644 --- a/app/client/rest/threads.ts +++ b/app/client/rest/threads.ts @@ -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; getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise; + markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise; markThreadAsUnread: (userId: string, teamId: string, threadId: string, postId: string) => Promise; updateTeamThreadsAsRead: (userId: string, teamId: string) => Promise; - updateThreadRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise; updateThreadFollow: (userId: string, teamId: string, threadId: string, state: boolean) => Promise; } @@ -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( diff --git a/app/database/models/server/post.ts b/app/database/models/server/post.ts index a62c2c611..bec2ec069 100644 --- a/app/database/models/server/post.ts +++ b/app/database/models/server/post.ts @@ -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 } diff --git a/app/database/models/server/thread.ts b/app/database/models/server/thread.ts index ad3dd26dd..afae4f7a0 100644 --- a/app/database/models/server/thread.ts +++ b/app/database/models/server/thread.ts @@ -63,10 +63,4 @@ export default class ThreadModel extends Model implements ThreadModelInterface { /** post : The root post of this thread */ @immutableRelation(POST, 'id') post!: Relation; - - async destroyPermanently() { - await this.participants.destroyAllPermanently(); - await this.threadsInTeam.destroyAllPermanently(); - super.destroyPermanently(); - } } diff --git a/app/database/operator/server_data_operator/handlers/thread.ts b/app/database/operator/server_data_operator/handlers/thread.ts index 9e5a02cc1..22bf1c6d6 100644 --- a/app/database/operator/server_data_operator/handlers/thread.ts +++ b/app/database/operator/server_data_operator/handlers/thread.ts @@ -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; @@ -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(THREAD).query(Q.where('id', Q.oneOf(deletedThreadIds))).fetch(); + if (threadsToDelete.length) { + await database.write(async () => { + const promises: Array> = []; + 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[]; diff --git a/app/queries/servers/post.ts b/app/queries/servers/post.ts index 1b7b212c7..e5eae7ee0 100644 --- a/app/queries/servers/post.ts +++ b/app/queries/servers/post.ts @@ -36,6 +36,24 @@ export const prepareDeletePost = async (post: PostModel): Promise => { 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; }; diff --git a/app/screens/thread/thread_post_list/thread_post_list.tsx b/app/screens/thread/thread_post_list/thread_post_list.tsx index 986bb1a23..ded727603 100644 --- a/app/screens/thread/thread_post_list/thread_post_list.tsx +++ b/app/screens/thread/thread_post_list/thread_post_list.tsx @@ -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]); diff --git a/app/screens/thread_options/options/mark_as_unread_option.tsx b/app/screens/thread_options/options/mark_as_unread_option.tsx index 13afa39d3..2d9ab8488 100644 --- a/app/screens/thread_options/options/mark_as_unread_option.tsx +++ b/app/screens/thread_options/options/mark_as_unread_option.tsx @@ -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); } diff --git a/types/api/threads.d.ts b/types/api/threads.d.ts index d6087817d..a5ff20742 100644 --- a/types/api/threads.d.ts +++ b/types/api/threads.d.ts @@ -11,6 +11,7 @@ type Thread = { is_following?: boolean; unread_replies: number; unread_mentions: number; + delete_at: number; }; type ThreadParticipant = { diff --git a/types/database/models/servers/thread.d.ts b/types/database/models/servers/thread.d.ts index 95727690a..c5b8f4a24 100644 --- a/types/database/models/servers/thread.d.ts +++ b/types/database/models/servers/thread.d.ts @@ -41,6 +41,9 @@ export default class ThreadModel extends Model { /** participants: All the participants of the thread */ participants: Query; + /** threadsInTeam : All the threadsInTeam associated with this Thread */ + threadsInTeam: Query; + /** post : Query returning the post data for the current thread */ post: Relation; }