[MM-14058] Add support for i18n (#2190)
* Add language files * Add react-intl, mmjstool, setup for adding translations * Translated main module * Translations for renderer * A few minor fixes * More fixes * Add CI, add missing menu translations, other cleanup * Added setting to manually select the language of the app * Force English for E2e * Unit tests * Fix mmjstool * Move set language to before update menu * PR feedback
This commit is contained in:
parent
22c97591d5
commit
59e4e7e516
|
@ -12,19 +12,19 @@ executors:
|
|||
check-image:
|
||||
working_directory: ~/mattermost-desktop
|
||||
docker:
|
||||
- image: electronuserland/builder:wine-chrome
|
||||
- image: electronuserland/builder:16-wine-chrome
|
||||
environment:
|
||||
TAR_OPTIONS: --no-same-owner
|
||||
wine-chrome:
|
||||
working_directory: ~/mattermost-desktop
|
||||
docker:
|
||||
- image: electronuserland/builder:wine-chrome
|
||||
- image: electronuserland/builder:16-wine-chrome
|
||||
environment:
|
||||
TAR_OPTIONS: --no-same-owner
|
||||
wine-mono:
|
||||
working_directory: ~/mattermost-desktop
|
||||
docker:
|
||||
- image: electronuserland/builder:wine-mono
|
||||
- image: electronuserland/builder:16-wine-mono
|
||||
mac:
|
||||
working_directory: ~/mattermost-desktop
|
||||
macos:
|
||||
|
@ -140,6 +140,13 @@ jobs:
|
|||
- update_image:
|
||||
apt_opts: "--no-install-recommends"
|
||||
- run: npm run check-types
|
||||
- run:
|
||||
name: i18n check
|
||||
command: |
|
||||
cp i18n/en.json /tmp/en.json
|
||||
npm run mmjstool -- i18n extract-desktop --desktop-dir .
|
||||
diff /tmp/en.json i18n/en.json
|
||||
rm -rf tmp
|
||||
- run: ELECTRON_DISABLE_SANDBOX=1 npm run test:unit-ci
|
||||
- run: mkdir -p /tmp/test-results
|
||||
- run: cp test-results.xml /tmp/test-results/
|
||||
|
|
|
@ -95,6 +95,7 @@ const demoConfig = {
|
|||
darkMode: false,
|
||||
lastActiveTeam: 0,
|
||||
spellCheckerLocales: [],
|
||||
appLanguage: 'en',
|
||||
};
|
||||
|
||||
const demoMattermostConfig = {
|
||||
|
|
1
i18n/bg.json
Normal file
1
i18n/bg.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/de.json
Normal file
1
i18n/de.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
234
i18n/en.json
Normal file
234
i18n/en.json
Normal file
|
@ -0,0 +1,234 @@
|
|||
{
|
||||
"common.permissions.canBasicAuth": "Web Authentication",
|
||||
"common.tabs.TAB_FOCALBOARD": "Boards",
|
||||
"common.tabs.TAB_MESSAGING": "Channels",
|
||||
"common.tabs.TAB_PLAYBOOKS": "Playbooks",
|
||||
"label.accept": "Accept",
|
||||
"label.add": "Add",
|
||||
"label.cancel": "Cancel",
|
||||
"label.change": "Change",
|
||||
"label.close": "Close",
|
||||
"label.login": "Login",
|
||||
"label.no": "No",
|
||||
"label.ok": "OK",
|
||||
"label.remove": "Remove",
|
||||
"label.save": "Save",
|
||||
"label.yes": "Yes",
|
||||
"main.allowProtocolDialog.button.saveProtocolAsAllowed": "Yes (Save {protocol} as allowed)",
|
||||
"main.allowProtocolDialog.detail": "The requested link is {URL}. Do you want to continue?",
|
||||
"main.allowProtocolDialog.message": "{protocol} link requires an external application.",
|
||||
"main.allowProtocolDialog.title": "Non http(s) protocol",
|
||||
"main.app.app.handleAppCertificateError.certError.button.cancelConnection": "Cancel Connection",
|
||||
"main.app.app.handleAppCertificateError.certError.button.moreDetails": "More Details",
|
||||
"main.app.app.handleAppCertificateError.certError.dialog.detail": "{extraDetail}origin: {origin}\nError: {error}",
|
||||
"main.app.app.handleAppCertificateError.certError.dialog.message": "There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.",
|
||||
"main.app.app.handleAppCertificateError.certError.dialog.title": "Certificate Error",
|
||||
"main.app.app.handleAppCertificateError.certNotTrusted.button.cancelConnection": "Cancel Connection",
|
||||
"main.app.app.handleAppCertificateError.certNotTrusted.button.trustInsecureCertificate": "Trust Insecure Certificate",
|
||||
"main.app.app.handleAppCertificateError.certNotTrusted.dialog.message": "Certificate from \"{issuerName}\" is not trusted.",
|
||||
"main.app.app.handleAppCertificateError.certNotTrusted.dialog.title": "Certificate Not Trusted",
|
||||
"main.app.app.handleAppCertificateError.dialog.extraDetail": "Certificate is different from previous one.\n\n",
|
||||
"main.app.initialize.downloadBox.allFiles": "All files",
|
||||
"main.app.utils.migrateMacAppStore.button.dontImport": "Don't Import",
|
||||
"main.app.utils.migrateMacAppStore.button.selectAndImport": "Select Directory and Import",
|
||||
"main.app.utils.migrateMacAppStore.dialog.detail": "It appears that an existing {appName} configuration exists, would you like to import it? You will be asked to pick the correct configuration directory.",
|
||||
"main.app.utils.migrateMacAppStore.dialog.message": "Import Existing Configuration",
|
||||
"main.autoUpdater.download.dialog.button.download": "Download",
|
||||
"main.autoUpdater.download.dialog.button.remindMeLater": "Remind me Later",
|
||||
"main.autoUpdater.download.dialog.detail": "A new version of the {appName} Desktop App is available for you to download and install now.",
|
||||
"main.autoUpdater.download.dialog.message": "New desktop version available",
|
||||
"main.autoUpdater.noUpdate.detail": "You are using the latest version of the {appName} Desktop App (version {version}). You'll be notified when a new version is available to install.",
|
||||
"main.autoUpdater.noUpdate.message": "You're up to date",
|
||||
"main.autoUpdater.update.dialog.button.remindMeLater": "Remind me Later",
|
||||
"main.autoUpdater.update.dialog.button.restartAndUpdate": "Restart and Update",
|
||||
"main.autoUpdater.update.dialog.detail": "A new version of the {appName} Desktop App is ready to install.",
|
||||
"main.autoUpdater.update.dialog.message": "A new version is ready to install",
|
||||
"main.badge.noUnreads": "You have no unread messages",
|
||||
"main.badge.sessionExpired": "Session Expired: Please sign in to continue receiving notifications.",
|
||||
"main.badge.unreadChannels": "You have unread channels",
|
||||
"main.badge.unreadMentions": "You have unread mentions ({mentionCount})",
|
||||
"main.CriticalErrorHandler.uncaughtException.button.reopen": "Reopen",
|
||||
"main.CriticalErrorHandler.uncaughtException.button.showDetails": "Show Details",
|
||||
"main.CriticalErrorHandler.uncaughtException.dialog.message": "The {appName} app quit unexpectedly. Click \"{showDetails}\" to learn more or \"{reopen}\" to open the application again.\n\nInternal error: {err}",
|
||||
"main.CriticalErrorHandler.unresponsive.dialog.message": "The window is no longer responsive.\nDo you wait until the window becomes responsive again?",
|
||||
"main.menus.app.edit": "&Edit",
|
||||
"main.menus.app.edit.copy": "Copy",
|
||||
"main.menus.app.edit.cut": "Cut",
|
||||
"main.menus.app.edit.paste": "Paste",
|
||||
"main.menus.app.edit.pasteAndMatchStyle": "Paste and Match Style",
|
||||
"main.menus.app.edit.redo": "Redo",
|
||||
"main.menus.app.edit.selectAll": "Select All",
|
||||
"main.menus.app.edit.undo": "Undo",
|
||||
"main.menus.app.file": "&File",
|
||||
"main.menus.app.file.about": "About {appName}",
|
||||
"main.menus.app.file.exit": "Exit",
|
||||
"main.menus.app.file.hide": "Hide {appName}",
|
||||
"main.menus.app.file.hideOthers": "Hide Others",
|
||||
"main.menus.app.file.preferences": "Preferences...",
|
||||
"main.menus.app.file.quit": "Quit {appName}",
|
||||
"main.menus.app.file.settings": "Settings...",
|
||||
"main.menus.app.file.signInToAnotherServer": "Sign in to Another Server",
|
||||
"main.menus.app.file.unhide": "Show All",
|
||||
"main.menus.app.help": "Hel&p",
|
||||
"main.menus.app.help.checkForUpdates": "Check for Updates",
|
||||
"main.menus.app.help.commitString": " commit: {hashVersion}",
|
||||
"main.menus.app.help.downloadUpdate": "Download Update",
|
||||
"main.menus.app.help.learnMore": "Learn More...",
|
||||
"main.menus.app.help.restartAndUpdate": "Restart and Update",
|
||||
"main.menus.app.help.versionString": "Version {version}{commit}",
|
||||
"main.menus.app.history": "&History",
|
||||
"main.menus.app.history.back": "Back",
|
||||
"main.menus.app.history.forward": "Forward",
|
||||
"main.menus.app.view": "&View",
|
||||
"main.menus.app.view.actualSize": "Actual Size",
|
||||
"main.menus.app.view.clearCacheAndReload": "Clear Cache and Reload",
|
||||
"main.menus.app.view.devToolsAppWrapper": "Developer Tools for Application Wrapper",
|
||||
"main.menus.app.view.devToolsCurrentServer": "Developer Tools for Current Server",
|
||||
"main.menus.app.view.find": "Find..",
|
||||
"main.menus.app.view.fullscreen": "Toggle Full Screen",
|
||||
"main.menus.app.view.reload": "Reload",
|
||||
"main.menus.app.view.toggleDarkMode": "Toggle Dark Mode",
|
||||
"main.menus.app.view.zoomIn": "Zoom In",
|
||||
"main.menus.app.view.zoomOut": "Zoom Out",
|
||||
"main.menus.app.window": "&Window",
|
||||
"main.menus.app.window.bringAllToFront": "Bring All to Front",
|
||||
"main.menus.app.window.close": "Close",
|
||||
"main.menus.app.window.closeWindow": "Close Window",
|
||||
"main.menus.app.window.minimize": "Minimize",
|
||||
"main.menus.app.window.selectNextTab": "Select Next Tab",
|
||||
"main.menus.app.window.selectPreviousTab": "Select Previous Tab",
|
||||
"main.menus.app.window.showServers": "Show Servers",
|
||||
"main.menus.app.window.zoom": "Zoom",
|
||||
"main.menus.tray.preferences": "Preferences...",
|
||||
"main.menus.tray.settings": "Settings...",
|
||||
"main.notifications.download.complete.body": "Download Complete \n {fileName}",
|
||||
"main.notifications.download.complete.title": "Download Complete",
|
||||
"main.notifications.mention.title": "Someone mentioned you",
|
||||
"main.notifications.upgrade.newVersion.body": "A new version is available for you to download now.",
|
||||
"main.notifications.upgrade.newVersion.title": "New desktop version available",
|
||||
"main.notifications.upgrade.readyToInstall.body": "A new desktop version is ready to install now.",
|
||||
"main.notifications.upgrade.readyToInstall.title": "Click to restart and install update",
|
||||
"main.tray.tray.expired": "Session Expired: Please sign in to continue receiving notifications.",
|
||||
"main.tray.tray.mention": "You have been mentioned",
|
||||
"main.tray.tray.unread": "You have unread channels",
|
||||
"main.views.viewManager.handleDeepLink.error.body": "There is no configured server in the app that matches the requested url: {url}",
|
||||
"main.views.viewManager.handleDeepLink.error.title": "No matching server",
|
||||
"main.windows.mainWindow.closeApp.dialog.checkboxLabel": "Don't ask again",
|
||||
"main.windows.mainWindow.closeApp.dialog.detail": "You will no longer receive notifications for messages. If you want to leave {appName} running in the system tray, you can enable this in Settings.",
|
||||
"main.windows.mainWindow.closeApp.dialog.message": "Are you sure you want to quit?",
|
||||
"main.windows.mainWindow.closeApp.dialog.title": "Close Application",
|
||||
"main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel": "Don't show again",
|
||||
"main.windows.mainWindow.minimizeToTray.dialog.message": "{appName} will continue to run in the system tray. This can be disabled in Settings.",
|
||||
"main.windows.mainWindow.minimizeToTray.dialog.title": "Minimize to Tray",
|
||||
"renderer.components.autoSaveIndicator.saved": "Saved",
|
||||
"renderer.components.autoSaveIndicator.saving": "Saving...",
|
||||
"renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}",
|
||||
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.",
|
||||
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:",
|
||||
"renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow": "You can reach <link>{url}</link> from a browser window.",
|
||||
"renderer.components.errorView.troubleshooting.computerIsConnected": "Your computer is connected to the internet.",
|
||||
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "The {appName} URL <link>{url}</link> is correct",
|
||||
"renderer.components.extraBar.back": "Back",
|
||||
"renderer.components.mainPage.contextMenu.ariaLabel": "Context menu",
|
||||
"renderer.components.mainPage.downloadingUpdate": "Downloading update. {percentDone}% of {total} @ {speed}/s",
|
||||
"renderer.components.mainPage.updateAvailable": "Update available",
|
||||
"renderer.components.mainPage.updateReady": "Update ready to install",
|
||||
"renderer.components.newTeamModal.error.nameRequired": "Name is required.",
|
||||
"renderer.components.newTeamModal.error.serverNameExists": "A server with the same name already exists.",
|
||||
"renderer.components.newTeamModal.error.serverUrlExists": "A server with the same URL already exists.",
|
||||
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
|
||||
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
|
||||
"renderer.components.newTeamModal.error.urlRequired": "URL is required.",
|
||||
"renderer.components.newTeamModal.serverDisplayName": "Server Display Name",
|
||||
"renderer.components.newTeamModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
|
||||
"renderer.components.newTeamModal.serverURL": "Server URL",
|
||||
"renderer.components.newTeamModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
|
||||
"renderer.components.newTeamModal.title.add": "Add Server",
|
||||
"renderer.components.newTeamModal.title.edit": "Edit Server",
|
||||
"renderer.components.removeServerModal.body": "This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.",
|
||||
"renderer.components.removeServerModal.confirm": "Confirm you wish to remove the {serverName} server?",
|
||||
"renderer.components.removeServerModal.title": "Remove Server",
|
||||
"renderer.components.settingsPage.afterRestart": "Setting takes effect after restarting the app.",
|
||||
"renderer.components.settingsPage.appLanguage": "Set app language (beta)",
|
||||
"renderer.components.settingsPage.appLanguage.description": "Chooses the language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.",
|
||||
"renderer.components.settingsPage.appLanguage.useSystemDefault": "Use system default",
|
||||
"renderer.components.settingsPage.appOptions": "App Options",
|
||||
"renderer.components.settingsPage.bounceIcon": "Bounce the Dock icon",
|
||||
"renderer.components.settingsPage.bounceIcon.description": "If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.",
|
||||
"renderer.components.settingsPage.bounceIcon.once": "once",
|
||||
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "until I open the app",
|
||||
"renderer.components.settingsPage.checkSpelling": "Check spelling",
|
||||
"renderer.components.settingsPage.checkSpelling.description": "Highlight misspelled words in your messages based on your system language or language preference.",
|
||||
"renderer.components.settingsPage.checkSpelling.editSpellcheckUrl": "Use an alternative dictionary URL",
|
||||
"renderer.components.settingsPage.checkSpelling.preferredLanguages": "Select preferred language(s)",
|
||||
"renderer.components.settingsPage.checkSpelling.revertToDefault": "Revert to default",
|
||||
"renderer.components.settingsPage.checkSpelling.specifyURL": "Specify the url where dictionary definitions can be retrieved",
|
||||
"renderer.components.settingsPage.downloadLocation": "Download Location",
|
||||
"renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.",
|
||||
"renderer.components.settingsPage.enableHardwareAcceleration": "Use GPU hardware acceleration",
|
||||
"renderer.components.settingsPage.enableHardwareAcceleration.description": "If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.",
|
||||
"renderer.components.settingsPage.flashWindow": "Flash taskbar icon when a new message is received",
|
||||
"renderer.components.settingsPage.flashWindow.description": "If enabled, the taskbar icon will flash for a few seconds when a new message is received.",
|
||||
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "This functionality may not work with all Linux window managers.",
|
||||
"renderer.components.settingsPage.flashWindow.description.note": "NOTE: ",
|
||||
"renderer.components.settingsPage.fullscreen": "Open app in fullscreen",
|
||||
"renderer.components.settingsPage.fullscreen.description": "If enabled, the Mattermost application will always open in full screen",
|
||||
"renderer.components.settingsPage.header": "Settings",
|
||||
"renderer.components.settingsPage.launchAppMinimized": "Launch app minimized",
|
||||
"renderer.components.settingsPage.launchAppMinimized.description": "If enabled, the app will start in system tray, and will not show the window on launch.",
|
||||
"renderer.components.settingsPage.loadingConfig": "Loading configuration...",
|
||||
"renderer.components.settingsPage.loggingLevel": "Logging level",
|
||||
"renderer.components.settingsPage.loggingLevel.description": "Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.",
|
||||
"renderer.components.settingsPage.loggingLevel.description.subtitle": "Increasing the log level increases disk space usage and can impact performance. We recommend only increasing the log level if you are having issues.",
|
||||
"renderer.components.settingsPage.loggingLevel.level.debug": "Debug (debug)",
|
||||
"renderer.components.settingsPage.loggingLevel.level.error": "Errors (error)",
|
||||
"renderer.components.settingsPage.loggingLevel.level.info": "Info (info)",
|
||||
"renderer.components.settingsPage.loggingLevel.level.silly": "Finest (silly)",
|
||||
"renderer.components.settingsPage.loggingLevel.level.verbose": "Verbose (verbose)",
|
||||
"renderer.components.settingsPage.loggingLevel.level.warn": "Errors and Warnings (warn)",
|
||||
"renderer.components.settingsPage.minimizeToTray": "Leave app running in notification area when application window is closed",
|
||||
"renderer.components.settingsPage.minimizeToTray.description": "If enabled, the app stays running in the notification area after app window is closed.",
|
||||
"renderer.components.settingsPage.saving.error": "Can't save your changes. Please try again.",
|
||||
"renderer.components.settingsPage.showUnreadBadge": "Show red badge on {taskbar} icon to indicate unread messages",
|
||||
"renderer.components.settingsPage.showUnreadBadge.description": "Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.",
|
||||
"renderer.components.settingsPage.startAppOnLogin": "Start app on login",
|
||||
"renderer.components.settingsPage.startAppOnLogin.description": "If enabled, the app starts automatically when you log in to your machine.",
|
||||
"renderer.components.settingsPage.trayIcon.show": "Show icon in the notification area",
|
||||
"renderer.components.settingsPage.trayIcon.show.darwin": "Show {appName} icon in the menu bar",
|
||||
"renderer.components.settingsPage.trayIcon.theme": "Icon theme: ",
|
||||
"renderer.components.settingsPage.trayIcon.theme.dark": "Dark",
|
||||
"renderer.components.settingsPage.trayIcon.theme.light": "Light",
|
||||
"renderer.components.settingsPage.trayIcon.theme.systemDefault": "Use system default",
|
||||
"renderer.components.settingsPage.updates": "Updates",
|
||||
"renderer.components.settingsPage.updates.automatic": "Automatically check for updates",
|
||||
"renderer.components.settingsPage.updates.automatic.description": "If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.",
|
||||
"renderer.components.settingsPage.updates.checkNow": "Check for Updates Now",
|
||||
"renderer.components.showCertificateModal.algorithm": "Algorithm",
|
||||
"renderer.components.showCertificateModal.commonName": "Common Name",
|
||||
"renderer.components.showCertificateModal.issuerName": "Issuer Name",
|
||||
"renderer.components.showCertificateModal.noCertSelected": "No certificate Selected",
|
||||
"renderer.components.showCertificateModal.notValidAfter": "Not Valid After",
|
||||
"renderer.components.showCertificateModal.notValidBefore": "Not Valid Before",
|
||||
"renderer.components.showCertificateModal.publicKeyInfo": "Public Key Info",
|
||||
"renderer.components.showCertificateModal.serialNumber": "Serial Number",
|
||||
"renderer.components.showCertificateModal.subjectName": "Subject Name",
|
||||
"renderer.components.teamDropdownButton.noServersConfigured": "No servers configured",
|
||||
"renderer.dropdown.addAServer": "Add a server",
|
||||
"renderer.dropdown.servers": "Servers",
|
||||
"renderer.modals.certificate.certificateModal.certInfoButton": "Certificate Information",
|
||||
"renderer.modals.certificate.certificateModal.issuer": "Issuer",
|
||||
"renderer.modals.certificate.certificateModal.noCertsAvailable": "No certificates available",
|
||||
"renderer.modals.certificate.certificateModal.serial": "Serial",
|
||||
"renderer.modals.certificate.certificateModal.subject": "Subject",
|
||||
"renderer.modals.certificate.certificateModal.subtitle": "Select a certificate to authenticate yourself to {url}",
|
||||
"renderer.modals.certificate.certificateModal.title": "Select a certificate",
|
||||
"renderer.modals.login.loginModal.message.proxy": "The proxy {host}:{port} requires a username and password.",
|
||||
"renderer.modals.login.loginModal.message.server": "The server {url} requires a username and password.",
|
||||
"renderer.modals.login.loginModal.password": "Password",
|
||||
"renderer.modals.login.loginModal.title": "Authentication Required",
|
||||
"renderer.modals.login.loginModal.username": "User Name",
|
||||
"renderer.modals.permission.permissionModal.body": "A site that's not included in your Mattermost server configuration requires access for {permission}.",
|
||||
"renderer.modals.permission.permissionModal.requestOriginatedFrom": "This request originated from ",
|
||||
"renderer.modals.permission.permissionModal.title": "{permission} Required",
|
||||
"renderer.modals.permission.permissionModal.unknownOrigin": "unknown origin"
|
||||
}
|
1
i18n/en_AU.json
Normal file
1
i18n/en_AU.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/es.json
Normal file
1
i18n/es.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/fa.json
Normal file
1
i18n/fa.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/fr.json
Normal file
1
i18n/fr.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/hu.json
Normal file
1
i18n/hu.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
161
i18n/i18n.ts
Normal file
161
i18n/i18n.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable import/order */
|
||||
import bg from './bg.json';
|
||||
import de from './de.json';
|
||||
import en from './en.json';
|
||||
import enAU from './en_AU.json';
|
||||
import es from './es.json';
|
||||
import fa from './fa.json';
|
||||
import fr from './fr.json';
|
||||
import hu from './hu.json';
|
||||
import it from './it.json';
|
||||
import ja from './ja.json';
|
||||
import ko from './ko.json';
|
||||
import nl from './nl.json';
|
||||
import pl from './pl.json';
|
||||
import ptBR from './pt-BR.json';
|
||||
import ro from './ro.json';
|
||||
import ru from './ru.json';
|
||||
import sv from './sv.json';
|
||||
import tr from './tr.json';
|
||||
import uk from './uk.json';
|
||||
import zhTW from './zh-TW.json';
|
||||
import zhCN from './zh-CN.json';
|
||||
|
||||
export type Language = {
|
||||
value: string;
|
||||
name: string;
|
||||
order: number;
|
||||
url: Record<string, string>;
|
||||
};
|
||||
|
||||
export const languages: Record<string, Language> = {
|
||||
de: {
|
||||
value: 'de',
|
||||
name: 'Deutsch',
|
||||
order: 0,
|
||||
url: de,
|
||||
},
|
||||
en: {
|
||||
value: 'en',
|
||||
name: 'English (US)',
|
||||
order: 1,
|
||||
url: en,
|
||||
},
|
||||
'en-AU': {
|
||||
value: 'en-AU',
|
||||
name: 'English (Australia)',
|
||||
order: 2,
|
||||
url: enAU,
|
||||
},
|
||||
es: {
|
||||
value: 'es',
|
||||
name: 'Español',
|
||||
order: 3,
|
||||
url: es,
|
||||
},
|
||||
fr: {
|
||||
value: 'fr',
|
||||
name: 'Français',
|
||||
order: 4,
|
||||
url: fr,
|
||||
},
|
||||
it: {
|
||||
value: 'it',
|
||||
name: 'Italiano (Alpha)',
|
||||
order: 5,
|
||||
url: it,
|
||||
},
|
||||
hu: {
|
||||
value: 'hu',
|
||||
name: 'Magyar',
|
||||
order: 6,
|
||||
url: hu,
|
||||
},
|
||||
nl: {
|
||||
value: 'nl',
|
||||
name: 'Nederlands',
|
||||
order: 7,
|
||||
url: nl,
|
||||
},
|
||||
pl: {
|
||||
value: 'pl',
|
||||
name: 'Polski',
|
||||
order: 8,
|
||||
url: pl,
|
||||
},
|
||||
'pt-BR': {
|
||||
value: 'pt-BR',
|
||||
name: 'Português (Brasil) (Beta)',
|
||||
order: 9,
|
||||
url: ptBR,
|
||||
},
|
||||
ro: {
|
||||
value: 'ro',
|
||||
name: 'Română (Beta)',
|
||||
order: 10,
|
||||
url: ro,
|
||||
},
|
||||
sv: {
|
||||
value: 'sv',
|
||||
name: 'Svenska',
|
||||
order: 11,
|
||||
url: sv,
|
||||
},
|
||||
tr: {
|
||||
value: 'tr',
|
||||
name: 'Türkçe',
|
||||
order: 12,
|
||||
url: tr,
|
||||
},
|
||||
bg: {
|
||||
value: 'bg',
|
||||
name: 'Български',
|
||||
order: 13,
|
||||
url: bg,
|
||||
},
|
||||
ru: {
|
||||
value: 'ru',
|
||||
name: 'Pусский',
|
||||
order: 14,
|
||||
url: ru,
|
||||
},
|
||||
uk: {
|
||||
value: 'uk',
|
||||
name: 'Yкраїнська (Alpha)',
|
||||
order: 15,
|
||||
url: uk,
|
||||
},
|
||||
fa: {
|
||||
value: 'fa',
|
||||
name: 'فارسی (Beta)',
|
||||
order: 16,
|
||||
url: fa,
|
||||
},
|
||||
ko: {
|
||||
value: 'ko',
|
||||
name: '한국어 (Alpha)',
|
||||
order: 17,
|
||||
url: ko,
|
||||
},
|
||||
'zh-CN': {
|
||||
value: 'zh-CN',
|
||||
name: '中文 (简体)',
|
||||
order: 18,
|
||||
url: zhCN,
|
||||
},
|
||||
'zh-TW': {
|
||||
value: 'zh-TW',
|
||||
name: '中文 (繁體)',
|
||||
order: 19,
|
||||
url: zhTW,
|
||||
},
|
||||
ja: {
|
||||
value: 'ja',
|
||||
name: '日本語',
|
||||
order: 20,
|
||||
url: ja,
|
||||
},
|
||||
};
|
1
i18n/it.json
Normal file
1
i18n/it.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/ja.json
Normal file
1
i18n/ja.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/ko.json
Normal file
1
i18n/ko.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/nl.json
Normal file
1
i18n/nl.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/pl.json
Normal file
1
i18n/pl.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/pt-BR.json
Normal file
1
i18n/pt-BR.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/ro.json
Normal file
1
i18n/ro.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/ru.json
Normal file
1
i18n/ru.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/sv.json
Normal file
1
i18n/sv.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/tr.json
Normal file
1
i18n/tr.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/uk.json
Normal file
1
i18n/uk.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/zh-CN.json
Normal file
1
i18n/zh-CN.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
i18n/zh-TW.json
Normal file
1
i18n/zh-TW.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
2897
package-lock.json
generated
2897
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -65,7 +65,9 @@
|
|||
"check-build-config:build": "babel ./src/common/config/buildConfig.ts -o ./dist/buildConfig.js",
|
||||
"check-build-config:run": "node -r @babel/register scripts/check_build_config.js",
|
||||
"check-types": "tsc",
|
||||
"prune": "ts-prune"
|
||||
"prune": "ts-prune",
|
||||
"mmjstool": "mmjstool",
|
||||
"i18n-extract": "npm run mmjstool -- i18n extract-desktop"
|
||||
},
|
||||
"jest": {
|
||||
"clearMocks": true,
|
||||
|
@ -160,6 +162,7 @@
|
|||
"jest": "27.5.1",
|
||||
"jest-junit": "13.1.0",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"mmjstool": "github:mattermost/mattermost-utilities#d849d3819112bd828f08caf0155bd7ed62f18950",
|
||||
"mocha-circleci-reporter": "0.0.3",
|
||||
"node-gyp": "9.0.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
|
@ -171,7 +174,7 @@
|
|||
"ts-prune": "0.10.3",
|
||||
"typescript": "4.6.3",
|
||||
"webpack": "5.71.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-dev-server": "4.8.0",
|
||||
"webpack-merge": "5.8.0"
|
||||
},
|
||||
|
@ -193,6 +196,7 @@
|
|||
"react-beautiful-dnd": "13.1.0",
|
||||
"react-bootstrap": "1.6.4",
|
||||
"react-dom": "16.14.0",
|
||||
"react-intl": "5.20.10",
|
||||
"react-select": "5.2.2",
|
||||
"sass": "1.49.11",
|
||||
"semver": "7.3.5",
|
||||
|
|
|
@ -122,3 +122,7 @@ export const RELOAD_CURRENT_VIEW = 'reload-current-view';
|
|||
|
||||
export const PING_DOMAIN = 'ping-domain';
|
||||
export const PING_DOMAIN_RESPONSE = 'ping-domain-response';
|
||||
|
||||
export const GET_LANGUAGE_INFORMATION = 'get-language-information';
|
||||
export const RETRIEVED_LANGUAGE_INFORMATION = 'retrieved-language-information';
|
||||
export const GET_AVAILABLE_LANGUAGES = 'get-available-languages';
|
||||
|
|
|
@ -328,6 +328,10 @@ export class Config extends EventEmitter {
|
|||
return this.combinedData?.autoCheckForUpdates;
|
||||
}
|
||||
|
||||
get appLanguage() {
|
||||
return this.combinedData?.appLanguage;
|
||||
}
|
||||
|
||||
// initialization/processing methods
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,5 +6,5 @@ export const BASIC_AUTH_PERMISSION = 'canBasicAuth';
|
|||
|
||||
// Permission descriptions
|
||||
export const PERMISSION_DESCRIPTION = {
|
||||
[BASIC_AUTH_PERMISSION]: 'Web Authentication',
|
||||
[BASIC_AUTH_PERMISSION]: 'common.permissions.canBasicAuth',
|
||||
};
|
||||
|
|
|
@ -59,10 +59,6 @@ export function getServerView(srv: MattermostServer, tab: Tab) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getTabViewName(serverName: string, tabType: string) {
|
||||
return `${serverName}___${tabType}`;
|
||||
}
|
||||
|
||||
export function getTabDisplayName(tabType: TabType) {
|
||||
switch (tabType) {
|
||||
case TAB_MESSAGING:
|
||||
|
@ -76,6 +72,10 @@ export function getTabDisplayName(tabType: TabType) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getTabViewName(serverName: string, tabType: string) {
|
||||
return `${serverName}___${tabType}`;
|
||||
}
|
||||
|
||||
export function canCloseTab(tabType: TabType) {
|
||||
return tabType !== TAB_MESSAGING;
|
||||
}
|
||||
|
|
|
@ -60,6 +60,10 @@ function isVersionGreaterThanOrEqualTo(currentVersion: string, compareVersion: s
|
|||
return true;
|
||||
}
|
||||
|
||||
export function t(s: string) {
|
||||
return s;
|
||||
}
|
||||
|
||||
export default {
|
||||
getDisplayBoundaries,
|
||||
runMode,
|
||||
|
|
|
@ -14,6 +14,10 @@ jest.mock('electron', () => ({
|
|||
|
||||
jest.mock('electron-is-dev', () => false);
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('main/AutoLauncher', () => {
|
||||
let autoLauncher;
|
||||
const isEnabled = jest.fn();
|
||||
|
|
|
@ -22,7 +22,7 @@ export class AutoLauncher {
|
|||
return;
|
||||
}
|
||||
const appLauncher = new AutoLaunch({
|
||||
name: 'Mattermost',
|
||||
name: app.name,
|
||||
});
|
||||
const enabled = await appLauncher.isEnabled();
|
||||
if (enabled) {
|
||||
|
|
|
@ -36,6 +36,10 @@ jest.mock('child_process', () => ({
|
|||
spawn: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('main/CriticalErrorHandler', () => {
|
||||
const criticalErrorHandler = new CriticalErrorHandler();
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -8,12 +8,9 @@ import os from 'os';
|
|||
import path from 'path';
|
||||
|
||||
import {app, BrowserWindow, dialog} from 'electron';
|
||||
|
||||
import log from 'electron-log';
|
||||
|
||||
const BUTTON_OK = 'OK';
|
||||
const BUTTON_SHOW_DETAILS = 'Show Details';
|
||||
const BUTTON_REOPEN = 'Reopen';
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
function createErrorReport(err: Error) {
|
||||
// eslint-disable-next-line no-undef
|
||||
|
@ -52,8 +49,11 @@ export class CriticalErrorHandler {
|
|||
dialog.showMessageBox(this.mainWindow, {
|
||||
type: 'warning',
|
||||
title: app.name,
|
||||
message: 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?',
|
||||
buttons: ['No', 'Yes'],
|
||||
message: localizeMessage('main.CriticalErrorHandler.unresponsive.dialog.message', 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?'),
|
||||
buttons: [
|
||||
localizeMessage('label.no', 'No'),
|
||||
localizeMessage('label.yes', 'Yes'),
|
||||
],
|
||||
defaultId: 0,
|
||||
}).then(({response}) => {
|
||||
if (response === 0) {
|
||||
|
@ -69,9 +69,17 @@ export class CriticalErrorHandler {
|
|||
fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL));
|
||||
|
||||
if (app.isReady()) {
|
||||
const buttons = [BUTTON_SHOW_DETAILS, BUTTON_OK, BUTTON_REOPEN];
|
||||
const buttons = [
|
||||
localizeMessage('main.CriticalErrorHandler.uncaughtException.button.showDetails', 'Show Details'),
|
||||
localizeMessage('label.ok', 'OK'),
|
||||
localizeMessage('main.CriticalErrorHandler.uncaughtException.button.reopen', 'Reopen'),
|
||||
];
|
||||
let indexOfReopen = 2;
|
||||
let indexOfShowDetails = 0;
|
||||
if (process.platform === 'darwin') {
|
||||
buttons.reverse();
|
||||
indexOfReopen = 0;
|
||||
indexOfShowDetails = 2;
|
||||
}
|
||||
if (!this.mainWindow?.isVisible) {
|
||||
return;
|
||||
|
@ -81,15 +89,24 @@ export class CriticalErrorHandler {
|
|||
{
|
||||
type: 'error',
|
||||
title: app.name,
|
||||
message: `The ${app.name} app quit unexpectedly. Click "Show Details" to learn more or "Reopen" to open the application again.\n\nInternal error: ${err.message}`,
|
||||
message: localizeMessage(
|
||||
'main.CriticalErrorHandler.uncaughtException.dialog.message',
|
||||
'The {appName} app quit unexpectedly. Click "{showDetails}" to learn more or "{reopen}" to open the application again.\n\nInternal error: {err}',
|
||||
{
|
||||
appName: app.name,
|
||||
showDetails: localizeMessage('main.CriticalErrorHandler.uncaughtException.button.showDetails', 'Show Details'),
|
||||
reopen: localizeMessage('main.CriticalErrorHandler.uncaughtException.button.reopen', 'Reopen'),
|
||||
err: err.message,
|
||||
},
|
||||
),
|
||||
buttons,
|
||||
defaultId: buttons.indexOf(BUTTON_REOPEN),
|
||||
defaultId: indexOfReopen,
|
||||
noLink: true,
|
||||
},
|
||||
).then(({response}) => {
|
||||
let child;
|
||||
switch (response) {
|
||||
case buttons.indexOf(BUTTON_SHOW_DETAILS):
|
||||
case indexOfShowDetails:
|
||||
child = openDetachedExternal(file);
|
||||
if (child) {
|
||||
child.on(
|
||||
|
@ -101,7 +118,7 @@ export class CriticalErrorHandler {
|
|||
child.unref();
|
||||
}
|
||||
break;
|
||||
case buttons.indexOf(BUTTON_REOPEN):
|
||||
case indexOfReopen:
|
||||
app.relaunch();
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,9 @@ function triageArgs(args: string[]) {
|
|||
// Note that yargs is able to exit the node process when handling
|
||||
// certain flags, like version or help.
|
||||
// https://github.com/yargs/yargs/blob/main/docs/api.md#exitprocessenable
|
||||
|
||||
// TODO: Translations?
|
||||
|
||||
function parseArgs(args: string[]) {
|
||||
return yargs.
|
||||
alias('dataDir', 'd').
|
||||
|
|
|
@ -131,6 +131,7 @@ const configDataSchemaV3 = Joi.object<ConfigV3>({
|
|||
alwaysMinimize: Joi.boolean(),
|
||||
alwaysClose: Joi.boolean(),
|
||||
logLevel: Joi.string().default('info'),
|
||||
appLanguage: Joi.string().allow(''),
|
||||
});
|
||||
|
||||
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
|
||||
|
|
|
@ -49,6 +49,10 @@ jest.mock('./windows/windowManager', () => ({
|
|||
getMainWindow: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('main/allowProtocolDialog', () => {
|
||||
describe('init', () => {
|
||||
it('should copy data from file when no error', () => {
|
||||
|
|
|
@ -8,6 +8,8 @@ import fs from 'fs';
|
|||
import {dialog, shell} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
import {protocols} from '../../electron-builder.json';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
@ -54,15 +56,15 @@ export class AllowProtocolDialog {
|
|||
return;
|
||||
}
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
title: 'Non http(s) protocol',
|
||||
message: `${protocol} link requires an external application.`,
|
||||
detail: `The requested link is ${URL} . Do you want to continue?`,
|
||||
title: localizeMessage('main.allowProtocolDialog.title', 'Non http(s) protocol'),
|
||||
message: localizeMessage('main.allowProtocolDialog.message', '{protocol} link requires an external application.', {protocol}),
|
||||
detail: localizeMessage('main.allowProtocolDialog.detail', 'The requested link is {URL}. Do you want to continue?', {URL}),
|
||||
defaultId: 2,
|
||||
type: 'warning',
|
||||
buttons: [
|
||||
'Yes',
|
||||
`Yes (Save ${protocol} as allowed)`,
|
||||
'No',
|
||||
localizeMessage('label.yes', 'Yes'),
|
||||
localizeMessage('main.allowProtocolDialog.button.saveProtocolAsAllowed', 'Yes (Save {protocol} as allowed)', {protocol}),
|
||||
localizeMessage('label.no', 'No'),
|
||||
],
|
||||
cancelId: 2,
|
||||
noLink: true,
|
||||
|
|
|
@ -33,6 +33,9 @@ jest.mock('main/certificateStore', () => ({
|
|||
add: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/tray/tray', () => ({}));
|
||||
jest.mock('main/windows/windowManager', () => ({
|
||||
getMainWindow: jest.fn(),
|
||||
|
|
|
@ -8,6 +8,7 @@ import urlUtils from 'common/utils/url';
|
|||
|
||||
import updateManager from 'main/autoUpdater';
|
||||
import CertificateStore from 'main/certificateStore';
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
import {destroyTray} from 'main/tray/tray';
|
||||
import WindowManager from 'main/windows/windowManager';
|
||||
|
||||
|
@ -96,8 +97,8 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo
|
|||
certificateErrorCallbacks.set(errorID, callback);
|
||||
return;
|
||||
}
|
||||
const extraDetail = CertificateStore.isExisting(origin) ? 'Certificate is different from previous one.\n\n' : '';
|
||||
const detail = `${extraDetail}origin: ${origin}\nError: ${error}`;
|
||||
const extraDetail = CertificateStore.isExisting(origin) ? localizeMessage('main.app.app.handleAppCertificateError.dialog.extraDetail', 'Certificate is different from previous one.\n\n') : '';
|
||||
const detail = localizeMessage('main.app.app.handleAppCertificateError.certError.dialog.detail', '{extraDetail}origin: {origin}\nError: {error}', {extraDetail, origin, error});
|
||||
|
||||
certificateErrorCallbacks.set(errorID, callback);
|
||||
|
||||
|
@ -109,21 +110,27 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo
|
|||
|
||||
try {
|
||||
let result = await dialog.showMessageBox(mainWindow, {
|
||||
title: 'Certificate Error',
|
||||
message: 'There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.',
|
||||
title: localizeMessage('main.app.app.handleAppCertificateError.certError.dialog.title', 'Certificate Error'),
|
||||
message: localizeMessage('main.app.app.handleAppCertificateError.certError.dialog.message', 'There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.'),
|
||||
type: 'error',
|
||||
detail,
|
||||
buttons: ['More Details', 'Cancel Connection'],
|
||||
buttons: [
|
||||
localizeMessage('main.app.app.handleAppCertificateError.certError.button.moreDetails', 'More Details'),
|
||||
localizeMessage('main.app.app.handleAppCertificateError.certError.button.cancelConnection', 'Cancel Connection'),
|
||||
],
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
result = await dialog.showMessageBox(mainWindow, {
|
||||
title: 'Certificate Not Trusted',
|
||||
message: `Certificate from "${certificate.issuerName}" is not trusted.`,
|
||||
title: localizeMessage('main.app.app.handleAppCertificateError.certNotTrusted.dialog.title', 'Certificate Not Trusted'),
|
||||
message: localizeMessage('main.app.app.handleAppCertificateError.certNotTrusted.dialog.message', 'Certificate from "{issuerName}" is not trusted.', {issuerName: certificate.issuerName}),
|
||||
detail: extraDetail,
|
||||
type: 'error',
|
||||
buttons: ['Trust Insecure Certificate', 'Cancel Connection'],
|
||||
buttons: [
|
||||
localizeMessage('main.app.app.handleAppCertificateError.certNotTrusted.button.trustInsecureCertificate', 'Trust Insecure Certificate'),
|
||||
localizeMessage('main.app.app.handleAppCertificateError.certNotTrusted.button.cancelConnection', 'Cancel Connection'),
|
||||
],
|
||||
cancelId: 1,
|
||||
checkboxChecked: false,
|
||||
checkboxLabel: "Don't ask again",
|
||||
|
|
|
@ -39,6 +39,8 @@ jest.mock('electron', () => ({
|
|||
setAppUserModelId: jest.fn(),
|
||||
getVersion: jest.fn(),
|
||||
whenReady: jest.fn(),
|
||||
getLocale: jest.fn(),
|
||||
getLocaleCountryCode: jest.fn(),
|
||||
},
|
||||
ipcMain: {
|
||||
on: jest.fn(),
|
||||
|
@ -54,6 +56,11 @@ jest.mock('electron', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
setLocale: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('electron-devtools-installer', () => {
|
||||
return () => ({
|
||||
REACT_DEVELOPER_TOOLS: 'react-developer-tools',
|
||||
|
|
|
@ -47,6 +47,7 @@ import {setupBadge} from 'main/badge';
|
|||
import CertificateManager from 'main/certificateManager';
|
||||
import {updatePaths} from 'main/constants';
|
||||
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
||||
import i18nManager, {localizeMessage} from 'main/i18nManager';
|
||||
import {displayDownloadCompleted} from 'main/notifications';
|
||||
import parseArgs from 'main/ParseArgs';
|
||||
import TrustedOriginsStore from 'main/trustedOrigins';
|
||||
|
@ -359,7 +360,7 @@ function initializeAfterAppReady() {
|
|||
const filters = [];
|
||||
if (fileElements.length > 1) {
|
||||
filters.push({
|
||||
name: 'All files',
|
||||
name: localizeMessage('main.app.initialize.downloadBox.allFiles', 'All files'),
|
||||
extensions: ['*'],
|
||||
});
|
||||
}
|
||||
|
@ -376,6 +377,14 @@ function initializeAfterAppReady() {
|
|||
});
|
||||
});
|
||||
|
||||
// needs to be done after app ready
|
||||
// must be done before update menu
|
||||
if (Config.appLanguage) {
|
||||
i18nManager.setLocale(Config.appLanguage);
|
||||
} else if (!i18nManager.setLocale(app.getLocale())) {
|
||||
i18nManager.setLocale(app.getLocaleCountryCode());
|
||||
}
|
||||
|
||||
handleUpdateMenuEvent();
|
||||
|
||||
ipcMain.emit('update-dict');
|
||||
|
|
|
@ -49,6 +49,9 @@ jest.mock('main/autoUpdater', () => ({}));
|
|||
jest.mock('main/constants', () => ({
|
||||
updatePaths: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
jest.mock('main/menus/app', () => ({}));
|
||||
jest.mock('main/menus/tray', () => ({}));
|
||||
jest.mock('main/server/serverInfo', () => ({
|
||||
|
|
|
@ -21,6 +21,7 @@ import Utils from 'common/utils/util';
|
|||
|
||||
import updateManager from 'main/autoUpdater';
|
||||
import {migrationInfoPath, updatePaths} from 'main/constants';
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
import {createMenu as createAppMenu} from 'main/menus/app';
|
||||
import {createMenu as createTrayMenu} from 'main/menus/tray';
|
||||
import {ServerInfo} from 'main/server/serverInfo';
|
||||
|
@ -224,11 +225,14 @@ export function migrateMacAppStore() {
|
|||
}
|
||||
|
||||
const cancelImport = dialog.showMessageBoxSync({
|
||||
title: 'Mattermost',
|
||||
message: 'Import Existing Configuration',
|
||||
detail: 'It appears that an existing Mattermost configuration exists, would you like to import it? You will be asked to pick the correct configuration directory.',
|
||||
title: app.name,
|
||||
message: localizeMessage('main.app.utils.migrateMacAppStore.dialog.message', 'Import Existing Configuration'),
|
||||
detail: localizeMessage('main.app.utils.migrateMacAppStore.dialog.detail', 'It appears that an existing {appName} configuration exists, would you like to import it? You will be asked to pick the correct configuration directory.', {appName: app.name}),
|
||||
icon: appIcon,
|
||||
buttons: ['Select Directory and Import', 'Don\'t Import'],
|
||||
buttons: [
|
||||
localizeMessage('main.app.utils.migrateMacAppStore.button.selectAndImport', 'Select Directory and Import'),
|
||||
localizeMessage('main.app.utils.migrateMacAppStore.button.dontImport', 'Don\'t Import'),
|
||||
],
|
||||
type: 'info',
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
|
|
|
@ -45,6 +45,10 @@ jest.mock('main/windows/windowManager', () => ({
|
|||
sendToRenderer: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('main/autoUpdater', () => {
|
||||
describe('constructor', () => {
|
||||
afterEach(() => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import log from 'electron-log';
|
|||
|
||||
import {autoUpdater, ProgressInfo, UpdateInfo} from 'electron-updater';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
import {displayUpgrade, displayRestartToUpgrade} from 'main/notifications';
|
||||
|
||||
import {CANCEL_UPGRADE, UPDATE_AVAILABLE, UPDATE_DOWNLOADED, CHECK_FOR_UPDATES, UPDATE_SHORTCUT_MENU, UPDATE_PROGRESS} from 'common/communication';
|
||||
|
@ -108,11 +109,14 @@ export class UpdateManager {
|
|||
clearTimeout(this.lastCheck);
|
||||
}
|
||||
dialog.showMessageBox({
|
||||
title: 'Mattermost',
|
||||
message: 'New desktop version available',
|
||||
detail: 'A new version of the Mattermost Desktop app is available for you to download and install now.',
|
||||
title: app.name,
|
||||
message: localizeMessage('main.autoUpdater.download.dialog.message', 'New desktop version available'),
|
||||
detail: localizeMessage('main.autoUpdater.download.dialog.detail', 'A new version of the {appName} Desktop App is available for you to download and install now.', {appName: app.name}),
|
||||
icon: appIcon,
|
||||
buttons: ['Download', 'Remind me Later'],
|
||||
buttons: [
|
||||
localizeMessage('main.autoUpdater.download.dialog.button.download', 'Download'),
|
||||
localizeMessage('main.autoUpdater.download.dialog.button.remindMeLater', 'Remind me Later'),
|
||||
],
|
||||
type: 'info',
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
|
@ -131,11 +135,14 @@ export class UpdateManager {
|
|||
|
||||
handleUpdate = (): void => {
|
||||
dialog.showMessageBox({
|
||||
title: 'Mattermost',
|
||||
message: 'A new version is ready to install',
|
||||
detail: 'A new version of the Mattermost Desktop app is ready to install.',
|
||||
title: app.name,
|
||||
message: localizeMessage('main.autoUpdater.update.dialog.message', 'A new version is ready to install'),
|
||||
detail: localizeMessage('main.autoUpdater.update.dialog.detail', 'A new version of the {appName} Desktop App is ready to install.', {appName: app.name}),
|
||||
icon: appIcon,
|
||||
buttons: ['Restart and Update', 'Remind me Later'],
|
||||
buttons: [
|
||||
localizeMessage('main.autoUpdater.update.dialog.button.restartAndUpdate', 'Restart and Update'),
|
||||
localizeMessage('main.autoUpdater.update.dialog.button.remindMeLater', 'Remind me Later'),
|
||||
],
|
||||
type: 'info',
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
|
@ -149,12 +156,12 @@ export class UpdateManager {
|
|||
displayNoUpgrade = (): void => {
|
||||
const version = app.getVersion();
|
||||
dialog.showMessageBox({
|
||||
title: 'Mattermost',
|
||||
title: app.name,
|
||||
icon: appIcon,
|
||||
message: 'You\'re up to date',
|
||||
message: localizeMessage('main.autoUpdater.noUpdate.message', 'You\'re up to date'),
|
||||
type: 'info',
|
||||
buttons: ['OK'],
|
||||
detail: `You are using the latest version of the Mattermost Desktop App (version ${version}). You'll be notified when a new version is available to install.`,
|
||||
buttons: [localizeMessage('label.ok', 'OK')],
|
||||
detail: localizeMessage('main.autoUpdater.noUpdate.detail', 'You are using the latest version of the {appName} Desktop App (version {version}). You\'ll be notified when a new version is available to install.', {appName: app.name, version}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,10 @@ jest.mock('./windows/windowManager', () => ({
|
|||
setOverlayIcon: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn().mockReturnValue(''),
|
||||
}));
|
||||
|
||||
describe('main/badge', () => {
|
||||
describe('showBadgeWindows', () => {
|
||||
it('should show dot when session expired', () => {
|
||||
|
|
|
@ -7,6 +7,8 @@ import log from 'electron-log';
|
|||
|
||||
import {UPDATE_BADGE} from 'common/communication';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
import WindowManager from './windows/windowManager';
|
||||
import * as AppState from './appState';
|
||||
|
||||
|
@ -15,17 +17,17 @@ const MAX_WIN_COUNT = 99;
|
|||
let showUnreadBadgeSetting: boolean;
|
||||
|
||||
export function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
|
||||
let description = 'You have no unread messages';
|
||||
let description = localizeMessage('main.badge.noUnreads', 'You have no unread messages');
|
||||
let text;
|
||||
if (mentionCount > 0) {
|
||||
text = (mentionCount > MAX_WIN_COUNT) ? `${MAX_WIN_COUNT}+` : mentionCount.toString();
|
||||
description = `You have unread mentions (${mentionCount})`;
|
||||
description = localizeMessage('main.badge.unreadMentions', 'You have unread mentions ({mentionCount})', {mentionCount});
|
||||
} else if (showUnreadBadge && showUnreadBadgeSetting) {
|
||||
text = '•';
|
||||
description = 'You have unread channels';
|
||||
description = localizeMessage('main.badge.unreadChannels', 'You have unread channels');
|
||||
} else if (sessionExpired) {
|
||||
text = '•';
|
||||
description = 'Session Expired: Please sign in to continue receiving notifications.';
|
||||
description = localizeMessage('main.badge.sessionExpired', 'Session Expired: Please sign in to continue receiving notifications.');
|
||||
}
|
||||
WindowManager.setOverlayIcon(text, description, mentionCount > 99);
|
||||
}
|
||||
|
|
72
src/main/i18nManager.test.js
Normal file
72
src/main/i18nManager.test.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import i18nManager, {I18nManager, localizeMessage} from 'main/i18nManager';
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('electron-log', () => ({
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('main/i18nManager', () => {
|
||||
it('should default to English', () => {
|
||||
const i18n = new I18nManager();
|
||||
expect(i18n.currentLanguage.value).toBe('en');
|
||||
});
|
||||
|
||||
it('should set locale only if available', () => {
|
||||
const i18n = new I18nManager();
|
||||
|
||||
expect(i18n.setLocale('fr')).toBe(true);
|
||||
expect(i18n.currentLanguage.value).toBe('fr');
|
||||
expect(i18n.setLocale('zz')).toBe(false);
|
||||
expect(i18n.currentLanguage.value).toBe('fr');
|
||||
});
|
||||
});
|
||||
|
||||
describe('main/i18nManager/localizeMessage', () => {
|
||||
i18nManager.currentLanguage = {
|
||||
url: {
|
||||
simple_key: 'simple_translation',
|
||||
simple_replace_key: 'simple_translation {key}',
|
||||
replace_two_key: '{replace} {replace_again}',
|
||||
nested_braces: '{{replace}}',
|
||||
multiple_same: '{replace} {replace} {key}',
|
||||
},
|
||||
};
|
||||
|
||||
it('should get a simple translation', () => {
|
||||
expect(localizeMessage('simple_key', 'different_translation')).toBe('simple_translation');
|
||||
});
|
||||
|
||||
it('should default if does not exist', () => {
|
||||
expect(localizeMessage('unsimple_key', 'different_translation')).toBe('different_translation');
|
||||
});
|
||||
|
||||
it('should replace', () => {
|
||||
expect(localizeMessage('simple_replace_key', null, {key: 'replacement'})).toBe('simple_translation replacement');
|
||||
});
|
||||
|
||||
it('should not replace if key is missing', () => {
|
||||
expect(localizeMessage('simple_replace_key', null, {})).toBe('simple_translation {key}');
|
||||
});
|
||||
|
||||
it('should replace twice', () => {
|
||||
expect(localizeMessage('replace_two_key', null, {replace: 'replacement1', replace_again: 'replacement2'})).toBe('replacement1 replacement2');
|
||||
});
|
||||
|
||||
it('should ignore nested braces', () => {
|
||||
expect(localizeMessage('nested_braces', null, {replace: 'replacement'})).toBe('{replacement}');
|
||||
});
|
||||
|
||||
it('should replace multiple of the same', () => {
|
||||
expect(localizeMessage('multiple_same', null, {replace: 'replacement', key: 'key1'})).toBe('replacement replacement key1');
|
||||
});
|
||||
});
|
60
src/main/i18nManager.ts
Normal file
60
src/main/i18nManager.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ipcMain} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {GET_AVAILABLE_LANGUAGES, GET_LANGUAGE_INFORMATION} from 'common/communication';
|
||||
|
||||
import {Language, languages} from '../../i18n/i18n';
|
||||
|
||||
export function localizeMessage(s: string, defaultString = '', values: any = {}) {
|
||||
let str = i18nManager.currentLanguage.url[s] || defaultString;
|
||||
for (const key of Object.keys(values)) {
|
||||
str = str.replace(new RegExp(`{${key}}`, 'g'), values[key]);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export class I18nManager {
|
||||
currentLanguage: Language;
|
||||
|
||||
constructor() {
|
||||
this.currentLanguage = this.getLanguages().en;
|
||||
|
||||
ipcMain.handle(GET_LANGUAGE_INFORMATION, this.getCurrentLanguage);
|
||||
ipcMain.handle(GET_AVAILABLE_LANGUAGES, this.getAvailableLanguages);
|
||||
}
|
||||
|
||||
setLocale = (locale: string) => {
|
||||
log.debug('i18nManager.setLocale', locale);
|
||||
|
||||
if (this.isLanguageAvailable(locale)) {
|
||||
this.currentLanguage = this.getLanguages()[locale];
|
||||
log.info('Set new language', locale);
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn('Failed to set new language', locale);
|
||||
return false;
|
||||
}
|
||||
|
||||
getLanguages = () => {
|
||||
return languages;
|
||||
}
|
||||
|
||||
getAvailableLanguages = () => {
|
||||
return Object.keys(languages);
|
||||
}
|
||||
|
||||
isLanguageAvailable = (locale: string) => {
|
||||
return Boolean(this.getLanguages()[locale]);
|
||||
}
|
||||
|
||||
getCurrentLanguage = () => {
|
||||
return this.currentLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
const i18nManager = new I18nManager();
|
||||
export default i18nManager;
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
import WindowManager from 'main/windows/windowManager';
|
||||
|
||||
import {createTemplate} from './app';
|
||||
|
@ -14,6 +15,10 @@ jest.mock('electron', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/windows/windowManager', () => ({
|
||||
getCurrentTeamName: jest.fn(),
|
||||
}));
|
||||
|
@ -97,6 +102,12 @@ describe('main/menus/app', () => {
|
|||
});
|
||||
|
||||
it('should include About <appname> in menu on mac', () => {
|
||||
localizeMessage.mockImplementation((id) => {
|
||||
if (id === 'main.menus.app.file.about') {
|
||||
return 'About AppName';
|
||||
}
|
||||
return id;
|
||||
});
|
||||
const menu = createTemplate(config);
|
||||
const appNameMenu = menu.find((item) => item.label === '&AppName');
|
||||
const menuItem = appNameMenu.submenu.find((item) => item.label === 'About AppName');
|
||||
|
@ -105,23 +116,45 @@ describe('main/menus/app', () => {
|
|||
});
|
||||
|
||||
it('should contain hide options', () => {
|
||||
localizeMessage.mockImplementation((id) => {
|
||||
if (id === 'main.menus.app.file') {
|
||||
return '&AppName';
|
||||
}
|
||||
return id;
|
||||
});
|
||||
const menu = createTemplate(config);
|
||||
const appNameMenu = menu.find((item) => item.label === '&AppName');
|
||||
expect(appNameMenu.submenu).toContainEqual({role: 'hide'});
|
||||
expect(appNameMenu.submenu).toContainEqual({role: 'unhide'});
|
||||
expect(appNameMenu.submenu).toContainEqual({role: 'hideOthers'});
|
||||
expect(appNameMenu.submenu).toContainEqual(expect.objectContaining({role: 'hide'}));
|
||||
expect(appNameMenu.submenu).toContainEqual(expect.objectContaining({role: 'unhide'}));
|
||||
expect(appNameMenu.submenu).toContainEqual(expect.objectContaining({role: 'hideOthers'}));
|
||||
});
|
||||
|
||||
it('should contain zoom and front options in Window', () => {
|
||||
localizeMessage.mockImplementation((id) => {
|
||||
if (id === 'main.menus.app.window') {
|
||||
return '&Window';
|
||||
}
|
||||
return id;
|
||||
});
|
||||
const menu = createTemplate(config);
|
||||
const windowMenu = menu.find((item) => item.label === '&Window');
|
||||
expect(windowMenu.role).toBe('windowMenu');
|
||||
expect(windowMenu.submenu).toContainEqual({role: 'zoom'});
|
||||
expect(windowMenu.submenu).toContainEqual({role: 'front'});
|
||||
expect(windowMenu.submenu).toContainEqual(expect.objectContaining({role: 'zoom'}));
|
||||
expect(windowMenu.submenu).toContainEqual(expect.objectContaining({role: 'front'}));
|
||||
});
|
||||
});
|
||||
|
||||
it('should show `Sign in to Another Server` if `enableServerManagement` is true', () => {
|
||||
localizeMessage.mockImplementation((id) => {
|
||||
switch (id) {
|
||||
case 'main.menus.app.file':
|
||||
return '&File';
|
||||
case 'main.menus.app.file.signInToAnotherServer':
|
||||
return 'Sign in to Another Server';
|
||||
default:
|
||||
return id;
|
||||
}
|
||||
});
|
||||
const menu = createTemplate(config);
|
||||
const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File');
|
||||
const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server');
|
||||
|
@ -129,6 +162,16 @@ describe('main/menus/app', () => {
|
|||
});
|
||||
|
||||
it('should not show `Sign in to Another Server` if `enableServerManagement` is false', () => {
|
||||
localizeMessage.mockImplementation((id) => {
|
||||
switch (id) {
|
||||
case 'main.menus.app.file':
|
||||
return '&File';
|
||||
case 'main.menus.app.file.signInToAnotherServer':
|
||||
return 'Sign in to Another Server';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
const modifiedConfig = {
|
||||
...config,
|
||||
enableServerManagement: false,
|
||||
|
@ -140,6 +183,12 @@ describe('main/menus/app', () => {
|
|||
});
|
||||
|
||||
it('should show the first 9 servers (using order) in the Window menu', () => {
|
||||
localizeMessage.mockImplementation((id) => {
|
||||
if (id === 'main.menus.app.window') {
|
||||
return '&Window';
|
||||
}
|
||||
return id;
|
||||
});
|
||||
const modifiedConfig = {
|
||||
data: {
|
||||
...config.data,
|
||||
|
@ -174,6 +223,15 @@ describe('main/menus/app', () => {
|
|||
});
|
||||
|
||||
it('should show the first 9 tabs (using order) in the Window menu', () => {
|
||||
localizeMessage.mockImplementation((id) => {
|
||||
if (id === 'main.menus.app.window') {
|
||||
return '&Window';
|
||||
}
|
||||
if (id.startsWith('common.tabs')) {
|
||||
return id.replace('common.tabs.', '');
|
||||
}
|
||||
return id;
|
||||
});
|
||||
WindowManager.getCurrentTeamName.mockImplementation(() => config.data.teams[0].name);
|
||||
|
||||
const modifiedConfig = {
|
||||
|
|
|
@ -6,9 +6,11 @@
|
|||
import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, clipboard} from 'electron';
|
||||
|
||||
import {BROWSER_HISTORY_BUTTON, OPEN_TEAMS_DROPDOWN, SHOW_NEW_SERVER_MODAL} from 'common/communication';
|
||||
import {t} from 'common/utils/util';
|
||||
import {getTabDisplayName, TabType} from 'common/tabs/TabView';
|
||||
import {Config} from 'common/config';
|
||||
import {TabType, getTabDisplayName} from 'common/tabs/TabView';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
import WindowManager from 'main/windows/windowManager';
|
||||
import {UpdateManager} from 'main/autoUpdater';
|
||||
|
||||
|
@ -19,16 +21,16 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
|
||||
const isMac = process.platform === 'darwin';
|
||||
const appName = app.name;
|
||||
const firstMenuName = isMac ? appName : 'File';
|
||||
const firstMenuName = isMac ? '&' + appName : localizeMessage('main.menus.app.file', '&File');
|
||||
const template = [];
|
||||
|
||||
const settingsLabel = isMac ? 'Preferences...' : 'Settings...';
|
||||
const settingsLabel = isMac ? localizeMessage('main.menus.app.file.preferences', 'Preferences...') : localizeMessage('main.menus.app.file.settings', 'Settings...');
|
||||
|
||||
let platformAppMenu = [];
|
||||
if (isMac) {
|
||||
platformAppMenu.push(
|
||||
{
|
||||
label: 'About ' + appName,
|
||||
label: localizeMessage('main.menus.app.file.about', 'About {appName}', {appName}),
|
||||
role: 'about',
|
||||
},
|
||||
);
|
||||
|
@ -44,7 +46,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
|
||||
if (config.data?.enableServerManagement === true) {
|
||||
platformAppMenu.push({
|
||||
label: 'Sign in to Another Server',
|
||||
label: localizeMessage('main.menus.app.file.signInToAnotherServer', 'Sign in to Another Server'),
|
||||
click() {
|
||||
ipcMain.emit(SHOW_NEW_SERVER_MODAL);
|
||||
},
|
||||
|
@ -55,67 +57,79 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
platformAppMenu = platformAppMenu.concat([
|
||||
separatorItem, {
|
||||
role: 'hide',
|
||||
label: localizeMessage('main.menus.app.file.hide', 'Hide {appName}', {appName}),
|
||||
}, {
|
||||
role: 'hideOthers',
|
||||
label: localizeMessage('main.menus.app.file.hideOthers', 'Hide Others'),
|
||||
}, {
|
||||
role: 'unhide',
|
||||
label: localizeMessage('main.menus.app.file.unhide', 'Show All'),
|
||||
}, separatorItem, {
|
||||
role: 'quit',
|
||||
label: localizeMessage('main.menus.app.file.quit', 'Quit {appName}', {appName}),
|
||||
}]);
|
||||
} else {
|
||||
platformAppMenu = platformAppMenu.concat([
|
||||
separatorItem, {
|
||||
role: 'quit',
|
||||
label: localizeMessage('main.menus.app.file.exit', 'Exit'),
|
||||
accelerator: 'CmdOrCtrl+Q',
|
||||
}]);
|
||||
}
|
||||
|
||||
template.push({
|
||||
label: '&' + firstMenuName,
|
||||
label: firstMenuName,
|
||||
submenu: [
|
||||
...platformAppMenu,
|
||||
],
|
||||
});
|
||||
template.push({
|
||||
label: '&Edit',
|
||||
label: localizeMessage('main.menus.app.edit', '&Edit'),
|
||||
submenu: [{
|
||||
role: 'undo',
|
||||
label: localizeMessage('main.menus.app.edit.undo', 'Undo'),
|
||||
accelerator: 'CmdOrCtrl+Z',
|
||||
}, {
|
||||
role: 'Redo',
|
||||
label: localizeMessage('main.menus.app.edit.redo', 'Redo'),
|
||||
accelerator: 'CmdOrCtrl+SHIFT+Z',
|
||||
}, separatorItem, {
|
||||
role: 'cut',
|
||||
label: localizeMessage('main.menus.app.edit.cut', 'Cut'),
|
||||
accelerator: 'CmdOrCtrl+X',
|
||||
}, {
|
||||
role: 'copy',
|
||||
label: localizeMessage('main.menus.app.edit.copy', 'Copy'),
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
}, {
|
||||
role: 'paste',
|
||||
label: localizeMessage('main.menus.app.edit.paste', 'Paste'),
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
}, {
|
||||
role: 'pasteAndMatchStyle',
|
||||
label: localizeMessage('main.menus.app.edit.pasteAndMatchStyle', 'Paste and Match Style'),
|
||||
accelerator: 'CmdOrCtrl+SHIFT+V',
|
||||
}, {
|
||||
role: 'selectall',
|
||||
label: localizeMessage('main.menus.app.edit.selectAll', 'Select All'),
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
}],
|
||||
});
|
||||
|
||||
const viewSubMenu = [{
|
||||
label: 'Find..',
|
||||
label: localizeMessage('main.menus.app.view.find', 'Find..'),
|
||||
accelerator: 'CmdOrCtrl+F',
|
||||
click() {
|
||||
WindowManager.sendToFind();
|
||||
},
|
||||
}, {
|
||||
label: 'Reload',
|
||||
label: localizeMessage('main.menus.app.view.reload', 'Reload'),
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click() {
|
||||
WindowManager.reload();
|
||||
},
|
||||
}, {
|
||||
label: 'Clear Cache and Reload',
|
||||
label: localizeMessage('main.menus.app.view.clearCacheAndReload', 'Clear Cache and Reload'),
|
||||
accelerator: 'Shift+CmdOrCtrl+R',
|
||||
click() {
|
||||
session.defaultSession.clearCache();
|
||||
|
@ -123,13 +137,15 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
},
|
||||
}, {
|
||||
role: 'togglefullscreen',
|
||||
label: localizeMessage('main.menus.app.view.fullscreen', 'Toggle Full Screen'),
|
||||
accelerator: isMac ? 'Ctrl+Cmd+F' : 'F11',
|
||||
}, separatorItem, {
|
||||
label: 'Actual Size',
|
||||
label: localizeMessage('main.menus.app.view.actualSize', 'Actual Size'),
|
||||
role: 'resetZoom',
|
||||
accelerator: 'CmdOrCtrl+0',
|
||||
}, {
|
||||
role: 'zoomIn',
|
||||
label: localizeMessage('main.menus.app.view.zoomIn', 'Zoom In'),
|
||||
accelerator: 'CmdOrCtrl+=',
|
||||
}, {
|
||||
role: 'zoomIn',
|
||||
|
@ -137,13 +153,14 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
accelerator: 'CmdOrCtrl+Shift+=',
|
||||
}, {
|
||||
role: 'zoomOut',
|
||||
label: localizeMessage('main.menus.app.view.zoomOut', 'Zoom Out'),
|
||||
accelerator: 'CmdOrCtrl+-',
|
||||
}, {
|
||||
role: 'zoomOut',
|
||||
visible: false,
|
||||
accelerator: 'CmdOrCtrl+Shift+-',
|
||||
}, separatorItem, {
|
||||
label: 'Developer Tools for Application Wrapper',
|
||||
label: localizeMessage('main.menus.app.view.devToolsAppWrapper', 'Developer Tools for Application Wrapper'),
|
||||
accelerator: (() => {
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Alt+Command+I';
|
||||
|
@ -161,7 +178,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
}
|
||||
},
|
||||
}, {
|
||||
label: 'Developer Tools for Current Server',
|
||||
label: localizeMessage('main.menus.app.view.devToolsCurrentServer', 'Developer Tools for Current Server'),
|
||||
click() {
|
||||
WindowManager.openBrowserViewDevTools();
|
||||
},
|
||||
|
@ -170,7 +187,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
if (process.platform !== 'darwin' && process.platform !== 'win32') {
|
||||
viewSubMenu.push(separatorItem);
|
||||
viewSubMenu.push({
|
||||
label: 'Toggle Dark Mode',
|
||||
label: localizeMessage('main.menus.app.view.toggleDarkMode', 'Toggle Dark Mode'),
|
||||
click() {
|
||||
config.toggleDarkModeManually();
|
||||
},
|
||||
|
@ -178,13 +195,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
}
|
||||
|
||||
template.push({
|
||||
label: '&View',
|
||||
label: localizeMessage('main.menus.app.view', '&View'),
|
||||
submenu: viewSubMenu,
|
||||
});
|
||||
template.push({
|
||||
label: '&History',
|
||||
label: localizeMessage('main.menus.app.history', '&History'),
|
||||
submenu: [{
|
||||
label: 'Back',
|
||||
label: localizeMessage('main.menus.app.history.back', 'Back'),
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Alt+Left',
|
||||
click: () => {
|
||||
const view = WindowManager.viewManager?.getCurrentView();
|
||||
|
@ -194,7 +211,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
}
|
||||
},
|
||||
}, {
|
||||
label: 'Forward',
|
||||
label: localizeMessage('main.menus.app.history.forward', 'Forward'),
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Alt+Right',
|
||||
click: () => {
|
||||
const view = WindowManager.viewManager?.getCurrentView();
|
||||
|
@ -208,21 +225,24 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
|
||||
const teams = config.data?.teams || [];
|
||||
const windowMenu = {
|
||||
label: '&Window',
|
||||
label: localizeMessage('main.menus.app.window', '&Window'),
|
||||
role: isMac ? 'windowMenu' : null,
|
||||
submenu: [{
|
||||
role: 'minimize',
|
||||
label: localizeMessage('main.menus.app.window.minimize', 'Minimize'),
|
||||
|
||||
// empty string removes shortcut on Windows; null will default by OS
|
||||
accelerator: process.platform === 'win32' ? '' : null,
|
||||
}, ...(isMac ? [{
|
||||
role: 'zoom',
|
||||
label: localizeMessage('main.menus.app.window.zoom', 'Zoom'),
|
||||
}, separatorItem,
|
||||
] : []), {
|
||||
role: 'close',
|
||||
label: isMac ? localizeMessage('main.menus.app.window.closeWindow', 'Close Window') : localizeMessage('main.menus.app.window.close', 'Close'),
|
||||
accelerator: 'CmdOrCtrl+W',
|
||||
}, separatorItem, {
|
||||
label: 'Show Servers',
|
||||
label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'),
|
||||
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`,
|
||||
click() {
|
||||
ipcMain.emit(OPEN_TEAMS_DROPDOWN);
|
||||
|
@ -239,7 +259,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
if (WindowManager.getCurrentTeamName() === team.name) {
|
||||
team.tabs.filter((tab) => tab.isOpen).sort((teamA, teamB) => teamA.order - teamB.order).slice(0, 9).forEach((tab, i) => {
|
||||
items.push({
|
||||
label: ` ${getTabDisplayName(tab.name as TabType)}`,
|
||||
label: ` ${localizeMessage(`common.tabs.${tab.name}`, getTabDisplayName(tab.name as TabType))}`,
|
||||
accelerator: `CmdOrCtrl+${i + 1}`,
|
||||
click() {
|
||||
WindowManager.switchTab(team.name, tab.name);
|
||||
|
@ -249,14 +269,14 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
}
|
||||
return items;
|
||||
}).flat(), separatorItem, {
|
||||
label: 'Select Next Tab',
|
||||
label: localizeMessage('main.menus.app.window.selectNextTab', 'Select Next Tab'),
|
||||
accelerator: 'Ctrl+Tab',
|
||||
click() {
|
||||
WindowManager.selectNextTab();
|
||||
},
|
||||
enabled: (teams.length > 1),
|
||||
}, {
|
||||
label: 'Select Previous Tab',
|
||||
label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'),
|
||||
accelerator: 'Ctrl+Shift+Tab',
|
||||
click() {
|
||||
WindowManager.selectPreviousTab();
|
||||
|
@ -264,6 +284,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
enabled: (teams.length > 1),
|
||||
}, ...(isMac ? [separatorItem, {
|
||||
role: 'front',
|
||||
label: localizeMessage('main.menus.app.window.bringAllToFront', 'Bring All to Front'),
|
||||
}] : []),
|
||||
],
|
||||
};
|
||||
|
@ -272,21 +293,21 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
if (updateManager && config.canUpgrade) {
|
||||
if (updateManager.versionDownloaded) {
|
||||
submenu.push({
|
||||
label: 'Restart and Update',
|
||||
label: localizeMessage('main.menus.app.help.restartAndUpdate', 'Restart and Update'),
|
||||
click() {
|
||||
updateManager.handleUpdate();
|
||||
},
|
||||
});
|
||||
} else if (updateManager.versionAvailable) {
|
||||
submenu.push({
|
||||
label: 'Download Update',
|
||||
label: localizeMessage('main.menus.app.help.downloadUpdate', 'Download Update'),
|
||||
click() {
|
||||
updateManager.handleDownload();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
submenu.push({
|
||||
label: 'Check for Updates',
|
||||
label: localizeMessage('main.menus.app.help.checkForUpdates', 'Check for Updates'),
|
||||
click() {
|
||||
updateManager.checkForUpdates(true);
|
||||
},
|
||||
|
@ -295,7 +316,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
}
|
||||
if (config.data?.helpLink) {
|
||||
submenu.push({
|
||||
label: 'Learn More...',
|
||||
label: localizeMessage('main.menus.app.help.learnMore', 'Learn More...'),
|
||||
click() {
|
||||
shell.openExternal(config.data!.helpLink);
|
||||
},
|
||||
|
@ -303,10 +324,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
submenu.push(separatorItem);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const version = `Version ${app.getVersion()}${__HASH_VERSION__ ? ` commit: ${__HASH_VERSION__}` : ''}`;
|
||||
const version = localizeMessage('main.menus.app.help.versionString', 'Version {version}{commit}', {
|
||||
version: app.getVersion(),
|
||||
// eslint-disable-next-line no-undef
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
commit: __HASH_VERSION__ ? localizeMessage('main.menus.app.help.commitString', ' commit: {hashVersion}', {hashVersion: __HASH_VERSION__}) : '',
|
||||
});
|
||||
submenu.push({
|
||||
label: version,
|
||||
enabled: true,
|
||||
|
@ -315,7 +339,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||
},
|
||||
});
|
||||
|
||||
template.push({label: 'Hel&p', submenu});
|
||||
template.push({label: localizeMessage('main.menus.app.help', 'Hel&p'), submenu});
|
||||
return template;
|
||||
}
|
||||
|
||||
|
@ -323,3 +347,7 @@ export function createMenu(config: Config, updateManager: UpdateManager) {
|
|||
// TODO: Electron is enforcing certain variables that it doesn't need
|
||||
return Menu.buildFromTemplate(createTemplate(config, updateManager) as Array<MenuItemConstructorOptions | MenuItem>);
|
||||
}
|
||||
|
||||
t('common.tabs.TAB_MESSAGING');
|
||||
t('common.tabs.TAB_FOCALBOARD');
|
||||
t('common.tabs.TAB_PLAYBOOKS');
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
import {createTemplate} from './tray';
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/windows/windowManager', () => ({}));
|
||||
|
||||
describe('main/menus/tray', () => {
|
||||
|
|
|
@ -7,6 +7,7 @@ import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
|
|||
import {CombinedConfig} from 'types/config';
|
||||
|
||||
import WindowManager from 'main/windows/windowManager';
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
export function createTemplate(config: CombinedConfig) {
|
||||
const teams = config.teams;
|
||||
|
@ -21,7 +22,7 @@ export function createTemplate(config: CombinedConfig) {
|
|||
}), {
|
||||
type: 'separator',
|
||||
}, {
|
||||
label: process.platform === 'darwin' ? 'Preferences...' : 'Settings',
|
||||
label: process.platform === 'darwin' ? localizeMessage('main.menus.tray.preferences', 'Preferences...') : localizeMessage('main.menus.tray.settings', 'Settings'),
|
||||
click: () => {
|
||||
WindowManager.showSettingsWindow();
|
||||
},
|
||||
|
|
|
@ -8,11 +8,13 @@ import {app, Notification} from 'electron';
|
|||
|
||||
import Utils from 'common/utils/util';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
|
||||
|
||||
const defaultOptions = {
|
||||
title: 'Download Complete',
|
||||
title: localizeMessage('main.notifications.download.complete.title', 'Download Complete'),
|
||||
silent: false,
|
||||
icon: appIconURL,
|
||||
urgency: 'normal' as Notification['urgency'],
|
||||
|
@ -27,8 +29,8 @@ export class DownloadNotification extends Notification {
|
|||
Reflect.deleteProperty(options, 'icon');
|
||||
}
|
||||
|
||||
options.title = process.platform === 'win32' ? serverName : 'Download Complete';
|
||||
options.body = process.platform === 'win32' ? `Download Complete \n ${fileName}` : fileName;
|
||||
options.title = process.platform === 'win32' ? serverName : localizeMessage('main.notifications.download.complete.title', 'Download Complete');
|
||||
options.body = process.platform === 'win32' ? localizeMessage('main.notifications.download.complete.body', 'Download Complete \n {fileName}', {fileName}) : fileName;
|
||||
|
||||
super(options);
|
||||
}
|
||||
|
|
|
@ -10,11 +10,13 @@ import {MentionOptions} from 'types/notification';
|
|||
|
||||
import Utils from 'common/utils/util';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
|
||||
|
||||
const defaultOptions = {
|
||||
title: 'Someone mentioned you',
|
||||
title: localizeMessage('main.notifications.mention.title', 'Someone mentioned you'),
|
||||
silent: false,
|
||||
icon: appIconURL,
|
||||
urgency: 'normal' as Notification['urgency'],
|
||||
|
|
|
@ -5,12 +5,14 @@ import path from 'path';
|
|||
|
||||
import {app, Notification} from 'electron';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
|
||||
|
||||
const defaultOptions = {
|
||||
title: 'New desktop version available',
|
||||
body: 'A new version is available for you to download now.',
|
||||
title: localizeMessage('main.notifications.upgrade.newVersion.title', 'New desktop version available'),
|
||||
body: localizeMessage('main.notifications.upgrade.newVersion.body', 'A new version is available for you to download now.'),
|
||||
silent: false,
|
||||
icon: appIconURL,
|
||||
urgency: 'normal' as Notification['urgency'],
|
||||
|
@ -33,8 +35,8 @@ export class NewVersionNotification extends Notification {
|
|||
export class UpgradeNotification extends Notification {
|
||||
constructor() {
|
||||
const options = {...defaultOptions};
|
||||
options.title = 'Click to restart and install update';
|
||||
options.body = 'A new desktop version is ready to install now.';
|
||||
options.title = localizeMessage('main.notifications.upgrade.readyToInstall.title', 'Click to restart and install update');
|
||||
options.body = localizeMessage('main.notifications.upgrade.readyToInstall.body', 'A new desktop version is ready to install now.');
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = appIconURL;
|
||||
} else if (process.platform === 'darwin') {
|
||||
|
|
|
@ -8,6 +8,8 @@ import {Notification, shell} from 'electron';
|
|||
import {PLAY_SOUND} from 'common/communication';
|
||||
import {TAB_MESSAGING} from 'common/tabs/TabView';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
import WindowManager from '../windows/windowManager';
|
||||
|
||||
import {displayMention, displayDownloadCompleted, currentNotifications} from './index';
|
||||
|
@ -58,6 +60,10 @@ jest.mock('../windows/windowManager', () => ({
|
|||
switchTab: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('main/notifications', () => {
|
||||
describe('displayMention', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -152,6 +158,7 @@ describe('main/notifications', () => {
|
|||
|
||||
describe('displayDownloadCompleted', () => {
|
||||
it('should open file when clicked', () => {
|
||||
localizeMessage.mockReturnValue('test_filename');
|
||||
displayDownloadCompleted(
|
||||
'test_filename',
|
||||
'/path/to/file',
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
SHOW_EDIT_SERVER_MODAL,
|
||||
SHOW_REMOVE_SERVER_MODAL,
|
||||
UPDATE_TEAMS,
|
||||
GET_LANGUAGE_INFORMATION,
|
||||
RETRIEVED_LANGUAGE_INFORMATION,
|
||||
} from 'common/communication';
|
||||
|
||||
console.log('preloaded for the dropdown!');
|
||||
|
@ -50,6 +52,9 @@ window.addEventListener('message', async (event) => {
|
|||
case UPDATE_TEAMS:
|
||||
ipcRenderer.invoke(UPDATE_TEAMS, event.data.data);
|
||||
break;
|
||||
case GET_LANGUAGE_INFORMATION:
|
||||
window.postMessage({type: RETRIEVED_LANGUAGE_INFORMATION, data: await ipcRenderer.invoke(GET_LANGUAGE_INFORMATION)});
|
||||
break;
|
||||
default:
|
||||
console.log(`got a message: ${event}`);
|
||||
console.log(event);
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
|
||||
import {ipcRenderer, contextBridge} from 'electron';
|
||||
|
||||
import {
|
||||
GET_LANGUAGE_INFORMATION,
|
||||
RETRIEVED_LANGUAGE_INFORMATION,
|
||||
} from 'common/communication';
|
||||
|
||||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||
send: ipcRenderer.send,
|
||||
on: (channel, listener) => ipcRenderer.on(channel, (_, ...args) => listener(null, ...args)),
|
||||
|
@ -24,3 +29,11 @@ contextBridge.exposeInMainWorld('timers', {
|
|||
setImmediate,
|
||||
});
|
||||
|
||||
window.addEventListener('message', async (event) => {
|
||||
switch (event.data.type) {
|
||||
case GET_LANGUAGE_INFORMATION:
|
||||
window.postMessage({type: RETRIEVED_LANGUAGE_INFORMATION, data: await ipcRenderer.invoke(GET_LANGUAGE_INFORMATION)});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
MODAL_UNCLOSEABLE,
|
||||
PING_DOMAIN,
|
||||
PING_DOMAIN_RESPONSE,
|
||||
GET_LANGUAGE_INFORMATION,
|
||||
RETRIEVED_LANGUAGE_INFORMATION,
|
||||
} from 'common/communication';
|
||||
|
||||
console.log('preloaded for the modal!');
|
||||
|
@ -70,6 +72,9 @@ window.addEventListener('message', async (event) => {
|
|||
window.postMessage({type: PING_DOMAIN_RESPONSE, data: error}, window.location.href);
|
||||
}
|
||||
break;
|
||||
case GET_LANGUAGE_INFORMATION:
|
||||
window.postMessage({type: RETRIEVED_LANGUAGE_INFORMATION, data: await ipcRenderer.invoke(GET_LANGUAGE_INFORMATION)});
|
||||
break;
|
||||
default:
|
||||
console.log(`got a message: ${event}`);
|
||||
console.log(event);
|
||||
|
|
|
@ -7,6 +7,8 @@ import {app, nativeImage, Tray, systemPreferences, nativeTheme} from 'electron';
|
|||
|
||||
import {UPDATE_TRAY} from 'common/communication';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
import WindowManager from '../windows/windowManager';
|
||||
import * as AppState from '../appState';
|
||||
|
||||
|
@ -95,11 +97,11 @@ export function setupTray(icontheme: string) {
|
|||
|
||||
AppState.on(UPDATE_TRAY, (anyExpired, anyMentions, anyUnreads) => {
|
||||
if (anyMentions) {
|
||||
setTray('mention', 'You have been mentioned');
|
||||
setTray('mention', localizeMessage('main.tray.tray.mention', 'You have been mentioned'));
|
||||
} else if (anyUnreads) {
|
||||
setTray('unread', 'You have unread channels');
|
||||
setTray('unread', localizeMessage('main.tray.tray.unread', 'You have unread channels'));
|
||||
} else if (anyExpired) {
|
||||
setTray('mention', 'Session Expired: Please sign in to continue receiving notifications.');
|
||||
setTray('mention', localizeMessage('main.tray.tray.expired', 'Session Expired: Please sign in to continue receiving notifications.'));
|
||||
} else {
|
||||
setTray('normal', app.name);
|
||||
}
|
||||
|
|
|
@ -48,6 +48,10 @@ jest.mock('common/utils/url', () => ({
|
|||
getView: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/server/serverInfo', () => ({
|
||||
ServerInfo: jest.fn(),
|
||||
}));
|
||||
|
|
|
@ -28,6 +28,7 @@ import Utils from 'common/utils/util';
|
|||
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||
import {getServerView, getTabViewName, TabTuple, TabType} from 'common/tabs/TabView';
|
||||
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
import {ServerInfo} from 'main/server/serverInfo';
|
||||
|
||||
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
|
||||
|
@ -531,7 +532,10 @@ export class ViewManager {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
dialog.showErrorBox('No matching server', `there is no configured server in the app that matches the requested url: ${parsedURL.toString()}`);
|
||||
dialog.showErrorBox(
|
||||
localizeMessage('main.views.viewManager.handleDeepLink.error.title', 'No matching server'),
|
||||
localizeMessage('main.views.viewManager.handleDeepLink.error.body', 'There is no configured server in the app that matches the requested url: {url}', {url: parsedURL.toString()}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -69,6 +69,10 @@ jest.mock('../utils', () => ({
|
|||
getLocalURLString: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('main/windows/mainWindow', () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT, MINI
|
|||
import Utils from 'common/utils/util';
|
||||
|
||||
import {boundsInfoPath} from 'main/constants';
|
||||
import {localizeMessage} from 'main/i18nManager';
|
||||
|
||||
import * as Validator from '../Validator';
|
||||
import ContextMenu from '../contextMenu';
|
||||
|
@ -156,11 +157,11 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
|
|||
hideWindow(mainWindow);
|
||||
} else {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
title: 'Minimize to Tray',
|
||||
message: 'Mattermost will continue to run in the system tray. This can be disabled in Settings.',
|
||||
title: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.title', 'Minimize to Tray'),
|
||||
message: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.message', '{appName} will continue to run in the system tray. This can be disabled in Settings.', {appName: app.name}),
|
||||
type: 'info',
|
||||
checkboxChecked: true,
|
||||
checkboxLabel: 'Don\'t show again',
|
||||
checkboxLabel: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel', 'Don\'t show again'),
|
||||
}).then((result: {response: number; checkboxChecked: boolean}) => {
|
||||
Config.set('alwaysMinimize', result.checkboxChecked);
|
||||
hideWindow(mainWindow);
|
||||
|
@ -170,13 +171,16 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
|
|||
app.quit();
|
||||
} else {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
title: 'Close Application',
|
||||
message: 'Are you sure you want to quit?',
|
||||
detail: 'You will no longer receive notifications for messages. If you want to leave Mattermost running in the system tray, you can enable this in Settings.',
|
||||
title: localizeMessage('main.windows.mainWindow.closeApp.dialog.title', 'Close Application'),
|
||||
message: localizeMessage('main.windows.mainWindow.closeApp.dialog.message', 'Are you sure you want to quit?'),
|
||||
detail: localizeMessage('main.windows.mainWindow.closeApp.dialog.detail', 'You will no longer receive notifications for messages. If you want to leave {appName} running in the system tray, you can enable this in Settings.', {appName: app.name}),
|
||||
type: 'question',
|
||||
buttons: ['Yes', 'No'],
|
||||
buttons: [
|
||||
localizeMessage('label.yes', 'Yes'),
|
||||
localizeMessage('label.no', 'No'),
|
||||
],
|
||||
checkboxChecked: true,
|
||||
checkboxLabel: 'Don\'t ask again',
|
||||
checkboxLabel: localizeMessage('main.windows.mainWindow.closeApp.dialog.checkboxLabel', 'Don\'t ask again'),
|
||||
}).then((result: {response: number; checkboxChecked: boolean}) => {
|
||||
Config.set('alwaysClose', result.checkboxChecked && result.response === 0);
|
||||
if (result.response === 0) {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import {Alert} from 'react-bootstrap';
|
||||
import {IntlShape, useIntl} from 'react-intl';
|
||||
|
||||
const baseClassName = 'AutoSaveIndicator';
|
||||
const leaveClassName = `${baseClassName}-Leave`;
|
||||
|
@ -15,16 +16,16 @@ export enum SavingState {
|
|||
SAVING_STATE_DONE = 'done',
|
||||
}
|
||||
|
||||
function getClassNameAndMessage(savingState: SavingState, errorMessage?: string) {
|
||||
function getClassNameAndMessage(intl: IntlShape, savingState: SavingState, errorMessage?: React.ReactNode) {
|
||||
switch (savingState) {
|
||||
case SavingState.SAVING_STATE_SAVING:
|
||||
return {className: baseClassName, message: 'Saving...'};
|
||||
return {className: baseClassName, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saving', defaultMessage: 'Saving...'})};
|
||||
case SavingState.SAVING_STATE_SAVED:
|
||||
return {className: baseClassName, message: 'Saved'};
|
||||
return {className: baseClassName, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saved', defaultMessage: 'Saved'})};
|
||||
case SavingState.SAVING_STATE_ERROR:
|
||||
return {className: `${baseClassName}`, message: errorMessage};
|
||||
case SavingState.SAVING_STATE_DONE:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: 'Saved'};
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saved', defaultMessage: 'Saved'})};
|
||||
default:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
|
||||
}
|
||||
|
@ -33,12 +34,13 @@ function getClassNameAndMessage(savingState: SavingState, errorMessage?: string)
|
|||
type Props = {
|
||||
id?: string;
|
||||
savingState: SavingState;
|
||||
errorMessage?: string;
|
||||
errorMessage?: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function AutoSaveIndicator(props: Props) {
|
||||
const AutoSaveIndicator: React.FC<Props> = (props: Props) => {
|
||||
const intl = useIntl();
|
||||
const {savingState, errorMessage, ...rest} = props;
|
||||
const {className, message} = getClassNameAndMessage(savingState, errorMessage);
|
||||
const {className, message} = getClassNameAndMessage(intl, savingState, errorMessage);
|
||||
return (
|
||||
<Alert
|
||||
className={className}
|
||||
|
@ -48,4 +50,6 @@ export default function AutoSaveIndicator(props: Props) {
|
|||
{message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AutoSaveIndicator;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import {Container, Row, Col} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
type Props = {
|
||||
errorInfo?: string;
|
||||
|
@ -41,31 +42,74 @@ export default function ErrorView(props: Props) {
|
|||
md={10}
|
||||
lg={8}
|
||||
>
|
||||
<h2>{`Cannot connect to ${props.appName}`}</h2>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='renderer.components.errorView.cannotConnectToAppName'
|
||||
defaultMessage='Cannot connect to {appName}'
|
||||
values={{
|
||||
appName: props.appName,
|
||||
}}
|
||||
/>
|
||||
</h2>
|
||||
<hr/>
|
||||
<p>
|
||||
{`We're having trouble connecting to ${props.appName}. We'll continue to try and establish a connection.`}
|
||||
<FormattedMessage
|
||||
id='renderer.components.errorView.havingTroubleConnecting'
|
||||
defaultMessage={'We\'re having trouble connecting to {appName}. We\'ll continue to try and establish a connection.'}
|
||||
values={{
|
||||
appName: props.appName,
|
||||
}}
|
||||
/>
|
||||
<br/>
|
||||
{'If refreshing this page (Ctrl+R or Command+R) does not work please verify that:'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.errorView.refreshThenVerify'
|
||||
defaultMessage='If refreshing this page (Ctrl+R or Command+R) does not work please verify that:'
|
||||
/>
|
||||
</p>
|
||||
<ul className='ErrorView-bullets' >
|
||||
<li>{'Your computer is connected to the internet.'}</li>
|
||||
<li>{`The ${props.appName} URL `}
|
||||
<a
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='renderer.components.errorView.troubleshooting.computerIsConnected'
|
||||
defaultMessage='Your computer is connected to the internet.'
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect'
|
||||
defaultMessage='The {appName} URL <link>{url}</link> is correct'
|
||||
values={{
|
||||
appName: props.appName,
|
||||
url: props.url,
|
||||
link: (msg: React.ReactNode) => (
|
||||
<a
|
||||
|
||||
onClick={props.handleLink}
|
||||
href='#'
|
||||
>
|
||||
{props.url}
|
||||
</a>{' is correct.'}</li>
|
||||
<li>{'You can reach '}
|
||||
<a
|
||||
onClick={props.handleLink}
|
||||
href='#'
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id='renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow'
|
||||
defaultMessage='You can reach <link>{url}</link> from a browser window.'
|
||||
values={{
|
||||
url: props.url,
|
||||
link: (msg: React.ReactNode) => (
|
||||
<a
|
||||
|
||||
onClick={props.handleLink}
|
||||
href='#'
|
||||
>
|
||||
{props.url}
|
||||
</a>{' from a browser window.'}</li>
|
||||
onClick={props.handleLink}
|
||||
href='#'
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<div className='ErrorView-techInfo'>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import {Row, Button} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
type Props = {
|
||||
darkMode?: boolean;
|
||||
|
@ -39,7 +40,10 @@ export default class ExtraBar extends React.PureComponent<Props> {
|
|||
>
|
||||
<span className={'backIcon icon-arrow-left'}/>
|
||||
<span className={'backLabel'}>
|
||||
{'Back'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.extraBar.back'
|
||||
defaultMessage='Back'
|
||||
/>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ import classNames from 'classnames';
|
|||
import React, {Fragment} from 'react';
|
||||
import {Container, Row} from 'react-bootstrap';
|
||||
import {DropResult} from 'react-beautiful-dnd';
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
import {IpcRendererEvent} from 'electron/renderer';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
|
@ -82,6 +83,7 @@ type Props = {
|
|||
darkMode: boolean;
|
||||
appName: string;
|
||||
useNativeWindow: boolean;
|
||||
intl: IntlShape;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -115,7 +117,7 @@ type TabViewStatus = {
|
|||
};
|
||||
}
|
||||
|
||||
export default class MainPage extends React.PureComponent<Props, State> {
|
||||
class MainPage extends React.PureComponent<Props, State> {
|
||||
topBar: React.RefObject<HTMLDivElement>;
|
||||
threeDotMenu: React.RefObject<HTMLButtonElement>;
|
||||
|
||||
|
@ -363,6 +365,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {intl} = this.props;
|
||||
const currentTabs = this.props.teams.find((team) => team.name === this.state.activeServerName)?.tabs || [];
|
||||
|
||||
const tabsRow = (
|
||||
|
@ -419,13 +422,20 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
|||
let upgradeTooltip;
|
||||
switch (this.state.upgradeStatus) {
|
||||
case UpgradeStatus.AVAILABLE:
|
||||
upgradeTooltip = 'Update available';
|
||||
upgradeTooltip = intl.formatMessage({id: 'renderer.components.mainPage.updateAvailable', defaultMessage: 'Update available'});
|
||||
break;
|
||||
case UpgradeStatus.DOWNLOADED:
|
||||
upgradeTooltip = 'Update ready to install';
|
||||
upgradeTooltip = intl.formatMessage({id: 'renderer.components.mainPage.updateReady', defaultMessage: 'Update ready to install'});
|
||||
break;
|
||||
case UpgradeStatus.DOWNLOADING:
|
||||
upgradeTooltip = `Downloading update. ${String(this.state.upgradeProgress?.percent).split('.')[0]}% of ${prettyBytes(this.state.upgradeProgress?.total || 0)} @ ${prettyBytes(this.state.upgradeProgress?.bytesPerSecond || 0)}/s`;
|
||||
upgradeTooltip = intl.formatMessage({
|
||||
id: 'renderer.components.mainPage.downloadingUpdate',
|
||||
defaultMessage: 'Downloading update. {percentDone}% of {total} @ {speed}/s',
|
||||
}, {
|
||||
percentDone: String(this.state.upgradeProgress?.percent).split('.')[0],
|
||||
total: prettyBytes(this.state.upgradeProgress?.total || 0),
|
||||
speed: prettyBytes(this.state.upgradeProgress?.bytesPerSecond || 0),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -516,7 +526,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
|||
onClick={this.openMenu}
|
||||
tabIndex={0}
|
||||
ref={this.threeDotMenu}
|
||||
aria-label='Context menu'
|
||||
aria-label={intl.formatMessage({id: 'renderer.components.mainPage.contextMenu.ariaLabel', defaultMessage: 'Context menu'})}
|
||||
>
|
||||
<i className='icon-dots-vertical'/>
|
||||
</button>
|
||||
|
@ -593,3 +603,5 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(MainPage);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import {Modal, Button, FormGroup, FormControl, FormLabel, FormText} from 'react-bootstrap';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
|
||||
import {TeamWithIndex} from 'types/config';
|
||||
|
||||
|
@ -20,6 +21,7 @@ type Props = {
|
|||
restoreFocus?: boolean;
|
||||
currentOrder?: number;
|
||||
setInputRef?: (inputRef: HTMLInputElement) => void;
|
||||
intl: IntlShape;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -30,7 +32,7 @@ type State = {
|
|||
saveStarted: boolean;
|
||||
}
|
||||
|
||||
export default class NewTeamModal extends React.PureComponent<Props, State> {
|
||||
class NewTeamModal extends React.PureComponent<Props, State> {
|
||||
wasShown?: boolean;
|
||||
teamNameInputRef?: HTMLInputElement;
|
||||
|
||||
|
@ -70,10 +72,20 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
|
|||
currentTeams.splice(this.props.team.index, 1);
|
||||
}
|
||||
if (currentTeams.find((team) => team.name === this.state.teamName)) {
|
||||
return 'A server with the same name already exists.';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.error.serverNameExists'
|
||||
defaultMessage='A server with the same name already exists.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.state.teamName.length > 0 ? null : 'Name is required.';
|
||||
return this.state.teamName.length > 0 ? null : (
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.error.nameRequired'
|
||||
defaultMessage='Name is required.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getTeamNameValidationState() {
|
||||
|
@ -96,17 +108,37 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
|
|||
currentTeams.splice(this.props.team.index, 1);
|
||||
}
|
||||
if (currentTeams.find((team) => team.url === this.state.teamUrl)) {
|
||||
return 'A server with the same URL already exists.';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.error.serverUrlExists'
|
||||
defaultMessage='A server with the same URL already exists.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (this.state.teamUrl.length === 0) {
|
||||
return 'URL is required.';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.error.urlRequired'
|
||||
defaultMessage='URL is required.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) {
|
||||
return 'URL should start with http:// or https://.';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.error.urlNeedsHttp'
|
||||
defaultMessage='URL should start with http:// or https://.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!urlUtils.isValidURL(this.state.teamUrl.trim())) {
|
||||
return 'URL is not formatted correctly.';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.error.urlIncorrectFormatting'
|
||||
defaultMessage='URL is not formatted correctly.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -185,16 +217,36 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
|
|||
|
||||
getSaveButtonLabel() {
|
||||
if (this.props.editMode) {
|
||||
return 'Save';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='label.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return 'Add';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='label.add'
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getModalTitle() {
|
||||
if (this.props.editMode) {
|
||||
return 'Edit Server';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.title.edit'
|
||||
defaultMessage='Edit Server'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return 'Add Server';
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.title.add'
|
||||
defaultMessage='Add Server'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -235,12 +287,17 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
|
|||
<Modal.Body>
|
||||
<form>
|
||||
<FormGroup>
|
||||
<FormLabel>{'Server Display Name'}</FormLabel>
|
||||
<FormLabel>
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.serverDisplayName'
|
||||
defaultMessage='Server Display Name'
|
||||
/>
|
||||
</FormLabel>
|
||||
<FormControl
|
||||
id='teamNameInput'
|
||||
type='text'
|
||||
value={this.state.teamName}
|
||||
placeholder='Server Name'
|
||||
placeholder={this.props.intl.formatMessage({id: 'renderer.components.newTeamModal.serverDisplayName', defaultMessage: 'Server Display Name'})}
|
||||
onChange={this.handleTeamNameChange}
|
||||
ref={(ref: HTMLInputElement) => {
|
||||
this.teamNameInputRef = ref;
|
||||
|
@ -255,12 +312,22 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
|
|||
isInvalid={Boolean(this.getTeamNameValidationState())}
|
||||
/>
|
||||
<FormControl.Feedback/>
|
||||
<FormText>{'The name of the server displayed on your desktop app tab bar.'}</FormText>
|
||||
<FormText>
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.serverDisplayName.description'
|
||||
defaultMessage='The name of the server displayed on your desktop app tab bar.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
className='NewTeamModal-noBottomSpace'
|
||||
>
|
||||
<FormLabel>{'Server URL'}</FormLabel>
|
||||
<FormLabel>
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.serverURL'
|
||||
defaultMessage='Server URL'
|
||||
/>
|
||||
</FormLabel>
|
||||
<FormControl
|
||||
id='teamUrlInput'
|
||||
type='text'
|
||||
|
@ -273,7 +340,12 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
|
|||
isInvalid={Boolean(this.getTeamUrlValidationState())}
|
||||
/>
|
||||
<FormControl.Feedback/>
|
||||
<FormText className='NewTeamModal-noBottomSpace'>{'The URL of your Mattermost server. Must start with http:// or https://.'}</FormText>
|
||||
<FormText className='NewTeamModal-noBottomSpace'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.newTeamModal.serverURL.description'
|
||||
defaultMessage='The URL of your Mattermost server. Must start with http:// or https://.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</Modal.Body>
|
||||
|
@ -291,7 +363,10 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
|
|||
onClick={this.props.onClose}
|
||||
variant='link'
|
||||
>
|
||||
{'Cancel'}
|
||||
<FormattedMessage
|
||||
id='label.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
{this.props.onSave &&
|
||||
|
@ -310,3 +385,5 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(NewTeamModal);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import DestructiveConfirmationModal from './DestructiveConfirmModal';
|
||||
|
||||
|
@ -13,27 +14,36 @@ type Props = {
|
|||
onHide: () => void;
|
||||
onAccept: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onCancel: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
};
|
||||
|
||||
export default function RemoveServerModal(props: Props) {
|
||||
function RemoveServerModal(props: Props) {
|
||||
const intl = useIntl();
|
||||
const {serverName, ...rest} = props;
|
||||
return (
|
||||
<DestructiveConfirmationModal
|
||||
{...rest}
|
||||
title='Remove Server'
|
||||
acceptLabel='Remove'
|
||||
cancelLabel='Cancel'
|
||||
title={intl.formatMessage({id: 'renderer.components.removeServerModal.title', defaultMessage: 'Remove Server'})}
|
||||
acceptLabel={intl.formatMessage({id: 'label.remove', defaultMessage: 'Remove'})}
|
||||
cancelLabel={intl.formatMessage({id: 'label.cancel', defaultMessage: 'Cancel'})}
|
||||
body={(
|
||||
<Modal.Body>
|
||||
<p>
|
||||
{'This will remove the server from your Desktop App but will not delete any of its data' +
|
||||
' - you can add the server back to the app at any time.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.removeServerModal.body'
|
||||
defaultMessage='This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.'
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
{'Confirm you wish to remove the '}<strong>{serverName}</strong>{' server?'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.removeServerModal.confirm'
|
||||
defaultMessage='Confirm you wish to remove the {serverName} server?'
|
||||
values={{serverName}}
|
||||
/>
|
||||
</p>
|
||||
</Modal.Body>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveServerModal;
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'renderer/css/settings.css';
|
|||
|
||||
import React from 'react';
|
||||
import {FormCheck, Col, FormGroup, FormText, Container, Row, Button, FormControl} from 'react-bootstrap';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
import ReactSelect, {ActionMeta, MultiValue} from 'react-select';
|
||||
|
||||
import {CombinedConfig, LocalConfiguration} from 'types/config';
|
||||
|
@ -23,6 +24,7 @@ import {
|
|||
RELOAD_CONFIGURATION,
|
||||
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
|
||||
CHECK_FOR_UPDATES,
|
||||
GET_AVAILABLE_LANGUAGES,
|
||||
} from 'common/communication';
|
||||
|
||||
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
|
||||
|
@ -32,6 +34,10 @@ const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
|
|||
|
||||
type ConfigType = typeof CONFIG_TYPE_UPDATES | typeof CONFIG_TYPE_APP_OPTIONS;
|
||||
|
||||
type Props = {
|
||||
intl: IntlShape;
|
||||
}
|
||||
|
||||
type State = DeepPartial<CombinedConfig> & {
|
||||
ready: boolean;
|
||||
maximized?: boolean;
|
||||
|
@ -39,13 +45,14 @@ type State = DeepPartial<CombinedConfig> & {
|
|||
userOpenedDownloadDialog: boolean;
|
||||
allowSaveSpellCheckerURL: boolean;
|
||||
availableLanguages: Array<{label: string; value: string}>;
|
||||
availableSpellcheckerLanguages: Array<{label: string; value: string}>;
|
||||
canUpgrade?: boolean;
|
||||
}
|
||||
|
||||
type SavingStateItems = {
|
||||
appOptions: SavingState;
|
||||
updates: SavingState;
|
||||
};
|
||||
}
|
||||
|
||||
type SaveQueueItem = {
|
||||
configType: ConfigType;
|
||||
|
@ -53,7 +60,7 @@ type SaveQueueItem = {
|
|||
data: CombinedConfig[keyof CombinedConfig];
|
||||
}
|
||||
|
||||
export default class SettingsPage extends React.PureComponent<Record<string, never>, State> {
|
||||
class SettingsPage extends React.PureComponent<Props, State> {
|
||||
trayIconThemeRef: React.RefObject<HTMLDivElement>;
|
||||
downloadLocationRef: React.RefObject<HTMLInputElement>;
|
||||
showTrayIconRef: React.RefObject<HTMLInputElement>;
|
||||
|
@ -69,6 +76,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
startInFullscreenRef: React.RefObject<HTMLInputElement>;
|
||||
autoCheckForUpdatesRef: React.RefObject<HTMLInputElement>;
|
||||
logLevelRef: React.RefObject<HTMLSelectElement>;
|
||||
appLanguageRef: React.RefObject<HTMLSelectElement>;
|
||||
|
||||
saveQueue: SaveQueueItem[];
|
||||
|
||||
|
@ -77,7 +85,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
savingIsDebounced: boolean;
|
||||
resetSaveStateIsDebounced: boolean;
|
||||
|
||||
constructor(props: Record<string, never>) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ready: false,
|
||||
|
@ -88,6 +96,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
userOpenedDownloadDialog: false,
|
||||
allowSaveSpellCheckerURL: false,
|
||||
availableLanguages: [],
|
||||
availableSpellcheckerLanguages: [],
|
||||
};
|
||||
|
||||
this.getConfig();
|
||||
|
@ -106,6 +115,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
this.spellCheckerURLRef = React.createRef();
|
||||
this.autoCheckForUpdatesRef = React.createRef();
|
||||
this.logLevelRef = React.createRef();
|
||||
this.appLanguageRef = React.createRef();
|
||||
|
||||
this.saveQueue = [];
|
||||
this.selectedSpellCheckerLocales = [];
|
||||
|
@ -121,6 +131,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
});
|
||||
|
||||
window.ipcRenderer.invoke(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES).then((languages: string[]) => {
|
||||
const availableSpellcheckerLanguages = languages.filter((language) => localeTranslations[language]).map((language) => ({label: localeTranslations[language], value: language}));
|
||||
availableSpellcheckerLanguages.sort((a, b) => a.label.localeCompare(b.label));
|
||||
this.setState({availableSpellcheckerLanguages});
|
||||
});
|
||||
|
||||
window.ipcRenderer.invoke(GET_AVAILABLE_LANGUAGES).then((languages: string[]) => {
|
||||
const availableLanguages = languages.filter((language) => localeTranslations[language]).map((language) => ({label: localeTranslations[language], value: language}));
|
||||
availableLanguages.sort((a, b) => a.label.localeCompare(b.label));
|
||||
this.setState({availableLanguages});
|
||||
|
@ -318,6 +334,13 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
});
|
||||
}
|
||||
|
||||
handleChangeAppLanguage = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'appLanguage', data: this.appLanguageRef.current?.value});
|
||||
this.setState({
|
||||
appLanguage: this.appLanguageRef.current?.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeAutoCheckForUpdates = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_UPDATES, {key: 'autoCheckForUpdates', data: this.autoCheckForUpdatesRef.current?.checked});
|
||||
this.setState({
|
||||
|
@ -415,6 +438,8 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
}
|
||||
|
||||
render() {
|
||||
const {intl} = this.props;
|
||||
|
||||
const settingsPage = {
|
||||
close: {
|
||||
textDecoration: 'none',
|
||||
|
@ -471,6 +496,17 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
fontWeight: 500,
|
||||
},
|
||||
|
||||
appLanguageInput: {
|
||||
marginRight: '3px',
|
||||
marginTop: '8px',
|
||||
width: '320px',
|
||||
height: '34px',
|
||||
padding: '0 12px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
fontWeight: 500,
|
||||
},
|
||||
|
||||
container: {
|
||||
paddingBottom: '40px',
|
||||
},
|
||||
|
@ -496,9 +532,15 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.autostart}
|
||||
onChange={this.handleChangeAutoStart}
|
||||
/>
|
||||
{'Start app on login'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.startAppOnLogin'
|
||||
defaultMessage='Start app on login'
|
||||
/>
|
||||
<FormText>
|
||||
{'If enabled, the app starts automatically when you log in to your machine.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.startAppOnLogin.description'
|
||||
defaultMessage='If enabled, the app starts automatically when you log in to your machine.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>);
|
||||
|
||||
|
@ -512,9 +554,15 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.hideOnStart}
|
||||
onChange={this.handleChangeHideOnStart}
|
||||
/>
|
||||
{'Launch app minimized'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.launchAppMinimized'
|
||||
defaultMessage='Launch app minimized'
|
||||
/>
|
||||
<FormText>
|
||||
{'If enabled, the app will start in system tray, and will not show the window on launch.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.launchAppMinimized.description'
|
||||
defaultMessage='If enabled, the app will start in system tray, and will not show the window on launch.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>);
|
||||
}
|
||||
|
@ -530,10 +578,20 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.useSpellChecker}
|
||||
onChange={this.handleChangeUseSpellChecker}
|
||||
/>
|
||||
{'Check spelling'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling'
|
||||
defaultMessage='Check spelling'
|
||||
/>
|
||||
<FormText>
|
||||
{'Highlight misspelled words in your messages based on your system language or language preference. '}
|
||||
{'Setting takes effect after restarting the app.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.description'
|
||||
defaultMessage='Highlight misspelled words in your messages based on your system language or language preference.'
|
||||
/>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>
|
||||
{this.state.useSpellChecker &&
|
||||
|
@ -541,12 +599,17 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
inputId='inputSpellCheckerLocalesDropdown'
|
||||
className='SettingsPage__spellCheckerLocalesDropdown'
|
||||
classNamePrefix='SettingsPage__spellCheckerLocalesDropdown'
|
||||
options={this.state.availableLanguages}
|
||||
options={this.state.availableSpellcheckerLanguages}
|
||||
isMulti={true}
|
||||
isClearable={false}
|
||||
onChange={this.handleChangeSpellCheckerLocales}
|
||||
value={this.selectedSpellCheckerLocales}
|
||||
placeholder={'Select preferred language(s)'}
|
||||
placeholder={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.preferredLanguages'
|
||||
defaultMessage='Select preferred language(s)'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</>,
|
||||
|
@ -559,7 +622,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
key='editSpellcheckerURL'
|
||||
onClick={() => this.setState({spellCheckerURL: '', allowSaveSpellCheckerURL: false})}
|
||||
variant='link'
|
||||
>{'Use an alternative dictionary URL'}</Button>,
|
||||
>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.editSpellcheckUrl'
|
||||
defaultMessage='Use an alternative dictionary URL'
|
||||
/>
|
||||
</Button>,
|
||||
);
|
||||
} else {
|
||||
options.push(
|
||||
|
@ -583,22 +651,33 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
id='saveSpellCheckerURL'
|
||||
onClick={this.saveSpellCheckerURL}
|
||||
>
|
||||
<span>{'Save'}</span>
|
||||
<FormattedMessage
|
||||
id='label.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</Button>
|
||||
<FormText>
|
||||
{'Specify the url where dictionary definitions can be retrieved'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.specifyURL'
|
||||
defaultMessage='Specify the url where dictionary definitions can be retrieved'
|
||||
/>
|
||||
</FormText>
|
||||
<Button
|
||||
id='revertSpellcheckerURL'
|
||||
key='revertSpellcheckerURL'
|
||||
onClick={this.resetSpellCheckerURL}
|
||||
variant='link'
|
||||
>{'Revert to default'}</Button>
|
||||
>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.revertToDefault'
|
||||
defaultMessage='Revert to default'
|
||||
/>
|
||||
</Button>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
if (window.process.platform === 'darwin' || window.process.platform === 'win32') {
|
||||
const TASKBAR = window.process.platform === 'win32' ? 'taskbar' : 'Dock';
|
||||
const taskbar = window.process.platform === 'win32' ? 'taskbar' : 'Dock';
|
||||
options.push(
|
||||
<FormCheck
|
||||
key='showunreadbadge'
|
||||
|
@ -611,9 +690,17 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.showUnreadBadge}
|
||||
onChange={this.handleShowUnreadBadge}
|
||||
/>
|
||||
{`Show red badge on ${TASKBAR} icon to indicate unread messages`}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.showUnreadBadge'
|
||||
defaultMessage='Show red badge on {taskbar} icon to indicate unread messages'
|
||||
values={{taskbar}}
|
||||
/>
|
||||
<FormText>
|
||||
{`Regardless of this setting, mentions are always indicated with a red badge and item count on the ${TASKBAR} icon.`}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.showUnreadBadge.description'
|
||||
defaultMessage='Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.'
|
||||
values={{taskbar}}
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>);
|
||||
}
|
||||
|
@ -629,13 +716,30 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={!this.state.notifications || this.state.notifications.flashWindow === 2}
|
||||
onChange={this.handleFlashWindow}
|
||||
/>
|
||||
{'Flash taskbar icon when a new message is received'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow'
|
||||
defaultMessage='Flash taskbar icon when a new message is received'
|
||||
/>
|
||||
<FormText>
|
||||
{'If enabled, the taskbar icon will flash for a few seconds when a new message is received.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description'
|
||||
defaultMessage='If enabled, the taskbar icon will flash for a few seconds when a new message is received.'
|
||||
/>
|
||||
{window.process.platform === 'linux' && (
|
||||
<>
|
||||
<br/>
|
||||
<em><strong>{'NOTE: '}</strong>{'This functionality may not work with all Linux window managers.'}</em>
|
||||
<em>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description.note'
|
||||
defaultMessage='NOTE: '
|
||||
/>
|
||||
</strong>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description.linuxFunctionality'
|
||||
defaultMessage='This functionality may not work with all Linux window managers.'
|
||||
/>
|
||||
</em>
|
||||
</>
|
||||
)}
|
||||
</FormText>
|
||||
|
@ -656,7 +760,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.notifications ? this.state.notifications.bounceIcon : false}
|
||||
onChange={this.handleBounceIcon}
|
||||
style={{marginRight: '10px'}}
|
||||
label='Bounce the Dock icon'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon'
|
||||
defaultMessage='Bounce the Dock icon'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormCheck
|
||||
type='radio'
|
||||
|
@ -670,7 +779,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
this.state.notifications.bounceIconType === 'informational'
|
||||
}
|
||||
onChange={this.handleBounceIconType}
|
||||
label='once'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.once'
|
||||
defaultMessage='once'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{' '}
|
||||
<FormCheck
|
||||
|
@ -681,12 +795,20 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
disabled={!this.state.notifications || !this.state.notifications.bounceIcon}
|
||||
defaultChecked={this.state.notifications && this.state.notifications.bounceIconType === 'critical'}
|
||||
onChange={this.handleBounceIconType}
|
||||
label={'until I open the app'}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.untilOpenApp'
|
||||
defaultMessage='until I open the app'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormText
|
||||
style={{marginLeft: '20px'}}
|
||||
>
|
||||
{'If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.description'
|
||||
defaultMessage='If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormGroup>,
|
||||
);
|
||||
|
@ -704,9 +826,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.showTrayIcon}
|
||||
onChange={this.handleChangeShowTrayIcon}
|
||||
/>
|
||||
{window.process.platform === 'darwin' ? `Show ${this.state.appName} icon in the menu bar` : 'Show icon in the notification area'}
|
||||
{window.process.platform === 'darwin' ?
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.show.darwin'
|
||||
defaultMessage='Show {appName} icon in the menu bar'
|
||||
values={{appName: this.state.appName}}
|
||||
/> :
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.show'
|
||||
defaultMessage='Show icon in the notification area'
|
||||
/>
|
||||
}
|
||||
<FormText>
|
||||
{'Setting takes effect after restarting the app.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>);
|
||||
}
|
||||
|
@ -719,7 +854,10 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
ref={this.trayIconThemeRef}
|
||||
style={{marginLeft: '20px'}}
|
||||
>
|
||||
{'Icon theme: '}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme'
|
||||
defaultMessage='Icon theme: '
|
||||
/>
|
||||
{window.process.platform === 'win32' &&
|
||||
<>
|
||||
<FormCheck
|
||||
|
@ -729,7 +867,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
value='use_system'
|
||||
defaultChecked={this.state.trayIconTheme === 'use_system' || !this.state.trayIconTheme}
|
||||
onChange={() => this.handleChangeTrayIconTheme('use_system')}
|
||||
label={'Use system default'}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.systemDefault'
|
||||
defaultMessage='Use system default'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{' '}
|
||||
</>
|
||||
|
@ -741,7 +884,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
value='light'
|
||||
defaultChecked={this.state.trayIconTheme === 'light' || !this.state.trayIconTheme}
|
||||
onChange={() => this.handleChangeTrayIconTheme('light')}
|
||||
label={'Light'}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.light'
|
||||
defaultMessage='Light'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{' '}
|
||||
<FormCheck
|
||||
|
@ -751,7 +899,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
value='dark'
|
||||
defaultChecked={this.state.trayIconTheme === 'dark'}
|
||||
onChange={() => this.handleChangeTrayIconTheme('dark')}
|
||||
label={'Dark'}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.dark'
|
||||
defaultMessage='Dark'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormGroup>,
|
||||
);
|
||||
|
@ -771,10 +924,24 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.minimizeToTray}
|
||||
onChange={this.handleChangeMinimizeToTray}
|
||||
/>
|
||||
{'Leave app running in notification area when application window is closed'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.minimizeToTray'
|
||||
defaultMessage='Leave app running in notification area when application window is closed'
|
||||
/>
|
||||
<FormText>
|
||||
{'If enabled, the app stays running in the notification area after app window is closed.'}
|
||||
{this.state.showTrayIcon ? ' Setting takes effect after restarting the app.' : ''}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.minimizeToTray.description'
|
||||
defaultMessage='If enabled, the app stays running in the notification area after app window is closed.'
|
||||
/>
|
||||
{this.state.showTrayIcon &&
|
||||
<>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</FormText>
|
||||
</FormCheck>);
|
||||
}
|
||||
|
@ -790,10 +957,20 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.enableHardwareAcceleration}
|
||||
onChange={this.handleChangeEnableHardwareAcceleration}
|
||||
/>
|
||||
{'Use GPU hardware acceleration'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableHardwareAcceleration'
|
||||
defaultMessage='Use GPU hardware acceleration'
|
||||
/>
|
||||
<FormText>
|
||||
{'If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.'}
|
||||
{' Setting takes effect after restarting the app.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableHardwareAcceleration.description'
|
||||
defaultMessage='If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.'
|
||||
/>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>,
|
||||
);
|
||||
|
@ -809,9 +986,15 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.startInFullscreen}
|
||||
onChange={this.handleChangeStartInFullscreen}
|
||||
/>
|
||||
{'Open app in fullscreen'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.fullscreen'
|
||||
defaultMessage='Open app in fullscreen'
|
||||
/>
|
||||
<FormText>
|
||||
{'If enabled, the Mattermost application will always open in full screen'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.fullscreen.description'
|
||||
defaultMessage='If enabled, the Mattermost application will always open in full screen'
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>,
|
||||
);
|
||||
|
@ -822,7 +1005,50 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
key='containerDownloadLocation'
|
||||
>
|
||||
<hr/>
|
||||
<div>{'Download Location'}</div>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appLanguage'
|
||||
defaultMessage='Set app language (beta)'
|
||||
/>
|
||||
<FormControl
|
||||
style={settingsPage.appLanguageInput}
|
||||
as='select'
|
||||
id='inputAppLanguage'
|
||||
ref={this.appLanguageRef}
|
||||
value={this.state.appLanguage}
|
||||
onChange={this.handleChangeAppLanguage}
|
||||
>
|
||||
<option value=''>
|
||||
{intl.formatMessage({id: 'renderer.components.settingsPage.appLanguage.useSystemDefault', defaultMessage: 'Use system default'})}
|
||||
</option>
|
||||
{this.state.availableLanguages.map((language) => {
|
||||
return (
|
||||
<option
|
||||
key={language.value}
|
||||
value={language.value}
|
||||
>
|
||||
{language.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</FormControl>
|
||||
<FormText>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appLanguage.description'
|
||||
defaultMessage='Chooses the language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.'
|
||||
/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</FormText>
|
||||
<br/>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.downloadLocation'
|
||||
defaultMessage='Download Location'
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
disabled={true}
|
||||
style={settingsPage.downloadLocationInput}
|
||||
|
@ -837,13 +1063,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
id='saveDownloadLocation'
|
||||
onClick={this.selectDownloadLocation}
|
||||
>
|
||||
<span>{'Change'}</span>
|
||||
<FormattedMessage
|
||||
id='label.change'
|
||||
defaultMessage='Change'
|
||||
/>
|
||||
</Button>
|
||||
<FormText>
|
||||
{'Specify the folder where files will download.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.downloadLocation.description'
|
||||
defaultMessage='Specify the folder where files will download.'
|
||||
/>
|
||||
</FormText>
|
||||
<br/>
|
||||
{'Logging level'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.loggingLevel'
|
||||
defaultMessage='Logging level'
|
||||
/>
|
||||
<FormControl
|
||||
style={settingsPage.logLevelInput}
|
||||
as='select'
|
||||
|
@ -852,16 +1087,35 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
value={this.state.logLevel}
|
||||
onChange={this.handleChangeLogLevel}
|
||||
>
|
||||
<option value='error'>{'Errors (error)'}</option>
|
||||
<option value='warn'>{'Errors and Warnings (warn)'}</option>
|
||||
<option value='info'>{'Info (info)'}</option>
|
||||
<option value='verbose'>{'Verbose (verbose)'}</option>
|
||||
<option value='debug'>{'Debug (debug)'}</option>
|
||||
<option value='silly'>{'Finest (silly)'}</option>
|
||||
<option value='error'>
|
||||
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.error', defaultMessage: 'Errors (error)'})}
|
||||
</option>
|
||||
<option value='warn'>
|
||||
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.warn', defaultMessage: 'Errors and Warnings (warn)'})}
|
||||
</option>
|
||||
<option value='info'>
|
||||
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.info', defaultMessage: 'Info (info)'})}
|
||||
</option>
|
||||
<option value='verbose'>
|
||||
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.verbose', defaultMessage: 'Verbose (verbose)'})}
|
||||
</option>
|
||||
<option value='debug'>
|
||||
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.debug', defaultMessage: 'Debug (debug)'})}
|
||||
</option>
|
||||
<option value='silly'>
|
||||
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.silly', defaultMessage: 'Finest (silly)'})}
|
||||
</option>
|
||||
</FormControl>
|
||||
<FormText>
|
||||
{'Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.'}
|
||||
<br/>{'Increasing the log level increases disk space usage and can impact performance. We recommend only increasing the log level if you are having issues.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.loggingLevel.description'
|
||||
defaultMessage='Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.'
|
||||
/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.loggingLevel.description.subtitle'
|
||||
defaultMessage='Increasing the log level increases disk space usage and can impact performance. We recommend only increasing the log level if you are having issues.'
|
||||
/>
|
||||
</FormText>
|
||||
</div>,
|
||||
);
|
||||
|
@ -871,12 +1125,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
optionsRow = (
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<h2 style={settingsPage.sectionHeading}>{'App Options'}</h2>
|
||||
<h2 style={settingsPage.sectionHeading}>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appOptions'
|
||||
defaultMessage='App Options'
|
||||
/>
|
||||
</h2>
|
||||
<div className='IndicatorContainer appOptionsSaveIndicator'>
|
||||
<AutoSaveIndicator
|
||||
id='appOptionsSaveIndicator'
|
||||
savingState={this.state.savingState.appOptions}
|
||||
errorMessage={'Can\'t save your changes. Please try again.'}
|
||||
errorMessage={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.saving.error'
|
||||
defaultMessage={'Can\'t save your changes. Please try again.'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{ options.map((opt) => (
|
||||
|
@ -895,12 +1159,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
<>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<h2 style={settingsPage.sectionHeading}>{'Updates'}</h2>
|
||||
<h2 style={settingsPage.sectionHeading}>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates'
|
||||
defaultMessage='Updates'
|
||||
/>
|
||||
</h2>
|
||||
<div className='IndicatorContainer updatesSaveIndicator'>
|
||||
<AutoSaveIndicator
|
||||
id='updatesSaveIndicator'
|
||||
savingState={this.state.savingState.updates}
|
||||
errorMessage={'Can\'t save your changes. Please try again.'}
|
||||
errorMessage={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.saving.error'
|
||||
defaultMessage={'Can\'t save your changes. Please try again.'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormGroup
|
||||
|
@ -915,9 +1189,15 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
checked={this.state.autoCheckForUpdates}
|
||||
onChange={this.handleChangeAutoCheckForUpdates}
|
||||
/>
|
||||
{'Automatically check for updates'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.automatic'
|
||||
defaultMessage='Automatically check for updates'
|
||||
/>
|
||||
<FormText>
|
||||
{'If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.automatic.description'
|
||||
defaultMessage='If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.'
|
||||
/>
|
||||
</FormText>
|
||||
</FormCheck>
|
||||
<Button
|
||||
|
@ -925,7 +1205,10 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
id='checkForUpdatesNow'
|
||||
onClick={this.checkForUpdates}
|
||||
>
|
||||
<span>{'Check for Updates Now'}</span>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.checkNow'
|
||||
defaultMessage='Check for Updates Now'
|
||||
/>
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
@ -944,7 +1227,14 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
</>
|
||||
);
|
||||
} else {
|
||||
waitForIpc = (<p>{'Loading configuration...'}</p>);
|
||||
waitForIpc = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.loadingConfig'
|
||||
defaultMessage='Loading configuration...'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -962,7 +1252,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
}}
|
||||
>
|
||||
<div style={{position: 'relative'}}>
|
||||
<h1 style={settingsPage.heading}>{'Settings'}</h1>
|
||||
<h1 style={settingsPage.heading}>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.header'
|
||||
defaultMessage='Settings'
|
||||
/>
|
||||
</h1>
|
||||
<hr/>
|
||||
</div>
|
||||
<Container
|
||||
|
@ -975,3 +1270,5 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(SettingsPage);
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
import React from 'react';
|
||||
import {Nav, NavItem, NavLink} from 'react-bootstrap';
|
||||
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {Tab} from 'types/config';
|
||||
|
||||
import {getTabDisplayName, getTabViewName, TabType, canCloseTab} from 'common/tabs/TabView';
|
||||
import {getTabViewName, TabType, canCloseTab, getTabDisplayName} from 'common/tabs/TabView';
|
||||
|
||||
type Props = {
|
||||
activeTabName?: string;
|
||||
|
@ -25,6 +26,7 @@ type Props = {
|
|||
onDrop: (result: DropResult) => void;
|
||||
tabsDisabled?: boolean;
|
||||
isMenuOpen?: boolean;
|
||||
intl: IntlShape;
|
||||
};
|
||||
|
||||
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||
|
@ -38,7 +40,7 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
|||
return style;
|
||||
}
|
||||
|
||||
export default class TabBar extends React.PureComponent<Props> {
|
||||
class TabBar extends React.PureComponent<Props> {
|
||||
onCloseTab = (name: string) => {
|
||||
return (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
|
@ -102,7 +104,7 @@ export default class TabBar extends React.PureComponent<Props> {
|
|||
as='li'
|
||||
id={`teamTabItem${index}`}
|
||||
draggable={false}
|
||||
title={getTabDisplayName(tab.name as TabType)}
|
||||
title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getTabDisplayName(tab.name as TabType)})}
|
||||
className={classNames('teamTabItem', {
|
||||
active: this.props.activeTabName === tab.name,
|
||||
dragging: snapshot.isDragging,
|
||||
|
@ -121,9 +123,10 @@ export default class TabBar extends React.PureComponent<Props> {
|
|||
}}
|
||||
>
|
||||
<div className='TabBar-tabSeperator'>
|
||||
<span>
|
||||
{getTabDisplayName(tab.name as TabType)}
|
||||
</span>
|
||||
<FormattedMessage
|
||||
id={`common.tabs.${tab.name}`}
|
||||
defaultMessage={getTabDisplayName(tab.name as TabType)}
|
||||
/>
|
||||
{ badgeDiv }
|
||||
{canCloseTab(tab.name as TabType) &&
|
||||
<button
|
||||
|
@ -169,3 +172,5 @@ export default class TabBar extends React.PureComponent<Props> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(TabBar);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import classNames from 'classnames';
|
||||
import React, {useEffect} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {CLOSE_TEAMS_DROPDOWN, OPEN_TEAMS_DROPDOWN} from 'common/communication';
|
||||
|
||||
|
@ -64,7 +65,13 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
|
|||
<i className='icon-server-variant'/>
|
||||
{badgeDiv}
|
||||
</div>
|
||||
<span>{activeServerName || 'No servers configured'}</span>
|
||||
{activeServerName && <span>{activeServerName}</span>}
|
||||
{!activeServerName &&
|
||||
<FormattedMessage
|
||||
id='renderer.components.teamDropdownButton.noServersConfigured'
|
||||
defaultMessage='No servers configured'
|
||||
/>
|
||||
}
|
||||
<i className='icon-chevron-down'/>
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Button, Navbar, ProgressBar} from 'react-bootstrap';
|
||||
|
||||
type InstallButtonProps = {
|
||||
notifyOnly?: boolean;
|
||||
onClickInstall?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onClickDownload?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
function InstallButton(props: InstallButtonProps) {
|
||||
if (props.notifyOnly) {
|
||||
return (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={props.onClickDownload}
|
||||
>{'Download Update'}</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={props.onClickInstall}
|
||||
>{'Install Update'}</Button>
|
||||
);
|
||||
}
|
||||
|
||||
type UpdaterPageProps = {
|
||||
appName: string;
|
||||
notifyOnly?: boolean;
|
||||
isDownloading?: boolean;
|
||||
progress?: number;
|
||||
onClickInstall?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onClickDownload?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onClickReleaseNotes?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
onClickRemind?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onClickSkip?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onClickCancel?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
function UpdaterPage(props: UpdaterPageProps) {
|
||||
let footer;
|
||||
if (props.isDownloading) {
|
||||
footer = (
|
||||
<Navbar
|
||||
className='UpdaterPage-footer'
|
||||
fixed='bottom'
|
||||
>
|
||||
<ProgressBar
|
||||
animated={true}
|
||||
now={props.progress}
|
||||
label={`${props.progress}%`}
|
||||
/>
|
||||
<div className='pull-right'>
|
||||
<Button
|
||||
onClick={props.onClickCancel}
|
||||
>{'Cancel'}</Button>
|
||||
</div>
|
||||
</Navbar>
|
||||
);
|
||||
} else {
|
||||
footer = (
|
||||
<Navbar
|
||||
className='UpdaterPage-footer'
|
||||
fixed='bottom'
|
||||
>
|
||||
<Button
|
||||
className='UpdaterPage-skipButton'
|
||||
variant='link'
|
||||
onClick={props.onClickSkip}
|
||||
>{'Skip this version'}</Button>
|
||||
<div className='pull-right'>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={props.onClickRemind}
|
||||
>{'Remind me in 2 days'}</Button>
|
||||
<InstallButton
|
||||
notifyOnly={props.notifyOnly}
|
||||
onClickInstall={props.onClickInstall}
|
||||
onClickDownload={props.onClickDownload}
|
||||
/>
|
||||
</div>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='UpdaterPage'>
|
||||
<Navbar>
|
||||
<h1 className='UpdaterPage-heading'>{'New update is available'}</h1>
|
||||
</Navbar>
|
||||
<div className='container-fluid'>
|
||||
<p>{`A new version of the ${props.appName} is available!`}</p>
|
||||
<p>{'Read the '}
|
||||
<a
|
||||
href='#'
|
||||
onClick={props.onClickReleaseNotes}
|
||||
>{'release notes'}</a>
|
||||
{' to learn more.'}
|
||||
</p>
|
||||
</div>
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpdaterPage;
|
|
@ -1,53 +0,0 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
||||
|
||||
import {action} from '@storybook/addon-actions';
|
||||
|
||||
import UpdaterPage from '../UpdaterPage';
|
||||
import '../../css/components/UpdaterPage.css';
|
||||
|
||||
/*
|
||||
appName: propTypes.string.isRequired,
|
||||
notifyOnly: propTypes.bool.isRequired,
|
||||
isDownloading: propTypes.bool.isRequired,
|
||||
progress: propTypes.number,
|
||||
onClickInstall: propTypes.func.isRequired,
|
||||
onClickDownload: propTypes.func.isRequired,
|
||||
onClickReleaseNotes: propTypes.func.isRequired,
|
||||
onClickRemind: propTypes.func.isRequired,
|
||||
onClickSkip: propTypes.func.isRequired,
|
||||
*/
|
||||
const appName = 'Storybook App';
|
||||
|
||||
storiesOf('UpdaterPage', module).
|
||||
add('Normal', () => (
|
||||
<UpdaterPage
|
||||
appName={appName}
|
||||
notifyOnly={false}
|
||||
isDownloading={false}
|
||||
progress={0}
|
||||
onClickInstall={action('clicked install')}
|
||||
onClickReleaseNotes={action('clicked release notes')}
|
||||
onClickRemind={action('clicked remind')}
|
||||
onClickSkip={action('clicked skip')}
|
||||
/>
|
||||
)).
|
||||
add('NotifyOnly', () => (
|
||||
<UpdaterPage
|
||||
appName={appName}
|
||||
notifyOnly={true}
|
||||
onClickDownload={action('clicked download')}
|
||||
/>
|
||||
)).
|
||||
add('Downloading', () => (
|
||||
<UpdaterPage
|
||||
appName={appName}
|
||||
isDownloading={true}
|
||||
progress={0}
|
||||
onClickCancel={action('clicked cancel')}
|
||||
/>
|
||||
));
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import React, {Fragment} from 'react';
|
||||
import {Modal, Button, Row, Col} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {Certificate} from 'electron/renderer';
|
||||
|
||||
type Props = {
|
||||
|
@ -54,7 +55,10 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
|
|||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Body>
|
||||
{'No certificate Selected'}
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.noCertSelected'
|
||||
defaultMessage='No certificate Selected'
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -69,7 +73,7 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
|
|||
const expiration = utcSeconds(this.state.certificate?.validExpiry || 0);
|
||||
const creation = utcSeconds(this.state.certificate?.validStart || 0);
|
||||
const dateDisplayOptions = {dateStyle: 'full' as const, timeStyle: 'full' as const};
|
||||
const dateLocale = 'en-US';
|
||||
const dateLocale = 'en-US'; // TODO: Translate?
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
|
@ -83,21 +87,72 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
|
|||
<Modal.Body>
|
||||
<p className='details'>{'Details'}</p>
|
||||
<dl>
|
||||
{certificateSection('Subject Name')}
|
||||
{certificateItem('Common Name', this.state.certificate?.subject.commonName)}
|
||||
{certificateSection(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.subjectName'
|
||||
defaultMessage='Subject Name'
|
||||
/>,
|
||||
)}
|
||||
{certificateItem(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.commonName'
|
||||
defaultMessage='Common Name'
|
||||
/>,
|
||||
this.state.certificate?.subject.commonName,
|
||||
)}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateSection('Issuer Name')}
|
||||
{certificateItem('Common Name', this.state.certificate?.issuer.commonName)}
|
||||
{certificateSection(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.issuerName'
|
||||
defaultMessage='Issuer Name'
|
||||
/>,
|
||||
)}
|
||||
{certificateItem(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.commonName'
|
||||
defaultMessage='Common Name'
|
||||
/>,
|
||||
this.state.certificate?.issuer.commonName,
|
||||
)}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateItem('Serial Number', this.state.certificate?.serialNumber)}
|
||||
{certificateItem('Not Valid Before', creation.toLocaleString(dateLocale, dateDisplayOptions))}
|
||||
{certificateItem('Not Valid After', expiration.toLocaleString(dateLocale, dateDisplayOptions))}
|
||||
{certificateItem(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.serialNumber'
|
||||
defaultMessage='Serial Number'
|
||||
/>,
|
||||
this.state.certificate?.serialNumber,
|
||||
)}
|
||||
{certificateItem(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.notValidBefore'
|
||||
defaultMessage='Not Valid Before'
|
||||
/>,
|
||||
creation.toLocaleString(dateLocale, dateDisplayOptions),
|
||||
)}
|
||||
{certificateItem(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.notValidAfter'
|
||||
defaultMessage='Not Valid After'
|
||||
/>,
|
||||
expiration.toLocaleString(dateLocale, dateDisplayOptions),
|
||||
)}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateSection('Public Key Info')}
|
||||
{certificateItem('Algorithm', this.state.certificate?.fingerprint.split('/')[0])}
|
||||
{certificateSection(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.publicKeyInfo'
|
||||
defaultMessage='Public Key Info'
|
||||
/>,
|
||||
)}
|
||||
{certificateItem(
|
||||
<FormattedMessage
|
||||
id='renderer.components.showCertificateModal.algorithm'
|
||||
defaultMessage='Algorithm'
|
||||
/>,
|
||||
this.state.certificate?.fingerprint.split('/')[0],
|
||||
)}
|
||||
</dl>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'no-border'}>
|
||||
|
@ -108,7 +163,12 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
|
|||
variant='primary'
|
||||
onClick={this.handleOk}
|
||||
className={'primary'}
|
||||
>{'Close'}</Button>
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
|
||||
|
||||
|
@ -23,6 +24,8 @@ import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH_MAC} from 'common/utils/constants';
|
|||
|
||||
import './css/dropdown.scss';
|
||||
|
||||
import IntlProvider from './intl_provider';
|
||||
|
||||
type State = {
|
||||
teams?: TeamWithTabs[];
|
||||
orderedTeams?: TeamWithTabs[];
|
||||
|
@ -227,137 +230,147 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
onClick={this.preventPropogation}
|
||||
className={classNames('TeamDropdown', {
|
||||
darkMode: this.state.darkMode,
|
||||
})}
|
||||
style={{
|
||||
maxHeight: this.state.windowBounds ? (this.state.windowBounds.height - TAB_BAR_HEIGHT - 16) : undefined,
|
||||
maxWidth: this.state.windowBounds ? (this.state.windowBounds.width - THREE_DOT_MENU_WIDTH_MAC) : undefined,
|
||||
}}
|
||||
>
|
||||
<div className='TeamDropdown__header'>
|
||||
<span className='TeamDropdown__servers'>{'Servers'}</span>
|
||||
<span className='TeamDropdown__keyboardShortcut'>
|
||||
{window.process.platform === 'darwin' ? '⌃⌘S' : 'Ctrl + Shift + S'}
|
||||
</span>
|
||||
</div>
|
||||
<hr className='TeamDropdown__divider'/>
|
||||
<DragDropContext
|
||||
onDragStart={this.onDragStart}
|
||||
onDragEnd={this.onDragEnd}
|
||||
<IntlProvider>
|
||||
<div
|
||||
onClick={this.preventPropogation}
|
||||
className={classNames('TeamDropdown', {
|
||||
darkMode: this.state.darkMode,
|
||||
})}
|
||||
style={{
|
||||
maxHeight: this.state.windowBounds ? (this.state.windowBounds.height - TAB_BAR_HEIGHT - 16) : undefined,
|
||||
maxWidth: this.state.windowBounds ? (this.state.windowBounds.width - THREE_DOT_MENU_WIDTH_MAC) : undefined,
|
||||
}}
|
||||
>
|
||||
<Droppable
|
||||
isDropDisabled={this.state.hasGPOTeams}
|
||||
droppableId='TeamDropdown__droppable'
|
||||
<div className='TeamDropdown__header'>
|
||||
<span className='TeamDropdown__servers'>
|
||||
<FormattedMessage
|
||||
id='renderer.dropdown.servers'
|
||||
defaultMessage='Servers'
|
||||
/>
|
||||
</span>
|
||||
<span className='TeamDropdown__keyboardShortcut'>
|
||||
{window.process.platform === 'darwin' ? '⌃⌘S' : 'Ctrl + Shift + S'}
|
||||
</span>
|
||||
</div>
|
||||
<hr className='TeamDropdown__divider'/>
|
||||
<DragDropContext
|
||||
onDragStart={this.onDragStart}
|
||||
onDragEnd={this.onDragEnd}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
className='TeamDropdown__droppable'
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{this.state.orderedTeams?.map((team, orderedIndex) => {
|
||||
const index = this.state.teams?.indexOf(team);
|
||||
const {sessionExpired, hasUnreads, mentionCount} = team.tabs.reduce((counts, tab) => {
|
||||
const tabName = getTabViewName(team.name, tab.name);
|
||||
counts.sessionExpired = this.state.expired?.get(tabName) || counts.sessionExpired;
|
||||
counts.hasUnreads = this.state.unreads?.get(tabName) || counts.hasUnreads;
|
||||
counts.mentionCount += this.state.mentions?.get(tabName) || 0;
|
||||
return counts;
|
||||
}, {sessionExpired: false, hasUnreads: false, mentionCount: 0});
|
||||
<Droppable
|
||||
isDropDisabled={this.state.hasGPOTeams}
|
||||
droppableId='TeamDropdown__droppable'
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
className='TeamDropdown__droppable'
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{this.state.orderedTeams?.map((team, orderedIndex) => {
|
||||
const index = this.state.teams?.indexOf(team);
|
||||
const {sessionExpired, hasUnreads, mentionCount} = team.tabs.reduce((counts, tab) => {
|
||||
const tabName = getTabViewName(team.name, tab.name);
|
||||
counts.sessionExpired = this.state.expired?.get(tabName) || counts.sessionExpired;
|
||||
counts.hasUnreads = this.state.unreads?.get(tabName) || counts.hasUnreads;
|
||||
counts.mentionCount += this.state.mentions?.get(tabName) || 0;
|
||||
return counts;
|
||||
}, {sessionExpired: false, hasUnreads: false, mentionCount: 0});
|
||||
|
||||
let badgeDiv: React.ReactNode;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-expired'>
|
||||
<i className='icon-alert-circle-outline'/>
|
||||
</div>
|
||||
);
|
||||
} else if (mentionCount && mentionCount > 0) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-count'>
|
||||
<span>{mentionCount > 99 ? '99+' : mentionCount}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (hasUnreads) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-dot'/>
|
||||
);
|
||||
}
|
||||
let badgeDiv: React.ReactNode;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-expired'>
|
||||
<i className='icon-alert-circle-outline'/>
|
||||
</div>
|
||||
);
|
||||
} else if (mentionCount && mentionCount > 0) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-count'>
|
||||
<span>{mentionCount > 99 ? '99+' : mentionCount}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (hasUnreads) {
|
||||
badgeDiv = (
|
||||
<div className='TeamDropdown__badge-dot'/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={index}
|
||||
draggableId={`TeamDropdown__draggable-${index}`}
|
||||
index={orderedIndex}
|
||||
disableInteractiveElementBlocking={true}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<button
|
||||
className={classNames('TeamDropdown__button', {
|
||||
dragging: snapshot.isDragging,
|
||||
anyDragging: this.state.isAnyDragging,
|
||||
active: this.isActiveTeam(team),
|
||||
})}
|
||||
ref={this.setButtonRef(orderedIndex, provided.innerRef)}
|
||||
{...provided.draggableProps}
|
||||
onClick={this.selectServer(team)}
|
||||
style={getStyle(provided.draggableProps.style)}
|
||||
>
|
||||
<div
|
||||
className={classNames('TeamDropdown__draggable-handle', {
|
||||
return (
|
||||
<Draggable
|
||||
key={index}
|
||||
draggableId={`TeamDropdown__draggable-${index}`}
|
||||
index={orderedIndex}
|
||||
disableInteractiveElementBlocking={true}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<button
|
||||
className={classNames('TeamDropdown__button', {
|
||||
dragging: snapshot.isDragging,
|
||||
anyDragging: this.state.isAnyDragging,
|
||||
active: this.isActiveTeam(team),
|
||||
})}
|
||||
{...provided.dragHandleProps}
|
||||
onClick={this.handleClickOnDragHandle}
|
||||
ref={this.setButtonRef(orderedIndex, provided.innerRef)}
|
||||
{...provided.draggableProps}
|
||||
onClick={this.selectServer(team)}
|
||||
style={getStyle(provided.draggableProps.style)}
|
||||
>
|
||||
<i className='icon-drag-vertical'/>
|
||||
{this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
|
||||
<span>{team.name}</span>
|
||||
</div>
|
||||
<div className='TeamDropdown__indicators'>
|
||||
<button
|
||||
className='TeamDropdown__button-edit'
|
||||
onClick={this.editServer(team.name)}
|
||||
<div
|
||||
className={classNames('TeamDropdown__draggable-handle', {
|
||||
dragging: snapshot.isDragging,
|
||||
})}
|
||||
{...provided.dragHandleProps}
|
||||
onClick={this.handleClickOnDragHandle}
|
||||
>
|
||||
<i className='icon-pencil-outline'/>
|
||||
</button>
|
||||
<button
|
||||
className='TeamDropdown__button-remove'
|
||||
onClick={this.removeServer(team.name)}
|
||||
>
|
||||
<i className='icon-trash-can-outline'/>
|
||||
</button>
|
||||
{badgeDiv && <div className='TeamDropdown__badge'>
|
||||
{badgeDiv}
|
||||
</div>}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<hr className='TeamDropdown__divider'/>
|
||||
{this.state.enableServerManagement &&
|
||||
<button
|
||||
ref={(ref) => {
|
||||
this.addButtonRef(this.state.orderedTeams?.length || 0, ref);
|
||||
}}
|
||||
className='TeamDropdown__button addServer'
|
||||
onClick={this.addServer}
|
||||
>
|
||||
<i className='icon-plus'/>
|
||||
<span>{'Add a server'}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<i className='icon-drag-vertical'/>
|
||||
{this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
|
||||
<span>{team.name}</span>
|
||||
</div>
|
||||
<div className='TeamDropdown__indicators'>
|
||||
<button
|
||||
className='TeamDropdown__button-edit'
|
||||
onClick={this.editServer(team.name)}
|
||||
>
|
||||
<i className='icon-pencil-outline'/>
|
||||
</button>
|
||||
<button
|
||||
className='TeamDropdown__button-remove'
|
||||
onClick={this.removeServer(team.name)}
|
||||
>
|
||||
<i className='icon-trash-can-outline'/>
|
||||
</button>
|
||||
{badgeDiv && <div className='TeamDropdown__badge'>
|
||||
{badgeDiv}
|
||||
</div>}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<hr className='TeamDropdown__divider'/>
|
||||
{this.state.enableServerManagement &&
|
||||
<button
|
||||
ref={(ref) => {
|
||||
this.addButtonRef(this.state.orderedTeams?.length || 0, ref);
|
||||
}}
|
||||
className='TeamDropdown__button addServer'
|
||||
onClick={this.addServer}
|
||||
>
|
||||
<i className='icon-plus'/>
|
||||
<FormattedMessage
|
||||
id='renderer.dropdown.addAServer'
|
||||
defaultMessage='Add a server'
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {CombinedConfig, Team} from 'types/config';
|
|||
import {GET_CONFIGURATION, UPDATE_TEAMS, QUIT, RELOAD_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import MainPage from './components/MainPage';
|
||||
import IntlProvider from './intl_provider';
|
||||
|
||||
type State = {
|
||||
config?: CombinedConfig;
|
||||
|
@ -120,15 +121,17 @@ class Root extends React.PureComponent<Record<string, never>, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<MainPage
|
||||
teams={config.teams}
|
||||
lastActiveTeam={config.lastActiveTeam}
|
||||
moveTabs={this.moveTabs}
|
||||
openMenu={this.openMenu}
|
||||
darkMode={config.darkMode}
|
||||
appName={config.appName}
|
||||
useNativeWindow={config.useNativeWindow}
|
||||
/>
|
||||
<IntlProvider>
|
||||
<MainPage
|
||||
teams={config.teams}
|
||||
lastActiveTeam={config.lastActiveTeam}
|
||||
moveTabs={this.moveTabs}
|
||||
openMenu={this.openMenu}
|
||||
darkMode={config.darkMode}
|
||||
appName={config.appName}
|
||||
useNativeWindow={config.useNativeWindow}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
55
src/renderer/intl_provider.tsx
Normal file
55
src/renderer/intl_provider.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {IntlProvider as BaseIntlProvider} from 'react-intl';
|
||||
|
||||
import {GET_LANGUAGE_INFORMATION, RETRIEVED_LANGUAGE_INFORMATION} from 'common/communication';
|
||||
|
||||
import {Language} from '../../i18n/i18n';
|
||||
|
||||
type State = {
|
||||
language?: Language;
|
||||
}
|
||||
|
||||
export default class IntlProvider extends React.PureComponent<any, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.handleMessageEvent);
|
||||
window.postMessage({type: GET_LANGUAGE_INFORMATION});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.handleMessageEvent);
|
||||
}
|
||||
|
||||
handleMessageEvent = (event: MessageEvent<{type: string; data: Language}>) => {
|
||||
if (event.data.type === RETRIEVED_LANGUAGE_INFORMATION) {
|
||||
this.setState({
|
||||
language: event.data.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.language) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseIntlProvider
|
||||
key={this.state.language.value}
|
||||
locale={this.state.language.value}
|
||||
messages={this.state.language.url}
|
||||
textComponent='span'
|
||||
wrapRichTextChunksInFragment={false}
|
||||
>
|
||||
{this.props.children}
|
||||
</BaseIntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,12 +4,15 @@
|
|||
import {Certificate} from 'electron/renderer';
|
||||
import React, {Fragment} from 'react';
|
||||
import {Modal, Button, Table, Row, Col} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {CertificateModalData} from 'types/certificate';
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
|
||||
import IntlProvider from 'renderer/intl_provider';
|
||||
|
||||
import ShowCertificateModal from '../../components/showCertificateModal';
|
||||
|
||||
type Props = {
|
||||
|
@ -91,7 +94,12 @@ export default class SelectCertificateModal extends React.PureComponent<Props, S
|
|||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (<Fragment><tr/><tr><td/><td>{'No certificates available'}</td><td/></tr></Fragment>);
|
||||
return (<Fragment><tr/><tr><td/><td>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.certificate.certificateModal.noCertsAvailable'
|
||||
defaultMessage='No certificates available'
|
||||
/>
|
||||
</td><td/></tr></Fragment>);
|
||||
}
|
||||
|
||||
getSelectedCert = () => {
|
||||
|
@ -128,65 +136,108 @@ export default class SelectCertificateModal extends React.PureComponent<Props, S
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='certificate-modal'
|
||||
show={Boolean(this.state.list && this.state.url)}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title >{'Select a certificate'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className={'subtitle'}>{`Select a certificate to authenticate yourself to ${this.state.url}`}</p>
|
||||
<Table
|
||||
striped={true}
|
||||
hover={true}
|
||||
responsive={true}
|
||||
className='certificate-list'
|
||||
tabIndex={1}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span className={'divider'}>{'Subject'}</span></th>
|
||||
<th><span className={'divider'}>{'Issuer'}</span></th>
|
||||
<th>{'Serial'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderCerts(this.state.list!)}
|
||||
<tr/* this is to correct table height without affecting real rows *//>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'no-border'}>
|
||||
<div className={'container-fluid'}>
|
||||
<Row>
|
||||
<Col sm={4}>
|
||||
<Button
|
||||
variant='info'
|
||||
disabled={this.state.selectedIndex === null}
|
||||
onClick={this.handleCertificateInfo}
|
||||
className={'info'}
|
||||
>{'Certificate Information'}</Button>
|
||||
</Col>
|
||||
<Col sm={8}>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={this.props.onCancel}
|
||||
className={'secondary'}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={this.handleOk}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
className={'primary'}
|
||||
>{'OK'}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
<IntlProvider>
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='certificate-modal'
|
||||
show={Boolean(this.state.list && this.state.url)}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.certificate.certificateModal.title'
|
||||
defaultMessage='Select a certificate'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className={'subtitle'}>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.certificate.certificateModal.subtitle'
|
||||
defaultMessage='Select a certificate to authenticate yourself to {url}'
|
||||
values={{url: this.state.url}}
|
||||
/>
|
||||
</p>
|
||||
<Table
|
||||
striped={true}
|
||||
hover={true}
|
||||
responsive={true}
|
||||
className='certificate-list'
|
||||
tabIndex={1}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span className={'divider'}>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.certificate.certificateModal.subject'
|
||||
defaultMessage='Subject'
|
||||
/>
|
||||
</span></th>
|
||||
<th><span className={'divider'}>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.certificate.certificateModal.issuer'
|
||||
defaultMessage='Issuer'
|
||||
/>
|
||||
</span></th>
|
||||
<th>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.certificate.certificateModal.serial'
|
||||
defaultMessage='Serial'
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderCerts(this.state.list!)}
|
||||
<tr/* this is to correct table height without affecting real rows *//>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'no-border'}>
|
||||
<div className={'container-fluid'}>
|
||||
<Row>
|
||||
<Col sm={4}>
|
||||
<Button
|
||||
variant='info'
|
||||
disabled={this.state.selectedIndex === null}
|
||||
onClick={this.handleCertificateInfo}
|
||||
className={'info'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.certificate.certificateModal.certInfoButton'
|
||||
defaultMessage='Certificate Information'
|
||||
/>
|
||||
</Button>
|
||||
</Col>
|
||||
<Col sm={8}>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={this.props.onCancel}
|
||||
className={'secondary'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={this.handleOk}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
className={'primary'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.ok'
|
||||
defaultMessage='OK'
|
||||
/>
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import {ModalMessage} from 'types/modals';
|
|||
|
||||
import {MODAL_CANCEL, MODAL_INFO, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
|
||||
|
||||
import IntlProvider from 'renderer/intl_provider';
|
||||
|
||||
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
|
||||
|
||||
import setupDarkMode from '../darkMode';
|
||||
|
@ -48,14 +50,16 @@ const EditServerModalWrapper: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<NewTeamModal
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
editMode={true}
|
||||
show={Boolean(server)}
|
||||
team={server}
|
||||
currentTeams={currentTeams}
|
||||
/>
|
||||
<IntlProvider>
|
||||
<NewTeamModal
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
editMode={true}
|
||||
show={Boolean(server)}
|
||||
team={server}
|
||||
currentTeams={currentTeams}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import {Button, Col, FormLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
|
||||
import {LoginModalData} from 'types/auth';
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
@ -12,10 +13,13 @@ import {AuthenticationResponseDetails, AuthInfo} from 'electron/renderer';
|
|||
import urlUtils from 'common/utils/url';
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
|
||||
import IntlProvider from 'renderer/intl_provider';
|
||||
|
||||
type Props = {
|
||||
onCancel: (request: AuthenticationResponseDetails) => void;
|
||||
onLogin: (request: AuthenticationResponseDetails, username: string, password: string) => void;
|
||||
getAuthInfo: () => void;
|
||||
intl: IntlShape;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -25,7 +29,7 @@ type State = {
|
|||
authInfo?: AuthInfo;
|
||||
};
|
||||
|
||||
export default class LoginModal extends React.PureComponent<Props, State> {
|
||||
class LoginModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -86,83 +90,126 @@ export default class LoginModal extends React.PureComponent<Props, State> {
|
|||
this.setState({password: e.target.value});
|
||||
}
|
||||
|
||||
render() {
|
||||
let theServer = '';
|
||||
renderLoginModalMessage = () => {
|
||||
if (!(this.state.request && this.state.authInfo)) {
|
||||
theServer = '';
|
||||
return null;
|
||||
} else if (this.state.authInfo.isProxy) {
|
||||
theServer = `The proxy ${this.state.authInfo.host}:${this.state.authInfo.port}`;
|
||||
} else {
|
||||
const tmpURL = urlUtils.parseURL(this.state.request.url);
|
||||
theServer = `The server ${tmpURL?.protocol}//${tmpURL?.host}`;
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='renderer.modals.login.loginModal.message.proxy'
|
||||
defaultMessage='The proxy {host}:{port} requires a username and password.'
|
||||
values={{host: this.state.authInfo.host, port: this.state.authInfo.port}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const message = `${theServer} requires a username and password.`;
|
||||
const tmpURL = urlUtils.parseURL(this.state.request.url);
|
||||
return (
|
||||
<Modal
|
||||
show={Boolean(this.state.request && this.state.authInfo)}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{'Authentication Required'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
{ message }
|
||||
</p>
|
||||
<Form
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<Col
|
||||
as={FormLabel}
|
||||
sm={2}
|
||||
>{'User Name'}</Col>
|
||||
<Col sm={10}>
|
||||
<FormControl
|
||||
type='text'
|
||||
placeholder='User Name'
|
||||
onChange={this.setUsername}
|
||||
value={this.state.username}
|
||||
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Col
|
||||
as={FormLabel}
|
||||
sm={2}
|
||||
>{'Password'}</Col>
|
||||
<Col sm={10}>
|
||||
<FormControl
|
||||
type='password'
|
||||
placeholder='Password'
|
||||
onChange={this.setPassword}
|
||||
value={this.state.password}
|
||||
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Col sm={12}>
|
||||
<div className='pull-right'>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
>{'Login'}</Button>
|
||||
{ ' ' }
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={this.handleCancel}
|
||||
>{'Cancel'}</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.login.loginModal.message.server'
|
||||
defaultMessage='The server {url} requires a username and password.'
|
||||
values={{url: `${tmpURL?.protocol}//${tmpURL?.host}`}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {intl} = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider>
|
||||
<Modal
|
||||
show={Boolean(this.state.request && this.state.authInfo)}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.login.loginModal.title'
|
||||
defaultMessage='Authentication Required'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
{this.renderLoginModalMessage()}
|
||||
</p>
|
||||
<Form
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<Col
|
||||
as={FormLabel}
|
||||
sm={2}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.login.loginModal.username'
|
||||
defaultMessage='User Name'
|
||||
/>
|
||||
</Col>
|
||||
<Col sm={10}>
|
||||
<FormControl
|
||||
type='text'
|
||||
placeholder={intl.formatMessage({id: 'renderer.modals.login.loginModal.username', defaultMessage: 'User Name'})}
|
||||
onChange={this.setUsername}
|
||||
value={this.state.username}
|
||||
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Col
|
||||
as={FormLabel}
|
||||
sm={2}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.login.loginModal.password'
|
||||
defaultMessage='Password'
|
||||
/>
|
||||
</Col>
|
||||
<Col sm={10}>
|
||||
<FormControl
|
||||
type='password'
|
||||
placeholder={intl.formatMessage({id: 'renderer.modals.login.loginModal.password', defaultMessage: 'Password'})}
|
||||
onChange={this.setPassword}
|
||||
value={this.state.password}
|
||||
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Col sm={12}>
|
||||
<div className='pull-right'>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.login'
|
||||
defaultMessage='Login'
|
||||
/>
|
||||
</Button>
|
||||
{ ' ' }
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={this.handleCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(LoginModal);
|
||||
|
|
|
@ -12,6 +12,8 @@ import {ModalMessage} from 'types/modals';
|
|||
|
||||
import {GET_MODAL_UNCLOSEABLE, MODAL_CANCEL, MODAL_INFO, MODAL_RESULT, MODAL_UNCLOSEABLE, RETRIEVE_MODAL_INFO} from 'common/communication';
|
||||
|
||||
import IntlProvider from 'renderer/intl_provider';
|
||||
|
||||
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
|
||||
|
||||
import setupDarkMode from '../darkMode';
|
||||
|
@ -55,13 +57,15 @@ const NewServerModalWrapper: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<NewTeamModal
|
||||
onClose={unremoveable ? undefined : onClose}
|
||||
onSave={onSave}
|
||||
editMode={false}
|
||||
show={true}
|
||||
currentTeams={currentTeams}
|
||||
/>
|
||||
<IntlProvider>
|
||||
<NewTeamModal
|
||||
onClose={unremoveable ? undefined : onClose}
|
||||
onSave={onSave}
|
||||
editMode={false}
|
||||
show={true}
|
||||
currentTeams={currentTeams}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,20 +3,24 @@
|
|||
|
||||
import React from 'react';
|
||||
import {Modal, Button} from 'react-bootstrap';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
|
||||
import {PermissionType} from 'types/trustedOrigin';
|
||||
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
||||
import urlUtil from 'common/utils/url';
|
||||
import {t} from 'common/utils/util';
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
import {PERMISSION_DESCRIPTION} from 'common/permissions';
|
||||
import IntlProvider from 'renderer/intl_provider';
|
||||
|
||||
type Props = {
|
||||
handleDeny: React.MouseEventHandler<HTMLButtonElement>;
|
||||
handleGrant: React.MouseEventHandler<HTMLButtonElement>;
|
||||
getPermissionInfo: () => void;
|
||||
openExternalLink: (protocol: string, url: string) => void;
|
||||
intl: IntlShape;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -24,7 +28,7 @@ type State = {
|
|||
permission?: PermissionType;
|
||||
}
|
||||
|
||||
export default class PermissionModal extends React.PureComponent<Props, State> {
|
||||
class PermissionModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
|
@ -53,12 +57,13 @@ export default class PermissionModal extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
getModalTitle() {
|
||||
return `${PERMISSION_DESCRIPTION[this.state.permission!]} Required`;
|
||||
const permission = this.props.intl.formatMessage({id: `common.permissions.${PERMISSION_DESCRIPTION[this.state.permission!]}`});
|
||||
return this.props.intl.formatMessage({id: 'renderer.modals.permission.permissionModal.title', defaultMessage: '{permission} Required'}, {permission});
|
||||
}
|
||||
|
||||
getModalBody() {
|
||||
const {url, permission} = this.state;
|
||||
const originDisplay = url ? urlUtil.getHost(url) : 'unknown origin';
|
||||
const originDisplay = url ? urlUtil.getHost(url) : this.props.intl.formatMessage({id: 'renderer.modals.permission.permissionModal.unknownOrigin', defaultMessage: 'unknown origin'});
|
||||
const originLink = url ? originDisplay : '';
|
||||
|
||||
const click = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
|
@ -75,10 +80,20 @@ export default class PermissionModal extends React.PureComponent<Props, State> {
|
|||
return (
|
||||
<div>
|
||||
<p>
|
||||
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission!]}.`}
|
||||
<FormattedMessage
|
||||
id='renderer.modals.permission.permissionModal.body'
|
||||
defaultMessage={'A site that\'s not included in your Mattermost server configuration requires access for {permission}.'}
|
||||
values={{
|
||||
permission: this.props.intl.formatMessage({id: `common.permissions.${PERMISSION_DESCRIPTION[permission!]}`}),
|
||||
}}
|
||||
/>
|
||||
{}
|
||||
</p>
|
||||
<p>
|
||||
<span>{'This request originated from '}</span>
|
||||
<FormattedMessage
|
||||
id='renderer.modals.permission.permissionModal.requestOriginatedFrom'
|
||||
defaultMessage='This request originated from '
|
||||
/>
|
||||
<a onClick={click}>{originDisplay}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -87,32 +102,46 @@ export default class PermissionModal extends React.PureComponent<Props, State> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='permission-modal'
|
||||
show={Boolean(this.state.url && this.state.permission)}
|
||||
id='requestPermissionModal'
|
||||
enforceFocus={true}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{this.getModalTitle()}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.getModalBody()}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'remove-border'}>
|
||||
<div>
|
||||
<Button
|
||||
onClick={this.props.handleDeny}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={this.props.handleGrant}
|
||||
>{'Accept'}</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
<IntlProvider>
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='permission-modal'
|
||||
show={Boolean(this.state.url && this.state.permission)}
|
||||
id='requestPermissionModal'
|
||||
enforceFocus={true}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{this.getModalTitle()}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.getModalBody()}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'remove-border'}>
|
||||
<div>
|
||||
<Button onClick={this.props.handleDeny}>
|
||||
<FormattedMessage
|
||||
id='label.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={this.props.handleGrant}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.accept'
|
||||
defaultMessage='Accept'
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
t('common.permissions.canBasicAuth');
|
||||
|
||||
export default injectIntl(PermissionModal);
|
||||
|
|
|
@ -11,6 +11,8 @@ import {ModalMessage} from 'types/modals';
|
|||
|
||||
import {MODAL_CANCEL, MODAL_INFO, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
|
||||
|
||||
import IntlProvider from 'renderer/intl_provider';
|
||||
|
||||
import RemoveServerModal from '../../components/RemoveServerModal';
|
||||
|
||||
import setupDarkMode from '../darkMode';
|
||||
|
@ -45,19 +47,21 @@ const RemoveServerModalWrapper: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<RemoveServerModal
|
||||
show={true}
|
||||
serverName={serverName}
|
||||
onHide={() => {
|
||||
onClose();
|
||||
}}
|
||||
onCancel={() => {
|
||||
onSave(false);
|
||||
}}
|
||||
onAccept={() => {
|
||||
onSave(true);
|
||||
}}
|
||||
/>
|
||||
<IntlProvider>
|
||||
<RemoveServerModal
|
||||
show={true}
|
||||
serverName={serverName}
|
||||
onHide={() => {
|
||||
onClose();
|
||||
}}
|
||||
onCancel={() => {
|
||||
onSave(false);
|
||||
}}
|
||||
onAccept={() => {
|
||||
onSave(true);
|
||||
}}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import {DARK_MODE_CHANGE, GET_DARK_MODE} from 'common/communication';
|
|||
import darkStyles from 'renderer/css/lazy/settings-dark.lazy.css';
|
||||
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
import IntlProvider from './intl_provider';
|
||||
|
||||
const setDarkMode = (darkMode: boolean) => {
|
||||
if (darkMode) {
|
||||
|
@ -28,7 +29,12 @@ window.ipcRenderer.invoke(GET_DARK_MODE).then(setDarkMode);
|
|||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<SettingsPage/>,
|
||||
(
|
||||
<IntlProvider>
|
||||
<SettingsPage/>
|
||||
</IntlProvider>
|
||||
)
|
||||
,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Updater</title>
|
||||
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="css/components/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content"></div>
|
||||
<script src="updater_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,162 +0,0 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// TODO: Commented out for now since remote is removed, will be changed with the autoupdater changes anyway.
|
||||
|
||||
// import url from 'url';
|
||||
|
||||
// import React from 'react';
|
||||
// import ReactDOM from 'react-dom';
|
||||
// //import {remote} from 'electron';
|
||||
|
||||
// import UpdaterPage from './components/UpdaterPage';
|
||||
|
||||
// const thisURL = url.parse(location.href, true);
|
||||
// const notifyOnly = thisURL.query.notifyOnly === 'true';
|
||||
|
||||
// type Props = {
|
||||
// notifyOnly: boolean;
|
||||
// initialState: State;
|
||||
// };
|
||||
|
||||
// type State = {
|
||||
|
||||
// }
|
||||
|
||||
// class UpdaterPageContainer extends React.PureComponent<Props, State> {
|
||||
// constructor(props: Props) {
|
||||
// super(props);
|
||||
// this.state = props.initialState;
|
||||
// }
|
||||
|
||||
// getTabWebContents() {
|
||||
// return null;//remote.webContents.getFocusedWebContents();
|
||||
// }
|
||||
|
||||
// componentDidMount() {
|
||||
// window.ipcRenderer.on('start-download', () => {
|
||||
// this.setState({
|
||||
// isDownloading: true,
|
||||
// });
|
||||
// });
|
||||
// window.ipcRenderer.on('progress', (event, progress) => {
|
||||
// this.setState({
|
||||
// progress,
|
||||
// });
|
||||
// });
|
||||
// window.ipcRenderer.on('zoom-in', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// if (activeTabWebContents.zoomLevel >= 9) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.zoomLevel += 1;
|
||||
// });
|
||||
|
||||
// window.ipcRenderer.on('zoom-out', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// if (activeTabWebContents.zoomLevel <= -8) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.zoomLevel -= 1;
|
||||
// });
|
||||
|
||||
// window.ipcRenderer.on('zoom-reset', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.zoomLevel = 0;
|
||||
// });
|
||||
|
||||
// window.ipcRenderer.on('undo', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.undo();
|
||||
// });
|
||||
|
||||
// window.ipcRenderer.on('redo', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.redo();
|
||||
// });
|
||||
|
||||
// window.ipcRenderer.on('cut', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.cut();
|
||||
// });
|
||||
|
||||
// window.ipcRenderer.on('copy', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.copy();
|
||||
// });
|
||||
|
||||
// window.ipcRenderer.on('paste', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.paste();
|
||||
// });
|
||||
|
||||
// window.ipcRenderer.on('paste-and-match', () => {
|
||||
// const activeTabWebContents = this.getTabWebContents();
|
||||
// if (!activeTabWebContents) {
|
||||
// return;
|
||||
// }
|
||||
// activeTabWebContents.pasteAndMatchStyle();
|
||||
// });
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// return (
|
||||
// <UpdaterPage
|
||||
// appName={`${remote.app.name} Desktop App`}
|
||||
// notifyOnly={this.props.notifyOnly}
|
||||
// {...this.state}
|
||||
// onClickReleaseNotes={() => {
|
||||
// window.ipcRenderer.send('click-release-notes');
|
||||
// }}
|
||||
// onClickSkip={() => {
|
||||
// window.ipcRenderer.send('click-skip');
|
||||
// }}
|
||||
// onClickRemind={() => {
|
||||
// window.ipcRenderer.send('click-remind');
|
||||
// }}
|
||||
// onClickInstall={() => {
|
||||
// window.ipcRenderer.send('click-install');
|
||||
// }}
|
||||
// onClickDownload={() => {
|
||||
// window.ipcRenderer.send('click-download');
|
||||
// }}
|
||||
// onClickCancel={() => {
|
||||
// window.ipcRenderer.send('click-cancel');
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// ReactDOM.render(
|
||||
// <UpdaterPageContainer
|
||||
// notifyOnly={notifyOnly}
|
||||
// initialState={{isDownloading: false, progress: 0}}
|
||||
// />,
|
||||
// document.getElementById('content'),
|
||||
// );
|
|
@ -45,6 +45,7 @@ export type ConfigV3 = {
|
|||
alwaysMinimize?: boolean;
|
||||
alwaysClose?: boolean;
|
||||
logLevel?: string;
|
||||
appLanguage?: string;
|
||||
}
|
||||
|
||||
export type ConfigV2 = {
|
||||
|
|
Loading…
Reference in a new issue