From 3a394b25e4356a20b066b4ea0087f806c85febba Mon Sep 17 00:00:00 2001 From: Andre Vasconcelos Date: Fri, 23 Jan 2026 17:41:11 +0200 Subject: [PATCH 1/3] Bumping version of prepackaged Gitlab plugin to 1.12.0 (#35033) --- server/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Makefile b/server/Makefile index 2ed636180dd..94602b7539d 100644 --- a/server/Makefile +++ b/server/Makefile @@ -154,7 +154,7 @@ TEMPLATES_DIR=templates PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:) PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0 PLUGIN_PACKAGES += mattermost-plugin-github-v2.5.0 -PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.11.0 +PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.12.0 PLUGIN_PACKAGES += mattermost-plugin-jira-v4.5.1 PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.6.1 PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.4.0 From 09c4a61fed8580ceae5bd05e64aa4ac9615c4444 Mon Sep 17 00:00:00 2001 From: Matthew Birtch Date: Fri, 23 Jan 2026 13:24:27 -0500 Subject: [PATCH 2/3] [MM-67030] Remove newsletter signup and replace with terms/privacy agreement (#34801) * remove newsletter signup and replace with terms/privacy agreement * removed subscribeToSecurityNewsletter, made checkbox required * update signup test to remove newsletter and ensure the terms checkbox is required * update unit test and e2e test to reflect changes * fix e2e test * Removed susbcribe-newsletter endpoint in server * Update signup.test.tsx * remove unused css * remove unused css * fixed broken tests * fixed linter issues * Remove redundant IntlProvider and comments * Remove usage of test IDs from Signup tests * Remove usage of fireEvent * Remove usage of mountWithIntl from Signup tests * update e2e tests * fix playwright test * Fix Lint in signup.ts --------- Co-authored-by: maria.nunez Co-authored-by: Mattermost Build Co-authored-by: Harrison Healey Co-authored-by: yasserfaraazkhan --- .../auth_sso/authentication_1_spec.ts | 6 + .../auth_sso/authentication_2_spec.ts | 6 + .../auth_sso/authentication_4_spec.ts | 6 + .../auth_sso/authentication_spec.ts | 2 + .../onboarding/existing_email_adress_spec.js | 3 + .../login_page_link_account_creation_spec.js | 1 + .../use_team_invite_link_to_sign_up_spec.js | 3 + .../signin_authentication/signup_spec.js | 42 ++- .../closed_team_invite_by_email_spec.js | 3 + ...closed_team_with_not_allowed_email_spec.js | 5 +- .../playwright/lib/src/ui/pages/signup.ts | 23 +- .../common/signup_user_complete.spec.ts | 26 +- server/channels/api4/hosted_customer.go | 33 --- server/einterfaces/cloud.go | 2 - server/einterfaces/mocks/CloudInterface.go | 18 -- server/i18n/en.json | 4 - server/public/model/hosted_customer.go | 10 - server/scripts/vet-api-check.sh | 1 - .../signup/__snapshots__/signup.test.tsx.snap | 126 +++----- .../src/components/signup/signup.scss | 29 -- .../src/components/signup/signup.test.tsx | 278 +++++++++--------- .../channels/src/components/signup/signup.tsx | 145 +++------ webapp/channels/src/i18n/en.json | 7 +- webapp/platform/client/src/client4.ts | 8 - webapp/platform/types/src/cloud.ts | 5 - 25 files changed, 310 insertions(+), 482 deletions(-) delete mode 100644 server/public/model/hosted_customer.go diff --git a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts index 6f4061bb835..1f42c1561a6 100644 --- a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts @@ -61,6 +61,8 @@ describe('Authentication', () => { cy.get('#input_name').clear().type(`test${getRandomId()}`); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + cy.findByText('Create account').click(); // * Make sure account was created successfully and we are at the select team page @@ -113,6 +115,8 @@ describe('Authentication', () => { cy.get('#input_name').clear().type(`test${getRandomId()}`); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + cy.findByText('Create account').click(); // * Make sure account was not created successfully @@ -146,6 +150,8 @@ describe('Authentication', () => { cy.get('#input_name').clear().type(username); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + cy.findByText('Create account').click(); // * Make sure account was created successfully and we are on the team joining page diff --git a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_2_spec.ts b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_2_spec.ts index 7198829b4f8..2c366bdf523 100644 --- a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_2_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_2_spec.ts @@ -61,6 +61,8 @@ describe('Authentication', () => { cy.get('#input_password-input').clear().type('less'); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + cy.findByText('Create account').click(); // * Assert the error is what is expected; @@ -68,6 +70,8 @@ describe('Authentication', () => { cy.get('#input_password-input').clear().type('greaterthan7'); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + cy.findByText('Create account').click(); // * Assert that we are not shown an MFA screen and instead a Teams You Can join page @@ -112,6 +116,8 @@ describe('Authentication', () => { cy.get('#input_name').clear().type(`BestUsernameInTheWorld${getRandomId()}`); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + ['NOLOWERCASE123!', 'noupppercase123!', 'NoNumber!', 'NoSymbol123'].forEach((option) => { cy.get('#input_password-input').clear().type(option); cy.findByText('Create account').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts index 004d1895015..64234aecb66 100644 --- a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts @@ -149,6 +149,8 @@ describe('Authentication', () => { cy.get('#input_password-input').type('Test123456!'); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + ['1user', 'te', 'user#1', 'user!1'].forEach((option) => { cy.get('#input_name').clear().type(option); cy.findByText('Create account').click(); @@ -183,6 +185,8 @@ describe('Authentication', () => { cy.get('#input_name').clear().type(`Test${getRandomId()}`); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + cy.findByText('Create account').click(); // * Make sure account was created successfully and we are on the team joining page @@ -245,6 +249,8 @@ describe('Authentication', () => { cy.get('#input_name').clear().type(`Test${getRandomId()}`); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + cy.findByText('Create account').click(); // * Make sure account was not created successfully diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/auth_sso/authentication_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/auth_sso/authentication_spec.ts index 22b2701697b..0c4713b7aaa 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/auth_sso/authentication_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/auth_sso/authentication_spec.ts @@ -58,6 +58,8 @@ describe('Authentication', () => { cy.get('#input_name').clear().type(`Test${getRandomId()}`); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + cy.findByText('Create account').click(); // * Make sure account was not created successfully diff --git a/e2e-tests/cypress/tests/integration/channels/onboarding/existing_email_adress_spec.js b/e2e-tests/cypress/tests/integration/channels/onboarding/existing_email_adress_spec.js index c0c74a79c75..fff60fcc00d 100644 --- a/e2e-tests/cypress/tests/integration/channels/onboarding/existing_email_adress_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/onboarding/existing_email_adress_spec.js @@ -31,6 +31,9 @@ function signupWithEmail(name, pw) { // # Type 'unique1pw' for password cy.get('#input_password-input').type(pw); + // # Check the terms and privacy checkbox + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + // # Click on Create Account button cy.findByText('Create account').click(); } diff --git a/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js b/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js index 14f55a8f762..8430167e667 100644 --- a/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js @@ -71,6 +71,7 @@ describe('Onboarding', () => { cy.get('#input_email').should('be.focused').and('be.visible').type(email); cy.get('#input_name').should('be.visible').type(username); cy.get('#input_password-input').should('be.visible').type(password); + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); cy.findByText('Create account').click(); cy.findByText('You’re almost done!').should('be.visible'); diff --git a/e2e-tests/cypress/tests/integration/channels/onboarding/use_team_invite_link_to_sign_up_spec.js b/e2e-tests/cypress/tests/integration/channels/onboarding/use_team_invite_link_to_sign_up_spec.js index 92eb39043d5..35574b8c482 100644 --- a/e2e-tests/cypress/tests/integration/channels/onboarding/use_team_invite_link_to_sign_up_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/onboarding/use_team_invite_link_to_sign_up_spec.js @@ -71,6 +71,9 @@ describe('Onboarding', () => { cy.get('#input_name').should('be.visible').type(username); cy.get('#input_password-input').should('be.visible').type(password); + // # Check the terms and privacy checkbox + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + // # Attempt to create an account by clicking on the 'Create account' button cy.findByText('Create account').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/signin_authentication/signup_spec.js b/e2e-tests/cypress/tests/integration/channels/signin_authentication/signup_spec.js index 7054d953939..2f8821ea12c 100644 --- a/e2e-tests/cypress/tests/integration/channels/signin_authentication/signup_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/signin_authentication/signup_spec.js @@ -72,15 +72,22 @@ describe('Signup Email page', () => { cy.get('#input_password-input').should('be.visible').and('have.attr', 'placeholder', 'Choose a Password'); cy.findByText('Your password must be 5-72 characters long.').should('be.visible'); + // * Check terms and privacy checkbox + cy.get('#signup-body-card-form-check-terms-and-privacy').should('be.visible').and('not.be.checked'); + cy.findByText(/I agree to the/i).should('be.visible'); + + // * Check that submit button is disabled without accepting terms cy.get('#saveSetting').scrollIntoView().should('be.visible'); - cy.get('#saveSetting').should('contain', 'Create account'); - - // * Check newsletter subscription checkbox text and links - cy.findByText('I would like to receive Mattermost security updates via newsletter.').should('be.visible'); - cy.findByText(/By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news\./).should('be.visible'); - cy.findByText(/I have read the/).parent().within(() => { - cy.findByRole('link', {name: 'Privacy Policy'}).should('be.visible').and('have.attr', 'href').and('include', 'mattermost.com/pl/privacy-policy/'); - cy.findByRole('link', {name: 'unsubscribe'}).should('be.visible').and('have.attr', 'href').and('include', 'forms.mattermost.com/UnsubscribePage.html'); + cy.get('#saveSetting').should('contain', 'Create account').and('be.disabled'); + + // * Check terms and privacy links (now part of checkbox label) + cy.get('label[for="signup-body-card-form-check-terms-and-privacy"]').within(() => { + cy.findByText('Acceptable Use Policy').should('be.visible'). + and('have.attr', 'href'). + and('include', config.SupportSettings.TermsOfServiceLink || TERMS_OF_SERVICE_LINK); + cy.findByText('Privacy Policy').should('be.visible'). + and('have.attr', 'href'). + and('include', config.SupportSettings.PrivacyPolicyLink || PRIVACY_POLICY_LINK); }); }); @@ -116,4 +123,23 @@ describe('Signup Email page', () => { cy.get('.footer-copyright').should('contain', `© ${currentYear} Mattermost Inc.`); }); }); + + it('should enable submit button when terms checkbox is checked', () => { + // # Fill in valid form data + cy.get('#input_email').type('test@example.com'); + cy.get('#input_name').type('testuser'); + cy.get('#input_password-input').type('validPassword123'); + + // * Verify submit button is disabled + cy.get('#saveSetting').should('be.disabled'); + + // # Check the terms and privacy checkbox + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + + // * Verify checkbox is now checked + cy.get('#signup-body-card-form-check-terms-and-privacy').should('be.checked'); + + // * Verify submit button is now enabled + cy.get('#saveSetting').should('not.be.disabled'); + }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js b/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js index 9512f2bf3c1..6884526358a 100644 --- a/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js @@ -114,6 +114,9 @@ describe('Team Settings', () => { cy.wait(TIMEOUTS.HALF_SEC); cy.get('#input_password-input').type(password); + // # Check the terms and privacy checkbox + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + // # Attempt to create an account by clicking on the 'Create Account' button cy.findByText('Create account').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/team_settings/join_closed_team_with_not_allowed_email_spec.js b/e2e-tests/cypress/tests/integration/channels/team_settings/join_closed_team_with_not_allowed_email_spec.js index 976ad356bc3..105f741ecd9 100644 --- a/e2e-tests/cypress/tests/integration/channels/team_settings/join_closed_team_with_not_allowed_email_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/team_settings/join_closed_team_with_not_allowed_email_spec.js @@ -87,7 +87,10 @@ describe('Team Settings', () => { cy.get('#input_name').type(username); cy.get('#input_password-input').type(password); - // # Attempt to create an account by clicking on the 'Create account' button + // # Check the terms and privacy checkbox + cy.get('#signup-body-card-form-check-terms-and-privacy').check(); + + // # Attempt to create an account by clicking on the 'Create Account' button cy.findByText('Create account').click(); // * Assert that the expected error message from creating an account with an email not from the allowed email domain exists and is visible diff --git a/e2e-tests/playwright/lib/src/ui/pages/signup.ts b/e2e-tests/playwright/lib/src/ui/pages/signup.ts index 240bbcbfe20..f695b7dab44 100644 --- a/e2e-tests/playwright/lib/src/ui/pages/signup.ts +++ b/e2e-tests/playwright/lib/src/ui/pages/signup.ts @@ -16,11 +16,9 @@ export default class SignupPage { readonly usernameInput; readonly passwordInput; readonly passwordToggleButton; - readonly newsLetterCheckBox; - readonly newsLetterPrivacyPolicyLink; - readonly newsLetterUnsubscribeLink; - readonly agreementTermsOfUseLink; - readonly agreementPrivacyPolicyLink; + readonly termsAndPrivacyCheckBox; + readonly termsAndPrivacyAcceptableUsePolicyLink; + readonly termsAndPrivacyPrivacyPolicyLink; readonly createAccountButton; readonly loginLink; readonly emailError; @@ -48,14 +46,12 @@ export default class SignupPage { ); this.passwordError = page.locator('text=Must be 5-72 characters long.'); - const newsletterBlock = page.locator('.check-input'); - this.newsLetterCheckBox = newsletterBlock.getByRole('checkbox', {name: 'newsletter checkbox'}); - this.newsLetterPrivacyPolicyLink = newsletterBlock.locator('text=Privacy Policy'); - this.newsLetterUnsubscribeLink = newsletterBlock.locator('text=unsubscribe'); - - const agreementBlock = page.locator('.signup-body-card-agreement'); - this.agreementTermsOfUseLink = agreementBlock.locator('text=Terms of Use'); - this.agreementPrivacyPolicyLink = agreementBlock.locator('text=Privacy Policy'); + const termsAndPrivacyBlock = page.locator('.check-input'); + this.termsAndPrivacyCheckBox = termsAndPrivacyBlock.getByRole('checkbox', { + name: 'Terms and privacy policy checkbox', + }); + this.termsAndPrivacyAcceptableUsePolicyLink = termsAndPrivacyBlock.locator('text=Acceptable Use Policy'); + this.termsAndPrivacyPrivacyPolicyLink = termsAndPrivacyBlock.locator('text=Privacy Policy'); this.header = new components.MainHeader(page.locator('.hfroute-header')); this.footer = new components.Footer(page.locator('.hfroute-footer')); @@ -79,6 +75,7 @@ export default class SignupPage { await this.emailInput.fill(user.email); await this.usernameInput.fill(user.username); await this.passwordInput.fill(user.password); + await this.termsAndPrivacyCheckBox.check(); await this.createAccountButton.click(); if (waitForRedirect) { diff --git a/e2e-tests/playwright/specs/accessibility/common/signup_user_complete.spec.ts b/e2e-tests/playwright/specs/accessibility/common/signup_user_complete.spec.ts index e0456404b69..714d2a2d272 100644 --- a/e2e-tests/playwright/specs/accessibility/common/signup_user_complete.spec.ts +++ b/e2e-tests/playwright/specs/accessibility/common/signup_user_complete.spec.ts @@ -43,25 +43,21 @@ test('/signup_user_complete accessibility tab support', async ({pw}, testInfo) = await pw.signupPage.passwordInput.press('Tab'); expect(await pw.signupPage.passwordToggleButton).toBeFocused(); - // * Should move focus to newsletter checkbox after tab + // * Should move focus to terms and privacy checkbox after tab await pw.signupPage.passwordToggleButton.press('Tab'); - expect(await pw.signupPage.newsLetterCheckBox).toBeFocused(); + expect(await pw.signupPage.termsAndPrivacyCheckBox).toBeFocused(); - // * Should move focus to newsletter privacy policy link after tab - await pw.signupPage.newsLetterCheckBox.press('Tab'); - expect(await pw.signupPage.newsLetterPrivacyPolicyLink).toBeFocused(); + // * Should move focus to acceptable use policy link after tab + await pw.signupPage.termsAndPrivacyCheckBox.press('Tab'); + expect(await pw.signupPage.termsAndPrivacyAcceptableUsePolicyLink).toBeFocused(); - // * Should move focus to newsletter unsubscribe link after tab - await pw.signupPage.newsLetterPrivacyPolicyLink.press('Tab'); - expect(await pw.signupPage.newsLetterUnsubscribeLink).toBeFocused(); - - // * Should move focus to agreement terms of use link after tab - await pw.signupPage.newsLetterUnsubscribeLink.press('Tab'); - expect(await pw.signupPage.agreementTermsOfUseLink).toBeFocused(); + // * Should move focus to privacy policy link after tab + await pw.signupPage.termsAndPrivacyAcceptableUsePolicyLink.press('Tab'); + expect(await pw.signupPage.termsAndPrivacyPrivacyPolicyLink).toBeFocused(); - // * Should move focus to agreement privacy policy link after tab - await pw.signupPage.agreementTermsOfUseLink.press('Tab'); - expect(await pw.signupPage.agreementPrivacyPolicyLink).toBeFocused(); + // * Should move focus to about link after tab (skips disabled create account button) + await pw.signupPage.termsAndPrivacyPrivacyPolicyLink.press('Tab'); + expect(await pw.signupPage.footer.aboutLink).toBeFocused(); // * Should move focus to privacy policy link after tab await pw.signupPage.footer.aboutLink.press('Tab'); diff --git a/server/channels/api4/hosted_customer.go b/server/channels/api4/hosted_customer.go index 90adf3f9cc6..d502cea41ce 100644 --- a/server/channels/api4/hosted_customer.go +++ b/server/channels/api4/hosted_customer.go @@ -4,8 +4,6 @@ package api4 import ( - "encoding/json" - "io" "net/http" "github.com/mattermost/mattermost/server/public/model" @@ -16,40 +14,9 @@ import ( func (api *API) InitHostedCustomer() { // POST /api/v4/hosted_customer/available api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods(http.MethodGet) - api.BaseRoutes.HostedCustomer.Handle("/subscribe-newsletter", api.APIHandler(handleSubscribeToNewsletter)).Methods(http.MethodPost) } func handleSignupAvailable(c *Context, w http.ResponseWriter, r *http.Request) { const where = "Api4.handleSignupAvailable" c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented) } - -func handleSubscribeToNewsletter(c *Context, w http.ResponseWriter, r *http.Request) { - const where = "Api4.handleSubscribeToNewsletter" - ensured := ensureCloudInterface(c, where) - if !ensured { - return - } - - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) - return - } - - req := new(model.SubscribeNewsletterRequest) - err = json.Unmarshal(bodyBytes, req) - if err != nil { - c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err) - return - } - - req.ServerID = c.App.Srv().ServerId() - - if err := c.App.Cloud().SubscribeToNewsletter("", req); err != nil { - c.Err = model.NewAppError(where, "api.server.cws.subscribe_to_newsletter.app_error", nil, "CWS Server failed to subscribe to newsletter.", http.StatusInternalServerError).Wrap(err) - return - } - - ReturnStatusOK(w) -} diff --git a/server/einterfaces/cloud.go b/server/einterfaces/cloud.go index b3eab4d425d..e7b9eb96da0 100644 --- a/server/einterfaces/cloud.go +++ b/server/einterfaces/cloud.go @@ -34,8 +34,6 @@ type CloudInterface interface { CheckCWSConnection(userId string) error - SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error - ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error) GetIPFilters(userID string) (*model.AllowedIPRanges, error) GetInstallation(userID string) (*model.Installation, error) diff --git a/server/einterfaces/mocks/CloudInterface.go b/server/einterfaces/mocks/CloudInterface.go index 01260baec9d..d5ef4fc88df 100644 --- a/server/einterfaces/mocks/CloudInterface.go +++ b/server/einterfaces/mocks/CloudInterface.go @@ -503,24 +503,6 @@ func (_m *CloudInterface) RemoveAuditLoggingCert(userID string) error { return r0 } -// SubscribeToNewsletter provides a mock function with given fields: userID, req -func (_m *CloudInterface) SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error { - ret := _m.Called(userID, req) - - if len(ret) == 0 { - panic("no return value specified for SubscribeToNewsletter") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, *model.SubscribeNewsletterRequest) error); ok { - r0 = rf(userID, req) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) { ret := _m.Called(userID, customerInfo) diff --git a/server/i18n/en.json b/server/i18n/en.json index 1607abda20f..496817cb4bc 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -3158,10 +3158,6 @@ "id": "api.server.cws.needs_enterprise_edition", "translation": "Service only available in Mattermost Enterprise edition" }, - { - "id": "api.server.cws.subscribe_to_newsletter.app_error", - "translation": "CWS Server failed to subscribe to newsletter." - }, { "id": "api.server.hosted_signup_unavailable.error", "translation": "Portal unavailable for self-hosted signup." diff --git a/server/public/model/hosted_customer.go b/server/public/model/hosted_customer.go deleted file mode 100644 index ffaec15cba0..00000000000 --- a/server/public/model/hosted_customer.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -type SubscribeNewsletterRequest struct { - Email string `json:"email"` - ServerID string `json:"server_id"` - SubscribedContent string `json:"subscribed_content"` -} diff --git a/server/scripts/vet-api-check.sh b/server/scripts/vet-api-check.sh index d5e5d6cc310..7dc1ebc30e5 100755 --- a/server/scripts/vet-api-check.sh +++ b/server/scripts/vet-api-check.sh @@ -40,7 +40,6 @@ OUTPUT_EXCLUDING_IGNORED=$(echo "$OUTPUT" | grep -Fv \ -e 'Cannot find /api/v4/hosted_customer/confirm-expand method: POST in OpenAPI 3 spec.' \ -e 'Cannot find /api/v4/hosted_customer/invoices method: GET in OpenAPI 3 spec.' \ -e 'Cannot find /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/hosted_customer/subscribe-newsletter method: POST in OpenAPI 3 spec.' \ -e 'Cannot find /api/v4/license/review method: POST in OpenAPI 3 spec.' \ -e 'Cannot find /api/v4/license/review/status method: GET in OpenAPI 3 spec.' \ -e 'Cannot find /api/v4/posts/{post_id}/edit_history method: GET in OpenAPI 3 spec.' \ diff --git a/webapp/channels/src/components/signup/__snapshots__/signup.test.tsx.snap b/webapp/channels/src/components/signup/__snapshots__/signup.test.tsx.snap index 4ff9940f9e4..7e854ebf1ba 100644 --- a/webapp/channels/src/components/signup/__snapshots__/signup.test.tsx.snap +++ b/webapp/channels/src/components/signup/__snapshots__/signup.test.tsx.snap @@ -118,7 +118,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e autoFocus={true} className="signup-body-card-form-email-input" customMessage={null} - data-testid="signup-body-card-form-email-input" disabled={false} inputSize="large" name="email" @@ -136,7 +135,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e "value": "You can use lowercase letters, numbers, periods, dashes, and underscores.", } } - data-testid="signup-body-card-form-name-input" disabled={false} inputSize="large" name="name" @@ -148,7 +146,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e -
- - Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter? - - - Sign up at - - https://mattermost.com/security-updates/ - - . - -
+ + Acceptable Use Policy +
, + " and the ", + + Privacy Policy + , + ] + } + /> -

- -

@@ -284,7 +267,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e autoFocus={true} className="signup-body-card-form-email-input" customMessage={null} - data-testid="signup-body-card-form-email-input" disabled={false} inputSize="large" name="email" @@ -302,7 +284,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e "value": "You can use lowercase letters, numbers, periods, dashes, and underscores.", } } - data-testid="signup-body-card-form-name-input" disabled={false} inputSize="large" name="name" @@ -314,7 +295,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e -
- - Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter? - - - Sign up at - - https://mattermost.com/security-updates/ - - . - -
+ + Acceptable Use Policy +
, + " and the ", + + Privacy Policy + , + ] + } + /> -

- -

diff --git a/webapp/channels/src/components/signup/signup.scss b/webapp/channels/src/components/signup/signup.scss index 915e2ca60f8..24589406710 100644 --- a/webapp/channels/src/components/signup/signup.scss +++ b/webapp/channels/src/components/signup/signup.scss @@ -167,22 +167,6 @@ margin-top: 22px; } - .newsletter { - margin-top: 24px; - margin-bottom: 32px; - color: rgba(var(--center-channel-color-rgb), 0.75); - font-family: 'Open Sans'; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 16px; - - .interested { - display: block; - color: var(--center-channel-color); - } - } - .signup-body-card-form-button-submit { @include mixins.primary-button; @include mixins.button-large; @@ -222,19 +206,6 @@ row-gap: 24px; } } - - .signup-body-card-agreement { - margin-top: 32px; - color: rgba(var(--center-channel-color-rgb), 0.75); - font-size: 11px; - line-height: 16px; - - a { - @include mixins.link; - - font-size: 11px; - } - } } } diff --git a/webapp/channels/src/components/signup/signup.test.tsx b/webapp/channels/src/components/signup/signup.test.tsx index 0e4c3ac653c..e33aeb10674 100644 --- a/webapp/channels/src/components/signup/signup.test.tsx +++ b/webapp/channels/src/components/signup/signup.test.tsx @@ -1,24 +1,18 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {ReactWrapper} from 'enzyme'; import {shallow} from 'enzyme'; import React from 'react'; -import {IntlProvider} from 'react-intl'; -import {BrowserRouter} from 'react-router-dom'; import type {ClientConfig} from '@mattermost/types/config'; import {RequestStatus} from 'mattermost-redux/constants'; -import * as useCWSAvailabilityCheckAll from 'components/common/hooks/useCWSAvailabilityCheck'; -import SaveButton from 'components/save_button'; +import {redirectUserToDefaultTeam} from 'actions/global_actions'; + import Signup from 'components/signup/signup'; -import Input from 'components/widgets/inputs/input/input'; -import PasswordInput from 'components/widgets/inputs/password_input/password_input'; -import {mountWithIntl} from 'tests/helpers/intl-test-helper'; -import {act, renderWithContext, screen, fireEvent, waitFor} from 'tests/react_testing_utils'; +import {renderWithContext, screen, waitFor, userEvent} from 'tests/react_testing_utils'; import {WindowSizes} from 'utils/constants'; import type {GlobalState} from 'types/store'; @@ -30,12 +24,6 @@ let mockLicense = {IsLicensed: 'true', Cloud: 'false'}; let mockConfig: Partial; let mockDispatch = jest.fn(); -const intlProviderProps = { - defaultLocale: 'en', - locale: 'en', - messages: {}, -}; - jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux') as typeof import('react-redux'), useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState), @@ -56,13 +44,21 @@ jest.mock('mattermost-redux/selectors/entities/general', () => ({ getConfig: () => mockConfig, })); +let mockCurrentUserId = ''; + jest.mock('mattermost-redux/selectors/entities/users', () => ({ ...jest.requireActual('mattermost-redux/selectors/entities/users') as typeof import('mattermost-redux/selectors/entities/users'), - getCurrentUserId: () => '', + getCurrentUserId: () => mockCurrentUserId, +})); + +jest.mock('actions/global_actions', () => ({ + ...jest.requireActual('actions/global_actions'), + redirectUserToDefaultTeam: jest.fn(), })); jest.mock('actions/team_actions', () => ({ ...jest.requireActual('actions/team_actions') as typeof import('actions/team_actions'), + addUserToTeamFromInvite: jest.fn().mockResolvedValue({data: {}}), addUsersToTeamFromInvite: jest.fn().mockResolvedValue({name: 'teamName'}), })); @@ -76,27 +72,14 @@ jest.mock('actions/views/login', () => ({ loginById: jest.fn().mockResolvedValue({data: {}}), })); -jest.mock('actions/team_actions', () => ({ - ...jest.requireActual('actions/team_actions') as typeof import('actions/team_actions'), - addUserToTeamFromInvite: jest.fn().mockResolvedValue({data: {}}), -})); - jest.mock('actions/storage'); -const actImmediate = (wrapper: ReactWrapper) => - act( - () => - new Promise((resolve) => { - setImmediate(() => { - wrapper.update(); - resolve(); - }); - }), - ); - describe('components/signup/Signup', () => { beforeEach(() => { mockLocation = {pathname: '', search: '', hash: ''}; + mockHistoryPush.mockClear(); + mockDispatch.mockClear(); + mockCurrentUserId = ''; mockLicense = {IsLicensed: 'true', Cloud: 'false'}; @@ -208,33 +191,27 @@ describe('components/signup/Signup', () => { mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser mockResolvedValueOnce({error: {server_error_id: 'api.user.login.not_verified.app_error'}}); // loginById - const wrapper = mountWithIntl( - - - - - , + renderWithContext( + , ); - const emailInput = wrapper.find(Input).first().find('input').first(); - emailInput.simulate('change', {target: {value: 'jdoe@mm.com'}}); - - const nameInput = wrapper.find('#input_name').first(); - nameInput.simulate('change', {target: {value: 'jdoe'}}); - - const passwordInput = wrapper.find(PasswordInput).first().find('input').first(); - passwordInput.simulate('change', {target: {value: 'password'}}); + const emailInput = screen.getByLabelText('Email address'); + const usernameInput = screen.getByLabelText('Choose a Username'); + const passwordInput = screen.getByLabelText('Choose a Password'); + const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i}); + const submitButton = screen.getByRole('button', {name: 'Create account'}); - const saveButton = wrapper.find(SaveButton).first(); - expect(saveButton.props().disabled).toEqual(false); + await userEvent.type(emailInput, 'jdoe@mm.com'); + await userEvent.type(usernameInput, 'jdoe'); + await userEvent.type(passwordInput, 'password'); + await userEvent.click(termsCheckbox); - saveButton.find('button').first().simulate('click'); + expect(submitButton).not.toBeDisabled(); + await userEvent.click(submitButton); - await actImmediate(wrapper); - - expect(wrapper.find(Input).first().props().disabled).toEqual(true); - expect(wrapper.find('#input_name').first().props().disabled).toEqual(true); - expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true); + expect(emailInput).toBeDisabled(); + expect(usernameInput).toBeDisabled(); + expect(passwordInput).toBeDisabled(); expect(mockHistoryPush).toHaveBeenCalledWith('/should_verify_email?email=jdoe%40mm.com&teamname=teamName'); }); @@ -245,67 +222,77 @@ describe('components/signup/Signup', () => { mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser mockResolvedValueOnce({}); // loginById - const wrapper = mountWithIntl( - - - - - , + renderWithContext( + , ); - const emailInput = wrapper.find(Input).first().find('input').first(); - emailInput.simulate('change', {target: {value: 'jdoe@mm.com'}}); - - const nameInput = wrapper.find('#input_name').first(); - nameInput.simulate('change', {target: {value: 'jdoe'}}); - - const passwordInput = wrapper.find(PasswordInput).first().find('input').first(); - passwordInput.simulate('change', {target: {value: 'password'}}); + const emailInput = screen.getByLabelText('Email address'); + const usernameInput = screen.getByLabelText('Choose a Username'); + const passwordInput = screen.getByLabelText('Choose a Password'); + const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i}); + const submitButton = screen.getByRole('button', {name: 'Create account'}); - const saveButton = wrapper.find(SaveButton).first(); - expect(saveButton.props().disabled).toEqual(false); + await userEvent.type(emailInput, 'jdoe@mm.com'); + await userEvent.type(usernameInput, 'jdoe'); + await userEvent.type(passwordInput, 'password'); + await userEvent.click(termsCheckbox); - saveButton.find('button').first().simulate('click'); + expect(submitButton).not.toBeDisabled(); + await userEvent.click(submitButton); - await actImmediate(wrapper); + expect(emailInput).toBeDisabled(); + expect(usernameInput).toBeDisabled(); + expect(passwordInput).toBeDisabled(); - expect(wrapper.find(Input).first().props().disabled).toEqual(true); - expect(wrapper.find('#input_name').first().props().disabled).toEqual(true); - expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true); + expect(redirectUserToDefaultTeam).toHaveBeenCalled(); }); it('should focus email input when email validation fails', async () => { renderWithContext(, mockState); - const emailInput = screen.getByTestId('signup-body-card-form-email-input'); - const submitButton = screen.getByText('Create account'); + const emailInput = screen.getByLabelText('Email address'); + const usernameInput = screen.getByLabelText('Choose a Username'); + const passwordInput = screen.getByLabelText('Choose a Password'); + const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i}); + const submitButton = screen.getByRole('button', {name: 'Create account'}); // Submit with invalid email - fireEvent.change(emailInput, {target: {value: 'invalid-email'}}); - fireEvent.click(submitButton); + await userEvent.type(emailInput, 'invalid-email'); + await userEvent.type(usernameInput, 'testuser'); + await userEvent.type(passwordInput, '123'); + await userEvent.click(termsCheckbox); - await waitFor(() => { - expect(emailInput).toHaveFocus(); - }); + // The focus should no longer be on the email input before clicking submit + expect(emailInput).not.toHaveFocus(); + + await userEvent.click(submitButton); + + // And now the focus should move back to the email input + expect(emailInput).toHaveFocus(); }); it('should focus password input when password validation fails', async () => { renderWithContext(, mockState); - const emailInput = screen.getByTestId('signup-body-card-form-email-input'); - const usernameInput = screen.getByTestId('signup-body-card-form-name-input'); - const passwordInput = screen.getByTestId('signup-body-card-form-password-input'); + const emailInput = screen.getByLabelText('Email address'); + const usernameInput = screen.getByLabelText('Choose a Username'); + const passwordInput = screen.getByLabelText('Choose a Password'); + const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i}); const submitButton = screen.getByText('Create account'); // Submit with valid email and username but invalid password - fireEvent.change(emailInput, {target: {value: 'test@example.com'}}); - fireEvent.change(usernameInput, {target: {value: 'testuser'}}); - fireEvent.change(passwordInput, {target: {value: '123'}}); - fireEvent.click(submitButton); + await userEvent.type(emailInput, 'test@example.com'); + await userEvent.type(usernameInput, 'testuser'); + await userEvent.type(passwordInput, '123'); + await userEvent.click(termsCheckbox); - await waitFor(() => { - expect(passwordInput).toHaveFocus(); - }); + // The focus should no longer be on the password input before clicking submit + expect(emailInput).not.toHaveFocus(); + + await userEvent.click(submitButton); + + // And now the focus should move back to the password input + expect(passwordInput).toHaveFocus(); }); it('should focus username input when server returns username exists error', async () => { @@ -319,87 +306,102 @@ describe('components/signup/Signup', () => { renderWithContext(, mockState); - const emailInput = screen.getByTestId('signup-body-card-form-email-input'); - const usernameInput = screen.getByTestId('signup-body-card-form-name-input'); - const passwordInput = screen.getByTestId('signup-body-card-form-password-input'); + const emailInput = screen.getByLabelText('Email address'); + const usernameInput = screen.getByLabelText('Choose a Username'); + const passwordInput = screen.getByLabelText('Choose a Password'); + const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i}); const submitButton = screen.getByText('Create account'); // Submit with valid data that will trigger server error - fireEvent.change(emailInput, {target: {value: 'test@example.com'}}); - fireEvent.change(usernameInput, {target: {value: 'existinguser'}}); - fireEvent.change(passwordInput, {target: {value: 'password123'}}); - fireEvent.click(submitButton); + await userEvent.type(emailInput, 'test@example.com'); + await userEvent.type(usernameInput, 'existinguser'); + await userEvent.type(passwordInput, 'password123'); + await userEvent.click(termsCheckbox); - await waitFor(() => { - expect(usernameInput).toHaveFocus(); - }); + // The focus should no longer be on the email input before clicking submit + expect(usernameInput).not.toHaveFocus(); + + await userEvent.click(submitButton); + + // And now the focus should move back to the username input + expect(usernameInput).toHaveFocus(); }); it('should add user to team and redirect when team invite valid and logged in', async () => { mockLocation.search = '?id=ppni7a9t87fn3j4d56rwocdctc'; + mockCurrentUserId = 'user1'; // Simulate logged-in user - const wrapper = shallow( + mockDispatch = jest.fn(). + mockResolvedValueOnce({}). // removeGlobalItem in useEffect + mockResolvedValueOnce({data: {name: 'teamName'}}); // addUserToTeamFromInvite + + renderWithContext( , ); - setTimeout(() => { + await waitFor(() => { expect(mockHistoryPush).toHaveBeenCalledWith('/teamName/channels/town-square'); - expect(wrapper).toMatchSnapshot(); - }, 0); + }); }); - it('should handle failure adding user to team when team invite and logged in', () => { + it('should handle failure adding user to team when team invite and logged in', async () => { mockLocation.search = '?id=ppni7a9t87fn3j4d56rwocdctc'; + mockCurrentUserId = 'user1'; // Simulate logged-in user - const wrapper = shallow( - , - ); + mockDispatch = jest.fn(). + mockResolvedValueOnce({}). // removeGlobalItem in useEffect + mockResolvedValueOnce({ + error: { + server_error_id: 'api.team.add_user_to_team_from_invite.invalid.app_error', + message: 'Invalid invite', + }, + }); // addUserToTeamFromInvite with error + + renderWithContext(, mockState); - setTimeout(() => { + await waitFor(() => { expect(mockHistoryPush).not.toHaveBeenCalled(); - expect(wrapper.find('.content-layout-column-title').text()).toEqual('This invite link is invalid'); + expect(screen.getByText('This invite link is invalid')).toBeInTheDocument(); }); }); - it('should show newsletter check box opt-in for self-hosted non airgapped workspaces', async () => { - jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available); - mockLicense = {IsLicensed: 'true', Cloud: 'false'}; + it('should show terms and privacy checkbox', async () => { + mockConfig.TermsOfServiceLink = 'https://mattermost.com/terms'; + mockConfig.PrivacyPolicyLink = 'https://mattermost.com/privacy'; const {container: signupContainer} = renderWithContext( , ); - screen.getByTestId('signup-body-card-form-check-newsletter'); - const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter'); + const checkInput = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i}); expect(checkInput).toHaveAttribute('type', 'checkbox'); + expect(checkInput).not.toBeChecked(); - expect(signupContainer).toHaveTextContent('I would like to receive Mattermost security updates via newsletter. By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the Privacy Policy and understand that I can unsubscribe at any time'); + expect(signupContainer).toHaveTextContent('I agree to the Acceptable Use Policy and the Privacy Policy'); }); - it('should NOT show newsletter check box opt-in for self-hosted AND airgapped workspaces', async () => { - jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Unavailable); - mockLicense = {IsLicensed: 'true', Cloud: 'false'}; - - const {container: signupContainer} = renderWithContext( - , - ); + it('should require terms acceptance before enabling submit button', async () => { + renderWithContext(, mockState); - expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow(); - expect(signupContainer).toHaveTextContent('Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter?Sign up at https://mattermost.com/security-updates/.'); - }); + const emailInput = screen.getByLabelText('Email address'); + const usernameInput = screen.getByLabelText('Choose a Username'); + const passwordInput = screen.getByLabelText('Choose a Password'); + const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i}); - it('should show newsletter related opt-in or text for cloud', async () => { - jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available); - mockLicense = {IsLicensed: 'true', Cloud: 'true'}; + // Fill in all fields but don't check terms + await userEvent.type(emailInput, 'test@example.com'); + await userEvent.type(usernameInput, 'testuser'); + await userEvent.type(passwordInput, 'ValidPassword123!'); - const {container: signupContainer} = renderWithContext( - , - ); + // Submit button should be disabled (SaveButton uses disabled prop on inner button) + const submitButton = screen.getByRole('button', {name: /Create account/i}); + expect(submitButton).toBeDisabled(); - screen.getByTestId('signup-body-card-form-check-newsletter'); - const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter'); - expect(checkInput).toHaveAttribute('type', 'checkbox'); + // Check terms + await userEvent.click(termsCheckbox); - expect(signupContainer).toHaveTextContent('I would like to receive Mattermost security updates via newsletter. By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the Privacy Policy and understand that I can unsubscribe at any time'); + // Now submit button should be enabled + const enabledButton = screen.getByRole('button', {name: /Create account/i}); + expect(enabledButton).not.toBeDisabled(); }); }); diff --git a/webapp/channels/src/components/signup/signup.tsx b/webapp/channels/src/components/signup/signup.tsx index f37be9bce03..545316cafec 100644 --- a/webapp/channels/src/components/signup/signup.tsx +++ b/webapp/channels/src/components/signup/signup.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import throttle from 'lodash/throttle'; import React, {useState, useEffect, useRef, useCallback} from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; +import {useIntl} from 'react-intl'; import {useSelector, useDispatch} from 'react-redux'; import {useLocation, useHistory, Route} from 'react-router-dom'; @@ -27,7 +27,6 @@ import {getGlobalItem} from 'selectors/storage'; import AlertBanner from 'components/alert_banner'; import type {ModeType, AlertBannerProps} from 'components/alert_banner'; -import useCWSAvailabilityCheck, {CSWAvailabilityCheckTypes} from 'components/common/hooks/useCWSAvailabilityCheck'; import DesktopAuthToken from 'components/desktop_auth_token'; import ExternalLink from 'components/external_link'; import ExternalLoginButton from 'components/external_login_button/external_login_button'; @@ -48,7 +47,7 @@ import Input, {SIZE} from 'components/widgets/inputs/input/input'; import type {CustomMessageInputType} from 'components/widgets/inputs/input/input'; import PasswordInput from 'components/widgets/inputs/password_input/password_input'; -import {Constants, HostedCustomerLinks, ItemStatus, ValidationErrors} from 'utils/constants'; +import {Constants, ItemStatus, ValidationErrors} from 'utils/constants'; import {isValidPassword} from 'utils/password'; import {isDesktopApp} from 'utils/user_agent'; import {isValidUsername} from 'utils/utils'; @@ -139,28 +138,17 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { const [teamName, setTeamName] = useState(parsedTeamName ?? ''); const [alertBanner, setAlertBanner] = useState(null); const [isMobileView, setIsMobileView] = useState(false); - const [subscribeToSecurityNewsletter, setSubscribeToSecurityNewsletter] = useState(false); + const [acceptedTerms, setAcceptedTerms] = useState(false); const [submitClicked, setSubmitClicked] = useState(false); - const cwsAvailability = useCWSAvailabilityCheck(); - const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML; const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner); - const canSubmit = Boolean(email && name && password) && !hasError && !loading; + const canSubmit = Boolean(email && name && password && acceptedTerms) && !hasError && !loading; const passwordConfig = useSelector(getPasswordConfig); const {error: passwordInfo} = isValidPassword('', passwordConfig, intl); const [desktopLoginLink, setDesktopLoginLink] = useState(''); - const subscribeToSecurityNewsletterFunc = () => { - try { - Client4.subscribeToNewsletter({email, subscribed_content: 'security_newsletter'}); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - } - }; - const getExternalSignupOptions = () => { const externalLoginOptions: ExternalLoginButtonType[] = []; @@ -595,9 +583,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { } await handleSignupSuccess(user, data!); - if (subscribeToSecurityNewsletter) { - subscribeToSecurityNewsletterFunc(); - } } else { setIsWaiting(false); } @@ -605,68 +590,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { const handleReturnButtonOnClick = () => history.replace('/'); - const getNewsletterCheck = () => { - if (cwsAvailability === CSWAvailabilityCheckTypes.Available) { - return ( - setSubscribeToSecurityNewsletter(!subscribeToSecurityNewsletter)} - text={ - formatMessage( - {id: 'newsletter_optin.checkmark.text', defaultMessage: 'I would like to receive Mattermost security updates via newsletter. By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the Privacy Policy and understand that I can unsubscribe at any time'}, - { - a: (chunks: React.ReactNode | React.ReactNodeArray) => ( - - {chunks} - - ), - aa: (chunks: React.ReactNode | React.ReactNodeArray) => ( - - {chunks} - - ), - span: (chunks: React.ReactNode | React.ReactNodeArray) => ( - {chunks} - ), - }, - )} - checked={subscribeToSecurityNewsletter} - /> - ); - } - return ( -
- - {formatMessage({id: 'newsletter_optin.title', defaultMessage: 'Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter?'})} - - - {formatMessage( - {id: 'newsletter_optin.desc', defaultMessage: 'Sign up at {link}.'}, - { - link: HostedCustomerLinks.SECURITY_UPDATES, - a: (chunks: React.ReactNode | React.ReactNodeArray) => ( - - {chunks} - - ), - }, - )} - -
- ); - }; - const getContent = () => { if (!enableSignUpWithEmail && !enableExternalSignup) { return ( @@ -780,7 +703,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { {enableSignUpWithEmail && (
{ customMessage={emailCustomLabelForInput} /> { } /> { info={passwordInfo as string} error={passwordError} /> - {getNewsletterCheck()} + setAcceptedTerms(!acceptedTerms)} + text={ + formatMessage( + {id: 'signup.terms_and_privacy.checkmark.text', defaultMessage: 'I agree to the Acceptable Use Policy and the Privacy Policy'}, + { + privacyPolicyLink: (chunks: React.ReactNode | React.ReactNodeArray) => ( + + {chunks} + + ), + termsOfUseLink: (chunks: React.ReactNode | React.ReactNodeArray) => ( + + {chunks} + + ), + }, + )} + checked={acceptedTerms} + /> { ))} )} - {enableSignUpWithEmail && !serverError && ( -

- ( - - {chunks} - - ), - privacyPolicyLink: (chunks) => ( - - {chunks} - - ), - }} - /> -

- )} diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index c55621e1925..e5adc09ff09 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -5144,10 +5144,6 @@ "new_window_button.tooltip": "Open in new window", "newChannelWithBoard.tutorialTip.description": "The board you just created can be quickly accessed by clicking on the Boards icon in the App bar. You can view the boards that are linked to this channel in the right-hand sidebar and open one in full view.", "newChannelWithBoard.tutorialTip.title": "Access linked boards from the App Bar", - "newsletter_optin.checkmark.box": "newsletter checkbox", - "newsletter_optin.checkmark.text": "I would like to receive Mattermost security updates via newsletter. By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the Privacy Policy and understand that I can unsubscribe at any time", - "newsletter_optin.desc": "Sign up at {link}.", - "newsletter_optin.title": "Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter?", "next_steps_view.welcomeToMattermost": "Welcome to Mattermost", "no_results.channel_files_filtered.subtitle": "This channel doesn't contains any file with the selected file format.", "no_results.channel_files_filtered.title": "No files found", @@ -5953,8 +5949,9 @@ "signup_user_completed.userHelp": "You can use lowercase letters, numbers, periods, dashes, and underscores.", "signup_user_completed.usernameLength": "Usernames have to begin with a lowercase letter and be {min}-{max} characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.", "signup_user_completed.validEmail": "Please enter a valid email address", - "signup.agreement": "By proceeding to create your account and use {siteName}, you agree to our Terms of Use and Privacy Policy. If you do not agree, you cannot use {siteName}.", "signup.ldap": "AD/LDAP Credentials", + "signup.terms_and_privacy.checkmark.box": "Terms and privacy policy checkbox", + "signup.terms_and_privacy.checkmark.text": "I agree to the Acceptable Use Policy and the Privacy Policy", "signup.title": "Create Account | {siteName}", "single_image_view.copied_link_tooltip": "Copied", "single_image_view.copy_link_tooltip": "Copy link", diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index a88255396b9..caeb26768a6 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -38,7 +38,6 @@ import type { NotifyAdminRequest, Subscription, ValidBusinessEmail, - NewsletterRequestBody, Installation, PreviewModalContentData, } from '@mattermost/types/cloud'; @@ -4228,13 +4227,6 @@ export default class Client4 { ); }; - subscribeToNewsletter = (newletterRequestBody: NewsletterRequestBody) => { - return this.doFetch( - `${this.getHostedCustomerRoute()}/subscribe-newsletter`, - {method: 'post', body: JSON.stringify(newletterRequestBody)}, - ); - }; - cwsAvailabilityCheck = () => { return this.doFetchWithResponse( `${this.getCloudRoute()}/check-cws-connection`, diff --git a/webapp/platform/types/src/cloud.ts b/webapp/platform/types/src/cloud.ts index 71fe1dca577..ab09ec46981 100644 --- a/webapp/platform/types/src/cloud.ts +++ b/webapp/platform/types/src/cloud.ts @@ -191,11 +191,6 @@ export type ValidBusinessEmail = { is_valid: boolean; } -export interface NewsletterRequestBody { - email: string; - subscribed_content: string; -} - export const areShippingDetailsValid = (address: Address | null | undefined): boolean => { if (!address) { return false; From 777867dc361a3abb33e280b33ad373c683cd2bc9 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Fri, 23 Jan 2026 14:29:40 -0500 Subject: [PATCH 3/3] Define types for WebSocket messages and migrate WebSocket actions to TS (#34603) * Add TS definitions for every WebSocket event * Remove unused WebSocket events * Add a few extra fields to POSTED events * Stop reusing WS event types as Redux actions * Remove now-unused WS event types from mattermost-redux * Rename some types to be clearer * Use new WebSocketEvents and WebSocketMessage type everywhere * Reorganize and export named types for WS messages * Use new types in websocket_actions.jsx the best we can * Rename websocket_actions.jsx to websocket_actions.tsx * Migrate websocket_actions.tsx to TypeScript * Break up websocket_messages.ts and group together WebSocketMessages types * Rename websocket_actions.tsx to websocket_actions.ts --- server/public/model/websocket_message.go | 2 - .../src/actions/burn_on_read_websocket.ts | 16 +- .../channels/src/actions/global_actions.tsx | 2 +- webapp/channels/src/actions/new_post.ts | 19 +- .../src/actions/notification_actions.tsx | 9 +- .../channels/src/actions/post_actions.test.ts | 4 +- webapp/channels/src/actions/post_actions.ts | 9 +- .../src/actions/websocket_actions.test.jsx | 48 +- ...ocket_actions.jsx => websocket_actions.ts} | 568 +++++++++--------- .../common/hooks/useGetAgentsBridgeEnabled.ts | 6 +- .../components/logged_in/logged_in.test.tsx | 2 +- .../src/components/logged_in/logged_in.tsx | 2 +- .../src/components/msg_typing/actions.test.ts | 7 +- .../src/components/msg_typing/actions.ts | 7 +- .../src/components/msg_typing/msg_typing.tsx | 7 +- .../team_controller/team_controller.tsx | 2 +- .../src/action_types/index.ts | 2 + .../src/action_types/roles.ts | 1 - .../src/action_types/websocket.ts | 9 + .../src/actions/scheduled_posts.ts | 2 +- .../mattermost-redux/src/constants/index.ts | 3 +- .../src/constants/websocket.ts | 59 -- .../src/reducers/entities/roles.ts | 9 - .../src/reducers/entities/typing.test.ts | 26 +- .../src/reducers/entities/typing.ts | 6 +- webapp/channels/src/plugins/registry.ts | 19 +- webapp/channels/src/utils/constants.tsx | 90 +-- webapp/platform/client/src/client4.ts | 4 +- webapp/platform/client/src/index.ts | 6 +- webapp/platform/client/src/websocket.ts | 19 +- .../platform/client/src/websocket_events.ts | 93 +++ .../platform/client/src/websocket_message.ts | 117 ++++ .../platform/client/src/websocket_messages.ts | 441 ++++++++++++++ .../platform/types/src/channel_bookmarks.ts | 5 + webapp/platform/types/src/integrations.ts | 14 +- 35 files changed, 1058 insertions(+), 577 deletions(-) rename webapp/channels/src/actions/{websocket_actions.jsx => websocket_actions.ts} (75%) create mode 100644 webapp/channels/src/packages/mattermost-redux/src/action_types/websocket.ts delete mode 100644 webapp/channels/src/packages/mattermost-redux/src/constants/websocket.ts create mode 100644 webapp/platform/client/src/websocket_events.ts create mode 100644 webapp/platform/client/src/websocket_message.ts create mode 100644 webapp/platform/client/src/websocket_messages.ts diff --git a/server/public/model/websocket_message.go b/server/public/model/websocket_message.go index b8f2010ef7d..da6a14f72f0 100644 --- a/server/public/model/websocket_message.go +++ b/server/public/model/websocket_message.go @@ -50,7 +50,6 @@ const ( WebsocketEventReactionRemoved WebsocketEventType = "reaction_removed" WebsocketEventResponse WebsocketEventType = "response" WebsocketEventEmojiAdded WebsocketEventType = "emoji_added" - WebsocketEventChannelViewed WebsocketEventType = "channel_viewed" WebsocketEventMultipleChannelsViewed WebsocketEventType = "multiple_channels_viewed" WebsocketEventPluginStatusesChanged WebsocketEventType = "plugin_statuses_changed" WebsocketEventPluginEnabled WebsocketEventType = "plugin_enabled" @@ -72,7 +71,6 @@ const ( WebsocketEventSidebarCategoryUpdated WebsocketEventType = "sidebar_category_updated" WebsocketEventSidebarCategoryDeleted WebsocketEventType = "sidebar_category_deleted" WebsocketEventSidebarCategoryOrderUpdated WebsocketEventType = "sidebar_category_order_updated" - WebsocketEventCloudPaymentStatusUpdated WebsocketEventType = "cloud_payment_status_updated" WebsocketEventCloudSubscriptionChanged WebsocketEventType = "cloud_subscription_changed" WebsocketEventThreadUpdated WebsocketEventType = "thread_updated" WebsocketEventThreadFollowChanged WebsocketEventType = "thread_follow_changed" diff --git a/webapp/channels/src/actions/burn_on_read_websocket.ts b/webapp/channels/src/actions/burn_on_read_websocket.ts index dc8e231520d..1bb612a4498 100644 --- a/webapp/channels/src/actions/burn_on_read_websocket.ts +++ b/webapp/channels/src/actions/burn_on_read_websocket.ts @@ -1,25 +1,20 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {Post} from '@mattermost/types/posts'; +import type {WebSocketMessages} from '@mattermost/client'; import {PostTypes} from 'mattermost-redux/action_types'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import type {DispatchFunc, GetStateFunc} from 'types/store'; -export interface PostRevealedData { - post?: string | Post; - recipients?: string[]; -} - /** * Handles the post_revealed websocket event for burn-on-read posts. * Two scenarios: * 1. Post author: Updates recipients list for real-time recipient count tracking * 2. Revealing user: Updates post with revealed content for multi-device sync */ -export function handleBurnOnReadPostRevealed(data: PostRevealedData) { +export function handleBurnOnReadPostRevealed(data: WebSocketMessages.BurnOnReadPostRevealed['data']) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const currentUserId = getCurrentUserId(state); @@ -74,16 +69,11 @@ export function handleBurnOnReadPostRevealed(data: PostRevealedData) { }; } -export interface AllRevealedData { - post_id: string; - sender_expire_at: number; -} - /** * Handles the burn_on_read_all_revealed websocket event. * Sent to the post author when all recipients have revealed the message. */ -export function handleBurnOnReadAllRevealed(data: AllRevealedData) { +export function handleBurnOnReadAllRevealed(data: WebSocketMessages.BurnOnReadPostAllRevealed['data']) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const {post_id: postId, sender_expire_at: senderExpireAt} = data; diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx index 395fbca5635..71caf36c21c 100644 --- a/webapp/channels/src/actions/global_actions.tsx +++ b/webapp/channels/src/actions/global_actions.tsx @@ -32,7 +32,7 @@ import {loadProfilesForSidebar} from 'actions/user_actions'; import {clearUserCookie} from 'actions/views/cookie'; import {close as closeLhs} from 'actions/views/lhs'; import {closeRightHandSide, closeMenu as closeRhsMenu, updateRhsState} from 'actions/views/rhs'; -import * as WebsocketActions from 'actions/websocket_actions.jsx'; +import * as WebsocketActions from 'actions/websocket_actions'; import {getCurrentLocale} from 'selectors/i18n'; import {getIsRhsOpen, getPreviousRhsState, getRhsState} from 'selectors/rhs'; import BrowserStore from 'stores/browser_store'; diff --git a/webapp/channels/src/actions/new_post.ts b/webapp/channels/src/actions/new_post.ts index 079d3fe3c5f..5a4587cbc08 100644 --- a/webapp/channels/src/actions/new_post.ts +++ b/webapp/channels/src/actions/new_post.ts @@ -4,7 +4,7 @@ import type {AnyAction} from 'redux'; import {batchActions} from 'redux-batched-actions'; -import type {ChannelType} from '@mattermost/types/channels'; +import type {WebSocketMessages} from '@mattermost/client'; import type {Post} from '@mattermost/types/posts'; import { @@ -33,20 +33,7 @@ import {ActionTypes} from 'utils/constants'; import type {DispatchFunc, GetStateFunc, ActionFunc, ActionFuncAsync} from 'types/store'; -export type NewPostMessageProps = { - channel_type: ChannelType; - channel_display_name: string; - channel_name: string; - sender_name: string; - set_online: boolean; - mentions?: string; - followers?: string; - team_id: string; - should_ack: boolean; - otherFile?: 'true'; - image?: 'true'; - post: string; -} +export type NewPostMessageProps = Partial; export function completePostReceive(post: Post, websocketMessageProps: NewPostMessageProps, fetchedChannelMember?: boolean): ActionFuncAsync { return async (dispatch, getState) => { @@ -146,7 +133,7 @@ export function setChannelReadAndViewed(dispatch: DispatchFunc, getState: GetSta return actionsToMarkChannelAsRead(getState, post.channel_id); } - return actionsToMarkChannelAsUnread(getState, websocketMessageProps.team_id, post.channel_id, websocketMessageProps.mentions || '', fetchedChannelMember, post.root_id === '', post?.metadata?.priority?.priority); + return actionsToMarkChannelAsUnread(getState, websocketMessageProps.team_id || '', post.channel_id, websocketMessageProps.mentions || '', fetchedChannelMember, post.root_id === '', post?.metadata?.priority?.priority); } export function setThreadRead(post: Post): ActionFunc { diff --git a/webapp/channels/src/actions/notification_actions.tsx b/webapp/channels/src/actions/notification_actions.tsx index f883152c717..cf4567a0945 100644 --- a/webapp/channels/src/actions/notification_actions.tsx +++ b/webapp/channels/src/actions/notification_actions.tsx @@ -106,14 +106,14 @@ export function sendDesktopNotification(post: Post, msgProps: NewPostMessageProp return async (dispatch, getState) => { const state = getState(); - const teamId = msgProps.team_id; + const teamId = msgProps.team_id || ''; const channel = makeGetChannel()(state, post.channel_id) || { id: post.channel_id, name: msgProps.channel_name, display_name: msgProps.channel_display_name, type: msgProps.channel_type, - }; + } as Channel; const user = getCurrentUser(state); const member = getMyChannelMember(state, post.channel_id); const isCrtReply = isCollapsedThreadsEnabled(state) && post.root_id !== ''; @@ -185,7 +185,7 @@ const getNotificationTitle = (channel: Pick, m if (msgProps.channel_type === Constants.DM_CHANNEL) { title = Utils.localizeMessage({id: 'notification.dm', defaultMessage: 'Direct Message'}); } else { - title = msgProps.channel_display_name; + title = msgProps.channel_display_name || ''; } } @@ -218,8 +218,7 @@ const getNotificationBody = (state: GlobalState, post: Post, msgProps: NewPostMe let notifyText = post.message; - const msgPropsPost: Post = JSON.parse(msgProps.post); - const attachments = isMessageAttachmentArray(msgPropsPost?.props?.attachments) ? msgPropsPost.props.attachments : []; + const attachments = isMessageAttachmentArray(post.props?.attachments) ? post.props.attachments : []; let image = false; attachments.forEach((attachment) => { if (notifyText.length === 0) { diff --git a/webapp/channels/src/actions/post_actions.test.ts b/webapp/channels/src/actions/post_actions.test.ts index 3059cf6e947..01c1a0306a4 100644 --- a/webapp/channels/src/actions/post_actions.test.ts +++ b/webapp/channels/src/actions/post_actions.test.ts @@ -244,7 +244,7 @@ describe('Actions.Posts', () => { channelId: 'other_channel_id', fetchedChannelMember: false, onlyMentions: undefined, - teamId: undefined, + teamId: '', }, }, { @@ -263,7 +263,7 @@ describe('Actions.Posts', () => { amountUrgent: 0, channelId: 'other_channel_id', fetchedChannelMember: false, - teamId: undefined, + teamId: '', }, }, ], diff --git a/webapp/channels/src/actions/post_actions.ts b/webapp/channels/src/actions/post_actions.ts index 0d463184308..b09a38ab543 100644 --- a/webapp/channels/src/actions/post_actions.ts +++ b/webapp/channels/src/actions/post_actions.ts @@ -4,8 +4,8 @@ import type {AnyAction} from 'redux'; import {batchActions} from 'redux-batched-actions'; +import type {WebSocketMessages} from '@mattermost/client'; import type {FileInfo} from '@mattermost/types/files'; -import type {GroupChannel} from '@mattermost/types/groups'; import type {Post} from '@mattermost/types/posts'; import type {ScheduledPost} from '@mattermost/types/schedule_post'; @@ -58,7 +58,6 @@ import type { import type {PostDraft} from 'types/store/draft'; import type {StorageItem} from 'types/store/storage'; -import type {NewPostMessageProps} from './new_post'; import {completePostReceive} from './new_post'; import type {OnSubmitOptions, SubmitPostReturnType} from './views/create_comment'; @@ -67,7 +66,7 @@ export type CreatePostOptions = { ignorePostError?: boolean; } -export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & GroupChannel}): ActionFuncAsync { +export function handleNewPost(post: Post, msg?: WebSocketMessages.Posted | WebSocketMessages.EphemeralPost): ActionFuncAsync { return async (dispatch, getState) => { let websocketMessageProps = {}; const state = getState(); @@ -82,9 +81,9 @@ export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & Gr await dispatch(getMyChannelMember(post.channel_id)); } - dispatch(completePostReceive(post, websocketMessageProps as NewPostMessageProps, myChannelMemberDoesntExist)); + dispatch(completePostReceive(post, websocketMessageProps, myChannelMemberDoesntExist)); - if (msg && msg.data) { + if (msg && msg.data && 'channel_type' in msg.data) { if (msg.data.channel_type === Constants.DM_CHANNEL) { dispatch(loadNewDMIfNeeded(post.channel_id)); } else if (msg.data.channel_type === Constants.GM_CHANNEL) { diff --git a/webapp/channels/src/actions/websocket_actions.test.jsx b/webapp/channels/src/actions/websocket_actions.test.jsx index 91d2de49358..2ae198636a9 100644 --- a/webapp/channels/src/actions/websocket_actions.test.jsx +++ b/webapp/channels/src/actions/websocket_actions.test.jsx @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {WebSocketEvents} from '@mattermost/client'; + import {CloudTypes} from 'mattermost-redux/action_types'; import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories'; import {fetchAllMyTeamsChannels} from 'mattermost-redux/actions/channels'; @@ -24,7 +26,7 @@ import store from 'stores/redux_store'; import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; import configureStore from 'tests/test_store'; import {getHistory} from 'utils/browser_history'; -import Constants, {SocketEvents, ActionTypes, UserStatuses} from 'utils/constants'; +import Constants, {ActionTypes, UserStatuses} from 'utils/constants'; import { handleChannelUpdatedEvent, @@ -230,7 +232,7 @@ jest.mock('actions/views/rhs', () => ({ describe('handleEvent', () => { test('should dispatch channel updated event properly', () => { - const msg = {event: SocketEvents.CHANNEL_UPDATED}; + const msg = {event: WebSocketEvents.ChannelUpdated}; handleEvent(msg); @@ -861,7 +863,7 @@ describe('handleCloudSubscriptionChanged', () => { id: 'newsub', }; const msg = { - event: SocketEvents.CLOUD_PRODUCT_LIMITS_CHANGED, + event: WebSocketEvents.CloudSubscriptionChanged, data: { limits: newLimits, subscription: newSubscription, @@ -900,7 +902,7 @@ describe('handleCloudSubscriptionChanged', () => { }, }; const msg = { - event: SocketEvents.CLOUD_PRODUCT_LIMITS_CHANGED, + event: WebSocketEvents.CloudSubscriptionChanged, data: { limits: newLimits, }, @@ -934,7 +936,7 @@ describe('handleCloudSubscriptionChanged', () => { }; const msg = { - event: SocketEvents.CLOUD_PRODUCT_LIMITS_CHANGED, + event: WebSocketEvents.CloudSubscriptionChanged, data: { subscription: newSubscription, }, @@ -1294,7 +1296,7 @@ describe('handleStatusChangedEvent', () => { expect(getStatusForUserId(testStore.getState(), currentUserId)).toBe(UserStatuses.ONLINE); testStore.dispatch(handleStatusChangedEvent({ - event: SocketEvents.STATUS_CHANGED, + event: WebSocketEvents.StatusChange, data: { user_id: currentUserId, status: UserStatuses.AWAY, @@ -1304,7 +1306,7 @@ describe('handleStatusChangedEvent', () => { expect(getStatusForUserId(testStore.getState(), currentUserId)).toBe(UserStatuses.AWAY); testStore.dispatch(handleStatusChangedEvent({ - event: SocketEvents.STATUS_CHANGED, + event: WebSocketEvents.StatusChange, data: { user_id: currentUserId, status: UserStatuses.ONLINE, @@ -1314,7 +1316,7 @@ describe('handleStatusChangedEvent', () => { expect(getStatusForUserId(testStore.getState(), currentUserId)).toBe(UserStatuses.ONLINE); testStore.dispatch(handleStatusChangedEvent({ - event: SocketEvents.STATUS_CHANGED, + event: WebSocketEvents.StatusChange, data: { user_id: currentUserId, status: UserStatuses.OFFLINE, @@ -1346,7 +1348,7 @@ describe('handleCustomAttributeValuesUpdated', () => { expect(stateUser(testStore.getState(), currentUserId)).toEqual({id: currentUserId}); testStore.dispatch(handleCustomAttributeValuesUpdated({ - event: SocketEvents.CPA_VALUES_UPDATED, + event: WebSocketEvents.CPAValuesUpdated, data: { user_id: currentUserId, values: {field1: 'value1', field2: 'value2'}, @@ -1359,7 +1361,7 @@ describe('handleCustomAttributeValuesUpdated', () => { // update one field, add new field testStore.dispatch(handleCustomAttributeValuesUpdated({ - event: SocketEvents.CPA_VALUES_UPDATED, + event: WebSocketEvents.CPAValuesUpdated, data: { user_id: currentUserId, values: {field1: 'valueChanged', field3: 'new field'}, @@ -1378,7 +1380,7 @@ describe('handleCustomAttributeValuesUpdated', () => { expect(stateUser(testStore.getState(), currentUserId)).toEqual({id: currentUserId}); testStore.dispatch(handleCustomAttributeValuesUpdated({ - event: SocketEvents.CPA_VALUES_UPDATED, + event: WebSocketEvents.CPAValuesUpdated, data: { user_id: 'nonExistantUser', values: {field1: 'value1', field2: 'value2'}, @@ -1408,7 +1410,7 @@ describe('handleCustomAttributeCRUD', () => { const testStore = realConfigureStore(makeInitialState()); testStore.dispatch(handleCustomAttributesCreated({ - event: SocketEvents.CPA_FIELD_CREATED, + event: WebSocketEvents.CPAFieldCreated, data: { field: field1, }, @@ -1422,7 +1424,7 @@ describe('handleCustomAttributeCRUD', () => { // create second field testStore.dispatch(handleCustomAttributesCreated({ - event: SocketEvents.CPA_FIELD_CREATED, + event: WebSocketEvents.CPAFieldCreated, data: { field: field2, }, @@ -1436,7 +1438,7 @@ describe('handleCustomAttributeCRUD', () => { // update field testStore.dispatch(handleCustomAttributesUpdated({ - event: SocketEvents.CPA_FIELD_UPDATED, + event: WebSocketEvents.CPAFieldUpdated, data: { field: {...field1, name: 'Updated Name'}, }, @@ -1450,7 +1452,7 @@ describe('handleCustomAttributeCRUD', () => { // delete field testStore.dispatch(handleCustomAttributesDeleted({ - event: SocketEvents.CPA_FIELD_DELETED, + event: WebSocketEvents.CPAFieldDeleted, data: { field_id: field1.id, }, @@ -1468,7 +1470,7 @@ describe('handleCustomAttributeCRUD', () => { // First create a field testStore.dispatch(handleCustomAttributesCreated({ - event: SocketEvents.CPA_FIELD_CREATED, + event: WebSocketEvents.CPAFieldCreated, data: { field: field1, }, @@ -1482,7 +1484,7 @@ describe('handleCustomAttributeCRUD', () => { // Update the field const updatedField = {...field1, name: 'Updated Field Name'}; testStore.dispatch(handleCustomAttributesUpdated({ - event: SocketEvents.CPA_FIELD_UPDATED, + event: WebSocketEvents.CPAFieldUpdated, data: { field: updatedField, }, @@ -1514,7 +1516,7 @@ describe('handleCustomAttributeCRUD', () => { // First create a field testStore.dispatch(handleCustomAttributesCreated({ - event: SocketEvents.CPA_FIELD_CREATED, + event: WebSocketEvents.CPAFieldCreated, data: { field: field1, }, @@ -1523,7 +1525,7 @@ describe('handleCustomAttributeCRUD', () => { // Update the field with delete_values flag const updatedField = {...field1, type: 'select'}; testStore.dispatch(handleCustomAttributesUpdated({ - event: SocketEvents.CPA_FIELD_UPDATED, + event: WebSocketEvents.CPAFieldUpdated, data: { field: updatedField, delete_values: true, @@ -1561,7 +1563,7 @@ describe('handleCustomAttributeCRUD', () => { // First create a field testStore.dispatch(handleCustomAttributesCreated({ - event: SocketEvents.CPA_FIELD_CREATED, + event: WebSocketEvents.CPAFieldCreated, data: { field: field1, }, @@ -1570,7 +1572,7 @@ describe('handleCustomAttributeCRUD', () => { // Update the field but with delete_values flag set to false const updatedField = {...field1, name: 'Updated Field Name', type: 'text'}; testStore.dispatch(handleCustomAttributesUpdated({ - event: SocketEvents.CPA_FIELD_UPDATED, + event: WebSocketEvents.CPAFieldUpdated, data: { field: updatedField, delete_values: false, @@ -1609,7 +1611,7 @@ describe('handleCustomAttributeCRUD', () => { // First create a field testStore.dispatch(handleCustomAttributesCreated({ - event: SocketEvents.CPA_FIELD_CREATED, + event: WebSocketEvents.CPAFieldCreated, data: { field: field1, }, @@ -1618,7 +1620,7 @@ describe('handleCustomAttributeCRUD', () => { // Update the field without specifying delete_values const updatedField = {...field1, type: 'number'}; testStore.dispatch(handleCustomAttributesUpdated({ - event: SocketEvents.CPA_FIELD_UPDATED, + event: WebSocketEvents.CPAFieldUpdated, data: { field: updatedField, diff --git a/webapp/channels/src/actions/websocket_actions.jsx b/webapp/channels/src/actions/websocket_actions.ts similarity index 75% rename from webapp/channels/src/actions/websocket_actions.jsx rename to webapp/channels/src/actions/websocket_actions.ts index 1d8f868c1cd..d1837a6fa83 100644 --- a/webapp/channels/src/actions/websocket_actions.jsx +++ b/webapp/channels/src/actions/websocket_actions.ts @@ -5,6 +5,23 @@ import {batchActions} from 'redux-batched-actions'; +import type {WebSocketMessage, WebSocketMessages} from '@mattermost/client'; +import {WebSocketEvents} from '@mattermost/client'; +import type {ChannelBookmarkWithFileInfo, UpdateChannelBookmarkResponse} from '@mattermost/types/channel_bookmarks'; +import type {Channel, ChannelMembership} from '@mattermost/types/channels'; +import type {Draft} from '@mattermost/types/drafts'; +import type {Emoji} from '@mattermost/types/emojis'; +import type {Group, GroupMember} from '@mattermost/types/groups'; +import type {OpenDialogRequest} from '@mattermost/types/integrations'; +import type {Post, PostAcknowledgement} from '@mattermost/types/posts'; +import type {PreferenceType} from '@mattermost/types/preferences'; +import type {Reaction} from '@mattermost/types/reactions'; +import type {Role} from '@mattermost/types/roles'; +import type {ScheduledPost} from '@mattermost/types/schedule_post'; +import type {Team, TeamMembership} from '@mattermost/types/teams'; +import type {UserThread} from '@mattermost/types/threads'; + +import type {MMReduxAction} from 'mattermost-redux/action_types'; import { ChannelTypes, EmojiTypes, @@ -19,7 +36,6 @@ import { PreferenceTypes, AppsTypes, CloudTypes, - HostedCustomerTypes, ChannelBookmarkTypes, ScheduledPostTypes, ContentFlaggingTypes, @@ -37,7 +53,6 @@ import { fetchAllMyTeamsChannels, fetchChannelsAndMembers, } from 'mattermost-redux/actions/channels'; -import {getCloudSubscription} from 'mattermost-redux/actions/cloud'; import {clearErrors, logError} from 'mattermost-redux/actions/errors'; import {setServerVersion, getClientConfig, getCustomProfileAttributeFields} from 'mattermost-redux/actions/general'; import {getGroup as fetchGroup} from 'mattermost-redux/actions/groups'; @@ -118,7 +133,7 @@ import {syncPostsInChannel} from 'actions/views/channel'; import {setGlobalDraft, transformServerDraft} from 'actions/views/drafts'; import {openModal} from 'actions/views/modals'; import {closeRightHandSide} from 'actions/views/rhs'; -import {incrementWsErrorCount, resetWsErrorCount} from 'actions/views/system'; +import {resetWsErrorCount} from 'actions/views/system'; import {updateThreadLastOpened} from 'actions/views/threads'; import {getSelectedChannelId, getSelectedPost} from 'selectors/rhs'; import {isThreadOpen, isThreadManuallyUnread} from 'selectors/views/threads'; @@ -133,6 +148,8 @@ import {getHistory} from 'utils/browser_history'; import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatuses, ModalIdentifiers, PageLoadContext} from 'utils/constants'; import {getSiteURL} from 'utils/url'; +import type {ActionFunc, ThunkActionFunc} from 'types/store'; + import {temporarilySetPageLoadContext} from './telemetry_actions'; const dispatch = store.dispatch; @@ -140,7 +157,7 @@ const getState = store.getState; const MAX_WEBSOCKET_FAILS = 7; -const pluginEventHandlers = {}; +const pluginEventHandlers: Record void>> = {}; export function initialize() { if (!window.WebSocket) { @@ -157,25 +174,25 @@ export function initialize() { if (config.WebsocketURL) { connUrl = config.WebsocketURL; } else { - connUrl = new URL(getSiteURL()); + const url = new URL(getSiteURL()); // replace the protocol with a websocket one - if (connUrl.protocol === 'https:') { - connUrl.protocol = 'wss:'; + if (url.protocol === 'https:') { + url.protocol = 'wss:'; } else { - connUrl.protocol = 'ws:'; + url.protocol = 'ws:'; } // append a port number if one isn't already specified - if (!(/:\d+$/).test(connUrl.host)) { - if (connUrl.protocol === 'wss:') { - connUrl.host += ':' + config.WebsocketSecurePort; + if (!(/:\d+$/).test(url.host)) { + if (url.protocol === 'wss:') { + url.host += ':' + config.WebsocketSecurePort; } else { - connUrl.host += ':' + config.WebsocketPort; + url.host += ':' + config.WebsocketPort; } } - connUrl = connUrl.toString(); + connUrl = url.toString(); } // Strip any trailing slash before appending the pathname below. @@ -204,13 +221,13 @@ export function close() { WebSocketClient.removeCloseListener(handleClose); } -const pluginReconnectHandlers = {}; +const pluginReconnectHandlers: Record void> = {}; -export function registerPluginReconnectHandler(pluginId, handler) { +export function registerPluginReconnectHandler(pluginId: string, handler: () => void) { pluginReconnectHandlers[pluginId] = handler; } -export function unregisterPluginReconnectHandler(pluginId) { +export function unregisterPluginReconnectHandler(pluginId: string) { Reflect.deleteProperty(pluginReconnectHandlers, pluginId); } @@ -238,7 +255,7 @@ export function reconnect() { const currentUserId = getCurrentUserId(state); const currentChannelId = getCurrentChannelId(state); const mostRecentId = getMostRecentPostIdInChannel(state, currentChannelId); - const mostRecentPost = getPost(state, mostRecentId); + const mostRecentPost = mostRecentId && getPost(state, mostRecentId); if (appsEnabled(state)) { dispatch(handleRefreshAppsBindings()); @@ -301,7 +318,7 @@ export function reconnect() { dispatch(clearErrors()); } -function syncThreads(teamId, userId) { +function syncThreads(teamId: string, userId: string) { const state = getState(); const newestThread = getNewestThreadInTeam(state, teamId); @@ -312,14 +329,14 @@ function syncThreads(teamId, userId) { dispatch(getCountsAndThreadsSince(userId, teamId, newestThread.last_reply_at)); } -export function registerPluginWebSocketEvent(pluginId, event, action) { +export function registerPluginWebSocketEvent(pluginId: string, event: string, action: (msg: WebSocketMessages.Unknown) => void) { if (!pluginEventHandlers[pluginId]) { pluginEventHandlers[pluginId] = {}; } pluginEventHandlers[pluginId][event] = action; } -export function unregisterPluginWebSocketEvent(pluginId, event) { +export function unregisterPluginWebSocketEvent(pluginId: string, event: string) { const events = pluginEventHandlers[pluginId]; if (!events) { return; @@ -328,7 +345,7 @@ export function unregisterPluginWebSocketEvent(pluginId, event) { Reflect.deleteProperty(events, event); } -export function unregisterAllPluginWebSocketEvents(pluginId) { +export function unregisterAllPluginWebSocketEvents(pluginId: string) { Reflect.deleteProperty(pluginEventHandlers, pluginId); } @@ -342,275 +359,269 @@ function handleFirstConnect() { ])); } -function handleClose(failCount) { +function handleClose(failCount: number) { if (failCount > MAX_WEBSOCKET_FAILS) { - dispatch(logError({type: 'critical', message: AnnouncementBarMessages.WEBSOCKET_PORT_ERROR}, true)); + dispatch(logError({type: 'critical', message: AnnouncementBarMessages.WEBSOCKET_PORT_ERROR})); } dispatch(batchActions([ { type: GeneralTypes.WEBSOCKET_FAILURE, timestamp: Date.now(), }, - incrementWsErrorCount(), + + // TODO The accompanying logic causes the post textbox to turn yellow when there are WebSocket issues, + // and it's been broken since https://github.com/mattermost/mattermost-webapp/pull/2981. Either this and the + // batchActions should be removed, or we should fix this by changing incrementWsErrorCount to be a non-thunk + // action. + // incrementWsErrorCount(), ])); } -export function handleEvent(msg) { +export function handleEvent(msg: WebSocketMessage) { switch (msg.event) { - case SocketEvents.POSTED: - case SocketEvents.EPHEMERAL_MESSAGE: + case WebSocketEvents.Posted: + case WebSocketEvents.EphemeralMessage: handleNewPostEventDebounced(msg); break; - case SocketEvents.POST_EDITED: + case WebSocketEvents.PostEdited: handlePostEditEvent(msg); break; - case SocketEvents.POST_DELETED: + case WebSocketEvents.PostDeleted: handlePostDeleteEvent(msg); break; - case SocketEvents.POST_UNREAD: + case WebSocketEvents.PostUnread: handlePostUnreadEvent(msg); break; - case SocketEvents.BURN_ON_READ_POST_REVEALED: + case WebSocketEvents.BurnOnReadPostRevealed: dispatch(handleBurnOnReadPostRevealed(msg.data)); break; - case SocketEvents.BURN_ON_READ_POST_BURNED: + case WebSocketEvents.BurnOnReadPostBurned: dispatch(handlePostExpired(msg.data.post_id)); break; - case SocketEvents.BURN_ON_READ_ALL_REVEALED: + case WebSocketEvents.BurnOnReadPostAllRevealed: dispatch(handleBurnOnReadAllRevealed(msg.data)); break; - case SocketEvents.LEAVE_TEAM: + case WebSocketEvents.LeaveTeam: handleLeaveTeamEvent(msg); break; - case SocketEvents.UPDATE_TEAM: + case WebSocketEvents.UpdateTeam: handleUpdateTeamEvent(msg); break; - case SocketEvents.UPDATE_TEAM_SCHEME: - handleUpdateTeamSchemeEvent(msg); + case WebSocketEvents.UpdateTeamScheme: + handleUpdateTeamSchemeEvent(); break; - case SocketEvents.DELETE_TEAM: + case WebSocketEvents.DeleteTeam: handleDeleteTeamEvent(msg); break; - case SocketEvents.ADDED_TO_TEAM: + case WebSocketEvents.AddedToTeam: handleTeamAddedEvent(msg); break; - case SocketEvents.USER_ADDED: + case WebSocketEvents.UserAdded: dispatch(handleUserAddedEvent(msg)); break; - case SocketEvents.USER_REMOVED: + case WebSocketEvents.UserRemoved: handleUserRemovedEvent(msg); break; - case SocketEvents.USER_UPDATED: + case WebSocketEvents.UserUpdated: handleUserUpdatedEvent(msg); break; - case SocketEvents.ROLE_ADDED: - handleRoleAddedEvent(msg); - break; - - case SocketEvents.ROLE_REMOVED: - handleRoleRemovedEvent(msg); - break; - - case SocketEvents.CHANNEL_SCHEME_UPDATED: + case WebSocketEvents.ChannelSchemeUpdated: handleChannelSchemeUpdatedEvent(msg); break; - case SocketEvents.MEMBERROLE_UPDATED: + case WebSocketEvents.MemberRoleUpdated: handleUpdateMemberRoleEvent(msg); break; - case SocketEvents.ROLE_UPDATED: + case WebSocketEvents.RoleUpdated: handleRoleUpdatedEvent(msg); break; - case SocketEvents.CHANNEL_CREATED: + case WebSocketEvents.ChannelCreated: dispatch(handleChannelCreatedEvent(msg)); break; - case SocketEvents.CHANNEL_DELETED: + case WebSocketEvents.ChannelDeleted: handleChannelDeletedEvent(msg); break; - case SocketEvents.CHANNEL_UNARCHIVED: + case WebSocketEvents.ChannelRestored: handleChannelUnarchivedEvent(msg); break; - case SocketEvents.CHANNEL_CONVERTED: + case WebSocketEvents.ChannelConverted: handleChannelConvertedEvent(msg); break; - case SocketEvents.CHANNEL_UPDATED: + case WebSocketEvents.ChannelUpdated: dispatch(handleChannelUpdatedEvent(msg)); break; - case SocketEvents.CHANNEL_MEMBER_UPDATED: + case WebSocketEvents.ChannelMemberUpdated: handleChannelMemberUpdatedEvent(msg); break; - case SocketEvents.CHANNEL_BOOKMARK_CREATED: + case WebSocketEvents.ChannelBookmarkCreated: dispatch(handleChannelBookmarkCreated(msg)); break; - case SocketEvents.CHANNEL_BOOKMARK_UPDATED: + case WebSocketEvents.ChannelBookmarkUpdated: dispatch(handleChannelBookmarkUpdated(msg)); break; - case SocketEvents.CHANNEL_BOOKMARK_DELETED: + case WebSocketEvents.ChannelBookmarkDeleted: dispatch(handleChannelBookmarkDeleted(msg)); break; - case SocketEvents.CHANNEL_BOOKMARK_SORTED: + case WebSocketEvents.ChannelBookmarkSorted: dispatch(handleChannelBookmarkSorted(msg)); break; - case SocketEvents.DIRECT_ADDED: + case WebSocketEvents.DirectAdded: dispatch(handleDirectAddedEvent(msg)); break; - case SocketEvents.GROUP_ADDED: + case WebSocketEvents.GroupAdded: dispatch(handleGroupAddedEvent(msg)); break; - case SocketEvents.PREFERENCE_CHANGED: + case WebSocketEvents.PreferenceChanged: handlePreferenceChangedEvent(msg); break; - case SocketEvents.PREFERENCES_CHANGED: + case WebSocketEvents.PreferencesChanged: handlePreferencesChangedEvent(msg); break; - case SocketEvents.PREFERENCES_DELETED: + case WebSocketEvents.PreferencesDeleted: handlePreferencesDeletedEvent(msg); break; - case SocketEvents.STATUS_CHANGED: + case WebSocketEvents.StatusChange: dispatch(handleStatusChangedEvent(msg)); break; - case SocketEvents.HELLO: + case WebSocketEvents.Hello: handleHelloEvent(msg); break; - case SocketEvents.REACTION_ADDED: + case WebSocketEvents.ReactionAdded: handleReactionAddedEvent(msg); break; - case SocketEvents.REACTION_REMOVED: + case WebSocketEvents.ReactionRemoved: handleReactionRemovedEvent(msg); break; - case SocketEvents.EMOJI_ADDED: + case WebSocketEvents.EmojiAdded: handleAddEmoji(msg); break; - case SocketEvents.MULTIPLE_CHANNELS_VIEWED: + case WebSocketEvents.MultipleChannelsViewed: handleMultipleChannelsViewedEvent(msg); break; - case SocketEvents.PLUGIN_ENABLED: + case WebSocketEvents.PluginEnabled: handlePluginEnabled(msg); break; - case SocketEvents.PLUGIN_DISABLED: + case WebSocketEvents.PluginDisabled: handlePluginDisabled(msg); break; - case SocketEvents.USER_ROLE_UPDATED: + case WebSocketEvents.UserRoleUpdated: handleUserRoleUpdated(msg); break; - case SocketEvents.CONFIG_CHANGED: + case WebSocketEvents.ConfigChanged: handleConfigChanged(msg); break; - case SocketEvents.LICENSE_CHANGED: + case WebSocketEvents.LicenseChanged: handleLicenseChanged(msg); break; - case SocketEvents.PLUGIN_STATUSES_CHANGED: + case WebSocketEvents.PluginStatusesChanged: handlePluginStatusesChangedEvent(msg); break; - case SocketEvents.OPEN_DIALOG: + case WebSocketEvents.OpenDialog: handleOpenDialogEvent(msg); break; - case SocketEvents.RECEIVED_GROUP: + case WebSocketEvents.ReceivedGroup: handleGroupUpdatedEvent(msg); break; - case SocketEvents.GROUP_MEMBER_ADD: + case WebSocketEvents.GroupMemberAdded: dispatch(handleGroupAddedMemberEvent(msg)); break; - case SocketEvents.GROUP_MEMBER_DELETED: + case WebSocketEvents.GroupMemberDeleted: dispatch(handleGroupDeletedMemberEvent(msg)); break; - case SocketEvents.RECEIVED_GROUP_ASSOCIATED_TO_TEAM: + case WebSocketEvents.ReceivedGroupAssociatedToTeam: handleGroupAssociatedToTeamEvent(msg); break; - case SocketEvents.RECEIVED_GROUP_NOT_ASSOCIATED_TO_TEAM: + case WebSocketEvents.ReceivedGroupNotAssociatedToTeam: handleGroupNotAssociatedToTeamEvent(msg); break; - case SocketEvents.RECEIVED_GROUP_ASSOCIATED_TO_CHANNEL: + case WebSocketEvents.ReceivedGroupAssociatedToChannel: handleGroupAssociatedToChannelEvent(msg); break; - case SocketEvents.RECEIVED_GROUP_NOT_ASSOCIATED_TO_CHANNEL: + case WebSocketEvents.ReceivedGroupNotAssociatedToChannel: handleGroupNotAssociatedToChannelEvent(msg); break; - case SocketEvents.SIDEBAR_CATEGORY_CREATED: + case WebSocketEvents.SidebarCategoryCreated: dispatch(handleSidebarCategoryCreated(msg)); break; - case SocketEvents.SIDEBAR_CATEGORY_UPDATED: + case WebSocketEvents.SidebarCategoryUpdated: dispatch(handleSidebarCategoryUpdated(msg)); break; - case SocketEvents.SIDEBAR_CATEGORY_DELETED: + case WebSocketEvents.SidebarCategoryDeleted: dispatch(handleSidebarCategoryDeleted(msg)); break; - case SocketEvents.SIDEBAR_CATEGORY_ORDER_UPDATED: + case WebSocketEvents.SidebarCategoryOrderUpdated: dispatch(handleSidebarCategoryOrderUpdated(msg)); break; - case SocketEvents.USER_ACTIVATION_STATUS_CHANGED: + case WebSocketEvents.UserActivationStatusChange: dispatch(handleUserActivationStatusChange()); break; - case SocketEvents.CLOUD_PAYMENT_STATUS_UPDATED: - dispatch(handleCloudPaymentStatusUpdated(msg)); - break; - case SocketEvents.CLOUD_SUBSCRIPTION_CHANGED: + case WebSocketEvents.CloudSubscriptionChanged: dispatch(handleCloudSubscriptionChanged(msg)); break; - case SocketEvents.FIRST_ADMIN_VISIT_MARKETPLACE_STATUS_RECEIVED: + case WebSocketEvents.FirstAdminVisitMarketplaceStatusReceived: handleFirstAdminVisitMarketplaceStatusReceivedEvent(msg); break; - case SocketEvents.THREAD_FOLLOW_CHANGED: + case WebSocketEvents.ThreadFollowChanged: dispatch(handleThreadFollowChanged(msg)); break; - case SocketEvents.THREAD_READ_CHANGED: + case WebSocketEvents.ThreadReadChanged: dispatch(handleThreadReadChanged(msg)); break; - case SocketEvents.THREAD_UPDATED: + case WebSocketEvents.ThreadUpdated: dispatch(handleThreadUpdated(msg)); break; case SocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS: @@ -622,50 +633,47 @@ export function handleEvent(msg) { case SocketEvents.APPS_FRAMEWORK_PLUGIN_DISABLED: dispatch(handleAppsPluginDisabled()); break; - case SocketEvents.POST_ACKNOWLEDGEMENT_ADDED: + case WebSocketEvents.PostAcknowledgementAdded: dispatch(handlePostAcknowledgementAdded(msg)); break; - case SocketEvents.POST_ACKNOWLEDGEMENT_REMOVED: + case WebSocketEvents.PostAcknowledgementRemoved: dispatch(handlePostAcknowledgementRemoved(msg)); break; - case SocketEvents.DRAFT_CREATED: - case SocketEvents.DRAFT_UPDATED: + case WebSocketEvents.DraftCreated: + case WebSocketEvents.DraftUpdated: dispatch(handleUpsertDraftEvent(msg)); break; - case SocketEvents.DRAFT_DELETED: + case WebSocketEvents.DraftDeleted: dispatch(handleDeleteDraftEvent(msg)); break; - case SocketEvents.SCHEDULED_POST_CREATED: + case WebSocketEvents.ScheduledPostCreated: dispatch(handleCreateScheduledPostEvent(msg)); break; - case SocketEvents.SCHEDULED_POST_UPDATED: + case WebSocketEvents.ScheduledPostUpdated: dispatch(handleUpdateScheduledPostEvent(msg)); break; - case SocketEvents.SCHEDULED_POST_DELETED: + case WebSocketEvents.ScheduledPostDeleted: dispatch(handleDeleteScheduledPostEvent(msg)); break; - case SocketEvents.PERSISTENT_NOTIFICATION_TRIGGERED: + case WebSocketEvents.PersistentNotificationTriggered: dispatch(handlePersistentNotification(msg)); break; - case SocketEvents.HOSTED_CUSTOMER_SIGNUP_PROGRESS_UPDATED: - dispatch(handleHostedCustomerSignupProgressUpdated(msg)); - break; - case SocketEvents.CPA_VALUES_UPDATED: + case WebSocketEvents.CPAValuesUpdated: dispatch(handleCustomAttributeValuesUpdated(msg)); break; - case SocketEvents.CPA_FIELD_CREATED: + case WebSocketEvents.CPAFieldCreated: dispatch(handleCustomAttributesCreated(msg)); break; - case SocketEvents.CPA_FIELD_UPDATED: + case WebSocketEvents.CPAFieldUpdated: dispatch(handleCustomAttributesUpdated(msg)); break; - case SocketEvents.CPA_FIELD_DELETED: + case WebSocketEvents.CPAFieldDeleted: dispatch(handleCustomAttributesDeleted(msg)); break; - case SocketEvents.CONTENT_FLAGGING_REPORT_VALUE_CHANGED: + case WebSocketEvents.ContentFlaggingReportValueUpdated: dispatch(handleContentFlaggingReportValueChanged(msg)); break; - case SocketEvents.RECAP_UPDATED: + case WebSocketEvents.RecapUpdated: dispatch(handleRecapUpdated(msg)); break; default: @@ -683,7 +691,7 @@ export function handleEvent(msg) { } // handleChannelConvertedEvent handles updating of channel which is converted from public to private -function handleChannelConvertedEvent(msg) { +function handleChannelConvertedEvent(msg: WebSocketMessages.ChannelConverted) { const channelId = msg.data.channel_id; if (channelId) { const channel = getChannel(getState(), channelId); @@ -696,11 +704,15 @@ function handleChannelConvertedEvent(msg) { } } -export function handleChannelUpdatedEvent(msg) { +export function handleChannelUpdatedEvent(msg: WebSocketMessages.ChannelUpdated): ThunkActionFunc { return (doDispatch, doGetState) => { - const channel = JSON.parse(msg.data.channel); + if (!msg.data.channel) { + return; + } + + const channel = JSON.parse(msg.data.channel) as Channel; - const actions = [{type: ChannelTypes.RECEIVED_CHANNEL, data: channel}]; + const actions: MMReduxAction[] = [{type: ChannelTypes.RECEIVED_CHANNEL, data: channel}]; // handling the case of GM converted to private channel. const state = doGetState(); @@ -724,21 +736,21 @@ export function handleChannelUpdatedEvent(msg) { }; } -function handleChannelMemberUpdatedEvent(msg) { - const channelMember = JSON.parse(msg.data.channelMember); +function handleChannelMemberUpdatedEvent(msg: WebSocketMessages.ChannelMemberUpdated) { + const channelMember = JSON.parse(msg.data.channelMember) as ChannelMembership; const roles = channelMember.roles.split(' '); dispatch(loadRolesIfNeeded(roles)); dispatch({type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: channelMember}); } -function debouncePostEvent(wait) { - let timeout; - let queue = []; +function debouncePostEvent(wait: number) { + let timeout: number | undefined; + let queue: Array = []; let count = 0; // Called when timeout triggered const triggered = () => { - timeout = null; + timeout = undefined; if (queue.length > 0) { dispatch(handleNewPostEvents(queue)); @@ -748,7 +760,7 @@ function debouncePostEvent(wait) { count = 0; }; - return function fx(msg) { + return function fx(msg: WebSocketMessages.Posted | WebSocketMessages.EphemeralPost) { if (timeout && count > 4) { // If the timeout is going this is the second or further event so queue them up. if (queue.push(msg) > 200) { @@ -757,24 +769,24 @@ function debouncePostEvent(wait) { console.log('channel broken because of too many incoming messages'); //eslint-disable-line no-console } clearTimeout(timeout); - timeout = setTimeout(triggered, wait); + timeout = window.setTimeout(triggered, wait); } else { // Apply immediately for events up until count reaches limit count += 1; dispatch(handleNewPostEvent(msg)); clearTimeout(timeout); - timeout = setTimeout(triggered, wait); + timeout = window.setTimeout(triggered, wait); } }; } const handleNewPostEventDebounced = debouncePostEvent(100); -export function handleNewPostEvent(msg) { +export function handleNewPostEvent(msg: WebSocketMessages.Posted | WebSocketMessages.EphemeralPost): ThunkActionFunc { return (myDispatch, myGetState) => { - const post = JSON.parse(msg.data.post); + const post = JSON.parse(msg.data.post) as Post; - if (window.logPostEvents) { + if ((window as any).logPostEvents) { // eslint-disable-next-line no-console console.log('handleNewPostEvent - new post received', post); } @@ -790,7 +802,7 @@ export function handleNewPostEvent(msg) { if ( post.user_id !== getCurrentUserId(myGetState()) && !getIsManualStatusForUserId(myGetState(), post.user_id) && - msg.data.set_online + 'set_online' in msg.data && msg.data.set_online ) { myDispatch({ type: UserTypes.RECEIVED_STATUSES, @@ -800,18 +812,19 @@ export function handleNewPostEvent(msg) { }; } -export function handleNewPostEvents(queue) { +export function handleNewPostEvents(queue: Array): ThunkActionFunc { return (myDispatch, myGetState) => { // Note that this method doesn't properly update the sidebar state for these posts - const posts = queue.map((msg) => JSON.parse(msg.data.post)); + const posts = queue.map((msg) => JSON.parse(msg.data.post) as Post); - if (window.logPostEvents) { + if ((window as any).logPostEvents) { // eslint-disable-next-line no-console console.log('handleNewPostEvents - new posts received', posts); } posts.forEach((post, index) => { - if (queue[index].data.should_ack) { + const msg = queue[index]; + if ('should_ack' in msg.data && msg.data.should_ack) { WebSocketClient.acknowledgePostedNotification(post.id, 'not_sent', 'too_many_posts'); } }); @@ -827,11 +840,11 @@ export function handleNewPostEvents(queue) { }; } -export function handlePostEditEvent(msg) { +export function handlePostEditEvent(msg: WebSocketMessages.PostEdited) { // Store post - const post = JSON.parse(msg.data.post); + const post = JSON.parse(msg.data.post) as Post; - if (window.logPostEvents) { + if ((window as any).logPostEvents) { // eslint-disable-next-line no-console console.log('handlePostEditEvent - post edit received', post); } @@ -842,10 +855,10 @@ export function handlePostEditEvent(msg) { dispatch(batchFetchStatusesProfilesGroupsFromPosts([post])); } -async function handlePostDeleteEvent(msg) { - const post = JSON.parse(msg.data.post); +async function handlePostDeleteEvent(msg: WebSocketMessages.PostDeleted) { + const post = JSON.parse(msg.data.post) as Post; - if (window.logPostEvents) { + if ((window as any).logPostEvents) { // eslint-disable-next-line no-console console.log('handlePostDeleteEvent - post delete received', post); } @@ -883,7 +896,7 @@ async function handlePostDeleteEvent(msg) { } } -export function handlePostUnreadEvent(msg) { +export function handlePostUnreadEvent(msg: WebSocketMessages.PostUnread) { dispatch( { type: ActionTypes.POST_UNREAD_SUCCESS, @@ -900,7 +913,7 @@ export function handlePostUnreadEvent(msg) { ); } -async function handleTeamAddedEvent(msg) { +async function handleTeamAddedEvent(msg: WebSocketMessages.UserAddedToTeam) { await dispatch(TeamActions.getTeam(msg.data.team_id)); await dispatch(TeamActions.getMyTeamMembers()); const state = getState(); @@ -912,10 +925,10 @@ async function handleTeamAddedEvent(msg) { } } -export function handleLeaveTeamEvent(msg) { +export function handleLeaveTeamEvent(msg: WebSocketMessages.UserRemovedFromTeam) { const state = getState(); - const actions = [ + const actions: MMReduxAction[] = [ { type: UserTypes.RECEIVED_PROFILE_NOT_IN_TEAM, data: {id: msg.data.team_id, user_id: msg.data.user_id}, @@ -975,10 +988,10 @@ export function handleLeaveTeamEvent(msg) { } } -function handleUpdateTeamEvent(msg) { +function handleUpdateTeamEvent(msg: WebSocketMessages.Team) { const state = store.getState(); const license = getLicense(state); - dispatch({type: TeamTypes.UPDATED_TEAM, data: JSON.parse(msg.data.team)}); + dispatch({type: TeamTypes.UPDATED_TEAM, data: JSON.parse(msg.data.team) as Team}); if (license.Cloud === 'true') { dispatch(getTeamsUsage()); } @@ -988,8 +1001,8 @@ function handleUpdateTeamSchemeEvent() { dispatch(TeamActions.getMyTeamMembers()); } -function handleDeleteTeamEvent(msg) { - const deletedTeam = JSON.parse(msg.data.team); +function handleDeleteTeamEvent(msg: WebSocketMessages.Team) { + const deletedTeam = JSON.parse(msg.data.team) as Team; const state = store.getState(); const {teams} = state.entities.teams; const license = getLicense(state); @@ -1013,7 +1026,7 @@ function handleDeleteTeamEvent(msg) { teamMember && deletedTeam.id === teamMember.team_id ) { - const myTeams = {}; + const myTeams: Record = {}; getMyTeams(state).forEach((t) => { myTeams[t.id] = t; }); @@ -1050,8 +1063,8 @@ function handleDeleteTeamEvent(msg) { } } -function handleUpdateMemberRoleEvent(msg) { - const memberData = JSON.parse(msg.data.member); +function handleUpdateMemberRoleEvent(msg: WebSocketMessages.TeamMemberRoleUpdated) { + const memberData = JSON.parse(msg.data.member) as TeamMembership; const newRoles = memberData.roles.split(' '); dispatch(loadRolesIfNeeded(newRoles)); @@ -1062,15 +1075,15 @@ function handleUpdateMemberRoleEvent(msg) { }); } -function handleDirectAddedEvent(msg) { +function handleDirectAddedEvent(msg: WebSocketMessages.DirectChannelCreated) { return fetchChannelAndAddToSidebar(msg.broadcast.channel_id); } -function handleGroupAddedEvent(msg) { +function handleGroupAddedEvent(msg: WebSocketMessages.GroupChannelCreated) { return fetchChannelAndAddToSidebar(msg.broadcast.channel_id); } -function handleUserAddedEvent(msg) { +function handleUserAddedEvent(msg: WebSocketMessages.UserAddedToChannel): ThunkActionFunc { return async (doDispatch, doGetState) => { const state = doGetState(); const config = getConfig(state); @@ -1104,19 +1117,19 @@ function handleUserAddedEvent(msg) { }; } -function fetchChannelAndAddToSidebar(channelId) { +function fetchChannelAndAddToSidebar(channelId: string): ThunkActionFunc { return async (doDispatch) => { const {data, error} = await doDispatch(getChannelAndMyMember(channelId)); - if (!error) { + if (data && !error) { doDispatch(addChannelToInitialCategory(data.channel)); } }; } -export function handleUserRemovedEvent(msg) { +export function handleUserRemovedEvent(msg: WebSocketMessages.UserRemovedFromChannel) { const state = getState(); - const currentChannel = getCurrentChannel(state) || {}; + const currentChannel = getCurrentChannel(state); const currentUser = getCurrentUser(state); const config = getConfig(state); const license = getLicense(state); @@ -1129,7 +1142,7 @@ export function handleUserRemovedEvent(msg) { dispatch(closeRightHandSide()); } - if (msg.data.channel_id === currentChannel.id) { + if (currentChannel && msg.data.channel_id === currentChannel.id) { if (msg.data.remover_id !== msg.broadcast.user_id) { const user = getUser(state, msg.data.remover_id); if (!user) { @@ -1147,7 +1160,7 @@ export function handleUserRemovedEvent(msg) { } } - const channel = getChannel(state, msg.data.channel_id); + const channel = getChannel(state, msg.data.channel_id ?? ''); dispatch({ type: ChannelTypes.LEAVE_CHANNEL, @@ -1158,14 +1171,14 @@ export function handleUserRemovedEvent(msg) { }, }); - if (msg.data.channel_id === currentChannel.id) { + if (currentChannel && msg.data.channel_id === currentChannel.id) { redirectUserToDefaultTeam(); } if (isGuest(currentUser.roles)) { dispatch(removeNotVisibleUsers()); } - } else if (msg.broadcast.channel_id === currentChannel.id) { + } else if (currentChannel && msg.broadcast.channel_id === currentChannel.id) { dispatch(getChannelStats(currentChannel.id)); dispatch({ type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, @@ -1179,7 +1192,7 @@ export function handleUserRemovedEvent(msg) { if (msg.broadcast.user_id !== currentUser.id) { const channel = getChannel(state, msg.broadcast.channel_id); const members = getChannelMembersInChannels(state); - const isMember = Object.values(members).some((member) => member[msg.data.user_id]); + const isMember = Object.values(members).some((member) => msg.data.user_id && member[msg.data.user_id]); if (channel && isGuest(currentUser.roles) && !isMember) { const actions = [ { @@ -1195,7 +1208,7 @@ export function handleUserRemovedEvent(msg) { } } - const channelId = msg.broadcast.channel_id || msg.data.channel_id; + const channelId = msg.broadcast.channel_id || msg.data.channel_id || ''; const userId = msg.broadcast.user_id || msg.data.user_id; const channel = getChannel(state, channelId); if (channel && !haveISystemPermission(state, {permission: Permissions.VIEW_MEMBERS}) && !haveITeamPermission(state, channel.team_id, Permissions.VIEW_MEMBERS)) { @@ -1212,7 +1225,7 @@ export function handleUserRemovedEvent(msg) { } } -export async function handleUserUpdatedEvent(msg) { +export async function handleUserUpdatedEvent(msg: WebSocketMessages.UserUpdated) { // This websocket event is sent to all non-guest users on the server, so be careful requesting data from the server // in response to it. That can overwhelm the server if every connected user makes such a request at the same time. // See https://mattermost.atlassian.net/browse/MM-40050 for more information. @@ -1242,30 +1255,12 @@ export async function handleUserUpdatedEvent(msg) { } } -function handleRoleAddedEvent(msg) { - const role = JSON.parse(msg.data.role); - - dispatch({ - type: RoleTypes.RECEIVED_ROLE, - data: role, - }); -} - -function handleRoleRemovedEvent(msg) { - const role = JSON.parse(msg.data.role); - - dispatch({ - type: RoleTypes.ROLE_DELETED, - data: role, - }); -} - -function handleChannelSchemeUpdatedEvent(msg) { +function handleChannelSchemeUpdatedEvent(msg: WebSocketMessages.ChannelSchemeUpdated) { dispatch(getMyChannelMember(msg.broadcast.channel_id)); } -function handleRoleUpdatedEvent(msg) { - const role = JSON.parse(msg.data.role); +function handleRoleUpdatedEvent(msg: WebSocketMessages.RoleUpdated) { + const role = JSON.parse(msg.data.role) as Role; dispatch({ type: RoleTypes.RECEIVED_ROLE, @@ -1273,7 +1268,7 @@ function handleRoleUpdatedEvent(msg) { }); } -function handleChannelCreatedEvent(msg) { +function handleChannelCreatedEvent(msg: WebSocketMessages.ChannelCreated): ThunkActionFunc { return async (myDispatch, myGetState) => { const channelId = msg.data.channel_id; const teamId = msg.data.team_id; @@ -1288,21 +1283,25 @@ function handleChannelCreatedEvent(msg) { channel = getChannel(myGetState(), channelId); } + if (!channel) { + return; + } + myDispatch(addChannelToInitialCategory(channel, false)); } }; } -function handleChannelDeletedEvent(msg) { +function handleChannelDeletedEvent(msg: WebSocketMessages.ChannelDeleted) { dispatch({type: ChannelTypes.RECEIVED_CHANNEL_DELETED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, deleteAt: msg.data.delete_at, viewArchivedChannels: true}}); } -function handleChannelUnarchivedEvent(msg) { +function handleChannelUnarchivedEvent(msg: WebSocketMessages.ChannelRestored) { dispatch({type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, viewArchivedChannels: true}}); } -function handlePreferenceChangedEvent(msg) { - const preference = JSON.parse(msg.data.preference); +function handlePreferenceChangedEvent(msg: WebSocketMessages.PreferenceChanged) { + const preference = JSON.parse(msg.data.preference) as PreferenceType; dispatch({type: PreferenceTypes.RECEIVED_PREFERENCES, data: [preference]}); if (addedNewDmUser(preference)) { @@ -1314,8 +1313,8 @@ function handlePreferenceChangedEvent(msg) { } } -function handlePreferencesChangedEvent(msg) { - const preferences = JSON.parse(msg.data.preferences); +function handlePreferencesChangedEvent(msg: WebSocketMessages.PreferencesChanged) { + const preferences = JSON.parse(msg.data.preferences) as PreferenceType[]; dispatch({type: PreferenceTypes.RECEIVED_PREFERENCES, data: preferences}); if (preferences.findIndex(addedNewDmUser) !== -1) { @@ -1327,34 +1326,34 @@ function handlePreferencesChangedEvent(msg) { } } -function handlePreferencesDeletedEvent(msg) { - const preferences = JSON.parse(msg.data.preferences); +function handlePreferencesDeletedEvent(msg: WebSocketMessages.PreferencesChanged) { + const preferences = JSON.parse(msg.data.preferences) as PreferenceType[]; dispatch({type: PreferenceTypes.DELETED_PREFERENCES, data: preferences}); } -function addedNewDmUser(preference) { +function addedNewDmUser(preference: PreferenceType) { return preference.category === Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW && preference.value === 'true'; } -function addedNewGmUser(preference) { +function addedNewGmUser(preference: PreferenceType) { return preference.category === Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW && preference.value === 'true'; } -export function handleStatusChangedEvent(msg) { +export function handleStatusChangedEvent(msg: WebSocketMessages.StatusChanged) { return { type: UserTypes.RECEIVED_STATUSES, data: {[msg.data.user_id]: msg.data.status}, }; } -function handleHelloEvent(msg) { +function handleHelloEvent(msg: WebSocketMessages.Hello) { dispatch(setServerVersion(msg.data.server_version)); dispatch(setConnectionId(msg.data.connection_id)); dispatch(setServerHostname(msg.data.server_hostname)); } -function handleReactionAddedEvent(msg) { - const reaction = JSON.parse(msg.data.reaction); +function handleReactionAddedEvent(msg: WebSocketMessages.PostReaction) { + const reaction = JSON.parse(msg.data.reaction) as Reaction; dispatch(getCustomEmojiForReaction(reaction.emoji_name)); @@ -1364,22 +1363,22 @@ function handleReactionAddedEvent(msg) { }); } -function setConnectionId(connectionId) { +function setConnectionId(connectionId: string) { return { type: GeneralTypes.SET_CONNECTION_ID, payload: {connectionId}, }; } -function setServerHostname(serverHostname) { +function setServerHostname(serverHostname: string | undefined) { return { type: GeneralTypes.SET_SERVER_HOSTNAME, payload: {serverHostname}, }; } -function handleAddEmoji(msg) { - const data = JSON.parse(msg.data.emoji); +function handleAddEmoji(msg: WebSocketMessages.EmojiAdded) { + const data = JSON.parse(msg.data.emoji) as Emoji; dispatch({ type: EmojiTypes.RECEIVED_CUSTOM_EMOJI, @@ -1387,8 +1386,8 @@ function handleAddEmoji(msg) { }); } -function handleReactionRemovedEvent(msg) { - const reaction = JSON.parse(msg.data.reaction); +function handleReactionRemovedEvent(msg: WebSocketMessages.PostReaction) { + const reaction = JSON.parse(msg.data.reaction) as Reaction; dispatch({ type: PostTypes.REACTION_DELETED, @@ -1396,13 +1395,13 @@ function handleReactionRemovedEvent(msg) { }); } -function handleMultipleChannelsViewedEvent(msg) { +function handleMultipleChannelsViewedEvent(msg: WebSocketMessages.MultipleChannelsViewed) { if (getCurrentUserId(getState()) === msg.broadcast.user_id) { dispatch(markMultipleChannelsAsRead(msg.data.channel_times)); } } -export function handlePluginEnabled(msg) { +export function handlePluginEnabled(msg: WebSocketMessages.Plugin) { const manifest = msg.data.manifest; dispatch({type: ActionTypes.RECEIVED_WEBAPP_PLUGIN, data: manifest}); @@ -1411,12 +1410,12 @@ export function handlePluginEnabled(msg) { }); } -export function handlePluginDisabled(msg) { +export function handlePluginDisabled(msg: WebSocketMessages.Plugin) { const manifest = msg.data.manifest; removePlugin(manifest); } -function handleUserRoleUpdated(msg) { +function handleUserRoleUpdated(msg: WebSocketMessages.UserRoleUpdated) { const user = store.getState().entities.users.profiles[msg.data.user_id]; if (user) { @@ -1433,24 +1432,24 @@ function handleUserRoleUpdated(msg) { } } -function handleConfigChanged(msg) { +function handleConfigChanged(msg: WebSocketMessages.ConfigChanged) { store.dispatch({type: GeneralTypes.CLIENT_CONFIG_RECEIVED, data: msg.data.config}); } -function handleLicenseChanged(msg) { +function handleLicenseChanged(msg: WebSocketMessages.LicenseChanged) { store.dispatch({type: GeneralTypes.CLIENT_LICENSE_RECEIVED, data: msg.data.license}); // Refresh server limits when license changes since limits may have changed dispatch(getServerLimits()); } -function handlePluginStatusesChangedEvent(msg) { +function handlePluginStatusesChangedEvent(msg: WebSocketMessages.PluginStatusesChanged) { store.dispatch({type: AdminTypes.RECEIVED_PLUGIN_STATUSES, data: msg.data.plugin_statuses}); } -function handleOpenDialogEvent(msg) { - const data = (msg.data && msg.data.dialog) || {}; - const dialog = JSON.parse(data); +function handleOpenDialogEvent(msg: WebSocketMessages.OpenDialog) { + const data = (msg.data && msg.data.dialog); + const dialog = JSON.parse(data) as OpenDialogRequest || {}; store.dispatch({type: IntegrationTypes.RECEIVED_DIALOG, data: dialog}); @@ -1463,8 +1462,8 @@ function handleOpenDialogEvent(msg) { store.dispatch(openModal({modalId: ModalIdentifiers.INTERACTIVE_DIALOG, dialogType: DialogRouter})); } -function handleGroupUpdatedEvent(msg) { - const data = JSON.parse(msg.data.group); +function handleGroupUpdatedEvent(msg: WebSocketMessages.ReceivedGroup) { + const data = JSON.parse(msg.data.group) as Group; dispatch( { type: GroupTypes.PATCHED_GROUP, @@ -1473,7 +1472,7 @@ function handleGroupUpdatedEvent(msg) { ); } -function handleMyGroupUpdate(groupMember) { +function handleMyGroupUpdate(groupMember: GroupMember) { dispatch(batchActions([ { type: GroupTypes.ADD_MY_GROUP, @@ -1492,11 +1491,11 @@ function handleMyGroupUpdate(groupMember) { ])); } -export function handleGroupAddedMemberEvent(msg) { +export function handleGroupAddedMemberEvent(msg: WebSocketMessages.GroupMember): ThunkActionFunc { return async (doDispatch, doGetState) => { const state = doGetState(); const currentUserId = getCurrentUserId(state); - const groupMember = JSON.parse(msg.data.group_member); + const groupMember = JSON.parse(msg.data.group_member) as GroupMember; if (currentUserId === groupMember.user_id) { const group = getGroup(state, groupMember.group_id); @@ -1512,11 +1511,11 @@ export function handleGroupAddedMemberEvent(msg) { }; } -function handleGroupDeletedMemberEvent(msg) { +function handleGroupDeletedMemberEvent(msg: WebSocketMessages.GroupMember): ThunkActionFunc { return (doDispatch, doGetState) => { const state = doGetState(); const currentUserId = getCurrentUserId(state); - const data = JSON.parse(msg.data.group_member); + const data = JSON.parse(msg.data.group_member) as GroupMember; if (currentUserId === data.user_id) { dispatch(batchActions([ @@ -1540,35 +1539,35 @@ function handleGroupDeletedMemberEvent(msg) { }; } -function handleGroupAssociatedToTeamEvent(msg) { +function handleGroupAssociatedToTeamEvent(msg: WebSocketMessages.GroupAssociatedToTeam) { store.dispatch({ type: GroupTypes.RECEIVED_GROUP_ASSOCIATED_TO_TEAM, data: {teamID: msg.broadcast.team_id, groups: [{id: msg.data.group_id}]}, }); } -function handleGroupNotAssociatedToTeamEvent(msg) { +function handleGroupNotAssociatedToTeamEvent(msg: WebSocketMessages.GroupAssociatedToTeam) { store.dispatch({ type: GroupTypes.RECEIVED_GROUP_NOT_ASSOCIATED_TO_TEAM, data: {teamID: msg.broadcast.team_id, groups: [{id: msg.data.group_id}]}, }); } -function handleGroupAssociatedToChannelEvent(msg) { +function handleGroupAssociatedToChannelEvent(msg: WebSocketMessages.GroupAssociatedToChannel) { store.dispatch({ type: GroupTypes.RECEIVED_GROUP_ASSOCIATED_TO_CHANNEL, data: {channelID: msg.broadcast.channel_id, groups: [{id: msg.data.group_id}]}, }); } -function handleGroupNotAssociatedToChannelEvent(msg) { +function handleGroupNotAssociatedToChannelEvent(msg: WebSocketMessages.GroupAssociatedToChannel) { store.dispatch({ type: GroupTypes.RECEIVED_GROUP_NOT_ASSOCIATED_TO_CHANNEL, data: {channelID: msg.broadcast.channel_id, groups: [{id: msg.data.group_id}]}, }); } -function handleSidebarCategoryCreated(msg) { +function handleSidebarCategoryCreated(msg: WebSocketMessages.SidebarCategoryCreated): ThunkActionFunc { return (doDispatch, doGetState) => { const state = doGetState(); @@ -1587,7 +1586,7 @@ function handleSidebarCategoryCreated(msg) { }; } -function handleSidebarCategoryUpdated(msg) { +function handleSidebarCategoryUpdated(msg: WebSocketMessages.SidebarCategoryUpdated): ThunkActionFunc { return (doDispatch, doGetState) => { const state = doGetState(); @@ -1606,7 +1605,7 @@ function handleSidebarCategoryUpdated(msg) { }; } -function handleSidebarCategoryDeleted(msg) { +function handleSidebarCategoryDeleted(msg: WebSocketMessages.SidebarCategoryDeleted): ThunkActionFunc { return (doDispatch, doGetState) => { const state = doGetState(); @@ -1624,11 +1623,11 @@ function handleSidebarCategoryDeleted(msg) { }; } -function handleSidebarCategoryOrderUpdated(msg) { +function handleSidebarCategoryOrderUpdated(msg: WebSocketMessages.SidebarCategoryOrderUpdated) { return receivedCategoryOrder(msg.broadcast.team_id, msg.data.order); } -export function handleUserActivationStatusChange() { +export function handleUserActivationStatusChange(): ThunkActionFunc { return (doDispatch, doGetState) => { const state = doGetState(); const license = getLicense(state); @@ -1642,11 +1641,7 @@ export function handleUserActivationStatusChange() { }; } -function handleCloudPaymentStatusUpdated() { - return (doDispatch) => doDispatch(getCloudSubscription()); -} - -export function handleCloudSubscriptionChanged(msg) { +export function handleCloudSubscriptionChanged(msg: WebSocketMessages.CloudSubscriptionChanged): ActionFunc { return (doDispatch, doGetState) => { const state = doGetState(); const license = getLicense(state); @@ -1670,7 +1665,7 @@ export function handleCloudSubscriptionChanged(msg) { }; } -function handleRefreshAppsBindings() { +function handleRefreshAppsBindings(): ThunkActionFunc { return (doDispatch, doGetState) => { const state = doGetState(); @@ -1679,7 +1674,7 @@ function handleRefreshAppsBindings() { const siteURL = state.entities.general.config.SiteURL; const currentURL = window.location.href; let threadIdentifier; - if (currentURL.startsWith(siteURL)) { + if (siteURL && currentURL.startsWith(siteURL)) { const parts = currentURL.substr(siteURL.length + (siteURL.endsWith('/') ? 0 : 1)).split('/'); if (parts.length === 3 && parts[1] === 'threads') { threadIdentifier = parts[2]; @@ -1715,14 +1710,14 @@ export function handleAppsPluginDisabled() { }; } -function handleFirstAdminVisitMarketplaceStatusReceivedEvent(msg) { - const receivedData = JSON.parse(msg.data.firstAdminVisitMarketplaceStatus); +function handleFirstAdminVisitMarketplaceStatusReceivedEvent(msg: WebSocketMessages.FirstAdminVisitMarketplaceStatusReceived) { + const receivedData = JSON.parse(msg.data.firstAdminVisitMarketplaceStatus) as boolean; store.dispatch({type: GeneralTypes.FIRST_ADMIN_VISIT_MARKETPLACE_STATUS_RECEIVED, data: receivedData}); } -function handleThreadReadChanged(msg) { +function handleThreadReadChanged(msg: WebSocketMessages.ThreadReadChanged): ThunkActionFunc { return (doDispatch, doGetState) => { - if (msg.data.thread_id) { + if (msg.data.thread_id && msg.data.channel_id && msg.data.unread_mentions && msg.data.unread_replies) { const state = doGetState(); const thread = getThreads(state)?.[msg.data.thread_id]; @@ -1753,11 +1748,11 @@ function handleThreadReadChanged(msg) { }; } -function handleThreadUpdated(msg) { +function handleThreadUpdated(msg: WebSocketMessages.ThreadUpdated): ThunkActionFunc { return (doDispatch, doGetState) => { let threadData; try { - threadData = JSON.parse(msg.data.thread); + threadData = JSON.parse(msg.data.thread) as UserThread; } catch { // invalid JSON return; @@ -1801,7 +1796,7 @@ function handleThreadUpdated(msg) { }; } -function handleThreadFollowChanged(msg) { +function handleThreadFollowChanged(msg: WebSocketMessages.ThreadFollowedChanged): ThunkActionFunc { return async (doDispatch, doGetState) => { const state = doGetState(); const thread = getThread(state, msg.data.thread_id); @@ -1812,8 +1807,8 @@ function handleThreadFollowChanged(msg) { }; } -function handlePostAcknowledgementAdded(msg) { - const data = JSON.parse(msg.data.acknowledgement); +function handlePostAcknowledgementAdded(msg: WebSocketMessages.PostAcknowledgement) { + const data = JSON.parse(msg.data.acknowledgement) as PostAcknowledgement; return { type: PostTypes.CREATE_ACK_POST_SUCCESS, @@ -1821,8 +1816,8 @@ function handlePostAcknowledgementAdded(msg) { }; } -function handlePostAcknowledgementRemoved(msg) { - const data = JSON.parse(msg.data.acknowledgement); +function handlePostAcknowledgementRemoved(msg: WebSocketMessages.PostAcknowledgement) { + const data = JSON.parse(msg.data.acknowledgement) as PostAcknowledgement; return { type: PostTypes.DELETE_ACK_POST_SUCCESS, @@ -1830,9 +1825,9 @@ function handlePostAcknowledgementRemoved(msg) { }; } -function handleUpsertDraftEvent(msg) { +function handleUpsertDraftEvent(msg: WebSocketMessages.PostDraft): ThunkActionFunc { return async (doDispatch) => { - const draft = JSON.parse(msg.data.draft); + const draft = JSON.parse(msg.data.draft) as Draft; const {key, value} = transformServerDraft(draft); value.show = true; @@ -1840,9 +1835,9 @@ function handleUpsertDraftEvent(msg) { }; } -function handleCreateScheduledPostEvent(msg) { +function handleCreateScheduledPostEvent(msg: WebSocketMessages.ScheduledPost): ThunkActionFunc { return async (doDispatch) => { - const scheduledPost = JSON.parse(msg.data.scheduledPost); + const scheduledPost = JSON.parse(msg.data.scheduledPost) as ScheduledPost; const state = getState(); const teamId = getTeamIdByChannelId(state, scheduledPost.channel_id); @@ -1856,9 +1851,9 @@ function handleCreateScheduledPostEvent(msg) { }; } -function handleUpdateScheduledPostEvent(msg) { +function handleUpdateScheduledPostEvent(msg: WebSocketMessages.ScheduledPost): ThunkActionFunc { return async (doDispatch) => { - const scheduledPost = JSON.parse(msg.data.scheduledPost); + const scheduledPost = JSON.parse(msg.data.scheduledPost) as ScheduledPost; doDispatch({ type: ScheduledPostTypes.SCHEDULED_POST_UPDATED, @@ -1869,9 +1864,9 @@ function handleUpdateScheduledPostEvent(msg) { }; } -function handleDeleteScheduledPostEvent(msg) { +function handleDeleteScheduledPostEvent(msg: WebSocketMessages.ScheduledPost): ThunkActionFunc { return async (doDispatch) => { - const scheduledPost = JSON.parse(msg.data.scheduledPost); + const scheduledPost = JSON.parse(msg.data.scheduledPost) as ScheduledPost; doDispatch({ type: ScheduledPostTypes.SCHEDULED_POST_DELETED, @@ -1882,9 +1877,9 @@ function handleDeleteScheduledPostEvent(msg) { }; } -function handleDeleteDraftEvent(msg) { +function handleDeleteDraftEvent(msg: WebSocketMessages.PostDraft): ThunkActionFunc { return async (doDispatch) => { - const draft = JSON.parse(msg.data.draft); + const draft = JSON.parse(msg.data.draft) as Draft; const {key} = transformServerDraft(draft); doDispatch(setGlobalItem(key, { @@ -1895,23 +1890,16 @@ function handleDeleteDraftEvent(msg) { }; } -function handlePersistentNotification(msg) { +function handlePersistentNotification(msg: WebSocketMessages.PersistentNotificationTriggered): ThunkActionFunc { return async (doDispatch) => { - const post = JSON.parse(msg.data.post); + const post = JSON.parse(msg.data.post) as Post; doDispatch(sendDesktopNotification(post, msg.data)); }; } -function handleHostedCustomerSignupProgressUpdated(msg) { - return { - type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_SIGNUP_PROGRESS, - data: msg.data.progress, - }; -} - -function handleChannelBookmarkCreated(msg) { - const bookmark = JSON.parse(msg.data.bookmark); +function handleChannelBookmarkCreated(msg: WebSocketMessages.ChannelBookmarkCreated) { + const bookmark = JSON.parse(msg.data.bookmark) as ChannelBookmarkWithFileInfo; return { type: ChannelBookmarkTypes.RECEIVED_BOOKMARK, @@ -1919,9 +1907,9 @@ function handleChannelBookmarkCreated(msg) { }; } -function handleChannelBookmarkUpdated(msg) { +function handleChannelBookmarkUpdated(msg: WebSocketMessages.ChannelBookmarkUpdated): ThunkActionFunc { return async (doDispatch) => { - const {updated, deleted} = JSON.parse(msg.data.bookmarks); + const {updated, deleted} = JSON.parse(msg.data.bookmarks) as UpdateChannelBookmarkResponse; if (updated) { doDispatch({ @@ -1939,8 +1927,8 @@ function handleChannelBookmarkUpdated(msg) { }; } -function handleChannelBookmarkDeleted(msg) { - const bookmark = JSON.parse(msg.data.bookmark); +function handleChannelBookmarkDeleted(msg: WebSocketMessages.ChannelBookmarkDeleted) { + const bookmark = JSON.parse(msg.data.bookmark) as ChannelBookmarkWithFileInfo; return { type: ChannelBookmarkTypes.BOOKMARK_DELETED, @@ -1948,8 +1936,8 @@ function handleChannelBookmarkDeleted(msg) { }; } -function handleChannelBookmarkSorted(msg) { - const bookmarks = JSON.parse(msg.data.bookmarks); +function handleChannelBookmarkSorted(msg: WebSocketMessages.ChannelBookmarkSorted) { + const bookmarks = JSON.parse(msg.data.bookmarks) as ChannelBookmarkWithFileInfo[]; return { type: ChannelBookmarkTypes.RECEIVED_BOOKMARKS, @@ -1957,21 +1945,21 @@ function handleChannelBookmarkSorted(msg) { }; } -export function handleCustomAttributeValuesUpdated(msg) { +export function handleCustomAttributeValuesUpdated(msg: WebSocketMessages.CPAValuesUpdated) { return { type: UserTypes.RECEIVED_CPA_VALUES, data: {userID: msg.data.user_id, customAttributeValues: msg.data.values}, }; } -export function handleCustomAttributesCreated(msg) { +export function handleCustomAttributesCreated(msg: WebSocketMessages.CPAFieldCreated) { return { type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_CREATED, data: msg.data.field, }; } -export function handleCustomAttributesUpdated(msg) { +export function handleCustomAttributesUpdated(msg: WebSocketMessages.CPAFieldUpdated): ThunkActionFunc { return (dispatch) => { const {field, delete_values: deleteValues} = msg.data; @@ -1992,21 +1980,21 @@ export function handleCustomAttributesUpdated(msg) { }; } -export function handleCustomAttributesDeleted(msg) { +export function handleCustomAttributesDeleted(msg: WebSocketMessages.CPAFieldDeleted) { return { type: GeneralTypes.CUSTOM_PROFILE_ATTRIBUTE_FIELD_DELETED, data: msg.data.field_id, }; } -export function handleContentFlaggingReportValueChanged(msg) { +export function handleContentFlaggingReportValueChanged(msg: WebSocketMessages.ContentFlaggingReportValueUpdated) { return { type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED, data: msg.data, }; } -export function handleRecapUpdated(msg) { +export function handleRecapUpdated(msg: WebSocketMessages.RecapUpdated): ThunkActionFunc { const recapId = msg.data.recap_id; return async (doDispatch) => { diff --git a/webapp/channels/src/components/common/hooks/useGetAgentsBridgeEnabled.ts b/webapp/channels/src/components/common/hooks/useGetAgentsBridgeEnabled.ts index d2a0d851846..e4d02205e92 100644 --- a/webapp/channels/src/components/common/hooks/useGetAgentsBridgeEnabled.ts +++ b/webapp/channels/src/components/common/hooks/useGetAgentsBridgeEnabled.ts @@ -5,11 +5,11 @@ import {useCallback, useEffect, useRef} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import type {WebSocketMessage} from '@mattermost/client'; +import {WebSocketEvents} from '@mattermost/client'; import {getAgents as getAgentsAction} from 'mattermost-redux/actions/agents'; import {getAgents} from 'mattermost-redux/selectors/entities/agents'; -import {SocketEvents} from 'utils/constants'; import {useWebSocket} from 'utils/use_websocket/hooks'; const AI_PLUGIN_ID = 'mattermost-ai'; @@ -63,10 +63,10 @@ export default function useGetAgentsBridgeEnabled(): boolean { // Note: When a plugin is enabled/disabled, the backend fires CONFIG_CHANGED first, // then PLUGIN_ENABLED/PLUGIN_DISABLED. We debounce to avoid duplicate fetches. const isPluginEvent = - (msg.event === SocketEvents.PLUGIN_ENABLED || msg.event === SocketEvents.PLUGIN_DISABLED) && + (msg.event === WebSocketEvents.PluginEnabled || msg.event === WebSocketEvents.PluginDisabled) && msg.data?.manifest?.id === AI_PLUGIN_ID; - const isConfigChange = msg.event === SocketEvents.CONFIG_CHANGED; + const isConfigChange = msg.event === WebSocketEvents.ConfigChanged; if (isPluginEvent || isConfigChange) { debouncedRefetch(); diff --git a/webapp/channels/src/components/logged_in/logged_in.test.tsx b/webapp/channels/src/components/logged_in/logged_in.test.tsx index e95bdc69df9..6cd1d6f1761 100644 --- a/webapp/channels/src/components/logged_in/logged_in.test.tsx +++ b/webapp/channels/src/components/logged_in/logged_in.test.tsx @@ -14,7 +14,7 @@ import type {Props} from 'components/logged_in/logged_in'; import {fireEvent, renderWithContext, screen} from 'tests/react_testing_utils'; -jest.mock('actions/websocket_actions.jsx', () => ({ +jest.mock('actions/websocket_actions', () => ({ initialize: jest.fn(), close: jest.fn(), })); diff --git a/webapp/channels/src/components/logged_in/logged_in.tsx b/webapp/channels/src/components/logged_in/logged_in.tsx index 4d2e6725404..1b2726d0cb6 100644 --- a/webapp/channels/src/components/logged_in/logged_in.tsx +++ b/webapp/channels/src/components/logged_in/logged_in.tsx @@ -7,7 +7,7 @@ import {Redirect} from 'react-router-dom'; import type {UserProfile} from '@mattermost/types/users'; import * as GlobalActions from 'actions/global_actions'; -import * as WebSocketActions from 'actions/websocket_actions.jsx'; +import * as WebSocketActions from 'actions/websocket_actions'; import BrowserStore from 'stores/browser_store'; import LoadingScreen from 'components/loading_screen'; diff --git a/webapp/channels/src/components/msg_typing/actions.test.ts b/webapp/channels/src/components/msg_typing/actions.test.ts index d02fc786d9d..2e7502237e1 100644 --- a/webapp/channels/src/components/msg_typing/actions.test.ts +++ b/webapp/channels/src/components/msg_typing/actions.test.ts @@ -1,8 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {WebSocketTypes} from 'mattermost-redux/action_types'; import {getMissingProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users'; -import {General, WebsocketEvents} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; import configureStore from 'tests/test_store'; @@ -39,8 +40,8 @@ describe('handleUserTypingEvent', () => { store.dispatch(userStartedTyping(userId, channelId, rootId, Date.now())); - expect(store.getActions().find((action) => action.type === WebsocketEvents.TYPING)).toMatchObject({ - type: WebsocketEvents.TYPING, + expect(store.getActions().find((action) => action.type === WebSocketTypes.TYPING)).toMatchObject({ + type: WebSocketTypes.TYPING, data: { id: channelId + rootId, userId, diff --git a/webapp/channels/src/components/msg_typing/actions.ts b/webapp/channels/src/components/msg_typing/actions.ts index d95e2489222..b417a305ad5 100644 --- a/webapp/channels/src/components/msg_typing/actions.ts +++ b/webapp/channels/src/components/msg_typing/actions.ts @@ -3,8 +3,9 @@ import type {GlobalState} from '@mattermost/types/store'; +import {WebSocketTypes} from 'mattermost-redux/action_types'; import {getMissingProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users'; -import {General, Preferences, WebsocketEvents} from 'mattermost-redux/constants'; +import {General, Preferences} from 'mattermost-redux/constants'; import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common'; import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general'; import {getBool} from 'mattermost-redux/selectors/entities/preferences'; @@ -30,7 +31,7 @@ export function userStartedTyping(userId: string, channelId: string, rootId: str } dispatch({ - type: WebsocketEvents.TYPING, + type: WebSocketTypes.TYPING, data: { id: channelId + rootId, userId, @@ -72,7 +73,7 @@ function fillInMissingInfo(userId: string): ActionFuncAsync { export function userStoppedTyping(userId: string, channelId: string, rootId: string, now: number) { return { - type: WebsocketEvents.STOP_TYPING, + type: WebSocketTypes.STOPPED_TYPING, data: { id: channelId + rootId, userId, diff --git a/webapp/channels/src/components/msg_typing/msg_typing.tsx b/webapp/channels/src/components/msg_typing/msg_typing.tsx index 59490300640..88636b29d8e 100644 --- a/webapp/channels/src/components/msg_typing/msg_typing.tsx +++ b/webapp/channels/src/components/msg_typing/msg_typing.tsx @@ -4,9 +4,8 @@ import React, {useCallback} from 'react'; import {FormattedMessage} from 'react-intl'; -import type {WebSocketMessage} from '@mattermost/client'; +import {WebSocketEvents, type WebSocketMessage} from '@mattermost/client'; -import {SocketEvents} from 'utils/constants'; import {useWebSocket} from 'utils/use_websocket'; type Props = { @@ -21,7 +20,7 @@ export default function MsgTyping(props: Props) { const {userStartedTyping, userStoppedTyping} = props; useWebSocket({ handler: useCallback((msg: WebSocketMessage) => { - if (msg.event === SocketEvents.TYPING) { + if (msg.event === WebSocketEvents.Typing) { const channelId = msg.broadcast.channel_id; const rootId = msg.data.parent_id; const userId = msg.data.user_id; @@ -29,7 +28,7 @@ export default function MsgTyping(props: Props) { if (props.channelId === channelId && props.rootId === rootId) { userStartedTyping(userId, channelId, rootId, Date.now()); } - } else if (msg.event === SocketEvents.POSTED) { + } else if (msg.event === WebSocketEvents.Posted) { const post = JSON.parse(msg.data.post); const channelId = post.channel_id; diff --git a/webapp/channels/src/components/team_controller/team_controller.tsx b/webapp/channels/src/components/team_controller/team_controller.tsx index 7502803453d..7daca5ff98e 100644 --- a/webapp/channels/src/components/team_controller/team_controller.tsx +++ b/webapp/channels/src/components/team_controller/team_controller.tsx @@ -15,7 +15,7 @@ import { } from 'mattermost-redux/selectors/entities/content_flagging'; import type {ActionResult} from 'mattermost-redux/types/actions'; -import {reconnect} from 'actions/websocket_actions.jsx'; +import {reconnect} from 'actions/websocket_actions'; import LocalStorageStore from 'stores/local_storage_store'; import {makeAsyncComponent, makeAsyncPluggableComponent} from 'components/async_load'; diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/index.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/index.ts index da9295deaa1..8b49cffe547 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/index.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/index.ts @@ -35,6 +35,7 @@ import SharedChannelTypes from './shared_channels'; import TeamTypes from './teams'; import ThreadTypes from './threads'; import UserTypes from './users'; +import WebSocketTypes from './websocket'; export { ErrorTypes, @@ -69,6 +70,7 @@ export { SharedChannelTypes, ContentFlaggingTypes, AgentTypes, + WebSocketTypes, }; /** diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/roles.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/roles.ts index a6a8a25984b..d2a8d3f3624 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/roles.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/roles.ts @@ -22,7 +22,6 @@ export default keyMirror({ RECEIVED_ROLES: null, RECEIVED_ROLE: null, - ROLE_DELETED: null, SET_PENDING_ROLES: null, }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/websocket.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/websocket.ts new file mode 100644 index 00000000000..be33ddb2376 --- /dev/null +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/websocket.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import keyMirror from 'mattermost-redux/utils/key_mirror'; + +export default keyMirror({ + TYPING: null, + STOPPED_TYPING: null, +}); diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/scheduled_posts.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/scheduled_posts.ts index f9969e12ebf..9f077e59c56 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/scheduled_posts.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/scheduled_posts.ts @@ -31,7 +31,7 @@ export function createSchedulePost(schedulePost: ScheduledPost, teamId: string, }; } -export function fetchTeamScheduledPosts(teamId: string, includeDirectChannels: boolean, prune?: false) { +export function fetchTeamScheduledPosts(teamId: string, includeDirectChannels: boolean, prune?: boolean) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let scheduledPosts; diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/index.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/index.ts index 403abefb116..526d9009dd1 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/constants/index.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/constants/index.ts @@ -14,6 +14,5 @@ import Stats from './stats'; import Teams from './teams'; import Threads from './threads'; import Users from './users'; -import WebsocketEvents from './websocket'; -export {General, Preferences, Posts, Files, RequestStatus, WebsocketEvents, Teams, Stats, Permissions, Emoji, Plugins, Users, Roles, Threads}; +export {General, Preferences, Posts, Files, RequestStatus, Teams, Stats, Permissions, Emoji, Plugins, Users, Roles, Threads}; diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/websocket.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/websocket.ts deleted file mode 100644 index 776529106f6..00000000000 --- a/webapp/channels/src/packages/mattermost-redux/src/constants/websocket.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -const WebsocketEvents = { - POSTED: 'posted', - POST_EDITED: 'post_edited', - POST_DELETED: 'post_deleted', - POST_UNREAD: 'post_unread', - CHANNEL_CONVERTED: 'channel_converted', - CHANNEL_CREATED: 'channel_created', - CHANNEL_DELETED: 'channel_deleted', - CHANNEL_UNARCHIVED: 'channel_restored', - CHANNEL_UPDATED: 'channel_updated', - MULTIPLE_CHANNELS_VIEWED: 'multiple_channels_viewed', - CHANNEL_MEMBER_UPDATED: 'channel_member_updated', - CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated', - DIRECT_ADDED: 'direct_added', - ADDED_TO_TEAM: 'added_to_team', - LEAVE_TEAM: 'leave_team', - UPDATE_TEAM: 'update_team', - USER_ADDED: 'user_added', - USER_REMOVED: 'user_removed', - USER_UPDATED: 'user_updated', - USER_ROLE_UPDATED: 'user_role_updated', - ROLE_ADDED: 'role_added', - ROLE_REMOVED: 'role_removed', - ROLE_UPDATED: 'role_updated', - TYPING: 'typing', - STOP_TYPING: 'stop_typing', - PREFERENCE_CHANGED: 'preference_changed', - PREFERENCES_CHANGED: 'preferences_changed', - PREFERENCES_DELETED: 'preferences_deleted', - EPHEMERAL_MESSAGE: 'ephemeral_message', - STATUS_CHANGED: 'status_change', - HELLO: 'hello', - WEBRTC: 'webrtc', - REACTION_ADDED: 'reaction_added', - REACTION_REMOVED: 'reaction_removed', - EMOJI_ADDED: 'emoji_added', - LICENSE_CHANGED: 'license_changed', - CONFIG_CHANGED: 'config_changed', - PLUGIN_STATUSES_CHANGED: 'plugin_statuses_changed', - OPEN_DIALOG: 'open_dialog', - INCREASE_POST_VISIBILITY_BY_ONE: 'increase_post_visibility_by_one', - RECEIVED_GROUP: 'received_group', - RECEIVED_GROUP_ASSOCIATED_TO_TEAM: 'group_associated_to_team', - RECEIVED_GROUP_NOT_ASSOCIATED_TO_TEAM: 'group_not_associated_to_team', - RECEIVED_GROUP_ASSOCIATED_TO_CHANNEL: 'group_associated_to_channel', - RECEIVED_GROUP_NOT_ASSOCIATED_TO_CHANNEL: 'group_not_associated_to_channel', - THREAD_UPDATED: 'thread_updated', - THREAD_FOLLOW_CHANGED: 'thread_follow_changed', - THREAD_READ_CHANGED: 'thread_read_changed', - FIRST_ADMIN_VISIT_MARKETPLACE_STATUS_RECEIVED: 'first_admin_visit_marketplace_status_received', - GROUP_MEMBER_DELETED: 'group_member_deleted', - BURN_ON_READ_POST_REVEALED: 'post_revealed', - BURN_ON_READ_POST_BURNED: 'post_burned', - BURN_ON_READ_ALL_REVEALED: 'burn_on_read_all_revealed', -}; -export default WebsocketEvents; diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/roles.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/roles.ts index 37e98d0a87f..2b06afd71a2 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/roles.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/roles.ts @@ -32,15 +32,6 @@ function roles(state: Record = {}, action: MMReduxAction) { return state; } - case RoleTypes.ROLE_DELETED: { - if (action.data) { - const nextState = {...state}; - Reflect.deleteProperty(nextState, action.data.name); - return nextState; - } - - return state; - } case RoleTypes.RECEIVED_ROLE: { if (action.data) { const nextState = {...state}; diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/typing.test.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/typing.test.ts index f20eff6ebb6..885ef9fa630 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/typing.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/typing.test.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {WebsocketEvents} from 'mattermost-redux/constants'; +import {WebSocketTypes} from 'mattermost-redux/action_types'; import typingReducer from 'mattermost-redux/reducers/entities/typing'; import TestHelper from '../../../test/test_helper'; @@ -17,7 +17,7 @@ describe('Reducers.Typing', () => { expect(state).toEqual({}); }); - it('WebsocketEvents.TYPING', async () => { + it('WebSocketTypes.TYPING', async () => { let state = {}; const id1 = TestHelper.generateId(); @@ -27,7 +27,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.TYPING, + type: WebSocketTypes.TYPING, data: { id: id1, userId: userId1, @@ -49,7 +49,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.TYPING, + type: WebSocketTypes.TYPING, data: { id: id2, userId: userId1, @@ -74,7 +74,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.TYPING, + type: WebSocketTypes.TYPING, data: { id: id1, userId: userId2, @@ -99,7 +99,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.TYPING, + type: WebSocketTypes.TYPING, data: { id: id2, userId: userId2, @@ -121,7 +121,7 @@ describe('Reducers.Typing', () => { }); }); - it('WebsocketEvents.STOP_TYPING', async () => { + it('WebSocketTypes.STOPPED_TYPING', async () => { const id1 = TestHelper.generateId(); const id2 = TestHelper.generateId(); @@ -147,7 +147,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.STOP_TYPING, + type: WebSocketTypes.STOPPED_TYPING, data: { id: id1, userId: userId1, @@ -170,7 +170,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.STOP_TYPING, + type: WebSocketTypes.STOPPED_TYPING, data: { id: id2, userId: userId1, @@ -192,7 +192,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.STOP_TYPING, + type: WebSocketTypes.STOPPED_TYPING, data: { id: id1, userId: userId2, @@ -212,7 +212,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.STOP_TYPING, + type: WebSocketTypes.STOPPED_TYPING, data: { id: id2, userId: userId2, @@ -232,7 +232,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.STOP_TYPING, + type: WebSocketTypes.STOPPED_TYPING, data: { id: id1, userId: userId1, @@ -251,7 +251,7 @@ describe('Reducers.Typing', () => { state = typingReducer( state, { - type: WebsocketEvents.STOP_TYPING, + type: WebSocketTypes.STOPPED_TYPING, data: { id: id1, userId: userId1, diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/typing.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/typing.ts index d90d79aafff..39f49488691 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/typing.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/typing.ts @@ -5,7 +5,7 @@ import type {AnyAction} from 'redux'; import type {Typing} from '@mattermost/types/typing'; -import {WebsocketEvents} from 'mattermost-redux/constants'; +import {WebSocketTypes} from 'mattermost-redux/action_types'; export default function typing(state: Typing = {}, action: AnyAction): Typing { const { @@ -14,7 +14,7 @@ export default function typing(state: Typing = {}, action: AnyAction): Typing { } = action; switch (type) { - case WebsocketEvents.TYPING: { + case WebSocketTypes.TYPING: { const { id, userId, @@ -33,7 +33,7 @@ export default function typing(state: Typing = {}, action: AnyAction): Typing { return state; } - case WebsocketEvents.STOP_TYPING: { + case WebSocketTypes.STOPPED_TYPING: { const { id, userId, diff --git a/webapp/channels/src/plugins/registry.ts b/webapp/channels/src/plugins/registry.ts index 8d33db439da..24610a4f926 100644 --- a/webapp/channels/src/plugins/registry.ts +++ b/webapp/channels/src/plugins/registry.ts @@ -5,6 +5,8 @@ import React from 'react'; import {isValidElementType} from 'react-is'; import type {Reducer} from 'redux'; +import type {WebSocketMessages} from '@mattermost/client'; + import reducerRegistry from 'mattermost-redux/store/reducer_registry'; import { @@ -24,7 +26,7 @@ import { unregisterPluginWebSocketEvent, registerPluginReconnectHandler, unregisterPluginReconnectHandler, -} from 'actions/websocket_actions.jsx'; +} from 'actions/websocket_actions'; import store from 'stores/redux_store'; import {ActionTypes} from 'utils/constants'; @@ -818,7 +820,16 @@ export default class PluginRegistry { * - handler - a function to handle the event, receives the event message as an argument * Returns undefined. */ - registerWebSocketEventHandler = reArg(['event', 'handler'], ({event, handler}) => { + registerWebSocketEventHandler = reArg([ + 'event', + 'handler', + ], ({ + event, + handler, + }: { + event: string; + handler: (msg: WebSocketMessages.Unknown) => void; + }) => { registerPluginWebSocketEvent(this.id, event, handler); }); @@ -827,7 +838,7 @@ export default class PluginRegistry { * Accepts a string event type. * Returns undefined. */ - unregisterWebSocketEventHandler = reArg(['event'], ({event}) => { + unregisterWebSocketEventHandler = reArg(['event'], ({event}: { event: string }) => { unregisterPluginWebSocketEvent(this.id, event); }); @@ -836,7 +847,7 @@ export default class PluginRegistry { * internet after previously disconnecting. * Accepts a function to handle the event. Returns undefined. */ - registerReconnectHandler = reArg(['handler'], ({handler}) => { + registerReconnectHandler = reArg(['handler'], ({handler}: {handler: () => void}) => { registerPluginReconnectHandler(this.id, handler); }); diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index c58d3f1bca5..ba537409428 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -612,97 +612,13 @@ export const AppEvents = { FOCUS_EDIT_TEXTBOX: 'focus_edit_textbox', }; +/** + * @deprecated Use WebSocketEvents from @mattermost/client instead. + */ export const SocketEvents = { - POSTED: 'posted', - POST_EDITED: 'post_edited', - POST_DELETED: 'post_deleted', - POST_UPDATED: 'post_updated', - POST_UNREAD: 'post_unread', - BURN_ON_READ_POST_REVEALED: 'post_revealed', - BURN_ON_READ_POST_BURNED: 'post_burned', - BURN_ON_READ_ALL_REVEALED: 'burn_on_read_all_revealed', - CHANNEL_CONVERTED: 'channel_converted', - CHANNEL_CREATED: 'channel_created', - CHANNEL_DELETED: 'channel_deleted', - CHANNEL_UNARCHIVED: 'channel_restored', - CHANNEL_UPDATED: 'channel_updated', - CHANNEL_BOOKMARK_CREATED: 'channel_bookmark_created', - CHANNEL_BOOKMARK_DELETED: 'channel_bookmark_deleted', - CHANNEL_BOOKMARK_UPDATED: 'channel_bookmark_updated', - CHANNEL_BOOKMARK_SORTED: 'channel_bookmark_sorted', - MULTIPLE_CHANNELS_VIEWED: 'multiple_channels_viewed', - CHANNEL_MEMBER_UPDATED: 'channel_member_updated', - CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated', - DIRECT_ADDED: 'direct_added', - GROUP_ADDED: 'group_added', - NEW_USER: 'new_user', - ADDED_TO_TEAM: 'added_to_team', - JOIN_TEAM: 'join_team', - LEAVE_TEAM: 'leave_team', - UPDATE_TEAM: 'update_team', - DELETE_TEAM: 'delete_team', - UPDATE_TEAM_SCHEME: 'update_team_scheme', - USER_ADDED: 'user_added', - USER_REMOVED: 'user_removed', - USER_UPDATED: 'user_updated', - USER_ROLE_UPDATED: 'user_role_updated', - MEMBERROLE_UPDATED: 'memberrole_updated', - ROLE_ADDED: 'role_added', - ROLE_REMOVED: 'role_removed', - ROLE_UPDATED: 'role_updated', - TYPING: 'typing', - PREFERENCE_CHANGED: 'preference_changed', - PREFERENCES_CHANGED: 'preferences_changed', - PREFERENCES_DELETED: 'preferences_deleted', - EPHEMERAL_MESSAGE: 'ephemeral_message', - STATUS_CHANGED: 'status_change', - HELLO: 'hello', - REACTION_ADDED: 'reaction_added', - REACTION_REMOVED: 'reaction_removed', - EMOJI_ADDED: 'emoji_added', - PLUGIN_ENABLED: 'plugin_enabled', - PLUGIN_DISABLED: 'plugin_disabled', - LICENSE_CHANGED: 'license_changed', - CONFIG_CHANGED: 'config_changed', - PLUGIN_STATUSES_CHANGED: 'plugin_statuses_changed', - OPEN_DIALOG: 'open_dialog', - RECEIVED_GROUP: 'received_group', - GROUP_MEMBER_ADD: 'group_member_add', - GROUP_MEMBER_DELETED: 'group_member_deleted', - RECEIVED_GROUP_ASSOCIATED_TO_TEAM: 'received_group_associated_to_team', - RECEIVED_GROUP_NOT_ASSOCIATED_TO_TEAM: 'received_group_not_associated_to_team', - RECEIVED_GROUP_ASSOCIATED_TO_CHANNEL: 'received_group_associated_to_channel', - RECEIVED_GROUP_NOT_ASSOCIATED_TO_CHANNEL: 'received_group_not_associated_to_channel', - SIDEBAR_CATEGORY_CREATED: 'sidebar_category_created', - SIDEBAR_CATEGORY_UPDATED: 'sidebar_category_updated', - SIDEBAR_CATEGORY_DELETED: 'sidebar_category_deleted', - SIDEBAR_CATEGORY_ORDER_UPDATED: 'sidebar_category_order_updated', - USER_ACTIVATION_STATUS_CHANGED: 'user_activation_status_change', - CLOUD_PAYMENT_STATUS_UPDATED: 'cloud_payment_status_updated', - CLOUD_SUBSCRIPTION_CHANGED: 'cloud_subscription_changed', APPS_FRAMEWORK_REFRESH_BINDINGS: 'custom_com.mattermost.apps_refresh_bindings', APPS_FRAMEWORK_PLUGIN_ENABLED: 'custom_com.mattermost.apps_plugin_enabled', APPS_FRAMEWORK_PLUGIN_DISABLED: 'custom_com.mattermost.apps_plugin_disabled', - FIRST_ADMIN_VISIT_MARKETPLACE_STATUS_RECEIVED: 'first_admin_visit_marketplace_status_received', - THREAD_UPDATED: 'thread_updated', - THREAD_FOLLOW_CHANGED: 'thread_follow_changed', - THREAD_READ_CHANGED: 'thread_read_changed', - POST_ACKNOWLEDGEMENT_ADDED: 'post_acknowledgement_added', - POST_ACKNOWLEDGEMENT_REMOVED: 'post_acknowledgement_removed', - DRAFT_CREATED: 'draft_created', - DRAFT_UPDATED: 'draft_updated', - DRAFT_DELETED: 'draft_deleted', - SCHEDULED_POST_CREATED: 'scheduled_post_created', - SCHEDULED_POST_UPDATED: 'scheduled_post_updated', - SCHEDULED_POST_DELETED: 'scheduled_post_deleted', - PERSISTENT_NOTIFICATION_TRIGGERED: 'persistent_notification_triggered', - HOSTED_CUSTOMER_SIGNUP_PROGRESS_UPDATED: 'hosted_customer_signup_progress_updated', - CPA_FIELD_CREATED: 'custom_profile_attributes_field_created', - CPA_FIELD_UPDATED: 'custom_profile_attributes_field_updated', - CPA_FIELD_DELETED: 'custom_profile_attributes_field_deleted', - CPA_VALUES_UPDATED: 'custom_profile_attributes_values_updated', - CONTENT_FLAGGING_REPORT_VALUE_CHANGED: 'content_flagging_report_value_updated', - RECAP_UPDATED: 'recap_updated', }; export const TutorialSteps = { diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index caeb26768a6..9e0b5fc1593 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -10,7 +10,7 @@ import type {AppBinding, AppCallRequest, AppCallResponse} from '@mattermost/type import type {Audit} from '@mattermost/types/audits'; import type {UserAutocomplete, AutocompleteSuggestion} from '@mattermost/types/autocomplete'; import type {Bot, BotPatch} from '@mattermost/types/bots'; -import type {ChannelBookmark, ChannelBookmarkCreate, ChannelBookmarkPatch} from '@mattermost/types/channel_bookmarks'; +import type {ChannelBookmark, ChannelBookmarkCreate, ChannelBookmarkPatch, UpdateChannelBookmarkResponse} from '@mattermost/types/channel_bookmarks'; import type {ChannelCategory, OrderedChannelCategories} from '@mattermost/types/channel_categories'; import type { Channel, @@ -2040,7 +2040,7 @@ export default class Client4 { }; updateChannelBookmark = (channelId: string, channelBookmarkId: string, patch: ChannelBookmarkPatch, connectionId: string) => { - return this.doFetch<{updated: ChannelBookmark; deleted: ChannelBookmark}>( + return this.doFetch( `${this.getChannelBookmarkRoute(channelId, channelBookmarkId)}`, {method: 'PATCH', body: JSON.stringify(patch), headers: {'Connection-Id': connectionId}}, ); diff --git a/webapp/platform/client/src/index.ts b/webapp/platform/client/src/index.ts index 610125b08f9..95f53cbc573 100644 --- a/webapp/platform/client/src/index.ts +++ b/webapp/platform/client/src/index.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type * as WebSocketMessages from './websocket_messages'; + export { default as Client4, ClientError, @@ -8,5 +10,7 @@ export { DEFAULT_LIMIT_BEFORE, } from './client4'; -export type {WebSocketMessage} from './websocket'; export {default as WebSocketClient} from './websocket'; +export {WebSocketEvents} from './websocket_events'; +export type {BaseWebSocketMessage, JsonEncodedValue, WebSocketBroadcast, WebSocketMessage} from './websocket_message'; +export {WebSocketMessages}; diff --git a/webapp/platform/client/src/websocket.ts b/webapp/platform/client/src/websocket.ts index ac90c424350..b7775f1d289 100644 --- a/webapp/platform/client/src/websocket.ts +++ b/webapp/platform/client/src/websocket.ts @@ -1,7 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -const WEBSOCKET_HELLO = 'hello'; +import {WebSocketEvents} from './websocket_events'; +import type {WebSocketMessage} from './websocket_message'; export type MessageListener = (msg: WebSocketMessage) => void; export type FirstConnectListener = () => void; @@ -362,7 +363,7 @@ export default class WebSocketClient { } } else if (this.eventCallback || this.messageListeners.size > 0) { // We check the hello packet, which is always the first packet in a stream. - if (msg.event === WEBSOCKET_HELLO && (this.missedEventCallback || this.missedMessageListeners.size > 0)) { + if (msg.event === WebSocketEvents.Hello && (this.missedEventCallback || this.missedMessageListeners.size > 0)) { console.log('got connection id ', msg.data.connection_id); //eslint-disable-line no-console // If we already have a connectionId present, and server sends a different one, // that means it's either a long timeout, or server restart, or sequence number is not found. @@ -675,17 +676,3 @@ export default class WebSocketClient { this.sendMessage('get_statuses_by_ids', data, callback); } } - -export type WebSocketBroadcast = { - omit_users: Record; - user_id: string; - channel_id: string; - team_id: string; -} - -export type WebSocketMessage = { - event: string; - data: T; - broadcast: WebSocketBroadcast; - seq: number; -} diff --git a/webapp/platform/client/src/websocket_events.ts b/webapp/platform/client/src/websocket_events.ts new file mode 100644 index 00000000000..686e102402f --- /dev/null +++ b/webapp/platform/client/src/websocket_events.ts @@ -0,0 +1,93 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export const enum WebSocketEvents { + Typing = 'typing', + Posted = 'posted', + PostEdited = 'post_edited', + PostDeleted = 'post_deleted', + PostUnread = 'post_unread', + BurnOnReadPostRevealed = 'post_revealed', + BurnOnReadPostBurned = 'post_burned', + BurnOnReadPostAllRevealed = 'burn_on_read_all_revealed', + ChannelConverted = 'channel_converted', + ChannelCreated = 'channel_created', + ChannelDeleted = 'channel_deleted', + ChannelRestored = 'channel_restored', + ChannelUpdated = 'channel_updated', + ChannelMemberUpdated = 'channel_member_updated', + ChannelSchemeUpdated = 'channel_scheme_updated', + DirectAdded = 'direct_added', + GroupAdded = 'group_added', + NewUser = 'new_user', + AddedToTeam = 'added_to_team', + LeaveTeam = 'leave_team', + UpdateTeam = 'update_team', + DeleteTeam = 'delete_team', + RestoreTeam = 'restore_team', // This isn't currently used by the web app + UpdateTeamScheme = 'update_team_scheme', + UserAdded = 'user_added', + UserUpdated = 'user_updated', + UserRoleUpdated = 'user_role_updated', + MemberRoleUpdated = 'memberrole_updated', + UserRemoved = 'user_removed', + PreferenceChanged = 'preference_changed', + PreferencesChanged = 'preferences_changed', + PreferencesDeleted = 'preferences_deleted', + EphemeralMessage = 'ephemeral_message', + StatusChange = 'status_change', + Hello = 'hello', + AuthenticationChallenge = 'authentication_challenge', // This isn't currently used by the web app, and it's a message that would be sent from a client to the server + ReactionAdded = 'reaction_added', + ReactionRemoved = 'reaction_removed', + Response = 'response', + EmojiAdded = 'emoji_added', + MultipleChannelsViewed = 'multiple_channels_viewed', + PluginStatusesChanged = 'plugin_statuses_changed', + PluginEnabled = 'plugin_enabled', + PluginDisabled = 'plugin_disabled', + RoleUpdated = 'role_updated', + LicenseChanged = 'license_changed', + ConfigChanged = 'config_changed', + OpenDialog = 'open_dialog', + GuestsDeactivated = 'guests_deactivated', // This isn't currently used by the web app + UserActivationStatusChange = 'user_activation_status_change', + ReceivedGroup = 'received_group', + ReceivedGroupAssociatedToTeam = 'received_group_associated_to_team', + ReceivedGroupNotAssociatedToTeam = 'received_group_not_associated_to_team', + ReceivedGroupAssociatedToChannel = 'received_group_associated_to_channel', + ReceivedGroupNotAssociatedToChannel = 'received_group_not_associated_to_channel', + GroupMemberDeleted = 'group_member_deleted', + GroupMemberAdded = 'group_member_add', + SidebarCategoryCreated = 'sidebar_category_created', + SidebarCategoryUpdated = 'sidebar_category_updated', + SidebarCategoryDeleted = 'sidebar_category_deleted', + SidebarCategoryOrderUpdated = 'sidebar_category_order_updated', + CloudSubscriptionChanged = 'cloud_subscription_changed', + ThreadUpdated = 'thread_updated', + ThreadFollowChanged = 'thread_follow_changed', + ThreadReadChanged = 'thread_read_changed', + FirstAdminVisitMarketplaceStatusReceived = 'first_admin_visit_marketplace_status_received', + DraftCreated = 'draft_created', + DraftUpdated = 'draft_updated', + DraftDeleted = 'draft_deleted', + PostAcknowledgementAdded = 'post_acknowledgement_added', + PostAcknowledgementRemoved = 'post_acknowledgement_removed', + PersistentNotificationTriggered = 'persistent_notification_triggered', + HostedCustomerSignupProgressUpdated = 'hosted_customer_signup_progress_updated', + ChannelBookmarkCreated = 'channel_bookmark_created', + ChannelBookmarkUpdated = 'channel_bookmark_updated', + ChannelBookmarkDeleted = 'channel_bookmark_deleted', + ChannelBookmarkSorted = 'channel_bookmark_sorted', + PresenceIndicator = 'presence', + PostedNotifyAck = 'posted_notify_ack', // This isn't currently used by the web app + ScheduledPostCreated = 'scheduled_post_created', + ScheduledPostUpdated = 'scheduled_post_updated', + ScheduledPostDeleted = 'scheduled_post_deleted', + CPAFieldCreated = 'custom_profile_attributes_field_created', + CPAFieldUpdated = 'custom_profile_attributes_field_updated', + CPAFieldDeleted = 'custom_profile_attributes_field_deleted', + CPAValuesUpdated = 'custom_profile_attributes_values_updated', + ContentFlaggingReportValueUpdated = 'content_flagging_report_value_updated', + RecapUpdated = 'recap_updated', +} diff --git a/webapp/platform/client/src/websocket_message.ts b/webapp/platform/client/src/websocket_message.ts new file mode 100644 index 00000000000..2400cf70a7d --- /dev/null +++ b/webapp/platform/client/src/websocket_message.ts @@ -0,0 +1,117 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {WebSocketEvents} from './websocket_events'; +import type * as Messages from './websocket_messages'; + +export type WebSocketMessage = ( + Messages.Hello | + Messages.AuthenticationChallenge | + Messages.Response | + + Messages.Posted | + Messages.PostEdited | + Messages.PostDeleted | + Messages.PostUnread | + Messages.BurnOnReadPostRevealed | + Messages.BurnOnReadPostBurned | + Messages.BurnOnReadPostAllRevealed | + Messages.EphemeralPost | + Messages.PostReaction | + Messages.PostAcknowledgement | + Messages.PostDraft | + Messages.PersistentNotificationTriggered | + Messages.ScheduledPost | + + Messages.ThreadUpdated | + Messages.ThreadFollowedChanged | + Messages.ThreadReadChanged | + + Messages.ChannelCreated | + Messages.ChannelUpdated | + Messages.ChannelConverted | + Messages.ChannelSchemeUpdated | + Messages.ChannelDeleted | + Messages.ChannelRestored | + Messages.DirectChannelCreated | + Messages.GroupChannelCreated | + Messages.UserAddedToChannel | + Messages.UserRemovedFromChannel | + Messages.ChannelMemberUpdated | + Messages.MultipleChannelsViewed | + + Messages.ChannelBookmarkCreated | + Messages.ChannelBookmarkUpdated | + Messages.ChannelBookmarkDeleted | + Messages.ChannelBookmarkSorted | + + Messages.Team | + Messages.UpdateTeamScheme | + Messages.UserAddedToTeam | + Messages.UserRemovedFromTeam | + Messages.TeamMemberRoleUpdated | + + Messages.NewUser | + Messages.UserUpdated | + Messages.UserActivationStatusChanged | + Messages.UserRoleUpdated | + Messages.StatusChanged | + Messages.Typing | + + Messages.ReceivedGroup | + Messages.GroupAssociatedToTeam | + Messages.GroupAssociatedToChannel | + Messages.GroupMember | + + Messages.PreferenceChanged | + Messages.PreferencesChanged | + + Messages.SidebarCategoryCreated | + Messages.SidebarCategoryUpdated | + Messages.SidebarCategoryDeleted | + Messages.SidebarCategoryOrderUpdated | + + Messages.EmojiAdded | + + Messages.RoleUpdated | + + Messages.ConfigChanged | + Messages.GuestsDeactivated | + Messages.LicenseChanged | + Messages.CloudSubscriptionChanged | + Messages.FirstAdminVisitMarketplaceStatusReceived | + Messages.HostedCustomerSignupProgressUpdated | + + Messages.CPAFieldCreated | + Messages.CPAFieldUpdated | + Messages.CPAFieldDeleted | + Messages.CPAValuesUpdated | + + Messages.ContentFlaggingReportValueUpdated | + + Messages.RecapUpdated | + + Messages.Plugin | + Messages.PluginStatusesChanged | + Messages.OpenDialog | + + BaseWebSocketMessage | + BaseWebSocketMessage +); + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type JsonEncodedValue = string; + +export type BaseWebSocketMessage> = { + event: Event; + data: T; + broadcast: WebSocketBroadcast; + seq: number; +} + +export type WebSocketBroadcast = { + omit_users: Record; + user_id: string; + channel_id: string; + team_id: string; +} diff --git a/webapp/platform/client/src/websocket_messages.ts b/webapp/platform/client/src/websocket_messages.ts new file mode 100644 index 00000000000..7cd38181a6b --- /dev/null +++ b/webapp/platform/client/src/websocket_messages.ts @@ -0,0 +1,441 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {ChannelBookmarkWithFileInfo, UpdateChannelBookmarkResponse} from '@mattermost/types/channel_bookmarks'; +import type {ChannelCategory} from '@mattermost/types/channel_categories'; +import type {Channel, ChannelMembership, ChannelType} from '@mattermost/types/channels'; +import type {Limits, Subscription} from '@mattermost/types/cloud'; +import type {ClientConfig, ClientLicense} from '@mattermost/types/config'; +import type {Draft} from '@mattermost/types/drafts'; +import type {CustomEmoji} from '@mattermost/types/emojis'; +import type {Group, GroupMember as GroupMemberType} from '@mattermost/types/groups'; +import type {OpenDialogRequest} from '@mattermost/types/integrations'; +import type {PluginManifest, PluginStatus} from '@mattermost/types/plugins'; +import type {Post, PostAcknowledgement as PostAcknowledgementType} from '@mattermost/types/posts'; +import type {PreferenceType} from '@mattermost/types/preferences'; +import type {PropertyField, PropertyValue} from '@mattermost/types/properties'; +import type {Reaction} from '@mattermost/types/reactions'; +import type {Role} from '@mattermost/types/roles'; +import type {ScheduledPost as ScheduledPostType} from '@mattermost/types/schedule_post'; +import type {Team as TeamType, TeamMembership} from '@mattermost/types/teams'; +import type {UserThread} from '@mattermost/types/threads'; +import type {UserProfile, UserStatus} from '@mattermost/types/users'; + +import type {WebSocketEvents} from './websocket_events'; +import type {BaseWebSocketMessage, JsonEncodedValue} from './websocket_message'; + +// WebSocket-related messages + +export type Hello = BaseWebSocketMessage; + +export type AuthenticationChallenge = BaseWebSocketMessage; + +export type Response = BaseWebSocketMessage; + +// Post, reactions, and acknowledgement messages + +export type Posted = BaseWebSocketMessage; + + /** + * If the current user is mentioned by this post, this field will contain the ID of that user. Otherwise, + * it will be empty. + */ + mentions?: JsonEncodedValue; + + /** + * If the current user is following this post, this field will contain the ID of that user. Otherwise, + * it will be empty. + */ + followers?: JsonEncodedValue; + + should_ack?: boolean; +}>; + +export type PostEdited = BaseWebSocketMessage; +}>; + +export type PostDeleted = BaseWebSocketMessage; + + /** The user ID of the user who deleted the post, only sent to admin users. */ + delete_by?: string; +}>; + +export type PostUnread = BaseWebSocketMessage; + +export type BurnOnReadPostRevealed = BaseWebSocketMessage; + +export type BurnOnReadPostBurned = BaseWebSocketMessage; + +export type BurnOnReadPostAllRevealed = BaseWebSocketMessage; + +export type EphemeralPost = BaseWebSocketMessage; +}>; + +export type PostReaction = + BaseWebSocketMessage; + }>; + +export type PostAcknowledgement = + BaseWebSocketMessage; + }>; + +export type PostDraft = + BaseWebSocketMessage; + }>; + +export type PersistentNotificationTriggered = + BaseWebSocketMessage; + channel_type: ChannelType; + channel_display_name: string; + channel_name: string; + sender_name: string; + team_id: string; + otherFile?: boolean; + image?: boolean; + mentions?: JsonEncodedValue; + }>; + +export type ScheduledPost = + BaseWebSocketMessage; + }>; + +// Thread messages + +export type ThreadUpdated = BaseWebSocketMessage; + + previous_unread_mentions?: number; + previous_unread_replies?: number; +}>; + +export type ThreadFollowedChanged = BaseWebSocketMessage; + +export type ThreadReadChanged = BaseWebSocketMessage; + +// Channel and channel member messages + +export type ChannelCreated = BaseWebSocketMessage; + +export type ChannelUpdated = BaseWebSocketMessage; + channel_id?: string; +}>; + +export type ChannelConverted = BaseWebSocketMessage; + +export type ChannelSchemeUpdated = BaseWebSocketMessage; + +export type ChannelDeleted = BaseWebSocketMessage; + +export type ChannelRestored = BaseWebSocketMessage; + +export type DirectChannelCreated = BaseWebSocketMessage; + +export type GroupChannelCreated = BaseWebSocketMessage; +}>; + +export type UserAddedToChannel = BaseWebSocketMessage; + +export type UserRemovedFromChannel = BaseWebSocketMessage; + +export type ChannelMemberUpdated = BaseWebSocketMessage; +}>; + +export type MultipleChannelsViewed = BaseWebSocketMessage; +}>; + +// Channel bookmark messages + +export type ChannelBookmarkCreated = BaseWebSocketMessage; +}>; + +export type ChannelBookmarkUpdated = BaseWebSocketMessage; +}>; + +export type ChannelBookmarkDeleted = BaseWebSocketMessage; +}>; + +export type ChannelBookmarkSorted = BaseWebSocketMessage; +}>; + +// Team and team member messages + +export type Team = + BaseWebSocketMessage; + }>; + +export type UserAddedToTeam = BaseWebSocketMessage; + +export type UserRemovedFromTeam = BaseWebSocketMessage; + +export type UpdateTeamScheme = BaseWebSocketMessage; +}>; + +export type TeamMemberRoleUpdated = BaseWebSocketMessage; +}>; + +// User and status messages + +export type NewUser = BaseWebSocketMessage; + +export type UserUpdated = BaseWebSocketMessage; + +export type UserActivationStatusChanged = BaseWebSocketMessage; + +export type UserRoleUpdated = BaseWebSocketMessage; + +export type StatusChanged = BaseWebSocketMessage; + +export type Typing = BaseWebSocketMessage; + +// Group-related messages + +export type ReceivedGroup = BaseWebSocketMessage; +}>; + +export type GroupAssociatedToTeam = + BaseWebSocketMessage; + +export type GroupAssociatedToChannel = + BaseWebSocketMessage; + +export type GroupMember = + BaseWebSocketMessage; + }>; + +// Preference messages + +export type PreferenceChanged = BaseWebSocketMessage; +}>; + +export type PreferencesChanged = + BaseWebSocketMessage; + }>; + +// Channel sidebar messages + +export type SidebarCategoryCreated = BaseWebSocketMessage; + +export type SidebarCategoryUpdated = BaseWebSocketMessage; +}>; + +export type SidebarCategoryDeleted = BaseWebSocketMessage; + +export type SidebarCategoryOrderUpdated = BaseWebSocketMessage; + +// Emoji messages + +export type EmojiAdded = BaseWebSocketMessage>; +}>; + +// Role messages + +export type RoleUpdated = BaseWebSocketMessage; +}>; + +// Configuration and license messages + +export type ConfigChanged = BaseWebSocketMessage; + +export type GuestsDeactivated = BaseWebSocketMessage; + +export type LicenseChanged = BaseWebSocketMessage; + +export type CloudSubscriptionChanged = BaseWebSocketMessage; + +export type FirstAdminVisitMarketplaceStatusReceived = + BaseWebSocketMessage; + }>; + +export type HostedCustomerSignupProgressUpdated = + BaseWebSocketMessage + +// Custom properties messages + +export type CPAFieldCreated = BaseWebSocketMessage; + +export type CPAFieldUpdated = BaseWebSocketMessage; + +export type CPAFieldDeleted = BaseWebSocketMessage; + +export type CPAValuesUpdated = BaseWebSocketMessage>; +}>; + +// Content flagging messages + +export type ContentFlaggingReportValueUpdated = + BaseWebSocketMessage>>; + target_id: string; + }>; + +// Recap messages + +export type RecapUpdated = BaseWebSocketMessage; + +// Plugin and integration messages + +export type Plugin = BaseWebSocketMessage; + +export type PluginStatusesChanged = BaseWebSocketMessage; + +export type OpenDialog = BaseWebSocketMessage; +}>; + +/** + * Unknown is used for WebSocket messages which don't come from Mattermost itself. It's primarily intended for use + * by plugins. + */ +export type Unknown = BaseWebSocketMessage; diff --git a/webapp/platform/types/src/channel_bookmarks.ts b/webapp/platform/types/src/channel_bookmarks.ts index 69f64697362..d051f135240 100644 --- a/webapp/platform/types/src/channel_bookmarks.ts +++ b/webapp/platform/types/src/channel_bookmarks.ts @@ -48,6 +48,11 @@ export type ChannelBookmarkPatch = { emoji?: string; } +export type UpdateChannelBookmarkResponse = { + updated: ChannelBookmark; + deleted: ChannelBookmark; +}; + export type ChannelBookmarkWithFileInfo = ChannelBookmark & { file: FileInfo; } diff --git a/webapp/platform/types/src/integrations.ts b/webapp/platform/types/src/integrations.ts index 70d78a1a515..dc1a8c45c25 100644 --- a/webapp/platform/types/src/integrations.ts +++ b/webapp/platform/types/src/integrations.ts @@ -142,14 +142,16 @@ export type IntegrationsState = { commands: IDMappedObjects; dialogArguments?: DialogArgs; dialogTriggerId: string; - dialog?: { - url: string; - dialog: Dialog; - trigger_id: string; - }; + dialog?: OpenDialogRequest; }; -type Dialog = { +export type OpenDialogRequest = { + trigger_id: string; + url: string; + dialog: Dialog; +} + +export type Dialog = { callback_id?: string; elements?: DialogElement[]; title: string;