[GH-25484] Fix draft removal on post deletion (#25715)

* [GH-25484] Fix draft removal on post deletion

* [GH-25484] Add batch migration to remove orphan drafts

* [GH-25484] Fix tests of migration and draft store

* [GH-25484] Remove translation file changes.

* [GH-25484] Remove translation file changes.

---------

Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
This commit is contained in:
Utsav Ladani
2024-01-29 20:25:34 +05:30
committed by GitHub
parent 435da9bea7
commit 3ac6edb406
19 changed files with 1046 additions and 15 deletions

View File

@@ -37,13 +37,10 @@ export function setGlobalItem(name: string, value: any) {
};
}
export function removeGlobalItem(name: string): NewActionFunc {
return (dispatch) => {
dispatch({
type: StorageTypes.REMOVE_GLOBAL_ITEM,
data: {name},
});
return {data: true};
export function removeGlobalItem(name: string) {
return {
type: StorageTypes.REMOVE_GLOBAL_ITEM,
data: {name},
};
}

View File

@@ -5,7 +5,7 @@ import {Client4} from 'mattermost-redux/client';
import {Posts, Preferences} from 'mattermost-redux/constants';
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import {setGlobalItem} from 'actions/storage';
import {removeGlobalItem, setGlobalItem} from 'actions/storage';
import mockStore from 'tests/test_store';
import {StoragePrefixes} from 'utils/constants';
@@ -174,6 +174,8 @@ describe('draft actions', () => {
uploadsInProgress: [],
}));
testStore.dispatch(removeGlobalItem(StoragePrefixes.DRAFT + channelId));
expect(store.getActions()).toEqual(testStore.getActions());
});

View File

@@ -9,6 +9,7 @@ import type {PostMetadata, PostPriorityMetadata} from '@mattermost/types/posts';
import type {PreferenceType} from '@mattermost/types/preferences';
import type {UserProfile} from '@mattermost/types/users';
import {getPost} from 'mattermost-redux/actions/posts';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {Client4} from 'mattermost-redux/client';
import Preferences from 'mattermost-redux/constants/preferences';
@@ -16,7 +17,7 @@ import {syncedDraftsAreAllowedAndEnabled} from 'mattermost-redux/selectors/entit
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import type {NewActionFunc, NewActionFuncAsync} from 'mattermost-redux/types/actions';
import {setGlobalItem} from 'actions/storage';
import {removeGlobalItem, setGlobalItem} from 'actions/storage';
import {makeGetDrafts} from 'selectors/drafts';
import {getConnectionId} from 'selectors/general';
import {getGlobalItem} from 'selectors/storage';
@@ -44,13 +45,40 @@ export function getDrafts(teamId: string): NewActionFuncAsync<boolean, GlobalSta
let serverDrafts: Draft[] = [];
try {
serverDrafts = (await Client4.getUserDrafts(teamId)).map((draft) => transformServerDraft(draft));
const response = await Client4.getUserDrafts(teamId);
// check if response is an array
if (Array.isArray(response)) {
serverDrafts = response.map((draft) => transformServerDraft(draft));
}
} catch (error) {
return {data: false, error};
}
const drafts = [...serverDrafts];
const localDrafts = getLocalDrafts(state);
const drafts = [...serverDrafts, ...localDrafts];
// drafts that are not on server, but on local storage
const localOnlyDrafts = localDrafts.filter((localDraft) => {
return !serverDrafts.find((serverDraft) => serverDraft.key === localDraft.key);
});
// check if drafts are still valid
await Promise.all(localOnlyDrafts.map(async (draft) => {
if (draft.value.rootId) {
// get post from server to check if it exists
const {error} = await dispatch(getPost(draft.value.rootId));
// remove locally stored draft if post does not exist
if (error.status_code === 404) {
await dispatch(setGlobalItem(draft.key, {message: '', fileInfos: [], uploadsInProgress: []}));
await dispatch(removeGlobalItem(draft.key));
return;
}
}
drafts.push(draft);
}));
// Reconcile drafts and only keep the latest version of a draft.
const draftsMap = new Map(drafts.map((draft) => [draft.key, draft]));
@@ -74,7 +102,11 @@ export function removeDraft(key: string, channelId: string, rootId = ''): NewAct
return async (dispatch, getState) => {
const state = getState();
dispatch(setGlobalItem(key, {message: '', fileInfos: [], uploadsInProgress: []}));
// set draft to empty to re-render the component
await dispatch(setGlobalItem(key, {message: '', fileInfos: [], uploadsInProgress: []}));
// remove draft from storage
await dispatch(removeGlobalItem(key));
if (syncedDraftsAreAllowedAndEnabled(state)) {
const connectionId = getConnectionId(getState());

View File

@@ -100,7 +100,7 @@ import {redirectUserToDefaultTeam} from 'actions/global_actions';
import {sendDesktopNotification} from 'actions/notification_actions.jsx';
import {handleNewPost} from 'actions/post_actions';
import * as StatusActions from 'actions/status_actions';
import {setGlobalItem} from 'actions/storage';
import {removeGlobalItem, setGlobalItem} from 'actions/storage';
import {loadProfilesForDM, loadProfilesForGM} from 'actions/user_actions';
import {syncPostsInChannel} from 'actions/views/channel';
import {setGlobalDraft, transformServerDraft} from 'actions/views/drafts';
@@ -118,7 +118,7 @@ import RemovedFromChannelModal from 'components/removed_from_channel_modal';
import WebSocketClient from 'client/web_websocket_client';
import {loadPlugin, loadPluginsIfNecessary, removePlugin} from 'plugins';
import {getHistory} from 'utils/browser_history';
import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatuses, ModalIdentifiers, WarnMetricTypes, PageLoadContext} from 'utils/constants';
import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatuses, ModalIdentifiers, WarnMetricTypes, PageLoadContext, StoragePrefixes} from 'utils/constants';
import {getSiteURL} from 'utils/url';
import {temporarilySetPageLoadContext} from './telemetry_actions';
@@ -786,6 +786,19 @@ async function handlePostDeleteEvent(msg) {
dispatch(postDeleted(post));
// remove draft associated with this post from store
const draftKey = `${StoragePrefixes.COMMENT_DRAFT}${post.id}`;
// update the draft first to re-render
await dispatch(setGlobalItem(draftKey, {
message: '',
fileInfos: [],
uploadsInProgress: [],
}));
// then remove it
await dispatch(removeGlobalItem(draftKey));
// update thread when a comment is deleted and CRT is on
if (post.root_id && collapsedThreads) {
const thread = getThread(state, post.root_id);
@@ -1757,11 +1770,15 @@ function handleDeleteDraftEvent(msg) {
const draft = JSON.parse(msg.data.draft);
const {key} = transformServerDraft(draft);
doDispatch(setGlobalItem(key, {
// update the draft first to re-render
await doDispatch(setGlobalItem(key, {
message: '',
fileInfos: [],
uploadsInProgress: [],
}));
// then remove it
await doDispatch(removeGlobalItem(key));
};
}