[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:
BIN
src/renderer/assets/fonts/fontawesome-webfont.eot
Normal file
BIN
src/renderer/assets/fonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
BIN
src/renderer/assets/fonts/fontawesome-webfont.ttf
Normal file
BIN
src/renderer/assets/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
src/renderer/assets/fonts/fontawesome-webfont.woff
Normal file
BIN
src/renderer/assets/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
src/renderer/assets/fonts/fontawesome-webfont.woff2
Normal file
BIN
src/renderer/assets/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
BIN
src/renderer/assets/fonts/glyphicons-halflings-regular.eot
Normal file
BIN
src/renderer/assets/fonts/glyphicons-halflings-regular.eot
Normal file
Binary file not shown.
BIN
src/renderer/assets/fonts/glyphicons-halflings-regular.ttf
Normal file
BIN
src/renderer/assets/fonts/glyphicons-halflings-regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/assets/fonts/glyphicons-halflings-regular.woff
Normal file
BIN
src/renderer/assets/fonts/glyphicons-halflings-regular.woff
Normal file
Binary file not shown.
BIN
src/renderer/assets/fonts/glyphicons-halflings-regular.woff2
Normal file
BIN
src/renderer/assets/fonts/glyphicons-halflings-regular.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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,
|
||||
};
|
179
src/renderer/css/components/AddServerModal.css
Normal file
179
src/renderer/css/components/AddServerModal.css
Normal file
@@ -0,0 +1,179 @@
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.NewTeamModal-noBottomSpace {
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
border-radius: 4px;
|
||||
border: 1px solid;
|
||||
border-color: #666;
|
||||
padding: 5px;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
width: 1000px;
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -25%);
|
||||
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
form.modalForm {
|
||||
padding-left: 10px;
|
||||
display: block;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
|
||||
-webkit-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #66afe9;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
button.modalButton {
|
||||
color: #007bff;
|
||||
border-color: #007bff;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: .375rem .75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: .25rem;
|
||||
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
box-sizing: border-box;
|
||||
margin-left: 0.2em;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
button.modalButton:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
button.modalButton:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button.modalButton:active:focus {
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
color: #fff;
|
||||
background-color: #337ab7;
|
||||
border-color: #2e6da4;
|
||||
}
|
||||
|
||||
button.primary:focus {
|
||||
background-color: #286090;
|
||||
border-color: #122b40;
|
||||
}
|
||||
|
||||
button.default {
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
button.default:focus {
|
||||
background-color: #e6e6e6;
|
||||
border-color: #8c8c8c;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 5px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
.modalHeader::after {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
content: '';
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
margin: 5px;
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.modalFooter::before {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
content: '';
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pull-left {
|
||||
float: left;
|
||||
}
|
||||
.modal-error {
|
||||
color: #a94442;
|
||||
}
|
||||
|
||||
.has-error .form-control {
|
||||
border-color: #a94442;
|
||||
}
|
||||
|
210
src/renderer/css/components/CertificateModal.css
Normal file
210
src/renderer/css/components/CertificateModal.css
Normal file
@@ -0,0 +1,210 @@
|
||||
.certificate-modal .modal,
|
||||
.show-certificate .modal {
|
||||
background-color: aliceblue;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.show-certificate .modal-dialog {
|
||||
width: 800px;
|
||||
}
|
||||
.modal-header {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
.modal-header::after {
|
||||
border-bottom: solid 1px #E5E5E5;
|
||||
width: 100%;
|
||||
transform: translateY(15px);
|
||||
}
|
||||
.modal-footer::before {
|
||||
border-top: solid 1px #E5E5E5;
|
||||
width: 100%;
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
.modal-body :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.certificate-modal .col-sm-4 {
|
||||
padding: 0px;
|
||||
text-align: left;
|
||||
}
|
||||
.certificate-modal .col-sm-8 {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.certificate-list thead {
|
||||
width: 557.89px;
|
||||
|
||||
}
|
||||
|
||||
.certificate-list thead>tr {
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
.certificate-list thead>tr>th {
|
||||
font-family: Helvetica Neue;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 3px 5px;
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
color: #333333;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.certificate-list thead tr th:first-child span {
|
||||
padding-left: 5px;
|
||||
}
|
||||
.certificate-list thead tr th span {
|
||||
border-right: solid 1px #E5E5E5;
|
||||
display: block;
|
||||
}
|
||||
.certificate-list thead tr th:last-child span {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.certificate-list tbody tr {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.certificate-list tbody>tr>td {
|
||||
max-width: 165px;
|
||||
height: 47px;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
padding: 15px 10px;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.certificate-list tbody tr td:first-child {
|
||||
padding-left: 15px;
|
||||
max-width: 227px;
|
||||
}
|
||||
|
||||
.certificate-list tbody tr.selected {
|
||||
background: #457AB2;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
table.certificate-list {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CCCCCC;
|
||||
border-radius: 4px;
|
||||
border-collapse: unset;
|
||||
box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.0008);
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
table.certificate-list:focus {
|
||||
border: 1px solid #66AFE9;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 0px 8px rgba(102, 175, 233, 0.6), inset 1px 1px 0px rgba(0, 0, 0, 0.00075);
|
||||
}
|
||||
|
||||
|
||||
.show-certificate button,
|
||||
.certificate-modal button {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CCCCCC;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
padding: 9px 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.show-certificate button:disabled,
|
||||
.certificate-modal button:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.show-certificate button.primary,
|
||||
.certificate-modal button.primary {
|
||||
background: #457AB2;
|
||||
color: #FFFFFF;
|
||||
border: 1px solid #2E6DA4;
|
||||
}
|
||||
|
||||
|
||||
.show-certificate button.primary:hover,
|
||||
.certificate-modal button.primary:hover {
|
||||
background: #659AD2;
|
||||
}
|
||||
|
||||
.certificate-modal button.info {
|
||||
color: #457AB2;
|
||||
}
|
||||
|
||||
.certificate-modal button.info:disabled {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.show-certificate .subtitle,
|
||||
.certificate-modal .subtitle {
|
||||
color: #737373;
|
||||
margin: 0px 0px 15px 0px;
|
||||
}
|
||||
|
||||
.show-certificate .no-border,
|
||||
.certificate-modal .no-border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.show-certificate dl {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.show-certificate dt, dd {
|
||||
float: left;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.show-certificate dt {
|
||||
width: 150px;
|
||||
clear:both
|
||||
}
|
||||
|
||||
.show-certificate dd {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.certificate-key {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
color: #333333;
|
||||
text-align: right;
|
||||
}
|
||||
.certificate-value {
|
||||
padding-top: 1px;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
text-align: left;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.certificate-section {
|
||||
border-bottom: 1px solid #E5E5E5;
|
||||
width: 598px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.show-certificate .details {
|
||||
margin: 15px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
color: #333333;
|
||||
}
|
36
src/renderer/css/components/ErrorView.css
Normal file
36
src/renderer/css/components/ErrorView.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.ErrorView {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.ErrorView-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ErrorView-tableStyle {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ErrorView-cellStyle {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.ErrorView-bullets {
|
||||
padding-left: 15px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.ErrorView-techInfo {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
33
src/renderer/css/components/ExtraBar.css
Normal file
33
src/renderer/css/components/ExtraBar.css
Normal file
@@ -0,0 +1,33 @@
|
||||
#extra-bar {
|
||||
max-height: 76px;
|
||||
transition: max-height 0.25s ease;
|
||||
background-color: #f5f5f5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#extra-bar div {
|
||||
padding: 0 0.93em 0.2em;
|
||||
}
|
||||
|
||||
#extra-bar.hidden {
|
||||
max-height: 0px;
|
||||
}
|
||||
|
||||
span.backLabel {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
color: #166de0;
|
||||
}
|
||||
|
||||
span.backIcon {
|
||||
margin-right: 4px;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.container-fluid button:first-child {
|
||||
padding-left: 0px;
|
||||
}
|
79
src/renderer/css/components/Finder.css
Normal file
79
src/renderer/css/components/Finder.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.finder {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
padding: 4px;
|
||||
background: #eee;
|
||||
border: 1px solid #d7d7d7;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
font-size: 0px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.finder-input-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.finder button {
|
||||
border: none;
|
||||
background: #f0f0f0;
|
||||
outline: none;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.finder button:hover {
|
||||
background: #d2d2d2;
|
||||
}
|
||||
|
||||
.finder-input {
|
||||
border: 1px solid #d2d2d2;
|
||||
border-radius: 3px;
|
||||
width: 200px;
|
||||
outline: none;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
padding: 0px 35px 0px 5px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.finder-input:focus {
|
||||
border-color: #35b5f4;
|
||||
box-shadow: 0 0 1px #35b5f4;
|
||||
}
|
||||
|
||||
.finder-progress__disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.finder-progress {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
right: 8px;
|
||||
top: 6px;
|
||||
color: #7b7b7b;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.finder .finder-close {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.finder-next {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.finder-prev {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
margin-left: 2px;
|
||||
}
|
30
src/renderer/css/components/HoveringURL.css
Normal file
30
src/renderer/css/components/HoveringURL.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.HoveringURL {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: gray;
|
||||
background-color: whitesmoke;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 0px;
|
||||
padding-right: 16px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
border-top: solid thin lightgray;
|
||||
pointer-events: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.HoveringURL-left {
|
||||
border-top-right-radius: 4px;
|
||||
border-right: solid thin lightgray;
|
||||
}
|
||||
|
||||
.HoveringURL p {
|
||||
margin: 0;
|
||||
font-size: small;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
294
src/renderer/css/components/LoadingAnimation.css
Normal file
294
src/renderer/css/components/LoadingAnimation.css
Normal file
@@ -0,0 +1,294 @@
|
||||
.LoadingAnimation {
|
||||
--fade-duration: 150ms;
|
||||
--colour: #166de0;
|
||||
--animation-initial-delay: 500ms;
|
||||
--animation-start-duration: 750ms;
|
||||
--animation-end-duration: 600ms;
|
||||
--animation-spinner-speed: 500ms;
|
||||
--animation-spinner-mask-stroke-length: 169.6;
|
||||
--ease-in-cubic: cubic-bezier(0.32, 0, 0.67, 0);
|
||||
--ease-in: var(--ease-in-cubic);
|
||||
--ease-in-out-cubic: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--ease-in-out: var(--ease-in-out-cubic);
|
||||
--ease-in-out-compass-shrink: cubic-bezier(0.1, 0.25, 0.3, 1);
|
||||
|
||||
opacity: 0;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
.LoadingAnimation--darkMode {
|
||||
--colour: white;
|
||||
}
|
||||
.LoadingAnimation g,
|
||||
.LoadingAnimation rect,
|
||||
.LoadingAnimation path,
|
||||
.LoadingAnimation circle {
|
||||
transform-origin: center center;
|
||||
}
|
||||
.LoadingAnimation svg {
|
||||
color: var(--colour);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__spinner-gradient-color {
|
||||
stop-color: var(--colour);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__spinner-mask {
|
||||
transform: scale3d(1.03, 1.03, 1);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__spinner-container {
|
||||
opacity: 0;
|
||||
transform: scale3D(2.08, 2.08, 1) rotate3d(0, 0, 1, -10deg);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__spinner-mask-container {
|
||||
transform: rotate3d(0, 0, 1, -86deg);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__spinner-mask {
|
||||
stroke-dasharray: var(--animation-spinner-mask-stroke-length);
|
||||
stroke-dashoffset: var(--animation-spinner-mask-stroke-length);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__compass {
|
||||
opacity: 1;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__compass-needle-container {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__compass-needle,
|
||||
.LoadingAnimation .LoadingAnimation__compass-needle-front-mask,
|
||||
.LoadingAnimation .LoadingAnimation__compass-needle-behind-mask {
|
||||
transform-origin: 54px 46px;
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__compass-base-mask-container {
|
||||
transform: rotate3d(0, 0, 1, -86deg);
|
||||
}
|
||||
.LoadingAnimation .LoadingAnimation__compass-base-mask {
|
||||
stroke-dasharray: var(--animation-spinner-mask-stroke-length);
|
||||
stroke-dashoffset: var(--animation-spinner-mask-stroke-length);
|
||||
}
|
||||
|
||||
.LoadingAnimation--loading {
|
||||
--fade-in-duration: 150ms;
|
||||
--fade-in-delay: 0ms;
|
||||
|
||||
animation:
|
||||
LoadingAnimation__fade-in var(--fade-in-duration) var(--fade-in-delay) var(--ease-in) forwards;
|
||||
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__spinner-container {
|
||||
--shrink-duration: calc(var(--animation-end-duration) * 0.5);
|
||||
--shrink-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.91 + var(--animation-initial-delay));
|
||||
--fade-in-duration: calc(var(--animation-end-duration) * 0.25);
|
||||
--fade-in-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) * 0.24 + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__spinner-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in-out-compass-shrink) forwards,
|
||||
LoadingAnimation__fade-in var(--fade-in-duration) var(--fade-in-delay) var(--ease-in) forwards;
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__spinner-mask {
|
||||
--reveal-duration: var(--animation-end-duration);
|
||||
--reveal-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__spinner-reveal var(--reveal-duration) var(--reveal-delay) var(--ease-in) forwards;
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass {
|
||||
--shrink-duration: calc(var(--animation-end-duration) * 0.5);
|
||||
--shrink-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.91 + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__compass-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in-out-compass-shrink) forwards;
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-container {
|
||||
--shrink-duration: calc(var(--animation-end-duration) * 0.25);
|
||||
--shrink-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) - var(--animation-end-duration) * 0.25 + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__needle-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in) forwards;
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle {
|
||||
--spin-left-duration: var(--animation-start-duration);
|
||||
--spin-left-delay: var(--animation-initial-delay);
|
||||
--spin-right-duration: var(--animation-end-duration);
|
||||
--spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
|
||||
--fade-out-duration: calc(var(--animation-end-duration) * 0.25);
|
||||
--fade-out-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) - var(--animation-end-duration) * 0.25 + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__needle-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards,
|
||||
LoadingAnimation__needle-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards,
|
||||
LoadingAnimation__fade-out var(--fade-out-duration) var(--fade-out-delay) var(--ease-in) forwards;
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-behind-mask {
|
||||
--spin-left-duration: var(--animation-start-duration);
|
||||
--spin-left-delay: var(--animation-initial-delay);
|
||||
--spin-right-duration: calc(var(--animation-end-duration) * 0.3666);
|
||||
--spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__needle-mask-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards,
|
||||
LoadingAnimation__needle-mask-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards;
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-front-mask {
|
||||
--spin-left-duration: var(--animation-start-duration);
|
||||
--spin-left-delay: var(--animation-initial-delay);
|
||||
--spin-right-duration: var(--animation-end-duration);
|
||||
--spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__needle-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards,
|
||||
LoadingAnimation__needle-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards;
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-base-mask {
|
||||
--conceal-duration: var(--animation-end-duration);
|
||||
--conceal-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__compass-base-conceal var(--conceal-duration) var(--conceal-delay) var(--ease-in) forwards;
|
||||
}
|
||||
.LoadingAnimation.LoadingAnimation--spinning .LoadingAnimation__spinner {
|
||||
--spin-duration: var(--animation-spinner-speed);
|
||||
--spin-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.95 + var(--animation-initial-delay));
|
||||
|
||||
animation:
|
||||
LoadingAnimation__spinner-spin var(--spin-duration) var(--spin-delay) linear infinite;
|
||||
}
|
||||
|
||||
.LoadingAnimation--loaded {
|
||||
--duration: 150ms;
|
||||
--delay: 0ms;
|
||||
|
||||
animation:
|
||||
LoadingAnimation__fade-out var(--duration) var(--delay) var(--ease-in) forwards,
|
||||
LoadingAnimation__shrink var(--duration) var(--delay) var(--ease-in) forwards;
|
||||
}
|
||||
.LoadingAnimation--loaded .LoadingAnimation__spinner-container {
|
||||
opacity: 1;
|
||||
transform: scale3D(1, 1, 1) rotate3d(0, 0, 1, -10deg);
|
||||
}
|
||||
.LoadingAnimation--loaded .LoadingAnimation__spinner-mask {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
.LoadingAnimation--loaded .LoadingAnimation__compass {
|
||||
transform: scale3D(0.4166666667, 0.4166666667, 1);
|
||||
}
|
||||
.LoadingAnimation--loaded .LoadingAnimation__compass-needle-container {
|
||||
transform: scale3d(0.35, 0.35, 1);
|
||||
}
|
||||
.LoadingAnimation--loaded .LoadingAnimation__compass-needle {
|
||||
opacity: 0;
|
||||
transform: rotate3d(0, 0, 1, 405deg);
|
||||
}
|
||||
.LoadingAnimation--loaded .LoadingAnimation__compass-needle-behind-mask {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
.LoadingAnimation--loaded .LoadingAnimation__compass-needle-front-mask {
|
||||
transform: rotate3d(0, 0, 1, 405deg);
|
||||
}
|
||||
.LoadingAnimation--loaded .LoadingAnimation__compass-base-mask {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
@keyframes LoadingAnimation__fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__shrink {
|
||||
0% {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
100% {
|
||||
transform: scale3d(0.35, 0.35, 1);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__spinner-shrink {
|
||||
0% {
|
||||
transform: scale3D(2.08, 2.08, 1) rotate3d(0, 0, 1, -10deg);
|
||||
}
|
||||
100% {
|
||||
transform: scale3D(1, 1, 1) rotate3d(0, 0, 1, -10deg);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__spinner-spin {
|
||||
from {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate3d(0, 0, 1, 359deg);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__spinner-reveal {
|
||||
0%, 5% {
|
||||
stroke-dashoffset: var(--animation-spinner-mask-stroke-length);
|
||||
}
|
||||
95%, 100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__needle-spin-left {
|
||||
0% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate3d(0, 0, 1, -20deg);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__needle-spin-right {
|
||||
0% {
|
||||
transform: rotate3d(0, 0, 1, -20deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate3d(0, 0, 1, 405deg);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__needle-mask-spin-left {
|
||||
0% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate3d(0, 0, 1, -20deg);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__needle-mask-spin-right {
|
||||
0% {
|
||||
transform: rotate3d(0, 0, 1, -20deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__needle-shrink {
|
||||
0% {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
100% {
|
||||
transform: scale3d(0.35, 0.35, 1);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__compass-shrink {
|
||||
0% {
|
||||
transform: scale3D(1, 1, 1);
|
||||
}
|
||||
100% {
|
||||
transform: scale3D(0.4166666667, 0.4166666667, 1);
|
||||
}
|
||||
}
|
||||
@keyframes LoadingAnimation__compass-base-conceal {
|
||||
0%, 5% {
|
||||
stroke-dashoffset: var(--animation-spinner-mask-stroke-length);
|
||||
}
|
||||
95%, 100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
59
src/renderer/css/components/LoadingScreen.css
Normal file
59
src/renderer/css/components/LoadingScreen.css
Normal file
@@ -0,0 +1,59 @@
|
||||
body {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.LoadingScreen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
vertical-align: middle;
|
||||
background: white url(../../../assets/window-background.svg) no-repeat center;
|
||||
background-size: cover;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
z-index: 10;
|
||||
overflow:hidden;
|
||||
|
||||
transition: opacity 150ms 0ms ease-out, visibility 150ms 0ms step-start;
|
||||
}
|
||||
|
||||
.LoadingScreen::before, .LoadingScreen::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 460px;
|
||||
height: 460px;
|
||||
opacity: 0.1;
|
||||
background-image: url(../../../assets/window-background-dots.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.LoadingScreen::before {
|
||||
left: -210px;
|
||||
bottom: 10px;
|
||||
}
|
||||
.LoadingScreen::after {
|
||||
right: -80px;
|
||||
top: 50%;
|
||||
margin-top: -230px;
|
||||
}
|
||||
|
||||
.LoadingScreen--darkMode {
|
||||
background-color: #323639;
|
||||
background-image: url(../../../assets/window-background_dark.svg);
|
||||
}
|
||||
.LoadingScreen--darkMode::before, .LoadingScreen--darkMode::after {
|
||||
background-image: url(../../../assets/window-background-dots_dark.svg);
|
||||
}
|
||||
|
||||
.LoadingScreen--loaded {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
transition: opacity 150ms 0ms ease-in, visibility 150ms 0ms step-end;
|
||||
}
|
13
src/renderer/css/components/MainPage.css
Normal file
13
src/renderer/css/components/MainPage.css
Normal file
@@ -0,0 +1,13 @@
|
||||
/*.mainPage,.mainPage > .container-fluid, .mainPage-viewsRow {
|
||||
height: 100%;
|
||||
}*/
|
||||
|
||||
.MainPage .HoveringURL {
|
||||
max-width: 95%;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
div[id*="-permissionDialog"] {
|
||||
max-width: 350px;
|
||||
}
|
37
src/renderer/css/components/MattermostView.css
Normal file
37
src/renderer/css/components/MattermostView.css
Normal file
@@ -0,0 +1,37 @@
|
||||
.mattermostView {
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.mattermostView-hidden {
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mattermostView .ErrorView {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mattermostView webview {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.mattermostView-hidden webview {
|
||||
visibility: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.mattermostView-error webview {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.allow-extra-bar webview {
|
||||
top: 76px;
|
||||
}
|
4
src/renderer/css/components/NewTeamModal.css
Normal file
4
src/renderer/css/components/NewTeamModal.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.NewTeamModal-noBottomSpace {
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
51
src/renderer/css/components/PermissionRequestDialog.css
Normal file
51
src/renderer/css/components/PermissionRequestDialog.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.PermissionRequestDialog-content{
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .popover-content {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content > *:nth-child(1) {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .PermissionRequestDialog-content-description {
|
||||
text-indent: 0.25em;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .PermissionRequestDialog-content-description .glyphicon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .PermissionRequestDialog-content-buttons {
|
||||
margin-bottom: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .PermissionRequestDialog-content-buttons > * {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .PermissionRequestDialog-content-buttons > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .PermissionRequestDialog-content-close {
|
||||
position:absolute;
|
||||
top: 0px;
|
||||
right:0px;
|
||||
color: gray;
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .PermissionRequestDialog-content-close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.permission-modal .modal-dialog {
|
||||
max-width: 580px;
|
||||
}
|
||||
.permission-modal .remove-border {
|
||||
border: none;
|
||||
}
|
261
src/renderer/css/components/TabBar.css
Normal file
261
src/renderer/css/components/TabBar.css
Normal file
@@ -0,0 +1,261 @@
|
||||
.TabBar {
|
||||
border: none;
|
||||
max-height: 36px;
|
||||
flex: 1 1 auto;
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
-webkit-app-region: drag;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.TabBar.darkMode {
|
||||
background-color: #202124;
|
||||
}
|
||||
|
||||
.TabBar .teamTabItem span {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.TabBar>li {
|
||||
-webkit-app-region: no-drag;
|
||||
-webkit-user-select: none;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.TabBar>li>a {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
line-height: 16px;
|
||||
margin-right: -1px;
|
||||
padding: 6px 0;
|
||||
color: rgba(61,60,64,0.7);
|
||||
font-family: Arial;
|
||||
font-size: 14px;
|
||||
letter-spacing: -0.2px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li>a {
|
||||
color: rgba(243,243,243,0.7);
|
||||
}
|
||||
|
||||
.TabBar>li>a:hover {
|
||||
background-color: rgba(255,255,255,0.4);
|
||||
text-decoration: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li>a:hover {
|
||||
background-color: rgba(50, 54, 57, 0.4);
|
||||
}
|
||||
|
||||
.TabBar>li>a:focus {
|
||||
background-color: #fff;
|
||||
color: rgba(61,60,64,1);
|
||||
text-decoration: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li>a:focus {
|
||||
background-color: #323639;
|
||||
color: rgba(243,243,243,1);
|
||||
}
|
||||
|
||||
.TabBar>li:before, .TabBar>li:after {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
content: "";
|
||||
background-color: inherit;
|
||||
z-index: 9;
|
||||
flex: 0 0 6px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem.active:before, .TabBar>li.teamTabItem.smooth-dnd-ghost:before {
|
||||
left: -4px;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-right: 2px solid #fff;
|
||||
border-bottom: 2px solid #fff;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.teamTabItem.active:before, .TabBar.darkMode>li.teamTabItem.smooth-dnd-ghost:before {
|
||||
border-right: 2px solid #323639;
|
||||
border-bottom: 2px solid #323639;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem.active:after, .TabBar>li.teamTabItem.smooth-dnd-ghost:after {
|
||||
border-bottom-left-radius: 6px;
|
||||
right: -5px;
|
||||
border-left: 2px solid #fff;
|
||||
border-bottom: 2px solid #fff;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.teamTabItem.active:after, .TabBar.darkMode>li.teamTabItem.smooth-dnd-ghost:after {
|
||||
border-left: 2px solid #323639;
|
||||
border-bottom: 2px solid #323639;
|
||||
}
|
||||
|
||||
.TabBar>li>a>div.TabBar-tabSeperator {
|
||||
padding: 2px 16px;
|
||||
max-height: 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.TabBar>li.TabBar-addServerButton{
|
||||
transition: none !important;
|
||||
transform: none !important;
|
||||
flex: 0 0 auto;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.TabBar>li.TabBar-addServerButton>a{
|
||||
color: rgba(61,60,64,0.7);
|
||||
transition: opacity 0.3s ease-in;
|
||||
}
|
||||
|
||||
.TabBar>li.TabBar-addServerButton svg{
|
||||
margin: -2px;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.TabBar-addServerButton>a{
|
||||
color: rgba(243,243,243,0.7);
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem.active>a, .TabBar>li.teamTabItem.smooth-dnd-ghost>a {
|
||||
border: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
color: rgba(61,60,64,1);
|
||||
background-color: #fff;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.smooth-dnd-no-user-select li.TabBar-addServerButton>a {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.teamTabItem.active>a, .TabBar.darkMode>li.teamTabItem.smooth-dnd-ghost>a {
|
||||
color: #f3f3f3;
|
||||
background-color: #323639;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active)+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
|
||||
border-left: 1px solid rgba(61,60,64,0.2);
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
||||
border-left: 1px solid rgba(61,60,64,0.2);
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.teamTabItem:not(.active)+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
|
||||
border-left: 1px solid rgba(243,243,243,0.2);
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
||||
border-left: 1px solid rgba(243,243,243,0.2);
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active):not(.disabled):hover+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
|
||||
border-left: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active):not(.disabled):hover+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
||||
border-left: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active):not(.disabled)+.TabBar-addServerButton>a:hover>div.TabBar-tabSeperator {
|
||||
border-left: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active):not(.disabled)+li.teamTabItem:not(.active)>a:hover>div.TabBar-tabSeperator {
|
||||
border-left: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.TabBar .TabBar-addServerButton>a {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.TabBar.darkMode .TabBar-addServerButton>a {
|
||||
color: rgba(243,243,243,0.7);
|
||||
}
|
||||
|
||||
.TabBar .TabBar-dot {
|
||||
background: #579EFF;
|
||||
float: right;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
border-radius: 4px;
|
||||
flex: 0 0 6px;
|
||||
}
|
||||
|
||||
.TabBar .TabBar-expired {
|
||||
float: right;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: 8px;
|
||||
background-image: url(../../../assets/icon-session-expired.svg);
|
||||
flex: 0 0 16px;
|
||||
}
|
||||
|
||||
.TabBar.darkMode .TabBar-expired {
|
||||
filter: invert(100%);
|
||||
-webkit-filter: invert(100%);
|
||||
}
|
||||
|
||||
.TabBar .TabBar-badge {
|
||||
background: #CB2431;
|
||||
float: right;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
height: 18px;
|
||||
margin-left: 8px;
|
||||
border-radius: 100px;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: bold;
|
||||
min-width: 18px;
|
||||
margin-top: -1px;
|
||||
letter-spacing: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
flex: 1 0 18px;
|
||||
}
|
||||
|
||||
.TabBar .TabBar-badge.TabBar-badge-nomention:after {
|
||||
content: "";
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: #fff;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.TabBar .teamTabItem-unread {
|
||||
font-weight: bold;
|
||||
}
|
8
src/renderer/css/components/TeamListItem.css
Normal file
8
src/renderer/css/components/TeamListItem.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.TeamListItem:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.TeamListItem-left {
|
||||
display: inline-block;
|
||||
width: calc(100% - 100px);
|
||||
}
|
16
src/renderer/css/components/UpdaterPage.css
Normal file
16
src/renderer/css/components/UpdaterPage.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.UpdaterPage-heading {
|
||||
font-size: 20pt;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
.UpdaterPage-skipButton {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.UpdaterPage-footer {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.UpdaterPage .progress-bar {
|
||||
min-width: 2em;
|
||||
}
|
14
src/renderer/css/components/index.css
Normal file
14
src/renderer/css/components/index.css
Normal file
@@ -0,0 +1,14 @@
|
||||
@import url("ErrorView.css");
|
||||
@import url("HoveringURL.css");
|
||||
@import url("MainPage.css");
|
||||
@import url("MattermostView.css");
|
||||
@import url("NewTeamModal.css");
|
||||
@import url("PermissionRequestDialog.css");
|
||||
@import url("TabBar.css");
|
||||
@import url("TeamListItem.css");
|
||||
@import url("Finder.css");
|
||||
@import url("UpdaterPage.css");
|
||||
@import url("CertificateModal.css");
|
||||
@import url("ExtraBar.css");
|
||||
@import url("LoadingScreen.css");
|
||||
@import url("LoadingAnimation.css");
|
252
src/renderer/css/index.css
Normal file
252
src/renderer/css/index.css
Normal file
@@ -0,0 +1,252 @@
|
||||
@import url("components/index.css");
|
||||
@import '~font-awesome/css/font-awesome.css';
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-300.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-300italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-600.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-600italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.hovering-enter {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
.hovering-enter.hovering-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.hovering-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hovering-exit.hovering-exit-active {
|
||||
opacity: 0.01;
|
||||
transition: opacity 500ms ease-in-out;
|
||||
}
|
||||
|
||||
.has-error .control-label,
|
||||
.has-error .help-block {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-error {
|
||||
color: #a94442;
|
||||
}
|
||||
|
||||
.topBar {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.topBar>.topBar-bg {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 36px;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.topBar>.topBar-bg.unfocused {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.topBar.darkMode {
|
||||
background-color: #323639;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.topBar.darkMode>.topBar-bg {
|
||||
background-color: #202124;
|
||||
}
|
||||
|
||||
.topBar .three-dot-menu {
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
height: 36px;
|
||||
float: left;
|
||||
padding-top: 5px;
|
||||
border: none;
|
||||
flex: 0 0 40px;
|
||||
z-index: 9;
|
||||
color: rgba(61,60,64,0.7);
|
||||
-webkit-app-region: no-drag;
|
||||
background-color: rgba(229, 229, 229, 1);
|
||||
}
|
||||
|
||||
.topBar .three-dot-menu svg {
|
||||
border-radius: 100px;
|
||||
padding: 4px;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.topBar .three-dot-menu:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.topBar .three-dot-menu:hover svg, .topBar .three-dot-menu:focus svg, .topBar .three-dot-menu:active svg {
|
||||
outline: none;
|
||||
background-color: #c8c8c8;
|
||||
}
|
||||
|
||||
.topBar.darkMode .three-dot-menu:hover svg, .topBar.darkMode .three-dot-menu:focus svg, .topBar.darkMode .three-dot-menu:active svg {
|
||||
background-color: #383A3F;
|
||||
}
|
||||
|
||||
.topBar.macOS .three-dot-menu:hover svg, .topBar.macOS .three-dot-menu:focus svg, .topBar.macOS .three-dot-menu:active svg {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.topBar.macOS .three-dot-menu {
|
||||
flex-basis: 80px;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.topBar.macOS.fullScreen .three-dot-menu {
|
||||
flex-basis: 0px;
|
||||
}
|
||||
|
||||
.topBar.macOS .three-dot-menu>svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topBar.darkMode .three-dot-menu {
|
||||
background-color: #202124;
|
||||
color: rgba(243,243,243,0.7);
|
||||
}
|
||||
|
||||
.topBar.darkMode .title-bar-btns {
|
||||
color: rgba(243,243,243,0.7);
|
||||
background-color: #202124;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns {
|
||||
position: relative;
|
||||
line-height: 36px;
|
||||
height: 36px;
|
||||
z-index: 9;
|
||||
color: rgba(61,60,64,0.7);
|
||||
-webkit-app-region: no-drag;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 46px);
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.button {
|
||||
grid-row: 1 / span 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.topBar.darkMode .title-bar-btns>.button:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.topBar.darkMode .title-bar-btns>.button:active {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.button:hover {
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.button:active {
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button:hover {
|
||||
background: #E81123 !important;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button:hover>img {
|
||||
filter: invert(100%);
|
||||
-webkit-filter: invert(100%);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button:active {
|
||||
background: #f1707a !important;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button:active>img {
|
||||
filter: invert(100%);
|
||||
-webkit-filter: invert(100%);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns img {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.topBar.darkMode .title-bar-btns img {
|
||||
filter: invert(100%);
|
||||
-webkit-filter: invert(100%);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.min-button {
|
||||
grid-column: 1;
|
||||
}
|
||||
.topBar .title-bar-btns>.max-button, .topBar .title-bar-btns>.restore-button {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button {
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
.topBar .overlay-gradient {
|
||||
flex: 0 0 40px;
|
||||
z-index: 9;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #e5e5e5 100%);
|
||||
-webkit-app-region: drag;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.topBar.darkMode .overlay-gradient {
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #202124 100%);
|
||||
}
|
3
src/renderer/css/modals.css
Normal file
3
src/renderer/css/modals.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: transparent;
|
||||
}
|
36
src/renderer/css/settings.css
Normal file
36
src/renderer/css/settings.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.CloseButton:hover span {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.IndicatorContainer {
|
||||
float: left;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.AutoSaveIndicator {
|
||||
padding: 5px 15px;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.AutoSaveIndicator.AutoSaveIndicator-Leave {
|
||||
opacity: 0;
|
||||
transition: opacity 1s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
|
||||
.TeamListItem p {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.checkbox > label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
height: 100%;
|
||||
}
|
44
src/renderer/hooks/useAnimationEnd.js
Normal file
44
src/renderer/hooks/useAnimationEnd.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* A custom hook to implement an animationend listener on the provided ref
|
||||
* @param {object} ref - A reference to a DOM element to add the listener to
|
||||
* @param {function} callback - A callback function that will be run for matching animation events
|
||||
* @param {string} animationName - The name of the animation to listen for
|
||||
* @param {boolean} listenForEventBubbling - A parameter that when true, listens for events on the target element and
|
||||
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
|
||||
* ignores events bubbling up from descendent elements
|
||||
*/
|
||||
function useAnimationEnd(
|
||||
ref,
|
||||
callback,
|
||||
animationName,
|
||||
listenForEventBubbling = true,
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleAnimationend(event) {
|
||||
if (!listenForEventBubbling && event.target !== ref.current) {
|
||||
return;
|
||||
}
|
||||
if (animationName && animationName !== event.animationName) {
|
||||
return;
|
||||
}
|
||||
callback(event);
|
||||
}
|
||||
|
||||
ref.current.addEventListener('animationend', handleAnimationend);
|
||||
|
||||
return () => {
|
||||
ref.current.removeEventListener('animationend', handleAnimationend);
|
||||
};
|
||||
}, [ref, callback, animationName, listenForEventBubbling]);
|
||||
}
|
||||
|
||||
export default useAnimationEnd;
|
57
src/renderer/hooks/useTransitionEnd.js
Normal file
57
src/renderer/hooks/useTransitionEnd.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* A custom hook to implement a transitionend listener on the provided ref
|
||||
* @param {object} ref - A reference to a DOM element to add the listener to
|
||||
* @param {function} callback - A callback function that will be run for matching animation events
|
||||
* @param {array} properties - An array of css property strings to listen for
|
||||
* @param {boolean} listenForEventBubbling - A parameter that when true, listens for events on the target element and
|
||||
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
|
||||
* ignores events bubbling up from descendent elements
|
||||
*/
|
||||
function useTransitionend(
|
||||
ref,
|
||||
callback,
|
||||
properties,
|
||||
listenForEventBubbling = true,
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleTransitionEnd(event) {
|
||||
if (!listenForEventBubbling && event.target !== ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (properties && typeof properties === 'object') {
|
||||
const property = properties.find(
|
||||
(propertyName) => propertyName === event.propertyName,
|
||||
);
|
||||
if (property) {
|
||||
callback(event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
callback(event);
|
||||
}
|
||||
|
||||
ref.current.addEventListener('transitionend', handleTransitionEnd);
|
||||
|
||||
return () => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
ref.current.removeEventListener(
|
||||
'transitionend',
|
||||
handleTransitionEnd,
|
||||
);
|
||||
};
|
||||
}, [ref, callback, properties, listenForEventBubbling]);
|
||||
}
|
||||
|
||||
export default useTransitionend;
|
10
src/renderer/index.html
Normal file
10
src/renderer/index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" />
|
||||
</body>
|
||||
</html>
|
133
src/renderer/index.jsx
Normal file
133
src/renderer/index.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/index.css';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
window.eval = global.eval = () => { // eslint-disable-line no-multi-assign, no-eval
|
||||
throw new Error('Sorry, Mattermost does not support window.eval() for security reasons.');
|
||||
};
|
||||
} else if (module.hot) {
|
||||
module.hot.accept();
|
||||
}
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import {GET_CONFIGURATION, UPDATE_TEAMS, QUIT, RELOAD_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import MainPage from './components/MainPage.jsx';
|
||||
class Root extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await this.setInitialConfig();
|
||||
|
||||
ipcRenderer.on('synchronize-config', () => {
|
||||
this.reloadConfig();
|
||||
});
|
||||
|
||||
ipcRenderer.on(RELOAD_CONFIGURATION, () => {
|
||||
this.reloadConfig();
|
||||
});
|
||||
|
||||
// Deny drag&drop navigation in mainWindow.
|
||||
// Drag&drop is allowed in webview of index.html.
|
||||
document.addEventListener('dragover', (event) => event.preventDefault());
|
||||
document.addEventListener('drop', (event) => event.preventDefault());
|
||||
}
|
||||
|
||||
setInitialConfig = async () => {
|
||||
const config = await this.requestConfig(true);
|
||||
this.setState({config});
|
||||
}
|
||||
|
||||
moveTabs = async (originalOrder, newOrder) => {
|
||||
const teams = this.state.config.teams.concat();
|
||||
const tabOrder = teams.map((team, index) => {
|
||||
return {
|
||||
index,
|
||||
order: team.order,
|
||||
};
|
||||
}).sort((a, b) => (a.order - b.order));
|
||||
|
||||
const team = tabOrder.splice(originalOrder, 1);
|
||||
tabOrder.splice(newOrder, 0, team[0]);
|
||||
|
||||
let teamIndex;
|
||||
tabOrder.forEach((t, order) => {
|
||||
if (order === newOrder) {
|
||||
teamIndex = t.index;
|
||||
}
|
||||
teams[t.index].order = order;
|
||||
});
|
||||
await this.teamConfigChange(teams);
|
||||
return teamIndex;
|
||||
};
|
||||
|
||||
teamConfigChange = async (updatedTeams, callback) => {
|
||||
const updatedConfig = await ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams);
|
||||
await this.reloadConfig();
|
||||
if (callback) {
|
||||
callback(updatedConfig);
|
||||
}
|
||||
};
|
||||
|
||||
reloadConfig = async () => {
|
||||
const config = await this.requestConfig();
|
||||
this.setState({config});
|
||||
};
|
||||
|
||||
requestConfig = async (exitOnError) => {
|
||||
// todo: should we block?
|
||||
try {
|
||||
const configRequest = await ipcRenderer.invoke(GET_CONFIGURATION);
|
||||
return configRequest;
|
||||
} catch (err) {
|
||||
console.log(`there was an error with the config: ${err}`);
|
||||
if (exitOnError) {
|
||||
ipcRenderer.send(QUIT, `unable to load configuration: ${err}`, err.stack);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
openMenu = () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
ipcRenderer.send('open-app-menu');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {config} = this.state;
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MainPage
|
||||
teams={config.teams}
|
||||
showAddServerButton={config.enableServerManagement}
|
||||
moveTabs={this.moveTabs}
|
||||
openMenu={this.openMenu}
|
||||
darkMode={config.darkMode}
|
||||
appName={config.appName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
ipcRenderer.invoke('get-app-version').then(({name, version}) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
console.log(`Starting ${name} v${version} commit: ${__HASH_VERSION__}`);
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<Root/>,
|
||||
document.getElementById('app'),
|
||||
);
|
38
src/renderer/modals/certificate/certificate.jsx
Normal file
38
src/renderer/modals/certificate/certificate.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
|
||||
|
||||
import SelectCertificateModal from './certificateModal.jsx';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
import 'renderer/css/components/CertificateModal.css';
|
||||
|
||||
const handleCancel = () => {
|
||||
window.postMessage({type: MODAL_CANCEL}, window.location.href);
|
||||
};
|
||||
|
||||
const handleSelect = (cert) => {
|
||||
window.postMessage({type: MODAL_RESULT, data: {cert}}, window.location.href);
|
||||
};
|
||||
|
||||
const getCertInfo = () => {
|
||||
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<SelectCertificateModal
|
||||
onSelect={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
getCertInfo={getCertInfo}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
||||
start();
|
182
src/renderer/modals/certificate/certificateModal.jsx
Normal file
182
src/renderer/modals/certificate/certificateModal.jsx
Normal file
@@ -0,0 +1,182 @@
|
||||
// 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, Table, Row, Col} from 'react-bootstrap';
|
||||
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
|
||||
import ShowCertificateModal from '../../components/showCertificateModal.jsx';
|
||||
|
||||
export default class SelectCertificateModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
getCertInfo: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedIndex: null,
|
||||
showCertificate: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.handleCertInfoMessage);
|
||||
|
||||
this.props.getCertInfo();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.handleCertInfoMessage);
|
||||
}
|
||||
|
||||
handleCertInfoMessage = (event) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {url, list} = event.data.data;
|
||||
this.setState({url, list});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
selectfn = (index) => {
|
||||
return (() => {
|
||||
this.setState({selectedIndex: index});
|
||||
});
|
||||
};
|
||||
|
||||
renderCert = (cert, index) => {
|
||||
const issuer = (cert.issuerName || (cert.issuer && cert.issuer.commonName) || '');
|
||||
const subject = (cert.subjectName || (cert.subject && cert.subject.commonName) || '');
|
||||
const serial = cert.serialNumber || '';
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={`cert-${index}`}
|
||||
onClick={this.selectfn(index)}
|
||||
className={this.state.selectedIndex === index ? 'selected' : ''}
|
||||
>
|
||||
<td
|
||||
title={subject}
|
||||
>{subject}</td>
|
||||
<td
|
||||
title={issuer}
|
||||
>{issuer}</td>
|
||||
<td
|
||||
title={serial}
|
||||
>{serial}</td>
|
||||
</tr>);
|
||||
};
|
||||
|
||||
renderCerts = (certificateList) => {
|
||||
if (certificateList) {
|
||||
const certs = certificateList.map(this.renderCert);
|
||||
return (
|
||||
<Fragment>
|
||||
{certs}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (<Fragment><tr/><tr><td/><td>{'No certificates available'}</td><td/></tr></Fragment>);
|
||||
}
|
||||
|
||||
getSelectedCert = () => {
|
||||
return this.state.selectedIndex === null ? null : this.state.list[this.state.selectedIndex];
|
||||
};
|
||||
|
||||
handleOk = () => {
|
||||
const cert = this.getSelectedCert();
|
||||
if (cert !== null) {
|
||||
this.props.onSelect(cert);
|
||||
}
|
||||
}
|
||||
|
||||
handleCertificateInfo = () => {
|
||||
const certificate = this.getSelectedCert();
|
||||
this.setState({showCertificate: certificate});
|
||||
}
|
||||
|
||||
certificateInfoClose = () => {
|
||||
this.setState({showCertificate: null});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.showCertificate) {
|
||||
return (
|
||||
<ShowCertificateModal
|
||||
certificate={this.state.showCertificate}
|
||||
onOk={this.certificateInfoClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='certificate-modal'
|
||||
show={Boolean(this.state.list && this.state.url)}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title >{'Select a certificate'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className={'subtitle'}>{`Select a certificate to authenticate yourself to ${this.state.url}`}</p>
|
||||
<Table
|
||||
striped={true}
|
||||
hover={true}
|
||||
size={'sm'}
|
||||
responsive={true}
|
||||
className='certificate-list'
|
||||
tabIndex={1}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span className={'divider'}>{'Subject'}</span></th>
|
||||
<th><span className={'divider'}>{'Issuer'}</span></th>
|
||||
<th>{'Serial'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderCerts(this.state.list)}
|
||||
<tr/* this is to correct table height without affecting real rows *//>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'no-border'}>
|
||||
<div className={'container-fluid'}>
|
||||
<Row>
|
||||
<Col sm={4}>
|
||||
<Button
|
||||
variant={'info'}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
onClick={this.handleCertificateInfo}
|
||||
className={'info'}
|
||||
>{'Certificate Information'}</Button>
|
||||
</Col>
|
||||
<Col sm={8}>
|
||||
<Button
|
||||
onClick={this.props.onCancel}
|
||||
variant={'secondary'}
|
||||
className={'secondary'}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={this.handleOk}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
className={'primary'}
|
||||
>{'OK'}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
178
src/renderer/modals/finder/finder.jsx
Normal file
178
src/renderer/modals/finder/finder.jsx
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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 Finder extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
foundInPage: false,
|
||||
searchTxt: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.searchInput.focus();
|
||||
|
||||
// synthetic events are not working all that reliably for touch bar with esc keys
|
||||
this.searchInput.addEventListener('keyup', this.handleKeyEvent);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (state.searchTxt) {
|
||||
return {
|
||||
foundInPage: Boolean(props.matches),
|
||||
matches: `${props.activeMatchOrdinal}/${props.matches}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {matches: '0/0'};
|
||||
}
|
||||
|
||||
findNext = () => {
|
||||
this.props.findInPage(this.state.searchTxt, {
|
||||
forward: true,
|
||||
findNext: true,
|
||||
});
|
||||
};
|
||||
|
||||
find = (keyword) => {
|
||||
this.props.stopFindInPage('clearSelection');
|
||||
if (keyword) {
|
||||
this.props.findInPage(keyword);
|
||||
} else {
|
||||
this.setState({
|
||||
matches: '0/0',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
findPrev = () => {
|
||||
this.props.findInPage(this.state.searchTxt, {forward: false, findNext: true});
|
||||
}
|
||||
|
||||
searchTxt = (event) => {
|
||||
this.setState({searchTxt: event.target.value});
|
||||
this.find(event.target.value);
|
||||
}
|
||||
|
||||
handleKeyEvent = (event) => {
|
||||
if (event.code === 'Escape') {
|
||||
this.close();
|
||||
} else if (event.code === 'Enter') {
|
||||
this.findNext();
|
||||
}
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.searchInput.removeEventListener('keyup', this.handleKeyEvent);
|
||||
this.props.stopFindInPage('clearSelection');
|
||||
this.props.close();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
id='finder'
|
||||
onClick={this.props.focus}
|
||||
>
|
||||
<div className='finder'>
|
||||
<div className='finder-input-wrapper'>
|
||||
<input
|
||||
className='finder-input'
|
||||
placeholder=''
|
||||
value={this.state.searchTxt}
|
||||
onChange={this.searchTxt}
|
||||
ref={(input) => {
|
||||
this.searchInput = input;
|
||||
}}
|
||||
/>
|
||||
<span className={this.state.foundInPage ? 'finder-progress' : 'finder-progress finder-progress__disabled'}>{this.state.matches}</span>
|
||||
</div>
|
||||
<button
|
||||
className='finder-prev'
|
||||
onClick={this.findPrev}
|
||||
disabled={!this.state.searchTxt}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
className='icon'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<polyline points='18 15 12 9 6 15'/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className='finder-next'
|
||||
onClick={this.findNext}
|
||||
disabled={!this.state.searchTxt}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
className='icon arrow-up'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<polyline points='6 9 12 15 18 9'/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className='finder-close'
|
||||
onClick={this.close}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
className='icon'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<line
|
||||
x1='18'
|
||||
y1='6'
|
||||
x2='6'
|
||||
y2='18'
|
||||
/>
|
||||
<line
|
||||
x1='6'
|
||||
y1='6'
|
||||
x2='18'
|
||||
y2='18'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Finder.propTypes = {
|
||||
close: PropTypes.func,
|
||||
findInPage: PropTypes.func,
|
||||
stopFindInPage: PropTypes.func,
|
||||
activeMatchOrdinal: PropTypes.number,
|
||||
matches: PropTypes.number,
|
||||
focus: PropTypes.func,
|
||||
};
|
73
src/renderer/modals/finder/index.jsx
Normal file
73
src/renderer/modals/finder/index.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {FIND_IN_PAGE, STOP_FIND_IN_PAGE, CLOSE_FINDER, FOUND_IN_PAGE, FOCUS_FINDER} from 'common/communication.js';
|
||||
|
||||
import Finder from './finder.jsx';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
import 'renderer/css/components/Finder.css';
|
||||
|
||||
const closeFinder = () => {
|
||||
window.postMessage({type: CLOSE_FINDER}, window.location.href);
|
||||
};
|
||||
|
||||
const findInPage = (searchText, options) => {
|
||||
window.postMessage({type: FIND_IN_PAGE, data: {searchText, options}}, window.location.href);
|
||||
};
|
||||
|
||||
const stopFindInPage = (action) => {
|
||||
window.postMessage({type: STOP_FIND_IN_PAGE, data: action}, window.location.href);
|
||||
};
|
||||
|
||||
const focusFinder = () => {
|
||||
window.postMessage({type: FOCUS_FINDER}, window.location.href);
|
||||
};
|
||||
class FinderRoot extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.handleMessageEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.handleMessageEvent);
|
||||
}
|
||||
|
||||
handleMessageEvent = (event) => {
|
||||
if (event.data.type === FOUND_IN_PAGE) {
|
||||
this.setState({
|
||||
activeMatchOrdinal: event.data.data.activeMatchOrdinal,
|
||||
matches: event.data.data.matches,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Finder
|
||||
activeMatchOrdinal={this.state.activeMatchOrdinal}
|
||||
matches={this.state.matches}
|
||||
close={closeFinder}
|
||||
focus={focusFinder}
|
||||
findInPage={findInPage}
|
||||
stopFindInPage={stopFindInPage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<FinderRoot/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
||||
start();
|
70
src/renderer/modals/loadingScreen/index.jsx
Normal file
70
src/renderer/modals/loadingScreen/index.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication.js';
|
||||
|
||||
import LoadingScreen from '../../components/LoadingScreen.jsx';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
import 'renderer/css/components/LoadingAnimation.css';
|
||||
import 'renderer/css/components/LoadingScreen.css';
|
||||
|
||||
class LoadingScreenRoot extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
showLoadingScreen: true,
|
||||
darkMode: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.postMessage({type: GET_LOADING_SCREEN_DATA}, window.location.href);
|
||||
|
||||
window.addEventListener('message', this.handleMessageEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.handleMessageEvent);
|
||||
}
|
||||
|
||||
handleMessageEvent = (event) => {
|
||||
if (event.data.type === RECEIVED_LOADING_SCREEN_DATA) {
|
||||
this.setState({
|
||||
darkMode: event.data.data.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.type === TOGGLE_LOADING_SCREEN_VISIBILITY) {
|
||||
this.setState({
|
||||
showLoadingScreen: event.data.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onFadeOutComplete = () => {
|
||||
window.postMessage({type: LOADING_SCREEN_ANIMATION_FINISHED}, window.location.href);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LoadingScreen
|
||||
loading={this.state.showLoadingScreen}
|
||||
darkMode={this.state.darkMode}
|
||||
onFadeOutComplete={this.onFadeOutComplete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<LoadingScreenRoot/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
||||
start();
|
37
src/renderer/modals/login/login.jsx
Normal file
37
src/renderer/modals/login/login.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
|
||||
|
||||
import LoginModal from './loginModal.jsx';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
||||
const handleLoginCancel = (request) => {
|
||||
window.postMessage({type: MODAL_CANCEL, data: {request}}, window.location.href);
|
||||
};
|
||||
|
||||
const handleLogin = (request, username, password) => {
|
||||
window.postMessage({type: MODAL_RESULT, data: {request, username, password}}, window.location.href);
|
||||
};
|
||||
|
||||
const getAuthInfo = () => {
|
||||
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<LoginModal
|
||||
onLogin={handleLogin}
|
||||
onCancel={handleLoginCancel}
|
||||
getAuthInfo={getAuthInfo}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
||||
start();
|
156
src/renderer/modals/login/loginModal.jsx
Normal file
156
src/renderer/modals/login/loginModal.jsx
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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, Col, ControlLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap';
|
||||
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
export default class LoginModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.handleAuthInfoMessage);
|
||||
|
||||
this.props.getAuthInfo();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.handleAuthInfoMessage);
|
||||
}
|
||||
|
||||
handleAuthInfoMessage = (event) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {request, authInfo} = event.data.data;
|
||||
this.setState({request, authInfo});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
this.props.onLogin(this.state.request, this.state.username, this.state.password);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
});
|
||||
}
|
||||
|
||||
handleCancel = (event) => {
|
||||
event.preventDefault();
|
||||
this.props.onCancel(this.state.request);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
});
|
||||
}
|
||||
|
||||
setUsername = (e) => {
|
||||
this.setState({username: e.target.value});
|
||||
}
|
||||
|
||||
setPassword = (e) => {
|
||||
this.setState({password: e.target.value});
|
||||
}
|
||||
|
||||
render() {
|
||||
let theServer = '';
|
||||
if (!(this.state.request && this.state.authInfo)) {
|
||||
theServer = '';
|
||||
} else if (this.state.authInfo.isProxy) {
|
||||
theServer = `The proxy ${this.state.authInfo.host}:${this.state.authInfo.port}`;
|
||||
} else {
|
||||
const tmpURL = urlUtils.parseURL(this.state.request.url);
|
||||
theServer = `The server ${tmpURL.protocol}//${tmpURL.host}`;
|
||||
}
|
||||
const message = `${theServer} requires a username and password.`;
|
||||
return (
|
||||
<Modal show={Boolean(this.state.request && this.state.authInfo)}>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{'Authentication Required'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
{ message }
|
||||
</p>
|
||||
<Form
|
||||
horizontal={true}
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<Col
|
||||
componentClass={ControlLabel}
|
||||
sm={2}
|
||||
>{'User Name'}</Col>
|
||||
<Col sm={10}>
|
||||
<FormControl
|
||||
type='text'
|
||||
placeholder='User Name'
|
||||
onChange={this.setUsername}
|
||||
value={this.state.username}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Col
|
||||
componentClass={ControlLabel}
|
||||
sm={2}
|
||||
>{'Password'}</Col>
|
||||
<Col sm={10}>
|
||||
<FormControl
|
||||
type='password'
|
||||
placeholder='Password'
|
||||
onChange={this.setPassword}
|
||||
value={this.state.password}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Col sm={12}>
|
||||
<div className='pull-right'>
|
||||
<Button
|
||||
type='submit'
|
||||
bsStyle='primary'
|
||||
>{'Login'}</Button>
|
||||
{ ' ' }
|
||||
<Button onClick={this.handleCancel}>{'Cancel'}</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginModal.propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
onLogin: PropTypes.func,
|
||||
getAuthInfo: PropTypes.func,
|
||||
};
|
38
src/renderer/modals/newServer/newServer.jsx
Normal file
38
src/renderer/modals/newServer/newServer.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication.js';
|
||||
|
||||
import NewTeamModal from '../../components/NewTeamModal.jsx'; //'./addServer.jsx';
|
||||
|
||||
const onClose = () => {
|
||||
window.postMessage({type: MODAL_CANCEL}, window.location.href);
|
||||
};
|
||||
|
||||
const onSave = (data) => {
|
||||
window.postMessage({type: MODAL_RESULT, data}, window.location.href);
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<NewTeamModal
|
||||
onClose={onClose}
|
||||
onSave={onSave}
|
||||
editMode={false}
|
||||
show={true}
|
||||
url={decodeURIComponent(urlParams.get('url'))}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
||||
start();
|
42
src/renderer/modals/permission/permission.jsx
Normal file
42
src/renderer/modals/permission/permission.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication.js';
|
||||
|
||||
import PermissionModal from './permissionModal.jsx';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
||||
const handleDeny = () => {
|
||||
window.postMessage({type: MODAL_CANCEL}, window.location.href);
|
||||
};
|
||||
|
||||
const handleGrant = () => {
|
||||
window.postMessage({type: MODAL_RESULT}, window.location.href);
|
||||
};
|
||||
|
||||
const getPermissionInfo = () => {
|
||||
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
|
||||
};
|
||||
|
||||
const openExternalLink = (protocol, url) => {
|
||||
window.postMessage({type: MODAL_SEND_IPC_MESSAGE, data: {type: 'confirm-protocol', args: [protocol, url]}}, window.location.href);
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<PermissionModal
|
||||
getPermissionInfo={getPermissionInfo}
|
||||
handleDeny={handleDeny}
|
||||
handleGrant={handleGrant}
|
||||
openExternalLink={openExternalLink}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
||||
start();
|
109
src/renderer/modals/permission/permissionModal.jsx
Normal file
109
src/renderer/modals/permission/permissionModal.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal, Button} from 'react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import urlUtil from 'common/utils/url';
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
import {PERMISSION_DESCRIPTION} from 'common/permissions';
|
||||
|
||||
export default class PermissionModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.handlePermissionInfoMessage);
|
||||
|
||||
this.props.getPermissionInfo();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.handlePermissionInfoMessage);
|
||||
}
|
||||
|
||||
handlePermissionInfoMessage = (event) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {url, permission} = event.data.data;
|
||||
this.setState({url, permission});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getModalTitle() {
|
||||
return `${PERMISSION_DESCRIPTION[this.state.permission]} Required`;
|
||||
}
|
||||
|
||||
getModalBody() {
|
||||
const {url, permission} = this.state;
|
||||
const originDisplay = url ? urlUtil.getHost(url) : 'unknown origin';
|
||||
const originLink = url ? originDisplay : '';
|
||||
|
||||
const click = (e) => {
|
||||
e.preventDefault();
|
||||
let parseUrl;
|
||||
try {
|
||||
parseUrl = urlUtil.parseURL(originLink);
|
||||
this.props.openExternalLink(parseUrl.protocol, originLink);
|
||||
} catch (err) {
|
||||
console.error(`invalid url ${originLink} supplied to externallink: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission]}.`}
|
||||
</p>
|
||||
<p>
|
||||
<span>{'This request originated from '}</span>
|
||||
<a onClick={click}>{originDisplay}</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='permission-modal'
|
||||
show={Boolean(this.state.url && this.state.permission)}
|
||||
id='requestPermissionModal'
|
||||
enforceFocus={true}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{this.getModalTitle()}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.getModalBody()}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'remove-border'}>
|
||||
<div>
|
||||
<Button
|
||||
onClick={this.props.handleDeny}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
onClick={this.props.handleGrant}
|
||||
>{'Accept'}</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PermissionModal.propTypes = {
|
||||
handleDeny: PropTypes.func,
|
||||
handleGrant: PropTypes.func,
|
||||
getPermissionInfo: PropTypes.func,
|
||||
openExternalLink: PropTypes.func,
|
||||
};
|
8
src/renderer/modals/urlView/index.html
Normal file
8
src/renderer/modals/urlView/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<body>
|
||||
<div id="app" />
|
||||
</body>
|
||||
</html>
|
23
src/renderer/modals/urlView/urlView.jsx
Normal file
23
src/renderer/modals/urlView/urlView.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import 'renderer/css/components/HoveringURL.css';
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import UrlDescription from '../../components/urlDescription.jsx';
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<UrlDescription
|
||||
url={decodeURIComponent(urlParams.get('url'))}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
||||
start();
|
30
src/renderer/notificationSounds.js
Normal file
30
src/renderer/notificationSounds.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {throttle} from 'underscore';
|
||||
|
||||
import ding from 'static/sounds/ding.mp3';
|
||||
import bing from 'static/sounds/bing.mp3';
|
||||
import crackle from 'static/sounds/crackle.mp3';
|
||||
import down from 'static/sounds/down.mp3';
|
||||
import hello from 'static/sounds/hello.mp3';
|
||||
import ripple from 'static/sounds/ripple.mp3';
|
||||
import upstairs from 'static/sounds/upstairs.mp3';
|
||||
|
||||
export const DEFAULT_WIN7 = 'Ding';
|
||||
const notificationSounds = new Map([
|
||||
[DEFAULT_WIN7, ding],
|
||||
['Bing', bing],
|
||||
['Crackle', crackle],
|
||||
['Down', down],
|
||||
['Hello', hello],
|
||||
['Ripple', ripple],
|
||||
['Upstairs', upstairs],
|
||||
]);
|
||||
|
||||
export const playSound = throttle((soundName) => {
|
||||
if (soundName) {
|
||||
const audio = new Audio(notificationSounds.get(soundName));
|
||||
audio.play();
|
||||
}
|
||||
}, 3000, {trailing: false});
|
43
src/renderer/settings.jsx
Normal file
43
src/renderer/settings.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/index.css';
|
||||
import 'renderer/css/settings.css';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
window.eval = global.eval = () => { // eslint-disable-line no-multi-assign, no-eval
|
||||
throw new Error('Sorry, Mattermost does not support window.eval() for security reasons.');
|
||||
};
|
||||
} else if (module.hot) {
|
||||
module.hot.accept();
|
||||
}
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import SettingsPage from './components/SettingsPage.jsx';
|
||||
|
||||
function openMenu() {
|
||||
if (process.platform !== 'darwin') {
|
||||
ipcRenderer.send('open-app-menu');
|
||||
}
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<SettingsPage
|
||||
openMenu={openMenu}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
||||
|
||||
// Deny drag&drop navigation in mainWindow.
|
||||
document.addEventListener('dragover', (event) => event.preventDefault());
|
||||
document.addEventListener('drop', (event) => event.preventDefault());
|
||||
|
||||
start();
|
13
src/renderer/updater.html
Normal file
13
src/renderer/updater.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Updater</title>
|
||||
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="css/components/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content"></div>
|
||||
<script src="updater_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
157
src/renderer/updater.jsx
Normal file
157
src/renderer/updater.jsx
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import url from 'url';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import propTypes from 'prop-types';
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
|
||||
import UpdaterPage from './components/UpdaterPage.jsx';
|
||||
|
||||
const thisURL = url.parse(location.href, true);
|
||||
const notifyOnly = thisURL.query.notifyOnly === 'true';
|
||||
|
||||
class UpdaterPageContainer extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = props.initialState;
|
||||
}
|
||||
|
||||
getTabWebContents() {
|
||||
return remote.webContents.getFocusedWebContents();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ipcRenderer.on('start-download', () => {
|
||||
this.setState({
|
||||
isDownloading: true,
|
||||
});
|
||||
});
|
||||
ipcRenderer.on('progress', (event, progress) => {
|
||||
this.setState({
|
||||
progress,
|
||||
});
|
||||
});
|
||||
ipcRenderer.on('zoom-in', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
if (activeTabWebContents.zoomLevel >= 9) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.zoomLevel += 1;
|
||||
});
|
||||
|
||||
ipcRenderer.on('zoom-out', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
if (activeTabWebContents.zoomLevel <= -8) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.zoomLevel -= 1;
|
||||
});
|
||||
|
||||
ipcRenderer.on('zoom-reset', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.zoomLevel = 0;
|
||||
});
|
||||
|
||||
ipcRenderer.on('undo', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.undo();
|
||||
});
|
||||
|
||||
ipcRenderer.on('redo', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.redo();
|
||||
});
|
||||
|
||||
ipcRenderer.on('cut', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.cut();
|
||||
});
|
||||
|
||||
ipcRenderer.on('copy', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.copy();
|
||||
});
|
||||
|
||||
ipcRenderer.on('paste', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.paste();
|
||||
});
|
||||
|
||||
ipcRenderer.on('paste-and-match', () => {
|
||||
const activeTabWebContents = this.getTabWebContents();
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.pasteAndMatchStyle();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<UpdaterPage
|
||||
appName={`${remote.app.name} Desktop App`}
|
||||
notifyOnly={this.props.notifyOnly}
|
||||
{...this.state}
|
||||
onClickReleaseNotes={() => {
|
||||
ipcRenderer.send('click-release-notes');
|
||||
}}
|
||||
onClickSkip={() => {
|
||||
ipcRenderer.send('click-skip');
|
||||
}}
|
||||
onClickRemind={() => {
|
||||
ipcRenderer.send('click-remind');
|
||||
}}
|
||||
onClickInstall={() => {
|
||||
ipcRenderer.send('click-install');
|
||||
}}
|
||||
onClickDownload={() => {
|
||||
ipcRenderer.send('click-download');
|
||||
}}
|
||||
onClickCancel={() => {
|
||||
ipcRenderer.send('click-cancel');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UpdaterPageContainer.propTypes = {
|
||||
notifyOnly: propTypes.bool,
|
||||
initialState: propTypes.object,
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<UpdaterPageContainer
|
||||
notifyOnly={notifyOnly}
|
||||
initialState={{isDownloading: false, progress: 0}}
|
||||
/>,
|
||||
document.getElementById('content'),
|
||||
);
|
Reference in New Issue
Block a user