[MM-23067] Browser View (#1514)
* Browser-view: initial architectural changes + webpack (#1358) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * remove old webpack generated files * add assets files * more remove files and fix localurls * CR changes * Browserview settings window (#1362) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * [BrowserView] renderer (#1378) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * make eslint be aware of webpack aliases * some extra disable lines * move badge management to main * remove unneded import * fixing errors * wip * back to having tabs * switch tab working * wip * wip * wip * fix quitting error * back to a working config * configure retries * add darkmode * wip * add error/loading screens * fix settings while removing remote usage * wip * fix lint, get preload to load * remove unused import * remove log statements * Bv menus (#1387) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * make eslint be aware of webpack aliases * some extra disable lines * move badge management to main * remove unneded import * fixing errors * wip * back to having tabs * switch tab working * wip * wip * wip * fix quitting error * back to a working config * configure retries * add darkmode * wip * add error/loading screens * fix settings while removing remote usage * wip * fix lint, get preload to load * remove unused import * wip * menus initially working as they should * update deps, show context menu * wip * wip * wip * fix forward/back menu * fix server menu * allow navigating to external urls in the browser * add defaults to menu * fix logic * set default options * remove logs * wip * package.json * fix merge results * fix package-lock * remove debug statements * address CR requests * [MM-22691][Browserview] fix tray icon (#1403) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * make eslint be aware of webpack aliases * some extra disable lines * move badge management to main * remove unneded import * fixing errors * wip * back to having tabs * switch tab working * wip * wip * wip * fix quitting error * back to a working config * configure retries * add darkmode * wip * add error/loading screens * fix settings while removing remote usage * wip * fix lint, get preload to load * remove unused import * wip * menus initially working as they should * update deps, show context menu * wip * wip * wip * fix forward/back menu * fix server menu * allow navigating to external urls in the browser * add defaults to menu * fix logic * set default options * remove logs * wip * fix webpack adding images to /dist so tray can render them * wait for config, fix menutray calls * remove .gitattributes from being tracked * remove unused reject * remove logs * Update webpack.config.renderer.js Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> * Browserview URLHover (#1393) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * make eslint be aware of webpack aliases * some extra disable lines * move badge management to main * remove unneded import * fixing errors * wip * back to having tabs * switch tab working * wip * wip * wip * fix quitting error * back to a working config * configure retries * add darkmode * wip * add error/loading screens * fix settings while removing remote usage * wip * fix lint, get preload to load * remove unused import * wip * menus initially working as they should * update deps, show context menu * wip * wip * wip * fix forward/back menu * fix server menu * allow navigating to external urls in the browser * add defaults to menu * fix logic * set default options * remove logs * wip * wip * wip urlview * wip * urlview when hovering on a link * change how to detect when the mouse hovers * [BrowserView] remove remote usage, fix menus and window buttons in Win (#1418) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * make eslint be aware of webpack aliases * some extra disable lines * move badge management to main * remove unneded import * fixing errors * wip * back to having tabs * switch tab working * wip * wip * wip * fix quitting error * back to a working config * configure retries * add darkmode * wip * add error/loading screens * fix settings while removing remote usage * wip * fix lint, get preload to load * remove unused import * wip * menus initially working as they should * update deps, show context menu * wip * wip * wip * fix forward/back menu * fix server menu * allow navigating to external urls in the browser * add defaults to menu * fix logic * set default options * remove logs * wip * fix webpack adding images to /dist so tray can render them * wait for config, fix menutray calls * remove .gitattributes from being tracked * remove unused reject * remove logs * Update webpack.config.renderer.js Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> * fix three dot menu * remove most remote usage, fix window buttons in Windows Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> * fix different errors when loading config (#1420) * [BrowserView] Native modules & registry access (#1417) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * make eslint be aware of webpack aliases * some extra disable lines * move badge management to main * remove unneded import * fixing errors * wip * back to having tabs * switch tab working * wip * wip * wip * fix quitting error * back to a working config * configure retries * add darkmode * wip * add error/loading screens * fix settings while removing remote usage * wip * fix lint, get preload to load * remove unused import * wip * menus initially working as they should * update deps, show context menu * wip * wip * wip * fix forward/back menu * fix server menu * allow navigating to external urls in the browser * add defaults to menu * fix logic * set default options * remove logs * wip * fix webpack adding images to /dist so tray can render them * wait for config, fix menutray calls * remove .gitattributes from being tracked * restart-working native modules * setup env variables for installing native modules * [browserview] Electron notifications (#1411) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * make eslint be aware of webpack aliases * some extra disable lines * move badge management to main * remove unneded import * fixing errors * wip * back to having tabs * switch tab working * wip * wip * wip * fix quitting error * back to a working config * configure retries * add darkmode * wip * add error/loading screens * fix settings while removing remote usage * wip * fix lint, get preload to load * remove unused import * wip * menus initially working as they should * update deps, show context menu * wip * wip * wip * fix forward/back menu * fix server menu * allow navigating to external urls in the browser * add defaults to menu * fix logic * set default options * remove logs * wip * wip * move viewmanager into windowmanager * working notifications * remove logs, switch tab on notification click * download notifications * fix tray * fix menu switch server * fix error * [MM-23078] TabBar fixes for BrowserView (#1423) * [MM-23078] TabBar fixes for BrowserView * Removing unnecessary logging * [Browserview] 4.6 and 4.7 PRs (#1424) * [MM-28620] allow navigating links to admin_console #1374 * [MM-25789] - Update default settings for new installations #1376 * [MM-27332] show window at autolaunch #1379 * Update NOTICE.txt (#1385) * Update NOTICE.txt * Update NOTICE.txt * Update NOTICE.txt * convert to markdown * md linting * Update NOTICE.md * Revert "Update NOTICE.md" This reverts commit 9381fca895c0677bcad1cf1c1071ca88afd6f486. * Revert "md linting" This reverts commit e7a68f120109d47b9849cf816d4fef79483ad22f. * Revert "convert to markdown" This reverts commit 1e7ed8a67c9c98cd0d0f3ff6cdc70782effb143d. * add missing licenses to joi and jq * Remove devDependencies Co-authored-by: Guillermo Vaya <guivaya@gmail.com> * Notification sounds, also added tab name to notification title * [MM-22013] - Allow users to specify default download locations #1383 * [MM-21835] Use URL instead of the url library #1384 * remove debug console.log statements Co-authored-by: Amy Blais <amy_blais@hotmail.com> * [MM-31266] fix access url when it's not a mm server (#1431) * [MM-31224] fix reloading servers and other tab issues (#1434) * [MM-31224] fix reloading servers and other tab issues * reload if url changes * Change the dev server port to 9001 to avoid conflict with mattermost-minio (#1437) * remove dev_web_server (#1438) * [MM-31225][MM-31217][MM-31219][Browserview] fix linux compilation + other fixes (#1433) * fix linux errors * remove registry, remove env_vars * devtools in separate window, prevent config errors * fix registry path * move dist to root when packaging * make devtools dettached to avoid browserview * remove unneeded comment * use reject in case of registry failure * fix handling results * fix application menu * make linter happy * fix missing key on apt-get (#1440) (#1442) see https://github.com/electron-userland/electron-builder/issues/5485#issuecomment-749244332 * [MM-31221][BrowserView] first modal: adding a server while in a server view (#1400) * reorder code to support webpack * start backend changes * remove simple-spellchecker * wip * first browserview run * settings window routing * wip * back to webpack * working build * back to using electron-builder * fix linting * linting errors missed * back to just 1 config * missing changes * refactor and have the settings in its own page * reminder to restore disabling window.eval * wip * wip * remove old webpack generated files * add assets files * more remove files and fix localurls * wip settings, needs fixing saving prefs * remove linting errors * remove settings as a modal * fix linting * remove view from window on destroy * restore visibility if reloaded * debug log * look for closed windows, remove managers from settings as it is a full window * restore view on configuration save * linting and debug * remove debug message * make eslint be aware of webpack aliases * some extra disable lines * move badge management to main * remove unneded import * fixing errors * wip * back to having tabs * switch tab working * wip * wip * wip * fix quitting error * back to a working config * configure retries * add darkmode * wip * add error/loading screens * fix settings while removing remote usage * wip * fix lint, get preload to load * remove unused import * wip * menus initially working as they should * update deps, show context menu * wip * wip * wip * fix forward/back menu * fix server menu * allow navigating to external urls in the browser * add defaults to menu * fix logic * set default options * remove logs * wip * wip * wip urlview * wip * urlview when hovering on a link * wip * wip * first working modal * fix config loading * upgrade electron to 10.1.5 * esc exits modals * first modal * add env variables for settings and modals devtools * adress CSS review comments * Address review comments * fix dist in prod * fix preload path on build * [MM-31987] Allow camera use for jitsi (#1443) (#1450) * [MM-31987] allow camera use for jitsi * update message for access * [MM-31261] Use manual resizing of BrowserViews on resize, maximize and full-screen (#1449) * [MM-31261] Use manual resizing of BrowserViews on resize, maximize and full-screen * Update src/main/windows/windowManager.js Co-authored-by: Guillermo Vayá <guivaya@gmail.com> Co-authored-by: Guillermo Vayá <guivaya@gmail.com> * add own branch for testing (#1448) * add own branch for testing * remove signing for windows * add message to channel * Bv pipeline elisabeth (#1452) * Add parameter and remove schedule * Add jq * Fix adding jq * Fix adding jq * Fix adding jq * fix quotes * upload as JSON * use previous, parse json * fixes * use json Co-authored-by: Elisabeth Kulzer <elikul@elikul.de> * [MM-30144][MM-30145][MM-30146][MM-30147] Migrate auth and certificate modals to BrowserView (#1445) * WIP * WIP * WIP * WIP * WIP * [MM-30144][MM-30145] Migrate LoginModal and PermissionModal to BrowserView * [MM-30146][MM-30147] Migrate certificate modals to BrowserView * Fixed transparency on the bootstrap modals * PR feedback * Added better error reporting in case the modal promise fails * [MM-31233] Reverse maximize logic typo (#1454) * [browser view] MM-32277 bump version, exe, cache errrors (#1456) * bump version * enable msi and remove src/package* * ensure variable exists * remove cleanCache script * default expansion for env variable * add commit version, missing package-lock.json * remove duplicated command * [MM-31467] Move protocol handling over from original MattermostView into web contents handler (#1453) * WIP * WIP * [MM-31467] Move protocol handling over from original MattermostView into web contents handler * Remove log statement * [MM-32392] prevent crash when checking a URL (#1457) * [MM-31215][MM-31387] Fixes for bad tab navigation and dragging (#1461) * [MM-31387] Send to renderer on clicking server from settings window * Use different event name for sending switch server info to renderer * Have the viewManager let the renderer know when the tab has changed * Couple more fixes around tabs * Simplify URL compare logic * [MM-31650] Restore focus to active server on modal and settings window closure + other fixes (#1455) * [MM-31650] Focus active server on settings window and modal closure * Disable tabs when a modal is open * Revert to using original NewTeamModal component * fix resize (#1462) * [MM-32424] fix server devtools being hidden by browserview (#1459) * [MM-32424] fix server devtools being hidden by browserview * reverse logic * [MM-20227][MM-31388] move to roles and fix focus (#1463) * [MM-31570] update mentions/unreads/session on jewel, tray and dock (#1460) * [MM-32333] Open public links in the user's default browser (#1468) * [MM-32333] Open public links in the user's default browser * Removed commented code * [MM-31232] fix urlview present with no content (#1467) * [MM-31343] Migrate Finder to BrowserView (#1466) * WIP * WIP * WIP * [MM-31343] Migrate Finder to BrowserView * PR feedback * Removing reference to this in non-class file * use electron to handle spellchecking (#1469) * [MM-32382] Use resize event instead of will-resize for monitoring size of BV (#1470) * [MM-32570] Use OpenSans as the font for the URL preview modal (#1471) * [MM-32570] Use OpenSans as the font for the URL preview modal * Don't use bootstrap * Fix draw badge (#1477) * use canvas from window * fix errors * fix errors * safer code injection * [MM-31554] Add listener for config synchronization on the settings window (#1473) * [MM-31554] Add listener for config synchronization on the settings window * Synchronize the config if updated from outside the settings window * [MM-28541] restore deeplinking (#1475) * handle deeplinking * fix app handling deeplinking * remove outdated comment * address review comments * MM-32765_prevent crash on checking unread state (#1479) * MM-31383 make no the default when asking to add a protocol (#1481) * [MM-31340] Resize browser view and show back button when on non-team URL (#1472) * WIP * [MM-31340] Resize browser view and show back button when on non-team URL * Fixed issue where switching tabs and resizing hides the back button * Add error checking around going back in history * [MM-31399] Use webapp ESLint config in desktop app and resolve inconsistencies (#1482) * Import webapp eslint and update packages * FIrst pass with new ruleset * Allow setState * Fix rule for tests * Comment out skippeed tests, removed some TODOs and fixed some warnings * Remove errors from MainPage * Use indenting profile from webapp * Update editorconfig for new indenting * Fix indenting for class properties * Only disable no-console for renderer process and scripts * Remove rule overrides and changes * Fix merge issues * PR feedback and fixed a bad merge * [MM-25122] Use modded version of winreg that supports UTF-8 (#1488) * fix appicon path resolution (#1484) * [MM-33141] Fixed use of bad context in TeamList (#1487) * [MM-33141] Fixed use of bad context in TeamList * Refactor to pull the functions out * Remove unnecessary props * [MM-25355] Throttle notifications for Windows by channel id (#1486) * [MM-25355] Throttle notifications for Windows * Use teamId as well to key the notifications * Merge'd * Use Map instead of Set * [MM-33050] move webcontent events out of main (#1489) * wip * wip * fix webcontent events, move views to its own folder * [MM-33238] Check for admin URL when toggling back bar (#1495) * [MM-31342] fix "save image as" context menu crash (#1490) * [MM-33231] update jewel on new mentions/when read (#1493) * [MM-33231] update state properly for a purecomponent * remove unneeded comment * [MM-33032] Use `hidden` titleBarStyle value to fix macOS Catalina click issue (#1496) * [MM-32809] Remove Toggle Dark Mode menu item for Windows, enable toggling on Linux (#1494) * [MM-32809] Remove Toggle Dark Mode menu item for Windows * Just check for !win32 and !darwin * Enable correct dark mode functionality on non-macOS/non-Windows machines * [MM-33334] Restore keyboard shortcuts for menu items moved to roles (#1499) * [MM-33434] Upgrade to Electron v11, some other dependency upgrades (#1501) * [MM-33434] Upgrade to Electron v11, some other dependency upgrades * Missed a version change * context menu fix * Forgot to remove a log statement * Added resized for redundancy and upgraded to spectron 13 * Don't need resized * [MM-33542] Trigger finder cleanup on pressing close or Escape (#1502) * [MM-33542] Clear the Finder selection when closing the finder * Remove listener on close as well * Run close() on escape as well * [MM-33607] Remove old badge code, update unreads code (#1503) * [MM-33607] Remove old badge code, update unreads code * Fix 2 random lint errors * [MM-33247] Have the app handle links to other teams as a deep link (#1498) * [MM-33373] Trigger the smaller font for 99+ mentions (#1507) * [MM-32805] Merge master, migrate LoadingScreen to BrowserView (#1504) * [MM-467] Notification sounds (#1351) * Custom sounds * Trying new version * Trying new version * Some fixes * Rollback version change * Allow native sound * Increase version * Playing custom sounds :) * Fix var name * Fix * Update src/browser/js/notification.js Co-authored-by: Guillermo Vayá <guivaya@gmail.com> * Update src/browser/js/notification.js Co-authored-by: Guillermo Vayá <guivaya@gmail.com> * Update src/browser/js/notification.js Co-authored-by: Guillermo Vayá <guivaya@gmail.com> * Several suggestions * Update src/browser/js/notification.js Co-authored-by: Guillermo Vayá <guivaya@gmail.com> * Restore of version Co-authored-by: Guillermo Vayá <guivaya@gmail.com> * Clean caches on depcheck failure (#1369) Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * [MM-28595] Open team links within the app (#1373) * [MM-25789] - Update default settings for new installations (#1376) * [MM-25789] - Update default settings for new installations * Update src/main.js Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> * Update src/main.js Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> * Fix linter Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MacBook-Pro-2.local> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> * add Russian language in the list available for spellcheck (#1375) * [MM-28620] allow navigating links to admin_console (#1374) * [MM-28620] allow navigating links to admin_console * Fix when there is not a server associated * [MM-27332] show window at autolaunch (#1379) * Bump to version 4.7.0-develop * Update NOTICE.txt (#1385) * Update NOTICE.txt * Update NOTICE.txt * Update NOTICE.txt * convert to markdown * md linting * Update NOTICE.md * Revert "Update NOTICE.md" This reverts commit 9381fca895c0677bcad1cf1c1071ca88afd6f486. * Revert "md linting" This reverts commit e7a68f120109d47b9849cf816d4fef79483ad22f. * Revert "convert to markdown" This reverts commit 1e7ed8a67c9c98cd0d0f3ff6cdc70782effb143d. * add missing licenses to joi and jq * Remove devDependencies Co-authored-by: Guillermo Vaya <guivaya@gmail.com> * [MM-9922] Hide tooltip for internal links (channels, timestamps, etc.) (#1386) * Hide tooltip for internal links (channels, timestamps, etc.) * Only hide tooltip for internal links on the *current* team * feat(spellcheck): add Ukrainian language for spellcheck (#1382) * [MM-29677] fix download complete notification not appearing (#1388) * fix soundname not existing (#1390) * [MM-29921] fix custom sound not playing when receiving a notification (#1396) * [MM-29921] fix sound notification * remove logs * Update release-process.md (#1394) * [MM-22013] - Allow users to specify default download locations (#1383) * [MM-22013] - Allow users to specify default download locations * PR comments * Add proper config prop * Update src/browser/components/SettingsPage.jsx Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> * Remove string ref * Fix styling * Update styling * Disable input * Add variable for windows * Prevent dialog from opening twice Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MBP-2.fritz.box> Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MacBook-Pro-2.local> Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * [MM-21835] Use URL instead of the url library (#1384) Additionally, migrate all of the URL related helper functions from `src/utils/utils.js` to the new `src/utils/url.js` file and migrate tests. Issue MM-21835 Fixes #1206 * Merge Powershell files together and remove AppVeyor related code * Ensure nodejs deps are met before running script argument directly * [MM-22810] Update loading screen with new design & animation (#1409) * Update loading screen with new design & animation * add prop back in * adjust z-index for tests * tweaks to pass tests * address offline feedback - shrink initial logo size - introduce a slight delay before fading loading spinner out - fix horizontal scrollbar showing on load screen * add missing css variable * no need to remove loading icon * Apply suggestions from code review Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> * Move LoadingScreen.jsx to file-only component * Rename prop for better clarity * Default prop to none and check when needed * Update import paths * Add ESDocs and remove unecessary conditional * Forgot to remove the eslint override Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> * [MM-22960] - Keep desktop app pinned to taskb bar when the app upgrades (#1397) Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MacBook-Pro-2.local> * Bump highlight.js from 9.18.1 to 9.18.5 (#1421) Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 9.18.1 to 9.18.5. - [Release notes](https://github.com/highlightjs/highlight.js/releases) - [Changelog](https://github.com/highlightjs/highlight.js/blob/9.18.5/CHANGES.md) - [Commits](https://github.com/highlightjs/highlight.js/compare/9.18.1...9.18.5) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ini from 1.3.5 to 1.3.7 (#1427) Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix missing key on apt-get (#1440) see https://github.com/electron-userland/electron-builder/issues/5485#issuecomment-749244332 * [MM-31987] Allow camera use for jitsi (#1443) * [MM-31987] allow camera use for jitsi * update message for access * Created codeql analysis (#1441) Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * [MM-31626] bypass gitlab browser-check for oauth login (#1439) * MM-31626 make User Agent configurable by user * add info * remove chrome from UA for gitlab.com * remove previous solution Co-authored-by: Mattermod <mattermod@users.noreply.github.com> * Add Swedish sv-SE (already in simple-spellchecker) (#1483) * Add Swedish sv-SE (already in simple-spellchecker) * Remove spaces in empty lines * Add some sv-SE test for spellchecker Co-authored-by: Peter Johansson <peter.johansson@havochvatten.se> * Add loading screen, fix reload * WIP * Migrate LoadingScreen to BrowserView * Lint fixes * Removed gitlab fix code, also returning null is bad apparently * Fix reload logic Co-authored-by: Rodrigo Villablanca <villa061004@gmail.com> Co-authored-by: Guillermo Vayá <guivaya@gmail.com> Co-authored-by: Juho Nurminen <juho.nurminen@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> Co-authored-by: Nev Angelova <nevy.angelova@gmail.com> Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MacBook-Pro-2.local> Co-authored-by: Eugeny Fomin <github.com@jeka.ru> Co-authored-by: Amy Blais <amy_blais@hotmail.com> Co-authored-by: Nathan Bolender <nathan@nathanbolender.com> Co-authored-by: Dmitriy Danilov <daniloff200@gmail.com> Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MBP-2.fritz.box> Co-authored-by: FalseHonesty <skipboman0@gmail.com> Co-authored-by: William Gathoye <william@gathoye.be> Co-authored-by: Dean Whillier <deanwhillier@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rohitesh Gupta <srkg.gupta@gmail.com> Co-authored-by: petermcj <petermcj@gmail.com> Co-authored-by: Peter Johansson <peter.johansson@havochvatten.se> * [MM-33668] Restore tests to browser-view branch (#1506) * happy eslint * wip * wip * remove aliases * almost working tests * green tests * Revert "remove aliases" This reverts commit 803d3695538197407b45e0d8d30dc429b259b7f3. * add unit test, reconfigure package scripts, make test pass * [MM-33542] Trigger finder cleanup on pressing close or Escape (#1502) * [MM-33542] Clear the Finder selection when closing the finder * Remove listener on close as well * Run close() on escape as well * [MM-33607] Remove old badge code, update unreads code (#1503) * [MM-33607] Remove old badge code, update unreads code * Fix 2 random lint errors * fix script naming in circle * fix check deps * attempt to fix dependency-check download * remove check-deps step Co-authored-by: = <=> Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> * Cleanup of BrowserView migration, some bug fixes (#1509) * 1st round of cleanup * 2nd round of cleanup * Set constant for reload-config * Cleaned up some TODOs * store daily build to S3 (#1508) * store daily build to S3 * missing colon * fix paths * try to keep folders * remove unneeded step * change from arn to bucket name * keep organization consistent * fix indentation * fix indentation x2 Co-authored-by: = <=> * MM-33551 keep tray state between themes (#1511) Co-authored-by: = <=> * Set to version v4.7 Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> Co-authored-by: Amy Blais <amy_blais@hotmail.com> Co-authored-by: Guillermo Vayá <guivaya@gmail.com> Co-authored-by: Elisabeth Kulzer <elikul@elikul.de> Co-authored-by: Rodrigo Villablanca <villa061004@gmail.com> Co-authored-by: Juho Nurminen <juho.nurminen@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Nev Angelova <nevy.angelova@gmail.com> Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MacBook-Pro-2.local> Co-authored-by: Eugeny Fomin <github.com@jeka.ru> Co-authored-by: Nathan Bolender <nathan@nathanbolender.com> Co-authored-by: Dmitriy Danilov <daniloff200@gmail.com> Co-authored-by: Nevyana Angelova <nevyangelova@Nevyanas-MBP-2.fritz.box> Co-authored-by: FalseHonesty <skipboman0@gmail.com> Co-authored-by: William Gathoye <william@gathoye.be> Co-authored-by: Dean Whillier <deanwhillier@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rohitesh Gupta <srkg.gupta@gmail.com> Co-authored-by: petermcj <petermcj@gmail.com> Co-authored-by: Peter Johansson <peter.johansson@havochvatten.se>
This commit is contained in:
56
src/renderer/components/AutoSaveIndicator.jsx
Normal file
56
src/renderer/components/AutoSaveIndicator.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Alert} from 'react-bootstrap';
|
||||
|
||||
const baseClassName = 'AutoSaveIndicator';
|
||||
const leaveClassName = `${baseClassName}-Leave`;
|
||||
|
||||
const SAVING_STATE_SAVING = 'saving';
|
||||
const SAVING_STATE_SAVED = 'saved';
|
||||
const SAVING_STATE_ERROR = 'error';
|
||||
const SAVING_STATE_DONE = 'done';
|
||||
|
||||
function getClassNameAndMessage(savingState, errorMessage) {
|
||||
switch (savingState) {
|
||||
case SAVING_STATE_SAVING:
|
||||
return {className: baseClassName, message: 'Saving...'};
|
||||
case SAVING_STATE_SAVED:
|
||||
return {className: baseClassName, message: 'Saved'};
|
||||
case SAVING_STATE_ERROR:
|
||||
return {className: `${baseClassName}`, message: errorMessage};
|
||||
case SAVING_STATE_DONE:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: 'Saved'};
|
||||
default:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
|
||||
}
|
||||
}
|
||||
|
||||
export default function AutoSaveIndicator(props) {
|
||||
const {savingState, errorMessage, ...rest} = props;
|
||||
const {className, message} = getClassNameAndMessage(savingState, errorMessage);
|
||||
return (
|
||||
<Alert
|
||||
className={className}
|
||||
{...rest}
|
||||
bsStyle={savingState === 'error' ? 'danger' : 'info'}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
AutoSaveIndicator.propTypes = {
|
||||
savingState: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
Object.assign(AutoSaveIndicator, {
|
||||
SAVING_STATE_SAVING,
|
||||
SAVING_STATE_SAVED,
|
||||
SAVING_STATE_ERROR,
|
||||
SAVING_STATE_DONE,
|
||||
});
|
28
src/renderer/components/Button/Button.stories.jsx
Normal file
28
src/renderer/components/Button/Button.stories.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
||||
|
||||
import {action} from '@storybook/addon-actions';
|
||||
import {Button, ButtonToolbar} from 'react-bootstrap';
|
||||
|
||||
storiesOf('Button', module).
|
||||
add('bsStyle', () => (
|
||||
<ButtonToolbar>
|
||||
<Button onClick={action('clicked default')}>{'Default'}</Button>
|
||||
<Button
|
||||
onClick={action('clicked primary')}
|
||||
bsStyle='primary'
|
||||
>{'Primary'}</Button>
|
||||
<Button
|
||||
onClick={action('clicked danger')}
|
||||
bsStyle='danger'
|
||||
>{'Danger'}</Button>
|
||||
<Button
|
||||
onClick={action('clicked link')}
|
||||
bsStyle='link'
|
||||
>{'Link'}</Button>
|
||||
</ButtonToolbar>
|
||||
));
|
45
src/renderer/components/DestructiveConfirmModal.jsx
Normal file
45
src/renderer/components/DestructiveConfirmModal.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Modal} from 'react-bootstrap';
|
||||
|
||||
export default function DestructiveConfirmationModal(props) {
|
||||
const {
|
||||
title,
|
||||
body,
|
||||
acceptLabel,
|
||||
cancelLabel,
|
||||
onAccept,
|
||||
onCancel,
|
||||
...rest} = props;
|
||||
return (
|
||||
<Modal {...rest}>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
{body}
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
bsStyle='link'
|
||||
onClick={onCancel}
|
||||
>{cancelLabel}</Button>
|
||||
<Button
|
||||
bsStyle='danger'
|
||||
onClick={onAccept}
|
||||
>{acceptLabel}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
DestructiveConfirmationModal.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
body: PropTypes.node.isRequired,
|
||||
acceptLabel: PropTypes.string.isRequired,
|
||||
cancelLabel: PropTypes.string.isRequired,
|
||||
onAccept: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
88
src/renderer/components/ErrorView.jsx
Normal file
88
src/renderer/components/ErrorView.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
// ErrorCode: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Grid, Row, Col} from 'react-bootstrap';
|
||||
|
||||
export default function ErrorView(props) {
|
||||
const classNames = ['container', 'ErrorView'];
|
||||
if (!props.active) {
|
||||
classNames.push('ErrorView-hidden');
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
id={props.id}
|
||||
bsClass={classNames.join(' ')}
|
||||
>
|
||||
<div className='ErrorView-table'>
|
||||
<div className='ErrorView-cell'>
|
||||
<Row>
|
||||
<Col
|
||||
xs={0}
|
||||
sm={1}
|
||||
md={1}
|
||||
lg={2}
|
||||
/>
|
||||
<Col
|
||||
xs={12}
|
||||
sm={10}
|
||||
md={10}
|
||||
lg={8}
|
||||
>
|
||||
<h2>{`Cannot connect to ${props.appName}`}</h2>
|
||||
<hr/>
|
||||
<p>{`We're having trouble connecting to ${props.appName}. If refreshing this page (Ctrl+R or Command+R) does not work please verify that:`}</p>
|
||||
<br/>
|
||||
<ul className='ErrorView-bullets' >
|
||||
<li>{'Your computer is connected to the internet.'}</li>
|
||||
<li>{`The ${props.appName} URL `}
|
||||
<a
|
||||
|
||||
//onClick={handleClick}
|
||||
href={props.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{props.url}
|
||||
</a>{' is correct.'}</li>
|
||||
<li>{'You can reach '}
|
||||
<a
|
||||
|
||||
// onClick={handleClick}
|
||||
href={props.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{props.errorInfo.validatedURL}
|
||||
</a>{' from a browser window.'}</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<div className='ErrorView-techInfo'>
|
||||
{props.errorInfo}
|
||||
</div>
|
||||
</Col>
|
||||
<Col
|
||||
xs={0}
|
||||
sm={1}
|
||||
md={1}
|
||||
lg={2}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorView.propTypes = {
|
||||
errorInfo: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
appName: PropTypes.string,
|
||||
};
|
50
src/renderer/components/ExtraBar.jsx
Normal file
50
src/renderer/components/ExtraBar.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Row, Button} from 'react-bootstrap';
|
||||
|
||||
export default class ExtraBar extends React.PureComponent {
|
||||
handleBack = () => {
|
||||
if (this.props.goBack) {
|
||||
this.props.goBack();
|
||||
}
|
||||
}
|
||||
render() {
|
||||
let barClass = 'clear-mode';
|
||||
if (!this.props.show) {
|
||||
barClass = 'hidden';
|
||||
} else if (this.props.darkMode) {
|
||||
barClass = 'dark-mode';
|
||||
}
|
||||
|
||||
return (
|
||||
<Row
|
||||
id={'extra-bar'}
|
||||
className={barClass}
|
||||
>
|
||||
<div
|
||||
className={'container-fluid'}
|
||||
onClick={this.handleBack}
|
||||
>
|
||||
<Button
|
||||
bsStyle={'link'}
|
||||
bsSize={'xsmall'}
|
||||
>
|
||||
<span className={'backIcon fa fa-1x fa-angle-left'}/>
|
||||
<span className={'backLabel'}>
|
||||
{'Back'}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExtraBar.propTypes = {
|
||||
darkMode: PropTypes.bool,
|
||||
goBack: PropTypes.func,
|
||||
show: PropTypes.bool,
|
||||
};
|
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useAnimationEnd from '../../hooks/useAnimationEnd.js';
|
||||
|
||||
import LoadingIcon from './LoadingIcon.jsx';
|
||||
|
||||
const LOADING_STATE = {
|
||||
INITIALIZING: 'initializing', // animation graphics are hidden
|
||||
LOADING: 'loading', // animation graphics fade in and animate
|
||||
LOADED: 'loaded', // animation graphics fade out
|
||||
COMPLETE: 'complete', // animation graphics are removed from the DOM
|
||||
};
|
||||
|
||||
const ANIMATION_COMPLETION_DELAY = 500;
|
||||
|
||||
/**
|
||||
* A function component for rendering the animated MM logo loading sequence
|
||||
* @param {boolean} loading - Prop that indicates whether currently loading or not
|
||||
* @param {boolean} darkMode - Prop that indicates if dark mode is enabled
|
||||
* @param {function} onLoadingAnimationComplete - Callback function to update when internal loading animation is complete
|
||||
*/
|
||||
function LoadingAnimation({
|
||||
loading = false,
|
||||
darkMode = false,
|
||||
onLoadAnimationComplete = null},
|
||||
) {
|
||||
const loadingIconContainerRef = React.useRef(null);
|
||||
const [animationState, setAnimationState] = React.useState(LOADING_STATE.INITIALIZING);
|
||||
const [loadingAnimationComplete, setLoadingAnimationComplete] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (loading) {
|
||||
setAnimationState(LOADING_STATE.LOADING);
|
||||
setLoadingAnimationComplete(false);
|
||||
}
|
||||
|
||||
// in order for the logo animation to fully complete before fading out, the LOADED state is not set until
|
||||
// both the external loaded prop changes back to false and the internal loading animation is complete
|
||||
if (!loading && loadingAnimationComplete) {
|
||||
setAnimationState(LOADING_STATE.LOADED);
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// in order for the logo animation to fully complete before fading out, the LOADED state is not set until
|
||||
// both the external loaded prop goes back to false and the internal loading animation is complete
|
||||
if (!loading && loadingAnimationComplete) {
|
||||
setAnimationState(LOADING_STATE.LOADED);
|
||||
}
|
||||
}, [loadingAnimationComplete]);
|
||||
|
||||
// listen for end of the css logo animation sequence
|
||||
useAnimationEnd(loadingIconContainerRef, () => {
|
||||
setTimeout(() => {
|
||||
setLoadingAnimationComplete(true);
|
||||
}, ANIMATION_COMPLETION_DELAY);
|
||||
}, 'LoadingAnimation__compass-shrink');
|
||||
|
||||
// listen for end of final css logo fade/shrink animation sequence
|
||||
useAnimationEnd(loadingIconContainerRef, () => {
|
||||
if (onLoadAnimationComplete) {
|
||||
onLoadAnimationComplete();
|
||||
}
|
||||
setAnimationState(LOADING_STATE.COMPLETE);
|
||||
}, 'LoadingAnimation__shrink');
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={loadingIconContainerRef}
|
||||
className={classNames('LoadingAnimation', {
|
||||
'LoadingAnimation--darkMode': darkMode,
|
||||
'LoadingAnimation--spinning': animationState !== LOADING_STATE.INITIALIZING && animationState !== LOADING_STATE.COMPLETE,
|
||||
'LoadingAnimation--loading': animationState === LOADING_STATE.LOADING && animationState !== LOADING_STATE.COMPLETE,
|
||||
'LoadingAnimation--loaded': animationState === LOADING_STATE.LOADED && animationState !== LOADING_STATE.COMPLETE,
|
||||
})}
|
||||
>
|
||||
<LoadingIcon/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LoadingAnimation.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
darkMode: PropTypes.bool,
|
||||
onLoadAnimationComplete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LoadingAnimation;
|
197
src/renderer/components/LoadingAnimation/LoadingIcon.jsx
Normal file
197
src/renderer/components/LoadingAnimation/LoadingIcon.jsx
Normal file
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* A function component for inlining SVG code for animation logo loader
|
||||
*/
|
||||
function LoadingAnimation() {
|
||||
return (
|
||||
<svg
|
||||
width='104'
|
||||
height='104'
|
||||
viewBox='0 0 104 104'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id='LoadingAnimation__spinner-gradient'
|
||||
x1='0%'
|
||||
y1='72px'
|
||||
x2='0%'
|
||||
y2='32px'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop
|
||||
offset='0'
|
||||
className='LoadingAnimation__spinner-gradient-color'
|
||||
stopOpacity='1'
|
||||
/>
|
||||
<stop
|
||||
offset='1'
|
||||
className='LoadingAnimation__spinner-gradient-color'
|
||||
stopOpacity='0'
|
||||
/>
|
||||
</linearGradient>
|
||||
<mask id='LoadingAnimation__base-wipe-mask'>
|
||||
<rect
|
||||
x='0'
|
||||
y='0'
|
||||
width='104'
|
||||
height='104'
|
||||
fill='white'
|
||||
/>
|
||||
<g className='LoadingAnimation__compass-base-mask-container'>
|
||||
<circle
|
||||
className='LoadingAnimation__compass-base-mask'
|
||||
r='27'
|
||||
cx='52'
|
||||
cy='52'
|
||||
fill='white'
|
||||
stroke='black'
|
||||
strokeWidth='54'
|
||||
/>
|
||||
</g>
|
||||
</mask>
|
||||
<mask id='LoadingAnimation__base-mask'>
|
||||
<rect
|
||||
x='0'
|
||||
y='0'
|
||||
width='104'
|
||||
height='104'
|
||||
fill='white'
|
||||
/>
|
||||
<circle
|
||||
r='37'
|
||||
cx='54'
|
||||
cy='46'
|
||||
fill='black'
|
||||
/>
|
||||
<g className='LoadingAnimation__compass-needle-behind-mask'>
|
||||
<g transform='translate(54,46)'>
|
||||
<g transform='translate(-29, -61.3)'>
|
||||
<path
|
||||
d='M38.5984 0C45.476 1.07762 51.9794 3.28918 57.9108 6.43722V61.1566C57.9108 77.1373 44.9364 90.1119 28.9554 90.1119C12.9744 90.1119 0 77.1373 0 61.1566C0 55.3848 1.69443 50.0063 4.60763 45.4861L38.5984 0Z'
|
||||
fill='black'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g className='LoadingAnimation__compass-needle-front-mask'>
|
||||
<g transform='translate(54,46)'>
|
||||
<g transform='translate(-29,-61.3)'>
|
||||
<path
|
||||
d='M38.5984 0C45.476 1.07762 51.9794 3.28918 57.9108 6.43722V61.1566C57.9108 77.1373 44.9364 90.1119 28.9554 90.1119C12.9744 90.1119 0 77.1373 0 61.1566C0 55.3848 1.69443 50.0063 4.60763 45.4861L38.5984 0Z'
|
||||
fill='black'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<mask id='LoadingAnimation__spinner-left-half-mask'>
|
||||
<rect
|
||||
x='0'
|
||||
y='0'
|
||||
width='52'
|
||||
height='104'
|
||||
fill='white'
|
||||
/>
|
||||
<circle
|
||||
className='LoadingAnimation__spinner-mask'
|
||||
r='20'
|
||||
cx='52'
|
||||
cy='52'
|
||||
fill='black'
|
||||
/>
|
||||
</mask>
|
||||
<mask id='LoadingAnimation__spinner-right-half-mask'>
|
||||
<rect
|
||||
x='52'
|
||||
y='0'
|
||||
width='52'
|
||||
height='104'
|
||||
fill='white'
|
||||
/>
|
||||
<circle
|
||||
className='LoadingAnimation__spinner-mask'
|
||||
r='20'
|
||||
cx='52'
|
||||
cy='52'
|
||||
fill='black'
|
||||
/>
|
||||
</mask>
|
||||
<mask id='LoadingAnimation__spinner-wipe-mask'>
|
||||
<rect
|
||||
x='0'
|
||||
y='0'
|
||||
width='104'
|
||||
height='104'
|
||||
fill='white'
|
||||
/>
|
||||
<g className='LoadingAnimation__spinner-mask-container'>
|
||||
<circle
|
||||
className='LoadingAnimation__spinner-mask'
|
||||
r='27'
|
||||
cx='52'
|
||||
cy='52'
|
||||
fill='black'
|
||||
stroke='white'
|
||||
strokeWidth='54'
|
||||
/>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<g
|
||||
className='LoadingAnimation__spinner-container'
|
||||
mask='url(#LoadingAnimation__spinner-wipe-mask)'
|
||||
>
|
||||
<g className='LoadingAnimation__spinner'>
|
||||
<circle
|
||||
r='25'
|
||||
cx='52'
|
||||
cy='52'
|
||||
fill='currentColor'
|
||||
mask='url(#LoadingAnimation__spinner-left-half-mask)'
|
||||
/>
|
||||
<circle
|
||||
r='25'
|
||||
cx='52'
|
||||
cy='52'
|
||||
fill='url(#LoadingAnimation__spinner-gradient)'
|
||||
mask='url(#LoadingAnimation__spinner-right-half-mask)'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g className='LoadingAnimation__compass'>
|
||||
<g
|
||||
className='LoadingAnimation__compass-base-container'
|
||||
mask='url(#LoadingAnimation__base-wipe-mask)'
|
||||
>
|
||||
<circle
|
||||
className='LoadingAnimation__compass-base'
|
||||
r='52'
|
||||
cx='52'
|
||||
cy='52'
|
||||
fill='currentColor'
|
||||
mask='url(#LoadingAnimation__base-mask)'
|
||||
/>
|
||||
</g>
|
||||
<g className='LoadingAnimation__compass-needle-container'>
|
||||
<g className='LoadingAnimation__compass-needle'>
|
||||
<g transform='translate(54,46)'>
|
||||
<g transform='translate(-15,-42)'>
|
||||
<path
|
||||
d='M29.9539 1.4977C29.9539 0.670968 29.2827 0 28.4562 0C27.9597 0 27.5192 0.242028 27.2468 0.614415C27.216 0.656555 27.1873 0.700359 27.1609 0.745666L3.66519 32.1191C1.38202 34.7479 0 38.1803 0 41.9355C0 50.207 6.70541 56.9124 14.977 56.9124C23.2485 56.9124 29.9539 50.207 29.9539 41.9355L29.9539 41.9013V1.50252C29.9539 1.50091 29.9539 1.49931 29.9539 1.4977Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingAnimation;
|
4
src/renderer/components/LoadingAnimation/index.js
Normal file
4
src/renderer/components/LoadingAnimation/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './LoadingAnimation.jsx';
|
78
src/renderer/components/LoadingScreen.jsx
Normal file
78
src/renderer/components/LoadingScreen.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useTransitionEnd from '../hooks/useTransitionEnd.js';
|
||||
|
||||
import LoadingAnimation from './LoadingAnimation';
|
||||
|
||||
/**
|
||||
* A function component for rendering the desktop app loading screen
|
||||
* @param {boolean} loading - Prop that indicates whether currently loading or not
|
||||
* @param {boolean} darkMode - Prop that indicates if dark mode is enabled
|
||||
* @param {() => void} onFadeOutComplete - Function to call when the loading animation is completely finished
|
||||
*/
|
||||
function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = () => null}) {
|
||||
const loadingScreenRef = React.useRef(null);
|
||||
|
||||
const [loadingIsComplete, setLoadingIsComplete] = React.useState(true);
|
||||
const [loadAnimationIsComplete, setLoadAnimationIsComplete] = React.useState(true);
|
||||
const [fadeOutIsComplete, setFadeOutIsComplete] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
// reset internal state if loading restarts
|
||||
if (loading) {
|
||||
resetState();
|
||||
} else {
|
||||
setLoadingIsComplete(true);
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
function handleLoadAnimationComplete() {
|
||||
setLoadAnimationIsComplete(true);
|
||||
}
|
||||
|
||||
useTransitionEnd(loadingScreenRef, React.useCallback(() => {
|
||||
setFadeOutIsComplete(true);
|
||||
onFadeOutComplete();
|
||||
}), ['opacity']);
|
||||
|
||||
function loadingInProgress() {
|
||||
return !(loadingIsComplete && loadAnimationIsComplete && fadeOutIsComplete);
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
setLoadingIsComplete(false);
|
||||
setLoadAnimationIsComplete(false);
|
||||
setFadeOutIsComplete(false);
|
||||
}
|
||||
|
||||
const loadingScreen = (
|
||||
<div
|
||||
ref={loadingScreenRef}
|
||||
className={classNames('LoadingScreen', {
|
||||
'LoadingScreen--darkMode': darkMode,
|
||||
'LoadingScreen--loaded': loadingIsComplete && loadAnimationIsComplete,
|
||||
})}
|
||||
>
|
||||
<LoadingAnimation
|
||||
loading={loading}
|
||||
darkMode={darkMode}
|
||||
onLoadAnimationComplete={handleLoadAnimationComplete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return loadingInProgress() ? loadingScreen : null;
|
||||
}
|
||||
|
||||
LoadingScreen.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
darkMode: PropTypes.bool,
|
||||
onFadeOutComplete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
453
src/renderer/components/MainPage.jsx
Normal file
453
src/renderer/components/MainPage.jsx
Normal file
@@ -0,0 +1,453 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import os from 'os';
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Grid, Row} from 'react-bootstrap';
|
||||
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
|
||||
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import {
|
||||
FOCUS_BROWSERVIEW,
|
||||
MAXIMIZE_CHANGE,
|
||||
DARK_MODE_CHANGE,
|
||||
HISTORY,
|
||||
LOAD_RETRY,
|
||||
LOAD_SUCCESS,
|
||||
LOAD_FAILED,
|
||||
SHOW_NEW_SERVER_MODAL,
|
||||
SWITCH_SERVER,
|
||||
WINDOW_CLOSE,
|
||||
WINDOW_MINIMIZE,
|
||||
WINDOW_RESTORE,
|
||||
WINDOW_MAXIMIZE,
|
||||
DOUBLE_CLICK_ON_WINDOW,
|
||||
PLAY_SOUND,
|
||||
MODAL_OPEN,
|
||||
MODAL_CLOSE,
|
||||
SET_SERVER_KEY,
|
||||
UPDATE_MENTIONS,
|
||||
TOGGLE_BACK_BUTTON,
|
||||
SELECT_NEXT_TAB,
|
||||
SELECT_PREVIOUS_TAB,
|
||||
ADD_SERVER,
|
||||
FOCUS_THREE_DOT_MENU,
|
||||
} from 'common/communication';
|
||||
|
||||
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
|
||||
import maximizeButton from '../../assets/titlebar/chrome-maximize.svg';
|
||||
import minimizeButton from '../../assets/titlebar/chrome-minimize.svg';
|
||||
import closeButton from '../../assets/titlebar/chrome-close.svg';
|
||||
|
||||
import {playSound} from '../notificationSounds';
|
||||
|
||||
import TabBar from './TabBar.jsx';
|
||||
import ExtraBar from './ExtraBar.jsx';
|
||||
import ErrorView from './ErrorView.jsx';
|
||||
|
||||
const LOADING = 1;
|
||||
const DONE = 2;
|
||||
const RETRY = -1;
|
||||
const FAILED = 0;
|
||||
const NOSERVERS = -2;
|
||||
|
||||
export default class MainPage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.topBar = React.createRef();
|
||||
this.threeDotMenu = React.createRef();
|
||||
|
||||
this.state = {
|
||||
key: this.props.teams.findIndex((team) => team.order === 0),
|
||||
sessionsExpired: {},
|
||||
unreadCounts: {},
|
||||
mentionCounts: {},
|
||||
targetURL: '',
|
||||
maximized: false,
|
||||
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: LOADING, extra: null}])),
|
||||
darkMode: this.props.darkMode,
|
||||
};
|
||||
}
|
||||
|
||||
getTabStatus() {
|
||||
if (this.props.teams.length) {
|
||||
const tab = this.props.teams[this.state.key];
|
||||
if (tab) {
|
||||
const tabname = tab.name;
|
||||
return this.state.tabStatus.get(tabname);
|
||||
}
|
||||
}
|
||||
return {status: NOSERVERS};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// set page on retry
|
||||
ipcRenderer.on(LOAD_RETRY, (_, server, retry, err, loadUrl) => {
|
||||
console.log(`${server}: failed to load ${err}, but retrying`);
|
||||
const status = this.state.tabStatus;
|
||||
const statusValue = {
|
||||
status: RETRY,
|
||||
extra: {
|
||||
retry,
|
||||
error: err,
|
||||
url: loadUrl,
|
||||
},
|
||||
};
|
||||
status.set(server, statusValue);
|
||||
this.setState({tabStatus: status});
|
||||
});
|
||||
|
||||
ipcRenderer.on(LOAD_SUCCESS, (_, server) => {
|
||||
const status = this.state.tabStatus;
|
||||
status.set(server, {status: DONE});
|
||||
this.setState({tabStatus: status});
|
||||
});
|
||||
|
||||
ipcRenderer.on(LOAD_FAILED, (_, server, err, loadUrl) => {
|
||||
console.log(`${server}: failed to load ${err}`);
|
||||
const status = this.state.tabStatus;
|
||||
const statusValue = {
|
||||
status: FAILED,
|
||||
extra: {
|
||||
error: err,
|
||||
url: loadUrl,
|
||||
},
|
||||
};
|
||||
status.set(server, statusValue);
|
||||
this.setState({tabStatus: status});
|
||||
});
|
||||
|
||||
ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => {
|
||||
this.setState({darkMode});
|
||||
});
|
||||
|
||||
// can't switch tabs sequentially for some reason...
|
||||
ipcRenderer.on(SET_SERVER_KEY, (event, key) => {
|
||||
const nextIndex = this.props.teams.findIndex((team) => team.order === key);
|
||||
this.handleSetServerKey(nextIndex);
|
||||
});
|
||||
ipcRenderer.on(SELECT_NEXT_TAB, () => {
|
||||
const currentOrder = this.props.teams[this.state.key].order;
|
||||
const nextOrder = ((currentOrder + 1) % this.props.teams.length);
|
||||
const nextIndex = this.props.teams.findIndex((team) => team.order === nextOrder);
|
||||
const team = this.props.teams[nextIndex];
|
||||
this.handleSelect(team.name, nextIndex);
|
||||
});
|
||||
|
||||
ipcRenderer.on(SELECT_PREVIOUS_TAB, () => {
|
||||
const currentOrder = this.props.teams[this.state.key].order;
|
||||
|
||||
// js modulo operator returns a negative number if result is negative, so we have to ensure it's positive
|
||||
const nextOrder = ((this.props.teams.length + (currentOrder - 1)) % this.props.teams.length);
|
||||
const nextIndex = this.props.teams.findIndex((team) => team.order === nextOrder);
|
||||
const team = this.props.teams[nextIndex];
|
||||
this.handleSelect(team.name, nextIndex);
|
||||
});
|
||||
|
||||
ipcRenderer.on(MAXIMIZE_CHANGE, this.handleMaximizeState);
|
||||
|
||||
ipcRenderer.on('enter-full-screen', () => this.handleFullScreenState(true));
|
||||
ipcRenderer.on('leave-full-screen', () => this.handleFullScreenState(false));
|
||||
|
||||
ipcRenderer.on(ADD_SERVER, () => {
|
||||
this.addServer();
|
||||
});
|
||||
|
||||
ipcRenderer.on(PLAY_SOUND, (_event, soundName) => {
|
||||
playSound(soundName);
|
||||
});
|
||||
|
||||
ipcRenderer.on(MODAL_OPEN, () => {
|
||||
this.setState({modalOpen: true});
|
||||
});
|
||||
|
||||
ipcRenderer.on(MODAL_CLOSE, () => {
|
||||
this.setState({modalOpen: false});
|
||||
});
|
||||
|
||||
ipcRenderer.on(TOGGLE_BACK_BUTTON, (event, showExtraBar) => {
|
||||
this.setState({showExtraBar});
|
||||
});
|
||||
|
||||
ipcRenderer.on(UPDATE_MENTIONS, (_event, team, mentions, unreads, isExpired) => {
|
||||
const key = this.props.teams.findIndex((server) => server.name === team);
|
||||
const {unreadCounts, mentionCounts, sessionsExpired} = this.state;
|
||||
|
||||
const newMentionCounts = {...mentionCounts};
|
||||
newMentionCounts[key] = mentions || 0;
|
||||
|
||||
const newUnreads = {...unreadCounts};
|
||||
newUnreads[key] = unreads || false;
|
||||
|
||||
const expired = {...sessionsExpired};
|
||||
expired[key] = isExpired || false;
|
||||
|
||||
this.setState({unreadCounts: newUnreads, mentionCounts: newMentionCounts, sessionsExpired: expired});
|
||||
});
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
ipcRenderer.on(FOCUS_THREE_DOT_MENU, () => {
|
||||
if (this.threeDotMenu.current) {
|
||||
this.threeDotMenu.current.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleMaximizeState = (_, maximized) => {
|
||||
this.setState({maximized});
|
||||
}
|
||||
|
||||
handleFullScreenState = (isFullScreen) => {
|
||||
this.setState({fullScreen: isFullScreen});
|
||||
}
|
||||
|
||||
handleSetServerKey = (key) => {
|
||||
const newKey = (this.props.teams.length + key) % this.props.teams.length;
|
||||
this.setState({key: newKey});
|
||||
}
|
||||
|
||||
handleSelect = (name, key) => {
|
||||
ipcRenderer.send(SWITCH_SERVER, name);
|
||||
this.handleSetServerKey(key);
|
||||
}
|
||||
|
||||
handleDragAndDrop = async (dropResult) => {
|
||||
const {removedIndex, addedIndex} = dropResult;
|
||||
if (removedIndex !== addedIndex) {
|
||||
const teamIndex = await this.props.moveTabs(removedIndex, addedIndex < this.props.teams.length ? addedIndex : this.props.teams.length - 1);
|
||||
const name = this.props.teams[teamIndex].name;
|
||||
this.handleSelect(name, teamIndex);
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = (e) => {
|
||||
e.stopPropagation(); // since it is our button, the event goes into MainPage's onclick event, getting focus back.
|
||||
ipcRenderer.send(WINDOW_CLOSE);
|
||||
}
|
||||
|
||||
handleMinimize = (e) => {
|
||||
e.stopPropagation();
|
||||
ipcRenderer.send(WINDOW_MINIMIZE);
|
||||
}
|
||||
|
||||
handleMaximize = (e) => {
|
||||
e.stopPropagation();
|
||||
ipcRenderer.send(WINDOW_MAXIMIZE);
|
||||
}
|
||||
|
||||
handleRestore = () => {
|
||||
ipcRenderer.send(WINDOW_RESTORE);
|
||||
}
|
||||
|
||||
openMenu = () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
this.threeDotMenu.current.blur();
|
||||
}
|
||||
this.props.openMenu();
|
||||
}
|
||||
|
||||
handleDoubleClick = () => {
|
||||
ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW);
|
||||
}
|
||||
|
||||
addServer = () => {
|
||||
ipcRenderer.send(SHOW_NEW_SERVER_MODAL);
|
||||
}
|
||||
|
||||
focusOnWebView = () => {
|
||||
ipcRenderer.send(FOCUS_BROWSERVIEW);
|
||||
}
|
||||
|
||||
setInputRef = (ref) => {
|
||||
this.inputRef = ref;
|
||||
}
|
||||
|
||||
render() {
|
||||
const tabsRow = (
|
||||
<TabBar
|
||||
id='tabBar'
|
||||
isDarkMode={this.state.darkMode}
|
||||
teams={this.props.teams}
|
||||
sessionsExpired={this.state.sessionsExpired}
|
||||
unreadCounts={this.state.unreadCounts}
|
||||
mentionCounts={this.state.mentionCounts}
|
||||
activeKey={this.state.key}
|
||||
onSelect={this.handleSelect}
|
||||
onAddServer={this.addServer}
|
||||
showAddServerButton={this.props.showAddServerButton}
|
||||
onDrop={this.handleDragAndDrop}
|
||||
tabsDisabled={this.state.modalOpen}
|
||||
/>
|
||||
);
|
||||
|
||||
let topBarClassName = 'topBar';
|
||||
if (process.platform === 'darwin') {
|
||||
topBarClassName += ' macOS';
|
||||
}
|
||||
if (this.state.darkMode) {
|
||||
topBarClassName += ' darkMode';
|
||||
}
|
||||
if (this.state.fullScreen) {
|
||||
topBarClassName += ' fullScreen';
|
||||
}
|
||||
|
||||
let maxButton;
|
||||
if (this.state.maximized) {
|
||||
maxButton = (
|
||||
<div
|
||||
className='button restore-button'
|
||||
onClick={this.handleRestore}
|
||||
>
|
||||
<img src={restoreButton}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
maxButton = (
|
||||
<div
|
||||
className='button max-button'
|
||||
onClick={this.handleMaximize}
|
||||
>
|
||||
<img src={maximizeButton}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let overlayGradient;
|
||||
if (process.platform !== 'darwin') {
|
||||
overlayGradient = (
|
||||
<span className='overlay-gradient'/>
|
||||
);
|
||||
}
|
||||
|
||||
let titleBarButtons;
|
||||
if (os.platform() === 'win32' && os.release().startsWith('10')) {
|
||||
titleBarButtons = (
|
||||
<span className='title-bar-btns'>
|
||||
<div
|
||||
className='button min-button'
|
||||
onClick={this.handleMinimize}
|
||||
>
|
||||
<img src={minimizeButton}/>
|
||||
</div>
|
||||
{maxButton}
|
||||
<div
|
||||
className='button close-button'
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<img src={closeButton}/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const topRow = (
|
||||
<Row
|
||||
className={topBarClassName}
|
||||
onDoubleClick={this.handleDoubleClick}
|
||||
>
|
||||
<div
|
||||
ref={this.topBar}
|
||||
className={`topBar-bg${this.state.unfocused ? ' unfocused' : ''}`}
|
||||
>
|
||||
<button
|
||||
className='three-dot-menu'
|
||||
onClick={this.openMenu}
|
||||
tabIndex={0}
|
||||
ref={this.threeDotMenu}
|
||||
aria-label='Context menu'
|
||||
>
|
||||
<DotsVerticalIcon/>
|
||||
</button>
|
||||
{tabsRow}
|
||||
{overlayGradient}
|
||||
{titleBarButtons}
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const views = () => {
|
||||
let component;
|
||||
const tabStatus = this.getTabStatus();
|
||||
if (!tabStatus) {
|
||||
const tab = this.props.teams[this.state.key];
|
||||
if (tab) {
|
||||
console.error(`Not tabStatus for ${this.props.teams[this.state.key].name}`);
|
||||
} else {
|
||||
console.error('No tab status, tab doesn\'t exist anymore');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
switch (tabStatus.status) {
|
||||
case NOSERVERS: // TODO: substitute with https://mattermost.atlassian.net/browse/MM-25003
|
||||
component = (
|
||||
<ErrorView
|
||||
id={'NoServers'}
|
||||
className='errorView'
|
||||
errorInfo={'No Servers configured'}
|
||||
url={tabStatus.extra ? tabStatus.extra.url : ''}
|
||||
active={true}
|
||||
retry={null}
|
||||
appName={this.props.appName}
|
||||
/>);
|
||||
break;
|
||||
case FAILED:
|
||||
component = (
|
||||
<ErrorView
|
||||
id={this.state.key + '-fail'}
|
||||
className='errorView'
|
||||
errorInfo={tabStatus.extra ? tabStatus.extra.error : null}
|
||||
url={tabStatus.extra ? tabStatus.extra.url : ''}
|
||||
active={true}
|
||||
appName={this.props.appName}
|
||||
/>);
|
||||
break;
|
||||
case LOADING:
|
||||
case RETRY:
|
||||
case DONE:
|
||||
component = null;
|
||||
}
|
||||
return component;
|
||||
};
|
||||
|
||||
const viewsRow = (
|
||||
<Fragment>
|
||||
<ExtraBar
|
||||
darkMode={this.state.darkMode}
|
||||
show={this.state.showExtraBar}
|
||||
goBack={() => {
|
||||
ipcRenderer.send(HISTORY, -1);
|
||||
}}
|
||||
/>
|
||||
<Row>
|
||||
{views()}
|
||||
</Row>
|
||||
</Fragment>);
|
||||
|
||||
return (
|
||||
<div
|
||||
className='MainPage'
|
||||
onClick={this.focusOnWebView}
|
||||
>
|
||||
<Grid fluid={true}>
|
||||
{topRow}
|
||||
{viewsRow}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MainPage.propTypes = {
|
||||
teams: PropTypes.array.isRequired,
|
||||
showAddServerButton: PropTypes.bool.isRequired,
|
||||
moveTabs: PropTypes.func.isRequired,
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
darkMode: PropTypes.bool.isRequired,
|
||||
appName: PropTypes.string.isRequired,
|
||||
};
|
244
src/renderer/components/NewTeamModal.jsx
Normal file
244
src/renderer/components/NewTeamModal.jsx
Normal file
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
export default class NewTeamModal extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
restoreFocus: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.wasShown = false;
|
||||
this.state = {
|
||||
teamName: '',
|
||||
teamUrl: '',
|
||||
teamOrder: props.currentOrder || 0,
|
||||
saveStarted: false,
|
||||
};
|
||||
}
|
||||
|
||||
initializeOnShow() {
|
||||
this.setState({
|
||||
teamName: this.props.team ? this.props.team.name : '',
|
||||
teamUrl: this.props.team ? this.props.team.url : '',
|
||||
teamIndex: this.props.team ? this.props.team.index : false,
|
||||
teamOrder: this.props.team ? this.props.team.order : (this.props.currentOrder || 0),
|
||||
saveStarted: false,
|
||||
});
|
||||
}
|
||||
|
||||
getTeamNameValidationError() {
|
||||
if (!this.state.saveStarted) {
|
||||
return null;
|
||||
}
|
||||
return this.state.teamName.length > 0 ? null : 'Name is required.';
|
||||
}
|
||||
|
||||
getTeamNameValidationState() {
|
||||
return this.getTeamNameValidationError() === null ? null : 'error';
|
||||
}
|
||||
|
||||
handleTeamNameChange = (e) => {
|
||||
this.setState({
|
||||
teamName: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
getTeamUrlValidationError() {
|
||||
if (!this.state.saveStarted) {
|
||||
return null;
|
||||
}
|
||||
if (this.state.teamUrl.length === 0) {
|
||||
return 'URL is required.';
|
||||
}
|
||||
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) {
|
||||
return 'URL should start with http:// or https://.';
|
||||
}
|
||||
if (!urlUtils.isValidURL(this.state.teamUrl.trim())) {
|
||||
return 'URL is not formatted correctly.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getTeamUrlValidationState() {
|
||||
return this.getTeamUrlValidationError() === null ? null : 'error';
|
||||
}
|
||||
|
||||
handleTeamUrlChange = (e) => {
|
||||
this.setState({
|
||||
teamUrl: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
getError() {
|
||||
const nameError = this.getTeamNameValidationError();
|
||||
const urlError = this.getTeamUrlValidationError();
|
||||
|
||||
if (nameError && urlError) {
|
||||
return 'Name and URL are required.';
|
||||
} else if (nameError) {
|
||||
return nameError;
|
||||
} else if (urlError) {
|
||||
return urlError;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
return this.getTeamNameValidationState() === null &&
|
||||
this.getTeamUrlValidationState() === null;
|
||||
}
|
||||
|
||||
save = () => {
|
||||
this.setState({
|
||||
saveStarted: true,
|
||||
}, () => {
|
||||
if (this.validateForm()) {
|
||||
this.props.onSave({
|
||||
url: this.state.teamUrl,
|
||||
name: this.state.teamName,
|
||||
index: this.state.teamIndex,
|
||||
order: this.state.teamOrder,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSaveButtonLabel() {
|
||||
if (this.props.editMode) {
|
||||
return 'Save';
|
||||
}
|
||||
return 'Add';
|
||||
}
|
||||
|
||||
getModalTitle() {
|
||||
if (this.props.editMode) {
|
||||
return 'Edit Server';
|
||||
}
|
||||
return 'Add Server';
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.wasShown !== this.props.show && this.props.show) {
|
||||
this.initializeOnShow();
|
||||
}
|
||||
this.wasShown = this.props.show;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='NewTeamModal'
|
||||
show={this.props.show}
|
||||
id='newServerModal'
|
||||
enforceFocus={true}
|
||||
onEntered={() => this.teamNameInputRef.focus()}
|
||||
onHide={this.props.onClose}
|
||||
restoreFocus={this.props.restoreFocus}
|
||||
onKeyDown={(e) => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
this.save();
|
||||
|
||||
// The add button from behind this might still be focused
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{this.getModalTitle()}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<form>
|
||||
<FormGroup
|
||||
validationState={this.getTeamNameValidationState()}
|
||||
>
|
||||
<ControlLabel>{'Server Display Name'}</ControlLabel>
|
||||
<FormControl
|
||||
id='teamNameInput'
|
||||
type='text'
|
||||
value={this.state.teamName}
|
||||
placeholder='Server Name'
|
||||
onChange={this.handleTeamNameChange}
|
||||
inputRef={(ref) => {
|
||||
this.teamNameInputRef = ref;
|
||||
if (this.props.setInputRef) {
|
||||
this.props.setInputRef(ref);
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<FormControl.Feedback/>
|
||||
<HelpBlock>{'The name of the server displayed on your desktop app tab bar.'}</HelpBlock>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
className='NewTeamModal-noBottomSpace'
|
||||
validationState={this.getTeamUrlValidationState()}
|
||||
>
|
||||
<ControlLabel>{'Server URL'}</ControlLabel>
|
||||
<FormControl
|
||||
id='teamUrlInput'
|
||||
type='text'
|
||||
value={this.state.teamUrl}
|
||||
placeholder='https://example.com'
|
||||
onChange={this.handleTeamUrlChange}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
<FormControl.Feedback/>
|
||||
<HelpBlock className='NewTeamModal-noBottomSpace'>{'The URL of your Mattermost server. Must start with http:// or https://.'}</HelpBlock>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<div
|
||||
className='pull-left modal-error'
|
||||
>
|
||||
{this.getError()}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
id='cancelNewServerModal'
|
||||
onClick={this.props.onClose}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
id='saveNewServerModal'
|
||||
onClick={this.save}
|
||||
disabled={!this.validateForm()}
|
||||
bsStyle='primary'
|
||||
>{this.getSaveButtonLabel()}</Button>
|
||||
</Modal.Footer>
|
||||
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewTeamModal.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
team: PropTypes.object,
|
||||
editMode: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
restoreFocus: PropTypes.bool,
|
||||
currentOrder: PropTypes.number,
|
||||
setInputRef: PropTypes.func,
|
||||
};
|
36
src/renderer/components/RemoveServerModal.jsx
Normal file
36
src/renderer/components/RemoveServerModal.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import DestructiveConfirmationModal from './DestructiveConfirmModal.jsx';
|
||||
|
||||
export default function RemoveServerModal(props) {
|
||||
const {serverName, ...rest} = props;
|
||||
return (
|
||||
<DestructiveConfirmationModal
|
||||
{...rest}
|
||||
title='Remove Server'
|
||||
acceptLabel='Remove'
|
||||
cancelLabel='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.'}
|
||||
</p>
|
||||
<p>
|
||||
{'Confirm you wish to remove the '}<strong>{serverName}</strong>{' server?'}
|
||||
</p>
|
||||
</Modal.Body>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
RemoveServerModal.propTypes = {
|
||||
serverName: PropTypes.string.isRequired,
|
||||
};
|
723
src/renderer/components/SettingsPage.jsx
Normal file
723
src/renderer/components/SettingsPage.jsx
Normal file
@@ -0,0 +1,723 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import 'renderer/css/settings.css';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row, Button} from 'react-bootstrap';
|
||||
|
||||
import {ipcRenderer} from 'electron';
|
||||
import {debounce} from 'underscore';
|
||||
|
||||
import {GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, DOUBLE_CLICK_ON_WINDOW, GET_DOWNLOAD_LOCATION, SWITCH_SERVER, ADD_SERVER, RELOAD_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import TeamList from './TeamList.jsx';
|
||||
import AutoSaveIndicator from './AutoSaveIndicator.jsx';
|
||||
|
||||
const CONFIG_TYPE_SERVERS = 'servers';
|
||||
const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
|
||||
|
||||
function backToIndex(serverName) {
|
||||
ipcRenderer.send(SWITCH_SERVER, serverName);
|
||||
window.close();
|
||||
}
|
||||
|
||||
export default class SettingsPage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ready: false,
|
||||
teams: [],
|
||||
showAddTeamForm: false,
|
||||
savingState: {
|
||||
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
servers: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
},
|
||||
userOpenedDownloadDialog: false,
|
||||
};
|
||||
|
||||
this.getConfig();
|
||||
this.trayIconThemeRef = React.createRef();
|
||||
this.downloadLocationRef = React.createRef();
|
||||
this.showTrayIconRef = React.createRef();
|
||||
this.autostartRef = React.createRef();
|
||||
this.minimizeToTrayRef = React.createRef();
|
||||
this.flashWindowRef = React.createRef();
|
||||
this.bounceIconRef = React.createRef();
|
||||
this.showUnreadBadgeRef = React.createRef();
|
||||
this.useSpellCheckerRef = React.createRef();
|
||||
this.enableHardwareAccelerationRef = React.createRef();
|
||||
|
||||
this.saveQueue = [];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ipcRenderer.on(ADD_SERVER, () => {
|
||||
this.setState({
|
||||
showAddTeamForm: true,
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on(RELOAD_CONFIGURATION, () => {
|
||||
this.updateSaveState();
|
||||
this.getConfig();
|
||||
});
|
||||
}
|
||||
|
||||
getConfig = () => {
|
||||
ipcRenderer.invoke(GET_LOCAL_CONFIGURATION).then((config) => {
|
||||
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config)});
|
||||
});
|
||||
}
|
||||
|
||||
convertConfigDataToState = (configData, currentState = {}) => {
|
||||
const newState = Object.assign({}, configData);
|
||||
newState.showAddTeamForm = currentState.showAddTeamForm || false;
|
||||
newState.trayWasVisible = currentState.trayWasVisible || false;
|
||||
if (newState.teams.length === 0 && currentState.firstRun !== false) {
|
||||
newState.firstRun = false;
|
||||
newState.showAddTeamForm = true;
|
||||
}
|
||||
newState.savingState = currentState.savingState || {
|
||||
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
servers: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
|
||||
saveSetting = (configType, {key, data}) => {
|
||||
this.saveQueue.push({
|
||||
configType,
|
||||
key,
|
||||
data,
|
||||
});
|
||||
this.updateSaveState();
|
||||
this.processSaveQueue();
|
||||
}
|
||||
|
||||
processSaveQueue = debounce(() => {
|
||||
ipcRenderer.send(UPDATE_CONFIGURATION, this.saveQueue.splice(0, this.saveQueue.length));
|
||||
}, 500);
|
||||
|
||||
updateSaveState = () => {
|
||||
let queuedUpdateCounts = {
|
||||
[CONFIG_TYPE_SERVERS]: 0,
|
||||
[CONFIG_TYPE_APP_OPTIONS]: 0,
|
||||
};
|
||||
|
||||
queuedUpdateCounts = this.saveQueue.reduce((updateCounts, {configType}) => {
|
||||
updateCounts[configType]++;
|
||||
return updateCounts;
|
||||
}, queuedUpdateCounts);
|
||||
|
||||
const savingState = Object.assign({}, this.state.savingState);
|
||||
|
||||
Object.entries(queuedUpdateCounts).forEach(([configType, count]) => {
|
||||
if (count > 0) {
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVING;
|
||||
} else if (count === 0 && savingState[configType] === AutoSaveIndicator.SAVING_STATE_SAVING) {
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVED;
|
||||
this.resetSaveState(configType);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({savingState});
|
||||
}
|
||||
|
||||
resetSaveState = debounce((configType) => {
|
||||
if (this.state.savingState[configType] !== AutoSaveIndicator.SAVING_STATE_SAVING) {
|
||||
const savingState = Object.assign({}, this.state.savingState);
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_DONE;
|
||||
this.setState({savingState});
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
handleTeamsChange = (teams) => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
showAddTeamForm: false,
|
||||
teams,
|
||||
});
|
||||
if (teams.length === 0) {
|
||||
this.setState({showAddTeamForm: true});
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeShowTrayIcon = () => {
|
||||
const shouldShowTrayIcon = !this.showTrayIconRef.current.props.checked;
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
|
||||
this.setState({
|
||||
showTrayIcon: shouldShowTrayIcon,
|
||||
});
|
||||
|
||||
if (process.platform === 'darwin' && !shouldShowTrayIcon) {
|
||||
this.setState({
|
||||
minimizeToTray: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeTrayIconTheme = (theme) => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'trayIconTheme', data: theme});
|
||||
this.setState({
|
||||
trayIconTheme: theme,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeAutoStart = () => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.autostartRef.current.props.checked});
|
||||
this.setState({
|
||||
autostart: !this.autostartRef.current.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeMinimizeToTray = () => {
|
||||
const shouldMinimizeToTray = this.state.showTrayIcon && !this.minimizeToTrayRef.current.props.checked;
|
||||
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'minimizeToTray', data: shouldMinimizeToTray});
|
||||
this.setState({
|
||||
minimizeToTray: shouldMinimizeToTray,
|
||||
});
|
||||
}
|
||||
|
||||
toggleShowTeamForm = () => {
|
||||
this.setState({
|
||||
showAddTeamForm: !this.state.showAddTeamForm,
|
||||
});
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
setShowTeamFormVisibility = (val) => {
|
||||
this.setState({
|
||||
showAddTeamForm: val,
|
||||
});
|
||||
}
|
||||
|
||||
handleFlashWindow = () => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
|
||||
key: 'notifications',
|
||||
data: {
|
||||
...this.state.notifications,
|
||||
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
|
||||
},
|
||||
});
|
||||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleBounceIcon = () => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
|
||||
key: 'notifications',
|
||||
data: {
|
||||
...this.state.notifications,
|
||||
bounceIcon: !this.bounceIconRef.current.props.checked,
|
||||
},
|
||||
});
|
||||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
bounceIcon: !this.bounceIconRef.current.props.checked,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleBounceIconType = (event) => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
|
||||
key: 'notifications',
|
||||
data: {
|
||||
...this.state.notifications,
|
||||
bounceIconType: event.target.value,
|
||||
},
|
||||
});
|
||||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
bounceIconType: event.target.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleShowUnreadBadge = () => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.showUnreadBadgeRef.current.props.checked});
|
||||
this.setState({
|
||||
showUnreadBadge: !this.showUnreadBadgeRef.current.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeUseSpellChecker = () => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.useSpellCheckerRef.current.props.checked});
|
||||
this.setState({
|
||||
useSpellChecker: !this.useSpellCheckerRef.current.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeEnableHardwareAcceleration = () => {
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.enableHardwareAccelerationRef.current.props.checked});
|
||||
this.setState({
|
||||
enableHardwareAcceleration: !this.enableHardwareAccelerationRef.current.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
saveDownloadLocation = (location) => {
|
||||
this.setState({
|
||||
downloadLocation: location,
|
||||
});
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'downloadLocation', data: location});
|
||||
}
|
||||
|
||||
handleChangeDownloadLocation = (e) => {
|
||||
this.saveDownloadLocation(e.target.value);
|
||||
}
|
||||
|
||||
selectDownloadLocation = () => {
|
||||
if (!this.state.userOpenedDownloadDialog) {
|
||||
ipcRenderer.invoke(GET_DOWNLOAD_LOCATION, `/Users/${process.env.USER || process.env.USERNAME}/Downloads`).then((result) => this.saveDownloadLocation(result));
|
||||
this.setState({userOpenedDownloadDialog: true});
|
||||
}
|
||||
this.setState({userOpenedDownloadDialog: false});
|
||||
}
|
||||
|
||||
updateTeam = (index, newData) => {
|
||||
const teams = this.state.teams;
|
||||
teams[index] = newData;
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
teams,
|
||||
});
|
||||
}
|
||||
|
||||
addServer = (team) => {
|
||||
const teams = this.state.teams;
|
||||
teams.push(team);
|
||||
setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
teams,
|
||||
});
|
||||
}
|
||||
|
||||
openMenu = () => {
|
||||
// @eslint-ignore
|
||||
this.threeDotMenu.current.blur();
|
||||
this.props.openMenu();
|
||||
}
|
||||
|
||||
handleDoubleClick = () => {
|
||||
ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, 'settings');
|
||||
}
|
||||
|
||||
render() {
|
||||
const settingsPage = {
|
||||
navbar: {
|
||||
backgroundColor: '#fff',
|
||||
position: 'relative',
|
||||
},
|
||||
close: {
|
||||
textDecoration: 'none',
|
||||
position: 'absolute',
|
||||
right: '0',
|
||||
top: '5px',
|
||||
fontSize: '35px',
|
||||
fontWeight: 'normal',
|
||||
color: '#bbb',
|
||||
},
|
||||
heading: {
|
||||
textAlign: 'center',
|
||||
fontSize: '24px',
|
||||
margin: '0',
|
||||
padding: '1em 0',
|
||||
},
|
||||
sectionHeading: {
|
||||
fontSize: '20px',
|
||||
margin: '0',
|
||||
padding: '1em 0',
|
||||
float: 'left',
|
||||
},
|
||||
sectionHeadingLink: {
|
||||
marginTop: '24px',
|
||||
display: 'inline-block',
|
||||
fontSize: '15px',
|
||||
},
|
||||
footer: {
|
||||
padding: '0.4em 0',
|
||||
},
|
||||
downloadLocationInput: {
|
||||
marginRight: '3px',
|
||||
marginTop: '8px',
|
||||
width: '320px',
|
||||
height: '34px',
|
||||
padding: '0 12px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
downloadLocationButton: {
|
||||
marginBottom: '4px',
|
||||
},
|
||||
|
||||
container: {
|
||||
paddingBottom: '40px',
|
||||
},
|
||||
};
|
||||
|
||||
const teamsRow = (
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<TeamList
|
||||
teams={this.state.teams}
|
||||
showAddTeamForm={this.state.showAddTeamForm}
|
||||
toggleAddTeamForm={this.toggleShowTeamForm}
|
||||
setAddTeamFormVisibility={this.setShowTeamFormVisibility}
|
||||
onTeamsChange={this.handleTeamsChange}
|
||||
updateTeam={this.updateTeam}
|
||||
addServer={this.addServer}
|
||||
allowTeamEdit={this.state.enableServerManagement}
|
||||
onTeamClick={(name) => {
|
||||
backToIndex(name);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const serversRow = (
|
||||
<Row>
|
||||
<Col
|
||||
md={10}
|
||||
xs={8}
|
||||
>
|
||||
<h2 style={settingsPage.sectionHeading}>{'Server Management'}</h2>
|
||||
<div className='IndicatorContainer'>
|
||||
<AutoSaveIndicator
|
||||
id='serversSaveIndicator'
|
||||
savingState={this.state.savingState.servers}
|
||||
errorMessage={'Can\'t save your changes. Please try again.'}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col
|
||||
md={2}
|
||||
xs={4}
|
||||
>
|
||||
<p className='text-right'>
|
||||
<a
|
||||
style={settingsPage.sectionHeadingLink}
|
||||
id='addNewServer'
|
||||
href='#'
|
||||
onClick={this.toggleShowTeamForm}
|
||||
>{'+ Add New Server'}</a>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
let srvMgmt;
|
||||
if (this.state.enableServerManagement === true) {
|
||||
srvMgmt = (
|
||||
<div>
|
||||
{serversRow}
|
||||
{teamsRow}
|
||||
<hr/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const options = [];
|
||||
|
||||
// MacOS has an option in the Dock, to set the app to autostart, so we choose to not support this option for OSX
|
||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||
options.push(
|
||||
<Checkbox
|
||||
key='inputAutoStart'
|
||||
id='inputAutoStart'
|
||||
ref={this.autostartRef}
|
||||
checked={this.state.autostart}
|
||||
onChange={this.handleChangeAutoStart}
|
||||
>
|
||||
{'Start app on login'}
|
||||
<HelpBlock>
|
||||
{'If enabled, the app starts automatically when you log in to your machine.'}
|
||||
</HelpBlock>
|
||||
</Checkbox>);
|
||||
}
|
||||
|
||||
options.push(
|
||||
<Checkbox
|
||||
key='inputSpellChecker'
|
||||
id='inputSpellChecker'
|
||||
ref={this.useSpellCheckerRef}
|
||||
checked={this.state.useSpellChecker}
|
||||
onChange={this.handleChangeUseSpellChecker}
|
||||
>
|
||||
{'Check spelling'}
|
||||
<HelpBlock>
|
||||
{'Highlight misspelled words in your messages.'}
|
||||
{' Available for English, French, German, Portuguese, Spanish, and Dutch.'}
|
||||
</HelpBlock>
|
||||
</Checkbox>);
|
||||
|
||||
if (process.platform === 'darwin' || process.platform === 'win32') {
|
||||
const TASKBAR = process.platform === 'win32' ? 'taskbar' : 'Dock';
|
||||
options.push(
|
||||
<Checkbox
|
||||
key='inputShowUnreadBadge'
|
||||
id='inputShowUnreadBadge'
|
||||
ref={this.showUnreadBadgeRef}
|
||||
checked={this.state.showUnreadBadge}
|
||||
onChange={this.handleShowUnreadBadge}
|
||||
>
|
||||
{`Show red badge on ${TASKBAR} icon to indicate unread messages`}
|
||||
<HelpBlock>
|
||||
{`Regardless of this setting, mentions are always indicated with a red badge and item count on the ${TASKBAR} icon.`}
|
||||
</HelpBlock>
|
||||
</Checkbox>);
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||
options.push(
|
||||
<Checkbox
|
||||
key='flashWindow'
|
||||
id='inputflashWindow'
|
||||
ref={this.flashWindowRef}
|
||||
checked={!this.state.notifications || this.state.notifications.flashWindow === 2}
|
||||
onChange={this.handleFlashWindow}
|
||||
>
|
||||
{'Flash app window and taskbar icon when a new message is received'}
|
||||
<HelpBlock>
|
||||
{'If enabled, app window and taskbar icon flash for a few seconds when a new message is received.'}
|
||||
</HelpBlock>
|
||||
</Checkbox>);
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
options.push(
|
||||
<FormGroup
|
||||
key='OptionsForm'
|
||||
>
|
||||
<Checkbox
|
||||
inline={true}
|
||||
key='bounceIcon'
|
||||
id='inputBounceIcon'
|
||||
ref={this.bounceIconRef}
|
||||
checked={this.state.notifications ? this.state.notifications.bounceIcon : false}
|
||||
onChange={this.handleBounceIcon}
|
||||
style={{marginRight: '10px'}}
|
||||
>
|
||||
{'Bounce the Dock icon'}
|
||||
</Checkbox>
|
||||
<Radio
|
||||
inline={true}
|
||||
name='bounceIconType'
|
||||
value='informational'
|
||||
disabled={!this.state.notifications || !this.state.notifications.bounceIcon}
|
||||
defaultChecked={
|
||||
!this.state.notifications ||
|
||||
!this.state.notifications.bounceIconType ||
|
||||
this.state.notifications.bounceIconType === 'informational'
|
||||
}
|
||||
onChange={this.handleBounceIconType}
|
||||
>
|
||||
{'once'}
|
||||
</Radio>
|
||||
{' '}
|
||||
<Radio
|
||||
inline={true}
|
||||
name='bounceIconType'
|
||||
value='critical'
|
||||
disabled={!this.state.notifications || !this.state.notifications.bounceIcon}
|
||||
defaultChecked={this.state.notifications && this.state.notifications.bounceIconType === 'critical'}
|
||||
onChange={this.handleBounceIconType}
|
||||
>
|
||||
{'until I open the app'}
|
||||
</Radio>
|
||||
<HelpBlock
|
||||
style={{marginLeft: '20px'}}
|
||||
>
|
||||
{'If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.'}
|
||||
</HelpBlock>
|
||||
</FormGroup>,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||
options.push(
|
||||
<Checkbox
|
||||
key='inputShowTrayIcon'
|
||||
id='inputShowTrayIcon'
|
||||
ref={this.showTrayIconRef}
|
||||
checked={this.state.showTrayIcon}
|
||||
onChange={this.handleChangeShowTrayIcon}
|
||||
>
|
||||
{process.platform === 'darwin' ? `Show ${this.state.appName} icon in the menu bar` : 'Show icon in the notification area'}
|
||||
<HelpBlock>
|
||||
{'Setting takes effect after restarting the app.'}
|
||||
</HelpBlock>
|
||||
</Checkbox>);
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
options.push(
|
||||
<FormGroup
|
||||
key='trayIconTheme'
|
||||
ref={this.trayIconThemeRef}
|
||||
style={{marginLeft: '20px'}}
|
||||
>
|
||||
{'Icon theme: '}
|
||||
<Radio
|
||||
inline={true}
|
||||
name='trayIconTheme'
|
||||
value='light'
|
||||
defaultChecked={this.state.trayIconTheme === 'light' || !this.state.trayIconTheme}
|
||||
onChange={(event) => this.handleChangeTrayIconTheme('light', event)}
|
||||
>
|
||||
{'Light'}
|
||||
</Radio>
|
||||
{' '}
|
||||
<Radio
|
||||
inline={true}
|
||||
name='trayIconTheme'
|
||||
value='dark'
|
||||
defaultChecked={this.state.trayIconTheme === 'dark'}
|
||||
onChange={(event) => this.handleChangeTrayIconTheme('dark', event)}
|
||||
>{'Dark'}</Radio>
|
||||
</FormGroup>,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
options.push(
|
||||
<Checkbox
|
||||
key='inputMinimizeToTray'
|
||||
id='inputMinimizeToTray'
|
||||
ref={this.minimizeToTrayRef}
|
||||
disabled={!this.state.showTrayIcon || !this.state.trayWasVisible}
|
||||
checked={this.state.minimizeToTray}
|
||||
onChange={this.handleChangeMinimizeToTray}
|
||||
>
|
||||
{'Leave app running in notification area when application window is closed'}
|
||||
<HelpBlock>
|
||||
{'If enabled, the app stays running in the notification area after app window is closed.'}
|
||||
{this.state.trayWasVisible || !this.state.showTrayIcon ? '' : ' Setting takes effect after restarting the app.'}
|
||||
</HelpBlock>
|
||||
</Checkbox>);
|
||||
}
|
||||
|
||||
options.push(
|
||||
<Checkbox
|
||||
key='inputEnableHardwareAcceleration'
|
||||
id='inputEnableHardwareAcceleration'
|
||||
ref={this.enableHardwareAccelerationRef}
|
||||
checked={this.state.enableHardwareAcceleration}
|
||||
onChange={this.handleChangeEnableHardwareAcceleration}
|
||||
>
|
||||
{'Use GPU hardware acceleration'}
|
||||
<HelpBlock>
|
||||
{'If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.'}
|
||||
{' Setting takes effect after restarting the app.'}
|
||||
</HelpBlock>
|
||||
</Checkbox>,
|
||||
);
|
||||
|
||||
options.push(
|
||||
<div style={settingsPage.container}>
|
||||
<hr/>
|
||||
<div>{'Download Location'}</div>
|
||||
<input
|
||||
disabled={true}
|
||||
style={settingsPage.downloadLocationInput}
|
||||
key='inputDownloadLocation'
|
||||
id='inputDownloadLocation'
|
||||
ref={this.downloadLocationRef}
|
||||
onChange={this.handleChangeDownloadLocation}
|
||||
value={this.state.downloadLocation}
|
||||
/>
|
||||
<Button
|
||||
style={settingsPage.downloadLocationButton}
|
||||
id='saveDownloadLocation'
|
||||
onClick={this.selectDownloadLocation}
|
||||
>
|
||||
<span>{'Change'}</span>
|
||||
</Button>
|
||||
<HelpBlock>
|
||||
{'Specify the folder where files will download.'}
|
||||
</HelpBlock>
|
||||
</div>,
|
||||
);
|
||||
|
||||
let optionsRow = null;
|
||||
if (options.length > 0) {
|
||||
optionsRow = (
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<h2 style={settingsPage.sectionHeading}>{'App Options'}</h2>
|
||||
<div className='IndicatorContainer'>
|
||||
<AutoSaveIndicator
|
||||
id='appOptionsSaveIndicator'
|
||||
savingState={this.state.savingState.appOptions}
|
||||
errorMessage={'Can\'t save your changes. Please try again.'}
|
||||
/>
|
||||
</div>
|
||||
{ options.map((opt) => (
|
||||
<FormGroup key={opt.key}>
|
||||
{opt}
|
||||
</FormGroup>
|
||||
)) }
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
let waitForIpc;
|
||||
if (this.state.ready) {
|
||||
waitForIpc = (
|
||||
<>
|
||||
{srvMgmt}
|
||||
{optionsRow}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
waitForIpc = (<p>{'Loading configuration...'}</p>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='container-fluid'
|
||||
style={{
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
height: '100%',
|
||||
margin: '0 -15px',
|
||||
}}
|
||||
>
|
||||
<Navbar
|
||||
className='navbar-fixed-top'
|
||||
style={settingsPage.navbar}
|
||||
>
|
||||
<div style={{position: 'relative'}}>
|
||||
<h1 style={settingsPage.heading}>{'Settings'}</h1>
|
||||
</div>
|
||||
</Navbar>
|
||||
<Grid
|
||||
className='settingsPage'
|
||||
>
|
||||
{waitForIpc}
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsPage.propTypes = {
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
};
|
154
src/renderer/components/TabBar.jsx
Normal file
154
src/renderer/components/TabBar.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import {ipcRenderer} from 'electron';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Nav, NavItem} from 'react-bootstrap';
|
||||
import {Container, Draggable} from 'react-smooth-dnd';
|
||||
import PlusIcon from 'mdi-react/PlusIcon';
|
||||
|
||||
import {GET_CONFIGURATION} from 'common/communication';
|
||||
|
||||
export default class TabBar extends React.PureComponent { // need "this"
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasGPOTeams: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ipcRenderer.invoke(GET_CONFIGURATION).then((config) => {
|
||||
this.setState({hasGPOTeams: config.registryTeams && config.registryTeams.length > 0});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const orderedTabs = this.props.teams.concat().sort((a, b) => a.order - b.order);
|
||||
const tabs = orderedTabs.map((team) => {
|
||||
const index = this.props.teams.indexOf(team);
|
||||
|
||||
const sessionExpired = this.props.sessionsExpired[index];
|
||||
const hasUnreads = this.props.unreadCounts[index];
|
||||
|
||||
let mentionCount = 0;
|
||||
if (this.props.mentionCounts[index] > 0) {
|
||||
mentionCount = this.props.mentionCounts[index];
|
||||
}
|
||||
|
||||
let badgeDiv;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-expired'/>
|
||||
);
|
||||
} else if (mentionCount !== 0) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-badge'>
|
||||
{mentionCount}
|
||||
</div>
|
||||
);
|
||||
} else if (hasUnreads) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-dot'/>
|
||||
);
|
||||
}
|
||||
|
||||
const id = `teamTabItem${index}`;
|
||||
const navItem = () => (
|
||||
<NavItem
|
||||
key={index}
|
||||
id={id}
|
||||
eventKey={index}
|
||||
draggable={false}
|
||||
ref={id}
|
||||
active={this.props.activeKey === index}
|
||||
activeKey={this.props.activeKey}
|
||||
onMouseDown={() => {
|
||||
this.props.onSelect(team.name, index);
|
||||
}}
|
||||
onSelect={() => {
|
||||
this.props.onSelect(team.name, index);
|
||||
}}
|
||||
title={team.name}
|
||||
disabled={this.props.tabsDisabled}
|
||||
>
|
||||
<div className='TabBar-tabSeperator'>
|
||||
<span>
|
||||
{team.name}
|
||||
</span>
|
||||
{ badgeDiv }
|
||||
</div>
|
||||
</NavItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={id}
|
||||
render={navItem}
|
||||
className='teamTabItem'
|
||||
/>);
|
||||
});
|
||||
if (this.props.showAddServerButton === true) {
|
||||
tabs.push(
|
||||
<NavItem
|
||||
className='TabBar-addServerButton'
|
||||
key='addServerButton'
|
||||
id='addServerButton'
|
||||
eventKey='addServerButton'
|
||||
draggable={false}
|
||||
title='Add new server'
|
||||
activeKey={this.props.activeKey}
|
||||
onSelect={() => {
|
||||
this.props.onAddServer();
|
||||
}}
|
||||
disabled={this.props.tabsDisabled}
|
||||
>
|
||||
<div className='TabBar-tabSeperator'>
|
||||
<PlusIcon size={20}/>
|
||||
</div>
|
||||
</NavItem>,
|
||||
);
|
||||
}
|
||||
|
||||
const navContainer = (ref) => (
|
||||
<Nav
|
||||
ref={ref}
|
||||
className={`smooth-dnd-container TabBar${this.props.isDarkMode ? ' darkMode' : ''}`}
|
||||
id={this.props.id}
|
||||
bsStyle='tabs'
|
||||
>
|
||||
{ tabs }
|
||||
</Nav>
|
||||
);
|
||||
return (
|
||||
<Container
|
||||
ref={this.container}
|
||||
render={navContainer}
|
||||
orientation='horizontal'
|
||||
lockAxis={'x'}
|
||||
onDrop={this.props.onDrop}
|
||||
animationDuration={300}
|
||||
shouldAcceptDrop={() => {
|
||||
return !this.state.hasGPOTeams && !this.props.tabsDisabled;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabBar.propTypes = {
|
||||
activeKey: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
isDarkMode: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
teams: PropTypes.array,
|
||||
sessionsExpired: PropTypes.object,
|
||||
unreadCounts: PropTypes.object,
|
||||
mentionCounts: PropTypes.object,
|
||||
showAddServerButton: PropTypes.bool,
|
||||
onAddServer: PropTypes.func,
|
||||
onDrop: PropTypes.func,
|
||||
tabsDisabled: PropTypes.bool,
|
||||
};
|
187
src/renderer/components/TeamList.jsx
Normal file
187
src/renderer/components/TeamList.jsx
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ListGroup} from 'react-bootstrap';
|
||||
|
||||
import TeamListItem from './TeamListItem.jsx';
|
||||
import NewTeamModal from './NewTeamModal.jsx';
|
||||
import RemoveServerModal from './RemoveServerModal.jsx';
|
||||
|
||||
export default class TeamList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showEditTeamForm: false,
|
||||
indexToRemoveServer: -1,
|
||||
team: {
|
||||
url: '',
|
||||
name: '',
|
||||
index: false,
|
||||
order: props.teams.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleTeamRemove = (index) => {
|
||||
console.log(index);
|
||||
const teams = this.props.teams;
|
||||
const removedOrder = this.props.teams[index].order;
|
||||
teams.splice(index, 1);
|
||||
teams.forEach((value) => {
|
||||
if (value.order > removedOrder) {
|
||||
value.order--;
|
||||
}
|
||||
});
|
||||
this.props.onTeamsChange(teams);
|
||||
}
|
||||
|
||||
handleTeamAdd = (team) => {
|
||||
const teams = this.props.teams;
|
||||
|
||||
// check if team already exists and then change existing team or add new one
|
||||
if ((typeof team.index !== 'undefined') && teams[team.index]) {
|
||||
teams[team.index].name = team.name;
|
||||
teams[team.index].url = team.url;
|
||||
teams[team.index].order = team.order;
|
||||
} else {
|
||||
teams.push(team);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showEditTeamForm: false,
|
||||
team: {
|
||||
url: '',
|
||||
name: '',
|
||||
index: false,
|
||||
order: teams.length,
|
||||
},
|
||||
});
|
||||
|
||||
this.props.onTeamsChange(teams);
|
||||
}
|
||||
|
||||
openServerRemoveModal = (indexForServer) => {
|
||||
this.setState({indexToRemoveServer: indexForServer});
|
||||
}
|
||||
|
||||
closeServerRemoveModal = () => {
|
||||
this.setState({indexToRemoveServer: -1});
|
||||
}
|
||||
|
||||
handleTeamRemovePrompt = (index) => {
|
||||
return () => {
|
||||
document.activeElement.blur();
|
||||
this.openServerRemoveModal(index);
|
||||
};
|
||||
}
|
||||
|
||||
handleTeamEditing = (team, index) => {
|
||||
return () => {
|
||||
document.activeElement.blur();
|
||||
this.setState({
|
||||
showEditTeamForm: true,
|
||||
team: {
|
||||
url: team.url,
|
||||
name: team.name,
|
||||
index,
|
||||
order: team.order,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const teamNodes = this.props.teams.map((team, i) => {
|
||||
return (
|
||||
<TeamListItem
|
||||
key={`teamListItem_${team.name}`}
|
||||
name={team.name}
|
||||
url={team.url}
|
||||
onTeamRemove={this.handleTeamRemovePrompt(i)}
|
||||
onTeamEditing={this.handleTeamEditing(team, i)}
|
||||
onTeamClick={() => this.props.onTeamClick(team.name)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const addServerForm = (
|
||||
<NewTeamModal
|
||||
currentOrder={this.props.teams.length}
|
||||
show={this.props.showAddTeamForm || this.state.showEditTeamForm}
|
||||
editMode={this.state.showEditTeamForm}
|
||||
onClose={() => {
|
||||
this.setState({
|
||||
showEditTeamForm: false,
|
||||
team: {
|
||||
name: '',
|
||||
url: '',
|
||||
index: false,
|
||||
order: this.props.teams.length,
|
||||
},
|
||||
});
|
||||
this.props.setAddTeamFormVisibility(false);
|
||||
}}
|
||||
onSave={(newTeam) => {
|
||||
const teamData = {
|
||||
name: newTeam.name,
|
||||
url: newTeam.url,
|
||||
order: newTeam.order,
|
||||
};
|
||||
if (this.props.showAddTeamForm) {
|
||||
this.props.addServer(teamData);
|
||||
} else {
|
||||
this.props.updateTeam(newTeam.index, teamData);
|
||||
}
|
||||
this.setState({
|
||||
showNewTeamModal: false,
|
||||
showEditTeamForm: false,
|
||||
team: {
|
||||
name: '',
|
||||
url: '',
|
||||
index: false,
|
||||
order: newTeam.order + 1,
|
||||
},
|
||||
});
|
||||
this.render();
|
||||
this.props.setAddTeamFormVisibility(false);
|
||||
}}
|
||||
team={this.state.team}
|
||||
/>);
|
||||
|
||||
const removeServer = this.props.teams[this.state.indexToRemoveServer];
|
||||
const removeServerModal = (
|
||||
<RemoveServerModal
|
||||
show={this.state.indexToRemoveServer !== -1}
|
||||
serverName={removeServer ? removeServer.name : ''}
|
||||
onHide={this.closeServerRemoveModal}
|
||||
onCancel={this.closeServerRemoveModal}
|
||||
onAccept={() => {
|
||||
this.handleTeamRemove(this.state.indexToRemoveServer);
|
||||
this.closeServerRemoveModal();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListGroup className='teamList'>
|
||||
{ teamNodes }
|
||||
{ addServerForm }
|
||||
{ removeServerModal}
|
||||
</ListGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamList.propTypes = {
|
||||
onTeamClick: PropTypes.func,
|
||||
onTeamsChange: PropTypes.func,
|
||||
showAddTeamForm: PropTypes.bool,
|
||||
teams: PropTypes.array,
|
||||
addServer: PropTypes.func,
|
||||
updateTeam: PropTypes.func,
|
||||
setAddTeamFormVisibility: PropTypes.func,
|
||||
};
|
49
src/renderer/components/TeamListItem.jsx
Normal file
49
src/renderer/components/TeamListItem.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class TeamListItem extends React.PureComponent {
|
||||
handleTeamRemove = () => {
|
||||
this.props.onTeamRemove();
|
||||
}
|
||||
handleTeamEditing = () => {
|
||||
this.props.onTeamEditing();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className='TeamListItem list-group-item'>
|
||||
<div
|
||||
className='TeamListItem-left'
|
||||
onClick={this.props.onTeamClick}
|
||||
>
|
||||
<h4 className='list-group-item-heading'>{ this.props.name }</h4>
|
||||
<p className='list-group-item-text'>
|
||||
{ this.props.url }
|
||||
</p>
|
||||
</div>
|
||||
<div className='pull-right'>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleTeamEditing}
|
||||
>{'Edit'}</a>
|
||||
{' - '}
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleTeamRemove}
|
||||
>{'Remove'}</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamListItem.propTypes = {
|
||||
name: PropTypes.string,
|
||||
onTeamEditing: PropTypes.func,
|
||||
onTeamRemove: PropTypes.func,
|
||||
onTeamClick: PropTypes.func,
|
||||
url: PropTypes.string,
|
||||
};
|
113
src/renderer/components/UpdaterPage.jsx
Normal file
113
src/renderer/components/UpdaterPage.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import {Button, Navbar, ProgressBar} from 'react-bootstrap';
|
||||
|
||||
function InstallButton(props) {
|
||||
if (props.notifyOnly) {
|
||||
return (
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
onClick={props.onClickDownload}
|
||||
>{'Download Update'}</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
onClick={props.onClickInstall}
|
||||
>{'Install Update'}</Button>
|
||||
);
|
||||
}
|
||||
|
||||
InstallButton.propTypes = {
|
||||
notifyOnly: propTypes.bool.isRequired,
|
||||
onClickInstall: propTypes.func.isRequired,
|
||||
onClickDownload: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function UpdaterPage(props) {
|
||||
let footer;
|
||||
if (props.isDownloading) {
|
||||
footer = (
|
||||
<Navbar
|
||||
className='UpdaterPage-footer'
|
||||
fixedBottom={true}
|
||||
fluid={true}
|
||||
>
|
||||
<ProgressBar
|
||||
active={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'
|
||||
fixedBottom={true}
|
||||
fluid={true}
|
||||
>
|
||||
<Button
|
||||
className='UpdaterPage-skipButton'
|
||||
bsStyle='link'
|
||||
onClick={props.onClickSkip}
|
||||
>{'Skip this version'}</Button>
|
||||
<div className='pull-right'>
|
||||
<Button
|
||||
bsStyle='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 fluid={true} >
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
UpdaterPage.propTypes = {
|
||||
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,
|
||||
onClickCancel: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UpdaterPage;
|
53
src/renderer/components/UpdaterPage/UpdaterPage.stories.jsx
Normal file
53
src/renderer/components/UpdaterPage/UpdaterPage.stories.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
||||
|
||||
import {action} from '@storybook/addon-actions';
|
||||
|
||||
import UpdaterPage from '../UpdaterPage.jsx';
|
||||
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')}
|
||||
/>
|
||||
));
|
114
src/renderer/components/showCertificateModal.jsx
Normal file
114
src/renderer/components/showCertificateModal.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, Row, Col} from 'react-bootstrap';
|
||||
|
||||
export default class ShowCertificateModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
certificate: PropTypes.object,
|
||||
onOk: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
certificate: props.certificate,
|
||||
};
|
||||
}
|
||||
|
||||
handleOk = () => {
|
||||
this.setState({certificate: null});
|
||||
this.props.onOk();
|
||||
}
|
||||
|
||||
render() {
|
||||
const certificateSection = (descriptor) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<dt className={'certificate-key'}>{descriptor}</dt>
|
||||
<dd className={'certificate-section'}><span/></dd>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
const certificateItem = (descriptor, value) => {
|
||||
const val = value ? `${value}` : <span/>;
|
||||
return (
|
||||
<Fragment>
|
||||
<dt className={'certificate-key'}>{descriptor}</dt>
|
||||
<dd className={'certificate-value'}>{val}</dd>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
if (this.state.certificate === null) {
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='show-certificate'
|
||||
>
|
||||
<Modal.Body>
|
||||
{'No certificate Selected'}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const utcSeconds = (date) => {
|
||||
const d = new Date(0);
|
||||
d.setUTCSeconds(date);
|
||||
return d;
|
||||
};
|
||||
|
||||
const expiration = utcSeconds(this.state.certificate.validExpiry);
|
||||
const creation = utcSeconds(this.state.certificate.validStart);
|
||||
const dateDisplayOptions = {dateStyle: 'full', timeStyle: 'full'};
|
||||
const dateLocale = 'en-US';
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='show-certificate'
|
||||
show={this.state.certificate !== null}
|
||||
scrollable={'true'}
|
||||
>
|
||||
<Modal.Header className={'no-border'}>
|
||||
<Modal.Title>{'Certificate information'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className='details'>{'Details'}</p>
|
||||
<dl>
|
||||
{certificateSection('Subject Name')}
|
||||
{certificateItem('Common Name', this.state.certificate.subject.commonName)}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateSection('Issuer Name')}
|
||||
{certificateItem('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))}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateSection('Public Key Info')}
|
||||
{certificateItem('Algorithm', this.state.certificate.fingerprint.split('/')[0])}
|
||||
</dl>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'no-border'}>
|
||||
<div className='container-fluid'>
|
||||
<Row>
|
||||
<Col>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={this.handleOk}
|
||||
className={'primary'}
|
||||
>{'Close'}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
19
src/renderer/components/urlDescription.jsx
Normal file
19
src/renderer/components/urlDescription.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
export default function UrlDescription(props) {
|
||||
if (props.url) {
|
||||
return (
|
||||
<div className='HoveringURL HoveringURL-left'>
|
||||
<p>{props.url}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UrlDescription.propTypes = {
|
||||
url: propTypes.string,
|
||||
};
|
Reference in New Issue
Block a user