[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:
@@ -1,7 +1,11 @@
|
||||
version: 2.1
|
||||
parameters:
|
||||
run_nightly:
|
||||
default: false
|
||||
type: boolean
|
||||
orbs:
|
||||
win: circleci/windows@1.0.0
|
||||
aws-s3: circleci/aws-s3@1.0.11
|
||||
aws-s3: circleci/aws-s3@2.0.0
|
||||
owasp: entur/owasp@0.0.10
|
||||
|
||||
executors:
|
||||
@@ -44,7 +48,6 @@ commands:
|
||||
- run:
|
||||
command: |
|
||||
export VERSION=$(jq -r .version package.json)
|
||||
echo "payload=" > /tmp/webhook-data.json;
|
||||
echo '{}' | jq "{
|
||||
\"username\": \"<< parameters.username >>\",
|
||||
\"icon_url\": \"<< parameters.icon >>\",
|
||||
@@ -52,7 +55,7 @@ commands:
|
||||
}" >> /tmp/webhook-data.json
|
||||
- run:
|
||||
command: |
|
||||
curl -i -X POST -d @/tmp/webhook-data.json $MATTERMOST_RELEASE_WEBHOOK_URL_DESKTOP || echo "NOFICATION FAILED! check logs as this will succeed intentionally"
|
||||
curl -i -H "Content-Type: application/json" -X POST -d @/tmp/webhook-data.json $MATTERMOST_RELEASE_WEBHOOK_URL_DESKTOP || echo "NOFICATION FAILED! check logs as this will succeed intentionally"
|
||||
|
||||
update_image:
|
||||
description: "Update base image"
|
||||
@@ -105,8 +108,7 @@ jobs:
|
||||
apt_opts: "--no-install-recommends"
|
||||
- restore_cache:
|
||||
key: lint-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
|
||||
- run: npm run lint:js-quiet
|
||||
- run: ELECTRON_DISABLE_SANDBOX=1 xvfb-run npm run test:app
|
||||
- run: ELECTRON_DISABLE_SANDBOX=1 xvfb-run npm run test
|
||||
- run: mkdir -p /tmp/test-results
|
||||
- run: cp test-results.xml /tmp/test-results/
|
||||
- store_test_results:
|
||||
@@ -115,68 +117,6 @@ jobs:
|
||||
key: lint-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
|
||||
paths:
|
||||
- "node_modules"
|
||||
- "src/node_modules"
|
||||
|
||||
check-deps:
|
||||
parameters:
|
||||
cve_data_directory:
|
||||
type: string
|
||||
default: "~/.owasp/dependency-check-data"
|
||||
working_directory: ~/mattermost/desktop
|
||||
executor: owasp/default
|
||||
environment:
|
||||
version_url: "https://jeremylong.github.io/DependencyCheck/current.txt"
|
||||
executable_url: "https://dl.bintray.com/jeremy-long/owasp/dependency-check-VERSION-release.zip"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Link dependency cache
|
||||
command: sudo ln -s ~/mattermost/desktop /root/mattermost-desktop; sudo chmod 777 /root
|
||||
- restore_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "src/package-lock.json" }}
|
||||
- run:
|
||||
name: Adjust permissions
|
||||
command: |
|
||||
sudo chown -R `id -nu`:`id -ng` node_modules
|
||||
sudo chown -R `id -nu`:`id -ng` src/node_modules
|
||||
- run:
|
||||
name: Checkout config
|
||||
command: cd .. && git clone https://github.com/mattermost/security-automation-config
|
||||
- run:
|
||||
name: Install Go
|
||||
command: sudo apt-get update && sudo apt-get install golang
|
||||
- owasp/with_commandline:
|
||||
steps:
|
||||
# Taken from https://github.com/entur/owasp-orb/blob/master/src/%40orb.yml#L349-L361
|
||||
- owasp/generate_cache_keys:
|
||||
cache_key: commmandline-default-cache-key-v6
|
||||
- owasp/restore_owasp_cache
|
||||
- run:
|
||||
name: Update OWASP Dependency-Check Database
|
||||
command: |
|
||||
if ! ~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly; then
|
||||
# Update failed, probably due to a bad DB version; delete cached DB and try again
|
||||
rm -rv ~/.owasp/dependency-check-data/*.db
|
||||
~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly
|
||||
fi
|
||||
- owasp/store_owasp_cache:
|
||||
cve_data_directory: <<parameters.cve_data_directory>>
|
||||
- run:
|
||||
name: Run OWASP Dependency-Check Analyzer
|
||||
command: |
|
||||
~/.owasp/dependency-check/bin/dependency-check.sh \
|
||||
--data << parameters.cve_data_directory >> --format ALL --noupdate --enableExperimental \
|
||||
--propertyfile ../security-automation-config/dependency-check/dependencycheck.properties \
|
||||
--suppression ../security-automation-config/dependency-check/suppression.xml \
|
||||
--suppression ../security-automation-config/dependency-check/suppression.$CIRCLE_PROJECT_REPONAME.xml \
|
||||
--scan './**/*' || true
|
||||
- owasp/collect_reports:
|
||||
persist_to_workspace: false
|
||||
- run:
|
||||
name: Post results to Mattermost
|
||||
command: go run ../security-automation-config/dependency-check/post_results.go
|
||||
|
||||
build-linux:
|
||||
executor: wine-mono
|
||||
@@ -187,8 +127,6 @@ jobs:
|
||||
at: ./dist
|
||||
- restore_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "src/package-lock.json" }}
|
||||
- update_image:
|
||||
apt_opts: "--no-install-recommends jq icnsutils graphicsmagick tzdata"
|
||||
- build
|
||||
@@ -198,10 +136,6 @@ jobs:
|
||||
- "node_modules"
|
||||
- "~/.cache/electron"
|
||||
- "~/.cache/electron-builder"
|
||||
- save_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "src/package-lock.json" }}
|
||||
paths:
|
||||
- "src/node_modules"
|
||||
|
||||
build-win-no-installer:
|
||||
executor: wine-mono
|
||||
@@ -212,8 +146,6 @@ jobs:
|
||||
at: ./dist
|
||||
- restore_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "src/package-lock.json" }}
|
||||
- update_image:
|
||||
apt_opts: "--no-install-recommends jq icnsutils graphicsmagick tzdata"
|
||||
- build:
|
||||
@@ -226,10 +158,6 @@ jobs:
|
||||
- "node_modules"
|
||||
- "~/.cache/electron"
|
||||
- "~/.cache/electron-builder"
|
||||
- save_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "src/package-lock.json" }}
|
||||
paths:
|
||||
- "src/node_modules"
|
||||
|
||||
build-mac-no-dmg:
|
||||
executor: wine-mono
|
||||
@@ -240,8 +168,6 @@ jobs:
|
||||
at: ./dist
|
||||
- restore_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
|
||||
- restore_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "src/package-lock.json" }}
|
||||
- update_image:
|
||||
apt_opts: "--no-install-recommends jq icnsutils graphicsmagick tzdata"
|
||||
- run: jq '.mac.target=["zip"]' electron-builder.json | jq '.mac.gatekeeperAssess=false' > /tmp/electron-builder.json && cp /tmp/electron-builder.json .
|
||||
@@ -256,10 +182,6 @@ jobs:
|
||||
- "node_modules"
|
||||
- "~/.cache/electron"
|
||||
- "~/.cache/electron-builder"
|
||||
- save_cache:
|
||||
key: npm-{{ arch }}-{{ .Branch }}-{{ checksum "src/package-lock.json" }}
|
||||
paths:
|
||||
- "src/node_modules"
|
||||
|
||||
msi_installer:
|
||||
executor: win/vs2019
|
||||
@@ -310,6 +232,33 @@ jobs:
|
||||
path: ./dist
|
||||
destination: packages
|
||||
|
||||
share_to_channel:
|
||||
executor: wine-chrome
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ./dist
|
||||
- run: wget -qO - https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_18.04/Release.key | apt-key add -
|
||||
- run: apt-get update && apt-get -y install jq
|
||||
- run: mkdir -p ./links
|
||||
- run: echo "### Nightly builds:\n" > ./links/linklist.txt
|
||||
- run:
|
||||
name: "Get urls for sharing"
|
||||
command: |
|
||||
echo "Links for $(date +"%b-%d-%Y")" > ./links/linklist.txt
|
||||
curl -H "Circle-Token: $CIRCLE_TOKEN" -H "Accept: application/json" -X GET "https://circleci.com/api/v2/project/github/mattermost/desktop/$CIRCLE_PREVIOUS_BUILD_NUM/artifacts" | jq -r '.items[].url' >> ./links/linklist.txt
|
||||
echo "Retrieved links for job #${CIRCLE_PREVIOUS_BUILD_NUM}"
|
||||
- run:
|
||||
command: |
|
||||
linklist=$(<./links/linklist.txt);
|
||||
echo '{}' | jq "{
|
||||
\"username\": \"NightBuilder\",
|
||||
\"icon_url\": \"https://upload.wikimedia.org/wikipedia/commons/1/17/Luna_symbol.png\",
|
||||
\"text\": \"${linklist}\"
|
||||
}" >> /tmp/webhook-data.json
|
||||
- run:
|
||||
command: |
|
||||
curl -i -X POST -H "Content-Type: application/json" -d @/tmp/webhook-data.json $MM_TOKEN || echo "NOFICATION FAILED! check logs as this will succeed intentionally"
|
||||
|
||||
upload_to_s3:
|
||||
executor: aws
|
||||
steps:
|
||||
@@ -332,6 +281,21 @@ jobs:
|
||||
to: s3://releases.mattermost.com/desktop/$(jq -r .version package.json)/
|
||||
arguments: --acl public-read --cache-control "no-cache" --recursive
|
||||
|
||||
upload_to_s3_daily:
|
||||
executor: aws
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./dist
|
||||
- run:
|
||||
name: "Normalize folder names"
|
||||
command: |
|
||||
mv ./dist/macos-release ./dist/macos
|
||||
- aws-s3/copy:
|
||||
from: ./dist/
|
||||
to: s3://mattermost-desktop-daily-builds/
|
||||
arguments: --acl public-read --cache-control "no-cache" --recursive
|
||||
|
||||
upload_to_github:
|
||||
executor: github
|
||||
steps:
|
||||
@@ -402,10 +366,6 @@ workflows:
|
||||
- build-linux:
|
||||
requires:
|
||||
- check
|
||||
- check-deps:
|
||||
context: sast-webhook
|
||||
requires:
|
||||
- build-linux
|
||||
|
||||
- build-win-no-installer:
|
||||
requires:
|
||||
@@ -486,3 +446,28 @@ workflows:
|
||||
# release-XX.YY.ZZ
|
||||
# release-XX.YY.ZZ-rc-something
|
||||
- /^release-\d+(\.\d+){1,2}(-rc.*)?/
|
||||
nightly_browser_view:
|
||||
when: << pipeline.parameters.run_nightly >>
|
||||
jobs:
|
||||
- build-linux
|
||||
- build-win-no-installer:
|
||||
context: electron-installer
|
||||
- mac_installer:
|
||||
context: codesign-certificates
|
||||
- store_artifacts:
|
||||
# for master/PR builds
|
||||
requires:
|
||||
- build-linux
|
||||
- build-win-no-installer
|
||||
- mac_installer
|
||||
- upload_to_s3_daily:
|
||||
context: mattermost-desktop-daily-s3
|
||||
requires:
|
||||
- build-linux
|
||||
- build-win-no-installer
|
||||
- mac_installer
|
||||
- share_to_channel:
|
||||
context: desktop_browserview
|
||||
requires:
|
||||
- store_artifacts
|
||||
|
||||
|
@@ -4,5 +4,5 @@ root = true
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
|
@@ -1,13 +1,20 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:mattermost/react",
|
||||
"plugin:cypress/recommended"
|
||||
"plugin:cypress/recommended",
|
||||
"plugin:jquery/deprecated"
|
||||
],
|
||||
"plugins": [
|
||||
"import",
|
||||
"babel",
|
||||
"mattermost",
|
||||
"cypress"
|
||||
"import",
|
||||
"cypress",
|
||||
"jquery",
|
||||
"no-only-tests",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"jest": true,
|
||||
"cypress/globals": true
|
||||
@@ -16,14 +23,16 @@
|
||||
"import/resolver": "webpack",
|
||||
"react": {
|
||||
"pragma": "React",
|
||||
"version": "16.4"
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-expressions": 0,
|
||||
"babel/no-unused-expressions": 2,
|
||||
"eol-last": ["error", "always"],
|
||||
"import/no-unresolved": 2,
|
||||
"comma-dangle": 0,
|
||||
"import/order": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"newlines-between": "always-and-inside-groups",
|
||||
"groups": [
|
||||
@@ -38,25 +47,9 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-magic-numbers": [
|
||||
1,
|
||||
{
|
||||
"ignore": [
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"enforceConst": true,
|
||||
"detectObjects": true
|
||||
}
|
||||
],
|
||||
"react/jsx-filename-extension": [
|
||||
1,
|
||||
{
|
||||
"extensions": [".js", ".jsx"]
|
||||
}
|
||||
],
|
||||
"no-undefined": 0,
|
||||
"no-use-before-define": 0,
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react/prop-types": [
|
||||
2,
|
||||
{
|
||||
@@ -66,11 +59,74 @@
|
||||
"component"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"react/no-string-refs": 2,
|
||||
"no-only-tests/no-only-tests": ["error", {"focus": ["only", "skip"]}],
|
||||
"react/style-prop-object": [2, {
|
||||
"allow": ["Timestamp"]
|
||||
}]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["tests/**"],
|
||||
"files": ["**/*.tsx", "**/*.ts"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"camelcase": 0,
|
||||
"no-shadow": 0,
|
||||
"import/no-unresolved": 0, // ts handles this better
|
||||
"@typescript-eslint/naming-convention": [
|
||||
2,
|
||||
{
|
||||
"selector": "function",
|
||||
"format": ["camelCase", "PascalCase"]
|
||||
},
|
||||
{
|
||||
"selector": "variable",
|
||||
"format": ["camelCase", "PascalCase", "UPPER_CASE"]
|
||||
},
|
||||
{
|
||||
"selector": "parameter",
|
||||
"format": ["camelCase", "PascalCase"],
|
||||
"leadingUnderscore": "allow"
|
||||
},
|
||||
{
|
||||
"selector": "typeLike",
|
||||
"format": ["PascalCase"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
2,
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "after-used"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
"@typescript-eslint/prefer-interface": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/indent": [
|
||||
2,
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 0
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
2,
|
||||
{
|
||||
"classes": false,
|
||||
"functions": false,
|
||||
"variables": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**", "**/*.test.*"],
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
@@ -78,12 +134,67 @@
|
||||
"func-names": 0,
|
||||
"global-require": 0,
|
||||
"new-cap": 0,
|
||||
"prefer-arrow-callback": 0
|
||||
"prefer-arrow-callback": 0,
|
||||
"no-import-assign": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/e2e/**"],
|
||||
"files": ["e2e/**"],
|
||||
"rules": {
|
||||
"babel/no-unused-expressions": 0,
|
||||
"func-names": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"jquery/no-ajax": 0,
|
||||
"jquery/no-ajax-events": 0,
|
||||
"jquery/no-animate": 0,
|
||||
"jquery/no-attr": 0,
|
||||
"jquery/no-bind": 0,
|
||||
"jquery/no-class": 0,
|
||||
"jquery/no-clone": 0,
|
||||
"jquery/no-closest": 0,
|
||||
"jquery/no-css": 0,
|
||||
"jquery/no-data": 0,
|
||||
"jquery/no-deferred": 0,
|
||||
"jquery/no-delegate": 0,
|
||||
"jquery/no-each": 0,
|
||||
"jquery/no-extend": 0,
|
||||
"jquery/no-fade": 0,
|
||||
"jquery/no-filter": 0,
|
||||
"jquery/no-find": 0,
|
||||
"jquery/no-global-eval": 0,
|
||||
"jquery/no-grep": 0,
|
||||
"jquery/no-has": 0,
|
||||
"jquery/no-hide": 0,
|
||||
"jquery/no-html": 0,
|
||||
"jquery/no-in-array": 0,
|
||||
"jquery/no-is-array": 0,
|
||||
"jquery/no-is-function": 0,
|
||||
"jquery/no-is": 0,
|
||||
"jquery/no-load": 0,
|
||||
"jquery/no-map": 0,
|
||||
"jquery/no-merge": 0,
|
||||
"jquery/no-param": 0,
|
||||
"jquery/no-parent": 0,
|
||||
"jquery/no-parents": 0,
|
||||
"jquery/no-parse-html": 0,
|
||||
"jquery/no-prop": 0,
|
||||
"jquery/no-proxy": 0,
|
||||
"jquery/no-ready": 0,
|
||||
"jquery/no-serialize": 0,
|
||||
"jquery/no-show": 0,
|
||||
"jquery/no-size": 0,
|
||||
"jquery/no-sizzle": 0,
|
||||
"jquery/no-slide": 0,
|
||||
"jquery/no-submit": 0,
|
||||
"jquery/no-text": 0,
|
||||
"jquery/no-toggle": 0,
|
||||
"jquery/no-trigger": 0,
|
||||
"jquery/no-trim": 0,
|
||||
"jquery/no-val": 0,
|
||||
"jquery/no-when": 0,
|
||||
"jquery/no-wrap": 0,
|
||||
"max-nested-callbacks": 0,
|
||||
"no-process-env": 0,
|
||||
"no-unused-expressions": 0
|
||||
}
|
||||
}
|
||||
|
@@ -1,34 +1,59 @@
|
||||
{
|
||||
"extends": [
|
||||
"./.eslintrc-webapp.json",
|
||||
"plugin:eslint-comments/recommended"
|
||||
"./.eslintrc-webapp.json"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": "node"
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"config": "webpack.config.base.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"header/header": [2, "line", [
|
||||
"header/header": [
|
||||
2,
|
||||
"line",
|
||||
[
|
||||
" Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.",
|
||||
" See LICENSE.txt for license information."
|
||||
]],
|
||||
]
|
||||
],
|
||||
"import/no-commonjs": 2,
|
||||
"indent": [2, 2, {"SwitchCase": 0}],
|
||||
"no-console": 0,
|
||||
"no-process-env": 0,
|
||||
"no-underscore-dangle": 1,
|
||||
"no-var": 2,
|
||||
"react/jsx-indent": [2, 2],
|
||||
"react/jsx-indent-props": [2, 2],
|
||||
"react/no-find-dom-node": 2,
|
||||
"react/no-set-state": 1,
|
||||
"react/require-optimization": 0,
|
||||
"multiline-ternary": ["warn", "always-multiline"],
|
||||
"consistent-return": "off"
|
||||
"react/no-find-dom-node": 2
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"scripts/**/*",
|
||||
"src/main/preload/**/*",
|
||||
"src/renderer/**/*"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"test/**/*"
|
||||
],
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"babel/no-unused-expressions": "off", //TODO: rework tests to use correct notation
|
||||
"func-names": 0,
|
||||
"global-require": 0,
|
||||
"new-cap": 0,
|
||||
"prefer-arrow-callback": 0,
|
||||
"no-import-assign": 0,
|
||||
"no-only-tests/no-only-tests": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"webpack.config.renderer.js",
|
||||
@@ -40,7 +65,6 @@
|
||||
"test/specs/browser/settings_test.js",
|
||||
"test/modules/utils.js",
|
||||
"test/modules/environment.js",
|
||||
"webpack.config.main.js",
|
||||
"CHANGELOG.md",
|
||||
"webpack.config.base.js",
|
||||
"babel.config.js",
|
||||
@@ -54,7 +78,6 @@
|
||||
"src/main.js",
|
||||
"src/browser/js/contextMenu.js",
|
||||
"src/browser/updater.jsx",
|
||||
"src/browser/js/notification.js",
|
||||
"src/browser/js/badge.js",
|
||||
"src/browser/webview/mattermost.js",
|
||||
"src/browser/components/RemoveServerModal.jsx",
|
||||
@@ -105,11 +128,15 @@
|
||||
"src/main/menus/app.js"
|
||||
],
|
||||
"rules": {
|
||||
"header/header": [2, "line", [
|
||||
"header/header": [
|
||||
2,
|
||||
"line",
|
||||
[
|
||||
" Copyright (c) 2015-2016 Yuya Ochiai",
|
||||
" Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.",
|
||||
" See LICENSE.txt for license information."
|
||||
]]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,7 +14,4 @@ test_config.json
|
||||
.idea
|
||||
testUserData
|
||||
|
||||
src/browser/*.png
|
||||
src/browser/*.svg
|
||||
src/browser/*.woff2
|
||||
src/browser/assets/fonts/
|
||||
.gitattributes
|
||||
|
@@ -107,3 +107,10 @@ Mattermost Desktop
|
||||
- **node_modules/** - Third party Node.js modules to develop and build the application.
|
||||
- **release/** - Packaged distributable applications.
|
||||
- **src/node_modules/** - Third party Node.js modules to use in the application.
|
||||
|
||||
|
||||
### Developer tools for debugging
|
||||
While you can access the developer tools for the renderer and current browserview, there are some other that usually don't need access. With the new browserview you can automatically call for the devtools when showing the settings window or any of the modals. To do so you'll need to setup environment variables:
|
||||
|
||||
- MM_DEBUG_SETTINGS for the new settings window
|
||||
- MM_DEBUG_MODALS for any modal that needs to be debugged. Currently we can't target only one specifically.
|
@@ -34,7 +34,7 @@ No pull requests for features should be merged to the current release after this
|
||||
- Confirm date of marketing announcement for the release and update Desktop App channel header if needed
|
||||
2. Dev/PM/QA:
|
||||
- Prioritize reviewing, testing, and merging of pull requests for current release until there are no more tickets in the [pull request queue](https://github.com/mattermost/desktop/pulls) marked for the current release
|
||||
- Verify `version` in [package.json](https://github.com/mattermost/desktop/blob/master/package.json) and [src/package.json](https://github.com/mattermost/desktop/blob/master/src/package.json) are updated to the new release version
|
||||
- Verify `version` in [package.json](https://github.com/mattermost/desktop/blob/master/package.json) is updated to the new release version
|
||||
- Master is tagged and branched and "Release Candidate 1" is cut (e.g. 1.1.0-RC1)
|
||||
3. Marketing:
|
||||
- Tweet announcement that RC1 is ready
|
||||
|
@@ -7,17 +7,19 @@
|
||||
"artifactName": "${name}-${version}-${os}-${arch}.${ext}",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"app": "src",
|
||||
"output": "release"
|
||||
},
|
||||
"extraMetadata": {
|
||||
"main": "index.js"
|
||||
},
|
||||
"files": [
|
||||
"main_bundle.js",
|
||||
"browser/**/*{.html,.css,_bundle.js,.svg,.png}",
|
||||
"assets/**/*",
|
||||
"browser/assets/fonts/*",
|
||||
"node_modules/bootstrap/dist/**",
|
||||
"node_modules/font-awesome/{css,fonts}/**",
|
||||
"node_modules/simple-spellchecker/dict/*.dic"
|
||||
{
|
||||
"from": "dist",
|
||||
"to": ".",
|
||||
"filter": "**/*"
|
||||
}
|
||||
],
|
||||
"protocols": [
|
||||
{
|
||||
@@ -35,7 +37,7 @@
|
||||
"afterPack": "scripts/afterpack.js",
|
||||
"afterSign": "scripts/notarize.js",
|
||||
"deb": {
|
||||
"synopsis": "Mattermost"
|
||||
"synopsis": "Mattermost Desktop App"
|
||||
},
|
||||
"linux": {
|
||||
"category": "Network;InstantMessaging",
|
||||
@@ -124,4 +126,3 @@
|
||||
"artifactName": "${name}-setup-${version}-win.${ext}"
|
||||
}
|
||||
}
|
||||
|
||||
|
7746
package-lock.json
generated
7746
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
102
package.json
102
package.json
@@ -3,9 +3,11 @@
|
||||
"productName": "Mattermost",
|
||||
"version": "4.7.0-develop",
|
||||
"description": "Mattermost",
|
||||
"main": "main.js",
|
||||
"main": "dist/index.js",
|
||||
"author": "Mattermost, Inc. <feedback@mattermost.com>",
|
||||
"license": "Apache-2.0",
|
||||
"desktopName": "Mattermost.Desktop",
|
||||
"homepage": "https://about.mattermost.com",
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
},
|
||||
@@ -13,26 +15,39 @@
|
||||
"type": "git",
|
||||
"url": "git://github.com/mattermost/desktop.git"
|
||||
},
|
||||
"config": {
|
||||
"target": "11.3.0",
|
||||
"arch": "x64",
|
||||
"target_arch": "x64",
|
||||
"disturl": "https://electronjs.org/headers",
|
||||
"runtime": "electron",
|
||||
"build_from_source": true
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && npm run extract-dict",
|
||||
"extract-dict": "node scripts/extract_dict.js src/node_modules/simple-spellchecker/dict",
|
||||
"build": "npm-run-all build:*",
|
||||
"build:main": "webpack-cli --bail --config webpack.config.main.js",
|
||||
"build:renderer": "webpack-cli --bail --config webpack.config.renderer.js",
|
||||
"start": "electron src --disable-dev-mode",
|
||||
"build-prod": "npm-run-all build:*",
|
||||
"start": "electron dist/ --disable-dev-mode",
|
||||
"restart": "npm run build && npm run start",
|
||||
"storybook": "start-storybook -p 9001 -c src/.storybook",
|
||||
"clean": "rm -rf release/ node_modules/ src/node_modules/ && find src -name '*_bundle.js' | xargs rm",
|
||||
"clean": "rm -rf release/ node_modules/ dist/ && find src -name '*_bundle.js' | xargs rm",
|
||||
"clean-install": "npm run clean && npm install",
|
||||
"clean-dist": "rm -rf dist/",
|
||||
"watch": "run-p watch:*",
|
||||
"watch:main": "node scripts/watch_main_and_preload.js",
|
||||
"watch:renderer": "webpack-dev-server --config webpack.config.renderer.js",
|
||||
"test": "npm-run-all lint:js test:*",
|
||||
"test:app": "cross-env NODE_ENV=production npm run build && mocha -r @babel/register --reporter mocha-circleci-reporter --recursive test/specs",
|
||||
"test": "npm-run-all lint:js test:unit test:e2e",
|
||||
"test:e2e": "npm-run-all test:e2e:build test:e2e:run",
|
||||
"test:e2e:build": "cross-env NODE_ENV=test npm run build",
|
||||
"test:e2e:run": "cross-env NODE_ENV=test electron-mocha -r @babel/register --reporter mocha-circleci-reporter --recursive test/specs",
|
||||
"test:unit": "npm-run-all test:unit:build test:unit:run",
|
||||
"test:unit:build": "cross-env NODE_ENV=test webpack-cli --bail --config webpack.config.test.js",
|
||||
"test:unit:run": "cross-env NODE_ENV=test mocha --reporter mocha-circleci-reporter dist/tests/test_bundle.js",
|
||||
"package:all": "cross-env NODE_ENV=production npm-run-all check-build-config package:windows package:mac package:linux",
|
||||
"package:windows": "cross-env NODE_ENV=production npm-run-all check-build-config build && electron-builder --win --x64 --ia32 --publish=never",
|
||||
"package:mac": "cross-env NODE_ENV=production npm-run-all check-build-config build && electron-builder --mac --publish=never",
|
||||
"package:linux": "cross-env NODE_ENV=production npm-run-all check-build-config build && electron-builder --linux --x64 --ia32 --publish=never",
|
||||
"package:windows": "cross-env NODE_ENV=production npm-run-all check-build-config build-prod && electron-builder --win --x64 --ia32 --publish=never",
|
||||
"package:mac": "cross-env NODE_ENV=production npm-run-all check-build-config build-prod && electron-builder --mac --publish=never",
|
||||
"package:linux": "cross-env NODE_ENV=production npm-run-all check-build-config build-prod && electron-builder --linux --x64 --ia32 --publish=never",
|
||||
"lint:js": "eslint --ignore-path .gitignore --ignore-pattern node_modules --ext .js --ext .jsx .",
|
||||
"lint:js-quiet": "eslint --ignore-path .gitignore --ignore-pattern node_modules --ext .js --ext .jsx . --quiet",
|
||||
"fix:js": "eslint --ignore-path .gitignore --ignore-pattern node_modules --quiet --ext .js --ext .jsx . --fix",
|
||||
@@ -44,42 +59,73 @@
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.2.0",
|
||||
"@babel/preset-env": "^7.2.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@babel/register": "^7.0.0",
|
||||
"@storybook/addon-actions": "^4.0.11",
|
||||
"@storybook/react": "^4.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "4.15.0",
|
||||
"@typescript-eslint/parser": "4.15.0",
|
||||
"awesome-node-loader": "^1.1.1",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-loader": "^8.0.4",
|
||||
"chai": "^4.2.0",
|
||||
"copy-webpack-plugin": "^6.2.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.1",
|
||||
"devtron": "^1.4.0",
|
||||
"electron": "^7.3.2",
|
||||
"electron-builder": "^22.2.0",
|
||||
"electron": "^11.3.0",
|
||||
"electron-builder": "^22.10.5",
|
||||
"electron-connect": "^0.6.3",
|
||||
"electron-notarize": "^0.1.1",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-plugin-cypress": "^2.7.0",
|
||||
"eslint-plugin-eslint-comments": "^3.1.2",
|
||||
"eslint-plugin-header": "^3.0.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#070ce792d105482ffb2b27cfc0b7e78b3d20acee",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"electron-mocha": "^10.0.0",
|
||||
"electron-notarize": "^1.0.0",
|
||||
"electron-webpack": "^2.8.2",
|
||||
"eslint": "7.19.0",
|
||||
"eslint-import-resolver-webpack": "0.13.0",
|
||||
"eslint-plugin-babel": "5.3.1",
|
||||
"eslint-plugin-cypress": "2.11.2",
|
||||
"eslint-plugin-header": "3.1.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-jquery": "1.5.1",
|
||||
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#46ad99355644a719bf32082f472048f526605181",
|
||||
"eslint-plugin-no-only-tests": "2.4.0",
|
||||
"eslint-plugin-react": "7.22.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"image-webpack-loader": "5.0.0",
|
||||
"mdi-react": "^6.2.0",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-circleci-reporter": "0.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"spectron": "^13.0.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"typescript": "4.1.3",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-merge": "^4.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/joi": "^16.1.8",
|
||||
"auto-launch": "^5.0.5",
|
||||
"bootstrap": "^3.3.7",
|
||||
"brace-expansion": "^2.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"electron-context-menu": "^2.5.0",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-log": "^4.3.2",
|
||||
"electron-updater": "4.3.8",
|
||||
"font-awesome": "^4.7.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.6.3",
|
||||
"react-bootstrap": "~0.32.4",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-smooth-dnd": "github:mattermost/react-smooth-dnd#af6b471295007274560a375799622c1cd52d678a",
|
||||
"spectron": "^9.0.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.14",
|
||||
"webpack-merge": "^4.1.4"
|
||||
"react-transition-group": "^2.5.0",
|
||||
"semver": "^5.5.0",
|
||||
"underscore": "^1.9.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"winreg-utf8": "^0.1.1",
|
||||
"yargs": "^15.3.1"
|
||||
}
|
||||
}
|
||||
|
@@ -20,12 +20,12 @@ if [[ -f "${SRC}/mattermost-desktop-${VERSION}-win-x64.zip" ]]; then
|
||||
cp "${SRC}/mattermost-desktop-${VERSION}-win-x64.zip" "${DEST}/mattermost-desktop-${VERSION}-win64.zip"
|
||||
SOMETHING_COPIED=$((SOMETHING_COPIED + 2))
|
||||
fi
|
||||
# We are not supplying this since we supply the msi
|
||||
# if [[ -f "${SRC}/mattermost-desktop-setup-${VERSION}-win.exe" ]]; then
|
||||
# echo -e "Copying win-no-arch\n"
|
||||
# cp "${SRC}/mattermost-desktop-setup-${VERSION}-win.exe" "${DEST}/"
|
||||
# SOMETHING_COPIED=$((SOMETHING_COPIED + 4))
|
||||
# fi
|
||||
|
||||
if [[ ${MM_WIN_INSTALLERS-0} -eq 1 && -f "${SRC}/mattermost-desktop-setup-${VERSION}-win.exe" ]]; then
|
||||
echo -e "Copying win-no-arch\n"
|
||||
cp "${SRC}/mattermost-desktop-setup-${VERSION}-win.exe" "${DEST}/"
|
||||
SOMETHING_COPIED=$((SOMETHING_COPIED + 4))
|
||||
fi
|
||||
if [[ -f "${SRC}/mattermost-desktop-${VERSION}-mac.zip" ]]; then
|
||||
echo -e "Copying mac\n"
|
||||
cp "${SRC}"/mattermost-desktop-*-mac.* "${DEST}/"
|
||||
|
@@ -4,11 +4,12 @@
|
||||
'use strict';
|
||||
|
||||
const spawnSync = require('child_process').spawnSync;
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const path7za = require('7zip-bin').path7za;
|
||||
|
||||
const pkg = require('../src/package.json');
|
||||
const pkg = require('../package.json');
|
||||
const appVersion = pkg.version;
|
||||
const name = pkg.name;
|
||||
|
||||
|
@@ -26,12 +26,8 @@ function write_package_version {
|
||||
jq ".version = \"${1}\"" ./package.json > "${temp_file}" && mv "${temp_file}" ./package.json
|
||||
temp_file="$(mktemp -t package-lock.json)"
|
||||
jq ".version = \"${1}\"" ./package-lock.json > "${temp_file}" && mv "${temp_file}" ./package-lock.json
|
||||
temp_file="$(mktemp -t src-package.json)"
|
||||
jq ".version = \"${1}\"" ./src/package.json > "${temp_file}" && mv "${temp_file}" ./src/package.json
|
||||
temp_file="$(mktemp -t src-package-lock.json)"
|
||||
jq ".version = \"${1}\"" ./src/package-lock.json > "${temp_file}" && mv "${temp_file}" ./src/package-lock.json
|
||||
|
||||
git add ./package.json ./package-lock.json ./src/package.json ./src/package-lock.json
|
||||
git add ./package.json ./package-lock.json
|
||||
git commit -qm "Bump to version ${1}"
|
||||
}
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
const webpack = require('webpack');
|
||||
const electron = require('electron-connect').server.create({path: 'src'});
|
||||
const electron = require('electron-connect').server.create({path: 'dist/'});
|
||||
|
||||
const mainConfig = require('../webpack.config.main.js');
|
||||
const rendererConfig = require('../webpack.config.renderer.js');
|
||||
@@ -23,13 +23,6 @@ mainCompiler.watch({}, (err, stats) => {
|
||||
}
|
||||
});
|
||||
|
||||
for (const key in rendererConfig.entry) {
|
||||
if (!key.startsWith('webview/')) {
|
||||
if ({}.hasOwnProperty.call(rendererConfig.entry, key)) {
|
||||
delete rendererConfig.entry[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
const preloadCompiler = webpack(rendererConfig);
|
||||
preloadCompiler.watch({}, (err) => {
|
||||
if (err) {
|
||||
|
BIN
src/assets/fonts/fontawesome-webfont.eot
Normal file
BIN
src/assets/fonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
BIN
src/assets/fonts/fontawesome-webfont.ttf
Normal file
BIN
src/assets/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/fontawesome-webfont.woff
Normal file
BIN
src/assets/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/fontawesome-webfont.woff2
Normal file
BIN
src/assets/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import 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,
|
||||
});
|
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
||||
|
||||
import {action} from '@storybook/addon-actions';
|
||||
import {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>
|
||||
));
|
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import 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,
|
||||
};
|
@@ -1,84 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// 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';
|
||||
import {shell, remote} from 'electron';
|
||||
|
||||
export default function ErrorView(props) {
|
||||
const classNames = ['container', 'ErrorView'];
|
||||
if (!props.active) {
|
||||
classNames.push('ErrorView-hidden');
|
||||
}
|
||||
function handleClick(event) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(props.errorInfo.validatedURL);
|
||||
}
|
||||
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 ${remote.app.name}`}</h2>
|
||||
<hr/>
|
||||
<p>{`We're having trouble connecting to ${remote.app.name}. 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 ${remote.app.name} URL `}
|
||||
<a
|
||||
onClick={handleClick}
|
||||
href={props.errorInfo.validatedURL}
|
||||
>
|
||||
{props.errorInfo.validatedURL}
|
||||
</a>{' is correct.'}</li>
|
||||
<li>{'You can reach '}
|
||||
<a
|
||||
onClick={handleClick}
|
||||
href={props.errorInfo.validatedURL}
|
||||
>
|
||||
{props.errorInfo.validatedURL}
|
||||
</a>{' from a browser window.'}</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<div className='ErrorView-techInfo'>
|
||||
{props.errorInfo.errorDescription}{' ('}
|
||||
{props.errorInfo.errorCode }{')'}</div>
|
||||
</Col>
|
||||
<Col
|
||||
xs={0}
|
||||
sm={1}
|
||||
md={1}
|
||||
lg={2}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorView.propTypes = {
|
||||
errorInfo: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
};
|
@@ -1,50 +0,0 @@
|
||||
// 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.Component {
|
||||
handleBack = () => {
|
||||
if (this.props.mattermostView) {
|
||||
this.props.mattermostView.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,
|
||||
mattermostView: PropTypes.object,
|
||||
show: PropTypes.bool,
|
||||
};
|
@@ -1,189 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// eslint-disable-next-line eslint-comments/disable-enable-pair
|
||||
/* eslint-disable react/no-set-state */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class Finder extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.webview = document.getElementById('mattermostView' + this.props.webviewKey);
|
||||
this.state = {
|
||||
foundInPage: false,
|
||||
searchTxt: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.webview.addEventListener('found-in-page', this.foundInPage);
|
||||
this.searchInput.focus();
|
||||
|
||||
// synthetic events are not working all that reliably for touch bar with esc keys
|
||||
this.searchInput.addEventListener('keyup', this.handleKeyEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.webview.stopFindInPage('clearSelection');
|
||||
this.webview.removeEventListener('found-in-page', this.foundInPage);
|
||||
this.searchInput.removeEventListener('keyup', this.handleKeyEvent);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.focusState && (this.props.focusState !== prevProps.focusState)) {
|
||||
this.searchInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
findNext = () => {
|
||||
this.webview.findInPage(this.state.searchTxt, {
|
||||
forward: true,
|
||||
findNext: true,
|
||||
});
|
||||
};
|
||||
|
||||
find = (keyword) => {
|
||||
this.webview.stopFindInPage('clearSelection');
|
||||
if (keyword) {
|
||||
this.webview.findInPage(keyword);
|
||||
} else {
|
||||
this.setState({
|
||||
matches: '0/0',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
findPrev = () => {
|
||||
this.webview.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.props.close();
|
||||
} else if (event.code === 'Enter') {
|
||||
this.findNext();
|
||||
}
|
||||
}
|
||||
|
||||
foundInPage = (event) => {
|
||||
const {matches, activeMatchOrdinal} = event.result;
|
||||
this.setState({
|
||||
foundInPage: true,
|
||||
matches: `${activeMatchOrdinal}/${matches}`,
|
||||
});
|
||||
}
|
||||
|
||||
inputFocus = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.inputFocus(e, true);
|
||||
}
|
||||
|
||||
inputBlur = (e) => {
|
||||
this.props.inputFocus(e, false);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id='finder'>
|
||||
<div className={`finder${process.platform === 'darwin' ? ' macOS' : ''}`}>
|
||||
<div className='finder-input-wrapper'>
|
||||
<input
|
||||
className='finder-input'
|
||||
placeholder=''
|
||||
value={this.state.searchTxt}
|
||||
onChange={this.searchTxt}
|
||||
onBlur={this.inputBlur}
|
||||
onClick={this.inputFocus}
|
||||
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}
|
||||
>
|
||||
<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}
|
||||
>
|
||||
<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.props.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,
|
||||
webviewKey: PropTypes.number,
|
||||
focusState: PropTypes.bool,
|
||||
inputFocus: PropTypes.func,
|
||||
};
|
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function HoveringURL(props) {
|
||||
return (
|
||||
<div className='HoveringURL HoveringURL-left'>
|
||||
{props.targetURL}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HoveringURL.propTypes = {
|
||||
targetURL: PropTypes.string,
|
||||
};
|
@@ -1,93 +0,0 @@
|
||||
// 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;
|
@@ -1,197 +0,0 @@
|
||||
// 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;
|
@@ -1,75 +0,0 @@
|
||||
// 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
|
||||
*/
|
||||
function LoadingScreen({loading = false, darkMode = false}) {
|
||||
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);
|
||||
}), ['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,
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
@@ -1,126 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Col, ControlLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap';
|
||||
|
||||
export default class LoginModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
this.props.onLogin(this.props.request, this.state.username, this.state.password);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
}
|
||||
|
||||
handleCancel = (event) => {
|
||||
event.preventDefault();
|
||||
this.props.onCancel(this.props.request);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
}
|
||||
|
||||
setUsername = (e) => {
|
||||
this.setState({username: e.target.value});
|
||||
}
|
||||
|
||||
setPassword = (e) => {
|
||||
this.setState({password: e.target.value});
|
||||
}
|
||||
|
||||
render() {
|
||||
let theServer = '';
|
||||
if (!this.props.show) {
|
||||
theServer = '';
|
||||
} else if (this.props.authInfo.isProxy) {
|
||||
theServer = `The proxy ${this.props.authInfo.host}:${this.props.authInfo.port}`;
|
||||
} else {
|
||||
theServer = `The server ${this.props.authServerURL}`;
|
||||
}
|
||||
const message = `${theServer} requires a username and password.`;
|
||||
return (
|
||||
<Modal show={this.props.show}>
|
||||
<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 = {
|
||||
authInfo: PropTypes.object,
|
||||
authServerURL: PropTypes.string,
|
||||
onCancel: PropTypes.func,
|
||||
onLogin: PropTypes.func,
|
||||
request: PropTypes.object,
|
||||
show: PropTypes.bool,
|
||||
};
|
@@ -1,912 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// This files uses setState().
|
||||
/* eslint-disable react/no-set-state */
|
||||
|
||||
import os from 'os';
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {CSSTransition, TransitionGroup} from 'react-transition-group';
|
||||
import {Grid, Row} from 'react-bootstrap';
|
||||
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
|
||||
|
||||
import {ipcRenderer, remote, shell} from 'electron';
|
||||
|
||||
import Utils from '../../utils/util';
|
||||
import urlUtils from '../../utils/url';
|
||||
import contextmenu from '../js/contextMenu';
|
||||
|
||||
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 LoginModal from './LoginModal.jsx';
|
||||
import MattermostView from './MattermostView.jsx';
|
||||
import TabBar from './TabBar.jsx';
|
||||
import HoveringURL from './HoveringURL.jsx';
|
||||
import Finder from './Finder.jsx';
|
||||
import NewTeamModal from './NewTeamModal.jsx';
|
||||
import SelectCertificateModal from './SelectCertificateModal.jsx';
|
||||
import PermissionModal from './PermissionModal.jsx';
|
||||
import ExtraBar from './ExtraBar.jsx';
|
||||
|
||||
export default class MainPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let key = this.props.teams.findIndex((team) => team.order === this.props.initialIndex);
|
||||
if (this.props.deeplinkingUrl !== null) {
|
||||
const parsedDeeplink = this.parseDeeplinkURL(this.props.deeplinkingUrl);
|
||||
if (parsedDeeplink) {
|
||||
key = parsedDeeplink.teamIndex;
|
||||
}
|
||||
}
|
||||
|
||||
this.topBar = React.createRef();
|
||||
|
||||
this.state = {
|
||||
key,
|
||||
sessionsExpired: new Array(this.props.teams.length),
|
||||
unreadCounts: new Array(this.props.teams.length),
|
||||
mentionCounts: new Array(this.props.teams.length),
|
||||
unreadAtActive: new Array(this.props.teams.length),
|
||||
mentionAtActiveCounts: new Array(this.props.teams.length),
|
||||
loginQueue: [],
|
||||
targetURL: '',
|
||||
certificateRequests: [],
|
||||
maximized: false,
|
||||
showNewTeamModal: false,
|
||||
focusFinder: false,
|
||||
finderVisible: false,
|
||||
};
|
||||
contextmenu.setup({
|
||||
useSpellChecker: this.props.useSpellChecker,
|
||||
onSelectSpellCheckerLocale: (locale) => {
|
||||
if (this.props.onSelectSpellCheckerLocale) {
|
||||
this.props.onSelectSpellCheckerLocale(locale);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
parseDeeplinkURL(deeplink, teams = this.props.teams) {
|
||||
if (deeplink && Array.isArray(teams) && teams.length) {
|
||||
const deeplinkURL = urlUtils.parseURL(deeplink);
|
||||
let parsedDeeplink = null;
|
||||
teams.forEach((team, index) => {
|
||||
const teamURL = urlUtils.parseURL(team.url);
|
||||
if (deeplinkURL.host === teamURL.host) {
|
||||
parsedDeeplink = {
|
||||
teamURL,
|
||||
teamIndex: index,
|
||||
originalURL: deeplinkURL,
|
||||
url: `${teamURL.origin}${deeplinkURL.pathname || '/'}`,
|
||||
path: deeplinkURL.pathname || '/',
|
||||
};
|
||||
}
|
||||
});
|
||||
return parsedDeeplink;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getTabWebContents(index = this.state.key || 0, teams = this.props.teams) {
|
||||
const allWebContents = remote.webContents.getAllWebContents();
|
||||
|
||||
if (this.state.showNewTeamModal) {
|
||||
const indexURL = '/browser/index.html';
|
||||
return allWebContents.find((webContents) => webContents.getURL().includes(indexURL));
|
||||
}
|
||||
|
||||
if (!teams || !teams.length || index > teams.length) {
|
||||
return null;
|
||||
}
|
||||
const tabURL = teams[index].url;
|
||||
if (!tabURL) {
|
||||
return null;
|
||||
}
|
||||
const tab = allWebContents.find((webContents) => webContents.isFocused() && webContents.getURL().includes(this.refs[`mattermostView${index}`].getSrc()));
|
||||
return tab || remote.webContents.getFocusedWebContents();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const self = this;
|
||||
|
||||
// Due to a bug in Chrome on macOS, mousemove events from the webview won't register when the webview isn't in focus,
|
||||
// thus you can't drag tabs unless you're right on the container.
|
||||
// this makes it so your tab won't get stuck to your cursor no matter where you mouse up
|
||||
if (process.platform === 'darwin') {
|
||||
self.topBar.current.addEventListener('mouseleave', () => {
|
||||
if (event.target === self.topBar.current) {
|
||||
const upEvent = document.createEvent('MouseEvents');
|
||||
upEvent.initMouseEvent('mouseup');
|
||||
document.dispatchEvent(upEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Hack for when it leaves the electron window because apparently mouseleave isn't good enough there...
|
||||
self.topBar.current.addEventListener('mousemove', () => {
|
||||
if (event.clientY === 0 || event.clientX === 0 || event.clientX >= window.innerWidth) {
|
||||
const upEvent = document.createEvent('MouseEvents');
|
||||
upEvent.initMouseEvent('mouseup');
|
||||
document.dispatchEvent(upEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on('login-request', (event, request, authInfo) => {
|
||||
this.loginRequest(event, request, authInfo);
|
||||
});
|
||||
|
||||
ipcRenderer.on('select-user-certificate', (_, origin, certificateList) => {
|
||||
const certificateRequests = self.state.certificateRequests;
|
||||
certificateRequests.push({
|
||||
server: origin,
|
||||
certificateList,
|
||||
});
|
||||
self.setState({
|
||||
certificateRequests,
|
||||
});
|
||||
if (certificateRequests.length === 1) {
|
||||
self.switchToTabForCertificateRequest(origin);
|
||||
}
|
||||
});
|
||||
|
||||
// can't switch tabs sequentially for some reason...
|
||||
ipcRenderer.on('switch-tab', (event, key) => {
|
||||
const nextIndex = this.props.teams.findIndex((team) => team.order === key);
|
||||
this.handleSelect(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);
|
||||
this.handleSelect(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);
|
||||
this.handleSelect(nextIndex);
|
||||
});
|
||||
|
||||
// reload the activated tab
|
||||
ipcRenderer.on('reload-tab', () => {
|
||||
this.refs[`mattermostView${this.state.key}`].reload();
|
||||
});
|
||||
ipcRenderer.on('clear-cache-and-reload-tab', () => {
|
||||
this.refs[`mattermostView${this.state.key}`].clearCacheAndReload();
|
||||
});
|
||||
ipcRenderer.on('download-complete', this.showDownloadCompleteNotification);
|
||||
|
||||
const currentWindow = remote.getCurrentWindow();
|
||||
currentWindow.on('focus', self.focusListener);
|
||||
currentWindow.on('blur', self.blurListener);
|
||||
window.addEventListener('beforeunload', () => {
|
||||
currentWindow.removeListener('focus', self.focusListener);
|
||||
});
|
||||
|
||||
if (currentWindow.isMaximized()) {
|
||||
self.setState({maximized: true});
|
||||
}
|
||||
currentWindow.on('maximize', this.handleMaximizeState);
|
||||
currentWindow.on('unmaximize', this.handleMaximizeState);
|
||||
|
||||
if (currentWindow.isFullScreen()) {
|
||||
self.setState({fullScreen: true});
|
||||
}
|
||||
currentWindow.on('enter-full-screen', this.handleFullScreenState);
|
||||
currentWindow.on('leave-full-screen', this.handleFullScreenState);
|
||||
|
||||
// https://github.com/mattermost/desktop/pull/371#issuecomment-263072803
|
||||
currentWindow.webContents.on('devtools-closed', () => {
|
||||
self.focusListener();
|
||||
});
|
||||
|
||||
ipcRenderer.on('open-devtool', () => {
|
||||
document.getElementById(`mattermostView${self.state.key}`).openDevTools();
|
||||
});
|
||||
|
||||
ipcRenderer.on('zoom-in', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
if (activeTabWebContents.zoomLevel >= 9) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.zoomLevel += 1;
|
||||
});
|
||||
|
||||
ipcRenderer.on('zoom-out', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
if (activeTabWebContents.zoomLevel <= -8) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.zoomLevel -= 1;
|
||||
});
|
||||
|
||||
ipcRenderer.on('zoom-reset', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.zoomLevel = 0;
|
||||
});
|
||||
|
||||
ipcRenderer.on('undo', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.undo();
|
||||
});
|
||||
|
||||
ipcRenderer.on('redo', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.redo();
|
||||
});
|
||||
|
||||
ipcRenderer.on('cut', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.cut();
|
||||
});
|
||||
|
||||
ipcRenderer.on('copy', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.copy();
|
||||
});
|
||||
|
||||
ipcRenderer.on('paste', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.paste();
|
||||
});
|
||||
|
||||
ipcRenderer.on('paste-and-match', () => {
|
||||
const activeTabWebContents = this.getTabWebContents(this.state.key);
|
||||
if (!activeTabWebContents) {
|
||||
return;
|
||||
}
|
||||
activeTabWebContents.pasteAndMatchStyle();
|
||||
});
|
||||
|
||||
//goBack and goForward
|
||||
ipcRenderer.on('go-back', () => {
|
||||
const mattermost = self.refs[`mattermostView${self.state.key}`];
|
||||
if (mattermost.canGoBack()) {
|
||||
mattermost.goBack();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('go-forward', () => {
|
||||
const mattermost = self.refs[`mattermostView${self.state.key}`];
|
||||
if (mattermost.canGoForward()) {
|
||||
mattermost.goForward();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('add-server', () => {
|
||||
this.addServer();
|
||||
});
|
||||
|
||||
ipcRenderer.on('focus-on-webview', () => {
|
||||
this.focusOnWebView();
|
||||
});
|
||||
|
||||
ipcRenderer.on('protocol-deeplink', (event, deepLinkUrl) => {
|
||||
const parsedDeeplink = this.parseDeeplinkURL(deepLinkUrl);
|
||||
if (parsedDeeplink) {
|
||||
if (this.state.key !== parsedDeeplink.teamIndex) {
|
||||
this.handleSelect(parsedDeeplink.teamIndex);
|
||||
}
|
||||
self.refs[`mattermostView${parsedDeeplink.teamIndex}`].handleDeepLink(parsedDeeplink.path);
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-find', () => {
|
||||
this.activateFinder(true);
|
||||
});
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
self.setState({
|
||||
isDarkMode: remote.nativeTheme.shouldUseDarkColors,
|
||||
});
|
||||
remote.systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => {
|
||||
self.setState({
|
||||
isDarkMode: remote.nativeTheme.shouldUseDarkColors,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
self.setState({
|
||||
isDarkMode: this.props.getDarkMode(),
|
||||
});
|
||||
|
||||
ipcRenderer.on('set-dark-mode', () => {
|
||||
this.setDarkMode();
|
||||
});
|
||||
|
||||
this.threeDotMenu = React.createRef();
|
||||
ipcRenderer.on('focus-three-dot-menu', () => {
|
||||
if (this.threeDotMenu.current) {
|
||||
this.threeDotMenu.current.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
focusListener = () => {
|
||||
if (this.state.showNewTeamModal && this.inputRef && this.inputRef.current) {
|
||||
this.inputRef.current.focus();
|
||||
} else if (!(this.state.finderVisible && this.state.focusFinder)) {
|
||||
this.handleOnTeamFocused(this.state.key);
|
||||
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
|
||||
}
|
||||
this.setState({unfocused: false});
|
||||
}
|
||||
|
||||
blurListener = () => {
|
||||
this.setState({unfocused: true});
|
||||
}
|
||||
loginRequest = (event, request, authInfo) => {
|
||||
const loginQueue = this.state.loginQueue;
|
||||
loginQueue.push({
|
||||
request,
|
||||
authInfo,
|
||||
});
|
||||
this.setState({
|
||||
loginRequired: true,
|
||||
loginQueue,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.key !== this.state.key) { // i.e. When tab has been changed
|
||||
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
|
||||
}
|
||||
}
|
||||
|
||||
switchToTabForCertificateRequest = (origin) => {
|
||||
// origin is server name + port, if the port doesn't match the protocol, it is kept by URL
|
||||
const originURL = urlUtils.parseURL(`http://${origin.split(':')[0]}`);
|
||||
const secureOriginURL = urlUtils.parseURL(`https://${origin.split(':')[0]}`);
|
||||
|
||||
const key = this.props.teams.findIndex((team) => {
|
||||
const parsedURL = urlUtils.parseURL(team.url);
|
||||
return (parsedURL.origin === originURL.origin) || (parsedURL.origin === secureOriginURL.origin);
|
||||
});
|
||||
this.handleSelect(key);
|
||||
};
|
||||
|
||||
handleInterTeamLink = (linkUrl) => {
|
||||
const selectedTeam = urlUtils.getServer(linkUrl, this.props.teams);
|
||||
if (!selectedTeam) {
|
||||
return;
|
||||
}
|
||||
this.refs[`mattermostView${selectedTeam.index}`].handleDeepLink(linkUrl.href);
|
||||
this.setState({key: selectedTeam.index});
|
||||
}
|
||||
|
||||
handleMaximizeState = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
this.setState({maximized: win.isMaximized()});
|
||||
}
|
||||
|
||||
handleFullScreenState = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
this.setState({fullScreen: win.isFullScreen()});
|
||||
}
|
||||
|
||||
handleSelect = (key) => {
|
||||
const newKey = (this.props.teams.length + key) % this.props.teams.length;
|
||||
this.setState({
|
||||
key: newKey,
|
||||
finderVisible: false,
|
||||
});
|
||||
const webview = document.getElementById('mattermostView' + newKey);
|
||||
ipcRenderer.send('update-title', {
|
||||
title: webview.getTitle(),
|
||||
});
|
||||
window.focus();
|
||||
webview.focus();
|
||||
this.handleOnTeamFocused(newKey);
|
||||
}
|
||||
|
||||
handleDragAndDrop = (dropResult) => {
|
||||
const {removedIndex, addedIndex} = dropResult;
|
||||
if (removedIndex !== addedIndex) {
|
||||
const teamIndex = this.props.moveTabs(removedIndex, addedIndex < this.props.teams.length ? addedIndex : this.props.teams.length - 1);
|
||||
this.handleSelect(teamIndex);
|
||||
}
|
||||
}
|
||||
|
||||
handleBadgeChange = (index, sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) => {
|
||||
const sessionsExpired = this.state.sessionsExpired;
|
||||
const unreadCounts = this.state.unreadCounts;
|
||||
const mentionCounts = this.state.mentionCounts;
|
||||
const unreadAtActive = this.state.unreadAtActive;
|
||||
const mentionAtActiveCounts = this.state.mentionAtActiveCounts;
|
||||
sessionsExpired[index] = sessionExpired;
|
||||
unreadCounts[index] = unreadCount;
|
||||
mentionCounts[index] = mentionCount;
|
||||
|
||||
// Never turn on the unreadAtActive flag at current focused tab.
|
||||
if (this.state.key !== index || !remote.getCurrentWindow().isFocused()) {
|
||||
unreadAtActive[index] = unreadAtActive[index] || isUnread;
|
||||
if (isMentioned) {
|
||||
mentionAtActiveCounts[index]++;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
sessionsExpired,
|
||||
unreadCounts,
|
||||
mentionCounts,
|
||||
unreadAtActive,
|
||||
mentionAtActiveCounts,
|
||||
});
|
||||
this.handleBadgesChange();
|
||||
}
|
||||
|
||||
markReadAtActive = (index) => {
|
||||
const unreadAtActive = this.state.unreadAtActive;
|
||||
const mentionAtActiveCounts = this.state.mentionAtActiveCounts;
|
||||
unreadAtActive[index] = false;
|
||||
mentionAtActiveCounts[index] = 0;
|
||||
this.setState({
|
||||
unreadAtActive,
|
||||
mentionAtActiveCounts,
|
||||
});
|
||||
this.handleBadgesChange();
|
||||
}
|
||||
|
||||
handleBadgesChange = () => {
|
||||
if (this.props.onBadgeChange) {
|
||||
const someSessionsExpired = this.state.sessionsExpired.some((sessionExpired) => sessionExpired);
|
||||
|
||||
let allUnreadCount = this.state.unreadCounts.reduce((prev, curr) => {
|
||||
return prev + curr;
|
||||
}, 0);
|
||||
this.state.unreadAtActive.forEach((state) => {
|
||||
if (state) {
|
||||
allUnreadCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
let allMentionCount = this.state.mentionCounts.reduce((prev, curr) => {
|
||||
return prev + curr;
|
||||
}, 0);
|
||||
this.state.mentionAtActiveCounts.forEach((count) => {
|
||||
allMentionCount += count;
|
||||
});
|
||||
|
||||
this.props.onBadgeChange(someSessionsExpired, allUnreadCount, allMentionCount);
|
||||
}
|
||||
}
|
||||
|
||||
handleOnTeamFocused = (index) => {
|
||||
// Turn off the flag to indicate whether unread message of active channel contains at current tab.
|
||||
this.markReadAtActive(index);
|
||||
}
|
||||
|
||||
handleLogin = (request, username, password) => {
|
||||
ipcRenderer.send('login-credentials', request, username, password);
|
||||
const loginQueue = this.state.loginQueue;
|
||||
loginQueue.shift();
|
||||
this.setState({loginQueue});
|
||||
}
|
||||
|
||||
handleLoginCancel = (request) => {
|
||||
ipcRenderer.send('login-cancel', request);
|
||||
|
||||
const loginQueue = this.state.loginQueue;
|
||||
loginQueue.shift();
|
||||
this.setState({loginQueue});
|
||||
}
|
||||
|
||||
handleTargetURLChange = (targetURL) => {
|
||||
clearTimeout(this.targetURLDisappearTimeout);
|
||||
if (targetURL === '' || this.parseDeeplinkURL(targetURL, [this.props.teams[this.state.key]])) { // Do not show URL for internal links on current team
|
||||
// set delay to avoid momentary disappearance when hovering over multiple links
|
||||
this.targetURLDisappearTimeout = setTimeout(() => {
|
||||
this.setState({targetURL: ''});
|
||||
}, 500);
|
||||
} else {
|
||||
this.setState({targetURL});
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = (e) => {
|
||||
e.stopPropagation(); // since it is our button, the event goes into MainPage's onclick event, getting focus back.
|
||||
const win = remote.getCurrentWindow();
|
||||
win.close();
|
||||
}
|
||||
|
||||
handleMinimize = (e) => {
|
||||
e.stopPropagation();
|
||||
const win = remote.getCurrentWindow();
|
||||
win.minimize();
|
||||
}
|
||||
|
||||
handleMaximize = (e) => {
|
||||
e.stopPropagation();
|
||||
const win = remote.getCurrentWindow();
|
||||
win.maximize();
|
||||
}
|
||||
|
||||
handleRestore = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
win.restore();
|
||||
}
|
||||
|
||||
openMenu = () => {
|
||||
// @eslint-ignore
|
||||
this.threeDotMenu.current.blur();
|
||||
this.props.openMenu();
|
||||
}
|
||||
|
||||
handleDoubleClick = () => {
|
||||
if (process.platform === 'darwin') {
|
||||
const doubleClickAction = remote.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
||||
const win = remote.getCurrentWindow();
|
||||
if (doubleClickAction === 'Minimize') {
|
||||
win.minimize();
|
||||
} else if (!win.isMaximized()) {
|
||||
win.maximize();
|
||||
} else if (win.isMaximized()) {
|
||||
win.unmaximize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addServer = () => {
|
||||
this.setState({
|
||||
showNewTeamModal: true,
|
||||
});
|
||||
}
|
||||
|
||||
focusOnWebView = () => {
|
||||
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
|
||||
}
|
||||
|
||||
activateFinder = () => {
|
||||
this.setState({
|
||||
finderVisible: true,
|
||||
focusFinder: true,
|
||||
});
|
||||
}
|
||||
|
||||
closeFinder = () => {
|
||||
this.setState({
|
||||
finderVisible: false,
|
||||
focusFinder: false,
|
||||
});
|
||||
}
|
||||
|
||||
inputFocus = (e, focus) => {
|
||||
this.setState({
|
||||
focusFinder: focus,
|
||||
});
|
||||
}
|
||||
|
||||
handleSelectCertificate = (certificate) => {
|
||||
const certificateRequests = this.state.certificateRequests;
|
||||
const current = certificateRequests.shift();
|
||||
this.setState({certificateRequests});
|
||||
ipcRenderer.send('selected-client-certificate', current.server, certificate);
|
||||
if (certificateRequests.length > 0) {
|
||||
this.switchToTabForCertificateRequest(certificateRequests[0].server);
|
||||
}
|
||||
}
|
||||
handleCancelCertificate = () => {
|
||||
const certificateRequests = this.state.certificateRequests;
|
||||
const current = certificateRequests.shift();
|
||||
this.setState({certificateRequests});
|
||||
ipcRenderer.send('selected-client-certificate', current.server);
|
||||
if (certificateRequests.length > 0) {
|
||||
this.switchToTabForCertificateRequest(certificateRequests[0].server);
|
||||
}
|
||||
};
|
||||
|
||||
showDownloadCompleteNotification = async (event, item) => {
|
||||
const title = process.platform === 'win32' ? item.serverInfo.name : 'Download Complete';
|
||||
const notificationBody = process.platform === 'win32' ? `Download Complete \n ${item.fileName}` : item.fileName;
|
||||
|
||||
await Utils.dispatchNotification(title, notificationBody, false, {}, () => {
|
||||
shell.showItemInFolder(item.path.normalize());
|
||||
});
|
||||
}
|
||||
|
||||
setDarkMode() {
|
||||
this.setState({
|
||||
isDarkMode: this.props.setDarkMode(),
|
||||
});
|
||||
}
|
||||
setInputRef = (ref) => {
|
||||
this.inputRef = ref;
|
||||
}
|
||||
|
||||
showExtraBar = () => {
|
||||
const ref = this.refs[`mattermostView${this.state.key}`];
|
||||
if (typeof ref !== 'undefined') {
|
||||
return !urlUtils.isTeamUrl(this.props.teams[this.state.key].url, ref.getSrc());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const self = this;
|
||||
const tabsRow = (
|
||||
<TabBar
|
||||
id='tabBar'
|
||||
isDarkMode={this.state.isDarkMode}
|
||||
teams={this.props.teams}
|
||||
sessionsExpired={this.state.sessionsExpired}
|
||||
unreadCounts={this.state.unreadCounts}
|
||||
mentionCounts={this.state.mentionCounts}
|
||||
unreadAtActive={this.state.unreadAtActive}
|
||||
mentionAtActiveCounts={this.state.mentionAtActiveCounts}
|
||||
activeKey={this.state.key}
|
||||
onSelect={this.handleSelect}
|
||||
onAddServer={this.addServer}
|
||||
showAddServerButton={this.props.showAddServerButton}
|
||||
onDrop={this.handleDragAndDrop}
|
||||
/>
|
||||
);
|
||||
|
||||
let topBarClassName = 'topBar';
|
||||
if (process.platform === 'darwin') {
|
||||
topBarClassName += ' macOS';
|
||||
}
|
||||
if (this.state.isDarkMode) {
|
||||
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 = this.props.teams.map((team, index) => {
|
||||
function handleBadgeChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) {
|
||||
self.handleBadgeChange(index, sessionExpired, unreadCount, mentionCount, isUnread, isMentioned);
|
||||
}
|
||||
function handleNotificationClick() {
|
||||
self.handleSelect(index);
|
||||
}
|
||||
const id = 'mattermostView' + index;
|
||||
const isActive = self.state.key === index;
|
||||
|
||||
let teamUrl = team.url;
|
||||
|
||||
if (this.props.deeplinkingUrl) {
|
||||
const parsedDeeplink = this.parseDeeplinkURL(this.props.deeplinkingUrl, [team]);
|
||||
if (parsedDeeplink) {
|
||||
teamUrl = parsedDeeplink.url;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MattermostView
|
||||
key={id}
|
||||
id={id}
|
||||
teams={this.props.teams}
|
||||
useSpellChecker={this.props.useSpellChecker}
|
||||
onSelectSpellCheckerLocale={this.props.onSelectSpellCheckerLocale}
|
||||
src={teamUrl}
|
||||
name={team.name}
|
||||
onTargetURLChange={self.handleTargetURLChange}
|
||||
onBadgeChange={handleBadgeChange}
|
||||
onNotificationClick={handleNotificationClick}
|
||||
handleInterTeamLink={self.handleInterTeamLink}
|
||||
ref={id}
|
||||
active={isActive}
|
||||
allowExtraBar={this.showExtraBar()}
|
||||
isDarkMode={this.state.isDarkMode}
|
||||
/>);
|
||||
});
|
||||
|
||||
const viewsRow = (
|
||||
<Fragment>
|
||||
<ExtraBar
|
||||
darkMode={this.state.isDarkMode}
|
||||
show={this.showExtraBar()}
|
||||
mattermostView={this.refs[`mattermostView${this.state.key}`]}
|
||||
/>
|
||||
<Row>
|
||||
{views}
|
||||
</Row>
|
||||
</Fragment>);
|
||||
|
||||
let request = null;
|
||||
let authServerURL = null;
|
||||
let authInfo = null;
|
||||
if (this.state.loginQueue.length !== 0) {
|
||||
request = this.state.loginQueue[0].request;
|
||||
const tmpURL = urlUtils.parseURL(this.state.loginQueue[0].request.url);
|
||||
authServerURL = tmpURL.origin;
|
||||
authInfo = this.state.loginQueue[0].authInfo;
|
||||
}
|
||||
const modal = (
|
||||
<NewTeamModal
|
||||
currentOrder={this.props.teams.length}
|
||||
show={this.state.showNewTeamModal}
|
||||
setInputRef={this.setInputRef}
|
||||
onClose={() => {
|
||||
this.setState({
|
||||
showNewTeamModal: false,
|
||||
});
|
||||
}}
|
||||
onSave={(newTeam) => {
|
||||
this.props.localTeams.push(newTeam);
|
||||
this.props.onTeamConfigChange(this.props.localTeams, () => {
|
||||
self.setState({
|
||||
showNewTeamModal: false,
|
||||
key: this.props.teams.length - 1,
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className='MainPage'
|
||||
onClick={this.focusOnWebView}
|
||||
>
|
||||
<LoginModal
|
||||
show={this.state.loginQueue.length !== 0}
|
||||
request={request}
|
||||
authInfo={authInfo}
|
||||
authServerURL={authServerURL}
|
||||
onLogin={this.handleLogin}
|
||||
onCancel={this.handleLoginCancel}
|
||||
/>
|
||||
<PermissionModal/>
|
||||
<SelectCertificateModal
|
||||
certificateRequests={this.state.certificateRequests}
|
||||
onSelect={this.handleSelectCertificate}
|
||||
onCancel={this.handleCancelCertificate}
|
||||
/>
|
||||
<Grid fluid={true}>
|
||||
{ topRow }
|
||||
{ viewsRow }
|
||||
{ this.state.finderVisible ? (
|
||||
<Finder
|
||||
webviewKey={this.state.key}
|
||||
close={this.closeFinder}
|
||||
focusState={this.state.focusFinder}
|
||||
inputFocus={this.inputFocus}
|
||||
/>
|
||||
) : null}
|
||||
</Grid>
|
||||
<TransitionGroup>
|
||||
{ (this.state.targetURL === '') ?
|
||||
null :
|
||||
<CSSTransition
|
||||
classNames='hovering'
|
||||
timeout={{enter: 300, exit: 500}}
|
||||
>
|
||||
<HoveringURL
|
||||
key='hoveringURL'
|
||||
targetURL={this.state.targetURL}
|
||||
/>
|
||||
</CSSTransition>
|
||||
}
|
||||
</TransitionGroup>
|
||||
<div>
|
||||
{ modal }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MainPage.propTypes = {
|
||||
onBadgeChange: PropTypes.func.isRequired,
|
||||
teams: PropTypes.array.isRequired,
|
||||
localTeams: PropTypes.array.isRequired,
|
||||
onTeamConfigChange: PropTypes.func.isRequired,
|
||||
initialIndex: PropTypes.number.isRequired,
|
||||
useSpellChecker: PropTypes.bool.isRequired,
|
||||
onSelectSpellCheckerLocale: PropTypes.func.isRequired,
|
||||
deeplinkingUrl: PropTypes.string,
|
||||
showAddServerButton: PropTypes.bool.isRequired,
|
||||
getDarkMode: PropTypes.func.isRequired,
|
||||
setDarkMode: PropTypes.func.isRequired,
|
||||
moveTabs: PropTypes.func.isRequired,
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/* eslint-enable react/no-set-state */
|
@@ -1,361 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// This file uses setState().
|
||||
/* eslint-disable react/no-set-state */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ipcRenderer, remote, shell} from 'electron';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import contextMenu from '../js/contextMenu';
|
||||
import Utils from '../../utils/util';
|
||||
import urlUtils from '../../utils/url';
|
||||
import {protocols} from '../../../electron-builder.json';
|
||||
const scheme = protocols[0].schemes[0];
|
||||
|
||||
import ErrorView from './ErrorView.jsx';
|
||||
import LoadingScreen from './LoadingScreen.jsx';
|
||||
|
||||
const preloadJS = `file://${remote.app.getAppPath()}/browser/webview/mattermost_bundle.js`;
|
||||
|
||||
const ERR_NOT_IMPLEMENTED = -11;
|
||||
const U2F_EXTENSION_URL = 'chrome-extension://kmendfapggjehodndflmmgagdbamhnfd/u2f-comms.html';
|
||||
const ERR_USER_ABORTED = -3;
|
||||
const AUTO_RELOAD_TIMER = 30000;
|
||||
|
||||
export default class MattermostView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errorInfo: null,
|
||||
isContextMenuAdded: false,
|
||||
reloadTimeoutID: null,
|
||||
isWebviewLoaded: false,
|
||||
basename: '/',
|
||||
};
|
||||
|
||||
this.webviewRef = React.createRef();
|
||||
}
|
||||
|
||||
handleUnreadCountChange = (sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) => {
|
||||
if (this.props.onBadgeChange) {
|
||||
this.props.onBadgeChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const self = this;
|
||||
const webview = this.webviewRef.current;
|
||||
|
||||
webview.addEventListener('did-fail-load', (e) => {
|
||||
console.log(self.props.name, 'webview did-fail-load', e);
|
||||
if (e.errorCode === ERR_USER_ABORTED) { // An operation was aborted (due to user action).
|
||||
return;
|
||||
}
|
||||
if (e.errorCode === ERR_NOT_IMPLEMENTED && e.validatedURL === U2F_EXTENSION_URL) {
|
||||
// U2F device is not supported, but the guest page should fall back to PIN code in 2FA.
|
||||
// https://github.com/mattermost/desktop/issues/708
|
||||
return;
|
||||
}
|
||||
|
||||
self.setState({
|
||||
errorInfo: e,
|
||||
isWebviewLoaded: true,
|
||||
});
|
||||
function reload() {
|
||||
window.removeEventListener('online', reload);
|
||||
self.reload();
|
||||
}
|
||||
if (navigator.onLine) {
|
||||
self.setState({
|
||||
reloadTimeoutID: setTimeout(reload, AUTO_RELOAD_TIMER),
|
||||
});
|
||||
} else {
|
||||
window.addEventListener('online', reload);
|
||||
}
|
||||
});
|
||||
|
||||
// Open link in browserWindow. for example, attached files.
|
||||
webview.addEventListener('new-window', (e) => {
|
||||
if (!urlUtils.isValidURI(e.url)) {
|
||||
return;
|
||||
}
|
||||
const currentURL = urlUtils.parseURL(webview.getURL());
|
||||
const destURL = urlUtils.parseURL(e.url);
|
||||
if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:' && destURL.protocol !== `${scheme}:`) {
|
||||
ipcRenderer.send('confirm-protocol', destURL.protocol, e.url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlUtils.isInternalURL(destURL, currentURL, this.state.basename)) {
|
||||
if (destURL.path.match(/^\/api\/v[3-4]\/public\/files\//)) {
|
||||
ipcRenderer.send('download-url', e.url);
|
||||
} else if (destURL.path.match(/^\/help\//)) {
|
||||
// continue to open special case internal urls in default browser
|
||||
shell.openExternal(e.url);
|
||||
} else if (urlUtils.isTeamUrl(this.props.src, e.url, true) || urlUtils.isAdminUrl(this.props.src, e.url)) {
|
||||
e.preventDefault();
|
||||
this.webviewRef.current.loadURL(e.url);
|
||||
} else if (urlUtils.isPluginUrl(this.props.src, e.url)) {
|
||||
// New window should disable nodeIntegration.
|
||||
window.open(e.url, remote.app.name, 'nodeIntegration=no, contextIsolation=yes, show=yes');
|
||||
} else if (urlUtils.isManagedResource(this.props.src, e.url)) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
shell.openExternal(e.url);
|
||||
}
|
||||
} else {
|
||||
const parsedURL = urlUtils.parseURL(e.url);
|
||||
const serverURL = urlUtils.getServer(parsedURL, this.props.teams);
|
||||
if (serverURL !== null && urlUtils.isTeamUrl(serverURL.url, parsedURL)) {
|
||||
this.props.handleInterTeamLink(parsedURL);
|
||||
} else {
|
||||
// if the link is external, use default os' application.
|
||||
ipcRenderer.send('confirm-protocol', destURL.protocol, e.url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 'dom-ready' means "content has been loaded"
|
||||
// So this would be emitted again when reloading a webview
|
||||
webview.addEventListener('dom-ready', () => {
|
||||
// webview.openDevTools();
|
||||
|
||||
// Remove this once https://github.com/electron/electron/issues/14474 is fixed
|
||||
// - fixes missing cursor bug in electron
|
||||
// - only apply this focus fix if the current view is active
|
||||
if (this.props.active) {
|
||||
webview.blur();
|
||||
webview.focus();
|
||||
}
|
||||
if (!this.state.isContextMenuAdded) {
|
||||
contextMenu.setup({
|
||||
window: webview,
|
||||
useSpellChecker: this.props.useSpellChecker,
|
||||
onSelectSpellCheckerLocale: (locale) => {
|
||||
if (this.props.onSelectSpellCheckerLocale) {
|
||||
this.props.onSelectSpellCheckerLocale(locale);
|
||||
}
|
||||
webview.send('set-spellchecker');
|
||||
},
|
||||
});
|
||||
this.setState({isContextMenuAdded: true});
|
||||
}
|
||||
});
|
||||
|
||||
webview.addEventListener('update-target-url', (event) => {
|
||||
if (self.props.onTargetURLChange) {
|
||||
self.props.onTargetURLChange(event.url);
|
||||
}
|
||||
});
|
||||
|
||||
webview.addEventListener('ipc-message', (event) => {
|
||||
switch (event.channel) {
|
||||
case 'onGuestInitialized':
|
||||
self.setState({
|
||||
isWebviewLoaded: true,
|
||||
basename: event.args[0] || '/',
|
||||
});
|
||||
break;
|
||||
case 'onBadgeChange': {
|
||||
self.handleUnreadCountChange(...event.args);
|
||||
break;
|
||||
}
|
||||
case 'dispatchNotification': {
|
||||
const [title, body, channel, teamId, silent, data] = event.args;
|
||||
Utils.dispatchNotification(title, body, silent, data, () => this.webviewRef.current.send('notification-clicked', {channel, teamId}));
|
||||
break;
|
||||
}
|
||||
case 'onNotificationClick':
|
||||
self.props.onNotificationClick();
|
||||
break;
|
||||
case 'mouse-move':
|
||||
this.handleMouseMove(event.args[0]);
|
||||
break;
|
||||
case 'mouse-up':
|
||||
this.handleMouseUp();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
webview.addEventListener('page-title-updated', (event) => {
|
||||
if (self.props.active) {
|
||||
ipcRenderer.send('update-title', {
|
||||
title: event.title,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
webview.addEventListener('console-message', (e) => {
|
||||
const message = `[${this.props.name}] ${e.message}`;
|
||||
switch (e.level) {
|
||||
case 0:
|
||||
console.log(message);
|
||||
break;
|
||||
case 1:
|
||||
console.warn(message);
|
||||
break;
|
||||
case 2:
|
||||
console.error(message);
|
||||
break;
|
||||
default:
|
||||
console.log(message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// start listening for user status updates from main
|
||||
ipcRenderer.on('user-activity-update', this.handleUserActivityUpdate);
|
||||
ipcRenderer.on('exit-fullscreen', this.handleExitFullscreen);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// stop listening for user status updates from main
|
||||
ipcRenderer.removeListener('user-activity-update', this.handleUserActivityUpdate);
|
||||
ipcRenderer.removeListener('exit-fullscreen', this.handleExitFullscreen);
|
||||
}
|
||||
|
||||
reload = () => {
|
||||
clearTimeout(this.state.reloadTimeoutID);
|
||||
this.setState({
|
||||
errorInfo: null,
|
||||
reloadTimeoutID: null,
|
||||
isWebviewLoaded: false,
|
||||
});
|
||||
const webview = this.webviewRef.current;
|
||||
if (webview) {
|
||||
webview.reload();
|
||||
}
|
||||
}
|
||||
|
||||
clearCacheAndReload = () => {
|
||||
this.setState({
|
||||
errorInfo: null,
|
||||
});
|
||||
const webContents = this.webviewRef.current.getWebContents();
|
||||
webContents.session.clearCache().then(webContents.reload);
|
||||
}
|
||||
|
||||
focusOnWebView = () => {
|
||||
const webview = this.webviewRef.current;
|
||||
webview.focus();
|
||||
}
|
||||
|
||||
handleMouseMove = (event) => {
|
||||
const moveEvent = document.createEvent('MouseEvents');
|
||||
moveEvent.initMouseEvent('mousemove', null, null, null, null, null, null, event.clientX, event.clientY);
|
||||
document.dispatchEvent(moveEvent);
|
||||
}
|
||||
|
||||
handleMouseUp = () => {
|
||||
const upEvent = document.createEvent('MouseEvents');
|
||||
upEvent.initMouseEvent('mouseup');
|
||||
document.dispatchEvent(upEvent);
|
||||
}
|
||||
|
||||
canGoBack = () => {
|
||||
const webview = this.webviewRef.current;
|
||||
return webview.getWebContents().canGoBack();
|
||||
}
|
||||
|
||||
canGoForward = () => {
|
||||
const webview = this.webviewRef.current;
|
||||
return webview.getWebContents().canGoForward();
|
||||
}
|
||||
|
||||
goBack = () => {
|
||||
try {
|
||||
const webview = this.webviewRef.current;
|
||||
webview.getWebContents().goBack();
|
||||
} catch (e) {
|
||||
console.log(`Error while trying to go back in history: ${e}`);
|
||||
this.webview.loadURL(this.props.src);
|
||||
}
|
||||
}
|
||||
|
||||
goForward = () => {
|
||||
const webview = this.webviewRef.current;
|
||||
webview.getWebContents().goForward();
|
||||
}
|
||||
|
||||
getSrc = () => {
|
||||
const webview = this.webviewRef.current;
|
||||
return webview.src;
|
||||
}
|
||||
|
||||
handleDeepLink = (relativeUrl) => {
|
||||
const webview = this.webviewRef.current;
|
||||
webview.executeJavaScript(
|
||||
'history.pushState(null, null, "' + relativeUrl + '");',
|
||||
);
|
||||
webview.executeJavaScript(
|
||||
'dispatchEvent(new PopStateEvent("popstate", null));',
|
||||
);
|
||||
}
|
||||
|
||||
handleUserActivityUpdate = (_, status) => {
|
||||
// pass user activity update to the webview
|
||||
this.webviewRef.current.send('user-activity-update', status);
|
||||
}
|
||||
|
||||
handleExitFullscreen = () => {
|
||||
// pass exit fullscreen request to the webview
|
||||
this.webviewRef.current.send('exit-fullscreen');
|
||||
}
|
||||
|
||||
render() {
|
||||
const errorView = this.state.errorInfo ? (
|
||||
<ErrorView
|
||||
id={this.props.id + '-fail'}
|
||||
className='errorView'
|
||||
errorInfo={this.state.errorInfo}
|
||||
active={this.props.active}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('mattermostView', {
|
||||
'mattermostView-with-tab': this.props.withTab,
|
||||
'mattermostView-hidden': !this.props.active,
|
||||
'mattermostView-error': this.state.errorInfo,
|
||||
'allow-extra-bar': this.props.allowExtraBar,
|
||||
})}
|
||||
>
|
||||
{ errorView }
|
||||
<LoadingScreen
|
||||
loading={!this.state.errorInfo && this.props.active && !this.state.isWebviewLoaded}
|
||||
darkMode={this.props.isDarkMode}
|
||||
/>
|
||||
<webview
|
||||
id={this.props.id}
|
||||
preload={preloadJS}
|
||||
src={this.props.src}
|
||||
ref={this.webviewRef}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
MattermostView.propTypes = {
|
||||
name: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
teams: PropTypes.array.isRequired,
|
||||
withTab: PropTypes.bool,
|
||||
onTargetURLChange: PropTypes.func,
|
||||
onBadgeChange: PropTypes.func,
|
||||
src: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
useSpellChecker: PropTypes.bool,
|
||||
onSelectSpellCheckerLocale: PropTypes.func,
|
||||
handleInterTeamLink: PropTypes.func,
|
||||
allowExtraBar: PropTypes.bool,
|
||||
isDarkMode: PropTypes.bool,
|
||||
};
|
||||
|
||||
/* eslint-enable react/no-set-state */
|
@@ -1,244 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
|
||||
|
||||
import urlUtils from '../../utils/url';
|
||||
|
||||
export default class NewTeamModal extends React.Component {
|
||||
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,
|
||||
};
|
@@ -1,151 +0,0 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
/* eslint-disable react/no-set-state */
|
||||
|
||||
import React from 'react';
|
||||
import {Modal, Button} from 'react-bootstrap';
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
import {log} from 'electron-log';
|
||||
|
||||
import {BASIC_AUTH_PERMISSION, REQUEST_PERMISSION_CHANNEL, DENY_PERMISSION_CHANNEL, GRANT_PERMISSION_CHANNEL, PERMISSION_DESCRIPTION} from '../../common/permissions';
|
||||
|
||||
import Util from '../../utils/util';
|
||||
|
||||
import ExternalLink from './externalLink.jsx';
|
||||
|
||||
function getKey(request, permission) {
|
||||
return `${request.url}:${permission}`;
|
||||
}
|
||||
|
||||
export default class PermissionModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tracker: new Map(), // permission request order is not preserved, but we won't have repetition of requests.
|
||||
current: null,
|
||||
};
|
||||
|
||||
ipcRenderer.on(REQUEST_PERMISSION_CHANNEL, (event, request, authInfo, permission) => {
|
||||
switch (permission) {
|
||||
case BASIC_AUTH_PERMISSION:
|
||||
this.requestBasicAuthPermission(event, request, authInfo, permission);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown permission request: ${permission}`);
|
||||
ipcRenderer.send(DENY_PERMISSION_CHANNEL, request, permission);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
requestBasicAuthPermission(event, request, authInfo, permission) {
|
||||
const key = getKey(request, permission);
|
||||
this.requestPermission(key, request.url, permission).then(() => {
|
||||
ipcRenderer.send(GRANT_PERMISSION_CHANNEL, request.url, permission);
|
||||
ipcRenderer.sendTo(remote.getCurrentWindow().webContents.id, 'login-request', request, authInfo);
|
||||
this.loadNext();
|
||||
}).catch((err) => {
|
||||
ipcRenderer.send(DENY_PERMISSION_CHANNEL, request.url, permission, err.message);
|
||||
ipcRenderer.send('login-cancel', request);
|
||||
this.loadNext();
|
||||
});
|
||||
}
|
||||
|
||||
requestPermission(key, url, permission) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tracker = new Map(this.state.tracker);
|
||||
const permissionRequest = {
|
||||
grant: resolve,
|
||||
deny: () => reject(new Error(`User denied ${permission} to ${url}`)),
|
||||
url,
|
||||
permission,
|
||||
};
|
||||
tracker.set(key, permissionRequest);
|
||||
const current = this.state.current ? this.state.current : key;
|
||||
this.setState({
|
||||
tracker,
|
||||
current,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentData() {
|
||||
if (this.state.current) {
|
||||
return this.state.tracker.get(this.state.current);
|
||||
}
|
||||
return {
|
||||
grant: () => {
|
||||
const err = new Error();
|
||||
log.error(`There isn't any permission to grant access to.\n Stack trace:\n${err.stack}`);
|
||||
},
|
||||
deny: () => {
|
||||
const err = new Error();
|
||||
log.error(`There isn't any permission to deny access to.\n Stack trace:\n${err.stack}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadNext() {
|
||||
const tracker = new Map(this.state.tracker);
|
||||
tracker.delete(this.state.current);
|
||||
const nextKey = tracker.keys().next();
|
||||
const current = nextKey.done ? null : nextKey.value;
|
||||
this.setState({
|
||||
tracker,
|
||||
current,
|
||||
});
|
||||
}
|
||||
|
||||
getModalTitle() {
|
||||
const {permission} = this.getCurrentData();
|
||||
return `${PERMISSION_DESCRIPTION[permission]} Required`;
|
||||
}
|
||||
|
||||
getModalBody() {
|
||||
const {url, permission} = this.getCurrentData();
|
||||
const originDisplay = url ? Util.getHost(url) : 'unknown origin';
|
||||
const originLink = url ? originDisplay : '';
|
||||
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>
|
||||
<ExternalLink href={originLink}>{`${originDisplay}`}</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {grant, deny} = this.getCurrentData();
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='permission-modal'
|
||||
show={Boolean(this.state.current)}
|
||||
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={deny}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
onClick={grant}
|
||||
>{'Accept'}</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
/* eslint-enable react/no-set-state */
|
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import 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,
|
||||
};
|
@@ -1,162 +0,0 @@
|
||||
// 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 ShowCertificateModal from './showCertificateModal.jsx';
|
||||
|
||||
export default class SelectCertificateModal extends React.Component {
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
certificateRequests: PropTypes.arrayOf(PropTypes.shape({
|
||||
server: PropTypes.string,
|
||||
certificateList: PropTypes.array,
|
||||
})),
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedIndex: null,
|
||||
showCertificate: null,
|
||||
};
|
||||
}
|
||||
|
||||
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.props.certificateRequests[0].certificateList[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() {
|
||||
const certList = this.props.certificateRequests.length ? this.props.certificateRequests[0].certificateList : [];
|
||||
const server = this.props.certificateRequests.length ? this.props.certificateRequests[0].server : '';
|
||||
if (this.state.showCertificate) {
|
||||
return (
|
||||
<ShowCertificateModal
|
||||
certificate={this.state.showCertificate}
|
||||
onOk={this.certificateInfoClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='certificate-modal'
|
||||
show={this.props.certificateRequests.length > 0}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title >{'Select a certificate'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className={'subtitle'}>{`Select a certificate to authenticate yourself to ${server}`}</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(certList)}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,146 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {remote} 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';
|
||||
|
||||
export default class TabBar extends React.Component { // need "this"
|
||||
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];
|
||||
|
||||
let unreadCount = 0;
|
||||
if (this.props.unreadCounts[index] > 0) {
|
||||
unreadCount = this.props.unreadCounts[index];
|
||||
}
|
||||
if (this.props.unreadAtActive[index]) {
|
||||
unreadCount += 1;
|
||||
}
|
||||
|
||||
let mentionCount = 0;
|
||||
if (this.props.mentionCounts[index] > 0) {
|
||||
mentionCount = this.props.mentionCounts[index];
|
||||
}
|
||||
if (this.props.mentionAtActiveCounts[index] > 0) {
|
||||
mentionCount += this.props.mentionAtActiveCounts[index];
|
||||
}
|
||||
|
||||
let badgeDiv;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-expired'/>
|
||||
);
|
||||
} else if (mentionCount !== 0) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-badge'>
|
||||
{mentionCount}
|
||||
</div>
|
||||
);
|
||||
} else if (unreadCount !== 0) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-dot'/>
|
||||
);
|
||||
}
|
||||
|
||||
const id = `teamTabItem${index}`;
|
||||
const navItem = () => (
|
||||
<NavItem
|
||||
key={id}
|
||||
id={id}
|
||||
eventKey={index}
|
||||
draggable={false}
|
||||
ref={id}
|
||||
active={this.props.activeKey === index}
|
||||
activeKey={this.props.activeKey}
|
||||
onMouseDown={() => {
|
||||
this.props.onSelect(index);
|
||||
}}
|
||||
onSelect={() => {
|
||||
this.props.onSelect(index);
|
||||
}}
|
||||
title={team.name}
|
||||
>
|
||||
<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();
|
||||
}}
|
||||
>
|
||||
<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 !(remote.getCurrentWindow().registryConfigData.teams && remote.getCurrentWindow().registryConfigData.teams.length > 0);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabBar.propTypes = {
|
||||
activeKey: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
isDarkMode: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
teams: PropTypes.array,
|
||||
sessionsExpired: PropTypes.array,
|
||||
unreadCounts: PropTypes.array,
|
||||
unreadAtActive: PropTypes.array,
|
||||
mentionCounts: PropTypes.array,
|
||||
mentionAtActiveCounts: PropTypes.array,
|
||||
showAddServerButton: PropTypes.bool,
|
||||
onAddServer: PropTypes.func,
|
||||
onDrop: PropTypes.func,
|
||||
};
|
@@ -1,193 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import 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.Component {
|
||||
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);
|
||||
}
|
||||
|
||||
handleTeamEditing = (teamName, teamUrl, teamIndex, teamOrder) => {
|
||||
this.setState({
|
||||
showEditTeamForm: true,
|
||||
team: {
|
||||
url: teamUrl,
|
||||
name: teamName,
|
||||
index: teamIndex,
|
||||
order: teamOrder,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
openServerRemoveModal = (indexForServer) => {
|
||||
this.setState({indexToRemoveServer: indexForServer});
|
||||
}
|
||||
|
||||
closeServerRemoveModal = () => {
|
||||
this.setState({indexToRemoveServer: -1});
|
||||
}
|
||||
|
||||
render() {
|
||||
const self = this;
|
||||
const teamNodes = this.props.teams.map((team, i) => {
|
||||
function handleTeamRemove() {
|
||||
document.activeElement.blur();
|
||||
self.openServerRemoveModal(i);
|
||||
}
|
||||
|
||||
function handleTeamEditing() {
|
||||
document.activeElement.blur();
|
||||
self.handleTeamEditing(team.name, team.url, i, team.order);
|
||||
}
|
||||
|
||||
function handleTeamClick() {
|
||||
self.props.onTeamClick(i);
|
||||
}
|
||||
|
||||
return (
|
||||
<TeamListItem
|
||||
index={i}
|
||||
key={'teamListItem' + i}
|
||||
name={team.name}
|
||||
url={team.url}
|
||||
onTeamRemove={handleTeamRemove}
|
||||
onTeamEditing={handleTeamEditing}
|
||||
onTeamClick={handleTeamClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
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 = {
|
||||
onTeamsChange: PropTypes.func,
|
||||
showAddTeamForm: PropTypes.bool,
|
||||
teams: PropTypes.array,
|
||||
addServer: PropTypes.func,
|
||||
updateTeam: PropTypes.func,
|
||||
toggleAddTeamForm: PropTypes.func,
|
||||
setAddTeamFormVisibility: PropTypes.func,
|
||||
onTeamClick: PropTypes.func,
|
||||
};
|
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class TeamListItem extends React.Component {
|
||||
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,
|
||||
};
|
@@ -1,104 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import 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) {
|
||||
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>
|
||||
{props.isDownloading ?
|
||||
<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> :
|
||||
<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>
|
||||
}
|
||||
</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;
|
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
||||
|
||||
import {action} from '@storybook/addon-actions';
|
||||
|
||||
import UpdaterPage from '../UpdaterPage.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')}
|
||||
/>
|
||||
));
|
@@ -1,34 +0,0 @@
|
||||
// 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 {ipcRenderer} from 'electron';
|
||||
|
||||
import urlUtils from '../../utils/url';
|
||||
|
||||
// this component is used to override some checks from the UI, leaving only to trust the protocol in case it wasn't http/s
|
||||
// it is used the same as an `a` JSX tag
|
||||
export default function ExternalLink(props) {
|
||||
const click = (e) => {
|
||||
e.preventDefault();
|
||||
let parseUrl;
|
||||
try {
|
||||
parseUrl = urlUtils.parseURL(props.href);
|
||||
ipcRenderer.send('confirm-protocol', parseUrl.protocol, props.href);
|
||||
} catch (err) {
|
||||
console.error(`invalid url ${props.href} supplied to externallink: ${err}`);
|
||||
}
|
||||
};
|
||||
const options = {
|
||||
onClick: click,
|
||||
...props,
|
||||
};
|
||||
return (
|
||||
<a {...options}/>
|
||||
);
|
||||
}
|
||||
|
||||
ExternalLink.propTypes = {
|
||||
href: PropTypes.string.isRequired,
|
||||
};
|
@@ -1,114 +0,0 @@
|
||||
// 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.Component {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content"></div>
|
||||
<script src="index_bundle.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -1,214 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import './css/index.css';
|
||||
|
||||
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.');
|
||||
};
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {remote, ipcRenderer} from 'electron';
|
||||
|
||||
import urlUtils from '../utils/url';
|
||||
|
||||
import Config from '../common/config';
|
||||
|
||||
import EnhancedNotification from './js/notification';
|
||||
import MainPage from './components/MainPage.jsx';
|
||||
import {createDataURL as createBadgeDataURL} from './js/badge';
|
||||
|
||||
Notification = EnhancedNotification; // eslint-disable-line no-global-assign, no-native-reassign
|
||||
|
||||
const config = new Config(remote.app.getPath('userData') + '/config.json', remote.getCurrentWindow().registryConfigData);
|
||||
|
||||
const teams = config.teams;
|
||||
|
||||
remote.getCurrentWindow().removeAllListeners('focus');
|
||||
|
||||
if (teams.length === 0) {
|
||||
remote.getCurrentWindow().loadFile('browser/settings.html');
|
||||
}
|
||||
|
||||
const parsedURLSearchParams = urlUtils.parseURL(window.location.href).searchParams;
|
||||
const parsedURLHasIndex = parsedURLSearchParams.has('index');
|
||||
const initialIndex = parsedURLHasIndex ? parseInt(parsedURLSearchParams.get('index'), 10) : getInitialIndex();
|
||||
|
||||
let deeplinkingUrl = null;
|
||||
if (!parsedURLHasIndex) {
|
||||
deeplinkingUrl = remote.getCurrentWindow().deeplinkingUrl;
|
||||
}
|
||||
|
||||
config.on('update', (configData) => {
|
||||
teams.splice(0, teams.length, ...configData.teams);
|
||||
});
|
||||
|
||||
config.on('synchronize', () => {
|
||||
ipcRenderer.send('reload-config');
|
||||
});
|
||||
|
||||
ipcRenderer.on('reload-config', () => {
|
||||
config.reload();
|
||||
});
|
||||
|
||||
function getInitialIndex() {
|
||||
const element = teams.find((e) => e.order === 0);
|
||||
return element ? teams.indexOf(element) : 0;
|
||||
}
|
||||
|
||||
function showBadgeWindows(sessionExpired, unreadCount, mentionCount) {
|
||||
function sendBadge(dataURL, description) {
|
||||
// window.setOverlayIcon() does't work with NativeImage across remote boundaries.
|
||||
// https://github.com/atom/electron/issues/4011
|
||||
ipcRenderer.send('update-unread', {
|
||||
overlayDataURL: dataURL,
|
||||
description,
|
||||
sessionExpired,
|
||||
unreadCount,
|
||||
mentionCount,
|
||||
});
|
||||
}
|
||||
|
||||
if (sessionExpired) {
|
||||
const dataURL = createBadgeDataURL('•');
|
||||
sendBadge(dataURL, 'Session Expired: Please sign in to continue receiving notifications.');
|
||||
} else if (mentionCount > 0) {
|
||||
const dataURL = createBadgeDataURL((mentionCount > 99) ? '99+' : mentionCount.toString(), mentionCount > 99);
|
||||
sendBadge(dataURL, 'You have unread mentions (' + mentionCount + ')');
|
||||
} else if (unreadCount > 0 && config.showUnreadBadge) {
|
||||
const dataURL = createBadgeDataURL('•');
|
||||
sendBadge(dataURL, 'You have unread channels (' + unreadCount + ')');
|
||||
} else {
|
||||
sendBadge(null, 'You have no unread messages');
|
||||
}
|
||||
}
|
||||
|
||||
function showBadgeOSX(sessionExpired, unreadCount, mentionCount) {
|
||||
if (sessionExpired) {
|
||||
remote.app.dock.setBadge('•');
|
||||
} else if (mentionCount > 0) {
|
||||
remote.app.dock.setBadge(mentionCount.toString());
|
||||
} else if (unreadCount > 0 && config.showUnreadBadge) {
|
||||
remote.app.dock.setBadge('•');
|
||||
} else {
|
||||
remote.app.dock.setBadge('');
|
||||
}
|
||||
|
||||
ipcRenderer.send('update-unread', {
|
||||
sessionExpired,
|
||||
unreadCount,
|
||||
mentionCount,
|
||||
});
|
||||
}
|
||||
|
||||
function showBadgeLinux(sessionExpired, unreadCount, mentionCount) {
|
||||
if (remote.app.isUnityRunning()) {
|
||||
if (sessionExpired) {
|
||||
remote.app.badgeCount = mentionCount + 1;
|
||||
} else {
|
||||
remote.app.badgeCount = mentionCount;
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.send('update-unread', {
|
||||
sessionExpired,
|
||||
unreadCount,
|
||||
mentionCount,
|
||||
});
|
||||
}
|
||||
|
||||
function showBadge(sessionExpired, unreadCount, mentionCount) {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
showBadgeWindows(sessionExpired, unreadCount, mentionCount);
|
||||
break;
|
||||
case 'darwin':
|
||||
showBadgeOSX(sessionExpired, unreadCount, mentionCount);
|
||||
break;
|
||||
case 'linux':
|
||||
showBadgeLinux(sessionExpired, unreadCount, mentionCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function teamConfigChange(updatedTeams, callback) {
|
||||
config.set('teams', updatedTeams);
|
||||
if (callback) {
|
||||
config.once('update', callback);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectSpellCheckerLocale(locale) {
|
||||
config.set('spellCheckerLocale', locale);
|
||||
ipcRenderer.send('update-dict', locale);
|
||||
}
|
||||
|
||||
function moveTabs(originalOrder, newOrder) {
|
||||
const tabOrder = teams.concat().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;
|
||||
});
|
||||
teamConfigChange(teams);
|
||||
return teamIndex;
|
||||
}
|
||||
|
||||
function getDarkMode() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return config.darkMode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setDarkMode() {
|
||||
if (process.platform !== 'darwin') {
|
||||
const darkMode = Boolean(config.darkMode);
|
||||
config.set('darkMode', !darkMode);
|
||||
return !darkMode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
if (process.platform !== 'darwin') {
|
||||
ipcRenderer.send('open-app-menu');
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<MainPage
|
||||
teams={teams}
|
||||
localTeams={config.localTeams}
|
||||
initialIndex={initialIndex}
|
||||
onBadgeChange={showBadge}
|
||||
onTeamConfigChange={teamConfigChange}
|
||||
useSpellChecker={config.useSpellChecker}
|
||||
onSelectSpellCheckerLocale={handleSelectSpellCheckerLocale}
|
||||
deeplinkingUrl={deeplinkingUrl}
|
||||
showAddServerButton={config.enableServerManagement}
|
||||
getDarkMode={getDarkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
moveTabs={moveTabs}
|
||||
openMenu={openMenu}
|
||||
/>,
|
||||
document.getElementById('content')
|
||||
);
|
||||
|
||||
// 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());
|
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
export function createDataURL(text, small) {
|
||||
const scale = 2; // should rely display dpi
|
||||
const size = (small ? 20 : 16) * scale;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('width', size);
|
||||
canvas.setAttribute('height', size);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// circle
|
||||
ctx.fillStyle = '#FF1744'; // Material Red A400
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// text
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = (11 * scale) + 'px sans-serif';
|
||||
ctx.fillText(text, size / 2, size / 2, size);
|
||||
|
||||
return canvas.toDataURL();
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
import electronContextMenu from 'electron-context-menu';
|
||||
|
||||
import urlUtils from '../../utils/url';
|
||||
|
||||
function getSuggestionsMenus(webcontents, suggestions) {
|
||||
if (suggestions.length === 0) {
|
||||
return [{
|
||||
label: 'No Suggestions',
|
||||
enabled: false,
|
||||
}];
|
||||
}
|
||||
const win = webcontents || remote.getCurrentWindow();
|
||||
return suggestions.map((s) => ({
|
||||
label: s,
|
||||
click() {
|
||||
(win.webContents || win.getWebContents()).replaceMisspelling(s);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function getSpellCheckerLocaleMenus(onSelectSpellCheckerLocale) {
|
||||
const currentLocale = ipcRenderer.sendSync('get-spellchecker-locale');
|
||||
const locales = [
|
||||
{language: 'English (UK)', locale: 'en-GB'},
|
||||
{language: 'English (US)', locale: 'en-US'},
|
||||
{language: 'French', locale: 'fr-FR'},
|
||||
{language: 'German', locale: 'de-DE'},
|
||||
{language: 'Polish', locale: 'pl-PL'},
|
||||
{language: 'Portuguese (BR)', locale: 'pt-BR'},
|
||||
{language: 'Russian', locale: 'ru-RU'},
|
||||
{language: 'Ukrainian', locale: 'uk-UA'},
|
||||
{language: 'Spanish (ES)', locale: 'es-ES'},
|
||||
{language: 'Spanish (MX)', locale: 'es-MX'},
|
||||
{language: 'Swedish', locale: 'sv-SE'},
|
||||
{language: 'Dutch', locale: 'nl-NL'},
|
||||
{language: 'Italian', locale: 'it-IT'},
|
||||
];
|
||||
return locales.map((l) => ({
|
||||
label: l.language,
|
||||
type: 'checkbox',
|
||||
checked: l.locale === currentLocale,
|
||||
click() {
|
||||
if (onSelectSpellCheckerLocale) {
|
||||
onSelectSpellCheckerLocale(l.locale);
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export default {
|
||||
setup(options) {
|
||||
const defaultOptions = {
|
||||
useSpellChecker: false,
|
||||
onSelectSpellCheckerLocale: null,
|
||||
shouldShowMenu: (e, p) => {
|
||||
const isInternalLink = p.linkURL.endsWith('#') && p.linkURL.slice(0, -1) === p.pageURL;
|
||||
let isInternalSrc;
|
||||
try {
|
||||
const srcurl = urlUtils.parseURL(p.srcURL);
|
||||
isInternalSrc = srcurl.protocol === 'file:';
|
||||
console.log(`srcrurl protocol: ${srcurl.protocol}`);
|
||||
} catch (err) {
|
||||
console.log(`ups: ${err}`);
|
||||
isInternalSrc = false;
|
||||
}
|
||||
return p.isEditable || (p.mediaType !== 'none' && !isInternalSrc) || (p.linkURL !== '' && !isInternalLink) || p.misspelledWord !== '' || p.selectionText !== '';
|
||||
}
|
||||
};
|
||||
const actualOptions = Object.assign({}, defaultOptions, options);
|
||||
|
||||
electronContextMenu({
|
||||
prepend(_defaultActions, params) {
|
||||
if (actualOptions.useSpellChecker) {
|
||||
const prependMenuItems = [];
|
||||
if (params.isEditable && params.misspelledWord !== '') {
|
||||
const suggestions = ipcRenderer.sendSync('get-spelling-suggestions', params.misspelledWord);
|
||||
prependMenuItems.push(...getSuggestionsMenus(options.window, suggestions));
|
||||
}
|
||||
if (params.isEditable) {
|
||||
prependMenuItems.push(
|
||||
{type: 'separator'},
|
||||
{label: 'Spelling Languages', submenu: getSpellCheckerLocaleMenus(actualOptions.onSelectSpellCheckerLocale)});
|
||||
}
|
||||
return prependMenuItems;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
...actualOptions,
|
||||
});
|
||||
},
|
||||
};
|
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
const OriginalNotification = Notification;
|
||||
import {throttle} from 'underscore';
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
|
||||
import osVersion from '../../common/osVersion';
|
||||
|
||||
import ding from '../../assets/sounds/ding.mp3';
|
||||
import bing from '../../assets/sounds/bing.mp3';
|
||||
import crackle from '../../assets/sounds/crackle.mp3';
|
||||
import down from '../../assets/sounds/down.mp3';
|
||||
import hello from '../../assets/sounds/hello.mp3';
|
||||
import ripple from '../../assets/sounds/ripple.mp3';
|
||||
import upstairs from '../../assets/sounds/upstairs.mp3';
|
||||
|
||||
const DEFAULT_WIN7 = 'Ding';
|
||||
const notificationSounds = new Map([
|
||||
[DEFAULT_WIN7, ding],
|
||||
['Bing', bing],
|
||||
['Crackle', crackle],
|
||||
['Down', down],
|
||||
['Hello', hello],
|
||||
['Ripple', ripple],
|
||||
['Upstairs', upstairs],
|
||||
]);
|
||||
|
||||
const appIconURL = `file:///${remote.app.getAppPath()}/assets/appicon_48.png`;
|
||||
|
||||
const playSound = throttle((soundName) => {
|
||||
const audio = new Audio(notificationSounds.get(soundName));
|
||||
audio.play();
|
||||
}, 3000, {trailing: false});
|
||||
|
||||
export default class EnhancedNotification extends OriginalNotification {
|
||||
constructor(title, options) {
|
||||
if (process.platform === 'win32') {
|
||||
// Replace with application icon.
|
||||
options.icon = appIconURL;
|
||||
} else if (process.platform === 'darwin') {
|
||||
// Notification Center shows app's icon, so there were two icons on the notification.
|
||||
Reflect.deleteProperty(options, 'icon');
|
||||
}
|
||||
|
||||
const isWin7 = (process.platform === 'win32' && osVersion.isLowerThanOrEqualWindows8_1() && DEFAULT_WIN7);
|
||||
const customSound = !options.silent && ((options.data && options.data.soundName !== 'None' && options.data.soundName) || isWin7);
|
||||
|
||||
if (customSound) {
|
||||
// disable native sound
|
||||
options.silent = true;
|
||||
}
|
||||
|
||||
super(title, options);
|
||||
|
||||
ipcRenderer.send('notified', {
|
||||
title,
|
||||
options,
|
||||
});
|
||||
|
||||
if (customSound) {
|
||||
playSound(customSound);
|
||||
}
|
||||
}
|
||||
|
||||
set onclick(handler) {
|
||||
super.onclick = () => {
|
||||
const currentWindow = remote.getCurrentWindow();
|
||||
if (process.platform === 'win32') {
|
||||
// show() breaks Aero Snap state.
|
||||
if (currentWindow.isVisible()) {
|
||||
currentWindow.focus();
|
||||
} else if (currentWindow.isMinimized()) {
|
||||
currentWindow.restore();
|
||||
} else {
|
||||
currentWindow.show();
|
||||
}
|
||||
} else if (currentWindow.isMinimized()) {
|
||||
currentWindow.restore();
|
||||
} else {
|
||||
currentWindow.show();
|
||||
}
|
||||
ipcRenderer.sendToHost('onNotificationClick');
|
||||
handler();
|
||||
};
|
||||
}
|
||||
|
||||
get onclick() {
|
||||
return super.onclick;
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Settings</title>
|
||||
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="css/index.css">
|
||||
<link rel="stylesheet" href="css/settings.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content"></div>
|
||||
<script src="settings_bundle.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {remote, ipcRenderer} from 'electron';
|
||||
|
||||
window.eval = global.eval = () => { // eslint-disable-line no-multi-assign, no-eval
|
||||
throw new Error(`Sorry, ${remote.app.name} does not support window.eval() for security reasons.`);
|
||||
};
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Config from '../common/config';
|
||||
|
||||
import SettingsPage from './components/SettingsPage.jsx';
|
||||
import contextMenu from './js/contextMenu';
|
||||
|
||||
contextMenu.setup();
|
||||
|
||||
const config = new Config(remote.app.getPath('userData') + '/config.json', remote.getCurrentWindow().registryConfigData);
|
||||
|
||||
function getDarkMode() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return config.darkMode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setDarkMode() {
|
||||
if (process.platform !== 'darwin') {
|
||||
const darkMode = Boolean(config.darkMode);
|
||||
config.set('darkMode', !darkMode);
|
||||
return !darkMode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
if (process.platform !== 'darwin') {
|
||||
ipcRenderer.send('open-app-menu');
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<SettingsPage
|
||||
getDarkMode={getDarkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
openMenu={openMenu}
|
||||
/>,
|
||||
document.getElementById('content')
|
||||
);
|
||||
|
||||
// Deny drag&drop navigation in mainWindow.
|
||||
document.addEventListener('dragover', (event) => event.preventDefault());
|
||||
document.addEventListener('drop', (event) => event.preventDefault());
|
@@ -1,157 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import propTypes from 'prop-types';
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
|
||||
import urlUtils from '../utils/url';
|
||||
|
||||
import UpdaterPage from './components/UpdaterPage.jsx';
|
||||
|
||||
const thisURL = urlUtils.parseURL(location.href);
|
||||
const notifyOnly = thisURL.searchParams.get('notifyOnly') === 'true';
|
||||
|
||||
class UpdaterPageContainer extends React.Component {
|
||||
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')
|
||||
);
|
@@ -1,272 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
import {ipcRenderer, webFrame, remote} from 'electron';
|
||||
|
||||
const UNREAD_COUNT_INTERVAL = 1000;
|
||||
const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
|
||||
|
||||
Reflect.deleteProperty(global.Buffer); // http://electron.atom.io/docs/tutorial/security/#buffer-global
|
||||
|
||||
function isReactAppInitialized() {
|
||||
const initializedRoot =
|
||||
document.querySelector('#root.channel-view') || // React 16 webapp
|
||||
document.querySelector('#root .signup-team__container') || // React 16 login
|
||||
document.querySelector('div[data-reactroot]'); // Older React apps
|
||||
if (initializedRoot === null) {
|
||||
return false;
|
||||
}
|
||||
return initializedRoot.children.length !== 0;
|
||||
}
|
||||
|
||||
function watchReactAppUntilInitialized(callback) {
|
||||
let count = 0;
|
||||
const interval = 500;
|
||||
const timeout = 30000;
|
||||
const timer = setInterval(() => {
|
||||
count += interval;
|
||||
if (isReactAppInitialized() || count >= timeout) { // assumed as webapp has been initialized.
|
||||
clearTimeout(timer);
|
||||
callback();
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if (document.getElementById('root') === null) {
|
||||
console.log('The guest is not assumed as mattermost-webapp');
|
||||
ipcRenderer.sendToHost('onGuestInitialized');
|
||||
return;
|
||||
}
|
||||
watchReactAppUntilInitialized(() => {
|
||||
ipcRenderer.sendToHost('onGuestInitialized', window.basename);
|
||||
});
|
||||
});
|
||||
|
||||
// Sent for drag and drop tabs to work properly
|
||||
document.addEventListener('mousemove', (event) => {
|
||||
ipcRenderer.sendToHost('mouse-move', {clientX: event.clientX, clientY: event.clientY});
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
ipcRenderer.sendToHost('mouse-up');
|
||||
});
|
||||
|
||||
// listen for messages from the webapp
|
||||
window.addEventListener('message', ({origin, data: {type, message = {}} = {}} = {}) => {
|
||||
if (origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case 'webapp-ready': {
|
||||
// register with the webapp to enable custom integration functionality
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'register-desktop',
|
||||
message: {
|
||||
version: remote.app.getVersion(),
|
||||
},
|
||||
},
|
||||
window.location.origin || '*'
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'dispatch-notification': {
|
||||
const {title, body, channel, teamId, silent, data} = message;
|
||||
ipcRenderer.sendToHost('dispatchNotification', title, body, channel, teamId, silent, data, () => handleNotificationClick({teamId, channel}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleNotificationClick = ({channel, teamId}) => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'notification-clicked',
|
||||
message: {
|
||||
channel,
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
};
|
||||
|
||||
ipcRenderer.on('notification-clicked', (event, {channel, teamId}) => {
|
||||
handleNotificationClick({channel, teamId});
|
||||
});
|
||||
|
||||
function hasClass(element, className) {
|
||||
const rclass = /[\t\r\n\f]/g;
|
||||
if ((' ' + element.className + ' ').replace(rclass, ' ').indexOf(className) > -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getUnreadCount() {
|
||||
if (!this.unreadCount) {
|
||||
this.unreadCount = 0;
|
||||
}
|
||||
if (!this.mentionCount) {
|
||||
this.mentionCount = 0;
|
||||
}
|
||||
|
||||
// LHS not found => Log out => Count should be 0, but session may be expired.
|
||||
if (document.getElementById('sidebar-left') === null) {
|
||||
const extraParam = (new URLSearchParams(window.location.search)).get('extra');
|
||||
const sessionExpired = extraParam === 'expired';
|
||||
|
||||
ipcRenderer.sendToHost('onBadgeChange', sessionExpired, 0, 0, false, false);
|
||||
this.sessionExpired = sessionExpired;
|
||||
this.unreadCount = 0;
|
||||
this.mentionCount = 0;
|
||||
setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
// unreadCount in sidebar
|
||||
// Note: the active channel doesn't have '.unread-title'.
|
||||
let unreadCount = document.getElementsByClassName('unread-title').length;
|
||||
|
||||
// unreadCount in team sidebar
|
||||
const teamSideBar = document.getElementsByClassName('team-sidebar'); // team-sidebar doesn't have id
|
||||
if (teamSideBar.length === 1) {
|
||||
unreadCount += teamSideBar[0].getElementsByClassName('unread').length;
|
||||
}
|
||||
|
||||
// mentionCount in sidebar
|
||||
const elem = document.querySelectorAll('#sidebar-left .badge, #channel_view .badge');
|
||||
let mentionCount = 0;
|
||||
for (let i = 0; i < elem.length; i++) {
|
||||
if (isElementVisible(elem[i]) && !hasClass(elem[i], 'badge-notify')) {
|
||||
mentionCount += Number(elem[i].innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
const postAttrName = 'data-reactid';
|
||||
const lastPostElem = document.querySelector('div[' + postAttrName + '="' + this.lastCheckedPostId + '"]');
|
||||
let isUnread = false;
|
||||
let isMentioned = false;
|
||||
if (lastPostElem === null || !isElementVisible(lastPostElem)) {
|
||||
// When load channel or change channel, this.lastCheckedPostId is invalid.
|
||||
// So we get latest post and save lastCheckedPostId.
|
||||
|
||||
// find active post-list.
|
||||
const postLists = document.querySelectorAll('div.post-list__content');
|
||||
if (postLists.length === 0) {
|
||||
setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL);
|
||||
return;
|
||||
}
|
||||
let post = null;
|
||||
for (let j = 0; j < postLists.length; j++) {
|
||||
if (isElementVisible(postLists[j])) {
|
||||
post = postLists[j].children[0];
|
||||
}
|
||||
}
|
||||
if (post === null) {
|
||||
setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
// find latest post and save.
|
||||
post = post.nextSibling;
|
||||
while (post) {
|
||||
if (post.nextSibling === null) {
|
||||
if (post.getAttribute(postAttrName) !== null) {
|
||||
this.lastCheckedPostId = post.getAttribute(postAttrName);
|
||||
}
|
||||
}
|
||||
post = post.nextSibling;
|
||||
}
|
||||
} else if (lastPostElem !== null) {
|
||||
let newPostElem = lastPostElem.nextSibling;
|
||||
while (newPostElem) {
|
||||
this.lastCheckedPostId = newPostElem.getAttribute(postAttrName);
|
||||
isUnread = true;
|
||||
const activeChannel = document.querySelector('.active .sidebar-channel');
|
||||
const closeButton = activeChannel.getElementsByClassName('btn-close');
|
||||
if (closeButton.length === 1 && closeButton[0].getAttribute('aria-describedby') === 'remove-dm-tooltip') {
|
||||
// If active channel is DM, all posts is treated as mention.
|
||||
isMentioned = true;
|
||||
break;
|
||||
} else {
|
||||
// If active channel is public/private channel, only mentioned post is treated as mention.
|
||||
const highlight = newPostElem.getElementsByClassName('mention-highlight');
|
||||
if (highlight.length !== 0 && isElementVisible(highlight[0])) {
|
||||
isMentioned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
newPostElem = newPostElem.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sessionExpired || this.unreadCount !== unreadCount || this.mentionCount !== mentionCount || isUnread || isMentioned) {
|
||||
ipcRenderer.sendToHost('onBadgeChange', false, unreadCount, mentionCount, isUnread, isMentioned);
|
||||
}
|
||||
this.unreadCount = unreadCount;
|
||||
this.mentionCount = mentionCount;
|
||||
this.sessionExpired = false;
|
||||
setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL);
|
||||
}
|
||||
setTimeout(getUnreadCount, UNREAD_COUNT_INTERVAL);
|
||||
|
||||
function isElementVisible(elem) {
|
||||
return elem.offsetHeight !== 0;
|
||||
}
|
||||
|
||||
function resetMisspelledState() {
|
||||
ipcRenderer.once('spellchecker-is-ready', () => {
|
||||
const element = document.activeElement;
|
||||
if (element) {
|
||||
element.blur();
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
ipcRenderer.send('reply-on-spellchecker-is-ready');
|
||||
}
|
||||
|
||||
function setSpellChecker() {
|
||||
const spellCheckerLocale = ipcRenderer.sendSync('get-spellchecker-locale');
|
||||
webFrame.setSpellCheckProvider(spellCheckerLocale, {
|
||||
spellCheck(words, callback) {
|
||||
const misspeltWords = words.filter((text) => {
|
||||
const res = ipcRenderer.sendSync('checkspell', text);
|
||||
const isCorrect = (res === null) ? true : res;
|
||||
return !isCorrect;
|
||||
});
|
||||
callback(misspeltWords);
|
||||
},
|
||||
});
|
||||
resetMisspelledState();
|
||||
}
|
||||
setSpellChecker();
|
||||
ipcRenderer.on('set-spellchecker', setSpellChecker);
|
||||
|
||||
// push user activity updates to the webapp
|
||||
ipcRenderer.on('user-activity-update', (event, {userIsActive, isSystemEvent}) => {
|
||||
if (window.location.origin !== 'null') {
|
||||
window.postMessage({type: 'user-activity-update', message: {userIsActive, manual: isSystemEvent}}, window.location.origin);
|
||||
}
|
||||
});
|
||||
|
||||
// exit fullscreen embedded elements like youtube - https://mattermost.atlassian.net/browse/MM-19226
|
||||
ipcRenderer.on('exit-fullscreen', () => {
|
||||
if (document.fullscreenElement && document.fullscreenElement.nodeName.toLowerCase() === 'iframe') {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
// mattermost-webapp is SPA. So cache is not cleared due to no navigation.
|
||||
// We needed to manually clear cache to free memory in long-term-use.
|
||||
// http://seenaburns.com/debugging-electron-memory-usage/
|
||||
setInterval(() => {
|
||||
webFrame.clearCache();
|
||||
}, CLEAR_CACHE_INTERVAL);
|
||||
|
||||
/* eslint-enable no-magic-numbers */
|
@@ -16,6 +16,8 @@ export default class JsonFileManager {
|
||||
writeToFile() {
|
||||
fs.writeFile(this.jsonFile, JSON.stringify(this.json, null, 2), (err) => {
|
||||
if (err) {
|
||||
// No real point in bringing electron-log into this otherwise electron-free file
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
83
src/common/communication.js
Normal file
83
src/common/communication.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const SWITCH_SERVER = 'switch-server';
|
||||
export const SET_SERVER_KEY = 'set-server-key';
|
||||
export const MARK_READ = 'mark-read';
|
||||
export const FOCUS_BROWSERVIEW = 'focus-browserview';
|
||||
export const ZOOM = 'zoom';
|
||||
export const UNDO = 'undo';
|
||||
export const REDO = 'redo';
|
||||
export const HISTORY = 'history';
|
||||
|
||||
export const QUIT = 'quit';
|
||||
|
||||
export const GET_CONFIGURATION = 'get-configuration';
|
||||
export const UPDATE_CONFIGURATION = 'update-configuration';
|
||||
export const GET_LOCAL_CONFIGURATION = 'get-local-configuration';
|
||||
export const RELOAD_CONFIGURATION = 'reload-config';
|
||||
|
||||
export const UPDATE_TEAMS = 'update-teams';
|
||||
export const DARK_MODE_CHANGE = 'dark_mode_change';
|
||||
export const USER_ACTIVITY_UPDATE = 'user-activity-update';
|
||||
|
||||
export const LOAD_RETRY = 'load_retry';
|
||||
export const LOAD_SUCCESS = 'load_success';
|
||||
export const LOAD_FAILED = 'load_fail';
|
||||
|
||||
export const MAXIMIZE_CHANGE = 'maximized_change';
|
||||
|
||||
export const OPEN_EXTERNAL = 'open_external';
|
||||
|
||||
export const DOUBLE_CLICK_ON_WINDOW = 'double_click';
|
||||
|
||||
export const SHOW_NEW_SERVER_MODAL = 'show_new_server_modal';
|
||||
|
||||
export const RETRIEVE_MODAL_INFO = 'retrieve-modal-info';
|
||||
export const MODAL_INFO = 'modal-info';
|
||||
export const MODAL_CANCEL = 'modal-cancel';
|
||||
export const MODAL_RESULT = 'modal-result';
|
||||
export const MODAL_SEND_IPC_MESSAGE = 'modal-send-ipc-message';
|
||||
export const MODAL_OPEN = 'modal-open';
|
||||
export const MODAL_CLOSE = 'modal-close';
|
||||
export const NOTIFY_MENTION = 'notify_mention';
|
||||
export const WINDOW_CLOSE = 'window_close';
|
||||
export const WINDOW_MINIMIZE = 'window_minimize';
|
||||
export const WINDOW_MAXIMIZE = 'window_maximize';
|
||||
export const WINDOW_RESTORE = 'window_restore';
|
||||
|
||||
export const UPDATE_TARGET_URL = 'update_target_url';
|
||||
|
||||
export const PLAY_SOUND = 'play_sound';
|
||||
|
||||
export const GET_DOWNLOAD_LOCATION = 'get_download_location';
|
||||
|
||||
export const FOUND_IN_PAGE = 'found-in-page';
|
||||
export const FIND_IN_PAGE = 'find-in-page';
|
||||
export const STOP_FIND_IN_PAGE = 'stop-find-in-page';
|
||||
export const CLOSE_FINDER = 'close-finder';
|
||||
export const FOCUS_FINDER = 'focus-finder';
|
||||
|
||||
export const UPDATE_MENTIONS = 'update_mentions';
|
||||
export const IS_UNREAD = 'is_unread';
|
||||
export const UNREAD_RESULT = 'unread_result';
|
||||
export const SESSION_EXPIRED = 'session_expired';
|
||||
export const UPDATE_TRAY = 'update_tray';
|
||||
export const UPDATE_BADGE = 'update_badge';
|
||||
|
||||
export const SET_SERVER_NAME = 'set-server-name';
|
||||
export const REACT_APP_INITIALIZED = 'react-app-initialized';
|
||||
|
||||
export const TOGGLE_BACK_BUTTON = 'toggle-back-button';
|
||||
|
||||
export const SHOW_SETTINGS_WINDOW = 'show-settings-window';
|
||||
|
||||
export const RECEIVED_LOADING_SCREEN_DATA = 'received-loading-screen-data';
|
||||
export const GET_LOADING_SCREEN_DATA = 'get-loading-screen-data';
|
||||
export const LOADING_SCREEN_ANIMATION_FINISHED = 'loading-screen-animation-finished';
|
||||
export const TOGGLE_LOADING_SCREEN_VISIBILITY = 'toggle-loading-screen-visibility';
|
||||
|
||||
export const SELECT_NEXT_TAB = 'select-next-tab';
|
||||
export const SELECT_PREVIOUS_TAB = 'select-previous-tab';
|
||||
export const ADD_SERVER = 'add-server';
|
||||
export const FOCUS_THREE_DOT_MENU = 'focus-three-dot-menu';
|
@@ -3,11 +3,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import WindowsRegistry from 'winreg';
|
||||
import log from 'electron-log';
|
||||
import WindowsRegistry from 'winreg-utf8';
|
||||
|
||||
const REGISTRY_HIVE_LIST = [WindowsRegistry.HKLM, WindowsRegistry.HKCU];
|
||||
const BASE_REGISTRY_KEY_PATH = '\\Software\\Policies\\Mattermost';
|
||||
export const REGISTRY_READ_EVENT = 'registry-read';
|
||||
|
||||
/**
|
||||
* Handles loading config data from the Windows registry set manually or by GPO
|
||||
@@ -35,7 +36,7 @@ export default class RegistryConfig extends EventEmitter {
|
||||
this.data.teams.push(...servers);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[RegistryConfig] Nothing retrieved for \'DefaultServerList\'', error);
|
||||
log.warn('[RegistryConfig] Nothing retrieved for \'DefaultServerList\'', error);
|
||||
}
|
||||
|
||||
// extract EnableServerManagement from the registry
|
||||
@@ -45,7 +46,7 @@ export default class RegistryConfig extends EventEmitter {
|
||||
this.data.enableServerManagement = enableServerManagement;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[RegistryConfig] Nothing retrieved for \'EnableServerManagement\'', error);
|
||||
log.warn('[RegistryConfig] Nothing retrieved for \'EnableServerManagement\'', error);
|
||||
}
|
||||
|
||||
// extract EnableAutoUpdater from the registry
|
||||
@@ -55,27 +56,29 @@ export default class RegistryConfig extends EventEmitter {
|
||||
this.data.enableAutoUpdater = enableAutoUpdater;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[RegistryConfig] Nothing retrieved for \'EnableAutoUpdater\'', error);
|
||||
log.warn('[RegistryConfig] Nothing retrieved for \'EnableAutoUpdater\'', error);
|
||||
}
|
||||
}
|
||||
|
||||
// this will happen wether we are on windows and load the info or not
|
||||
this.initialized = true;
|
||||
this.emit('update', this.data);
|
||||
this.emit(REGISTRY_READ_EVENT, this.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a list of servers
|
||||
*/
|
||||
async getServersListFromRegistry() {
|
||||
const defaultTeams = await this.getRegistryEntry(`${BASE_REGISTRY_KEY_PATH}\\DefaultServerList`);
|
||||
return defaultTeams.flat(2).reduce((teams, team) => {
|
||||
if (team) {
|
||||
teams.push({
|
||||
name: team.name,
|
||||
url: team.value,
|
||||
order: team.order,
|
||||
const defaultServers = await this.getRegistryEntry(`${BASE_REGISTRY_KEY_PATH}\\DefaultServerList`);
|
||||
return defaultServers.flat(2).reduce((servers, server, index) => {
|
||||
if (server) {
|
||||
servers.push({
|
||||
name: server.name,
|
||||
url: server.value,
|
||||
order: server.order || index,
|
||||
});
|
||||
}
|
||||
return teams;
|
||||
return servers;
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -106,7 +109,7 @@ export default class RegistryConfig extends EventEmitter {
|
||||
async getRegistryEntry(key, name) {
|
||||
const results = [];
|
||||
for (const hive of REGISTRY_HIVE_LIST) {
|
||||
results.push(this.getRegistryEntryValues(new WindowsRegistry({hive, key}), name));
|
||||
results.push(this.getRegistryEntryValues(hive, key, name));
|
||||
}
|
||||
const entryValues = await Promise.all(results);
|
||||
return entryValues.filter((value) => value);
|
||||
@@ -118,20 +121,26 @@ export default class RegistryConfig extends EventEmitter {
|
||||
* @param {WindowsRegistry} regKey A configured instance of the WindowsRegistry class
|
||||
* @param {string} name Name of the specific entry to retrieve (optional)
|
||||
*/
|
||||
getRegistryEntryValues(regKey, name) {
|
||||
return new Promise((resolve) => {
|
||||
regKey.values((error, items) => {
|
||||
if (error || !items || !items.length) {
|
||||
getRegistryEntryValues(hive, key, name) {
|
||||
const registry = new WindowsRegistry({hive, key, utf8: true});
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
registry.values((error, results) => {
|
||||
if (error || !results || results.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (name) { // looking for a single entry value
|
||||
const registryItem = items.find((item) => item.name === name);
|
||||
const registryItem = results.find((item) => item.name === name);
|
||||
resolve(registryItem && registryItem.value ? registryItem.value : null);
|
||||
} else { // looking for an entry list
|
||||
resolve(items);
|
||||
resolve(results);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(`There was an error accessing the registry for ${key}`);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ const defaultPreferences = {
|
||||
autostart: true,
|
||||
spellCheckerLocale: 'en-US',
|
||||
darkMode: false,
|
||||
downloadLocation: `/Users/${process.env.USER || process.env.USERNAME}/Downloads`
|
||||
downloadLocation: `/Users/${process.env.USER || process.env.USERNAME}/Downloads`,
|
||||
};
|
||||
|
||||
export default defaultPreferences;
|
||||
|
@@ -1,27 +1,55 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import {ipcMain, nativeTheme, app} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import * as Validator from '../../main/Validator';
|
||||
|
||||
import {UPDATE_TEAMS, GET_CONFIGURATION, UPDATE_CONFIGURATION, GET_LOCAL_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import defaultPreferences from './defaultPreferences';
|
||||
import upgradeConfigData from './upgradePreferences';
|
||||
import buildConfig from './buildConfig';
|
||||
import RegistryConfig, {REGISTRY_READ_EVENT} from './RegistryConfig';
|
||||
|
||||
/**
|
||||
* Handles loading and merging all sources of configuration as well as saving user provided config
|
||||
*/
|
||||
export default class Config extends EventEmitter {
|
||||
constructor(configFilePath, registryConfigData = {teams: []}) {
|
||||
constructor(configFilePath) {
|
||||
super();
|
||||
this.configFilePath = configFilePath;
|
||||
this.registryConfigData = registryConfigData;
|
||||
}
|
||||
|
||||
// separating constructor from init so main can setup event listeners
|
||||
init = () => {
|
||||
this.registryConfig = new RegistryConfig();
|
||||
this.registryConfig.once(REGISTRY_READ_EVENT, this.loadRegistry);
|
||||
this.registryConfig.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the teams from registry into the config object and reload
|
||||
*
|
||||
* @param {object} registryData Team configuration from the registry and if teams can be managed by user
|
||||
*/
|
||||
|
||||
loadRegistry = (registryData) => {
|
||||
this.registryConfigData = registryData;
|
||||
this.reload();
|
||||
ipcMain.handle(GET_CONFIGURATION, this.handleGetConfiguration);
|
||||
ipcMain.handle(GET_LOCAL_CONFIGURATION, this.handleGetLocalConfiguration);
|
||||
ipcMain.handle(UPDATE_TEAMS, this.handleUpdateTeams);
|
||||
ipcMain.on(UPDATE_CONFIGURATION, this.setMultiple);
|
||||
if (process.platform === 'darwin' || process.platform === 'win32') {
|
||||
nativeTheme.on('updated', this.handleUpdateTheme);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,21 +59,16 @@ export default class Config extends EventEmitter {
|
||||
* @emits {update} emitted once all data has been loaded and merged
|
||||
* @emits {synchronize} emitted when requested by a call to method; used to notify other config instances of changes
|
||||
*/
|
||||
reload(synchronize = false) {
|
||||
reload = () => {
|
||||
this.defaultConfigData = this.loadDefaultConfigData();
|
||||
this.buildConfigData = this.loadBuildConfigData();
|
||||
|
||||
this.localConfigData = this.loadLocalConfigFile();
|
||||
this.localConfigData = this.checkForConfigUpdates(this.localConfigData);
|
||||
|
||||
this.regenerateCombinedConfigData();
|
||||
|
||||
this.emit('update', this.combinedData);
|
||||
|
||||
if (synchronize) {
|
||||
this.emit('synchronize');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to save a single config property
|
||||
@@ -53,7 +76,7 @@ export default class Config extends EventEmitter {
|
||||
* @param {string} key name of config property to be saved
|
||||
* @param {*} data value to save for provided key
|
||||
*/
|
||||
set(key, data) {
|
||||
set = (key, data) => {
|
||||
if (key) {
|
||||
this.localConfigData[key] = data;
|
||||
this.regenerateCombinedConfigData();
|
||||
@@ -66,7 +89,7 @@ export default class Config extends EventEmitter {
|
||||
*
|
||||
* @param {array} properties an array of config properties to save
|
||||
*/
|
||||
setMultiple(properties = []) {
|
||||
setMultiple = (event, properties = []) => {
|
||||
if (properties.length) {
|
||||
properties.forEach(({key, data}) => {
|
||||
if (key) {
|
||||
@@ -76,9 +99,11 @@ export default class Config extends EventEmitter {
|
||||
this.regenerateCombinedConfigData();
|
||||
this.saveLocalConfigData();
|
||||
}
|
||||
|
||||
return this.localConfigData; //this is the only part that changes
|
||||
}
|
||||
|
||||
setRegistryConfigData(registryConfigData = {teams: []}) {
|
||||
setRegistryConfigData = (registryConfigData = {teams: []}) => {
|
||||
this.registryConfigData = Object.assign({}, registryConfigData);
|
||||
this.reload();
|
||||
}
|
||||
@@ -88,7 +113,7 @@ export default class Config extends EventEmitter {
|
||||
*
|
||||
* @param {object} configData a new, config data object to completely replace the existing config data
|
||||
*/
|
||||
replace(configData) {
|
||||
replace = (configData) => {
|
||||
const newConfigData = configData;
|
||||
|
||||
this.localConfigData = Object.assign({}, this.localConfigData, newConfigData);
|
||||
@@ -104,7 +129,7 @@ export default class Config extends EventEmitter {
|
||||
* @emits {synchronize} emitted once all data has been saved; used to notify other config instances of changes
|
||||
* @emits {error} emitted if saving local config data to file fails
|
||||
*/
|
||||
saveLocalConfigData() {
|
||||
saveLocalConfigData = () => {
|
||||
try {
|
||||
this.writeFile(this.configFilePath, this.localConfigData, (error) => {
|
||||
if (error) {
|
||||
@@ -192,21 +217,21 @@ export default class Config extends EventEmitter {
|
||||
/**
|
||||
* Returns a copy of the app's default config data
|
||||
*/
|
||||
loadDefaultConfigData() {
|
||||
loadDefaultConfigData = () => {
|
||||
return this.copy(defaultPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the app's build config data
|
||||
*/
|
||||
loadBuildConfigData() {
|
||||
loadBuildConfigData = () => {
|
||||
return this.copy(buildConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns locally stored config data from the filesystem or returns app defaults if no file is found
|
||||
*/
|
||||
loadLocalConfigFile() {
|
||||
loadLocalConfigFile = () => {
|
||||
let configData = {};
|
||||
try {
|
||||
configData = this.readFileSync(this.configFilePath);
|
||||
@@ -227,7 +252,7 @@ export default class Config extends EventEmitter {
|
||||
throw new Error('Provided configuration file does not validate, using defaults instead.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Failed to load configuration file from the filesystem. Using defaults.');
|
||||
log.warn('Failed to load configuration file from the filesystem. Using defaults.');
|
||||
configData = this.copy(this.defaultConfigData);
|
||||
|
||||
// add default team to teams if one exists and there arent currently any teams
|
||||
@@ -246,16 +271,16 @@ export default class Config extends EventEmitter {
|
||||
*
|
||||
* @param {*} data locally stored data
|
||||
*/
|
||||
checkForConfigUpdates(data) {
|
||||
checkForConfigUpdates = (data) => {
|
||||
let configData = data;
|
||||
try {
|
||||
if (configData.version !== this.defaultConfigData.version) {
|
||||
configData = upgradeConfigData(configData);
|
||||
this.writeFileSync(this.configFilePath, configData);
|
||||
console.log(`Configuration updated to version ${this.defaultConfigData.version} successfully.`);
|
||||
log.info(`Configuration updated to version ${this.defaultConfigData.version} successfully.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to update configuration to version ${this.defaultConfigData.version}.`);
|
||||
log.error(`Failed to update configuration to version ${this.defaultConfigData.version}.`);
|
||||
}
|
||||
return configData;
|
||||
}
|
||||
@@ -263,7 +288,7 @@ export default class Config extends EventEmitter {
|
||||
/**
|
||||
* Properly combines all sources of data into a single, manageable set of all config data
|
||||
*/
|
||||
regenerateCombinedConfigData() {
|
||||
regenerateCombinedConfigData = () => {
|
||||
// combine all config data in the correct order
|
||||
this.combinedData = Object.assign({}, this.defaultConfigData, this.localConfigData, this.buildConfigData, this.registryConfigData);
|
||||
|
||||
@@ -296,6 +321,10 @@ export default class Config extends EventEmitter {
|
||||
this.combinedData.localTeams = this.localConfigData.teams;
|
||||
this.combinedData.buildTeams = this.buildConfigData.defaultTeams;
|
||||
this.combinedData.registryTeams = this.registryConfigData.teams;
|
||||
if (process.platform === 'darwin' || process.platform === 'win32') {
|
||||
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
|
||||
}
|
||||
this.combinedData.appName = app.name;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,7 +332,7 @@ export default class Config extends EventEmitter {
|
||||
*
|
||||
* @param {array} teams array of teams to check for duplicates
|
||||
*/
|
||||
filterOutDuplicateTeams(teams) {
|
||||
filterOutDuplicateTeams = (teams) => {
|
||||
let newTeams = teams;
|
||||
const uniqueURLs = new Set();
|
||||
newTeams = newTeams.filter((team) => {
|
||||
@@ -316,7 +345,7 @@ export default class Config extends EventEmitter {
|
||||
* Returns the provided array fo teams with existing teams filtered out
|
||||
* @param {array} teams array of teams to check for already defined teams
|
||||
*/
|
||||
filterOutPredefinedTeams(teams) {
|
||||
filterOutPredefinedTeams = (teams) => {
|
||||
let newTeams = teams;
|
||||
|
||||
// filter out predefined teams
|
||||
@@ -331,7 +360,7 @@ export default class Config extends EventEmitter {
|
||||
* Apply a default sort order to the team list, if no order is specified.
|
||||
* @param {array} teams to sort
|
||||
*/
|
||||
sortUnorderedTeams(teams) {
|
||||
sortUnorderedTeams = (teams) => {
|
||||
// We want to preserve the array order of teams in the config, otherwise a lot of bugs will occur
|
||||
const mappedTeams = teams.map((team, index) => ({team, originalOrder: index}));
|
||||
|
||||
@@ -361,11 +390,11 @@ export default class Config extends EventEmitter {
|
||||
|
||||
// helper functions
|
||||
|
||||
readFileSync(filePath) {
|
||||
readFileSync = (filePath) => {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
writeFile(filePath, configData, callback) {
|
||||
writeFile = (filePath, configData, callback) => {
|
||||
if (configData.version !== this.defaultConfigData.version) {
|
||||
throw new Error('version ' + configData.version + ' is not equal to ' + this.defaultConfigData.version);
|
||||
}
|
||||
@@ -373,7 +402,7 @@ export default class Config extends EventEmitter {
|
||||
fs.writeFile(filePath, json, 'utf8', callback);
|
||||
}
|
||||
|
||||
writeFileSync(filePath, config) {
|
||||
writeFileSync = (filePath, config) => {
|
||||
if (config.version !== this.defaultConfigData.version) {
|
||||
throw new Error('version ' + config.version + ' is not equal to ' + this.defaultConfigData.version);
|
||||
}
|
||||
@@ -387,11 +416,54 @@ export default class Config extends EventEmitter {
|
||||
fs.writeFileSync(filePath, json, 'utf8');
|
||||
}
|
||||
|
||||
merge(base, target) {
|
||||
merge = (base, target) => {
|
||||
return Object.assign({}, base, target);
|
||||
}
|
||||
|
||||
copy(data) {
|
||||
copy = (data) => {
|
||||
return Object.assign({}, data);
|
||||
}
|
||||
|
||||
handleGetConfiguration = (event, option) => {
|
||||
const config = {...this.combinedData};
|
||||
if (option) {
|
||||
return config[option];
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
handleGetLocalConfiguration = (event, option) => {
|
||||
const config = {...this.localConfigData};
|
||||
config.appName = app.name;
|
||||
config.enableServerManagement = this.combinedData.enableServerManagement;
|
||||
if (option) {
|
||||
return config[option];
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
handleUpdateTeams = (event, newTeams) => {
|
||||
this.set('teams', newTeams);
|
||||
return this.combinedData.teams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects changes in darkmode if it is windows or osx, updates the config and propagates the changes
|
||||
* @emits 'darkModeChange'
|
||||
*/
|
||||
handleUpdateTheme = () => {
|
||||
if (this.combinedData.darkMode !== nativeTheme.shouldUseDarkColors) {
|
||||
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
|
||||
this.emit('darkModeChange', this.combinedData.darkMode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually toggles dark mode for OSes that don't have a native dark mode setting
|
||||
* @emits 'darkModeChange'
|
||||
*/
|
||||
toggleDarkModeManually = () => {
|
||||
this.set('darkMode', !this.combinedData.darkMode);
|
||||
this.emit('darkModeChange', this.combinedData.darkMode);
|
||||
}
|
||||
}
|
||||
|
10
src/common/utils/constants.js
Normal file
10
src/common/utils/constants.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const PRODUCTION = 'production';
|
||||
export const DEVELOPMENT = 'development';
|
||||
|
||||
export const SECOND = 1000;
|
||||
export const RELOAD_INTERVAL = 10 * SECOND;
|
||||
|
||||
export const MAX_SERVER_RETRIES = 5;
|
249
src/common/utils/url.js
Normal file
249
src/common/utils/url.js
Normal file
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {isHttpsUri, isHttpUri, isUri} from 'valid-url';
|
||||
|
||||
import buildConfig from '../config/buildConfig';
|
||||
|
||||
// supported custom login paths (oath, saml)
|
||||
const customLoginRegexPaths = [
|
||||
/^\/oauth\/authorize$/i,
|
||||
/^\/oauth\/deauthorize$/i,
|
||||
/^\/oauth\/access_token$/i,
|
||||
/^\/oauth\/[A-Za-z0-9]+\/complete$/i,
|
||||
/^\/oauth\/[A-Za-z0-9]+\/login$/i,
|
||||
/^\/oauth\/[A-Za-z0-9]+\/signup$/i,
|
||||
/^\/api\/v3\/oauth\/[A-Za-z0-9]+\/complete$/i,
|
||||
/^\/signup\/[A-Za-z0-9]+\/complete$/i,
|
||||
/^\/login\/[A-Za-z0-9]+\/complete$/i,
|
||||
/^\/login\/sso\/saml$/i,
|
||||
];
|
||||
|
||||
function getDomain(inputURL) {
|
||||
const parsedURL = parseURL(inputURL);
|
||||
return parsedURL.origin;
|
||||
}
|
||||
|
||||
function isValidURL(testURL) {
|
||||
return Boolean(isHttpUri(testURL) || isHttpsUri(testURL)) && parseURL(testURL) !== null;
|
||||
}
|
||||
|
||||
function isValidURI(testURL) {
|
||||
return Boolean(isUri(testURL));
|
||||
}
|
||||
|
||||
function parseURL(inputURL) {
|
||||
if (!inputURL) {
|
||||
return null;
|
||||
}
|
||||
if (inputURL instanceof URL) {
|
||||
return inputURL;
|
||||
}
|
||||
try {
|
||||
return new URL(inputURL);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getHost(inputURL) {
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (parsedURL) {
|
||||
return parsedURL.origin;
|
||||
}
|
||||
throw new Error(`Couldn't parse url: ${inputURL}`);
|
||||
}
|
||||
|
||||
// isInternalURL determines if the target url is internal to the application.
|
||||
// - currentURL is the current url inside the webview
|
||||
// - basename is the global export from the Mattermost application defining the subpath, if any
|
||||
function isInternalURL(targetURL, currentURL, basename = '/') {
|
||||
if (targetURL.host !== currentURL.host) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(targetURL.pathname || '/').startsWith(basename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getServerInfo(serverUrl) {
|
||||
const parsedServer = parseURL(serverUrl);
|
||||
if (!parsedServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// does the server have a subpath?
|
||||
const pn = parsedServer.pathname.toLowerCase();
|
||||
const subpath = pn.endsWith('/') ? pn.toLowerCase() : `${pn}/`;
|
||||
return {origin: parsedServer.origin, subpath, url: parsedServer};
|
||||
}
|
||||
|
||||
function getManagedResources() {
|
||||
if (!buildConfig) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return buildConfig.managedResources || [];
|
||||
}
|
||||
|
||||
function isAdminUrl(serverUrl, inputUrl) {
|
||||
const parsedURL = parseURL(inputUrl);
|
||||
const server = getServerInfo(serverUrl);
|
||||
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server, parsedURL))) {
|
||||
return null;
|
||||
}
|
||||
return (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}/admin_console/`) ||
|
||||
parsedURL.pathname.toLowerCase().startsWith('/admin_console/'));
|
||||
}
|
||||
|
||||
function isTeamUrl(serverUrl, inputUrl, withApi) {
|
||||
const parsedURL = parseURL(inputUrl);
|
||||
const server = getServerInfo(serverUrl);
|
||||
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server, parsedURL))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// pre process nonTeamUrlPaths
|
||||
let nonTeamUrlPaths = [
|
||||
'plugins',
|
||||
'signup',
|
||||
'login',
|
||||
'admin',
|
||||
'channel',
|
||||
'post',
|
||||
'oauth',
|
||||
'admin_console',
|
||||
];
|
||||
const managedResources = getManagedResources();
|
||||
nonTeamUrlPaths = nonTeamUrlPaths.concat(managedResources);
|
||||
|
||||
if (withApi) {
|
||||
nonTeamUrlPaths.push('api');
|
||||
}
|
||||
return !(nonTeamUrlPaths.some((testPath) => (
|
||||
parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${testPath}/`) ||
|
||||
parsedURL.pathname.toLowerCase().startsWith(`/${testPath}/`))));
|
||||
}
|
||||
|
||||
function isPluginUrl(serverUrl, inputURL) {
|
||||
const server = getServerInfo(serverUrl);
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (!parsedURL || !server) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
equalUrlsIgnoringSubpath(server, parsedURL) &&
|
||||
(parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}plugins/`) ||
|
||||
parsedURL.pathname.toLowerCase().startsWith('/plugins/')));
|
||||
}
|
||||
|
||||
function isManagedResource(serverUrl, inputURL) {
|
||||
const server = getServerInfo(serverUrl);
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (!parsedURL || !server) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const managedResources = getManagedResources();
|
||||
|
||||
return (
|
||||
equalUrlsIgnoringSubpath(server, parsedURL) && managedResources && managedResources.length &&
|
||||
managedResources.some((managedResource) => (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${managedResource}/`) || parsedURL.pathname.toLowerCase().startsWith(`/${managedResource}/`))));
|
||||
}
|
||||
|
||||
function getServer(inputURL, teams, ignoreScheme = false) {
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (!parsedURL) {
|
||||
return null;
|
||||
}
|
||||
let parsedServerUrl;
|
||||
let secondOption = null;
|
||||
for (let i = 0; i < teams.length; i++) {
|
||||
parsedServerUrl = parseURL(teams[i].url);
|
||||
|
||||
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
||||
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||
return {name: teams[i].name, url: parsedServerUrl, index: i};
|
||||
}
|
||||
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||
// in case the user added something on the path that doesn't really belong to the server
|
||||
// there might be more than one that matches, but we can't differentiate, so last one
|
||||
// is as good as any other in case there is no better match (e.g.: two subpath servers with the same origin)
|
||||
// e.g.: https://community.mattermost.com/core
|
||||
secondOption = {name: teams[i].name, url: parsedServerUrl, index: i};
|
||||
}
|
||||
}
|
||||
return secondOption;
|
||||
}
|
||||
|
||||
// next two functions are defined to clarify intent
|
||||
function equalUrlsWithSubpath(url1, url2, ignoreScheme) {
|
||||
if (ignoreScheme) {
|
||||
return url1.host === url2.host && url2.pathname.toLowerCase().startsWith(url1.pathname.toLowerCase());
|
||||
}
|
||||
return url1.origin === url2.origin && url2.pathname.toLowerCase().startsWith(url1.pathname.toLowerCase());
|
||||
}
|
||||
|
||||
function equalUrlsIgnoringSubpath(url1, url2, ignoreScheme) {
|
||||
if (ignoreScheme) {
|
||||
return url1.host.toLowerCase() === url2.host.toLowerCase();
|
||||
}
|
||||
return url1.origin.toLowerCase() === url2.origin.toLowerCase();
|
||||
}
|
||||
|
||||
function isTrustedURL(url, teams) {
|
||||
const parsedURL = parseURL(url);
|
||||
if (!parsedURL) {
|
||||
return false;
|
||||
}
|
||||
return getServer(parsedURL, teams) !== null;
|
||||
}
|
||||
|
||||
function isCustomLoginURL(url, server, teams) {
|
||||
const subpath = (server === null || typeof server === 'undefined') ? '' : server.url.pathname;
|
||||
const parsedURL = parseURL(url);
|
||||
if (!parsedURL) {
|
||||
return false;
|
||||
}
|
||||
if (!isTrustedURL(parsedURL, teams)) {
|
||||
return false;
|
||||
}
|
||||
const urlPath = parsedURL.pathname;
|
||||
if ((subpath !== '' || subpath !== '/') && urlPath.startsWith(subpath)) {
|
||||
const replacement = subpath.endsWith('/') ? '/' : '';
|
||||
const replacedPath = urlPath.replace(subpath, replacement);
|
||||
for (const regexPath of customLoginRegexPaths) {
|
||||
if (replacedPath.match(regexPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there is no subpath, or we are adding the team and got redirected to the real server it'll be caught here
|
||||
for (const regexPath of customLoginRegexPaths) {
|
||||
if (urlPath.match(regexPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
getDomain,
|
||||
isValidURL,
|
||||
isValidURI,
|
||||
isInternalURL,
|
||||
parseURL,
|
||||
getServer,
|
||||
getServerInfo,
|
||||
isAdminUrl,
|
||||
isTeamUrl,
|
||||
isPluginUrl,
|
||||
isManagedResource,
|
||||
getHost,
|
||||
isTrustedURL,
|
||||
isCustomLoginURL,
|
||||
};
|
51
src/common/utils/util.js
Normal file
51
src/common/utils/util.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import electron, {BrowserWindow} from 'electron';
|
||||
|
||||
import {DEVELOPMENT, PRODUCTION} from './constants';
|
||||
|
||||
function getDisplayBoundaries() {
|
||||
const {screen} = electron;
|
||||
|
||||
const displays = screen.getAllDisplays();
|
||||
|
||||
return displays.map((display) => {
|
||||
return {
|
||||
maxX: display.workArea.x + display.workArea.width,
|
||||
maxY: display.workArea.y + display.workArea.height,
|
||||
minX: display.workArea.x,
|
||||
minY: display.workArea.y,
|
||||
maxWidth: display.workArea.width,
|
||||
maxHeight: display.workArea.height,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function runMode() {
|
||||
return process.env.NODE_ENV === PRODUCTION ? PRODUCTION : DEVELOPMENT;
|
||||
}
|
||||
|
||||
// workaround until electron 12 hits, since fromWebContents return a null value if using a webcontent from browserview
|
||||
function browserWindowFromWebContents(content) {
|
||||
let window;
|
||||
if (content.type === 'browserview') {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
for (const view of win.getBrowserViews()) {
|
||||
if (view.webContents.id === content.id) {
|
||||
window = win;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
window = BrowserWindow.fromWebContents(content);
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
export default {
|
||||
getDisplayBoundaries,
|
||||
runMode,
|
||||
browserWindowFromWebContents,
|
||||
};
|
1226
src/main.js
1226
src/main.js
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import JsonFileManager from '../common/JsonFileManager';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
export default class AppStateManager extends JsonFileManager {
|
||||
constructor(file) {
|
||||
super(file);
|
||||
|
||||
// ensure data loaded from file is valid
|
||||
const validatedJSON = Validator.validateAppState(this.json);
|
||||
if (!validatedJSON) {
|
||||
this.setJson({});
|
||||
}
|
||||
}
|
||||
set lastAppVersion(version) {
|
||||
this.setValue('lastAppVersion', version);
|
||||
}
|
||||
|
||||
get lastAppVersion() {
|
||||
return this.getValue('lastAppVersion');
|
||||
}
|
||||
|
||||
set skippedVersion(version) {
|
||||
this.setValue('skippedVersion', version);
|
||||
}
|
||||
|
||||
get skippedVersion() {
|
||||
return this.getValue('skippedVersion');
|
||||
}
|
||||
|
||||
set updateCheckedDate(date) {
|
||||
this.setValue('updateCheckedDate', date.toISOString());
|
||||
}
|
||||
|
||||
get updateCheckedDate() {
|
||||
const date = this.getValue('updateCheckedDate');
|
||||
if (date) {
|
||||
return new Date(date);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
46
src/main/AppVersionManager.js
Normal file
46
src/main/AppVersionManager.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import JsonFileManager from '../common/JsonFileManager';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
export default class AppVersionManager extends JsonFileManager {
|
||||
constructor(file) {
|
||||
super(file);
|
||||
|
||||
// ensure data loaded from file is valid
|
||||
const validatedJSON = Validator.validateAppState(this.json);
|
||||
if (!validatedJSON) {
|
||||
this.setJson({});
|
||||
}
|
||||
}
|
||||
set lastAppVersion(version) {
|
||||
this.setValue('lastAppVersion', version);
|
||||
}
|
||||
|
||||
get lastAppVersion() {
|
||||
return this.getValue('lastAppVersion');
|
||||
}
|
||||
|
||||
set skippedVersion(version) {
|
||||
this.setValue('skippedVersion', version);
|
||||
}
|
||||
|
||||
get skippedVersion() {
|
||||
return this.getValue('skippedVersion');
|
||||
}
|
||||
|
||||
set updateCheckedDate(date) {
|
||||
this.setValue('updateCheckedDate', date.toISOString());
|
||||
}
|
||||
|
||||
get updateCheckedDate() {
|
||||
const date = this.getValue('updateCheckedDate');
|
||||
if (date) {
|
||||
return new Date(date);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@
|
||||
import AutoLaunch from 'auto-launch';
|
||||
import {app} from 'electron';
|
||||
import isDev from 'electron-is-dev';
|
||||
import log from 'electron-log';
|
||||
|
||||
export default class AutoLauncher {
|
||||
constructor() {
|
||||
@@ -24,7 +25,7 @@ export default class AutoLauncher {
|
||||
|
||||
async enable() {
|
||||
if (isDev) {
|
||||
console.log('In development mode, autostart config never effects');
|
||||
log.warn('In development mode, autostart config never effects');
|
||||
return this.blankPromise();
|
||||
}
|
||||
const enabled = await this.isEnabled();
|
||||
@@ -36,7 +37,7 @@ export default class AutoLauncher {
|
||||
|
||||
async disable() {
|
||||
if (isDev) {
|
||||
console.log('In development mode, autostart config never effects');
|
||||
log.warn('In development mode, autostart config never effects');
|
||||
return this.blankPromise();
|
||||
}
|
||||
const enabled = await this.isEnabled();
|
||||
|
@@ -3,6 +3,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {spawn} from 'child_process';
|
||||
import fs from 'fs';
|
||||
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
@@ -15,7 +16,8 @@ const BUTTON_SHOW_DETAILS = 'Show Details';
|
||||
const BUTTON_REOPEN = 'Reopen';
|
||||
|
||||
function createErrorReport(err) {
|
||||
return `Application: ${app.name} ${app.getVersion()}\n` +
|
||||
// eslint-disable-next-line no-undef
|
||||
return `Application: ${app.name} ${app.getVersion()} [commit: ${__HASH_VERSION__}]\n` +
|
||||
`Platform: ${os.type()} ${os.release()} ${os.arch()}\n` +
|
||||
`${err.stack}`;
|
||||
}
|
||||
@@ -77,7 +79,7 @@ export default class CriticalErrorHandler {
|
||||
buttons,
|
||||
defaultId: buttons.indexOf(BUTTON_REOPEN),
|
||||
noLink: true,
|
||||
}
|
||||
},
|
||||
).then(({response}) => {
|
||||
let child;
|
||||
switch (response) {
|
||||
@@ -87,8 +89,8 @@ export default class CriticalErrorHandler {
|
||||
child.on(
|
||||
'error',
|
||||
(spawnError) => {
|
||||
console.log(spawnError);
|
||||
}
|
||||
log.error(spawnError);
|
||||
},
|
||||
);
|
||||
child.unref();
|
||||
}
|
||||
|
30
src/main/MattermostServer.js
Normal file
30
src/main/MattermostServer.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
export class MattermostServer {
|
||||
constructor(name, serverUrl) {
|
||||
this.name = name;
|
||||
this.url = urlUtils.parseURL(serverUrl);
|
||||
if (!this.url) {
|
||||
throw new Error('Invalid url for creating a server');
|
||||
}
|
||||
}
|
||||
|
||||
getServerInfo = () => {
|
||||
// does the server have a subpath?
|
||||
const normalizedPath = this.url.pathname.toLowerCase();
|
||||
const subpath = normalizedPath.endsWith('/') ? normalizedPath : `${normalizedPath}/`;
|
||||
return {origin: this.url.origin, subpath, url: this.url.toString()};
|
||||
}
|
||||
|
||||
sameOrigin = (otherURL) => {
|
||||
const parsedUrl = urlUtils.parseURL(otherURL);
|
||||
return parsedUrl && this.url.origin === parsedUrl.origin;
|
||||
}
|
||||
|
||||
equals = (otherServer) => {
|
||||
return (this.name === otherServer.name) && (this.url.toString() === otherServer.url.toString());
|
||||
}
|
||||
}
|
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import simpleSpellChecker from 'simple-spellchecker';
|
||||
|
||||
/// Following approach for contractions is derived from electron-spellchecker.
|
||||
|
||||
// NB: This is to work around electron/electron#1005, where contractions
|
||||
// are incorrectly marked as spelling errors. This lets people get away with
|
||||
// incorrectly spelled contracted words, but it's the best we can do for now.
|
||||
const contractions = [
|
||||
"ain't", "aren't", "can't", "could've", "couldn't", "couldn't've", "didn't", "doesn't", "don't", "hadn't",
|
||||
"hadn't've", "hasn't", "haven't", "he'd", "he'd've", "he'll", "he's", "how'd", "how'll", "how's", "I'd",
|
||||
"I'd've", "I'll", "I'm", "I've", "isn't", "it'd", "it'd've", "it'll", "it's", "let's", "ma'am", "mightn't",
|
||||
"mightn't've", "might've", "mustn't", "must've", "needn't", "not've", "o'clock", "shan't", "she'd", "she'd've",
|
||||
"she'll", "she's", "should've", "shouldn't", "shouldn't've", "that'll", "that's", "there'd", "there'd've",
|
||||
"there're", "there's", "they'd", "they'd've", "they'll", "they're", "they've", "wasn't", "we'd", "we'd've",
|
||||
"we'll", "we're", "we've", "weren't", "what'll", "what're", "what's", "what've", "when's", "where'd",
|
||||
"where's", "where've", "who'd", "who'll", "who're", "who's", "who've", "why'll", "why're", "why's", "won't",
|
||||
"would've", "wouldn't", "wouldn't've", "y'all", "y'all'd've", "you'd", "you'd've", "you'll", "you're", "you've",
|
||||
];
|
||||
|
||||
const contractionMap = contractions.reduce((acc, word) => {
|
||||
acc[word.replace(/'.*/, '')] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/// End: derived from electron-spellchecker.
|
||||
|
||||
export default class SpellChecker extends EventEmitter {
|
||||
constructor(locale, dictDir, callback) {
|
||||
super();
|
||||
this.dict = null;
|
||||
this.locale = locale;
|
||||
simpleSpellChecker.getDictionary(locale, dictDir, (err, dict) => {
|
||||
if (err) {
|
||||
this.emit('error', err);
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
} else {
|
||||
this.dict = dict;
|
||||
this.emit('ready');
|
||||
if (callback) {
|
||||
callback(null, this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this.dict !== null;
|
||||
}
|
||||
|
||||
spellCheck(word) {
|
||||
if (word.toLowerCase() === 'mattermost') {
|
||||
return true;
|
||||
}
|
||||
if (isFinite(word)) { // Numerals are not included in the dictionary
|
||||
return true;
|
||||
}
|
||||
if (this.locale.match(/^en-?/) && contractionMap[word]) {
|
||||
return true;
|
||||
}
|
||||
return this.dict.spellCheck(word);
|
||||
}
|
||||
|
||||
getSuggestions(word, maxSuggestions) {
|
||||
const suggestions = this.dict.getSuggestions(word, maxSuggestions);
|
||||
|
||||
const firstCharWord = word.charAt(0);
|
||||
let i;
|
||||
for (i = 0; i < suggestions.length; i++) {
|
||||
if (suggestions[i].charAt(0).toUpperCase() === firstCharWord.toUpperCase()) {
|
||||
suggestions[i] = firstCharWord + suggestions[i].slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueSuggestions = suggestions.reduce((a, b) => {
|
||||
if (a.indexOf(b) < 0) {
|
||||
a.push(b);
|
||||
}
|
||||
return a;
|
||||
}, []);
|
||||
|
||||
return uniqueSuggestions;
|
||||
}
|
||||
}
|
||||
|
||||
SpellChecker.getSpellCheckerLocale = (electronLocale) => {
|
||||
if (electronLocale.match(/^en-?/)) {
|
||||
return 'en-US';
|
||||
}
|
||||
if (electronLocale.match(/^fr-?/)) {
|
||||
return 'fr-FR';
|
||||
}
|
||||
if (electronLocale.match(/^de-?/)) {
|
||||
return 'de-DE';
|
||||
}
|
||||
if (electronLocale.match(/^es-?/)) {
|
||||
return 'es-ES';
|
||||
}
|
||||
if (electronLocale.match(/^nl-?/)) {
|
||||
return 'nl-NL';
|
||||
}
|
||||
if (electronLocale.match(/^pl-?/)) {
|
||||
return 'pl-PL';
|
||||
}
|
||||
if (electronLocale.match(/^pt-?/)) {
|
||||
return 'pt-BR';
|
||||
}
|
||||
if (electronLocale.match(/^it-?/)) {
|
||||
return 'it-IT';
|
||||
}
|
||||
if (electronLocale.match(/^ru-?/)) {
|
||||
return 'ru-RU';
|
||||
}
|
||||
if (electronLocale.match(/^sv-?/)) {
|
||||
return 'sv-SE';
|
||||
}
|
||||
if (electronLocale.match(/^uk-?/)) {
|
||||
return 'uk-UA';
|
||||
}
|
||||
return 'en-US';
|
||||
};
|
@@ -4,6 +4,7 @@
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import electron from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
const {app} = electron;
|
||||
|
||||
@@ -61,7 +62,7 @@ export default class UserActivityMonitor extends EventEmitter {
|
||||
try {
|
||||
this.updateIdleTime(electron.powerMonitor.getSystemIdleTime());
|
||||
} catch (err) {
|
||||
console.log('Error getting system idle time:', err);
|
||||
log.error('Error getting system idle time:', err);
|
||||
}
|
||||
}, this.config.updateFrequencyMs);
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import log from 'electron-log';
|
||||
|
||||
import Joi from '@hapi/joi';
|
||||
|
||||
import urlUtils from '../utils/url';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
const defaultOptions = {
|
||||
stripUnknown: true,
|
||||
@@ -89,7 +91,7 @@ const certificateStoreSchema = Joi.object().pattern(
|
||||
Joi.object({
|
||||
data: Joi.string(),
|
||||
issuerName: Joi.string(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const originPermissionsSchema = Joi.object().keys({
|
||||
@@ -189,16 +191,16 @@ export function validateOriginPermissions(data) {
|
||||
|
||||
function validateAgainstSchema(data, schema) {
|
||||
if (typeof data !== 'object') {
|
||||
console.error(`Input 'data' is not an object we can validate: ${typeof data}`);
|
||||
log.error(`Input 'data' is not an object we can validate: ${typeof data}`);
|
||||
return false;
|
||||
}
|
||||
if (!schema) {
|
||||
console.error('No schema provided to validate');
|
||||
log.error('No schema provided to validate');
|
||||
return false;
|
||||
}
|
||||
const {error, value} = schema.validate(data, defaultOptions);
|
||||
if (error) {
|
||||
console.error(`Validation failed due to: ${error}`);
|
||||
log.error(`Validation failed due to: ${error}`);
|
||||
return false;
|
||||
}
|
||||
return value;
|
||||
|
@@ -3,14 +3,17 @@
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import {app, dialog, ipcMain, shell} from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
import {app, dialog, shell} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {protocols} from '../../electron-builder.json';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
import {getMainWindow} from './windows/windowManager';
|
||||
|
||||
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
|
||||
let allowedProtocols = [];
|
||||
@@ -22,7 +25,7 @@ function addScheme(scheme) {
|
||||
}
|
||||
}
|
||||
|
||||
function init(mainWindow) {
|
||||
function init() {
|
||||
fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => {
|
||||
if (!err) {
|
||||
allowedProtocols = JSON.parse(data);
|
||||
@@ -35,20 +38,19 @@ function init(mainWindow) {
|
||||
protocol.schemes.forEach(addScheme);
|
||||
}
|
||||
});
|
||||
initDialogEvent(mainWindow);
|
||||
});
|
||||
}
|
||||
|
||||
function initDialogEvent(mainWindow) {
|
||||
ipcMain.on('confirm-protocol', (event, protocol, URL) => {
|
||||
function handleDialogEvent(protocol, URL) {
|
||||
if (allowedProtocols.indexOf(protocol) !== -1) {
|
||||
shell.openExternal(URL);
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
dialog.showMessageBox(getMainWindow(), {
|
||||
title: 'Non http(s) protocol',
|
||||
message: `${protocol} link requires an external application.`,
|
||||
detail: `The requested link is ${URL} . Do you want to continue?`,
|
||||
defaultId: 2,
|
||||
type: 'warning',
|
||||
buttons: [
|
||||
'Yes',
|
||||
@@ -63,7 +65,7 @@ function initDialogEvent(mainWindow) {
|
||||
allowedProtocols.push(protocol);
|
||||
function handleError(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
log.error(err);
|
||||
}
|
||||
}
|
||||
fs.writeFile(allowedProtocolFile, JSON.stringify(allowedProtocols), handleError);
|
||||
@@ -77,9 +79,9 @@ function initDialogEvent(mainWindow) {
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
handleDialogEvent,
|
||||
};
|
||||
|
120
src/main/appState.js
Normal file
120
src/main/appState.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import events from 'events';
|
||||
import {ipcMain} from 'electron';
|
||||
|
||||
import {UPDATE_MENTIONS, UPDATE_TRAY, UPDATE_BADGE, SESSION_EXPIRED} from 'common/communication';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
const status = {
|
||||
unreads: new Map(),
|
||||
mentions: new Map(),
|
||||
expired: new Map(),
|
||||
emitter: new events.EventEmitter(),
|
||||
};
|
||||
|
||||
const emitMentions = (serverName) => {
|
||||
const newMentions = getMentions(serverName);
|
||||
const newUnreads = getUnreads(serverName);
|
||||
const isExpired = getIsExpired(serverName);
|
||||
|
||||
WindowManager.sendToRenderer(UPDATE_MENTIONS, serverName, newMentions, newUnreads, isExpired);
|
||||
emitStatus();
|
||||
};
|
||||
|
||||
const emitTray = (expired, mentions, unreads) => {
|
||||
status.emitter.emit(UPDATE_TRAY, expired, Boolean(mentions), unreads);
|
||||
};
|
||||
|
||||
const emitBadge = (expired, mentions, unreads) => {
|
||||
status.emitter.emit(UPDATE_BADGE, expired, mentions, unreads);
|
||||
};
|
||||
|
||||
const emitStatus = () => {
|
||||
const expired = anyExpired();
|
||||
const mentions = totalMentions();
|
||||
const unreads = anyUnreads();
|
||||
emitTray(expired, mentions, unreads);
|
||||
emitBadge(expired, mentions, unreads);
|
||||
};
|
||||
|
||||
export const updateMentions = (serverName, mentions, unreads) => {
|
||||
if (typeof unreads !== 'undefined') {
|
||||
status.unreads.set(serverName, Boolean(unreads));
|
||||
}
|
||||
status.mentions.set(serverName, mentions || 0);
|
||||
emitMentions(serverName);
|
||||
};
|
||||
|
||||
export const updateUnreads = (serverName, unreads) => {
|
||||
status.unreads.set(serverName, Boolean(unreads));
|
||||
emitMentions(serverName);
|
||||
};
|
||||
|
||||
export const getUnreads = (serverName) => {
|
||||
return status.unreads.get(serverName) || false;
|
||||
};
|
||||
|
||||
export const getMentions = (serverName) => {
|
||||
return status.mentions.get(serverName) || 0; // this might be undefined as a way to tell that we don't know as it might need to login still.
|
||||
};
|
||||
|
||||
export const getIsExpired = (serverName) => {
|
||||
return status.expired.get(serverName) || false;
|
||||
};
|
||||
|
||||
export const anyMentions = () => {
|
||||
for (const v of status.mentions.values()) {
|
||||
if (v > 0) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const totalMentions = () => {
|
||||
let total = 0;
|
||||
for (const v of status.mentions.values()) {
|
||||
total += v;
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
export const anyUnreads = () => {
|
||||
for (const v of status.unreads.values()) {
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const anyExpired = () => {
|
||||
for (const v of status.expired.values()) {
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// add any other event emitter methods if needed
|
||||
export const on = (event, listener) => {
|
||||
status.emitter.on(event, listener);
|
||||
};
|
||||
|
||||
export const setSessionExpired = (serverName, expired) => {
|
||||
const isExpired = Boolean(expired);
|
||||
const old = status.expired.get(serverName);
|
||||
status.expired.set(serverName, isExpired);
|
||||
if (typeof old !== 'undefined' && old !== isExpired) {
|
||||
emitTray();
|
||||
}
|
||||
emitMentions(serverName);
|
||||
};
|
||||
|
||||
ipcMain.on(SESSION_EXPIRED, (event, isExpired, serverName) => {
|
||||
setSessionExpired(serverName, isExpired);
|
||||
});
|
90
src/main/authManager.js
Normal file
90
src/main/authManager.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import log from 'electron-log';
|
||||
|
||||
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
import {addModal} from './views/modalManager';
|
||||
import {getLocalURLString, getLocalPreload} from './utils';
|
||||
|
||||
const modalPreload = getLocalPreload('modalPreload.js');
|
||||
const loginModalHtml = getLocalURLString('loginModal.html');
|
||||
const permissionModalHtml = getLocalURLString('permissionModal.html');
|
||||
|
||||
export class AuthManager {
|
||||
constructor(config, trustedOriginsStore) {
|
||||
this.config = config;
|
||||
this.trustedOriginsStore = trustedOriginsStore;
|
||||
this.loginCallbackMap = new Map();
|
||||
|
||||
config.on('update', this.handleConfigUpdate);
|
||||
}
|
||||
|
||||
handleConfigUpdate = (newConfig) => {
|
||||
this.config = newConfig;
|
||||
}
|
||||
|
||||
handleAppLogin = (event, webContents, request, authInfo, callback) => {
|
||||
event.preventDefault();
|
||||
const parsedURL = new URL(request.url);
|
||||
const server = urlUtils.getServer(parsedURL, this.config.teams);
|
||||
|
||||
this.loginCallbackMap.set(request.url, typeof callback === 'undefined' ? null : callback); // if callback is undefined set it to null instead so we know we have set it up with no value
|
||||
if (urlUtils.isTrustedURL(request.url, this.config.teams) || urlUtils.isCustomLoginURL(parsedURL, server, this.config.teams) || this.trustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
|
||||
this.popLoginModal(request, authInfo);
|
||||
} else {
|
||||
this.popPermissionModal(request, authInfo, BASIC_AUTH_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
popLoginModal = (request, authInfo) => {
|
||||
const modalPromise = addModal(`login-${request.url}`, loginModalHtml, modalPreload, {request, authInfo}, WindowManager.getMainWindow());
|
||||
modalPromise.then((data) => {
|
||||
const {username, password} = data;
|
||||
this.handleLoginCredentialsEvent(request, username, password);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing login request', err);
|
||||
}
|
||||
this.handleCancelLoginEvent(request);
|
||||
});
|
||||
}
|
||||
|
||||
popPermissionModal = (request, authInfo, permission) => {
|
||||
const modalPromise = addModal(`permission-${request.url}`, permissionModalHtml, modalPreload, {url: request.url, permission}, WindowManager.getMainWindow());
|
||||
modalPromise.then(() => {
|
||||
this.handlePermissionGranted(request.url, permission);
|
||||
this.addToLoginQueue(request, authInfo);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing permission request', err);
|
||||
}
|
||||
this.handleCancelLoginEvent(request);
|
||||
});
|
||||
}
|
||||
|
||||
handleLoginCredentialsEvent = (request, username, password) => {
|
||||
const callback = this.loginCallbackMap.get(request.url);
|
||||
if (typeof callback === 'undefined') {
|
||||
log.error(`Failed to retrieve login callback for ${request.url}`);
|
||||
return;
|
||||
}
|
||||
if (callback != null) {
|
||||
callback(username, password);
|
||||
}
|
||||
this.loginCallbackMap.delete(request.url);
|
||||
}
|
||||
|
||||
handleCancelLoginEvent = (request) => {
|
||||
log.info(`Cancelling request for ${request ? request.url : 'unknown'}`);
|
||||
this.handleLoginCredentialsEvent(request); // we use undefined to cancel the request
|
||||
}
|
||||
|
||||
handlePermissionGranted(url, permission) {
|
||||
this.trustedOriginsStore.addPermission(url, permission);
|
||||
this.trustedOriginsStore.save();
|
||||
}
|
||||
}
|
@@ -6,15 +6,15 @@ import path from 'path';
|
||||
|
||||
import {app, BrowserWindow, dialog, ipcMain, shell} from 'electron';
|
||||
|
||||
import logger from 'electron-log';
|
||||
import log from 'electron-log';
|
||||
import {autoUpdater, CancellationToken} from 'electron-updater';
|
||||
import semver from 'semver';
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
const UPDATER_INTERVAL_IN_MS = 48 * 60 * 60 * 1000; // 48 hours
|
||||
|
||||
autoUpdater.logger = logger;
|
||||
autoUpdater.logger.transports.file.level = 'info';
|
||||
autoUpdater.log = log;
|
||||
autoUpdater.log.transports.file.level = 'info';
|
||||
|
||||
let updaterModal = null;
|
||||
|
||||
@@ -95,7 +95,7 @@ function initialize(appState, mainWindow, notifyOnly = false) {
|
||||
autoUpdater.autoDownload = false; // To prevent upgrading on quit
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
autoUpdater.on('error', (err) => {
|
||||
console.error('Error in autoUpdater:', err.message);
|
||||
log.error('Error in autoUpdater:', err.message);
|
||||
}).on('update-available', (info) => {
|
||||
let cancellationToken = null;
|
||||
if (isUpdateApplicable(new Date(), appState.skippedVersion, info)) {
|
||||
@@ -119,7 +119,7 @@ function initialize(appState, mainWindow, notifyOnly = false) {
|
||||
updaterModal.webContents.send('start-download');
|
||||
autoUpdater.signals.progress((data) => { // eslint-disable-line max-nested-callbacks
|
||||
updaterModal.send('progress', Math.floor(data.percent));
|
||||
console.log('progress:', data);
|
||||
log.info('progress:', data);
|
||||
});
|
||||
cancellationToken = new CancellationToken();
|
||||
downloadAndInstall(cancellationToken);
|
||||
|
64
src/main/badge.js
Normal file
64
src/main/badge.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {app} from 'electron';
|
||||
|
||||
import {UPDATE_BADGE} from 'common/communication';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
import * as AppState from './appState';
|
||||
|
||||
const MAX_WIN_COUNT = 99;
|
||||
|
||||
function showBadgeWindows(sessionExpired, showUnreadBadge, mentionCount) {
|
||||
let description = 'You have no unread messages';
|
||||
let text;
|
||||
if (sessionExpired) {
|
||||
text = '•';
|
||||
description = 'Session Expired: Please sign in to continue receiving notifications.';
|
||||
} else if (mentionCount > 0) {
|
||||
text = (mentionCount > MAX_WIN_COUNT) ? `${MAX_WIN_COUNT}+` : mentionCount.toString();
|
||||
description = `You have unread mentions (${mentionCount})`;
|
||||
} else if (showUnreadBadge) {
|
||||
text = '•';
|
||||
description = 'You have unread channels';
|
||||
}
|
||||
WindowManager.setOverlayIcon(text, description, mentionCount > 99);
|
||||
}
|
||||
|
||||
function showBadgeOSX(sessionExpired, showUnreadBadge, mentionCount) {
|
||||
let badge = '';
|
||||
if (sessionExpired) {
|
||||
badge = '•';
|
||||
} else if (mentionCount > 0) {
|
||||
badge = mentionCount.toString();
|
||||
} else if (showUnreadBadge) {
|
||||
badge = '•';
|
||||
}
|
||||
app.dock.setBadge(badge);
|
||||
}
|
||||
|
||||
function showBadgeLinux(sessionExpired, showUnreadBadge, mentionCount) {
|
||||
if (app.isUnityRunning()) {
|
||||
const countExpired = sessionExpired ? 1 : 0;
|
||||
app.setBadgeCount(mentionCount + countExpired);
|
||||
}
|
||||
}
|
||||
|
||||
function showBadge(sessionExpired, mentionCount, showUnreadBadge) {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
showBadgeWindows(sessionExpired, showUnreadBadge, mentionCount);
|
||||
break;
|
||||
case 'darwin':
|
||||
showBadgeOSX(sessionExpired, showUnreadBadge, mentionCount);
|
||||
break;
|
||||
case 'linux':
|
||||
showBadgeLinux(sessionExpired, showUnreadBadge, mentionCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function setupBadge() {
|
||||
AppState.on(UPDATE_BADGE, showBadge);
|
||||
}
|
60
src/main/certificateManager.js
Normal file
60
src/main/certificateManager.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import path from 'path';
|
||||
import log from 'electron-log';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
import {addModal} from './views/modalManager';
|
||||
import {getLocalURLString} from './utils';
|
||||
|
||||
const modalPreload = path.resolve(__dirname, '../../dist/modalPreload.js');
|
||||
const html = getLocalURLString('certificateModal.html');
|
||||
|
||||
export class CertificateManager {
|
||||
constructor() {
|
||||
this.certificateRequestCallbackMap = new Map();
|
||||
}
|
||||
|
||||
handleSelectCertificate = (event, webContents, url, list, callback) => {
|
||||
if (list.length > 1) {
|
||||
event.preventDefault(); // prevent the app from getting the first certificate available
|
||||
|
||||
// store callback so it can be called with selected certificate
|
||||
this.certificateRequestCallbackMap.set(url, callback);
|
||||
this.popCertificateModal(url, list);
|
||||
} else {
|
||||
log.info(`There were ${list.length} candidate certificates. Skipping certificate selection`);
|
||||
}
|
||||
}
|
||||
|
||||
popCertificateModal = (url, list) => {
|
||||
const modalPromise = addModal(`certificate-${url}`, html, modalPreload, {url, list}, WindowManager.getMainWindow());
|
||||
modalPromise.then((data) => {
|
||||
const {cert} = data;
|
||||
this.handleSelectedCertificate(url, cert);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing certificate selection', err);
|
||||
}
|
||||
this.handleSelectedCertificate(url);
|
||||
});
|
||||
}
|
||||
|
||||
handleSelectedCertificate = (server, cert) => {
|
||||
const callback = this.certificateRequestCallbackMap.get(server);
|
||||
if (!callback) {
|
||||
log.error(`there was no callback associated with: ${server}`);
|
||||
return;
|
||||
}
|
||||
if (typeof cert === 'undefined') {
|
||||
log.info('user canceled certificate selection');
|
||||
} else {
|
||||
try {
|
||||
callback(cert);
|
||||
} catch (e) {
|
||||
log.error(`There was a problem using the selected certificate: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import urlUtils from '../utils/url';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
@@ -50,7 +50,7 @@ CertificateStore.prototype.add = function add(targetURL, certificate) {
|
||||
};
|
||||
|
||||
CertificateStore.prototype.isExisting = function isExisting(targetURL) {
|
||||
return this.data.hasOwnProperty(urlUtils.getHost(targetURL));
|
||||
return Object.prototype.hasOwnProperty.call(this.data, urlUtils.getHost(targetURL));
|
||||
};
|
||||
|
||||
CertificateStore.prototype.isTrusted = function isTrusted(targetURL, certificate) {
|
||||
|
64
src/main/contextMenu.js
Normal file
64
src/main/contextMenu.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import electronContextMenu from 'electron-context-menu';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
let disposeCurrent;
|
||||
let menuOptions = {
|
||||
shouldShowMenu: (e, p) => {
|
||||
const isInternalLink = p.linkURL.endsWith('#') && p.linkURL.slice(0, -1) === p.pageURL;
|
||||
let isInternalSrc;
|
||||
try {
|
||||
const srcurl = urlUtils.parseURL(p.srcURL);
|
||||
isInternalSrc = srcurl.protocol === 'file:';
|
||||
} catch (err) {
|
||||
isInternalSrc = false;
|
||||
}
|
||||
return p.isEditable || (p.mediaType !== 'none' && !isInternalSrc) || (p.linkURL !== '' && !isInternalLink) || p.misspelledWord !== '' || p.selectionText !== '';
|
||||
},
|
||||
showLookUpSelection: true,
|
||||
showSearchWithGoogle: true,
|
||||
showCopyImage: true,
|
||||
showSaveImage: true,
|
||||
showSaveImageAs: true,
|
||||
showServices: true,
|
||||
};
|
||||
|
||||
function dispose() {
|
||||
if (disposeCurrent) {
|
||||
disposeCurrent();
|
||||
disposeCurrent = null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveOptions(options) {
|
||||
const providedOptions = options || {};
|
||||
|
||||
menuOptions = Object.assign({}, menuOptions, providedOptions);
|
||||
}
|
||||
|
||||
function reload(target) {
|
||||
dispose();
|
||||
|
||||
/**
|
||||
* Work-around issue with passing `WebContents` to `electron-context-menu` in Electron 11
|
||||
* @see https://github.com/sindresorhus/electron-context-menu/issues/123
|
||||
*/
|
||||
const options = target ? {window: {webContents: target, inspectElement: target.inspectElement.bind(target), isDestroyed: target.isDestroyed.bind(target), off: target.off.bind(target)}, ...menuOptions} : menuOptions;
|
||||
disposeCurrent = electronContextMenu(options);
|
||||
}
|
||||
|
||||
function setup(options) {
|
||||
saveOptions(options);
|
||||
dispose();
|
||||
disposeCurrent = electronContextMenu(menuOptions);
|
||||
}
|
||||
|
||||
export default {
|
||||
setup,
|
||||
dispose,
|
||||
reload,
|
||||
};
|
@@ -2,10 +2,11 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {app} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
function flushCookiesStore(session) {
|
||||
session.cookies.flushStore().catch((err) => {
|
||||
console.log(`There was a problem flushing cookies:\n${err}`);
|
||||
log.error(`There was a problem flushing cookies:\n${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,14 +1,18 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
import zlib from 'zlib';
|
||||
|
||||
import electron from 'electron';
|
||||
const {app, dialog} = electron;
|
||||
|
||||
export default function downloadURL(browserWindow, URL, callback) {
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
export default function downloadURL(URL, callback) {
|
||||
const {net} = electron;
|
||||
const request = net.request(URL);
|
||||
request.setHeader('Accept-Encoding', 'gzip,deflate');
|
||||
@@ -18,14 +22,14 @@ export default function downloadURL(browserWindow, URL, callback) {
|
||||
defaultPath: path.join(app.getPath('downloads'), file),
|
||||
};
|
||||
dialog.showSaveDialog(
|
||||
browserWindow,
|
||||
dialogOptions
|
||||
WindowManager.getMainWindow(true),
|
||||
dialogOptions,
|
||||
).then(
|
||||
(filename) => {
|
||||
if (filename) {
|
||||
saveResponseBody(response, filename, callback);
|
||||
}
|
||||
}
|
||||
},
|
||||
).catch((err) => {
|
||||
callback(err);
|
||||
});
|
||||
|
705
src/main/main.js
Normal file
705
src/main/main.js
Normal file
@@ -0,0 +1,705 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
import fs from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import electron from 'electron';
|
||||
import isDev from 'electron-is-dev';
|
||||
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
|
||||
import log from 'electron-log';
|
||||
import 'airbnb-js-shims/target/es2015';
|
||||
|
||||
import Utils from 'common/utils/util';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import {
|
||||
SWITCH_SERVER,
|
||||
FOCUS_BROWSERVIEW,
|
||||
QUIT,
|
||||
DARK_MODE_CHANGE,
|
||||
DOUBLE_CLICK_ON_WINDOW,
|
||||
SHOW_NEW_SERVER_MODAL,
|
||||
WINDOW_CLOSE,
|
||||
WINDOW_MAXIMIZE,
|
||||
WINDOW_MINIMIZE,
|
||||
WINDOW_RESTORE,
|
||||
NOTIFY_MENTION,
|
||||
GET_DOWNLOAD_LOCATION,
|
||||
SHOW_SETTINGS_WINDOW,
|
||||
RELOAD_CONFIGURATION,
|
||||
USER_ACTIVITY_UPDATE,
|
||||
} from 'common/communication';
|
||||
import Config from 'common/config';
|
||||
|
||||
import {protocols} from '../../electron-builder.json';
|
||||
|
||||
import AutoLauncher from './AutoLauncher';
|
||||
import CriticalErrorHandler from './CriticalErrorHandler';
|
||||
import upgradeAutoLaunch from './autoLaunch';
|
||||
import CertificateStore from './certificateStore';
|
||||
import TrustedOriginsStore from './trustedOrigins';
|
||||
import appMenu from './menus/app';
|
||||
import trayMenu from './menus/tray';
|
||||
import allowProtocolDialog from './allowProtocolDialog';
|
||||
import AppVersionManager from './AppVersionManager';
|
||||
import initCookieManager from './cookieManager';
|
||||
import UserActivityMonitor from './UserActivityMonitor';
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
import {displayMention, displayDownloadCompleted} from './notifications';
|
||||
|
||||
import parseArgs from './ParseArgs';
|
||||
import {addModal} from './views/modalManager';
|
||||
import {getLocalURLString, getLocalPreload} from './utils';
|
||||
import {destroyTray, refreshTrayImages, setTrayMenu, setupTray} from './tray/tray';
|
||||
import {AuthManager} from './authManager';
|
||||
import {CertificateManager} from './certificateManager';
|
||||
import {setupBadge} from './badge';
|
||||
|
||||
if (process.env.NODE_ENV !== 'production' && module.hot) {
|
||||
module.hot.accept();
|
||||
}
|
||||
|
||||
// pull out required electron components like this
|
||||
// as not all components can be referenced before the app is ready
|
||||
const {
|
||||
app,
|
||||
Menu,
|
||||
ipcMain,
|
||||
dialog,
|
||||
session,
|
||||
} = electron;
|
||||
const criticalErrorHandler = new CriticalErrorHandler();
|
||||
const userActivityMonitor = new UserActivityMonitor();
|
||||
const certificateErrorCallbacks = new Map();
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let certificateStore = null;
|
||||
let trustedOriginsStore = null;
|
||||
let scheme = null;
|
||||
let appVersion = null;
|
||||
let config = null;
|
||||
let authManager = null;
|
||||
let certificateManager = null;
|
||||
|
||||
/**
|
||||
* Main entry point for the application, ensures that everything initializes in the proper order
|
||||
*/
|
||||
async function initialize() {
|
||||
process.on('uncaughtException', criticalErrorHandler.processUncaughtExceptionHandler.bind(criticalErrorHandler));
|
||||
global.willAppQuit = false;
|
||||
|
||||
// initialization that can run before the app is ready
|
||||
initializeArgs();
|
||||
await initializeConfig();
|
||||
initializeAppEventListeners();
|
||||
initializeBeforeAppReady();
|
||||
|
||||
// wait for registry config data to load and app ready event
|
||||
await Promise.all([
|
||||
app.whenReady(),
|
||||
]);
|
||||
|
||||
// no need to continue initializing if app is quitting
|
||||
if (global.willAppQuit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// initialization that should run once the app is ready
|
||||
initializeInterCommunicationEventListeners();
|
||||
initializeAfterAppReady();
|
||||
}
|
||||
|
||||
// attempt to initialize the application
|
||||
try {
|
||||
initialize();
|
||||
} catch (error) {
|
||||
throw new Error(`App initialization failed: ${error.toString()}`);
|
||||
}
|
||||
|
||||
//
|
||||
// initialization sub functions
|
||||
//
|
||||
|
||||
function initializeArgs() {
|
||||
global.args = parseArgs(process.argv.slice(1));
|
||||
|
||||
// output the application version via cli when requested (-v or --version)
|
||||
if (global.args.version) {
|
||||
process.stdout.write(`v.${app.getVersion()}\n`);
|
||||
process.exit(0); // eslint-disable-line no-process-exit
|
||||
}
|
||||
|
||||
global.isDev = isDev && !global.args.disableDevMode; // this doesn't seem to be right and isn't used as the single source of truth
|
||||
|
||||
if (global.args.dataDir) {
|
||||
app.setPath('userData', path.resolve(global.args.dataDir));
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeConfig() {
|
||||
const loadConfig = new Promise((resolve) => {
|
||||
config = new Config(app.getPath('userData') + '/config.json');
|
||||
config.once('update', (configData) => {
|
||||
config.on('update', handleConfigUpdate);
|
||||
config.on('synchronize', handleConfigSynchronize);
|
||||
config.on('darkModeChange', handleDarkModeChange);
|
||||
handleConfigUpdate(configData);
|
||||
resolve();
|
||||
});
|
||||
config.init();
|
||||
});
|
||||
|
||||
return loadConfig;
|
||||
}
|
||||
|
||||
function initializeAppEventListeners() {
|
||||
app.on('second-instance', handleAppSecondInstance);
|
||||
app.on('window-all-closed', handleAppWindowAllClosed);
|
||||
app.on('browser-window-created', handleAppBrowserWindowCreated);
|
||||
app.on('activate', handleAppActivate);
|
||||
app.on('before-quit', handleAppBeforeQuit);
|
||||
app.on('certificate-error', handleAppCertificateError);
|
||||
app.on('select-client-certificate', handleSelectCertificate);
|
||||
app.on('gpu-process-crashed', handleAppGPUProcessCrashed);
|
||||
app.on('login', handleAppLogin);
|
||||
app.on('will-finish-launching', handleAppWillFinishLaunching);
|
||||
}
|
||||
|
||||
function initializeBeforeAppReady() {
|
||||
certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
|
||||
trustedOriginsStore = new TrustedOriginsStore(path.resolve(app.getPath('userData'), 'trustedOrigins.json'));
|
||||
trustedOriginsStore.load();
|
||||
|
||||
// prevent using a different working directory, which happens on windows running after installation.
|
||||
const expectedPath = path.dirname(process.execPath);
|
||||
if (process.cwd() !== expectedPath && !isDev) {
|
||||
log.warn(`Current working directory is ${process.cwd()}, changing into ${expectedPath}`);
|
||||
process.chdir(expectedPath);
|
||||
}
|
||||
|
||||
// can only call this before the app is ready
|
||||
if (config.enableHardwareAcceleration === false) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
refreshTrayImages(config.trayIconTheme);
|
||||
|
||||
// If there is already an instance, quit this one
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.exit();
|
||||
global.willAppQuit = true;
|
||||
}
|
||||
|
||||
allowProtocolDialog.init();
|
||||
|
||||
authManager = new AuthManager(config, trustedOriginsStore);
|
||||
certificateManager = new CertificateManager();
|
||||
|
||||
if (isDev) {
|
||||
log.info('In development mode, deeplinking is disabled');
|
||||
} else if (protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]) {
|
||||
scheme = protocols[0].schemes[0];
|
||||
app.setAsDefaultProtocolClient(scheme);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeInterCommunicationEventListeners() {
|
||||
ipcMain.on(RELOAD_CONFIGURATION, handleReloadConfig);
|
||||
ipcMain.on(NOTIFY_MENTION, handleMentionNotification);
|
||||
ipcMain.handle('get-app-version', handleAppVersion);
|
||||
ipcMain.on('update-menu', handleUpdateMenuEvent);
|
||||
ipcMain.on(FOCUS_BROWSERVIEW, WindowManager.focusBrowserView);
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
ipcMain.on('open-app-menu', handleOpenAppMenu);
|
||||
}
|
||||
|
||||
ipcMain.on(SWITCH_SERVER, handleSwitchServer);
|
||||
|
||||
ipcMain.on(QUIT, handleQuit);
|
||||
|
||||
ipcMain.on(DOUBLE_CLICK_ON_WINDOW, WindowManager.handleDoubleClick);
|
||||
|
||||
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
|
||||
ipcMain.on(WINDOW_CLOSE, WindowManager.close);
|
||||
ipcMain.on(WINDOW_MAXIMIZE, WindowManager.maximize);
|
||||
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
|
||||
ipcMain.on(WINDOW_RESTORE, WindowManager.restore);
|
||||
ipcMain.on(SHOW_SETTINGS_WINDOW, WindowManager.showSettingsWindow);
|
||||
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
|
||||
}
|
||||
|
||||
//
|
||||
// config event handlers
|
||||
//
|
||||
|
||||
function handleConfigUpdate(newConfig) {
|
||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||
const appLauncher = new AutoLauncher();
|
||||
const autoStartTask = config.autostart ? appLauncher.enable() : appLauncher.disable();
|
||||
autoStartTask.then(() => {
|
||||
log.info('config.autostart has been configured:', newConfig.autostart);
|
||||
}).catch((err) => {
|
||||
log.error('error:', err);
|
||||
});
|
||||
WindowManager.setConfig(newConfig.data);
|
||||
}
|
||||
|
||||
ipcMain.emit('update-menu', true, config);
|
||||
}
|
||||
|
||||
function handleConfigSynchronize() {
|
||||
// TODO: send this to server manager
|
||||
WindowManager.setConfig(config.data);
|
||||
if (app.isReady()) {
|
||||
WindowManager.sendToRenderer(RELOAD_CONFIGURATION);
|
||||
}
|
||||
}
|
||||
|
||||
function handleReloadConfig() {
|
||||
config.reload();
|
||||
WindowManager.setConfig(config.data);
|
||||
}
|
||||
|
||||
function handleAppVersion() {
|
||||
return {
|
||||
name: app.getName(),
|
||||
version: app.getVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
function handleDarkModeChange(darkMode) {
|
||||
refreshTrayImages(config.trayIconTheme);
|
||||
WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode);
|
||||
WindowManager.updateLoadingScreenDarkMode(darkMode);
|
||||
}
|
||||
|
||||
//
|
||||
// app event handlers
|
||||
//
|
||||
|
||||
// activate first app instance, subsequent instances will quit themselves
|
||||
function handleAppSecondInstance(event, argv) {
|
||||
// Protocol handler for win32
|
||||
// argv: An array of the second instance’s (command line / deep linked) arguments
|
||||
const deeplinkingUrl = getDeeplinkingURL(argv);
|
||||
openDeepLink(deeplinkingUrl);
|
||||
}
|
||||
|
||||
function handleAppWindowAllClosed() {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppBrowserWindowCreated(error, newWindow) {
|
||||
// Screen cannot be required before app is ready
|
||||
resizeScreen(electron.screen, newWindow);
|
||||
}
|
||||
|
||||
function handleAppActivate() {
|
||||
WindowManager.showMainWindow();
|
||||
}
|
||||
|
||||
function handleAppBeforeQuit() {
|
||||
// Make sure tray icon gets removed if the user exits via CTRL-Q
|
||||
destroyTray();
|
||||
global.willAppQuit = true;
|
||||
}
|
||||
|
||||
function handleQuit(e, reason, stack) {
|
||||
log.error(`Exiting App. Reason: ${reason}`);
|
||||
log.info(`Stacktrace:\n${stack}`);
|
||||
handleAppBeforeQuit();
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function handleSelectCertificate(event, webContents, url, list, callback) {
|
||||
certificateManager.handleSelectCertificate(event, webContents, url, list, callback);
|
||||
}
|
||||
|
||||
function handleAppCertificateError(event, webContents, url, error, certificate, callback) {
|
||||
const parsedURL = new URL(url);
|
||||
if (!parsedURL) {
|
||||
return;
|
||||
}
|
||||
const origin = parsedURL.origin;
|
||||
if (certificateStore.isTrusted(origin, certificate)) {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
} else {
|
||||
// update the callback
|
||||
const errorID = `${origin}:${error}`;
|
||||
|
||||
// if we are already showing that error, don't add more dialogs
|
||||
if (certificateErrorCallbacks.has(errorID)) {
|
||||
log.warn(`Ignoring already shown dialog for ${errorID}`);
|
||||
certificateErrorCallbacks.set(errorID, callback);
|
||||
return;
|
||||
}
|
||||
const extraDetail = certificateStore.isExisting(origin) ? 'Certificate is different from previous one.\n\n' : '';
|
||||
const detail = `${extraDetail}origin: ${origin}\nError: ${error}`;
|
||||
|
||||
certificateErrorCallbacks.set(errorID, callback);
|
||||
|
||||
// TODO: should we move this to window manager or provide a handler for dialogs?
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
title: 'Certificate Error',
|
||||
message: 'There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.',
|
||||
type: 'error',
|
||||
detail,
|
||||
buttons: ['More Details', 'Cancel Connection'],
|
||||
cancelId: 1,
|
||||
}).then(
|
||||
({response}) => {
|
||||
if (response === 0) {
|
||||
return dialog.showMessageBox(mainWindow, {
|
||||
title: 'Certificate Not Trusted',
|
||||
message: `Certificate from "${certificate.issuerName}" is not trusted.`,
|
||||
detail: extraDetail,
|
||||
type: 'error',
|
||||
buttons: ['Trust Insecure Certificate', 'Cancel Connection'],
|
||||
cancelId: 1,
|
||||
});
|
||||
}
|
||||
return {response};
|
||||
}).then(
|
||||
({response: responseTwo}) => {
|
||||
if (responseTwo === 0) {
|
||||
certificateStore.add(origin, certificate);
|
||||
certificateStore.save();
|
||||
certificateErrorCallbacks.get(errorID)(true);
|
||||
certificateErrorCallbacks.delete(errorID);
|
||||
webContents.loadURL(url);
|
||||
} else {
|
||||
certificateErrorCallbacks.get(errorID)(false);
|
||||
certificateErrorCallbacks.delete(errorID);
|
||||
}
|
||||
}).catch(
|
||||
(dialogError) => {
|
||||
log.error(`There was an error with the Certificate Error dialog: ${dialogError}`);
|
||||
certificateErrorCallbacks.delete(errorID);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppLogin(event, webContents, request, authInfo, callback) {
|
||||
authManager.handleAppLogin(event, webContents, request, authInfo, callback);
|
||||
}
|
||||
|
||||
function handleAppGPUProcessCrashed(event, killed) {
|
||||
log.error(`The GPU process has crashed (killed = ${killed})`);
|
||||
}
|
||||
|
||||
function openDeepLink(deeplinkingUrl) {
|
||||
try {
|
||||
WindowManager.showMainWindow(deeplinkingUrl);
|
||||
} catch (err) {
|
||||
log.error(`There was an error opening the deeplinking url: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppWillFinishLaunching() {
|
||||
// Protocol handler for osx
|
||||
app.on('open-url', (event, url) => {
|
||||
log.info(`Handling deeplinking url: ${url}`);
|
||||
event.preventDefault();
|
||||
const deeplinkingUrl = getDeeplinkingURL([url]);
|
||||
if (deeplinkingUrl) {
|
||||
if (app.isReady() && deeplinkingUrl) {
|
||||
openDeepLink(deeplinkingUrl);
|
||||
} else {
|
||||
app.once('ready', () => openDeepLink(deeplinkingUrl));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleSwitchServer(event, serverName) {
|
||||
WindowManager.switchServer(serverName);
|
||||
}
|
||||
|
||||
function handleNewServerModal() {
|
||||
const html = getLocalURLString('newServer.html');
|
||||
|
||||
const modalPreload = getLocalPreload('modalPreload.js');
|
||||
|
||||
const modalPromise = addModal('newServer', html, modalPreload, {}, WindowManager.getMainWindow());
|
||||
if (modalPromise) {
|
||||
modalPromise.then((data) => {
|
||||
const teams = config.teams;
|
||||
const order = teams.length;
|
||||
teams.push({...data, order});
|
||||
config.set('teams', teams);
|
||||
}).catch((e) => {
|
||||
// e is undefined for user cancellation
|
||||
if (e) {
|
||||
log.error(`there was an error in the new server modal: ${e}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.warn('There is already a new server modal');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeAfterAppReady() {
|
||||
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
||||
|
||||
const appVersionJson = path.join(app.getPath('userData'), 'app-state.json');
|
||||
appVersion = new AppVersionManager(appVersionJson);
|
||||
if (wasUpdated(appVersion.lastAppVersion)) {
|
||||
clearAppCache();
|
||||
}
|
||||
appVersion.lastAppVersion = app.getVersion();
|
||||
|
||||
if (!global.isDev) {
|
||||
upgradeAutoLaunch();
|
||||
}
|
||||
|
||||
if (global.isDev) {
|
||||
installExtension(REACT_DEVELOPER_TOOLS).
|
||||
then((name) => log.info(`Added Extension: ${name}`)).
|
||||
catch((err) => log.error('An error occurred: ', err));
|
||||
}
|
||||
|
||||
// Workaround for MM-22193
|
||||
// From this post: https://github.com/electron/electron/issues/19468#issuecomment-549593139
|
||||
// Electron 6 has a bug that affects users on Windows 10 using dark mode, causing the app to hang
|
||||
// This workaround deletes a file that stops that from happening
|
||||
if (process.platform === 'win32') {
|
||||
const appUserDataPath = app.getPath('userData');
|
||||
const devToolsExtensionsPath = path.join(appUserDataPath, 'DevTools Extensions');
|
||||
try {
|
||||
fs.unlinkSync(devToolsExtensionsPath);
|
||||
} catch (_) {
|
||||
// don't complain if the file doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
let deeplinkingURL;
|
||||
|
||||
// Protocol handler for win32
|
||||
if (process.platform === 'win32') {
|
||||
const args = process.argv.slice(1);
|
||||
if (Array.isArray(args) && args.length > 0) {
|
||||
deeplinkingURL = getDeeplinkingURL(args);
|
||||
}
|
||||
}
|
||||
|
||||
initCookieManager(session.defaultSession);
|
||||
|
||||
WindowManager.showMainWindow(deeplinkingURL);
|
||||
|
||||
if (config.teams.length === 0) {
|
||||
WindowManager.showSettingsWindow();
|
||||
}
|
||||
|
||||
criticalErrorHandler.setMainWindow(WindowManager.getMainWindow());
|
||||
|
||||
// listen for status updates and pass on to renderer
|
||||
userActivityMonitor.on('status', (status) => {
|
||||
WindowManager.sendToMattermostViews(USER_ACTIVITY_UPDATE, status);
|
||||
});
|
||||
|
||||
// start monitoring user activity (needs to be started after the app is ready)
|
||||
userActivityMonitor.startMonitoring();
|
||||
|
||||
if (shouldShowTrayIcon()) {
|
||||
setupTray(config.trayIconTheme);
|
||||
}
|
||||
setupBadge();
|
||||
|
||||
session.defaultSession.on('will-download', (event, item, webContents) => {
|
||||
const filename = item.getFilename();
|
||||
const fileElements = filename.split('.');
|
||||
const filters = [];
|
||||
if (fileElements.length > 1) {
|
||||
filters.push({
|
||||
name: 'All files',
|
||||
extensions: ['*'],
|
||||
});
|
||||
}
|
||||
item.setSaveDialogOptions({
|
||||
title: filename,
|
||||
defaultPath: path.resolve(config.combinedData.downloadLocation, filename),
|
||||
filters,
|
||||
});
|
||||
|
||||
item.on('done', (doneEvent, state) => {
|
||||
if (state === 'completed') {
|
||||
displayDownloadCompleted(filename, item.savePath, urlUtils.getServer(webContents.getURL(), config.teams));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.emit('update-menu', true, config);
|
||||
|
||||
ipcMain.emit('update-dict');
|
||||
|
||||
// supported permission types
|
||||
const supportedPermissionTypes = [
|
||||
'media',
|
||||
'geolocation',
|
||||
'notifications',
|
||||
'fullscreen',
|
||||
'openExternal',
|
||||
];
|
||||
|
||||
// handle permission requests
|
||||
// - approve if a supported permission type and the request comes from the renderer or one of the defined servers
|
||||
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||
// is the requested permission type supported?
|
||||
if (!supportedPermissionTypes.includes(permission)) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// is the request coming from the renderer?
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
if (mainWindow && webContents.id === mainWindow.webContents.id) {
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestingURL = webContents.getURL();
|
||||
|
||||
// is the requesting url trusted?
|
||||
callback(urlUtils.isTrustedURL(requestingURL, config.teams));
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// ipc communication event handlers
|
||||
//
|
||||
|
||||
function handleMentionNotification(event, title, body, channel, teamId, silent, data) {
|
||||
displayMention(title, body, channel, teamId, silent, event.sender, data);
|
||||
}
|
||||
|
||||
function handleOpenAppMenu() {
|
||||
const windowMenu = Menu.getApplicationMenu();
|
||||
if (!windowMenu) {
|
||||
log.error('No application menu found');
|
||||
return;
|
||||
}
|
||||
windowMenu.popup({
|
||||
window: WindowManager.getMainWindow(),
|
||||
x: 18,
|
||||
y: 18,
|
||||
});
|
||||
}
|
||||
|
||||
function handleCloseAppMenu() {
|
||||
WindowManager.focusBrowserView();
|
||||
}
|
||||
|
||||
function handleUpdateMenuEvent(event, menuConfig) {
|
||||
// TODO: this might make sense to move to window manager? so it updates the window referenced if needed.
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
const aMenu = appMenu.createMenu(menuConfig);
|
||||
Menu.setApplicationMenu(aMenu);
|
||||
aMenu.addListener('menu-will-close', handleCloseAppMenu);
|
||||
|
||||
// set up context menu for tray icon
|
||||
if (shouldShowTrayIcon()) {
|
||||
const tMenu = trayMenu.createMenu(menuConfig.data);
|
||||
setTrayMenu(tMenu, mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSelectDownload(event, startFrom) {
|
||||
const message = 'Specify the folder where files will download';
|
||||
const result = await dialog.showOpenDialog({defaultPath: startFrom,
|
||||
message,
|
||||
properties:
|
||||
['openDirectory', 'createDirectory', 'dontAddToRecent', 'promptToCreate']});
|
||||
return result.filePaths[0];
|
||||
}
|
||||
|
||||
//
|
||||
// helper functions
|
||||
//
|
||||
|
||||
function getDeeplinkingURL(args) {
|
||||
if (Array.isArray(args) && args.length) {
|
||||
// deeplink urls should always be the last argument, but may not be the first (i.e. Windows with the app already running)
|
||||
const url = args[args.length - 1];
|
||||
if (url && scheme && url.startsWith(scheme) && urlUtils.isValidURI(url)) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldShowTrayIcon() {
|
||||
return config.showTrayIcon || process.platform === 'win32';
|
||||
}
|
||||
|
||||
function wasUpdated(lastAppVersion) {
|
||||
return lastAppVersion !== app.getVersion();
|
||||
}
|
||||
|
||||
function clearAppCache() {
|
||||
// TODO: clear cache on browserviews, not in the renderer.
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.session.clearCache().then(mainWindow.reload);
|
||||
} else {
|
||||
//Wait for mainWindow
|
||||
setTimeout(clearAppCache, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function isWithinDisplay(state, display) {
|
||||
const startsWithinDisplay = !(state.x > display.maxX || state.y > display.maxY || state.x < display.minX || state.y < display.minY);
|
||||
if (!startsWithinDisplay) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// is half the screen within the display?
|
||||
const midX = state.x + (state.width / 2);
|
||||
const midY = state.y + (state.height / 2);
|
||||
return !(midX > display.maxX || midY > display.maxY);
|
||||
}
|
||||
|
||||
function getValidWindowPosition(state) {
|
||||
// Check if the previous position is out of the viewable area
|
||||
// (e.g. because the screen has been plugged off)
|
||||
const boundaries = Utils.getDisplayBoundaries();
|
||||
const display = boundaries.find((boundary) => {
|
||||
return isWithinDisplay(state, boundary);
|
||||
});
|
||||
|
||||
if (typeof display === 'undefined') {
|
||||
return {};
|
||||
}
|
||||
return {x: state.x, y: state.y};
|
||||
}
|
||||
|
||||
function resizeScreen(screen, browserWindow) {
|
||||
function handle() {
|
||||
const position = browserWindow.getPosition();
|
||||
const size = browserWindow.getSize();
|
||||
const validPosition = getValidWindowPosition({
|
||||
x: position[0],
|
||||
y: position[1],
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
});
|
||||
if (typeof validPosition.x !== 'undefined' || typeof validPosition.y !== 'undefined') {
|
||||
browserWindow.setPosition(validPosition.x || 0, validPosition.y || 0);
|
||||
} else {
|
||||
browserWindow.center();
|
||||
}
|
||||
}
|
||||
|
||||
browserWindow.on('restore', handle);
|
||||
handle();
|
||||
}
|
@@ -1,174 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
import {app, BrowserWindow} from 'electron';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
function saveWindowState(file, window) {
|
||||
const windowState = window.getBounds();
|
||||
windowState.maximized = window.isMaximized();
|
||||
try {
|
||||
fs.writeFileSync(file, JSON.stringify(windowState));
|
||||
} catch (e) {
|
||||
// [Linux] error happens only when the window state is changed before the config dir is created.
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
function isFramelessWindow() {
|
||||
return os.platform() === 'darwin' || (os.platform() === 'win32' && os.release().startsWith('10'));
|
||||
}
|
||||
|
||||
function createMainWindow(config, options) {
|
||||
const defaultWindowWidth = 1000;
|
||||
const defaultWindowHeight = 700;
|
||||
const minimumWindowWidth = 400;
|
||||
const minimumWindowHeight = 240;
|
||||
|
||||
// Create the browser window.
|
||||
const boundsInfoPath = path.join(app.getPath('userData'), 'bounds-info.json');
|
||||
let windowOptions;
|
||||
try {
|
||||
windowOptions = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
||||
windowOptions = Validator.validateBoundsInfo(windowOptions);
|
||||
if (!windowOptions) {
|
||||
throw new Error('Provided bounds info file does not validate, using defaults instead.');
|
||||
}
|
||||
} catch (e) {
|
||||
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
|
||||
windowOptions = {width: defaultWindowWidth, height: defaultWindowHeight};
|
||||
}
|
||||
|
||||
const {maximized: windowIsMaximized} = windowOptions;
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
windowOptions.icon = options.linuxAppIcon;
|
||||
}
|
||||
Object.assign(windowOptions, {
|
||||
title: app.name,
|
||||
fullscreenable: true,
|
||||
show: false, // don't start the window until it is ready and only if it isn't hidden
|
||||
paintWhenInitiallyHidden: true, // we want it to start painting to get info from the webapp
|
||||
minWidth: minimumWindowWidth,
|
||||
minHeight: minimumWindowHeight,
|
||||
frame: !isFramelessWindow(),
|
||||
fullscreen: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
webviewTag: true,
|
||||
disableBlinkFeatures: 'Auxclick',
|
||||
},
|
||||
});
|
||||
|
||||
const mainWindow = new BrowserWindow(windowOptions);
|
||||
mainWindow.deeplinkingUrl = options.deeplinkingUrl;
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
|
||||
const indexURL = global.isDev ? 'http://localhost:8080/browser/index.html' : `file://${app.getAppPath()}/browser/index.html`;
|
||||
mainWindow.loadURL(indexURL);
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.webContents.zoomLevel = 0;
|
||||
|
||||
// handle showing the window when not launched by auto-start
|
||||
// - when not configured to auto-start, immediately show contents and optionally maximize as needed
|
||||
mainWindow.show();
|
||||
if (windowIsMaximized) {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.once('show', () => {
|
||||
// handle showing the app when hidden to the tray icon by auto-start
|
||||
// - optionally maximize the window as needed
|
||||
if (windowIsMaximized) {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.once('restore', () => {
|
||||
// handle restoring the window when minimized to the app icon by auto-start
|
||||
// - optionally maximize the window as needed
|
||||
if (windowIsMaximized) {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('will-attach-webview', (event, webPreferences) => {
|
||||
webPreferences.nodeIntegration = false;
|
||||
webPreferences.contextIsolation = true;
|
||||
});
|
||||
|
||||
// App should save bounds when a window is closed.
|
||||
// However, 'close' is not fired in some situations(shutdown, ctrl+c)
|
||||
// because main process is killed in such situations.
|
||||
// 'blur' event was effective in order to avoid this.
|
||||
// Ideally, app should detect that OS is shutting down.
|
||||
mainWindow.on('blur', () => {
|
||||
saveWindowState(boundsInfoPath, mainWindow);
|
||||
mainWindow.blurWebView();
|
||||
});
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
if (global.willAppQuit) { // when [Ctrl|Cmd]+Q
|
||||
saveWindowState(boundsInfoPath, mainWindow);
|
||||
} else { // Minimize or hide the window for close button.
|
||||
event.preventDefault();
|
||||
function hideWindow(window) {
|
||||
window.blur(); // To move focus to the next top-level window in Windows
|
||||
window.hide();
|
||||
}
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
hideWindow(mainWindow);
|
||||
break;
|
||||
case 'linux':
|
||||
if (config.minimizeToTray) {
|
||||
hideWindow(mainWindow);
|
||||
} else {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
break;
|
||||
case 'darwin':
|
||||
// need to leave fullscreen first, then hide the window
|
||||
if (mainWindow.isFullScreen()) {
|
||||
mainWindow.once('leave-full-screen', () => {
|
||||
app.hide();
|
||||
});
|
||||
mainWindow.setFullScreen(false);
|
||||
} else {
|
||||
app.hide();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register keyboard shortcuts
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
// Add Alt+Cmd+(Right|Left) as alternative to switch between servers
|
||||
if (process.platform === 'darwin') {
|
||||
if (input.alt && input.meta) {
|
||||
if (input.key === 'ArrowRight') {
|
||||
mainWindow.webContents.send('select-next-tab');
|
||||
}
|
||||
if (input.key === 'ArrowLeft') {
|
||||
mainWindow.webContents.send('select-previous-tab');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
export default createMainWindow;
|
@@ -3,52 +3,53 @@
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import {app, dialog, Menu, shell} from 'electron';
|
||||
import {app, Menu, session, shell, webContents} from 'electron';
|
||||
|
||||
function createTemplate(mainWindow, config, isDev) {
|
||||
const settingsURL = isDev ? 'http://localhost:8080/browser/settings.html' : `file://${app.getAppPath()}/browser/settings.html`;
|
||||
import {ADD_SERVER, SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
function createTemplate(config) {
|
||||
const separatorItem = {
|
||||
type: 'separator',
|
||||
};
|
||||
|
||||
const isMac = process.platform === 'darwin';
|
||||
const appName = app.name;
|
||||
const firstMenuName = (process.platform === 'darwin') ? appName : 'File';
|
||||
const firstMenuName = isMac ? appName : 'File';
|
||||
const template = [];
|
||||
|
||||
let platformAppMenu = process.platform === 'darwin' ? [{
|
||||
const settingsLabel = isMac ? 'Preferences...' : 'Settings...';
|
||||
|
||||
let platformAppMenu = [];
|
||||
if (isMac) {
|
||||
platformAppMenu.push(
|
||||
{
|
||||
label: 'About ' + appName,
|
||||
role: 'about',
|
||||
},
|
||||
);
|
||||
platformAppMenu.push(separatorItem);
|
||||
}
|
||||
platformAppMenu.push({
|
||||
label: settingsLabel,
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click() {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
buttons: ['OK'],
|
||||
message: `${appName} Desktop ${app.getVersion()}`,
|
||||
WindowManager.showSettingsWindow();
|
||||
},
|
||||
});
|
||||
},
|
||||
}, separatorItem, {
|
||||
label: 'Preferences...',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click() {
|
||||
mainWindow.loadURL(settingsURL);
|
||||
},
|
||||
}] : [{
|
||||
label: 'Settings...',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click() {
|
||||
mainWindow.loadURL(settingsURL);
|
||||
},
|
||||
}];
|
||||
|
||||
if (config.enableServerManagement === true) {
|
||||
if (config.data.enableServerManagement === true) {
|
||||
platformAppMenu.push({
|
||||
label: 'Sign in to Another Server',
|
||||
click() {
|
||||
mainWindow.webContents.send('add-server');
|
||||
WindowManager.sendToRenderer(ADD_SERVER);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
platformAppMenu = platformAppMenu.concat(process.platform === 'darwin' ? [
|
||||
if (isMac) {
|
||||
platformAppMenu = platformAppMenu.concat([
|
||||
separatorItem, {
|
||||
role: 'hide',
|
||||
}, {
|
||||
@@ -57,15 +58,14 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
role: 'unhide',
|
||||
}, separatorItem, {
|
||||
role: 'quit',
|
||||
}] : [
|
||||
}]);
|
||||
} else {
|
||||
platformAppMenu = platformAppMenu.concat([
|
||||
separatorItem, {
|
||||
role: 'quit',
|
||||
accelerator: 'CmdOrCtrl+Q',
|
||||
click() {
|
||||
app.quit();
|
||||
},
|
||||
}]
|
||||
);
|
||||
}]);
|
||||
}
|
||||
|
||||
template.push({
|
||||
label: '&' + firstMenuName,
|
||||
@@ -76,42 +76,23 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
template.push({
|
||||
label: '&Edit',
|
||||
submenu: [{
|
||||
label: 'Undo',
|
||||
role: 'undo',
|
||||
accelerator: 'CmdOrCtrl+Z',
|
||||
click() {
|
||||
mainWindow.webContents.send('undo');
|
||||
},
|
||||
}, {
|
||||
label: 'Redo',
|
||||
role: 'Redo',
|
||||
accelerator: 'CmdOrCtrl+SHIFT+Z',
|
||||
click() {
|
||||
mainWindow.webContents.send('redo');
|
||||
},
|
||||
}, separatorItem, {
|
||||
label: 'Cut',
|
||||
role: 'cut',
|
||||
accelerator: 'CmdOrCtrl+X',
|
||||
click() {
|
||||
mainWindow.webContents.send('cut');
|
||||
},
|
||||
}, {
|
||||
label: 'Copy',
|
||||
role: 'copy',
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
click() {
|
||||
mainWindow.webContents.send('copy');
|
||||
},
|
||||
}, {
|
||||
label: 'Paste',
|
||||
role: 'paste',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
click() {
|
||||
mainWindow.webContents.send('paste');
|
||||
},
|
||||
}, {
|
||||
label: 'Paste and Match Style',
|
||||
role: 'pasteAndMatchStyle',
|
||||
accelerator: 'CmdOrCtrl+SHIFT+V',
|
||||
visible: process.platform === 'darwin',
|
||||
click() {
|
||||
mainWindow.webContents.send('paste-and-match');
|
||||
},
|
||||
}, {
|
||||
role: 'selectall',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
@@ -121,54 +102,35 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
const viewSubMenu = [{
|
||||
label: 'Find..',
|
||||
accelerator: 'CmdOrCtrl+F',
|
||||
click(item, focusedWindow) {
|
||||
focusedWindow.webContents.send('toggle-find');
|
||||
click() {
|
||||
WindowManager.openFinder();
|
||||
},
|
||||
}, {
|
||||
label: 'Reload',
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
if (focusedWindow === mainWindow) {
|
||||
mainWindow.webContents.send('reload-tab');
|
||||
} else {
|
||||
focusedWindow.reload();
|
||||
}
|
||||
}
|
||||
click() {
|
||||
WindowManager.reload();
|
||||
},
|
||||
}, {
|
||||
label: 'Clear Cache and Reload',
|
||||
accelerator: 'Shift+CmdOrCtrl+R',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
if (focusedWindow === mainWindow) {
|
||||
mainWindow.webContents.send('clear-cache-and-reload-tab');
|
||||
} else {
|
||||
focusedWindow.webContents.session.clearCache().then(focusedWindow.reload);
|
||||
}
|
||||
}
|
||||
click() {
|
||||
session.defaultSession.clearCache();
|
||||
WindowManager.reload();
|
||||
},
|
||||
}, {
|
||||
role: 'togglefullscreen',
|
||||
accelerator: process.platform === 'darwin' ? 'Ctrl+Cmd+F' : 'F11',
|
||||
}, separatorItem, {
|
||||
label: 'Actual Size',
|
||||
role: 'resetZoom',
|
||||
accelerator: 'CmdOrCtrl+0',
|
||||
click() {
|
||||
mainWindow.webContents.send('zoom-reset');
|
||||
},
|
||||
}, {
|
||||
label: 'Zoom In',
|
||||
role: 'zoomIn',
|
||||
accelerator: 'CmdOrCtrl+SHIFT+=',
|
||||
click() {
|
||||
mainWindow.webContents.send('zoom-in');
|
||||
},
|
||||
}, {
|
||||
label: 'Zoom Out',
|
||||
role: 'zoomOut',
|
||||
accelerator: 'CmdOrCtrl+-',
|
||||
click() {
|
||||
mainWindow.webContents.send('zoom-out');
|
||||
},
|
||||
}, separatorItem, {
|
||||
label: 'Developer Tools for Application Wrapper',
|
||||
accelerator: (() => {
|
||||
@@ -179,22 +141,27 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
})(),
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.toggleDevTools();
|
||||
// toggledevtools opens it in the last known position, so sometimes it goes below the browserview
|
||||
if (focusedWindow.isDevToolsOpened()) {
|
||||
focusedWindow.closeDevTools();
|
||||
} else {
|
||||
focusedWindow.openDevTools({mode: 'detach'});
|
||||
}
|
||||
}
|
||||
},
|
||||
}, {
|
||||
label: 'Developer Tools for Current Server',
|
||||
click() {
|
||||
mainWindow.webContents.send('open-devtool');
|
||||
WindowManager.openBrowserViewDevTools();
|
||||
},
|
||||
}];
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
if (process.platform !== 'darwin' && process.platform !== 'win32') {
|
||||
viewSubMenu.push(separatorItem);
|
||||
viewSubMenu.push({
|
||||
label: 'Toggle Dark Mode',
|
||||
click() {
|
||||
mainWindow.webContents.send('set-dark-mode');
|
||||
config.toggleDarkModeManually();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -208,27 +175,25 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
submenu: [{
|
||||
label: 'Back',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Alt+Left',
|
||||
click: (item, focusedWindow) => {
|
||||
if (focusedWindow === mainWindow) {
|
||||
mainWindow.webContents.send('go-back');
|
||||
} else if (focusedWindow.webContents.canGoBack()) {
|
||||
focusedWindow.webContents.goBack();
|
||||
click: () => {
|
||||
const focused = webContents.getFocusedWebContents();
|
||||
if (focused.canGoBack()) {
|
||||
focused.goBack();
|
||||
}
|
||||
},
|
||||
}, {
|
||||
label: 'Forward',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Alt+Right',
|
||||
click: (item, focusedWindow) => {
|
||||
if (focusedWindow === mainWindow) {
|
||||
mainWindow.webContents.send('go-forward');
|
||||
} else if (focusedWindow.webContents.canGoForward()) {
|
||||
focusedWindow.webContents.goForward();
|
||||
click: () => {
|
||||
const focused = webContents.getFocusedWebContents();
|
||||
if (focused.canGoForward()) {
|
||||
focused.goForward();
|
||||
}
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
const teams = config.teams;
|
||||
const teams = config.data.teams || [];
|
||||
const windowMenu = {
|
||||
label: '&Window',
|
||||
submenu: [{
|
||||
@@ -244,39 +209,39 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
label: team.name,
|
||||
accelerator: `CmdOrCtrl+${i + 1}`,
|
||||
click() {
|
||||
mainWindow.show(); // for OS X
|
||||
mainWindow.webContents.send('switch-tab', i);
|
||||
WindowManager.switchServer(team.name, true);
|
||||
},
|
||||
};
|
||||
}), separatorItem, {
|
||||
label: 'Select Next Server',
|
||||
accelerator: 'Ctrl+Tab',
|
||||
click() {
|
||||
mainWindow.webContents.send('select-next-tab');
|
||||
WindowManager.sendToRenderer(SELECT_NEXT_TAB);
|
||||
},
|
||||
enabled: (teams.length > 1),
|
||||
}, {
|
||||
label: 'Select Previous Server',
|
||||
accelerator: 'Ctrl+Shift+Tab',
|
||||
click() {
|
||||
mainWindow.webContents.send('select-previous-tab');
|
||||
WindowManager.sendToRenderer(SELECT_PREVIOUS_TAB);
|
||||
},
|
||||
enabled: (teams.length > 1),
|
||||
}],
|
||||
};
|
||||
template.push(windowMenu);
|
||||
const submenu = [];
|
||||
if (config.helpLink) {
|
||||
if (config.data.MenuhelpLink) {
|
||||
submenu.push({
|
||||
label: 'Learn More...',
|
||||
click() {
|
||||
shell.openExternal(config.helpLink);
|
||||
shell.openExternal(config.data.helpLink);
|
||||
},
|
||||
});
|
||||
submenu.push(separatorItem);
|
||||
}
|
||||
submenu.push({
|
||||
label: `Version ${app.getVersion()}`,
|
||||
// eslint-disable-next-line no-undef
|
||||
label: `Version ${app.getVersion()} commit: ${__HASH_VERSION__}`,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
@@ -284,8 +249,8 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
return template;
|
||||
}
|
||||
|
||||
function createMenu(mainWindow, config, isDev) {
|
||||
return Menu.buildFromTemplate(createTemplate(mainWindow, config, isDev));
|
||||
function createMenu(config) {
|
||||
return Menu.buildFromTemplate(createTemplate(config));
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@@ -3,23 +3,18 @@
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import {app, Menu} from 'electron';
|
||||
import {Menu} from 'electron';
|
||||
|
||||
function createTemplate(mainWindow, config, isDev) {
|
||||
const settingsURL = isDev ? 'http://localhost:8080/browser/settings.html' : `file://${app.getAppPath()}/browser/settings.html`;
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
function createTemplate(config) {
|
||||
const teams = config.teams;
|
||||
const template = [
|
||||
...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team, i) => {
|
||||
...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team) => {
|
||||
return {
|
||||
label: team.name,
|
||||
click: () => {
|
||||
showOrRestore(mainWindow);
|
||||
mainWindow.webContents.send('switch-tab', i);
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
WindowManager.switchServer(team.name, true);
|
||||
},
|
||||
};
|
||||
}), {
|
||||
@@ -27,13 +22,7 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
}, {
|
||||
label: process.platform === 'darwin' ? 'Preferences...' : 'Settings',
|
||||
click: () => {
|
||||
mainWindow.loadURL(settingsURL);
|
||||
showOrRestore(mainWindow);
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
WindowManager.showSettingsWindow();
|
||||
},
|
||||
}, {
|
||||
type: 'separator',
|
||||
@@ -44,16 +33,8 @@ function createTemplate(mainWindow, config, isDev) {
|
||||
return template;
|
||||
}
|
||||
|
||||
function createMenu(mainWindow, config, isDev) {
|
||||
return Menu.buildFromTemplate(createTemplate(mainWindow, config, isDev));
|
||||
}
|
||||
|
||||
function showOrRestore(window) {
|
||||
if (window.isMinimized()) {
|
||||
window.restore();
|
||||
} else {
|
||||
window.show();
|
||||
}
|
||||
function createMenu(config) {
|
||||
return Menu.buildFromTemplate(createTemplate(config));
|
||||
}
|
||||
|
||||
export default {
|
||||
|
32
src/main/notifications/Download.js
Normal file
32
src/main/notifications/Download.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'path';
|
||||
import {app, Notification} from 'electron';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
|
||||
|
||||
const defaultOptions = {
|
||||
title: 'Download Complete',
|
||||
silent: false,
|
||||
icon: appIconURL,
|
||||
urgency: 'normal',
|
||||
};
|
||||
|
||||
export class DownloadNotification extends Notification {
|
||||
constructor(fileName, serverInfo) {
|
||||
const options = {...defaultOptions};
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = appIconURL;
|
||||
} else if (process.platform === 'darwin') {
|
||||
// Notification Center shows app's icon, so there were two icons on the notification.
|
||||
Reflect.deleteProperty(options, 'icon');
|
||||
}
|
||||
|
||||
options.title = process.platform === 'win32' ? serverInfo.name : 'Download Complete';
|
||||
options.body = process.platform === 'win32' ? `Download Complete \n ${fileName}` : fileName;
|
||||
|
||||
super(options);
|
||||
}
|
||||
}
|
41
src/main/notifications/Mention.js
Normal file
41
src/main/notifications/Mention.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'path';
|
||||
import {app, Notification} from 'electron';
|
||||
|
||||
import osVersion from 'common/osVersion';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
|
||||
|
||||
const defaultOptions = {
|
||||
title: 'Someone mentioned you',
|
||||
silent: false,
|
||||
icon: appIconURL,
|
||||
urgency: 'normal',
|
||||
};
|
||||
export const DEFAULT_WIN7 = 'Ding';
|
||||
|
||||
export class Mention extends Notification {
|
||||
constructor(customOptions, channel, teamId) {
|
||||
const options = {...defaultOptions, ...customOptions};
|
||||
if (process.platform === 'darwin') {
|
||||
// Notification Center shows app's icon, so there were two icons on the notification.
|
||||
Reflect.deleteProperty(options, 'icon');
|
||||
}
|
||||
const isWin7 = (process.platform === 'win32' && osVersion.isLowerThanOrEqualWindows8_1() && DEFAULT_WIN7);
|
||||
const customSound = !options.silent && ((options.data && options.data.soundName !== 'None' && options.data.soundName) || isWin7);
|
||||
if (customSound) {
|
||||
options.silent = true;
|
||||
}
|
||||
super(options);
|
||||
this.customSound = customSound;
|
||||
this.channel = channel;
|
||||
this.teamId = teamId;
|
||||
}
|
||||
|
||||
getNotificationSound = () => {
|
||||
return this.customSound;
|
||||
}
|
||||
}
|
74
src/main/notifications/index.js
Normal file
74
src/main/notifications/index.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shell, Notification} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {PLAY_SOUND} from 'common/communication';
|
||||
|
||||
import * as windowManager from '../windows/windowManager';
|
||||
|
||||
import {Mention} from './Mention';
|
||||
import {DownloadNotification} from './Download';
|
||||
|
||||
const currentNotifications = new Map();
|
||||
|
||||
export function displayMention(title, body, channel, teamId, silent, webcontents, data) {
|
||||
if (!Notification.isSupported()) {
|
||||
log.error('notification not supported');
|
||||
return;
|
||||
}
|
||||
const serverName = windowManager.getServerNameByWebContentsId(webcontents.id);
|
||||
|
||||
const options = {
|
||||
title: `${serverName}: ${title}`,
|
||||
body,
|
||||
silent,
|
||||
data,
|
||||
};
|
||||
|
||||
const mention = new Mention(options, channel, teamId);
|
||||
const mentionKey = `${mention.teamId}:${mention.channel.id}`;
|
||||
|
||||
mention.on('show', () => {
|
||||
// On Windows, manually dismiss notifications from the same channel and only show the latest one
|
||||
if (process.platform === 'win32') {
|
||||
if (currentNotifications.has(mentionKey)) {
|
||||
log.info(`close ${mentionKey}`);
|
||||
currentNotifications.get(mentionKey).close();
|
||||
currentNotifications.delete(mentionKey);
|
||||
}
|
||||
currentNotifications.set(mentionKey, mention);
|
||||
}
|
||||
const notificationSound = mention.getNotificationSound();
|
||||
if (notificationSound) {
|
||||
windowManager.sendToRenderer(PLAY_SOUND, notificationSound);
|
||||
}
|
||||
windowManager.flashFrame(true);
|
||||
});
|
||||
|
||||
mention.on('click', () => {
|
||||
if (serverName) {
|
||||
windowManager.switchServer(serverName, true);
|
||||
webcontents.send('notification-clicked', {channel, teamId});
|
||||
}
|
||||
});
|
||||
mention.show();
|
||||
}
|
||||
|
||||
export function displayDownloadCompleted(fileName, path, serverInfo) {
|
||||
if (!Notification.isSupported()) {
|
||||
log.error('notification not supported');
|
||||
return;
|
||||
}
|
||||
const download = new DownloadNotification(fileName, serverInfo);
|
||||
|
||||
download.on('show', () => {
|
||||
windowManager.flashFrame(true);
|
||||
});
|
||||
|
||||
download.on('click', () => {
|
||||
shell.showItemInFolder(path.normalize());
|
||||
});
|
||||
download.show();
|
||||
}
|
35
src/main/preload/finderPreload.js
Normal file
35
src/main/preload/finderPreload.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
'use strict';
|
||||
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import {FOUND_IN_PAGE, FIND_IN_PAGE, STOP_FIND_IN_PAGE, CLOSE_FINDER, FOCUS_FINDER} from 'common/communication';
|
||||
|
||||
console.log('preloaded for the finder!');
|
||||
|
||||
window.addEventListener('message', async (event) => {
|
||||
switch (event.data.type) {
|
||||
case FIND_IN_PAGE:
|
||||
ipcRenderer.send(FIND_IN_PAGE, event.data.data.searchText, event.data.data.options);
|
||||
break;
|
||||
case STOP_FIND_IN_PAGE:
|
||||
ipcRenderer.send(STOP_FIND_IN_PAGE, event.data.data);
|
||||
break;
|
||||
case CLOSE_FINDER:
|
||||
ipcRenderer.send(CLOSE_FINDER);
|
||||
break;
|
||||
case FOCUS_FINDER:
|
||||
ipcRenderer.send(FOCUS_FINDER);
|
||||
break;
|
||||
default:
|
||||
console.log(`got a message: ${event}`);
|
||||
console.log(event);
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on(FOUND_IN_PAGE, (event, result) => {
|
||||
window.postMessage({type: FOUND_IN_PAGE, data: result}, window.location.href);
|
||||
});
|
33
src/main/preload/loadingScreenPreload.js
Normal file
33
src/main/preload/loadingScreenPreload.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
'use strict';
|
||||
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
|
||||
|
||||
console.log('preloaded for the loading screen!');
|
||||
|
||||
window.addEventListener('message', async (event) => {
|
||||
switch (event.data.type) {
|
||||
case GET_LOADING_SCREEN_DATA:
|
||||
window.postMessage({type: RECEIVED_LOADING_SCREEN_DATA, data: await ipcRenderer.invoke(GET_LOADING_SCREEN_DATA)}, window.location.href);
|
||||
break;
|
||||
case LOADING_SCREEN_ANIMATION_FINISHED:
|
||||
ipcRenderer.send(LOADING_SCREEN_ANIMATION_FINISHED);
|
||||
break;
|
||||
default:
|
||||
console.log(`got a message: ${event}`);
|
||||
console.log(event);
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on(GET_LOADING_SCREEN_DATA, (_, result) => {
|
||||
window.postMessage({type: RECEIVED_LOADING_SCREEN_DATA, data: result}, window.location.href);
|
||||
});
|
||||
|
||||
ipcRenderer.on(TOGGLE_LOADING_SCREEN_VISIBILITY, (_, toggle) => {
|
||||
window.postMessage({type: TOGGLE_LOADING_SCREEN_VISIBILITY, data: toggle}, window.location.href);
|
||||
});
|
208
src/main/preload/mattermost.js
Normal file
208
src/main/preload/mattermost.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
import {ipcRenderer, webFrame} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {NOTIFY_MENTION, IS_UNREAD, UNREAD_RESULT, SESSION_EXPIRED, SET_SERVER_NAME, REACT_APP_INITIALIZED, USER_ACTIVITY_UPDATE} from 'common/communication';
|
||||
|
||||
const UNREAD_COUNT_INTERVAL = 1000;
|
||||
const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
|
||||
|
||||
Reflect.deleteProperty(global.Buffer); // http://electron.atom.io/docs/tutorial/security/#buffer-global
|
||||
|
||||
let appVersion;
|
||||
let appName;
|
||||
let sessionExpired;
|
||||
let serverName;
|
||||
|
||||
log.info('Initializing preload');
|
||||
|
||||
ipcRenderer.invoke('get-app-version').then(({name, version}) => {
|
||||
appVersion = version;
|
||||
appName = name;
|
||||
});
|
||||
|
||||
function isReactAppInitialized() {
|
||||
const initializedRoot =
|
||||
document.querySelector('#root.channel-view') || // React 16 webapp
|
||||
document.querySelector('#root .signup-team__container') || // React 16 login
|
||||
document.querySelector('div[data-reactroot]'); // Older React apps
|
||||
if (initializedRoot === null) {
|
||||
return false;
|
||||
}
|
||||
return initializedRoot.children.length !== 0;
|
||||
}
|
||||
|
||||
function watchReactAppUntilInitialized(callback) {
|
||||
let count = 0;
|
||||
const interval = 500;
|
||||
const timeout = 30000;
|
||||
const timer = setInterval(() => {
|
||||
count += interval;
|
||||
if (isReactAppInitialized() || count >= timeout) { // assumed as webapp has been initialized.
|
||||
clearTimeout(timer);
|
||||
callback();
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if (document.getElementById('root') === null) {
|
||||
console.log('The guest is not assumed as mattermost-webapp');
|
||||
return;
|
||||
}
|
||||
watchReactAppUntilInitialized(() => {
|
||||
ipcRenderer.send(REACT_APP_INITIALIZED, serverName);
|
||||
});
|
||||
});
|
||||
|
||||
const parentTag = (target) => {
|
||||
if (target.parentNode) {
|
||||
return target.parentNode.tagName.toUpperCase();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
document.addEventListener('mouseover', (event) => {
|
||||
if (event.target && (event.target.tagName === 'A')) {
|
||||
ipcRenderer.send('update-target-url', event.target.href);
|
||||
} else if (event.target && (parentTag(event.target) === 'A')) {
|
||||
ipcRenderer.send('update-target-url', event.target.parentNode.href);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', (event) => {
|
||||
if (event.target && event.target.tagName === 'A') {
|
||||
ipcRenderer.send('delete-target-url', event.target.href);
|
||||
}
|
||||
});
|
||||
|
||||
// listen for messages from the webapp
|
||||
window.addEventListener('message', ({origin, data = {}} = {}) => {
|
||||
const {type, message = {}} = data;
|
||||
if (origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case 'webapp-ready': {
|
||||
// register with the webapp to enable custom integration functionality
|
||||
console.log(`registering ${appName} v${appVersion} with the server`);
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'register-desktop',
|
||||
message: {
|
||||
version: appVersion,
|
||||
name: appName,
|
||||
},
|
||||
},
|
||||
window.location.origin || '*',
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'register-desktop':
|
||||
// it will be captured by itself too
|
||||
break;
|
||||
case 'dispatch-notification': {
|
||||
const {title, body, channel, teamId, silent, data: messageData} = message;
|
||||
ipcRenderer.send(NOTIFY_MENTION, title, body, channel, teamId, silent, messageData);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (typeof type === 'undefined') {
|
||||
console.log('ignoring message of undefined type:');
|
||||
console.log(data);
|
||||
} else {
|
||||
console.log(`ignored message of type: ${type}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleNotificationClick = ({channel, teamId}) => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'notification-clicked',
|
||||
message: {
|
||||
channel,
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
ipcRenderer.on('notification-clicked', (event, data) => {
|
||||
handleNotificationClick(data);
|
||||
});
|
||||
|
||||
const findUnread = (favicon) => {
|
||||
const classes = ['team-container unreads', 'SidebarChannel unread', 'sidebar-item unread-title'];
|
||||
const isUnread = classes.some((classPair) => {
|
||||
const result = document.getElementsByClassName(classPair);
|
||||
return result && result.length > 0;
|
||||
});
|
||||
ipcRenderer.send(UNREAD_RESULT, favicon, serverName, isUnread);
|
||||
};
|
||||
|
||||
ipcRenderer.on(IS_UNREAD, (event, favicon, server) => {
|
||||
if (typeof serverName === 'undefined') {
|
||||
serverName = server;
|
||||
}
|
||||
if (isReactAppInitialized()) {
|
||||
findUnread(favicon);
|
||||
} else {
|
||||
watchReactAppUntilInitialized(() => {
|
||||
findUnread(favicon);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on(SET_SERVER_NAME, (_, name) => {
|
||||
serverName = name;
|
||||
});
|
||||
|
||||
function getUnreadCount() {
|
||||
// LHS not found => Log out => Count should be 0, but session may be expired.
|
||||
if (typeof serverName !== 'undefined') {
|
||||
let isExpired;
|
||||
if (document.getElementById('sidebar-left') === null) {
|
||||
const extraParam = (new URLSearchParams(window.location.search)).get('extra');
|
||||
isExpired = extraParam === 'expired';
|
||||
} else {
|
||||
isExpired = false;
|
||||
}
|
||||
if (isExpired !== sessionExpired) {
|
||||
sessionExpired = isExpired;
|
||||
ipcRenderer.send(SESSION_EXPIRED, sessionExpired, serverName);
|
||||
}
|
||||
}
|
||||
}
|
||||
setInterval(getUnreadCount, UNREAD_COUNT_INTERVAL);
|
||||
|
||||
// push user activity updates to the webapp
|
||||
ipcRenderer.on(USER_ACTIVITY_UPDATE, (event, {userIsActive, isSystemEvent}) => {
|
||||
if (window.location.origin !== 'null') {
|
||||
window.postMessage({type: USER_ACTIVITY_UPDATE, message: {userIsActive, manual: isSystemEvent}}, window.location.origin);
|
||||
}
|
||||
});
|
||||
|
||||
// exit fullscreen embedded elements like youtube - https://mattermost.atlassian.net/browse/MM-19226
|
||||
ipcRenderer.on('exit-fullscreen', () => {
|
||||
if (document.fullscreenElement && document.fullscreenElement.nodeName.toLowerCase() === 'iframe') {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
// mattermost-webapp is SPA. So cache is not cleared due to no navigation.
|
||||
// We needed to manually clear cache to free memory in long-term-use.
|
||||
// http://seenaburns.com/debugging-electron-memory-usage/
|
||||
setInterval(() => {
|
||||
webFrame.clearCache();
|
||||
}, CLEAR_CACHE_INTERVAL);
|
||||
|
||||
/* eslint-enable no-magic-numbers */
|
43
src/main/preload/modalPreload.js
Normal file
43
src/main/preload/modalPreload.js
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
|
||||
|
||||
'use strict';
|
||||
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, MODAL_INFO, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication';
|
||||
|
||||
console.log('preloaded for the modal!');
|
||||
|
||||
window.addEventListener('message', async (event) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_CANCEL: {
|
||||
console.log('canceling modal');
|
||||
ipcRenderer.send(MODAL_CANCEL, event.data.data);
|
||||
break;
|
||||
}
|
||||
case MODAL_RESULT: {
|
||||
console.log(`accepting modal with ${event.data.data}`);
|
||||
ipcRenderer.send(MODAL_RESULT, event.data.data);
|
||||
break;
|
||||
}
|
||||
case RETRIEVE_MODAL_INFO:
|
||||
console.log('getting modal data');
|
||||
window.postMessage({type: MODAL_INFO, data: await ipcRenderer.invoke(RETRIEVE_MODAL_INFO)}, window.location.href);
|
||||
break;
|
||||
case MODAL_SEND_IPC_MESSAGE:
|
||||
console.log('sending custom ipc message');
|
||||
ipcRenderer.send(event.data.data.type, ...event.data.data.args);
|
||||
break;
|
||||
default:
|
||||
console.log(`got a message: ${event}`);
|
||||
console.log(event);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
ipcRenderer.send(MODAL_CANCEL);
|
||||
}
|
||||
});
|
150
src/main/tray/tray.js
Normal file
150
src/main/tray/tray.js
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'path';
|
||||
import {app, nativeImage, nativeTheme, Tray, systemPreferences} from 'electron';
|
||||
|
||||
import {UPDATE_TRAY} from 'common/communication';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import * as AppState from '../appState';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
|
||||
let trayImages;
|
||||
let trayIcon;
|
||||
let lastStatus = 'normal';
|
||||
let lastMessage = app.name;
|
||||
|
||||
export function refreshTrayImages(trayIconTheme) {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
trayImages = {
|
||||
normal: nativeImage.createFromPath(path.resolve(assetsDir, 'windows/tray.ico')),
|
||||
unread: nativeImage.createFromPath(path.resolve(assetsDir, 'windows/tray_unread.ico')),
|
||||
mention: nativeImage.createFromPath(path.resolve(assetsDir, 'windows/tray_mention.ico')),
|
||||
};
|
||||
break;
|
||||
case 'darwin':
|
||||
{
|
||||
trayImages = {
|
||||
light: {
|
||||
normal: nativeImage.createFromPath(path.resolve(assetsDir, 'osx/MenuIcon.png')),
|
||||
unread: nativeImage.createFromPath(path.resolve(assetsDir, 'osx/MenuIconUnread.png')),
|
||||
mention: nativeImage.createFromPath(path.resolve(assetsDir, 'osx/MenuIconMention.png')),
|
||||
},
|
||||
clicked: {
|
||||
normal: nativeImage.createFromPath(path.resolve(assetsDir, 'osx/ClickedMenuIcon.png')),
|
||||
unread: nativeImage.createFromPath(path.resolve(assetsDir, 'osx/ClickedMenuIconUnread.png')),
|
||||
mention: nativeImage.createFromPath(path.resolve(assetsDir, 'osx/ClickedMenuIconMention.png')),
|
||||
},
|
||||
};
|
||||
switchMenuIconImages(trayImages, nativeTheme.shouldUseDarkColors);
|
||||
break;
|
||||
}
|
||||
case 'linux':
|
||||
{
|
||||
const theme = trayIconTheme;
|
||||
try {
|
||||
trayImages = {
|
||||
normal: nativeImage.createFromPath(path.resolve(assetsDir, 'linux', theme, 'MenuIconTemplate.png')),
|
||||
unread: nativeImage.createFromPath(path.resolve(assetsDir, 'linux', theme, 'MenuIconUnreadTemplate.png')),
|
||||
mention: nativeImage.createFromPath(path.resolve(assetsDir, 'linux', theme, 'MenuIconMentionTemplate.png')),
|
||||
};
|
||||
} catch (e) {
|
||||
//Fallback for invalid theme setting
|
||||
trayImages = {
|
||||
normal: nativeImage.createFromPath(path.resolve(assetsDir, 'linux', 'light', 'MenuIconTemplate.png')),
|
||||
unread: nativeImage.createFromPath(path.resolve(assetsDir, 'linux', 'light', 'MenuIconUnreadTemplate.png')),
|
||||
mention: nativeImage.createFromPath(path.resolve(assetsDir, 'linux', 'light', 'MenuIconMentionTemplate.png')),
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
trayImages = {};
|
||||
}
|
||||
if (trayIcon) {
|
||||
setTray(lastStatus, lastMessage);
|
||||
}
|
||||
return trayImages;
|
||||
}
|
||||
|
||||
export function switchMenuIconImages(icons, isDarkMode) {
|
||||
if (isDarkMode) {
|
||||
icons.normal = icons.clicked.normal;
|
||||
icons.unread = icons.clicked.unread;
|
||||
icons.mention = icons.clicked.mention;
|
||||
} else {
|
||||
icons.normal = icons.light.normal;
|
||||
icons.unread = icons.light.unread;
|
||||
icons.mention = icons.light.mention;
|
||||
}
|
||||
}
|
||||
|
||||
export function setupTray(icontheme) {
|
||||
refreshTrayImages(icontheme);
|
||||
trayIcon = new Tray(trayImages.normal);
|
||||
if (process.platform === 'darwin') {
|
||||
trayIcon.setPressedImage(trayImages.clicked.normal);
|
||||
systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => {
|
||||
switchMenuIconImages(trayImages, nativeTheme.shouldUseDarkColors);
|
||||
trayIcon.setImage(trayImages.normal);
|
||||
});
|
||||
}
|
||||
|
||||
trayIcon.setToolTip(app.name);
|
||||
trayIcon.on('click', () => {
|
||||
WindowManager.restoreMain();
|
||||
});
|
||||
|
||||
trayIcon.on('right-click', () => {
|
||||
trayIcon.popUpContextMenu();
|
||||
});
|
||||
trayIcon.on('balloon-click', () => {
|
||||
WindowManager.restoreMain();
|
||||
});
|
||||
|
||||
AppState.on(UPDATE_TRAY, (anyExpired, anyMentions, anyUnreads) => {
|
||||
if (anyExpired) {
|
||||
setTray('mention', 'Session Expired: Please sign in to continue receiving notifications.');
|
||||
} else if (anyMentions) {
|
||||
setTray('mention', 'You have been mentioned');
|
||||
} else if (anyUnreads) {
|
||||
setTray('unread', 'You have unread channels');
|
||||
} else {
|
||||
setTray('normal', app.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setTray(status, message) {
|
||||
lastStatus = status;
|
||||
lastMessage = message;
|
||||
trayIcon.setImage(trayImages[status]);
|
||||
if (process.platform === 'darwin') {
|
||||
trayIcon.setPressedImage(trayImages.clicked[status]);
|
||||
}
|
||||
trayIcon.setToolTip(message);
|
||||
}
|
||||
|
||||
export function destroyTray() {
|
||||
if (trayIcon && process.platform === 'win32') {
|
||||
trayIcon.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export function setTrayMenu(tMenu, mainWindow) {
|
||||
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||
// store the information, if the tray was initialized, for checking in the settings, if the application
|
||||
// was restarted after setting "Show icon on menu bar"
|
||||
if (trayIcon) {
|
||||
trayIcon.setContextMenu(tMenu);
|
||||
mainWindow.trayWasVisible = true;
|
||||
} else {
|
||||
mainWindow.trayWasVisible = false;
|
||||
}
|
||||
} else if (trayIcon) {
|
||||
trayIcon.setContextMenu(tMenu);
|
||||
}
|
||||
}
|
@@ -7,7 +7,7 @@ import fs from 'fs';
|
||||
|
||||
import log from 'electron-log';
|
||||
|
||||
import urlUtils from '../utils/url';
|
||||
import urlUtils from '../common/utils/url';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
|
@@ -2,7 +2,14 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {app} from 'electron';
|
||||
import electron, {app} from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
import {PRODUCTION} from 'common/utils/constants';
|
||||
import Utils from 'common/utils/util';
|
||||
|
||||
const TAB_BAR_HEIGHT = 40;
|
||||
const BACK_BAR_HEIGHT = 36;
|
||||
|
||||
export function shouldBeHiddenOnStartup(parsedArgv) {
|
||||
if (parsedArgv.hidden) {
|
||||
@@ -15,3 +22,52 @@ export function shouldBeHiddenOnStartup(parsedArgv) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getWindowBoundaries(win, hasBackBar = false) {
|
||||
const {width, height} = win.getContentBounds();
|
||||
return getAdjustedWindowBoundaries(width, height, hasBackBar);
|
||||
}
|
||||
|
||||
export function getAdjustedWindowBoundaries(width, height, hasBackBar = false) {
|
||||
return {
|
||||
x: 0,
|
||||
y: TAB_BAR_HEIGHT + (hasBackBar ? BACK_BAR_HEIGHT : 0),
|
||||
width,
|
||||
height: height - TAB_BAR_HEIGHT - (hasBackBar ? BACK_BAR_HEIGHT : 0),
|
||||
};
|
||||
}
|
||||
|
||||
export function getLocalURLString(urlPath, query, isMain) {
|
||||
const localURL = getLocalURL(urlPath, query, isMain);
|
||||
return localURL.href;
|
||||
}
|
||||
|
||||
export function getLocalURL(urlPath, query, isMain) {
|
||||
let pathname;
|
||||
const processPath = isMain ? '' : '/renderer';
|
||||
const mode = Utils.runMode();
|
||||
const protocol = 'file';
|
||||
const hostname = '';
|
||||
const port = '';
|
||||
if (mode === PRODUCTION) {
|
||||
pathname = path.join(electron.app.getAppPath(), `${processPath}/${urlPath}`);
|
||||
} else {
|
||||
pathname = path.resolve(__dirname, `../../dist/${processPath}/${urlPath}`); // TODO: find a better way to work with webpack on this
|
||||
}
|
||||
const localUrl = new URL(`${protocol}://${hostname}${port}`);
|
||||
localUrl.pathname = pathname;
|
||||
if (query) {
|
||||
query.forEach((value, key) => {
|
||||
localUrl.searchParams.append(encodeURIComponent(key), encodeURIComponent(value));
|
||||
});
|
||||
}
|
||||
|
||||
return localUrl;
|
||||
}
|
||||
|
||||
export function getLocalPreload(file) {
|
||||
if (Utils.runMode() === PRODUCTION) {
|
||||
return path.join(electron.app.getAppPath(), `${file}`);
|
||||
}
|
||||
return path.resolve(__dirname, `../../dist/${file}`);
|
||||
}
|
||||
|
303
src/main/views/MattermostView.js
Normal file
303
src/main/views/MattermostView.js
Normal file
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserView, app, ipcMain} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND} from 'common/utils/constants';
|
||||
import urlUtils from 'common/utils/url';
|
||||
import {LOAD_RETRY, LOAD_SUCCESS, LOAD_FAILED, UPDATE_TARGET_URL, IS_UNREAD, UNREAD_RESULT, TOGGLE_BACK_BUTTON, SET_SERVER_NAME} from 'common/communication';
|
||||
|
||||
import {getWindowBoundaries, getLocalPreload} from '../utils';
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
import * as appState from '../appState';
|
||||
|
||||
// copying what webview sends
|
||||
// TODO: review
|
||||
const userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.146 Electron/6.1.7 Safari/537.36 Mattermost/${app.getVersion()}`;
|
||||
const READY = 1;
|
||||
const LOADING = 0;
|
||||
const ERROR = -1;
|
||||
|
||||
const ASTERISK_GROUP = 3;
|
||||
const MENTIONS_GROUP = 2;
|
||||
|
||||
export class MattermostView extends EventEmitter {
|
||||
constructor(server, win, options) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.window = win;
|
||||
|
||||
const preload = getLocalPreload('preload.js');
|
||||
const spellcheck = ((!options || typeof options.spellcheck === 'undefined') ? true : options.spellcheck);
|
||||
this.options = {
|
||||
webPreferences: {
|
||||
contextIsolation: process.env.NODE_ENV !== 'test',
|
||||
preload,
|
||||
spellcheck,
|
||||
additionalArguments: [
|
||||
`version=${app.version}`,
|
||||
`appName=${app.name}`,
|
||||
],
|
||||
enableRemoteModule: true,
|
||||
nodeIntegration: process.env.NODE_ENV === 'test',
|
||||
},
|
||||
...options,
|
||||
};
|
||||
this.isVisible = false;
|
||||
this.view = new BrowserView(this.options);
|
||||
this.resetLoadingStatus();
|
||||
|
||||
/**
|
||||
* for backward compatibility when reading the title.
|
||||
* null means we have yet to figure out if it uses it or not but we consider it false until proven wrong
|
||||
*/
|
||||
this.usesAsteriskForUnreads = null;
|
||||
|
||||
this.faviconMemoize = new Map();
|
||||
this.currentFavicon = null;
|
||||
log.info(`BrowserView created for server ${this.server.name}`);
|
||||
|
||||
this.isInitialized = false;
|
||||
this.hasBeenShown = false;
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
this.altLastPressed = false;
|
||||
this.view.webContents.on('before-input-event', this.handleInputEvents);
|
||||
}
|
||||
}
|
||||
|
||||
// use the same name as the server
|
||||
// TODO: we'll need unique identifiers if we have multiple instances of the same server in different tabs (1:N relationships)
|
||||
get name() {
|
||||
return this.server.name;
|
||||
}
|
||||
|
||||
resetLoadingStatus = () => {
|
||||
if (this.status !== LOADING) { // if it's already loading, don't touch anything
|
||||
this.retryLoad = null;
|
||||
this.status = LOADING;
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
}
|
||||
}
|
||||
|
||||
load = (someURL) => {
|
||||
const loadURL = (typeof someURL === 'undefined') ? `${this.server.url.toString()}` : urlUtils.parseURL(someURL).toString();
|
||||
log.info(`[${this.server.name}] Loading ${loadURL}`);
|
||||
const loading = this.view.webContents.loadURL(loadURL, {userAgent});
|
||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||
this.loadRetry(loadURL, err);
|
||||
});
|
||||
}
|
||||
|
||||
retry = (loadURL) => {
|
||||
return () => {
|
||||
// window was closed while retrying
|
||||
if (!this.view) {
|
||||
return;
|
||||
}
|
||||
const loading = this.view.webContents.loadURL(loadURL, {userAgent});
|
||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||
if (this.maxRetries-- > 0) {
|
||||
this.loadRetry(loadURL, err);
|
||||
} else {
|
||||
WindowManager.sendToRenderer(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
|
||||
this.emit(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
|
||||
log.info(`[${this.server.name}] Couldn't stablish a connection with ${loadURL}: ${err}.`);
|
||||
this.status = ERROR;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
loadRetry = (loadURL, err) => {
|
||||
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
|
||||
WindowManager.sendToRenderer(LOAD_RETRY, this.server.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
|
||||
log.info(`[${this.server.name}] failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
|
||||
}
|
||||
|
||||
loadSuccess = (loadURL) => {
|
||||
return () => {
|
||||
log.info(`[${this.server.name}] finished loading ${loadURL}`);
|
||||
WindowManager.sendToRenderer(LOAD_SUCCESS, this.server.name);
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
if (this.status === LOADING) {
|
||||
ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread);
|
||||
this.handleTitleUpdate(null, this.view.webContents.getTitle());
|
||||
this.findUnreadState(null);
|
||||
}
|
||||
this.status = READY;
|
||||
this.emit(LOAD_SUCCESS, this.server.name, loadURL.toString());
|
||||
this.view.webContents.send(SET_SERVER_NAME, this.server.name);
|
||||
};
|
||||
}
|
||||
|
||||
show = (requestedVisibility) => {
|
||||
this.hasBeenShown = true;
|
||||
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
|
||||
if (request && !this.isVisible) {
|
||||
this.window.addBrowserView(this.view);
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url, this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url, this.view.webContents.getURL()))));
|
||||
if (this.status === READY) {
|
||||
this.focus();
|
||||
}
|
||||
} else if (!request && this.isVisible) {
|
||||
this.window.removeBrowserView(this.view);
|
||||
}
|
||||
this.isVisible = request;
|
||||
}
|
||||
|
||||
reload = () => {
|
||||
this.resetLoadingStatus();
|
||||
this.load();
|
||||
}
|
||||
|
||||
hide = () => this.show(false);
|
||||
|
||||
setBounds = (boundaries) => {
|
||||
// todo: review this, as it might not work properly with devtools/minimizing/resizing
|
||||
this.view.setBounds(boundaries);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
if (this.retryLoad) {
|
||||
clearTimeout(this.retryLoad);
|
||||
}
|
||||
if (this.window) {
|
||||
this.window.removeBrowserView(this.view);
|
||||
}
|
||||
this.window = null;
|
||||
this.server = null;
|
||||
this.isVisible = false;
|
||||
clearTimeout(this.retryLoad);
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
if (this.view.webContents) {
|
||||
this.view.webContents.focus();
|
||||
} else {
|
||||
log.warn('trying to focus the browserview, but it doesn\'t yet have webcontents.');
|
||||
}
|
||||
}
|
||||
|
||||
isReady = () => {
|
||||
return this.status === READY;
|
||||
}
|
||||
|
||||
needsLoadingScreen = () => {
|
||||
return !(this.isInitialized && this.hasBeenShown);
|
||||
}
|
||||
|
||||
setInitialized = () => {
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
openDevTools = () => {
|
||||
this.view.webContents.openDevTools({mode: 'detach'});
|
||||
}
|
||||
|
||||
getWebContents = () => {
|
||||
if (this.status === READY) {
|
||||
return this.view.webContents;
|
||||
} else if (this.window) {
|
||||
return this.window.webContents; // if it's not ready you are looking at the renderer process
|
||||
}
|
||||
return WindowManager.getMainWindow.webContents;
|
||||
}
|
||||
|
||||
handleInputEvents = (_, input) => {
|
||||
// Handler for pressing the Alt key to focus the 3-dot menu
|
||||
if (input.key === 'Alt' && input.type === 'keyUp' && this.altLastPressed) {
|
||||
this.altLastPressed = false;
|
||||
WindowManager.focusThreeDotMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hack to detect keyPress so that alt+<key> combinations don't default back to the 3-dot menu
|
||||
if (input.key === 'Alt' && input.type === 'keyDown') {
|
||||
this.altLastPressed = true;
|
||||
} else {
|
||||
this.altLastPressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleDidNavigate = (event, url) => {
|
||||
const isUrlTeamUrl = urlUtils.isTeamUrl(this.server.url, url) || urlUtils.isAdminUrl(this.server.url, url);
|
||||
if (isUrlTeamUrl) {
|
||||
this.setBounds(getWindowBoundaries(this.window));
|
||||
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
|
||||
log.info('hide back button');
|
||||
} else {
|
||||
this.setBounds(getWindowBoundaries(this.window, true));
|
||||
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true);
|
||||
log.info('show back button');
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateTarget = (e, url) => {
|
||||
if (!this.server.sameOrigin(url)) {
|
||||
this.emit(UPDATE_TARGET_URL, url);
|
||||
}
|
||||
}
|
||||
|
||||
handleFoundInPage = (event, result) => WindowManager.foundInPage(result)
|
||||
|
||||
titleParser = /(\((\d+)\) )?(\*)?/g
|
||||
|
||||
handleTitleUpdate = (e, title) => {
|
||||
//const title = this.view.webContents.getTitle();
|
||||
const resultsIterator = title.matchAll(this.titleParser);
|
||||
const results = resultsIterator.next(); // we are only interested in the first set
|
||||
|
||||
// if not using asterisk (version > v5.28), it'll be marked as undefined and wont be used to check if there are unread channels
|
||||
const hasAsterisk = results && results.value && results.value[ASTERISK_GROUP];
|
||||
if (typeof hasAsterisk !== 'undefined') {
|
||||
this.usesAsteriskForUnreads = true;
|
||||
}
|
||||
let unreads;
|
||||
if (this.usesAsteriskForUnreads) {
|
||||
unreads = Boolean(hasAsterisk);
|
||||
}
|
||||
const mentions = (results && results.value && parseInt(results.value[MENTIONS_GROUP], 10)) || 0;
|
||||
|
||||
appState.updateMentions(this.server.name, mentions, unreads);
|
||||
}
|
||||
|
||||
handleFaviconUpdate = (e, favicons) => {
|
||||
if (!this.usesAsteriskForUnreads) {
|
||||
// if unread state is stored for that favicon, retrieve value.
|
||||
// if not, get related info from preload and store it for future changes
|
||||
this.currentFavicon = favicons[0];
|
||||
if (this.faviconMemoize.has(favicons[0])) {
|
||||
appState.updateUnreads(this.server.name, this.faviconMemoize.get(favicons[0]));
|
||||
} else {
|
||||
this.findUnreadState(favicons[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if favicon is null, it will affect appState, but won't be memoized
|
||||
findUnreadState = (favicon) => {
|
||||
try {
|
||||
this.view.webContents.send(IS_UNREAD, favicon, this.server.name);
|
||||
} catch (err) {
|
||||
log.error(`There was an error trying to request the unread state: ${err}`);
|
||||
log.error(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// if favicon is null, it means it is the initial load,
|
||||
// so don't memoize as we don't have the favicons and there is no rush to find out.
|
||||
handleFaviconIsUnread = (e, favicon, serverName, result) => {
|
||||
if (this.server && serverName === this.server.name) {
|
||||
if (favicon) {
|
||||
this.faviconMemoize.set(favicon, result);
|
||||
}
|
||||
if (favicon === null || favicon === this.currentFavicon) {
|
||||
appState.updateUnreads(serverName, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
109
src/main/views/modalManager.js
Normal file
109
src/main/views/modalManager.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ipcMain} from 'electron';
|
||||
|
||||
import {RETRIEVE_MODAL_INFO, MODAL_CANCEL, MODAL_RESULT, MODAL_OPEN, MODAL_CLOSE} from 'common/communication.js';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
import {ModalView} from './modalView';
|
||||
|
||||
let modalQueue = [];
|
||||
|
||||
// TODO: add a queue/add differentiation, in case we need to put a modal first in line
|
||||
// should we return the original promise if called multiple times with the same key?
|
||||
export function addModal(key, html, preload, data, win) {
|
||||
const foundModal = modalQueue.find((modal) => modal.key === key);
|
||||
if (!foundModal) {
|
||||
const modalPromise = new Promise((resolve, reject) => {
|
||||
const mv = new ModalView(key, html, preload, data, resolve, reject, win);
|
||||
modalQueue.push(mv);
|
||||
});
|
||||
|
||||
if (modalQueue.length === 1) {
|
||||
showModal();
|
||||
}
|
||||
|
||||
return modalPromise;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ipcMain.handle(RETRIEVE_MODAL_INFO, handleInfoRequest);
|
||||
ipcMain.on(MODAL_RESULT, handleModalResult);
|
||||
ipcMain.on(MODAL_CANCEL, handleModalCancel);
|
||||
|
||||
function findModalByCaller(event) {
|
||||
if (modalQueue.length) {
|
||||
const requestModal = modalQueue.find((modal) => {
|
||||
return (modal.view && modal.view.webContents && modal.view.webContents.id === event.sender.id);
|
||||
});
|
||||
return requestModal;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleInfoRequest(event) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
return requestModal.handleInfoRequest();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function showModal() {
|
||||
let noWindow;
|
||||
const withDevTools = process.env.MM_DEBUG_MODALS || false;
|
||||
modalQueue.forEach((modal, index) => {
|
||||
if (index === 0) {
|
||||
WindowManager.sendToRenderer(MODAL_OPEN);
|
||||
modal.show(noWindow, withDevTools);
|
||||
} else {
|
||||
WindowManager.sendToRenderer(MODAL_CLOSE);
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleModalResult(event, data) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
requestModal.resolve(data);
|
||||
}
|
||||
filterActive();
|
||||
if (modalQueue.length) {
|
||||
showModal();
|
||||
} else {
|
||||
WindowManager.sendToRenderer(MODAL_CLOSE);
|
||||
WindowManager.focusBrowserView();
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalCancel(event, data) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
requestModal.reject(data);
|
||||
}
|
||||
filterActive();
|
||||
if (modalQueue.length) {
|
||||
showModal();
|
||||
} else {
|
||||
WindowManager.sendToRenderer(MODAL_CLOSE);
|
||||
WindowManager.focusBrowserView();
|
||||
}
|
||||
}
|
||||
|
||||
function filterActive() {
|
||||
modalQueue = modalQueue.filter((modal) => modal.isActive());
|
||||
}
|
||||
|
||||
export function isModalDisplayed() {
|
||||
return modalQueue.some((modal) => modal.isActive());
|
||||
}
|
||||
|
||||
export function focusCurrentModal() {
|
||||
if (isModalDisplayed()) {
|
||||
modalQueue[0].view.webContents.focus();
|
||||
}
|
||||
}
|
101
src/main/views/modalView.js
Normal file
101
src/main/views/modalView.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserView} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {getWindowBoundaries} from '../utils';
|
||||
|
||||
const ACTIVE = 'active';
|
||||
const SHOWING = 'showing';
|
||||
const DONE = 'done';
|
||||
|
||||
export class ModalView {
|
||||
constructor(key, html, preload, data, onResolve, onReject, currentWindow) {
|
||||
this.key = key;
|
||||
this.html = html;
|
||||
this.data = data;
|
||||
log.info(`preloading with ${preload}`);
|
||||
this.view = new BrowserView({webPreferences: {
|
||||
contextIsolation: process.env.NODE_ENV !== 'test',
|
||||
preload,
|
||||
nodeIntegration: process.env.NODE_ENV === 'test',
|
||||
enableRemoteModule: process.env.NODE_ENV === 'test',
|
||||
}});
|
||||
this.onReject = onReject;
|
||||
this.onResolve = onResolve;
|
||||
this.window = currentWindow;
|
||||
this.windowAttached = null;
|
||||
this.status = ACTIVE;
|
||||
try {
|
||||
this.view.webContents.loadURL(this.html);
|
||||
} catch (e) {
|
||||
log.error('there was an error loading the modal:');
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
show = (win, withDevTools) => {
|
||||
if (this.windowAttached) {
|
||||
// we'll reatach
|
||||
this.windowAttached.removeBrowserView(this.view);
|
||||
}
|
||||
this.windowAttached = win || this.window;
|
||||
|
||||
this.windowAttached.addBrowserView(this.view);
|
||||
this.view.setBounds(getWindowBoundaries(this.windowAttached));
|
||||
this.view.setAutoResize({
|
||||
height: true,
|
||||
width: true,
|
||||
horizontal: true,
|
||||
vertical: true,
|
||||
});
|
||||
this.status = SHOWING;
|
||||
if (this.view.webContents.isLoading()) {
|
||||
this.view.webContents.once('did-finish-load', () => {
|
||||
this.view.webContents.focus();
|
||||
});
|
||||
} else {
|
||||
this.view.webContents.focus();
|
||||
}
|
||||
|
||||
if (withDevTools) {
|
||||
log.info(`showing dev tools for ${this.key}`);
|
||||
this.view.webContents.openDevTools({mode: 'detach'});
|
||||
}
|
||||
}
|
||||
|
||||
hide = () => {
|
||||
if (this.windowAttached) {
|
||||
if (this.view.webContents.isDevToolsOpened()) {
|
||||
this.view.webContents.closeDevTools();
|
||||
}
|
||||
|
||||
this.windowAttached.removeBrowserView(this.view);
|
||||
this.windowAttached = null;
|
||||
this.status = ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
handleInfoRequest = () => {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
reject = (data) => {
|
||||
if (this.onReject) {
|
||||
this.onReject(data);
|
||||
}
|
||||
this.hide();
|
||||
this.status = DONE;
|
||||
}
|
||||
|
||||
resolve = (data) => {
|
||||
if (this.onResolve) {
|
||||
this.onResolve(data);
|
||||
}
|
||||
this.hide();
|
||||
this.status = DONE;
|
||||
}
|
||||
|
||||
isActive = () => this.status !== DONE;
|
||||
}
|
367
src/main/views/viewManager.js
Normal file
367
src/main/views/viewManager.js
Normal file
@@ -0,0 +1,367 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import log from 'electron-log';
|
||||
import {BrowserView, dialog} from 'electron';
|
||||
|
||||
import {SECOND} from 'common/utils/constants';
|
||||
import {UPDATE_TARGET_URL, FOUND_IN_PAGE, SET_SERVER_KEY, LOAD_SUCCESS, LOAD_FAILED, TOGGLE_LOADING_SCREEN_VISIBILITY, GET_LOADING_SCREEN_DATA} from 'common/communication';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import contextMenu from '../contextMenu';
|
||||
import {MattermostServer} from '../MattermostServer';
|
||||
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
|
||||
|
||||
import {MattermostView} from './MattermostView';
|
||||
import {showModal, isModalDisplayed, focusCurrentModal} from './modalManager';
|
||||
import {addWebContentsEventListeners} from './webContentEvents';
|
||||
|
||||
const URL_VIEW_DURATION = 10 * SECOND;
|
||||
const URL_VIEW_HEIGHT = 36;
|
||||
const FINDER_WIDTH = 310;
|
||||
const FINDER_HEIGHT = 40;
|
||||
|
||||
export class ViewManager {
|
||||
constructor(config, mainWindow) {
|
||||
this.configServers = config.teams;
|
||||
this.viewOptions = {spellcheck: config.useSpellChecker};
|
||||
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
|
||||
this.currentView = null;
|
||||
this.urlView = null;
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
updateMainWindow = (mainWindow) => {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
getServers = () => {
|
||||
return this.configServers;
|
||||
}
|
||||
|
||||
loadServer = (server) => {
|
||||
const srv = new MattermostServer(server.name, server.url);
|
||||
const view = new MattermostView(srv, this.mainWindow, this.viewOptions);
|
||||
this.views.set(server.name, view);
|
||||
if (!this.loadingScreen) {
|
||||
this.createLoadingScreen();
|
||||
}
|
||||
view.once(LOAD_SUCCESS, this.activateView);
|
||||
view.load();
|
||||
view.on(UPDATE_TARGET_URL, this.showURLView);
|
||||
}
|
||||
|
||||
load = () => {
|
||||
this.configServers.forEach((server) => this.loadServer(server));
|
||||
}
|
||||
|
||||
reloadConfiguration = (configServers) => {
|
||||
this.configServers = configServers.concat();
|
||||
const oldviews = this.views;
|
||||
this.views = new Map();
|
||||
const sorted = this.configServers.sort((a, b) => a.order - b.order);
|
||||
let setFocus;
|
||||
sorted.forEach((server) => {
|
||||
const recycle = oldviews.get(server.name);
|
||||
if (recycle && recycle.isVisible) {
|
||||
setFocus = recycle.name;
|
||||
}
|
||||
if (recycle && recycle.server.url.toString() === urlUtils.parseURL(server.url).toString()) {
|
||||
oldviews.delete(recycle.name);
|
||||
this.views.set(recycle.name, recycle);
|
||||
} else {
|
||||
this.loadServer(server, this.mainWindow);
|
||||
}
|
||||
});
|
||||
oldviews.forEach((unused) => {
|
||||
unused.destroy();
|
||||
});
|
||||
if (setFocus) {
|
||||
this.showByName(setFocus);
|
||||
} else {
|
||||
this.showInitial();
|
||||
}
|
||||
}
|
||||
|
||||
showInitial = () => {
|
||||
if (this.configServers.length) {
|
||||
const element = this.configServers.find((e) => e.order === 0);
|
||||
if (element) {
|
||||
this.showByName(element.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showByName = (name) => {
|
||||
const newView = this.views.get(name);
|
||||
if (newView.isVisible) {
|
||||
return;
|
||||
}
|
||||
if (newView) {
|
||||
if (this.currentView && this.currentView !== name) {
|
||||
const previous = this.getCurrentView();
|
||||
if (previous) {
|
||||
previous.hide();
|
||||
}
|
||||
}
|
||||
|
||||
this.currentView = name;
|
||||
if (newView.needsLoadingScreen()) {
|
||||
this.showLoadingScreen();
|
||||
}
|
||||
const serverInfo = this.configServers.find((candidate) => candidate.name === newView.server.name);
|
||||
newView.window.webContents.send(SET_SERVER_KEY, serverInfo.order);
|
||||
if (newView.isReady()) {
|
||||
// if view is not ready, the renderer will have something to display instead.
|
||||
newView.show();
|
||||
if (newView.needsLoadingScreen()) {
|
||||
this.showLoadingScreen();
|
||||
} else {
|
||||
this.fadeLoadingScreen();
|
||||
}
|
||||
contextMenu.reload(newView.getWebContents());
|
||||
} else {
|
||||
log.warn(`couldn't show ${name}, not ready`);
|
||||
}
|
||||
} else {
|
||||
log.warn(`Couldn't find a view with name: ${name}`);
|
||||
}
|
||||
showModal();
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
if (isModalDisplayed()) {
|
||||
focusCurrentModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.getCurrentView();
|
||||
if (view) {
|
||||
view.focus();
|
||||
}
|
||||
}
|
||||
activateView = (viewName) => {
|
||||
if (this.currentView === viewName) {
|
||||
this.showByName(this.currentView);
|
||||
}
|
||||
const view = this.views.get(viewName);
|
||||
addWebContentsEventListeners(view, this.getServers);
|
||||
}
|
||||
|
||||
getCurrentView() {
|
||||
return this.views.get(this.currentView);
|
||||
}
|
||||
|
||||
openViewDevTools = () => {
|
||||
const view = this.getCurrentView();
|
||||
if (view) {
|
||||
view.openDevTools({mode: 'detach'});
|
||||
} else {
|
||||
log.error(`couldn't find ${this.currentView}`);
|
||||
}
|
||||
}
|
||||
|
||||
findByWebContent(webContentId) {
|
||||
let found = null;
|
||||
let serverName;
|
||||
let view;
|
||||
const entries = this.views.entries();
|
||||
|
||||
for ([serverName, view] of entries) {
|
||||
if (typeof serverName !== 'undefined') {
|
||||
const wc = view.getWebContents();
|
||||
if (wc && wc.id === webContentId) {
|
||||
found = serverName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
showURLView = (url) => {
|
||||
if (this.urlViewCancel) {
|
||||
this.urlViewCancel();
|
||||
}
|
||||
if (url && url !== '') {
|
||||
const urlString = typeof url === 'string' ? url : url.toString();
|
||||
const urlView = new BrowserView({
|
||||
webPreferences: {
|
||||
contextIsolation: process.env.NODE_ENV !== 'test',
|
||||
nodeIntegration: process.env.NODE_ENV === 'test',
|
||||
enableRemoteModule: process.env.NODE_ENV === 'test',
|
||||
}});
|
||||
const query = new Map([['url', urlString]]);
|
||||
const localURL = getLocalURLString('urlView.html', query);
|
||||
urlView.webContents.loadURL(localURL);
|
||||
const currentWindow = this.getCurrentView().window;
|
||||
currentWindow.addBrowserView(urlView);
|
||||
const boundaries = currentWindow.getBounds();
|
||||
urlView.setBounds({
|
||||
x: 0,
|
||||
y: boundaries.height - URL_VIEW_HEIGHT,
|
||||
width: Math.floor(boundaries.width / 3),
|
||||
height: URL_VIEW_HEIGHT,
|
||||
});
|
||||
|
||||
const hideView = () => {
|
||||
this.urlViewCancel = null;
|
||||
currentWindow.removeBrowserView(urlView);
|
||||
};
|
||||
|
||||
const timeout = setTimeout(hideView,
|
||||
URL_VIEW_DURATION);
|
||||
|
||||
this.urlViewCancel = () => {
|
||||
clearTimeout(timeout);
|
||||
hideView();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setFinderBounds = () => {
|
||||
if (this.finder) {
|
||||
const boundaries = this.mainWindow.getBounds();
|
||||
this.finder.setBounds({
|
||||
x: boundaries.width - FINDER_WIDTH - (process.platform === 'darwin' ? 20 : 200),
|
||||
y: 0,
|
||||
width: FINDER_WIDTH,
|
||||
height: FINDER_HEIGHT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
focusFinder = () => {
|
||||
if (this.finder) {
|
||||
this.finder.webContents.focus();
|
||||
}
|
||||
}
|
||||
|
||||
hideFinder = () => {
|
||||
if (this.finder) {
|
||||
this.mainWindow.removeBrowserView(this.finder);
|
||||
this.finder = null;
|
||||
}
|
||||
}
|
||||
|
||||
foundInPage = (result) => {
|
||||
if (this.finder) {
|
||||
this.finder.webContents.send(FOUND_IN_PAGE, result);
|
||||
}
|
||||
};
|
||||
|
||||
showFinder = () => {
|
||||
// just focus the current finder if it's already open
|
||||
if (this.finder) {
|
||||
this.finder.webContents.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const preload = getLocalPreload('finderPreload.js');
|
||||
this.finder = new BrowserView({webPreferences: {
|
||||
contextIsolation: process.env.NODE_ENV !== 'test',
|
||||
preload,
|
||||
nodeIntegration: process.env.NODE_ENV === 'test',
|
||||
enableRemoteModule: process.env.NODE_ENV === 'test', // TODO: try to use this only on testing
|
||||
}});
|
||||
const localURL = getLocalURLString('finder.html');
|
||||
this.finder.webContents.loadURL(localURL);
|
||||
this.mainWindow.addBrowserView(this.finder);
|
||||
this.setFinderBounds();
|
||||
|
||||
this.finder.webContents.focus();
|
||||
};
|
||||
|
||||
setLoadingScreenBounds = () => {
|
||||
if (this.loadingScreen) {
|
||||
this.loadingScreen.setBounds(getWindowBoundaries(this.mainWindow));
|
||||
}
|
||||
}
|
||||
|
||||
createLoadingScreen = () => {
|
||||
const preload = getLocalPreload('loadingScreenPreload.js');
|
||||
this.loadingScreen = new BrowserView({webPreferences: {
|
||||
contextIsolation: true,
|
||||
preload,
|
||||
}});
|
||||
const localURL = getLocalURLString('loadingScreen.html');
|
||||
this.loadingScreen.webContents.loadURL(localURL);
|
||||
}
|
||||
|
||||
showLoadingScreen = () => {
|
||||
if (!this.loadingScreen) {
|
||||
this.createLoadingScreen();
|
||||
}
|
||||
|
||||
this.loadingScreen.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
|
||||
|
||||
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen)) {
|
||||
this.mainWindow.setTopBrowserView(this.loadingScreen);
|
||||
} else {
|
||||
this.mainWindow.addBrowserView(this.loadingScreen);
|
||||
}
|
||||
|
||||
this.setLoadingScreenBounds();
|
||||
}
|
||||
|
||||
fadeLoadingScreen = () => {
|
||||
if (this.loadingScreen) {
|
||||
this.loadingScreen.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, false);
|
||||
}
|
||||
}
|
||||
|
||||
hideLoadingScreen = () => {
|
||||
if (this.loadingScreen) {
|
||||
this.mainWindow.removeBrowserView(this.loadingScreen);
|
||||
}
|
||||
}
|
||||
|
||||
setServerInitialized = (server) => {
|
||||
const view = this.views.get(server);
|
||||
if (view) {
|
||||
view.setInitialized();
|
||||
if (this.getCurrentView() === view) {
|
||||
this.fadeLoadingScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingScreenDarkMode = (darkMode) => {
|
||||
if (this.loadingScreen) {
|
||||
this.loadingScreen.webContents.send(GET_LOADING_SCREEN_DATA, {darkMode});
|
||||
}
|
||||
}
|
||||
|
||||
deeplinkSuccess = (serverName) => {
|
||||
const view = this.views.get(serverName);
|
||||
this.showByName(serverName);
|
||||
view.removeListener(LOAD_FAILED, this.deeplinkFailed);
|
||||
};
|
||||
|
||||
deeplinkFailed = (serverName, err, url) => {
|
||||
const view = this.views.get(serverName);
|
||||
log.error(`[${serverName}] failed to load deeplink ${url}: ${err}`);
|
||||
view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess);
|
||||
}
|
||||
|
||||
handleDeepLink = (url) => {
|
||||
if (url) {
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const server = urlUtils.getServer(parsedURL, this.configServers, true);
|
||||
if (server) {
|
||||
const view = this.views.get(server.name);
|
||||
|
||||
// attempting to change parsedURL protocol results in it not being modified.
|
||||
const urlWithSchema = `${view.server.url.origin}${parsedURL.pathname}${parsedURL.search}`;
|
||||
view.resetLoadingStatus();
|
||||
view.load(urlWithSchema);
|
||||
view.once(LOAD_SUCCESS, this.deeplinkSuccess);
|
||||
view.once(LOAD_FAILED, this.deeplinkFailed);
|
||||
} else {
|
||||
dialog.showErrorBox('No matching server', `there is no configured server in the app that matches the requested url: ${parsedURL.toString()}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sendToAllViews = (channel, ...args) => {
|
||||
this.views.forEach((view) => view.view.webContents.send(channel, ...args));
|
||||
}
|
||||
}
|
250
src/main/views/webContentEvents.js
Normal file
250
src/main/views/webContentEvents.js
Normal file
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserWindow, shell} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {DEVELOPMENT, PRODUCTION} from 'common/utils/constants';
|
||||
import urlUtils from 'common/utils/url';
|
||||
import Utils from 'common/utils/util';
|
||||
import {FOUND_IN_PAGE} from 'common/communication';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
import {protocols} from '../../../electron-builder.json';
|
||||
|
||||
import allowProtocolDialog from '../allowProtocolDialog';
|
||||
|
||||
const customLogins = {};
|
||||
const listeners = {};
|
||||
let popupWindow = null;
|
||||
|
||||
function isTrustedPopupWindow(webContents) {
|
||||
if (!webContents) {
|
||||
return false;
|
||||
}
|
||||
if (!popupWindow) {
|
||||
return false;
|
||||
}
|
||||
return Utils.browserWindowFromWebContents(webContents) === popupWindow;
|
||||
}
|
||||
|
||||
const nixUA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36';
|
||||
|
||||
const popupUserAgent = {
|
||||
darwin: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36',
|
||||
win32: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36',
|
||||
aix: nixUA,
|
||||
freebsd: nixUA,
|
||||
linux: nixUA,
|
||||
openbsd: nixUA,
|
||||
sunos: nixUA,
|
||||
};
|
||||
|
||||
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
|
||||
|
||||
const generateWillNavigate = (getServersFunction) => {
|
||||
return (event, url) => {
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const configServers = getServersFunction();
|
||||
const server = urlUtils.getServer(parsedURL, configServers);
|
||||
|
||||
if (server && (urlUtils.isTeamUrl(server.url, parsedURL) || urlUtils.isAdminUrl(server.url, parsedURL) || isTrustedPopupWindow(event.sender))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
|
||||
return;
|
||||
}
|
||||
if (parsedURL.protocol === 'mailto:') {
|
||||
return;
|
||||
}
|
||||
if (customLogins[contentID].inProgress) {
|
||||
return;
|
||||
}
|
||||
const mode = Utils.runMode();
|
||||
if (((mode === DEVELOPMENT || mode === PRODUCTION) &&
|
||||
(parsedURL.path === 'renderer/index.html' || parsedURL.path === 'renderer/settings.html'))) {
|
||||
log.info('loading settings page');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Prevented desktop from navigating to: ${url}`);
|
||||
event.preventDefault();
|
||||
};
|
||||
};
|
||||
|
||||
const generateDidStartNavigation = (getServersFunction) => {
|
||||
return (event, url) => {
|
||||
const serverList = getServersFunction();
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const server = urlUtils.getServer(parsedURL, serverList);
|
||||
|
||||
if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
||||
customLogins[contentID].inProgress = true;
|
||||
} else if (customLogins[contentID].inProgress) {
|
||||
customLogins[contentID].inProgress = false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
||||
return (event, url) => {
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const configServers = getServersFunction();
|
||||
|
||||
// Dev tools case
|
||||
if (parsedURL.protocol === 'devtools:') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
// Check for valid URL
|
||||
if (!urlUtils.isValidURI(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for custom protocol
|
||||
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:' && parsedURL.protocol !== `${scheme}:`) {
|
||||
allowProtocolDialog.handleDialogEvent(parsedURL.protocol, url);
|
||||
return;
|
||||
}
|
||||
|
||||
const server = urlUtils.getServer(parsedURL, configServers);
|
||||
|
||||
if (!server) {
|
||||
shell.openExternal(url);
|
||||
return;
|
||||
}
|
||||
|
||||
// Public download links case
|
||||
// TODO: We might be handling different types differently in the future, for now
|
||||
// we are going to mimic the browser and just pop a new browser window for public links
|
||||
if (parsedURL.pathname.match(/^(\/api\/v[3-4]\/public)*\/files\//)) {
|
||||
shell.openExternal(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedURL.pathname.match(/^\/help\//)) {
|
||||
// Help links case
|
||||
// continue to open special case internal urls in default browser
|
||||
shell.openExternal(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlUtils.isTeamUrl(server.url, parsedURL, true)) {
|
||||
WindowManager.showMainWindow(parsedURL);
|
||||
return;
|
||||
}
|
||||
if (urlUtils.isAdminUrl(server.url, parsedURL)) {
|
||||
log.info(`${url} is an admin console page, preventing to open a new window`);
|
||||
return;
|
||||
}
|
||||
if (popupWindow && !popupWindow.closed && popupWindow.getURL() === url) {
|
||||
log.info(`Popup window already open at provided url: ${url}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: move popups to its own and have more than one.
|
||||
if (urlUtils.isPluginUrl(server.url, parsedURL) || urlUtils.isManagedResource(server.url, parsedURL)) {
|
||||
if (!popupWindow || popupWindow.closed) {
|
||||
popupWindow = new BrowserWindow({
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
//parent: WindowManager.getMainWindow(),
|
||||
show: false,
|
||||
center: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: process.env.NODE_ENV === 'test',
|
||||
contextIsolation: process.env.NODE_ENV !== 'test',
|
||||
spellcheck: (typeof spellcheck === 'undefined' ? true : spellcheck),
|
||||
enableRemoteModule: process.env.NODE_ENV === 'test',
|
||||
},
|
||||
});
|
||||
popupWindow.once('ready-to-show', () => {
|
||||
popupWindow.show();
|
||||
});
|
||||
popupWindow.once('closed', () => {
|
||||
popupWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
if (urlUtils.isManagedResource(server.url, parsedURL)) {
|
||||
popupWindow.loadURL(url);
|
||||
} else {
|
||||
// currently changing the userAgent for popup windows to allow plugins to go through google's oAuth
|
||||
// should be removed once a proper oAuth2 implementation is setup.
|
||||
popupWindow.loadURL(url, {
|
||||
userAgent: popupUserAgent[process.platform],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const removeWebContentsListeners = (id) => {
|
||||
if (listeners[id]) {
|
||||
listeners[id]();
|
||||
}
|
||||
};
|
||||
|
||||
export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
||||
const contents = mmview.view.webContents;
|
||||
|
||||
// initialize custom login tracking
|
||||
customLogins[contents.id] = {
|
||||
inProgress: false,
|
||||
};
|
||||
|
||||
if (listeners[contents.id]) {
|
||||
removeWebContentsListeners(contents.id);
|
||||
}
|
||||
const willNavigate = generateWillNavigate(getServersFunction);
|
||||
contents.on('will-navigate', willNavigate);
|
||||
|
||||
// handle custom login requests (oath, saml):
|
||||
// 1. are we navigating to a supported local custom login path from the `/login` page?
|
||||
// - indicate custom login is in progress
|
||||
// 2. are we finished with the custom login process?
|
||||
// - indicate custom login is NOT in progress
|
||||
const didStartNavigation = generateDidStartNavigation(getServersFunction);
|
||||
contents.on('did-start-navigation', didStartNavigation);
|
||||
|
||||
const spellcheck = mmview.options.webPreferences.spellcheck;
|
||||
const newWindow = generateNewWindowListener(getServersFunction, spellcheck);
|
||||
contents.on('new-window', newWindow);
|
||||
|
||||
contents.on('page-title-updated', mmview.handleTitleUpdate);
|
||||
contents.on('page-favicon-updated', mmview.handleFaviconUpdate);
|
||||
contents.on('update-target-url', mmview.handleUpdateTarget);
|
||||
contents.on(FOUND_IN_PAGE, mmview.handleFoundInPage);
|
||||
contents.on('did-navigate', mmview.handleDidNavigate);
|
||||
|
||||
const removeListeners = () => {
|
||||
try {
|
||||
contents.removeListener('will-navigate', willNavigate);
|
||||
contents.removeListener('did-start-navigation', didStartNavigation);
|
||||
contents.removeListener('new-window', newWindow);
|
||||
contents.removeListener('page-title-updated', mmview.handleTitleUpdate);
|
||||
contents.removeListener('page-favicon-updated', mmview.handleFaviconUpdate);
|
||||
contents.removeListener('update-target-url', mmview.handleUpdateTarget);
|
||||
contents.removeListener(FOUND_IN_PAGE, mmview.handleFoundInPage);
|
||||
contents.removeListener('did-navigate', mmview.handleDidNavigate);
|
||||
} catch (e) {
|
||||
log.error(`Error while trying to detach listeners, this might be ok if the process crashed: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
listeners[contents.id] = removeListeners;
|
||||
contents.once('render-process-gone', (event, details) => {
|
||||
if (details !== 'clean-exit') {
|
||||
log.error(`Renderer process for a webcontent is no longer available: ${details}`);
|
||||
}
|
||||
removeListeners();
|
||||
});
|
||||
};
|
170
src/main/windows/mainWindow.js
Normal file
170
src/main/windows/mainWindow.js
Normal file
@@ -0,0 +1,170 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
import {app, BrowserWindow} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
|
||||
|
||||
import * as Validator from '../Validator';
|
||||
import contextMenu from '../contextMenu';
|
||||
import {getLocalURLString} from '../utils';
|
||||
|
||||
function saveWindowState(file, window) {
|
||||
const windowState = window.getBounds();
|
||||
windowState.maximized = window.isMaximized();
|
||||
try {
|
||||
fs.writeFileSync(file, JSON.stringify(windowState));
|
||||
} catch (e) {
|
||||
// [Linux] error happens only when the window state is changed before the config dir is created.
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function isFramelessWindow() {
|
||||
return os.platform() === 'darwin' || (os.platform() === 'win32' && os.release().startsWith('10'));
|
||||
}
|
||||
|
||||
function createMainWindow(config, options) {
|
||||
const defaultWindowWidth = 1000;
|
||||
const defaultWindowHeight = 700;
|
||||
const minimumWindowWidth = 400;
|
||||
const minimumWindowHeight = 240;
|
||||
|
||||
// Create the browser window.
|
||||
const boundsInfoPath = path.join(app.getPath('userData'), 'bounds-info.json');
|
||||
let windowOptions;
|
||||
try {
|
||||
windowOptions = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
||||
windowOptions = Validator.validateBoundsInfo(windowOptions);
|
||||
if (!windowOptions) {
|
||||
throw new Error('Provided bounds info file does not validate, using defaults instead.');
|
||||
}
|
||||
} catch (e) {
|
||||
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
|
||||
windowOptions = {width: defaultWindowWidth, height: defaultWindowHeight};
|
||||
}
|
||||
|
||||
const {maximized: windowIsMaximized} = windowOptions;
|
||||
|
||||
const spellcheck = (typeof config.useSpellChecker === 'undefined' ? true : config.useSpellChecker);
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
windowOptions.icon = options.linuxAppIcon;
|
||||
}
|
||||
Object.assign(windowOptions, {
|
||||
title: app.name,
|
||||
fullscreenable: true,
|
||||
show: false, // don't start the window until it is ready and only if it isn't hidden
|
||||
paintWhenInitiallyHidden: true, // we want it to start painting to get info from the webapp
|
||||
minWidth: minimumWindowWidth,
|
||||
minHeight: minimumWindowHeight,
|
||||
frame: !isFramelessWindow(),
|
||||
fullscreen: false,
|
||||
titleBarStyle: 'hidden',
|
||||
trafficLightPosition: {x: 12, y: 24},
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
disableBlinkFeatures: 'Auxclick',
|
||||
spellcheck,
|
||||
enableRemoteModule: process.env.NODE_ENV === 'test',
|
||||
},
|
||||
});
|
||||
|
||||
const mainWindow = new BrowserWindow(windowOptions);
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
|
||||
const localURL = getLocalURLString('index.html');
|
||||
mainWindow.loadURL(localURL).catch(
|
||||
(reason) => {
|
||||
log.error(`Main window failed to load: ${reason}`);
|
||||
});
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.webContents.zoomLevel = 0;
|
||||
|
||||
mainWindow.show();
|
||||
if (windowIsMaximized) {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.once('show', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
mainWindow.once('restore', () => {
|
||||
mainWindow.restore();
|
||||
});
|
||||
|
||||
// App should save bounds when a window is closed.
|
||||
// However, 'close' is not fired in some situations(shutdown, ctrl+c)
|
||||
// because main process is killed in such situations.
|
||||
// 'blur' event was effective in order to avoid this.
|
||||
// Ideally, app should detect that OS is shutting down.
|
||||
mainWindow.on('blur', () => {
|
||||
saveWindowState(boundsInfoPath, mainWindow);
|
||||
});
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
if (global.willAppQuit) { // when [Ctrl|Cmd]+Q
|
||||
saveWindowState(boundsInfoPath, mainWindow);
|
||||
} else { // Minimize or hide the window for close button.
|
||||
event.preventDefault();
|
||||
function hideWindow(window) {
|
||||
window.blur(); // To move focus to the next top-level window in Windows
|
||||
window.hide();
|
||||
}
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
hideWindow(mainWindow);
|
||||
break;
|
||||
case 'linux':
|
||||
if (config.minimizeToTray) {
|
||||
hideWindow(mainWindow);
|
||||
} else {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
break;
|
||||
case 'darwin':
|
||||
// need to leave fullscreen first, then hide the window
|
||||
if (mainWindow.isFullScreen()) {
|
||||
mainWindow.once('leave-full-screen', () => {
|
||||
app.hide();
|
||||
});
|
||||
mainWindow.setFullScreen(false);
|
||||
} else {
|
||||
app.hide();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register keyboard shortcuts
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
// Add Alt+Cmd+(Right|Left) as alternative to switch between servers
|
||||
if (process.platform === 'darwin') {
|
||||
if (input.alt && input.meta) {
|
||||
if (input.key === 'ArrowRight') {
|
||||
mainWindow.webContents.send(SELECT_NEXT_TAB);
|
||||
}
|
||||
if (input.key === 'ArrowLeft') {
|
||||
mainWindow.webContents.send(SELECT_PREVIOUS_TAB);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
contextMenu.setup({useSpellChecker: config.useSpellChecker});
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
export default createMainWindow;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user