mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-04 10:23:31 -05:00
[MM-67425] Add an unsupported Desktop App setting and screen for users (#35382)
* [MM-67425] Add an unsupported Desktop App setting and screen for users * Remove console.log statements * Fix e2e test config * Add e2e test * PR feedback * Update server/channels/web/static.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * PR feedback * Fix i18n * PR feedback * PR feedback * PR feedback * Gofmt --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent
062abe90bd
commit
885ebdd4f1
@@ -214,6 +214,7 @@ const defaultServerConfig: AdminConfig = {
|
||||
EnableWebHubChannelIteration: false,
|
||||
FrameAncestors: '',
|
||||
DeleteAccountLink: '',
|
||||
MinimumDesktopAppVersion: '',
|
||||
},
|
||||
TeamSettings: {
|
||||
SiteName: 'Mattermost',
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
const MINIMUM_VERSION = '6.0.0';
|
||||
const OLD_DESKTOP_VERSION = '5.0.0';
|
||||
const DESKTOP_APP_USER_AGENT = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Mattermost/${OLD_DESKTOP_VERSION} Chrome/120.0.0.0 Electron/28.0.0`;
|
||||
|
||||
test('Desktop App update required screen shows when connecting with older version', async ({pw}) => {
|
||||
const {adminClient} = await pw.initSetup();
|
||||
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {
|
||||
MinimumDesktopAppVersion: MINIMUM_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
const config = await adminClient.getConfig();
|
||||
const appDownloadLink = config.NativeAppSettings.AppDownloadLink;
|
||||
|
||||
const context = await pw.testBrowser.browser.newContext({
|
||||
userAgent: DESKTOP_APP_USER_AGENT,
|
||||
});
|
||||
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.getByRole('heading', {name: 'Update Required'})).toBeVisible();
|
||||
|
||||
const message = page.locator('.message');
|
||||
await expect(message).toContainText(OLD_DESKTOP_VERSION);
|
||||
await expect(message).toContainText(MINIMUM_VERSION);
|
||||
|
||||
const downloadLink = page.getByRole('link', {name: 'Download Updated App'});
|
||||
await expect(downloadLink).toBeVisible();
|
||||
await expect(downloadLink).toHaveAttribute('href', appDownloadLink);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
@@ -107,13 +107,30 @@ func getOSName(ua *uasurfer.UserAgent, userAgentString string) string {
|
||||
return osNames[uasurfer.OSUnknown]
|
||||
}
|
||||
|
||||
const desktopAppVersionPrefix = "Mattermost/"
|
||||
|
||||
var versionPrefixes = []string{
|
||||
"Mattermost Mobile/",
|
||||
"Mattermost/",
|
||||
desktopAppVersionPrefix,
|
||||
"mmctl/",
|
||||
"Franz/",
|
||||
}
|
||||
|
||||
func GetDesktopAppVersion(userAgentString string) (version string, ok bool) {
|
||||
idx := strings.Index(userAgentString, desktopAppVersionPrefix)
|
||||
if idx == -1 {
|
||||
return "", false
|
||||
}
|
||||
if idx > 0 && userAgentString[idx-1] != ' ' {
|
||||
return "", false
|
||||
}
|
||||
after := userAgentString[idx+len(desktopAppVersionPrefix):]
|
||||
if fields := strings.Fields(after); len(fields) > 0 {
|
||||
return limitStringLength(fields[0], maxUserAgentVersionLength), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func getBrowserVersion(ua *uasurfer.UserAgent, userAgentString string) string {
|
||||
for _, prefix := range versionPrefixes {
|
||||
if index := strings.Index(userAgentString, prefix); index != -1 {
|
||||
|
||||
@@ -314,3 +314,39 @@ func TestGetBrowserVersion(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDesktopAppVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
ua string
|
||||
version string
|
||||
ok bool
|
||||
}{
|
||||
{"Mattermost/5.0.0", "5.0.0", true},
|
||||
{"Mattermost/5.3.1 Chrome/110.0.5481.177", "5.3.1", true},
|
||||
{"Mozilla/5.0 ... Mattermost/5.9.0", "5.9.0", true},
|
||||
{"Mattermost/5.0.0-alpha", "5.0.0-alpha", true},
|
||||
{"Mattermost/6.0.0-rc.1 Electron/31.2.1", "6.0.0-rc.1", true},
|
||||
{"Mattermost Mobile/1.2.3", "", false},
|
||||
{"Mozilla/5.0 Chrome/120.0.0", "", false},
|
||||
{"Mattermost/", "", false},
|
||||
{"", "", false},
|
||||
{" ", "", false},
|
||||
{"Mattermost", "", false},
|
||||
{"Mattermost 5.0.0", "", false},
|
||||
{"Mattermost/ \t", "", false},
|
||||
{"MATTERMOST/5.0.0", "", false},
|
||||
{"mattermost/5.0.0", "", false},
|
||||
{"mmctl/1.2.3", "", false},
|
||||
{"Franz/4.0.4 Chrome/52.0.2743.82 Electron/1.3.1", "", false},
|
||||
{"MattermostMobile/1.0", "", false},
|
||||
{"SomeMattermost/2.0.0", "", false},
|
||||
{"Chrome/120.0.0 SomeMattermost/2.0.0", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.ua, func(t *testing.T) {
|
||||
version, ok := GetDesktopAppVersion(tt.ua)
|
||||
assert.Equal(t, tt.ok, ok)
|
||||
assert.Equal(t, tt.version, version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/templates"
|
||||
@@ -62,7 +63,8 @@ func (w *Web) InitStatic() {
|
||||
func root(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !CheckClientCompatibility(r.UserAgent()) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
data := renderUnsupportedBrowser(c.AppContext, r)
|
||||
subpath, _ := utils.GetSubpathFromConfig(c.App.Srv().Config())
|
||||
data := renderUnsupportedBrowser(c.AppContext, r, subpath)
|
||||
|
||||
err := c.App.Srv().TemplatesContainer().Render(w, "unsupported_browser", data)
|
||||
if err != nil {
|
||||
@@ -73,6 +75,26 @@ func root(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !CheckDesktopAppCompatibility(r.UserAgent(), c.App.Srv().Config().ServiceSettings.MinimumDesktopAppVersion) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
|
||||
currentVersion, ok := app.GetDesktopAppVersion(r.UserAgent())
|
||||
if !ok {
|
||||
currentVersion = "unknown"
|
||||
}
|
||||
cfg := c.App.Srv().Config()
|
||||
subpath, _ := utils.GetSubpathFromConfig(cfg)
|
||||
|
||||
data := renderUnsupportedDesktopApp(c.AppContext, cfg, currentVersion, subpath)
|
||||
err := c.App.Srv().TemplatesContainer().Render(w, "unsupported_desktop_app", data)
|
||||
if err != nil {
|
||||
c.Logger.Error("Failed to render template", mlog.Err(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if IsAPICall(c.App, r) {
|
||||
Handle404(c.App, w, r)
|
||||
return
|
||||
@@ -164,6 +186,16 @@ func unsupportedBrowserScriptHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, filepath.Join(templatesDir, "unsupported_browser.js"))
|
||||
}
|
||||
|
||||
func ensureTrailingSlash(s string) string {
|
||||
if s == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasSuffix(s, "/") {
|
||||
return s + "/"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getOpenGraphMetaTags(c *Context) string {
|
||||
siteName := model.TeamSettingsDefaultSiteName
|
||||
customSiteName := c.App.Srv().Config().TeamSettings.SiteName
|
||||
|
||||
@@ -44,9 +44,10 @@ type SystemBrowser struct {
|
||||
MakeDefaultString string
|
||||
}
|
||||
|
||||
func renderUnsupportedBrowser(rctx request.CTX, r *http.Request) templates.Data {
|
||||
func renderUnsupportedBrowser(rctx request.CTX, r *http.Request, subpath string) templates.Data {
|
||||
data := templates.Data{
|
||||
Props: map[string]any{
|
||||
"Subpath": ensureTrailingSlash(subpath),
|
||||
"DownloadAppOrUpgradeBrowserString": rctx.T("web.error.unsupported_browser.download_app_or_upgrade_browser"),
|
||||
"LearnMoreString": rctx.T("web.error.unsupported_browser.learn_more"),
|
||||
},
|
||||
@@ -92,7 +93,7 @@ func renderUnsupportedBrowser(rctx request.CTX, r *http.Request) templates.Data
|
||||
|
||||
func renderMattermostAppMac(rctx request.CTX) MattermostApp {
|
||||
return MattermostApp{
|
||||
"/static/images/browser-icons/mac.png",
|
||||
"mac.png",
|
||||
rctx.T("web.error.unsupported_browser.download_the_app"),
|
||||
rctx.T("web.error.unsupported_browser.min_os_version.mac"),
|
||||
rctx.T("web.error.unsupported_browser.download"),
|
||||
@@ -104,7 +105,7 @@ func renderMattermostAppMac(rctx request.CTX) MattermostApp {
|
||||
|
||||
func renderMattermostAppWindows(rctx request.CTX) MattermostApp {
|
||||
return MattermostApp{
|
||||
"/static/images/browser-icons/windows.svg",
|
||||
"windows.svg",
|
||||
rctx.T("web.error.unsupported_browser.download_the_app"),
|
||||
rctx.T("web.error.unsupported_browser.min_os_version.windows"),
|
||||
rctx.T("web.error.unsupported_browser.download"),
|
||||
@@ -116,7 +117,7 @@ func renderMattermostAppWindows(rctx request.CTX) MattermostApp {
|
||||
|
||||
func renderBrowserChrome(rctx request.CTX) Browser {
|
||||
return Browser{
|
||||
"/static/images/browser-icons/chrome.svg",
|
||||
"chrome.svg",
|
||||
rctx.T("web.error.unsupported_browser.browser_title.chrome"),
|
||||
rctx.T("web.error.unsupported_browser.min_browser_version.chrome"),
|
||||
"http://www.google.com/chrome",
|
||||
@@ -126,7 +127,7 @@ func renderBrowserChrome(rctx request.CTX) Browser {
|
||||
|
||||
func renderBrowserFirefox(rctx request.CTX) Browser {
|
||||
return Browser{
|
||||
"/static/images/browser-icons/firefox.svg",
|
||||
"firefox.svg",
|
||||
rctx.T("web.error.unsupported_browser.browser_title.firefox"),
|
||||
rctx.T("web.error.unsupported_browser.min_browser_version.firefox"),
|
||||
"https://www.mozilla.org/firefox/new/",
|
||||
@@ -136,7 +137,7 @@ func renderBrowserFirefox(rctx request.CTX) Browser {
|
||||
|
||||
func renderBrowserSafari(rctx request.CTX) Browser {
|
||||
return Browser{
|
||||
"/static/images/browser-icons/safari.svg",
|
||||
"safari.svg",
|
||||
rctx.T("web.error.unsupported_browser.browser_title.safari"),
|
||||
rctx.T("web.error.unsupported_browser.min_browser_version.safari"),
|
||||
"macappstore://showUpdatesPage",
|
||||
@@ -146,7 +147,7 @@ func renderBrowserSafari(rctx request.CTX) Browser {
|
||||
|
||||
func renderSystemBrowserEdge(rctx request.CTX, r *http.Request) SystemBrowser {
|
||||
return SystemBrowser{
|
||||
"/static/images/browser-icons/edge.svg",
|
||||
"edge.svg",
|
||||
rctx.T("web.error.unsupported_browser.browser_title.edge"),
|
||||
rctx.T("web.error.unsupported_browser.min_browser_version.edge"),
|
||||
rctx.T("web.error.unsupported_browser.open_system_browser.edge"),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/templates"
|
||||
)
|
||||
|
||||
func renderUnsupportedDesktopApp(rctx request.CTX, cfg *model.Config, currentVersion, subpath string) templates.Data {
|
||||
return templates.Data{
|
||||
Props: map[string]any{
|
||||
"Subpath": ensureTrailingSlash(subpath),
|
||||
"Title": rctx.T("web.error.unsupported_desktop_app.title"),
|
||||
"MessageString": rctx.T("web.error.unsupported_desktop_app.message", map[string]any{
|
||||
"SiteName": *cfg.TeamSettings.SiteName,
|
||||
"CurrentVersion": currentVersion,
|
||||
"MinimumVersion": *cfg.ServiceSettings.MinimumDesktopAppVersion,
|
||||
}),
|
||||
"DownloadButtonLabel": rctx.T("web.error.unsupported_desktop_app.download_button"),
|
||||
"AssistanceString": rctx.T("web.error.unsupported_desktop_app.assistance"),
|
||||
"FooterAboutLabel": rctx.T("web.error.unsupported_desktop_app.footer_about"),
|
||||
"FooterPrivacyLabel": rctx.T("web.error.unsupported_desktop_app.footer_privacy"),
|
||||
"FooterTermsLabel": rctx.T("web.error.unsupported_desktop_app.footer_terms"),
|
||||
"FooterHelpLabel": rctx.T("web.error.unsupported_desktop_app.footer_help"),
|
||||
"DownloadLink": *cfg.NativeAppSettings.AppDownloadLink,
|
||||
"CopyrightYear": time.Now().Year(),
|
||||
"SiteName": *cfg.TeamSettings.SiteName,
|
||||
"AboutLink": *cfg.SupportSettings.AboutLink,
|
||||
"PrivacyPolicyLink": *cfg.SupportSettings.PrivacyPolicyLink,
|
||||
"TermsOfServiceLink": *cfg.SupportSettings.TermsOfServiceLink,
|
||||
"HelpLink": *cfg.SupportSettings.HelpLink,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/avct/uasurfer"
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
@@ -58,6 +59,25 @@ func CheckClientCompatibility(agentString string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func CheckDesktopAppCompatibility(agentString string, minVersion *string) bool {
|
||||
if minVersion == nil || *minVersion == "" {
|
||||
return true
|
||||
}
|
||||
clientVersionStr, ok := app.GetDesktopAppVersion(agentString)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
clientVersion, err := semver.ParseTolerant(clientVersionStr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
required, err := semver.Parse(*minVersion)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return clientVersion.GTE(required)
|
||||
}
|
||||
|
||||
func Handle404(a *app.App, w http.ResponseWriter, r *http.Request) {
|
||||
err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound)
|
||||
ipAddress := utils.GetIPAddress(r, a.Config().ServiceSettings.TrustedProxyIPHeader)
|
||||
|
||||
@@ -475,3 +475,27 @@ func TestCheckClientCompatability(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDesktopAppCompatibility(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
userAgent string
|
||||
minVersion string
|
||||
want bool
|
||||
}{
|
||||
{"blank min version allows all", "Mattermost/5.0.0", "", true},
|
||||
{"desktop app at min version", "Mattermost/5.0.0", "5.0.0", true},
|
||||
{"desktop app above min version", "Mattermost/5.1.0", "5.0.0", true},
|
||||
{"desktop app below min version", "Mattermost/4.9.0", "5.0.0", false},
|
||||
{"browser user agent not checked", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0", "5.0.0", true},
|
||||
{"desktop app version at start of UA", "Mattermost/5.3.1 Chrome/110.0.5481.177", "5.0.0", true},
|
||||
{"desktop app version at end of UA", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/126.0.6478.127 Electron/31.2.1 Safari/537.36 Mattermost/5.9.0", "5.0.0", true},
|
||||
{"desktop app old version rejected", "Mattermost/3.7.1 Chrome/56.0.2924.87 Electron/1.6.11 Safari/537.36", "5.0.0", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := CheckDesktopAppCompatibility(tt.userAgent, &tt.minVersion)
|
||||
require.Equalf(t, tt.want, got, "CheckDesktopAppCompatibility(%q, %q)", tt.userAgent, tt.minVersion)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10428,6 +10428,10 @@
|
||||
"id": "model.config.is_valid.metrics_client_side_user_ids.app_error",
|
||||
"translation": "Number of elements in ClientSideUserIds {{.CurrentLength}} is higher than maximum limit of {{.MaxLength}}."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.minimum_desktop_app_version.app_error",
|
||||
"translation": "Invalid version number. Must be a valid semantic version (e.g. 5.0.0)."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.move_thread.domain_invalid.app_error",
|
||||
"translation": "Invalid domain for move thread settings"
|
||||
@@ -11868,6 +11872,38 @@
|
||||
"id": "web.error.unsupported_browser.system_browser_or",
|
||||
"translation": "or"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_desktop_app.assistance",
|
||||
"translation": "Contact your system administrator if you need further assistance."
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_desktop_app.download_button",
|
||||
"translation": "Download Updated App"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_desktop_app.footer_about",
|
||||
"translation": "About"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_desktop_app.footer_help",
|
||||
"translation": "Help"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_desktop_app.footer_privacy",
|
||||
"translation": "Privacy Policy"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_desktop_app.footer_terms",
|
||||
"translation": "Terms"
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_desktop_app.message",
|
||||
"translation": "Your {{.SiteName}} Desktop App version ({{.CurrentVersion}}) is below the minimum required version ({{.MinimumVersion}}). Please update your app or open {{.SiteName}} in a web browser to continue."
|
||||
},
|
||||
{
|
||||
"id": "web.error.unsupported_desktop_app.title",
|
||||
"translation": "Update Required"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.internal_saving.app_error",
|
||||
"translation": "Unable to update the user access data."
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/mattermost/ldap"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -420,24 +421,25 @@ type ServiceSettings struct {
|
||||
EnableAPIUserDeletion *bool
|
||||
EnableAPIPostDeletion *bool
|
||||
EnableDesktopLandingPage *bool
|
||||
ExperimentalEnableHardenedMode *bool `access:"experimental_features"`
|
||||
ExperimentalStrictCSRFEnforcement *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||
EnableEmailInvitations *bool `access:"authentication_signup"`
|
||||
DisableBotsWhenOwnerIsDeactivated *bool `access:"integrations_bot_accounts"`
|
||||
EnableBotAccountCreation *bool `access:"integrations_bot_accounts"`
|
||||
EnableSVGs *bool `access:"site_posts"`
|
||||
EnableLatex *bool `access:"site_posts"`
|
||||
EnableInlineLatex *bool `access:"site_posts"`
|
||||
PostPriority *bool `access:"site_posts"`
|
||||
AllowPersistentNotifications *bool `access:"site_posts"`
|
||||
AllowPersistentNotificationsForGuests *bool `access:"site_posts"`
|
||||
PersistentNotificationIntervalMinutes *int `access:"site_posts"`
|
||||
PersistentNotificationMaxCount *int `access:"site_posts"`
|
||||
PersistentNotificationMaxRecipients *int `access:"site_posts"`
|
||||
EnableBurnOnRead *bool `access:"site_posts"`
|
||||
BurnOnReadDurationSeconds *int `access:"site_posts"`
|
||||
BurnOnReadMaximumTimeToLiveSeconds *int `access:"site_posts"`
|
||||
BurnOnReadSchedulerFrequencySeconds *int `access:"site_posts,cloud_restrictable"`
|
||||
MinimumDesktopAppVersion *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
|
||||
ExperimentalEnableHardenedMode *bool `access:"experimental_features"`
|
||||
ExperimentalStrictCSRFEnforcement *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||
EnableEmailInvitations *bool `access:"authentication_signup"`
|
||||
DisableBotsWhenOwnerIsDeactivated *bool `access:"integrations_bot_accounts"`
|
||||
EnableBotAccountCreation *bool `access:"integrations_bot_accounts"`
|
||||
EnableSVGs *bool `access:"site_posts"`
|
||||
EnableLatex *bool `access:"site_posts"`
|
||||
EnableInlineLatex *bool `access:"site_posts"`
|
||||
PostPriority *bool `access:"site_posts"`
|
||||
AllowPersistentNotifications *bool `access:"site_posts"`
|
||||
AllowPersistentNotificationsForGuests *bool `access:"site_posts"`
|
||||
PersistentNotificationIntervalMinutes *int `access:"site_posts"`
|
||||
PersistentNotificationMaxCount *int `access:"site_posts"`
|
||||
PersistentNotificationMaxRecipients *int `access:"site_posts"`
|
||||
EnableBurnOnRead *bool `access:"site_posts"`
|
||||
BurnOnReadDurationSeconds *int `access:"site_posts"`
|
||||
BurnOnReadMaximumTimeToLiveSeconds *int `access:"site_posts"`
|
||||
BurnOnReadSchedulerFrequencySeconds *int `access:"site_posts,cloud_restrictable"`
|
||||
EnableAPIChannelDeletion *bool
|
||||
EnableLocalMode *bool `access:"cloud_restrictable"`
|
||||
LocalModeSocketLocation *string `access:"cloud_restrictable"` // telemetry: none
|
||||
@@ -883,6 +885,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
|
||||
s.EnableDesktopLandingPage = NewPointer(true)
|
||||
}
|
||||
|
||||
if s.MinimumDesktopAppVersion == nil {
|
||||
s.MinimumDesktopAppVersion = NewPointer("")
|
||||
}
|
||||
|
||||
if s.EnableSVGs == nil {
|
||||
if isUpdate {
|
||||
s.EnableSVGs = NewPointer(true)
|
||||
@@ -4630,6 +4636,12 @@ func (s *ServiceSettings) isValid() *AppError {
|
||||
}
|
||||
}
|
||||
|
||||
if *s.MinimumDesktopAppVersion != "" {
|
||||
if _, err := semver.Parse(*s.MinimumDesktopAppVersion); err != nil {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.minimum_desktop_app_version.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
host, port, _ := net.SplitHostPort(*s.ListenAddress)
|
||||
var isValidHost bool
|
||||
if host == "" {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class='browser'>
|
||||
<img
|
||||
class='browser-image'
|
||||
src='{{.LogoSrc}}'
|
||||
src='static/images/browser-icons/{{.LogoSrc}}'
|
||||
/>
|
||||
<div class='browser-top-text'>{{.Title}}</div>
|
||||
<div class='browser-bottom-text'>{{.SupportedVersionString}}</div>
|
||||
@@ -18,7 +18,7 @@
|
||||
>
|
||||
<img
|
||||
class='browser-image'
|
||||
src='{{.LogoSrc}}'
|
||||
src='static/images/browser-icons/{{.LogoSrc}}'
|
||||
/>
|
||||
<div class='browser-top-text'>{{.GetLatestString}}</div>
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class='browser'>
|
||||
<img
|
||||
class='browser-image'
|
||||
src='{{.LogoSrc}}'
|
||||
src='static/images/browser-icons/{{.LogoSrc}}'
|
||||
/>
|
||||
<div class='browser-top-text'>{{.Title}}</div>
|
||||
<div class='browser-bottom-text'>{{.SupportedVersionString}}</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class='browser browser-hover no-pointer hidden'>
|
||||
<img
|
||||
class='browser-image'
|
||||
src='{{.LogoSrc}}'
|
||||
src='static/images/browser-icons/{{.LogoSrc}}'
|
||||
/>
|
||||
<div
|
||||
class='browser-top-text pointer'
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class='mattermost-app'>
|
||||
<img
|
||||
class='mattermost-app-image'
|
||||
src='{{.LogoSrc}}'
|
||||
src='static/images/browser-icons/{{.LogoSrc}}'
|
||||
/>
|
||||
<div class='browser-top-text'>{{.Title}}</div>
|
||||
<div class='browser-bottom-text'>{{.SupportedVersionString}}</div>
|
||||
@@ -74,7 +74,7 @@
|
||||
<div class='mattermost-app mattermost-app-hover hidden'>
|
||||
<img
|
||||
class='mattermost-app-image'
|
||||
src='{{.LogoSrc}}'
|
||||
src='static/images/browser-icons/{{.LogoSrc}}'
|
||||
/>
|
||||
<button
|
||||
class='mattermost-app-button'
|
||||
@@ -92,6 +92,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base href="{{.Props.Subpath}}">
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
@@ -264,10 +265,24 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
<link
|
||||
href='https://fonts.googleapis.com/css?family=Open+Sans&display=swap'
|
||||
rel='stylesheet'
|
||||
/>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("static/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2") format('woff2'),
|
||||
url("static/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff") format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src:
|
||||
url("static/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2") format('woff2'),
|
||||
url("static/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-600.woff") format('woff');
|
||||
}
|
||||
</style>
|
||||
<script
|
||||
type='text/javascript'
|
||||
src='/unsupported_browser.js'
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
{{define "unsupported_desktop_app"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<base href="{{.Props.Subpath}}">
|
||||
<title>{{.Props.Title}}</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Metropolis';
|
||||
src: url("static/fonts/Metropolis-SemiBold.woff") format('woff');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #F0F0F0;
|
||||
background-image: url("static/images/admin-onboarding-background.jpg");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.header {
|
||||
padding: 36px 0 0 40px;
|
||||
}
|
||||
.logo {
|
||||
display: block;
|
||||
width: 170px;
|
||||
height: 30px;
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 40px 80px;
|
||||
}
|
||||
.content {
|
||||
text-align: center;
|
||||
max-width: 560px;
|
||||
}
|
||||
.warning-icon {
|
||||
display: block;
|
||||
width: 189.133px;
|
||||
height: 150px;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
.title {
|
||||
margin: 0 0 16px;
|
||||
font-family: 'Metropolis', sans-serif;
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
line-height: 48px;
|
||||
letter-spacing: -0.8px;
|
||||
color: #1E325C;
|
||||
}
|
||||
.message {
|
||||
margin: 0 0 24px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: rgba(63, 67, 80, 0.72);
|
||||
}
|
||||
.message a {
|
||||
color: #2389D7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.message a:hover {
|
||||
color: #1C6FBC;
|
||||
}
|
||||
.download-container {
|
||||
margin: 43px 0;
|
||||
}
|
||||
.download-button {
|
||||
display: inline-block;
|
||||
padding: 12px 28px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background-color: #2962FF;
|
||||
color: #FFFFFF;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.download-button:hover {
|
||||
background-color: #1E53E6;
|
||||
}
|
||||
.assistance {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: rgba(63, 67, 80, 0.64);
|
||||
}
|
||||
.footer,
|
||||
.footer-links {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.footer {
|
||||
padding: 16px 40px;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.22px;
|
||||
color: rgba(63, 67, 80, 0.56);
|
||||
}
|
||||
.footer-links a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("static/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2") format('woff2'),
|
||||
url("static/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff") format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src:
|
||||
url("static/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2") format('woff2'),
|
||||
url("static/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-600.woff") format('woff');
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<img class="logo" src="static/images/logo.svg" alt="{{.Props.SiteName}}" />
|
||||
</header>
|
||||
<main class="main">
|
||||
<div class="content">
|
||||
<img class="warning-icon" src="static/images/alert.svg" alt="" />
|
||||
<h1 class="title">{{.Props.Title}}</h1>
|
||||
<div class="message">{{.Props.MessageString}}</div>
|
||||
<div class="download-container">
|
||||
<a class="download-button" href="{{.Props.DownloadLink}}" target="_blank" rel="noopener noreferrer">{{.Props.DownloadButtonLabel}}</a>
|
||||
</div>
|
||||
<p class="assistance">{{.Props.AssistanceString}}</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<span>© {{.Props.CopyrightYear}} Mattermost Inc.</span>
|
||||
<span class="footer-links">
|
||||
{{if .Props.AboutLink}}<a href="{{.Props.AboutLink}}" target="_blank" rel="noopener noreferrer">{{.Props.FooterAboutLabel}}</a>{{end}}
|
||||
{{if .Props.PrivacyPolicyLink}}<a href="{{.Props.PrivacyPolicyLink}}" target="_blank" rel="noopener noreferrer">{{.Props.FooterPrivacyLabel}}</a>{{end}}
|
||||
{{if .Props.TermsOfServiceLink}}<a href="{{.Props.TermsOfServiceLink}}" target="_blank" rel="noopener noreferrer">{{.Props.FooterTermsLabel}}</a>{{end}}
|
||||
{{if .Props.HelpLink}}<a href="{{.Props.HelpLink}}" target="_blank" rel="noopener noreferrer">{{.Props.FooterHelpLabel}}</a>{{end}}
|
||||
</span>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -6,6 +6,7 @@
|
||||
import React from 'react';
|
||||
import {FormattedMessage, defineMessage, defineMessages} from 'react-intl';
|
||||
import {Link} from 'react-router-dom';
|
||||
import semver from 'semver';
|
||||
|
||||
import {AccountMultipleOutlineIcon, ChartBarIcon, CogOutlineIcon, CreditCardOutlineIcon, FlaskOutlineIcon, FormatListBulletedIcon, InformationOutlineIcon, PowerPlugOutlineIcon, ServerVariantIcon, ShieldOutlineIcon, SitemapIcon, TableLargeIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
@@ -2525,6 +2526,33 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
help_text: defineMessage({id: 'admin.customization.enableDesktopLandingPageDesc', defaultMessage: 'Whether or not to prompt a user to use the Desktop App when they first use Mattermost.'}),
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'ServiceSettings.MinimumDesktopAppVersion',
|
||||
label: defineMessage({
|
||||
id: 'admin.customization.minimumDesktopAppVersionTitle',
|
||||
defaultMessage: 'Minimum desktop app version:',
|
||||
}),
|
||||
placeholder: defineMessage({
|
||||
id: 'admin.customization.minimumDesktopAppVersionPlaceholder',
|
||||
defaultMessage: 'Input a version number (e.g. 5.0.0)',
|
||||
}),
|
||||
help_text: defineMessage({
|
||||
id: 'admin.customization.minimumDesktopAppVersionDesc',
|
||||
defaultMessage: 'Specify the minimum version of the Mattermost Desktop App required to connect to this server (e.g., 5.10.0). Users connecting with a Desktop App version below this minimum will be shown an update required page and will not be able to use the application until they update. Leave this field blank to allow all Desktop App versions to connect without restriction.',
|
||||
}),
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)),
|
||||
validate: (value) => {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : value;
|
||||
if (trimmed && !semver.valid(trimmed)) {
|
||||
return new ValidationResult(false, defineMessage({
|
||||
id: 'admin.customization.minimumDesktopAppVersionError',
|
||||
defaultMessage: 'Invalid version number. Must be a valid semantic version (e.g. 5.0.0).',
|
||||
}));
|
||||
}
|
||||
return new ValidationResult(true, '');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -835,6 +835,10 @@
|
||||
"admin.customization.iosAppDownloadLinkTitle": "iOS App Download Link:",
|
||||
"admin.customization.maxMarkdownNodesDesc": "When rendering Markdown text in the mobile app, controls the maximum number of Markdown elements (eg. emojis, links, table cells, etc) that can be in a single piece of text. If set to 0, a default limit will be used.",
|
||||
"admin.customization.maxMarkdownNodesTitle": "Maximum Markdown Nodes:",
|
||||
"admin.customization.minimumDesktopAppVersionDesc": "Specify the minimum version of the Mattermost Desktop App required to connect to this server (e.g., 5.10.0). Users connecting with a Desktop App version below this minimum will be shown an update required page and will not be able to use the application until they update. Leave this field blank to allow all Desktop App versions to connect without restriction.",
|
||||
"admin.customization.minimumDesktopAppVersionError": "Invalid version number. Must be a valid semantic version (e.g. 5.0.0).",
|
||||
"admin.customization.minimumDesktopAppVersionPlaceholder": "Input a version number (e.g. 5.0.0)",
|
||||
"admin.customization.minimumDesktopAppVersionTitle": "Minimum desktop app version:",
|
||||
"admin.customization.restrictLinkPreviewsDesc": "Link previews and image link previews will not be shown for the above list of comma-separated domains.",
|
||||
"admin.customization.restrictLinkPreviewsExample": "E.g.: \"internal.mycompany.com, images.example.com\"",
|
||||
"admin.customization.restrictLinkPreviewsTitle": "Disable website link previews from these domains:",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="190" height="151" viewBox="0 0 190 151" fill="none">
|
||||
<rect x="17.5217" y="13.7783" width="156.522" height="52.1739" rx="8.15217" fill="#1C58D9" fill-opacity="0.12"/>
|
||||
<rect y="72.4739" width="189.13" height="54.3478" rx="8.15217" fill="#1C58D9" fill-opacity="0.12"/>
|
||||
<path d="M87.7834 7.93257C90.1942 3.15274 97.0188 3.15274 99.4296 7.93257L160.377 128.776C162.565 133.114 159.412 138.235 154.554 138.235H32.6588C27.8007 138.235 24.6481 133.114 26.8357 128.776L87.7834 7.93257Z" fill="#FFBC1F"/>
|
||||
<path d="M84.4059 4.07446C86.4148 0.0912905 92.102 0.0913337 94.111 4.07446L155.059 124.918C156.882 128.533 154.255 132.8 150.207 132.8H28.3112C24.2628 132.8 21.6357 128.533 23.4586 124.918L84.4059 4.07446Z" stroke="#3F4350" stroke-width="2.17391"/>
|
||||
<path d="M107.828 7.77832L115.104 21.8653M163.002 114.604L149.057 87.6044L145.419 80.5609L140.569 71.1696L133.9 58.2566M129.049 48.8653L119.955 31.2566" stroke="#3F4350" stroke-opacity="0.56" stroke-width="2.34783" stroke-linecap="round"/>
|
||||
<path d="M82.7727 53.5246L87.7306 85.3656C87.7784 85.9975 88.0748 86.5886 88.56 87.02C89.0453 87.4515 89.6834 87.6914 90.3462 87.6914C91.0089 87.6914 91.647 87.4515 92.1323 87.02C92.6175 86.5886 92.9139 85.9975 92.9617 85.3656L97.9196 53.5246C98.8211 41.1093 81.8576 41.1093 82.7727 53.5246Z" fill="#3F4350"/>
|
||||
<path d="M89.2743 100.735C90.9934 100.738 92.673 101.251 94.101 102.208C95.5289 103.165 96.6411 104.524 97.2969 106.114C97.9527 107.703 98.1226 109.451 97.7853 111.137C97.448 112.822 96.6186 114.37 95.4019 115.585C94.1851 116.799 92.6357 117.626 90.9494 117.96C89.263 118.294 87.5154 118.121 85.9273 117.463C84.3393 116.804 82.9821 115.69 82.0273 114.26C81.0725 112.83 80.563 111.15 80.563 109.431C80.563 108.287 80.7882 107.155 81.2262 106.099C81.6642 105.043 82.3062 104.084 83.1153 103.276C83.9245 102.469 84.885 101.828 85.9418 101.392C86.9987 100.956 88.131 100.733 89.2743 100.735Z" fill="#3F4350"/>
|
||||
<path d="M105.48 147.474H152.437" stroke="#3F4350" stroke-opacity="0.56" stroke-width="2.34783" stroke-linecap="round"/>
|
||||
<path d="M21.8695 107.257L56.6521 39.8653" stroke="#3F4350" stroke-opacity="0.56" stroke-width="2.34783" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -191,12 +191,19 @@ var config = {
|
||||
{from: 'src/images/c_download.png', to: 'images'},
|
||||
{from: 'src/images/c_socket.png', to: 'images'},
|
||||
{from: 'src/images/admin-onboarding-background.jpg', to: 'images'},
|
||||
{from: 'src/images/logo.svg', to: 'images'},
|
||||
{from: 'src/images/alert.svg', to: 'images'},
|
||||
{from: 'src/images/cloud-laptop.png', to: 'images'},
|
||||
{from: 'src/images/cloud-laptop-error.png', to: 'images'},
|
||||
{from: 'src/images/cloud-laptop-warning.png', to: 'images'},
|
||||
{from: 'src/images/cloud-upgrade-person-hand-to-face.png', to: 'images'},
|
||||
{from: 'src/images/payment_processing.png', to: 'images'},
|
||||
{from: 'src/images/purchase_alert.png', to: 'images'},
|
||||
{from: 'src/fonts/Metropolis-SemiBold.woff', to: 'fonts'},
|
||||
{from: 'src/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2', to: 'fonts'},
|
||||
{from: 'src/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff', to: 'fonts'},
|
||||
{from: 'src/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2', to: 'fonts'},
|
||||
{from: 'src/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-600.woff', to: 'fonts'},
|
||||
{from: '../node_modules/pdfjs-dist/cmaps', to: 'cmaps'},
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -438,6 +438,7 @@ export type ServiceSettings = {
|
||||
EnableWebHubChannelIteration: boolean;
|
||||
FrameAncestors: string;
|
||||
DeleteAccountLink: string;
|
||||
MinimumDesktopAppVersion: string;
|
||||
};
|
||||
|
||||
export type TeamSettings = {
|
||||
|
||||
Reference in New Issue
Block a user