mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-27 05:37:15 -05:00
MM-69751 Replace concurrent React user setting with feature flag and add to root.html (#37422)
* MM-69751 Replace concurrent React user setting with feature flag and add to root.html * Address Coderabbit feedback
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -23,8 +24,8 @@ import (
|
||||
|
||||
// getSubpathScript renders the inline script that defines window.publicPath to change how webpack loads assets.
|
||||
func getSubpathScript(subpath string) string {
|
||||
if subpath == "" {
|
||||
subpath = "/"
|
||||
if subpath == "" || subpath == "/" {
|
||||
return ""
|
||||
}
|
||||
|
||||
newPath := path.Join(subpath, "static") + "/"
|
||||
@@ -32,21 +33,42 @@ func getSubpathScript(subpath string) string {
|
||||
return fmt.Sprintf("window.publicPath='%s'", newPath)
|
||||
}
|
||||
|
||||
// GetSubpathScriptHash computes the script-src addition required for the subpath script to bypass CSP protections.
|
||||
func GetSubpathScriptHash(subpath string) string {
|
||||
// No hash is required for the default subpath.
|
||||
if subpath == "" || subpath == "/" {
|
||||
// getConcurrentReactScript renders the inline script that defines window.enableConcurrentReact to change how React
|
||||
// initializes the web app.
|
||||
func getConcurrentReactScript(enableConcurrentReact bool) string {
|
||||
if !enableConcurrentReact {
|
||||
return ""
|
||||
}
|
||||
|
||||
scriptHash := sha256.Sum256([]byte(getSubpathScript(subpath)))
|
||||
return "window.enableConcurrentReact=true"
|
||||
}
|
||||
|
||||
// GetScriptHash computes the script-src addition required for an inline script injected into the root.html.
|
||||
func GetScriptHash(script string) string {
|
||||
// No hash is required when there's no script
|
||||
if script == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
scriptHash := sha256.Sum256([]byte(script))
|
||||
|
||||
return fmt.Sprintf(" 'sha256-%s'", base64.StdEncoding.EncodeToString(scriptHash[:]))
|
||||
}
|
||||
|
||||
// UpdateAssetsSubpathInDir rewrites assets in the given directory to assume the application is
|
||||
// hosted at the given subpath instead of at the root. No changes are written unless necessary.
|
||||
func UpdateAssetsSubpathInDir(subpath, directory string) error {
|
||||
// GetStaticScriptHashes computes the combined script-src additions required for the inline scripts injected
|
||||
// into root.html to bypass CSP protections.
|
||||
func GetStaticScriptHashes(subpath string, enableConcurrentReact bool) string {
|
||||
return GetScriptHash(getSubpathScript(subpath)) + GetScriptHash(getConcurrentReactScript(enableConcurrentReact))
|
||||
}
|
||||
|
||||
// UpdateAssetsSubpathInDir rewrites static assets in the given directory based on configuration options.
|
||||
// The following changes are made:
|
||||
// - HTML and CSS files are rewritten to assume the application is hosted at the given subpath instead of at the root.
|
||||
// - HTML is rewritten to add the enableConcurrentReact setting, needed while loading the web app. If omitted, the
|
||||
// existing value is preserved.
|
||||
//
|
||||
// No changes are written unless necessary.
|
||||
func UpdateAssetsSubpathInDir(subpath, directory string, enableConcurrentReactPtr *bool) error {
|
||||
if subpath == "" {
|
||||
subpath = "/"
|
||||
}
|
||||
@@ -82,8 +104,21 @@ func UpdateAssetsSubpathInDir(subpath, directory string) error {
|
||||
pathToReplace := path.Join(oldSubpath, "static") + "/"
|
||||
newPath := path.Join(subpath, "static") + "/"
|
||||
|
||||
// Keep the previous value of enableConcurrentReact if it isn't provided
|
||||
enableConcurrentReact := false
|
||||
if enableConcurrentReactPtr == nil {
|
||||
reConcurrentReactScript := regexp.MustCompile("window.enableConcurrentReact=(true|false)")
|
||||
if matches := reConcurrentReactScript.FindSubmatch(oldRootHTML); matches != nil {
|
||||
if value, convErr := strconv.ParseBool(string(matches[1])); convErr == nil {
|
||||
enableConcurrentReact = value
|
||||
}
|
||||
}
|
||||
} else {
|
||||
enableConcurrentReact = *enableConcurrentReactPtr
|
||||
}
|
||||
|
||||
// Update the root.html file
|
||||
if err := updateRootFile(string(oldRootHTML), rootHTMLPath, alreadyRewritten, pathToReplace, newPath, subpath); err != nil {
|
||||
if err := updateRootFile(string(oldRootHTML), rootHTMLPath, alreadyRewritten, pathToReplace, newPath, subpath, enableConcurrentReact); err != nil {
|
||||
return fmt.Errorf("failed to update root.html: %w", err)
|
||||
}
|
||||
|
||||
@@ -95,7 +130,7 @@ func UpdateAssetsSubpathInDir(subpath, directory string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateRootFile(oldRootHTML string, rootHTMLPath string, alreadyRewritten bool, pathToReplace, newPath, subpath string) error {
|
||||
func updateRootFile(oldRootHTML string, rootHTMLPath string, alreadyRewritten bool, pathToReplace, newPath, subpath string, enableConcurrentReact bool) error {
|
||||
newRootHTML := oldRootHTML
|
||||
|
||||
reCSP := regexp.MustCompile(`<meta http-equiv="Content-Security-Policy" content="script-src 'self'([^"]*)">`)
|
||||
@@ -105,7 +140,7 @@ func updateRootFile(oldRootHTML string, rootHTMLPath string, alreadyRewritten bo
|
||||
|
||||
newRootHTML = reCSP.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf(
|
||||
`<meta http-equiv="Content-Security-Policy" content="script-src 'self'%s">`,
|
||||
GetSubpathScriptHash(subpath),
|
||||
GetStaticScriptHashes(subpath, enableConcurrentReact),
|
||||
))
|
||||
|
||||
// Rewrite the root.html references to `/static/*` to include the given subpath.
|
||||
@@ -114,7 +149,6 @@ func updateRootFile(oldRootHTML string, rootHTMLPath string, alreadyRewritten bo
|
||||
newRootHTML = strings.Replace(newRootHTML, pathToReplace, newPath, -1)
|
||||
|
||||
publicPathInWindowsScriptRegex := regexp.MustCompile(`(?s)<script id="publicPathInWindowScript">(.*?)</script>`)
|
||||
|
||||
if alreadyRewritten && subpath == "/" {
|
||||
// Remove window global publicPath definition if subpath is root
|
||||
newRootHTML = publicPathInWindowsScriptRegex.ReplaceAllLiteralString(newRootHTML, "<script id=\"publicPathInWindowScript\"></script>")
|
||||
@@ -124,6 +158,10 @@ func updateRootFile(oldRootHTML string, rootHTMLPath string, alreadyRewritten bo
|
||||
newRootHTML = publicPathInWindowsScriptRegex.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf("<script id=\"publicPathInWindowScript\">%s</script>", subpathScript))
|
||||
}
|
||||
|
||||
// Inject (or clear) the script defining `window.enableConcurrentReact` to match the feature flag.
|
||||
concurrentReactScriptRegex := regexp.MustCompile(`(?s)<script id="enableConcurrentReactScript">(.*?)</script>`)
|
||||
newRootHTML = concurrentReactScriptRegex.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf("<script id=\"enableConcurrentReactScript\">%s</script>", getConcurrentReactScript(enableConcurrentReact)))
|
||||
|
||||
if newRootHTML == oldRootHTML {
|
||||
mlog.Debug("No need to rewrite unmodified root.html", mlog.String("from_subpath", pathToReplace), mlog.String("to_subpath", newPath))
|
||||
return nil
|
||||
@@ -169,8 +207,8 @@ func updateManifestAndCSSFiles(staticDir, pathToReplace, newPath, subpath string
|
||||
|
||||
// UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted
|
||||
// at the given subpath instead of at the root. No changes are written unless necessary.
|
||||
func UpdateAssetsSubpath(subpath string) error {
|
||||
return UpdateAssetsSubpathInDir(subpath, model.ClientDir)
|
||||
func UpdateAssetsSubpath(subpath string, enableConcurrentReact *bool) error {
|
||||
return UpdateAssetsSubpathInDir(subpath, model.ClientDir, enableConcurrentReact)
|
||||
}
|
||||
|
||||
// UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
|
||||
@@ -193,7 +231,12 @@ func UpdateAssetsSubpathFromConfig(config *model.Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return UpdateAssetsSubpath(subpath)
|
||||
enableConcurrentReact := false
|
||||
if config != nil && config.FeatureFlags != nil {
|
||||
enableConcurrentReact = config.FeatureFlags.EnableConcurrentReact
|
||||
}
|
||||
|
||||
return UpdateAssetsSubpath(subpath, &enableConcurrentReact)
|
||||
}
|
||||
|
||||
func GetSubpathFromConfig(config *model.Config) (string, error) {
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestUpdateAssetsSubpath(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
|
||||
err := utils.UpdateAssetsSubpath("/")
|
||||
err := utils.UpdateAssetsSubpath("/", nil)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestUpdateAssetsSubpath(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tempDir, model.ClientDir, "root.html"), []byte(testCase.RootHTML), 0700))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tempDir, model.ClientDir, "main.css"), []byte(testCase.MainCSS), 0700))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tempDir, model.ClientDir, "manifest.json"), []byte(testCase.ManifestJSON), 0700))
|
||||
err := utils.UpdateAssetsSubpath(testCase.Subpath)
|
||||
err := utils.UpdateAssetsSubpath(testCase.Subpath, nil)
|
||||
if testCase.ExpectedError != nil {
|
||||
require.Equal(t, testCase.ExpectedError, err)
|
||||
} else {
|
||||
@@ -186,6 +186,57 @@ func TestUpdateAssetsSubpath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateAssetsSubpathConcurrentReact(t *testing.T) {
|
||||
// concurrentReactHash is the CSP script-src addition for the "window.enableConcurrentReact=true" inline script.
|
||||
const concurrentReactHash = " 'sha256-VKORZJUo6WeDwDHwpxEgzZDt8C1kBbDOmUq72sfrx8M='"
|
||||
|
||||
// rootHTML renders a minimal root.html with the given CSP addition and concurrent React script body.
|
||||
rootHTML := func(cspExtra, concurrentReactBody string) string {
|
||||
return `<!DOCTYPE html> <html lang=en> <head> ` +
|
||||
`<meta http-equiv="Content-Security-Policy" content="script-src 'self'` + cspExtra + `"> ` +
|
||||
`<script id="publicPathInWindowScript"></script> ` +
|
||||
`<script id="enableConcurrentReactScript">` + concurrentReactBody + `</script> ` +
|
||||
`<link href="/static/main.js" rel="stylesheet"></head> <body></body> </html>`
|
||||
}
|
||||
|
||||
writeAndUpdate := func(t *testing.T, initial string, enableConcurrentReact *bool) string {
|
||||
t.Helper()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
require.NoError(t, os.Mkdir(model.ClientDir, 0700))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tempDir, model.ClientDir, "root.html"), []byte(initial), 0700))
|
||||
|
||||
require.NoError(t, utils.UpdateAssetsSubpath("/", enableConcurrentReact))
|
||||
|
||||
contents, err := os.ReadFile(filepath.Join(tempDir, model.ClientDir, "root.html"))
|
||||
require.NoError(t, err)
|
||||
return string(contents)
|
||||
}
|
||||
|
||||
t.Run("enabled injects the script and CSP hash", func(t *testing.T) {
|
||||
got := writeAndUpdate(t, rootHTML("", ""), model.NewPointer(true))
|
||||
require.Equal(t, rootHTML(concurrentReactHash, "window.enableConcurrentReact=true"), got)
|
||||
})
|
||||
|
||||
t.Run("disabled clears the script and CSP hash", func(t *testing.T) {
|
||||
got := writeAndUpdate(t, rootHTML(concurrentReactHash, "window.enableConcurrentReact=true"), model.NewPointer(false))
|
||||
require.Equal(t, rootHTML("", ""), got)
|
||||
})
|
||||
|
||||
t.Run("nil preserves an enabled script", func(t *testing.T) {
|
||||
initial := rootHTML(concurrentReactHash, "window.enableConcurrentReact=true")
|
||||
got := writeAndUpdate(t, initial, nil)
|
||||
require.Equal(t, initial, got)
|
||||
})
|
||||
|
||||
t.Run("nil preserves a disabled script", func(t *testing.T) {
|
||||
initial := rootHTML("", "")
|
||||
got := writeAndUpdate(t, initial, nil)
|
||||
require.Equal(t, initial, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSubpathFromConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Description string
|
||||
|
||||
@@ -48,9 +48,15 @@ func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) h
|
||||
}
|
||||
|
||||
func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
// Determine the CSP SHA directive needed for subpath support, if any. This value is fixed
|
||||
// on server start and intentionally requires a restart to take effect.
|
||||
subpath, _ := utils.GetSubpathFromConfig(w.srv.Config())
|
||||
// Determine the CSP SHA directives needed for the inline scripts injected into root.html.
|
||||
// These values are fixed on server start and intentionally require a restart to take effect.
|
||||
cfg := w.srv.Config()
|
||||
subpath, _ := utils.GetSubpathFromConfig(cfg)
|
||||
|
||||
enableConcurrentReact := false
|
||||
if cfg.FeatureFlags != nil {
|
||||
enableConcurrentReact = cfg.FeatureFlags.EnableConcurrentReact
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
Srv: w.srv,
|
||||
@@ -61,7 +67,7 @@ func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Reque
|
||||
RequireMfa: false,
|
||||
IsStatic: true,
|
||||
|
||||
cspShaDirective: utils.GetSubpathScriptHash(subpath),
|
||||
cspShaDirective: utils.GetStaticScriptHashes(subpath, enableConcurrentReact),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -338,6 +338,42 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
||||
assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self'"}, response.Header()["Content-Security-Policy"])
|
||||
})
|
||||
|
||||
t.Run("static, with EnableConcurrentReact enabled", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
// Feature flags are read-only in the config store, so mutate the live config directly.
|
||||
th.App.Config().FeatureFlags.EnableConcurrentReact = true
|
||||
|
||||
web := New(th.Server)
|
||||
|
||||
// NewStaticHandler computes the CSP SHA directive from the current config, so the
|
||||
// concurrent React inline script's hash must be present in the script-src directive.
|
||||
handler := web.NewStaticHandler(handlerForCSPHeader)
|
||||
|
||||
request := httptest.NewRequest("POST", "/", nil)
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
assert.Equal(t, 200, response.Code)
|
||||
assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self' 'sha256-VKORZJUo6WeDwDHwpxEgzZDt8C1kBbDOmUq72sfrx8M='"}, response.Header()["Content-Security-Policy"])
|
||||
})
|
||||
|
||||
t.Run("static, with EnableConcurrentReact disabled", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
// Feature flags are read-only in the config store, so mutate the live config directly.
|
||||
th.App.Config().FeatureFlags.EnableConcurrentReact = false
|
||||
|
||||
web := New(th.Server)
|
||||
|
||||
handler := web.NewStaticHandler(handlerForCSPHeader)
|
||||
|
||||
request := httptest.NewRequest("POST", "/", nil)
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
assert.Equal(t, 200, response.Code)
|
||||
assert.Equal(t, []string{"frame-ancestors 'self' " + *th.App.Config().ServiceSettings.FrameAncestors + "; script-src 'self'"}, response.Header()["Content-Security-Policy"])
|
||||
})
|
||||
|
||||
t.Run("static, with subpath and frame ancestors", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
|
||||
|
||||
@@ -554,7 +554,7 @@ func configSubpathCmdF(cmd *cobra.Command, _ []string) error {
|
||||
assetsDir, _ := cmd.Flags().GetString("assets-dir")
|
||||
path, _ := cmd.Flags().GetString("path")
|
||||
|
||||
if err := utils.UpdateAssetsSubpathInDir(path, assetsDir); err != nil {
|
||||
if err := utils.UpdateAssetsSubpathInDir(path, assetsDir, nil); err != nil {
|
||||
return errors.Wrap(err, "failed to update assets subpath")
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,9 @@ type FeatureFlags struct {
|
||||
ClusterGracefulDrain bool
|
||||
|
||||
ChannelBookmarks bool
|
||||
|
||||
// Enable React concurrent rendering
|
||||
EnableConcurrentReact bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@@ -196,6 +199,8 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.MmBlocksEnabled = true
|
||||
|
||||
f.ChannelBookmarks = true
|
||||
|
||||
f.EnableConcurrentReact = false
|
||||
}
|
||||
|
||||
// IsChannelPermissionPoliciesEnabled reports whether channel-scope
|
||||
|
||||
@@ -160,12 +160,12 @@ export function handleLoginLogoutSignal(e: StorageEvent): ThunkActionFunc<void>
|
||||
|
||||
export function logIfConcurrentReactEnabled(): ActionFuncAsync<boolean> {
|
||||
return async () => {
|
||||
const concurrentReactEnabled = localStorage.getItem('enable_concurrent_react_experimental') === 'true';
|
||||
const concurrentReactEnabled = window.enableConcurrentReact;
|
||||
|
||||
if (concurrentReactEnabled) {
|
||||
Client4.logClientError(
|
||||
"This user's session is using experimental concurrent React which may cause visual bugs. It can be " +
|
||||
'disabled from Settings > Advanced or by clearing their browser storage.',
|
||||
"This user's session is using experimental concurrent React which may cause visual bugs. It is enabled " +
|
||||
'for all users due to a feature flag.',
|
||||
LogLevel.Debug,
|
||||
);
|
||||
}
|
||||
|
||||
-184
@@ -1,184 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import useDidUpdate from 'components/common/hooks/useDidUpdate';
|
||||
import SettingItemMax from 'components/setting_item_max';
|
||||
import SettingItemMin from 'components/setting_item_min';
|
||||
|
||||
import {a11yFocus} from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
const section = 'concurrentReactExperimental';
|
||||
|
||||
const storageKey = 'enable_concurrent_react_experimental';
|
||||
|
||||
type Props = {
|
||||
activeSection: string;
|
||||
adminMode?: boolean;
|
||||
onUpdateSection: (section?: string) => void;
|
||||
renderOnOffLabel: (enabled: string) => React.JSX.Element;
|
||||
};
|
||||
|
||||
export default function EnableConcurrentReactExperimentalSection({
|
||||
activeSection,
|
||||
adminMode,
|
||||
onUpdateSection,
|
||||
renderOnOffLabel,
|
||||
}: Props) {
|
||||
const enableDeveloperMode = useSelector((state: GlobalState) => getConfig(state).EnableDeveloper === 'true');
|
||||
|
||||
const currentValue = useLocalStorageItem(storageKey, 'false');
|
||||
const [enabled, setEnabled] = useState(currentValue);
|
||||
|
||||
const active = activeSection === section;
|
||||
const [prevActive, setPrevActive] = useState(active);
|
||||
if (active !== prevActive) {
|
||||
setPrevActive(active);
|
||||
setEnabled(currentValue);
|
||||
}
|
||||
|
||||
const minRef = React.createRef<SettingItemMin>();
|
||||
useDidUpdate(() => {
|
||||
if (!active) {
|
||||
minRef.current?.focus();
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEnabled(e.currentTarget.value);
|
||||
|
||||
a11yFocus(e.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
localStorage.setItem(storageKey, enabled);
|
||||
|
||||
// Manually dispatch an event to update currentvalue because storage events are only fired for other tabs
|
||||
requestAnimationFrame(() => {
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: storageKey,
|
||||
newValue: enabled,
|
||||
storageArea: localStorage,
|
||||
}));
|
||||
|
||||
onUpdateSection();
|
||||
});
|
||||
}, [enabled, onUpdateSection]);
|
||||
|
||||
if (adminMode || !enableDeveloperMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
return (
|
||||
<SettingItemMin
|
||||
ref={minRef}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='user.settings.advance.concurrentReactExperimental'
|
||||
defaultMessage='Enable Concurrent React (Experimental)'
|
||||
/>
|
||||
}
|
||||
describe={renderOnOffLabel(currentValue)}
|
||||
section={section}
|
||||
updateSection={onUpdateSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingItemMax
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='user.settings.advance.concurrentReactExperimental'
|
||||
defaultMessage='Enable Concurrent React (Experimental)'
|
||||
/>
|
||||
}
|
||||
inputs={[
|
||||
<fieldset key='joinLeaveSetting'>
|
||||
<legend className='form-legend hidden-label'>
|
||||
<FormattedMessage
|
||||
id='user.settings.advance.concurrentReactExperimental'
|
||||
defaultMessage='Enable Concurrent React (Experimental)'
|
||||
/>
|
||||
</legend>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
id='joinLeaveOn'
|
||||
type='radio'
|
||||
value={'true'}
|
||||
name={section}
|
||||
checked={enabled === 'true'}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user.settings.advance.on'
|
||||
defaultMessage='On'
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
id='joinLeaveOff'
|
||||
type='radio'
|
||||
value={'false'}
|
||||
name={section}
|
||||
checked={enabled === 'false'}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user.settings.advance.off'
|
||||
defaultMessage='Off'
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='mt-5'>
|
||||
<FormattedMessage
|
||||
id='user.settings.advance.concurrentReactExperimentalDesc1'
|
||||
defaultMessage={'When "On", enable concurrent React support for development. This is known to cause issues and should only be done if you know what you\'re doing.'}
|
||||
tagName='p'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='user.settings.advance.concurrentReactExperimentalDesc2'
|
||||
defaultMessage={'You may need to refresh the page before this setting takes effect.'}
|
||||
tagName='p'
|
||||
/>
|
||||
</div>
|
||||
</fieldset>,
|
||||
]}
|
||||
submit={handleSubmit}
|
||||
saving={false}
|
||||
updateSection={onUpdateSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useLocalStorageItem(key: string, defaultValue: string) {
|
||||
const [value, setValue] = useState(() => (localStorage.getItem(key) || defaultValue));
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorageEvent = (e: StorageEvent) => {
|
||||
if (e.key === key && e.storageArea === localStorage) {
|
||||
setValue(e.newValue || defaultValue);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageEvent);
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageEvent);
|
||||
};
|
||||
}, [key, defaultValue]);
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import React from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {FormattedMessage, defineMessages} from 'react-intl';
|
||||
@@ -20,7 +18,6 @@ import SettingItemMax from 'components/setting_item_max';
|
||||
import Constants, {AdvancedSections, Preferences} from 'utils/constants';
|
||||
import {a11yFocus} from 'utils/utils';
|
||||
|
||||
import EnableConcurrentReactExperimentalSection from './enable_concurrent_react_experimental_section';
|
||||
import JoinLeaveSection from './join_leave_section';
|
||||
import PerformanceDebuggingSection from './performance_debugging_section';
|
||||
import WysiwygEditorSection from './wysiwyg_editor_section';
|
||||
@@ -796,13 +793,6 @@ export default class AdvancedSettingsDisplay extends React.PureComponent<Props,
|
||||
{syncDraftsSectionDivider}
|
||||
{syncDraftsSection}
|
||||
{formattingSectionDivider}
|
||||
<EnableConcurrentReactExperimentalSection
|
||||
activeSection={this.props.activeSection}
|
||||
onUpdateSection={this.handleUpdateSection}
|
||||
adminMode={this.props.adminMode}
|
||||
renderOnOffLabel={this.renderOnOffLabel}
|
||||
/>
|
||||
<div className='divider-light'/>
|
||||
{deactivateAccountSection}
|
||||
<div className='divider-dark'/>
|
||||
{makeConfirmationModal}
|
||||
|
||||
@@ -24,6 +24,7 @@ import '@mattermost/components/dist/index.esm.css';
|
||||
declare global {
|
||||
interface Window {
|
||||
publicPath?: string;
|
||||
enableConcurrentReact?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +57,9 @@ function preRenderSetup(onPreRenderSetupReady: () => void) {
|
||||
function renderReactRootComponent() {
|
||||
const container = document.getElementById('root')!;
|
||||
|
||||
if (localStorage.getItem('enable_concurrent_react_experimental') === 'true') {
|
||||
if (window.enableConcurrentReact) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'Enabling concurrent React 18. To disable this, go to Settings > Advanced > Enable Concurrent React ' +
|
||||
'(Experimental) or clear your browser storage.',
|
||||
);
|
||||
console.log('Enabling concurrent React 18 due to server-wide feature flag');
|
||||
|
||||
// Enable this experimentally since it may cause other issues
|
||||
ReactDOMClient.createRoot(container).render(<App/>);
|
||||
|
||||
@@ -7198,9 +7198,6 @@
|
||||
"user_settings.notifications.test_notification.send_button.sending": "Sending a test notification",
|
||||
"user_settings.notifications.test_notification.send_button.sent": "Test notification sent",
|
||||
"user_settings.notifications.test_notification.title": "Troubleshooting notifications",
|
||||
"user.settings.advance.concurrentReactExperimental": "Enable Concurrent React (Experimental)",
|
||||
"user.settings.advance.concurrentReactExperimentalDesc1": "When \"On\", enable concurrent React support for development. This is known to cause issues and should only be done if you know what you're doing.",
|
||||
"user.settings.advance.concurrentReactExperimentalDesc2": "You may need to refresh the page before this setting takes effect.",
|
||||
"user.settings.advance.confirmDeactivateAccountTitle": "Confirm Deactivation",
|
||||
"user.settings.advance.confirmDeactivateDesc": "Are you sure you want to deactivate your account? This can only be reversed by your System Administrator.",
|
||||
"user.settings.advance.deactivate_member_modal.deactivateButton": "Yes, deactivate my account",
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<!-- Initialize subpath empty script for subpath support which will be replaced by server/channels/utils/subpath.go -->
|
||||
<script id="publicPathInWindowScript"></script>
|
||||
|
||||
<!-- Initialize empty script for the EnableConcurrentReact feature flag which will be replaced by server/channels/utils/subpath.go -->
|
||||
<script id="enableConcurrentReactScript"></script>
|
||||
|
||||
<!--
|
||||
This "empty" link element is used as a dynamic inject placeholder for the syntax highlighting css styles:
|
||||
1. The highlight.js lib finds this element by its 'code_theme' class.
|
||||
|
||||
Reference in New Issue
Block a user