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: