From 48b5e9a919f7918c0d5dc9025ffe180dfd582416 Mon Sep 17 00:00:00 2001
From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com>
Date: Thu, 4 Jun 2026 12:24:05 +0530
Subject: [PATCH] Deleted removed post from content flagging redux store
(#36803) (#36882)
Automatic Merge
---
.../lib/src/ui/pages/content_review_dm.ts | 12 +
.../reviewer-actions/reviewer-actions.spec.ts | 9 +
tools/sharedchannel-test/go.mod | 22 +-
tools/sharedchannel-test/go.sum | 10 +
...lagged_message_confirmation_modal.test.tsx | 97 +++++++
...ove_flagged_message_confirmation_modal.tsx | 8 +-
.../src/action_types/content_flagging.ts | 1 +
.../src/actions/content_flagging.ts | 13 +-
.../entities/content_flagging.test.ts | 274 ++++++++++++++++++
.../src/reducers/entities/content_flagging.ts | 34 ++-
10 files changed, 465 insertions(+), 15 deletions(-)
create mode 100644 webapp/channels/src/packages/mattermost-redux/src/reducers/entities/content_flagging.test.ts
diff --git a/e2e-tests/playwright/lib/src/ui/pages/content_review_dm.ts b/e2e-tests/playwright/lib/src/ui/pages/content_review_dm.ts
index 4f090f1760e..3a70a26c323 100644
--- a/e2e-tests/playwright/lib/src/ui/pages/content_review_dm.ts
+++ b/e2e-tests/playwright/lib/src/ui/pages/content_review_dm.ts
@@ -166,6 +166,18 @@ export default class ContentReviewPage {
await expect(this.reportCard!.locator('.row:has-text("Message") .post-message__text')).toHaveText(expected);
}
+ async verifyFlaggedPostMessageInRHS(expected: string) {
+ await expect(this.rhsCard.locator('.row:has-text("Message") .post-message__text')).toHaveText(expected);
+ }
+
+ async verifyFlaggedPostMessageInCenter(postID: string, expected: string) {
+ const centerCard = this.page
+ .getByTestId('channel_view')
+ .locator('div.DataSpillageReport')
+ .filter({has: this.page.locator(`#postMessageText_${postID}`)});
+ await expect(centerCard.locator('.row:has-text("Message") .post-message__text')).toHaveText(expected);
+ }
+
async clickKeepMessage() {
await this.keepMessageButton.scrollIntoViewIfNeeded();
await this.keepMessageButton.click();
diff --git a/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-actions/reviewer-actions.spec.ts b/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-actions/reviewer-actions.spec.ts
index 232412e4ebd..1764d62d9cb 100644
--- a/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-actions/reviewer-actions.spec.ts
+++ b/e2e-tests/playwright/specs/functional/channels/content_flagging/reviewer-actions/reviewer-actions.spec.ts
@@ -78,6 +78,15 @@ test('Verify Removed Flagged posts show appropriate status and do not show the p
await secondContentReviewPage.confirmRemovePermanently();
await setupContentFlagging(adminClient, [adminUser.id, secondUserID, thirdUserID]);
+ // After the remove action succeeds, the reviewer's own view of the flagged
+ // post should be replaced with the moderation placeholder rather than the
+ // original message that is now cleared from the redux store (MM-69043).
+ // The placeholder appears both in the RHS detail view and in the report
+ // card shown in the @content-review center channel — assert each scope
+ // separately so a regression in either location is caught.
+ await secondContentReviewPage.verifyFlaggedPostMessageInRHS(contentModerationMessage);
+ await secondContentReviewPage.verifyFlaggedPostMessageInCenter(post.id, contentModerationMessage);
+
const {channelsPage: channelsPageThird, contentReviewPage: contentReviewPageThird} =
await pw.testBrowser.login(thirdUser);
await verifyAuthorNotification(
diff --git a/tools/sharedchannel-test/go.mod b/tools/sharedchannel-test/go.mod
index d6a818b6cf9..063d54115ef 100644
--- a/tools/sharedchannel-test/go.mod
+++ b/tools/sharedchannel-test/go.mod
@@ -1,6 +1,6 @@
module github.com/mattermost/mattermost/tools/sharedchannel-test
-go 1.26.2
+go 1.26.3
require github.com/mattermost/mattermost/server/public v0.1.12
@@ -18,14 +18,14 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
- github.com/hashicorp/go-plugin v1.7.0 // indirect
+ github.com/hashicorp/go-plugin v1.8.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
github.com/mattermost/logr/v2 v2.0.22 // indirect
github.com/mattermost/mattermost/server/v8 v8.0.0-20251014075701-833e0125320d // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-isatty v0.0.22 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
@@ -33,18 +33,18 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
- github.com/tinylib/msgp v1.6.3 // indirect
+ github.com/tinylib/msgp v1.6.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
- golang.org/x/crypto v0.49.0 // indirect
- golang.org/x/mod v0.34.0 // indirect
- golang.org/x/net v0.52.0 // indirect
- golang.org/x/sys v0.42.0 // indirect
- golang.org/x/text v0.35.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
- google.golang.org/grpc v1.79.3 // indirect
+ golang.org/x/crypto v0.51.0 // indirect
+ golang.org/x/mod v0.36.0 // indirect
+ golang.org/x/net v0.54.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
+ golang.org/x/text v0.37.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
+ google.golang.org/grpc v1.81.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/tools/sharedchannel-test/go.sum b/tools/sharedchannel-test/go.sum
index d5f75ecc6ed..5758a316eec 100644
--- a/tools/sharedchannel-test/go.sum
+++ b/tools/sharedchannel-test/go.sum
@@ -81,6 +81,7 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
+github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
@@ -111,6 +112,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -173,6 +175,7 @@ github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cb
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
+github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
@@ -204,6 +207,7 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
+golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -211,6 +215,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -223,6 +228,7 @@ golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -248,6 +254,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -255,6 +262,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -279,6 +287,7 @@ google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRn
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
@@ -286,6 +295,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
diff --git a/webapp/channels/src/components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal.test.tsx b/webapp/channels/src/components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal.test.tsx
index 485c3a2407a..4f59b5a481e 100644
--- a/webapp/channels/src/components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal.test.tsx
+++ b/webapp/channels/src/components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal.test.tsx
@@ -4,7 +4,9 @@
import {screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
+import type {MockStoreEnhanced} from 'redux-mock-store';
+import {ContentFlaggingTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {renderWithContext} from 'tests/react_testing_utils';
@@ -517,6 +519,101 @@ describe('KeepRemoveFlaggedMessageConfirmationModal', () => {
});
});
+ describe('store cleanup after remove', () => {
+ test('dispatches FLAGGED_POST_REMOVED after a successful removeFlaggedPost call', async () => {
+ const {store} = renderWithContext(
+ ,
+ {},
+ {useMockedStore: true},
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('generated-section')).toBeVisible();
+ });
+
+ await userEvent.click(screen.getByRole('button', {name: 'Remove permanently'}));
+
+ await waitFor(() => {
+ expect(Client4.removeFlaggedPost).toHaveBeenCalledWith(flaggedPost.id, '');
+ });
+
+ await waitFor(() => {
+ expect((store as unknown as MockStoreEnhanced).getActions()).toContainEqual({
+ type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
+ data: {postId: flaggedPost.id},
+ });
+ });
+ });
+
+ test('does not dispatch FLAGGED_POST_REMOVED when removeFlaggedPost fails', async () => {
+ Client4.removeFlaggedPost = jest.fn().mockRejectedValue({message: 'boom'});
+
+ const {store} = renderWithContext(
+ ,
+ {},
+ {useMockedStore: true},
+ );
+
+ await userEvent.click(screen.getByTestId('download-report-checkbox'));
+ await userEvent.click(screen.getByRole('button', {name: 'Remove message'}));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('skip-confirm-body')).toBeVisible();
+ });
+
+ await userEvent.click(screen.getByRole('button', {name: 'Remove without report'}));
+
+ await waitFor(() => {
+ expect(Client4.removeFlaggedPost).toHaveBeenCalledWith(flaggedPost.id, '');
+ });
+
+ expect((store as unknown as MockStoreEnhanced).getActions()).not.toContainEqual(
+ expect.objectContaining({type: ContentFlaggingTypes.FLAGGED_POST_REMOVED}),
+ );
+ });
+
+ test('does not dispatch FLAGGED_POST_REMOVED for keep action', async () => {
+ const {store} = renderWithContext(
+ ,
+ {},
+ {useMockedStore: true},
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('generated-section')).toBeVisible();
+ });
+
+ await userEvent.click(screen.getByRole('button', {name: 'Keep permanently'}));
+
+ await waitFor(() => {
+ expect(Client4.keepFlaggedPost).toHaveBeenCalledWith(flaggedPost.id, '');
+ });
+
+ expect((store as unknown as MockStoreEnhanced).getActions()).not.toContainEqual(
+ expect.objectContaining({type: ContentFlaggingTypes.FLAGGED_POST_REMOVED}),
+ );
+ });
+ });
+
describe('error handling', () => {
test('should show request error when API call fails', async () => {
const errorMessage = 'Failed to remove flagged post';
diff --git a/webapp/channels/src/components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal.tsx b/webapp/channels/src/components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal.tsx
index e746f2fb1aa..895d2ddce0b 100644
--- a/webapp/channels/src/components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal.tsx
+++ b/webapp/channels/src/components/remove_flagged_message_confirmation_modal/remove_flagged_message_confirmation_modal.tsx
@@ -3,12 +3,14 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
+import {useDispatch} from 'react-redux';
import {GenericModal} from '@mattermost/components';
import type {ServerError} from '@mattermost/types/errors';
import type {Post} from '@mattermost/types/posts';
import type {UserProfile} from '@mattermost/types/users';
+import {removeContentFlaggingPost} from 'mattermost-redux/actions/content_flagging';
import {Client4} from 'mattermost-redux/client';
import {useChannel} from 'components/common/hooks/useChannel';
@@ -39,6 +41,7 @@ type Step = 'form' | 'skip_confirm' | 'generating' | 'generated' | 'error';
export default function KeepRemoveFlaggedMessageConfirmationModal({action, onExited, flaggedPost, reportingUser}: Props) {
const {formatMessage} = useIntl();
+ const dispatch = useDispatch();
const flaggedPostChannel = useChannel(flaggedPost.channel_id);
const contentFlaggingConfig = useContentFlaggingConfig(flaggedPostChannel?.team_id || '');
@@ -91,6 +94,9 @@ export default function KeepRemoveFlaggedMessageConfirmationModal({action, onExi
setSubmitting(true);
setRequestError('');
await actionFunc(flaggedPost.id, comment);
+ if (action === 'remove') {
+ dispatch(removeContentFlaggingPost(flaggedPost.id));
+ }
handleClose();
} catch (error) {
// eslint-disable-next-line no-console
@@ -99,7 +105,7 @@ export default function KeepRemoveFlaggedMessageConfirmationModal({action, onExi
} finally {
setSubmitting(false);
}
- }, [action, comment, flaggedPost.id, handleClose]);
+ }, [action, comment, dispatch, flaggedPost.id, handleClose]);
const handleFormPrimary = useCallback(() => {
if (validateForm()) {
diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/content_flagging.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/content_flagging.ts
index 405fa9ccfb3..d2139d32cc9 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/action_types/content_flagging.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/content_flagging.ts
@@ -11,4 +11,5 @@ export default keyMirror({
RECEIVED_FLAGGED_POST: null,
RECEIVED_CONTENT_FLAGGING_CHANNEL: null,
RECEIVED_CONTENT_FLAGGING_TEAM: null,
+ FLAGGED_POST_REMOVED: null,
});
diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/content_flagging.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/content_flagging.ts
index 04ba86d3407..4e4728ef1a8 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/actions/content_flagging.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/actions/content_flagging.ts
@@ -11,7 +11,7 @@ import {TeamTypes, ContentFlaggingTypes} from 'mattermost-redux/action_types';
import {logError} from 'mattermost-redux/actions/errors';
import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers';
import {Client4} from 'mattermost-redux/client';
-import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
+import type {ActionFuncAsync, ActionFunc} from 'mattermost-redux/types/actions';
import {DelayedDataLoader} from 'mattermost-redux/utils/data_loader';
export type ContentFlaggingChannelRequestIdentifier = {
@@ -264,3 +264,14 @@ export function getPostContentFlaggingValues(postId: string): ActionFuncAsync {
+ return (dispatch) => {
+ dispatch({
+ type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
+ data: {postId},
+ });
+
+ return {};
+ };
+}
diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/content_flagging.test.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/content_flagging.test.ts
new file mode 100644
index 00000000000..bef85decc10
--- /dev/null
+++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/content_flagging.test.ts
@@ -0,0 +1,274 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import type {PropertyValue} from '@mattermost/types/properties';
+
+import {ContentFlaggingTypes} from 'mattermost-redux/action_types';
+
+import contentFlaggingReducer from './content_flagging';
+
+function makeValue(overrides: Partial> = {}): PropertyValue {
+ return {
+ id: 'value-1',
+ target_id: 'post-1',
+ target_type: 'post',
+ group_id: 'content_flagging',
+ field_id: 'field-1',
+ value: 'test',
+ create_at: 1000,
+ update_at: 1000,
+ delete_at: 0,
+ created_by: 'user-1',
+ updated_by: 'user-1',
+ ...overrides,
+ };
+}
+
+function valuesByFieldId(values: Array>) {
+ return Object.fromEntries(values.map((value) => [value.field_id, value]));
+}
+
+describe('Reducers.ContentFlagging', () => {
+ test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED stores values for posts without cached values', () => {
+ const value = makeValue({field_id: 'review_status', value: 'removed'});
+
+ const state = contentFlaggingReducer(undefined, {
+ type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
+ data: {
+ target_id: 'post-1',
+ property_values: JSON.stringify([value]),
+ },
+ });
+
+ expect(state.postValues['post-1']).toEqual([value]);
+ });
+
+ test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED merges updated values with cached values', () => {
+ const existingValue = makeValue({field_id: 'review_status', value: 'pending'});
+ const retainedValue = makeValue({id: 'value-2', field_id: 'reason', value: 'data_spillage'});
+ const updatedValue = makeValue({field_id: 'review_status', value: 'removed'});
+
+ const stateWithValues = contentFlaggingReducer(undefined, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-1',
+ values: [existingValue, retainedValue],
+ },
+ });
+
+ const state = contentFlaggingReducer(stateWithValues, {
+ type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
+ data: {
+ target_id: 'post-1',
+ property_values: JSON.stringify([updatedValue]),
+ },
+ });
+
+ expect(state.postValues['post-1']).toHaveLength(2);
+ expect(valuesByFieldId(state.postValues['post-1'])).toEqual({
+ review_status: updatedValue,
+ reason: retainedValue,
+ });
+ });
+
+ test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED merges multiple values and preserves other posts', () => {
+ const existingValue = makeValue({field_id: 'review_status', value: 'pending'});
+ const retainedValue = makeValue({id: 'value-2', field_id: 'reason', value: 'data_spillage'});
+ const otherPostValue = makeValue({id: 'value-3', target_id: 'post-2', field_id: 'review_status', value: 'pending'});
+ const updatedValue = makeValue({field_id: 'review_status', value: 'removed'});
+ const newValue = makeValue({id: 'value-4', field_id: 'removal_type', value: 'permanent'});
+
+ const stateWithPostOne = contentFlaggingReducer(undefined, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-1',
+ values: [existingValue, retainedValue],
+ },
+ });
+ const stateWithValues = contentFlaggingReducer(stateWithPostOne, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-2',
+ values: [otherPostValue],
+ },
+ });
+
+ const state = contentFlaggingReducer(stateWithValues, {
+ type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
+ data: {
+ target_id: 'post-1',
+ property_values: JSON.stringify([updatedValue, newValue]),
+ },
+ });
+
+ expect(state.postValues['post-1']).toHaveLength(3);
+ expect(valuesByFieldId(state.postValues['post-1'])).toEqual({
+ review_status: updatedValue,
+ reason: retainedValue,
+ removal_type: newValue,
+ });
+ expect(state.postValues['post-2']).toEqual([otherPostValue]);
+ });
+
+ test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED ignores malformed property values', () => {
+ const stateWithValues = contentFlaggingReducer(undefined, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-1',
+ values: [makeValue({field_id: 'review_status', value: 'pending'})],
+ },
+ });
+
+ expect(() => contentFlaggingReducer(stateWithValues, {
+ type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
+ data: {
+ target_id: 'post-1',
+ property_values: '{',
+ },
+ })).not.toThrow();
+
+ const state = contentFlaggingReducer(stateWithValues, {
+ type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
+ data: {
+ target_id: 'post-1',
+ property_values: '{',
+ },
+ });
+
+ expect(state).toBe(stateWithValues);
+ });
+
+ test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED ignores valid non-array property values', () => {
+ const stateWithValues = contentFlaggingReducer(undefined, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-1',
+ values: [makeValue({field_id: 'review_status', value: 'pending'})],
+ },
+ });
+
+ const state = contentFlaggingReducer(stateWithValues, {
+ type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
+ data: {
+ target_id: 'post-1',
+ property_values: '{}',
+ },
+ });
+
+ expect(state).toBe(stateWithValues);
+ });
+
+ test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED tolerates non-array cached values', () => {
+ const updatedValue = makeValue({field_id: 'review_status', value: 'removed'});
+ const invalidState = {
+ settings: {},
+ fields: {},
+ postValues: {
+ 'post-1': {
+ invalid: true,
+ },
+ },
+ flaggedPosts: {},
+ channels: {},
+ teams: {},
+ };
+
+ const state = contentFlaggingReducer(invalidState as unknown as Parameters[0], {
+ type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
+ data: {
+ target_id: 'post-1',
+ property_values: JSON.stringify([updatedValue]),
+ },
+ });
+
+ expect(state.postValues['post-1']).toEqual([updatedValue]);
+ });
+
+ describe('FLAGGED_POST_REMOVED', () => {
+ test('clears the flagged post and its property values for the given post id', () => {
+ const value = makeValue({field_id: 'review_status', value: 'pending'});
+ const otherValue = makeValue({id: 'value-2', target_id: 'post-2', field_id: 'review_status', value: 'pending'});
+
+ const flaggedPost = {id: 'post-1'} as any;
+ const otherFlaggedPost = {id: 'post-2'} as any;
+
+ const stateWithValues = contentFlaggingReducer(undefined, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-1',
+ values: [value],
+ },
+ });
+ const stateWithOtherValues = contentFlaggingReducer(stateWithValues, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-2',
+ values: [otherValue],
+ },
+ });
+ const stateWithFlaggedPost = contentFlaggingReducer(stateWithOtherValues, {
+ type: ContentFlaggingTypes.RECEIVED_FLAGGED_POST,
+ data: flaggedPost,
+ });
+ const stateWithBothFlaggedPosts = contentFlaggingReducer(stateWithFlaggedPost, {
+ type: ContentFlaggingTypes.RECEIVED_FLAGGED_POST,
+ data: otherFlaggedPost,
+ });
+
+ const state = contentFlaggingReducer(stateWithBothFlaggedPosts, {
+ type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
+ data: {postId: 'post-1'},
+ });
+
+ expect(state.postValues['post-1']).toBeUndefined();
+ expect(state.flaggedPosts['post-1']).toBeUndefined();
+ expect(state.postValues['post-2']).toEqual([otherValue]);
+ expect(state.flaggedPosts['post-2']).toEqual(otherFlaggedPost);
+ });
+
+ test('returns the same state reference when the post id is not present', () => {
+ const value = makeValue({field_id: 'review_status', value: 'pending'});
+
+ const stateWithValues = contentFlaggingReducer(undefined, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-1',
+ values: [value],
+ },
+ });
+
+ const state = contentFlaggingReducer(stateWithValues, {
+ type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
+ data: {postId: 'unknown-post'},
+ });
+
+ expect(state.postValues).toBe(stateWithValues.postValues);
+ expect(state.flaggedPosts).toBe(stateWithValues.flaggedPosts);
+ });
+
+ test('is a no-op when postId is missing from the action data', () => {
+ const value = makeValue({field_id: 'review_status', value: 'pending'});
+ const flaggedPost = {id: 'post-1'} as any;
+
+ const stateWithValues = contentFlaggingReducer(undefined, {
+ type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
+ data: {
+ postId: 'post-1',
+ values: [value],
+ },
+ });
+ const stateWithFlaggedPost = contentFlaggingReducer(stateWithValues, {
+ type: ContentFlaggingTypes.RECEIVED_FLAGGED_POST,
+ data: flaggedPost,
+ });
+
+ const state = contentFlaggingReducer(stateWithFlaggedPost, {
+ type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
+ data: {},
+ });
+
+ expect(state.postValues['post-1']).toEqual([value]);
+ expect(state.flaggedPosts['post-1']).toEqual(flaggedPost);
+ });
+ });
+});
diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/content_flagging.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/content_flagging.ts
index 92627b6935b..d747967d1e1 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/content_flagging.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/content_flagging.ts
@@ -15,6 +15,15 @@ import type {
import type {MMReduxAction} from 'mattermost-redux/action_types';
import {ContentFlaggingTypes, UserTypes} from 'mattermost-redux/action_types';
+function parsePropertyValues(propertyValues: string): Array> | null {
+ try {
+ const parsedPropertyValues = JSON.parse(propertyValues);
+ return Array.isArray(parsedPropertyValues) ? parsedPropertyValues : null;
+ } catch {
+ return null;
+ }
+}
+
function settings(state: ContentFlaggingState['settings'] = {} as ContentFlaggingConfig, action: MMReduxAction) {
switch (action.type) {
case ContentFlaggingTypes.RECEIVED_CONTENT_FLAGGING_CONFIG: {
@@ -55,8 +64,11 @@ function postValues(state: ContentFlaggingState['postValues'] = {}, action: MMRe
}
case ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED: {
const postId = action.data.target_id as string;
- const existingPropertyValues = state[postId] || {};
- const updatedPropertyValues = JSON.parse(action.data.property_values);
+ const existingPropertyValues = Array.isArray(state[postId]) ? state[postId] : [];
+ const updatedPropertyValues = parsePropertyValues(action.data.property_values);
+ if (!updatedPropertyValues) {
+ return state;
+ }
const valuesByFieldId = {} as Record>;
existingPropertyValues.forEach((property: PropertyValue) => {
@@ -71,6 +83,15 @@ function postValues(state: ContentFlaggingState['postValues'] = {}, action: MMRe
[postId]: Object.values(valuesByFieldId),
};
}
+ case ContentFlaggingTypes.FLAGGED_POST_REMOVED: {
+ const postId = action.data?.postId as string | undefined;
+ if (!postId || !(postId in state)) {
+ return state;
+ }
+ const nextState = {...state};
+ Reflect.deleteProperty(nextState, postId);
+ return nextState;
+ }
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
@@ -86,6 +107,15 @@ function flaggedPosts(state: ContentFlaggingState['flaggedPosts'] = {}, action:
[action.data.id]: action.data,
};
}
+ case ContentFlaggingTypes.FLAGGED_POST_REMOVED: {
+ const postId = action.data?.postId as string | undefined;
+ if (!postId || !(postId in state)) {
+ return state;
+ }
+ const nextState = {...state};
+ Reflect.deleteProperty(nextState, postId);
+ return nextState;
+ }
case UserTypes.LOGOUT_SUCCESS:
return {};
default: