[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:
Devin Binnie 2021-03-18 10:51:53 -04:00 committed by GitHub
parent 9551d6628c
commit e76e0dc0a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
209 changed files with 17905 additions and 13921 deletions

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -1,116 +1,143 @@
{
"extends": [
"./.eslintrc-webapp.json",
"plugin:eslint-comments/recommended"
],
"parserOptions": {
"ecmaVersion": 2017
},
"settings": {
"import/resolver": "node"
},
"rules": {
"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"
},
"overrides": [
{
"files": [
"webpack.config.renderer.js",
"test/specs/spellchecker_test.js",
"test/specs/app_test.js",
"test/specs/security_test.js",
"test/specs/permisson_test.js",
"test/specs/browser/index_test.js",
"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",
"README.md",
"scripts/watch_main_and_preload.js",
"scripts/extract_dict.js",
"scripts/manipulate_windows_zip.js",
"scripts/check_build_config.js",
"LICENSE.txt",
"src/utils/util.js",
"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",
"src/browser/components/MainPage.jsx",
"src/browser/components/HoveringURL.jsx",
"src/browser/components/AutoSaveIndicator.jsx",
"src/browser/components/MattermostView.jsx",
"src/browser/components/TabBar.jsx",
"src/browser/components/DestructiveConfirmModal.jsx",
"src/browser/components/ErrorView.jsx",
"src/browser/components/UpdaterPage.jsx",
"src/browser/components/PermissionRequestDialog.jsx",
"src/browser/components/Finder.jsx",
"src/browser/components/SettingsPage.jsx",
"src/browser/components/TeamListItem.jsx",
"src/browser/components/UpdaterPage/UpdaterPage.stories.jsx",
"src/browser/components/Button/Button.stories.jsx",
"src/browser/components/TeamList.jsx",
"src/browser/components/LoginModal.jsx",
"src/browser/components/NewTeamModal.jsx",
"src/browser/settings.jsx",
"src/browser/index.jsx",
"src/common/deepmerge.js",
"src/common/config/index.js",
"src/common/config/buildConfig.js",
"src/common/config/pastDefaultPreferences.js",
"src/common/config/upgradePreferences.js",
"src/common/config/RegistryConfig.js",
"src/common/osVersion.js",
"src/common/config/defaultPreferences.js",
"src/common/JsonFileManager.js",
"src/main/certificateStore.js",
"src/main/mainWindow.js",
"src/main/allowProtocolDialog.js",
"src/main/permissionRequestHandler.js",
"src/main/squirrelStartup.js",
"src/main/autoLaunch.js",
"src/main/PermissionManager.js",
"src/main/AutoLauncher.js",
"src/main/AppStateManager.js",
"src/main/menus/tray.js",
"src/main/CriticalErrorHandler.js",
"src/main/cookieManager.js",
"src/main/utils.js",
"src/main/downloadURL.js",
"src/main/autoUpdater.js",
"src/main/SpellChecker.js",
"src/main/menus/app.js"
],
"rules": {
"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."
]]
}
}
]
"extends": [
"./.eslintrc-webapp.json"
],
"parserOptions": {
"ecmaVersion": 2017
},
"settings": {
"import/resolver": {
"webpack": {
"config": "webpack.config.base.js"
}
}
},
"rules": {
"header/header": [
2,
"line",
[
" Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.",
" See LICENSE.txt for license information."
]
],
"import/no-commonjs": 2,
"no-process-env": 0,
"no-var": 2,
"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",
"test/specs/spellchecker_test.js",
"test/specs/app_test.js",
"test/specs/security_test.js",
"test/specs/permisson_test.js",
"test/specs/browser/index_test.js",
"test/specs/browser/settings_test.js",
"test/modules/utils.js",
"test/modules/environment.js",
"CHANGELOG.md",
"webpack.config.base.js",
"babel.config.js",
"README.md",
"scripts/watch_main_and_preload.js",
"scripts/extract_dict.js",
"scripts/manipulate_windows_zip.js",
"scripts/check_build_config.js",
"LICENSE.txt",
"src/utils/util.js",
"src/main.js",
"src/browser/js/contextMenu.js",
"src/browser/updater.jsx",
"src/browser/js/badge.js",
"src/browser/webview/mattermost.js",
"src/browser/components/RemoveServerModal.jsx",
"src/browser/components/MainPage.jsx",
"src/browser/components/HoveringURL.jsx",
"src/browser/components/AutoSaveIndicator.jsx",
"src/browser/components/MattermostView.jsx",
"src/browser/components/TabBar.jsx",
"src/browser/components/DestructiveConfirmModal.jsx",
"src/browser/components/ErrorView.jsx",
"src/browser/components/UpdaterPage.jsx",
"src/browser/components/PermissionRequestDialog.jsx",
"src/browser/components/Finder.jsx",
"src/browser/components/SettingsPage.jsx",
"src/browser/components/TeamListItem.jsx",
"src/browser/components/UpdaterPage/UpdaterPage.stories.jsx",
"src/browser/components/Button/Button.stories.jsx",
"src/browser/components/TeamList.jsx",
"src/browser/components/LoginModal.jsx",
"src/browser/components/NewTeamModal.jsx",
"src/browser/settings.jsx",
"src/browser/index.jsx",
"src/common/deepmerge.js",
"src/common/config/index.js",
"src/common/config/buildConfig.js",
"src/common/config/pastDefaultPreferences.js",
"src/common/config/upgradePreferences.js",
"src/common/config/RegistryConfig.js",
"src/common/osVersion.js",
"src/common/config/defaultPreferences.js",
"src/common/JsonFileManager.js",
"src/main/certificateStore.js",
"src/main/mainWindow.js",
"src/main/allowProtocolDialog.js",
"src/main/permissionRequestHandler.js",
"src/main/squirrelStartup.js",
"src/main/autoLaunch.js",
"src/main/PermissionManager.js",
"src/main/AutoLauncher.js",
"src/main/AppStateManager.js",
"src/main/menus/tray.js",
"src/main/CriticalErrorHandler.js",
"src/main/cookieManager.js",
"src/main/utils.js",
"src/main/downloadURL.js",
"src/main/autoUpdater.js",
"src/main/SpellChecker.js",
"src/main/menus/app.js"
],
"rules": {
"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
View file

@ -14,7 +14,4 @@ test_config.json
.idea
testUserData
src/browser/*.png
src/browser/*.svg
src/browser/*.woff2
src/browser/assets/fonts/
.gitattributes

View file

@ -3,17 +3,17 @@
// See LICENSE.txt for license information.
module.exports = (api) => { // eslint-disable-line import/no-commonjs
api.cache.forever();
return {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['Electron >= 2.0'],
node: '8.9',
},
}],
'@babel/preset-react',
],
plugins: ['@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'],
};
api.cache.forever();
return {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['Electron >= 2.0'],
node: '8.9',
},
}],
'@babel/preset-react',
],
plugins: ['@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'],
};
};

View file

@ -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.

View file

@ -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

View file

@ -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",
@ -123,5 +125,4 @@
"nsis": {
"artifactName": "${name}-setup-${version}-win.${ext}"
}
}
}

7658
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -8,16 +8,16 @@ const {spawn} = require('electron-notarize/lib/spawn.js');
const SETUID_PERMISSIONS = '4755';
exports.default = async function afterPack(context) {
if (context.electronPlatformName === 'linux') {
context.targets.forEach(async (target) => {
if (!['appimage', 'snap'].includes(target.name.toLowerCase())) {
const result = await spawn('chmod', [SETUID_PERMISSIONS, path.join(context.appOutDir, 'chrome-sandbox')]);
if (result.code !== 0) {
throw new Error(
`Failed to set proper permissions for linux arch on ${target.name}`,
);
}
}
});
}
};
if (context.electronPlatformName === 'linux') {
context.targets.forEach(async (target) => {
if (!['appimage', 'snap'].includes(target.name.toLowerCase())) {
const result = await spawn('chmod', [SETUID_PERMISSIONS, path.join(context.appOutDir, 'chrome-sandbox')]);
if (result.code !== 0) {
throw new Error(
`Failed to set proper permissions for linux arch on ${target.name}`,
);
}
}
});
}
};

View file

@ -4,16 +4,16 @@
const buildConfig = require('../src/common/config/buildConfig');
function validateBuildConfig(config) {
if (config.enableServerManagement === false && config.defaultTeams && config.defaultTeams.length === 0) {
return {
result: false,
message: `Specify at least one server for "defaultTeams" in buildConfig.js when "enableServerManagement is set to false.\n${JSON.stringify(config, null, 2)}`,
};
}
return {result: true};
if (config.enableServerManagement === false && config.defaultTeams && config.defaultTeams.length === 0) {
return {
result: false,
message: `Specify at least one server for "defaultTeams" in buildConfig.js when "enableServerManagement is set to false.\n${JSON.stringify(config, null, 2)}`,
};
}
return {result: true};
}
const ret = validateBuildConfig(buildConfig);
if (ret.result === false) {
throw new Error(ret.message);
throw new Error(ret.message);
}

View file

@ -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}/"

View file

@ -12,13 +12,13 @@ const {path7za} = require('7zip-bin');
const cwd = process.argv[2];
spawn(path7za, ['e', '-y', '*.zip'], {
cwd,
stdio: 'inherit',
cwd,
stdio: 'inherit',
}).on('error', (err) => {
console.error(err);
process.exit(1);
console.error(err);
process.exit(1);
}).on('close', (code) => {
process.exit(code);
process.exit(code);
});
/* eslint-enable no-process-exit */

View file

@ -4,27 +4,28 @@
'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;
function disableInstallUpdate(zipPath) {
const zipFullPath = path.resolve(__dirname, '..', zipPath);
const appUpdaterConfigFile = 'app-updater-config.json';
const zipFullPath = path.resolve(__dirname, '..', zipPath);
const appUpdaterConfigFile = 'app-updater-config.json';
const addResult = spawnSync(path7za, ['a', zipFullPath, appUpdaterConfigFile], {cwd: 'resources/windows'});
if (addResult.status !== 0) {
throw new Error(`7za a returned non-zero exit code for ${zipPath}`);
}
const addResult = spawnSync(path7za, ['a', zipFullPath, appUpdaterConfigFile], {cwd: 'resources/windows'});
if (addResult.status !== 0) {
throw new Error(`7za a returned non-zero exit code for ${zipPath}`);
}
const renameResult = spawnSync(path7za, ['rn', zipFullPath, appUpdaterConfigFile, `resources/${appUpdaterConfigFile}`]);
if (renameResult.status !== 0) {
throw new Error(`7za rn returned non-zero exit code for ${zipPath}`);
}
const renameResult = spawnSync(path7za, ['rn', zipFullPath, appUpdaterConfigFile, `resources/${appUpdaterConfigFile}`]);
if (renameResult.status !== 0) {
throw new Error(`7za rn returned non-zero exit code for ${zipPath}`);
}
}
console.log('Manipulating 64-bit zip...');

View file

@ -8,22 +8,22 @@ const {notarize} = require('electron-notarize');
const config = require('../electron-builder.json');
exports.default = async function notarizing(context) {
const {electronPlatformName, appOutDir} = context;
if (electronPlatformName !== 'darwin' || process.platform !== 'darwin') {
return;
}
const {electronPlatformName, appOutDir} = context;
if (electronPlatformName !== 'darwin' || process.platform !== 'darwin') {
return;
}
const appName = context.packager.appInfo.productFilename;
if (typeof process.env.APPLEID === 'undefined') {
console.log('skipping notarization, remember to setup environment variables for APPLEID and APPLEIDPASS if you want to notarize');
return;
}
await notarize({
const appName = context.packager.appInfo.productFilename;
if (typeof process.env.APPLEID === 'undefined') {
console.log('skipping notarization, remember to setup environment variables for APPLEID and APPLEIDPASS if you want to notarize');
return;
}
await notarize({
// should we change it to appBundleId: 'com.mattermost.desktop',
appBundleId: config.appId,
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLEID,
appleIdPassword: process.env.APPLEIDPASS,
});
};
// should we change it to appBundleId: 'com.mattermost.desktop',
appBundleId: config.appId,
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLEID,
appleIdPassword: process.env.APPLEIDPASS,
});
};

View file

@ -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}"
}

View file

@ -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');
@ -11,28 +11,21 @@ let started = false;
const mainCompiler = webpack(mainConfig);
mainCompiler.watch({}, (err, stats) => {
process.stdout.write(stats.toString({colors: true}));
process.stdout.write('\n');
if (!stats.hasErrors()) {
if (started) {
electron.restart();
} else {
electron.start();
started = true;
process.stdout.write(stats.toString({colors: true}));
process.stdout.write('\n');
if (!stats.hasErrors()) {
if (started) {
electron.restart();
} else {
electron.start();
started = true;
}
}
}
});
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) {
console.log(err);
}
if (err) {
console.log(err);
}
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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,
});

View file

@ -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>
));

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,
};

View file

@ -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 */

View file

@ -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 */

View file

@ -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,
};

View file

@ -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 */

View file

@ -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,
};

View file

@ -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

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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;

View file

@ -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')}
/>
));

View file

@ -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,
};

View file

@ -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>
);
}
}

View file

@ -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;

View file

@ -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>

View file

@ -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());

View file

@ -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();
}

View file

@ -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,
});
},
};

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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());

View file

@ -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')
);

View file

@ -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 */

View file

@ -4,34 +4,36 @@
import fs from 'fs';
export default class JsonFileManager {
constructor(file) {
this.jsonFile = file;
try {
this.json = JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch (err) {
this.json = {};
constructor(file) {
this.jsonFile = file;
try {
this.json = JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch (err) {
this.json = {};
}
}
}
writeToFile() {
fs.writeFile(this.jsonFile, JSON.stringify(this.json, null, 2), (err) => {
if (err) {
console.error(err);
}
});
}
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);
}
});
}
setJson(json) {
this.json = json;
this.writeToFile();
}
setJson(json) {
this.json = json;
this.writeToFile();
}
setValue(key, value) {
this.json[key] = value;
this.writeToFile();
}
setValue(key, value) {
this.json[key] = value;
this.writeToFile();
}
getValue(key) {
return this.json[key];
}
getValue(key) {
return this.json[key];
}
}

View 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';

View file

@ -3,135 +3,144 @@
// 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
*/
export default class RegistryConfig extends EventEmitter {
constructor() {
super();
this.initialized = false;
this.data = {
teams: [],
};
}
constructor() {
super();
this.initialized = false;
this.data = {
teams: [],
};
}
/**
/**
* Triggers loading data from Windows registry, supports async/await
*
* @emits {update} emitted once all data has been loaded from the registry
*/
async init() {
if (process.platform === 'win32') {
// extract DefaultServerList from the registry
try {
const servers = await this.getServersListFromRegistry();
if (servers.length) {
this.data.teams.push(...servers);
}
} catch (error) {
console.log('[RegistryConfig] Nothing retrieved for \'DefaultServerList\'', error);
}
async init() {
if (process.platform === 'win32') {
// extract DefaultServerList from the registry
try {
const servers = await this.getServersListFromRegistry();
if (servers.length) {
this.data.teams.push(...servers);
}
} catch (error) {
log.warn('[RegistryConfig] Nothing retrieved for \'DefaultServerList\'', error);
}
// extract EnableServerManagement from the registry
try {
const enableServerManagement = await this.getEnableServerManagementFromRegistry();
if (enableServerManagement !== null) {
this.data.enableServerManagement = enableServerManagement;
}
} catch (error) {
console.log('[RegistryConfig] Nothing retrieved for \'EnableServerManagement\'', error);
}
// extract EnableServerManagement from the registry
try {
const enableServerManagement = await this.getEnableServerManagementFromRegistry();
if (enableServerManagement !== null) {
this.data.enableServerManagement = enableServerManagement;
}
} catch (error) {
log.warn('[RegistryConfig] Nothing retrieved for \'EnableServerManagement\'', error);
}
// extract EnableAutoUpdater from the registry
try {
const enableAutoUpdater = await this.getEnableAutoUpdatorFromRegistry();
if (enableAutoUpdater !== null) {
this.data.enableAutoUpdater = enableAutoUpdater;
// extract EnableAutoUpdater from the registry
try {
const enableAutoUpdater = await this.getEnableAutoUpdatorFromRegistry();
if (enableAutoUpdater !== null) {
this.data.enableAutoUpdater = enableAutoUpdater;
}
} catch (error) {
log.warn('[RegistryConfig] Nothing retrieved for \'EnableAutoUpdater\'', error);
}
}
} catch (error) {
console.log('[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(REGISTRY_READ_EVENT, this.data);
}
this.initialized = true;
this.emit('update', 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,
});
}
return teams;
}, []);
}
async getServersListFromRegistry() {
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 servers;
}, []);
}
/**
/**
* Determines whether server management has been enabled, disabled or isn't configured
*/
async getEnableServerManagementFromRegistry() {
const entries = (await this.getRegistryEntry(BASE_REGISTRY_KEY_PATH, 'EnableServerManagement'));
const entry = entries.pop();
return entry ? entry === '0x1' : null;
}
async getEnableServerManagementFromRegistry() {
const entries = (await this.getRegistryEntry(BASE_REGISTRY_KEY_PATH, 'EnableServerManagement'));
const entry = entries.pop();
return entry ? entry === '0x1' : null;
}
/**
/**
* Determines whether the auto updated has been enabled, disabled or isn't configured
*/
async getEnableAutoUpdatorFromRegistry() {
const entries = (await this.getRegistryEntry(BASE_REGISTRY_KEY_PATH, 'EnableAutoUpdater'));
const entry = entries.pop();
return entry ? entry === '0x1' : null;
}
async getEnableAutoUpdatorFromRegistry() {
const entries = (await this.getRegistryEntry(BASE_REGISTRY_KEY_PATH, 'EnableAutoUpdater'));
const entry = entries.pop();
return entry ? entry === '0x1' : null;
}
/**
/**
* Initiates retrieval of a specific key in the Windows registry
*
* @param {string} key Path to the registry key to return
* @param {string} name Name of specific entry in the registry key to retrieve (optional)
*/
async getRegistryEntry(key, name) {
const results = [];
for (const hive of REGISTRY_HIVE_LIST) {
results.push(this.getRegistryEntryValues(new WindowsRegistry({hive, key}), name));
async getRegistryEntry(key, name) {
const results = [];
for (const hive of REGISTRY_HIVE_LIST) {
results.push(this.getRegistryEntryValues(hive, key, name));
}
const entryValues = await Promise.all(results);
return entryValues.filter((value) => value);
}
const entryValues = await Promise.all(results);
return entryValues.filter((value) => value);
}
/**
/**
* Handles actual retrieval of entries from a configured WindowsRegistry instance
*
* @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) {
resolve();
return;
}
if (name) { // looking for a single entry value
const registryItem = items.find((item) => item.name === name);
resolve(registryItem && registryItem.value ? registryItem.value : null);
} else { // looking for an entry list
resolve(items);
}
});
});
}
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 = results.find((item) => item.name === name);
resolve(registryItem && registryItem.value ? registryItem.value : null);
} else { // looking for an entry list
resolve(results);
}
});
} catch (e) {
log.error(`There was an error accessing the registry for ${key}`);
reject(e);
}
});
}
}

View file

@ -18,16 +18,16 @@
* @prop {[]} managedResources - Defines which paths are managed
*/
const buildConfig = {
defaultTeams: [/*
defaultTeams: [/*
{
name: 'example',
url: 'https://example.com'
}
*/],
helpLink: 'https://about.mattermost.com/default-desktop-app-documentation/',
enableServerManagement: true,
enableAutoUpdater: true,
managedResources: ['trusted'],
helpLink: 'https://about.mattermost.com/default-desktop-app-documentation/',
enableServerManagement: true,
enableAutoUpdater: true,
managedResources: ['trusted'],
};
export default buildConfig;

View file

@ -7,23 +7,23 @@
* @param {number} version - Scheme version. (Not application version)
*/
const defaultPreferences = {
version: 2,
teams: [],
showTrayIcon: true,
trayIconTheme: 'light',
minimizeToTray: true,
notifications: {
flashWindow: 2,
bounceIcon: true,
bounceIconType: 'informational',
},
showUnreadBadge: true,
useSpellChecker: true,
enableHardwareAcceleration: true,
autostart: true,
spellCheckerLocale: 'en-US',
darkMode: false,
downloadLocation: `/Users/${process.env.USER || process.env.USERNAME}/Downloads`
version: 2,
teams: [],
showTrayIcon: true,
trayIconTheme: 'light',
minimizeToTray: true,
notifications: {
flashWindow: 2,
bounceIcon: true,
bounceIconType: 'informational',
},
showUnreadBadge: true,
useSpellChecker: true,
enableHardwareAcceleration: true,
autostart: true,
spellCheckerLocale: 'en-US',
darkMode: false,
downloadLocation: `/Users/${process.env.USER || process.env.USERNAME}/Downloads`,
};
export default defaultPreferences;

View file

@ -1,397 +1,469 @@
// 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: []}) {
super();
this.configFilePath = configFilePath;
this.registryConfigData = registryConfigData;
this.reload();
}
/**
* Reload all sources of config data
*
* @param {boolean} synchronize determines whether or not to emit a synchronize event once config has been reloaded
* @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) {
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');
constructor(configFilePath) {
super();
this.configFilePath = configFilePath;
}
}
/**
* Used to save a single config property
*
* @param {string} key name of config property to be saved
* @param {*} data value to save for provided key
*/
set(key, data) {
if (key) {
this.localConfigData[key] = data;
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
// 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();
}
}
/**
* Used to save an array of config properties in one go
*
* @param {array} properties an array of config properties to save
*/
setMultiple(properties = []) {
if (properties.length) {
properties.forEach(({key, data}) => {
if (key) {
this.localConfigData[key] = data;
/**
* 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);
}
});
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
}
}
setRegistryConfigData(registryConfigData = {teams: []}) {
this.registryConfigData = Object.assign({}, registryConfigData);
this.reload();
}
/**
* Reload all sources of config data
*
* @param {boolean} synchronize determines whether or not to emit a synchronize event once config has been reloaded
* @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 = () => {
this.defaultConfigData = this.loadDefaultConfigData();
this.buildConfigData = this.loadBuildConfigData();
this.localConfigData = this.loadLocalConfigFile();
this.localConfigData = this.checkForConfigUpdates(this.localConfigData);
this.regenerateCombinedConfigData();
/**
* Used to replace the existing config data with new config data
*
* @param {object} configData a new, config data object to completely replace the existing config data
*/
replace(configData) {
const newConfigData = configData;
this.localConfigData = Object.assign({}, this.localConfigData, newConfigData);
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
}
/**
* Used to save the current set of local config data to disk
*
* @emits {update} emitted once all data has been saved
* @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() {
try {
this.writeFile(this.configFilePath, this.localConfigData, (error) => {
if (error) {
throw new Error(error);
}
this.emit('update', this.combinedData);
this.emit('synchronize');
});
} catch (error) {
this.emit('error', error);
}
}
// getters for accessing the various config data inputs
get data() {
return this.combinedData;
}
get localData() {
return this.localConfigData;
}
get defaultData() {
return this.defaultConfigData;
}
get buildData() {
return this.buildConfigData;
}
get registryData() {
return this.registryConfigData;
}
// convenience getters
get version() {
return this.combinedData.version;
}
get teams() {
return this.combinedData.teams;
}
get darkMode() {
return this.combinedData.darkMode;
}
get localTeams() {
return this.localConfigData.teams;
}
get predefinedTeams() {
return [...this.buildConfigData.defaultTeams, ...this.registryConfigData.teams];
}
get enableHardwareAcceleration() {
return this.combinedData.enableHardwareAcceleration;
}
get enableServerManagement() {
return this.combinedData.enableServerManagement;
}
get enableAutoUpdater() {
return this.combinedData.enableAutoUpdater;
}
get autostart() {
return this.combinedData.autostart;
}
get notifications() {
return this.combinedData.notifications;
}
get showUnreadBadge() {
return this.combinedData.showUnreadBadge;
}
get useSpellChecker() {
return this.combinedData.useSpellChecker;
}
get spellCheckerLocale() {
return this.combinedData.spellCheckerLocale;
}
get showTrayIcon() {
return this.combinedData.showTrayIcon;
}
get trayIconTheme() {
return this.combinedData.trayIconTheme;
}
get helpLink() {
return this.combinedData.helpLink;
}
// initialization/processing methods
/**
* Returns a copy of the app's default config data
*/
loadDefaultConfigData() {
return this.copy(defaultPreferences);
}
/**
* Returns a copy of the app's build config data
*/
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() {
let configData = {};
try {
configData = this.readFileSync(this.configFilePath);
// validate based on config file version
if (configData.version > 1) {
configData = Validator.validateV2ConfigData(configData);
} else {
switch (configData.version) {
case 1:
configData = Validator.validateV1ConfigData(configData);
break;
default:
configData = Validator.validateV0ConfigData(configData);
/**
* Used to save a single config property
*
* @param {string} key name of config property to be saved
* @param {*} data value to save for provided key
*/
set = (key, data) => {
if (key) {
this.localConfigData[key] = data;
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
}
}
if (!configData) {
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.');
configData = this.copy(this.defaultConfigData);
// add default team to teams if one exists and there arent currently any teams
if (!configData.teams.length && this.defaultConfigData.defaultTeam) {
configData.teams.push(this.defaultConfigData.defaultTeam);
}
delete configData.defaultTeam;
this.writeFileSync(this.configFilePath, configData);
}
return configData;
}
/**
* Determines if locally stored data needs to be updated and upgrades as needed
*
* @param {*} data locally stored 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.`);
}
} catch (error) {
console.log(`Failed to update configuration to version ${this.defaultConfigData.version}.`);
}
return configData;
}
/**
* Properly combines all sources of data into a single, manageable set of all config data
*/
regenerateCombinedConfigData() {
// combine all config data in the correct order
this.combinedData = Object.assign({}, this.defaultConfigData, this.localConfigData, this.buildConfigData, this.registryConfigData);
// remove unecessary data pulled from default and build config
delete this.combinedData.defaultTeam;
delete this.combinedData.defaultTeams;
// IMPORTANT: properly combine teams from all sources
let combinedTeams = [];
// - start by adding default teams from buildConfig, if any
if (this.buildConfigData.defaultTeams && this.buildConfigData.defaultTeams.length) {
combinedTeams.push(...this.buildConfigData.defaultTeams);
}
// - add registry defined teams, if any
if (this.registryConfigData.teams && this.registryConfigData.teams.length) {
combinedTeams.push(...this.registryConfigData.teams);
/**
* Used to save an array of config properties in one go
*
* @param {array} properties an array of config properties to save
*/
setMultiple = (event, properties = []) => {
if (properties.length) {
properties.forEach(({key, data}) => {
if (key) {
this.localConfigData[key] = data;
}
});
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
}
return this.localConfigData; //this is the only part that changes
}
// - add locally defined teams only if server management is enabled
if (this.enableServerManagement) {
combinedTeams.push(...this.localConfigData.teams);
setRegistryConfigData = (registryConfigData = {teams: []}) => {
this.registryConfigData = Object.assign({}, registryConfigData);
this.reload();
}
combinedTeams = this.filterOutDuplicateTeams(combinedTeams);
combinedTeams = this.sortUnorderedTeams(combinedTeams);
/**
* Used to replace the existing config data with new config data
*
* @param {object} configData a new, config data object to completely replace the existing config data
*/
replace = (configData) => {
const newConfigData = configData;
this.combinedData.teams = combinedTeams;
this.combinedData.localTeams = this.localConfigData.teams;
this.combinedData.buildTeams = this.buildConfigData.defaultTeams;
this.combinedData.registryTeams = this.registryConfigData.teams;
}
this.localConfigData = Object.assign({}, this.localConfigData, newConfigData);
/**
* Returns the provided list of teams with duplicates filtered out
*
* @param {array} teams array of teams to check for duplicates
*/
filterOutDuplicateTeams(teams) {
let newTeams = teams;
const uniqueURLs = new Set();
newTeams = newTeams.filter((team) => {
return uniqueURLs.has(team.url) ? false : uniqueURLs.add(team.url);
});
return newTeams;
}
/**
* 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) {
let newTeams = teams;
// filter out predefined teams
newTeams = newTeams.filter((newTeam) => {
return this.predefinedTeams.findIndex((existingTeam) => newTeam.url === existingTeam.url) === -1; // eslint-disable-line max-nested-callbacks
});
return newTeams;
}
/**
* Apply a default sort order to the team list, if no order is specified.
* @param {array} teams to sort
*/
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}));
// Make a best pass at interpreting sort order. If an order is not specified, assume it is 0.
//
const newTeams = mappedTeams.sort((x, y) => {
if (x.team.order == null) {
x.team.order = 0;
}
if (y.team.order == null) {
y.team.order = 0;
}
// once we ensured `order` exists, we can sort numerically
return x.team.order - y.team.order;
});
// Now re-number all items from 0 to (max), ensuring user's sort order is preserved. The
// new tabbed interface requires an item with order:0 in order to raise the first tab.
//
newTeams.forEach((mappedTeam, i) => {
mappedTeam.team.order = i;
});
return newTeams.sort((x, y) => x.originalOrder - y.originalOrder).map((mappedTeam) => mappedTeam.team);
}
// helper functions
readFileSync(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
writeFile(filePath, configData, callback) {
if (configData.version !== this.defaultConfigData.version) {
throw new Error('version ' + configData.version + ' is not equal to ' + this.defaultConfigData.version);
}
const json = JSON.stringify(configData, null, ' ');
fs.writeFile(filePath, json, 'utf8', callback);
}
writeFileSync(filePath, config) {
if (config.version !== this.defaultConfigData.version) {
throw new Error('version ' + config.version + ' is not equal to ' + this.defaultConfigData.version);
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
}
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
/**
* Used to save the current set of local config data to disk
*
* @emits {update} emitted once all data has been saved
* @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 = () => {
try {
this.writeFile(this.configFilePath, this.localConfigData, (error) => {
if (error) {
throw new Error(error);
}
this.emit('update', this.combinedData);
this.emit('synchronize');
});
} catch (error) {
this.emit('error', error);
}
}
const json = JSON.stringify(config, null, ' ');
fs.writeFileSync(filePath, json, 'utf8');
}
// getters for accessing the various config data inputs
merge(base, target) {
return Object.assign({}, base, target);
}
get data() {
return this.combinedData;
}
get localData() {
return this.localConfigData;
}
get defaultData() {
return this.defaultConfigData;
}
get buildData() {
return this.buildConfigData;
}
get registryData() {
return this.registryConfigData;
}
copy(data) {
return Object.assign({}, data);
}
// convenience getters
get version() {
return this.combinedData.version;
}
get teams() {
return this.combinedData.teams;
}
get darkMode() {
return this.combinedData.darkMode;
}
get localTeams() {
return this.localConfigData.teams;
}
get predefinedTeams() {
return [...this.buildConfigData.defaultTeams, ...this.registryConfigData.teams];
}
get enableHardwareAcceleration() {
return this.combinedData.enableHardwareAcceleration;
}
get enableServerManagement() {
return this.combinedData.enableServerManagement;
}
get enableAutoUpdater() {
return this.combinedData.enableAutoUpdater;
}
get autostart() {
return this.combinedData.autostart;
}
get notifications() {
return this.combinedData.notifications;
}
get showUnreadBadge() {
return this.combinedData.showUnreadBadge;
}
get useSpellChecker() {
return this.combinedData.useSpellChecker;
}
get spellCheckerLocale() {
return this.combinedData.spellCheckerLocale;
}
get showTrayIcon() {
return this.combinedData.showTrayIcon;
}
get trayIconTheme() {
return this.combinedData.trayIconTheme;
}
get helpLink() {
return this.combinedData.helpLink;
}
// initialization/processing methods
/**
* Returns a copy of the app's default config data
*/
loadDefaultConfigData = () => {
return this.copy(defaultPreferences);
}
/**
* Returns a copy of the app's build config data
*/
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 = () => {
let configData = {};
try {
configData = this.readFileSync(this.configFilePath);
// validate based on config file version
if (configData.version > 1) {
configData = Validator.validateV2ConfigData(configData);
} else {
switch (configData.version) {
case 1:
configData = Validator.validateV1ConfigData(configData);
break;
default:
configData = Validator.validateV0ConfigData(configData);
}
}
if (!configData) {
throw new Error('Provided configuration file does not validate, using defaults instead.');
}
} catch (e) {
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
if (!configData.teams.length && this.defaultConfigData.defaultTeam) {
configData.teams.push(this.defaultConfigData.defaultTeam);
}
delete configData.defaultTeam;
this.writeFileSync(this.configFilePath, configData);
}
return configData;
}
/**
* Determines if locally stored data needs to be updated and upgrades as needed
*
* @param {*} data locally stored data
*/
checkForConfigUpdates = (data) => {
let configData = data;
try {
if (configData.version !== this.defaultConfigData.version) {
configData = upgradeConfigData(configData);
this.writeFileSync(this.configFilePath, configData);
log.info(`Configuration updated to version ${this.defaultConfigData.version} successfully.`);
}
} catch (error) {
log.error(`Failed to update configuration to version ${this.defaultConfigData.version}.`);
}
return configData;
}
/**
* Properly combines all sources of data into a single, manageable set of all config data
*/
regenerateCombinedConfigData = () => {
// combine all config data in the correct order
this.combinedData = Object.assign({}, this.defaultConfigData, this.localConfigData, this.buildConfigData, this.registryConfigData);
// remove unecessary data pulled from default and build config
delete this.combinedData.defaultTeam;
delete this.combinedData.defaultTeams;
// IMPORTANT: properly combine teams from all sources
let combinedTeams = [];
// - start by adding default teams from buildConfig, if any
if (this.buildConfigData.defaultTeams && this.buildConfigData.defaultTeams.length) {
combinedTeams.push(...this.buildConfigData.defaultTeams);
}
// - add registry defined teams, if any
if (this.registryConfigData.teams && this.registryConfigData.teams.length) {
combinedTeams.push(...this.registryConfigData.teams);
}
// - add locally defined teams only if server management is enabled
if (this.enableServerManagement) {
combinedTeams.push(...this.localConfigData.teams);
}
combinedTeams = this.filterOutDuplicateTeams(combinedTeams);
combinedTeams = this.sortUnorderedTeams(combinedTeams);
this.combinedData.teams = combinedTeams;
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;
}
/**
* Returns the provided list of teams with duplicates filtered out
*
* @param {array} teams array of teams to check for duplicates
*/
filterOutDuplicateTeams = (teams) => {
let newTeams = teams;
const uniqueURLs = new Set();
newTeams = newTeams.filter((team) => {
return uniqueURLs.has(team.url) ? false : uniqueURLs.add(team.url);
});
return newTeams;
}
/**
* 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) => {
let newTeams = teams;
// filter out predefined teams
newTeams = newTeams.filter((newTeam) => {
return this.predefinedTeams.findIndex((existingTeam) => newTeam.url === existingTeam.url) === -1; // eslint-disable-line max-nested-callbacks
});
return newTeams;
}
/**
* Apply a default sort order to the team list, if no order is specified.
* @param {array} teams to sort
*/
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}));
// Make a best pass at interpreting sort order. If an order is not specified, assume it is 0.
//
const newTeams = mappedTeams.sort((x, y) => {
if (x.team.order == null) {
x.team.order = 0;
}
if (y.team.order == null) {
y.team.order = 0;
}
// once we ensured `order` exists, we can sort numerically
return x.team.order - y.team.order;
});
// Now re-number all items from 0 to (max), ensuring user's sort order is preserved. The
// new tabbed interface requires an item with order:0 in order to raise the first tab.
//
newTeams.forEach((mappedTeam, i) => {
mappedTeam.team.order = i;
});
return newTeams.sort((x, y) => x.originalOrder - y.originalOrder).map((mappedTeam) => mappedTeam.team);
}
// helper functions
readFileSync = (filePath) => {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
writeFile = (filePath, configData, callback) => {
if (configData.version !== this.defaultConfigData.version) {
throw new Error('version ' + configData.version + ' is not equal to ' + this.defaultConfigData.version);
}
const json = JSON.stringify(configData, null, ' ');
fs.writeFile(filePath, json, 'utf8', callback);
}
writeFileSync = (filePath, config) => {
if (config.version !== this.defaultConfigData.version) {
throw new Error('version ' + config.version + ' is not equal to ' + this.defaultConfigData.version);
}
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
const json = JSON.stringify(config, null, ' ');
fs.writeFileSync(filePath, json, 'utf8');
}
merge = (base, target) => {
return Object.assign({}, base, target);
}
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);
}
}

View file

@ -4,26 +4,26 @@
import defaultPreferences from './defaultPreferences';
const pastDefaultPreferences = {
0: {
url: '',
},
1: {
version: 1,
teams: [],
showTrayIcon: false,
trayIconTheme: 'light',
minimizeToTray: false,
notifications: {
flashWindow: 0,
bounceIcon: false,
bounceIconType: 'informational',
0: {
url: '',
},
1: {
version: 1,
teams: [],
showTrayIcon: false,
trayIconTheme: 'light',
minimizeToTray: false,
notifications: {
flashWindow: 0,
bounceIcon: false,
bounceIconType: 'informational',
},
showUnreadBadge: true,
useSpellChecker: true,
enableHardwareAcceleration: true,
autostart: true,
spellCheckerLocale: 'en-US',
},
showUnreadBadge: true,
useSpellChecker: true,
enableHardwareAcceleration: true,
autostart: true,
spellCheckerLocale: 'en-US',
},
};
pastDefaultPreferences[`${defaultPreferences.version}`] = defaultPreferences;

View file

@ -4,39 +4,39 @@
import pastDefaultPreferences from './pastDefaultPreferences';
function deepCopy(object) {
return JSON.parse(JSON.stringify(object));
return JSON.parse(JSON.stringify(object));
}
function upgradeV0toV1(configV0) {
const config = deepCopy(pastDefaultPreferences['1']);
if (config.version !== 1) {
throw new Error('pastDefaultPreferences[\'1\'].version is not equal to 1');
}
config.teams.push({
name: 'Primary team',
url: configV0.url,
});
return config;
const config = deepCopy(pastDefaultPreferences['1']);
if (config.version !== 1) {
throw new Error('pastDefaultPreferences[\'1\'].version is not equal to 1');
}
config.teams.push({
name: 'Primary team',
url: configV0.url,
});
return config;
}
function upgradeV1toV2(configV1) {
const config = deepCopy(configV1);
config.version = 2;
config.teams.forEach((value, index) => {
value.order = index;
});
config.darkMode = false;
return config;
const config = deepCopy(configV1);
config.version = 2;
config.teams.forEach((value, index) => {
value.order = index;
});
config.darkMode = false;
return config;
}
export default function upgradeToLatest(config) {
const configVersion = config.version ? config.version : 0;
switch (configVersion) {
case 1:
return upgradeToLatest(upgradeV1toV2(config));
case 0:
return upgradeToLatest(upgradeV0toV1(config));
default:
return config;
}
const configVersion = config.version ? config.version : 0;
switch (configVersion) {
case 1:
return upgradeToLatest(upgradeV1toV2(config));
case 0:
return upgradeToLatest(upgradeV0toV1(config));
default:
return config;
}
}

View file

@ -4,5 +4,5 @@
import deepmerge from 'deepmerge';
export default function deepMergeProxy(x, y, options) {
return deepmerge(x, y, options); // due to webpack conversion
return deepmerge(x, y, options); // due to webpack conversion
}

View file

@ -7,14 +7,14 @@ import os from 'os';
const releaseSplit = os.release().split('.');
export default {
major: parseInt(releaseSplit[0], 10),
minor: parseInt(releaseSplit[1], 10),
isLowerThanOrEqualWindows8_1() {
if (process.platform !== 'win32') {
return false;
}
major: parseInt(releaseSplit[0], 10),
minor: parseInt(releaseSplit[1], 10),
isLowerThanOrEqualWindows8_1() {
if (process.platform !== 'win32') {
return false;
}
// consider Windows 7 and later.
return (this.major <= 6 && this.minor <= 3);
},
// consider Windows 7 and later.
return (this.major <= 6 && this.minor <= 3);
},
};

View file

@ -11,5 +11,5 @@ export const BASIC_AUTH_PERMISSION = 'canBasicAuth';
// Permission descriptions
export const PERMISSION_DESCRIPTION = {
[BASIC_AUTH_PERMISSION]: 'Web Authentication',
[BASIC_AUTH_PERMISSION]: 'Web Authentication',
};

View 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
View 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
View 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,
};

File diff suppressed because it is too large Load diff

View file

@ -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;
}
}

View 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;
}
}

View file

@ -5,44 +5,45 @@
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() {
this.appLauncher = new AutoLaunch({
name: app.name,
isHidden: true,
});
}
isEnabled() {
return this.appLauncher.isEnabled();
}
async blankPromise() {
return null;
}
async enable() {
if (isDev) {
console.log('In development mode, autostart config never effects');
return this.blankPromise();
constructor() {
this.appLauncher = new AutoLaunch({
name: app.name,
isHidden: true,
});
}
const enabled = await this.isEnabled();
if (!enabled) {
return this.appLauncher.enable();
}
return this.blankPromise();
}
async disable() {
if (isDev) {
console.log('In development mode, autostart config never effects');
return this.blankPromise();
isEnabled() {
return this.appLauncher.isEnabled();
}
const enabled = await this.isEnabled();
if (enabled) {
return this.appLauncher.disable();
async blankPromise() {
return null;
}
async enable() {
if (isDev) {
log.warn('In development mode, autostart config never effects');
return this.blankPromise();
}
const enabled = await this.isEnabled();
if (!enabled) {
return this.appLauncher.enable();
}
return this.blankPromise();
}
async disable() {
if (isDev) {
log.warn('In development mode, autostart config never effects');
return this.blankPromise();
}
const enabled = await this.isEnabled();
if (enabled) {
return this.appLauncher.disable();
}
return this.blankPromise();
}
return this.blankPromise();
}
}

View file

@ -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,94 +16,95 @@ 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}`;
}
function openDetachedExternal(url) {
const spawnOption = {detached: true, stdio: 'ignore'};
switch (process.platform) {
case 'win32':
return spawn('cmd', ['/C', 'start', url], spawnOption);
case 'darwin':
return spawn('open', [url], spawnOption);
case 'linux':
return spawn('xdg-open', [url], spawnOption);
default:
return null;
}
const spawnOption = {detached: true, stdio: 'ignore'};
switch (process.platform) {
case 'win32':
return spawn('cmd', ['/C', 'start', url], spawnOption);
case 'darwin':
return spawn('open', [url], spawnOption);
case 'linux':
return spawn('xdg-open', [url], spawnOption);
default:
return null;
}
}
export default class CriticalErrorHandler {
constructor() {
this.mainWindow = null;
}
setMainWindow(mainWindow) {
this.mainWindow = mainWindow;
}
windowUnresponsiveHandler() {
dialog.showMessageBox(this.mainWindow, {
type: 'warning',
title: app.name,
message: 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?',
buttons: ['No', 'Yes'],
defaultId: 0,
}).then(({response}) => {
if (response === 0) {
throw new Error('BrowserWindow \'unresponsive\' event has been emitted');
}
});
}
processUncaughtExceptionHandler(err) {
const file = path.join(app.getPath('userData'), `uncaughtException-${Date.now()}.txt`);
const report = createErrorReport(err);
fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL));
if (app.isReady()) {
const buttons = [BUTTON_SHOW_DETAILS, BUTTON_OK, BUTTON_REOPEN];
if (process.platform === 'darwin') {
buttons.reverse();
}
const bindWindow = this.mainWindow && this.mainWindow.isVisible() ? this.mainWindow : null;
dialog.showMessageBox(
bindWindow,
{
type: 'error',
title: app.name,
message: `The ${app.name} app quit unexpectedly. Click "Show Details" to learn more or "Reopen" to open the application again.\n\nInternal error: ${err.message}`,
buttons,
defaultId: buttons.indexOf(BUTTON_REOPEN),
noLink: true,
}
).then(({response}) => {
let child;
switch (response) {
case buttons.indexOf(BUTTON_SHOW_DETAILS):
child = openDetachedExternal(file);
if (child) {
child.on(
'error',
(spawnError) => {
console.log(spawnError);
}
);
child.unref();
}
break;
case buttons.indexOf(BUTTON_REOPEN):
app.relaunch();
break;
}
app.exit(-1);
});
} else {
log.err(`Window wasn't ready to handle the error: ${err}\ntrace: ${err.stack}`);
throw err;
constructor() {
this.mainWindow = null;
}
setMainWindow(mainWindow) {
this.mainWindow = mainWindow;
}
windowUnresponsiveHandler() {
dialog.showMessageBox(this.mainWindow, {
type: 'warning',
title: app.name,
message: 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?',
buttons: ['No', 'Yes'],
defaultId: 0,
}).then(({response}) => {
if (response === 0) {
throw new Error('BrowserWindow \'unresponsive\' event has been emitted');
}
});
}
processUncaughtExceptionHandler(err) {
const file = path.join(app.getPath('userData'), `uncaughtException-${Date.now()}.txt`);
const report = createErrorReport(err);
fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL));
if (app.isReady()) {
const buttons = [BUTTON_SHOW_DETAILS, BUTTON_OK, BUTTON_REOPEN];
if (process.platform === 'darwin') {
buttons.reverse();
}
const bindWindow = this.mainWindow && this.mainWindow.isVisible() ? this.mainWindow : null;
dialog.showMessageBox(
bindWindow,
{
type: 'error',
title: app.name,
message: `The ${app.name} app quit unexpectedly. Click "Show Details" to learn more or "Reopen" to open the application again.\n\nInternal error: ${err.message}`,
buttons,
defaultId: buttons.indexOf(BUTTON_REOPEN),
noLink: true,
},
).then(({response}) => {
let child;
switch (response) {
case buttons.indexOf(BUTTON_SHOW_DETAILS):
child = openDetachedExternal(file);
if (child) {
child.on(
'error',
(spawnError) => {
log.error(spawnError);
},
);
child.unref();
}
break;
case buttons.indexOf(BUTTON_REOPEN):
app.relaunch();
break;
}
app.exit(-1);
});
} else {
log.err(`Window wasn't ready to handle the error: ${err}\ntrace: ${err.stack}`);
throw err;
}
}
}
}

View 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());
}
}

View file

@ -8,30 +8,30 @@ import {protocols} from '../../electron-builder.json';
import * as Validator from './Validator';
export default function parse(args) {
return validateArgs(parseArgs(triageArgs(args)));
return validateArgs(parseArgs(triageArgs(args)));
}
function triageArgs(args) {
// ensure any args following a possible deeplink are discarded
if (protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]) {
const scheme = protocols[0].schemes[0].toLowerCase();
const deeplinkIndex = args.findIndex((arg) => arg.toLowerCase().includes(`${scheme}:`));
if (deeplinkIndex !== -1) {
return args.slice(0, deeplinkIndex + 1);
// ensure any args following a possible deeplink are discarded
if (protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]) {
const scheme = protocols[0].schemes[0].toLowerCase();
const deeplinkIndex = args.findIndex((arg) => arg.toLowerCase().includes(`${scheme}:`));
if (deeplinkIndex !== -1) {
return args.slice(0, deeplinkIndex + 1);
}
}
}
return args;
return args;
}
function parseArgs(args) {
return yargs.
alias('dataDir', 'd').string('dataDir').describe('dataDir', 'Set the path to where user data is stored.').
alias('disableDevMode', 'p').boolean('disableDevMode').describe('disableDevMode', 'Disable development mode. Allows for testing as if it was Production.').
alias('version', 'v').boolean('version').describe('version', 'Prints the application version.').
help('help').
parse(args);
return yargs.
alias('dataDir', 'd').string('dataDir').describe('dataDir', 'Set the path to where user data is stored.').
alias('disableDevMode', 'p').boolean('disableDevMode').describe('disableDevMode', 'Disable development mode. Allows for testing as if it was Production.').
alias('version', 'v').boolean('version').describe('version', 'Prints the application version.').
help('help').
parse(args);
}
function validateArgs(args) {
return Validator.validateArgs(args) || {};
return Validator.validateArgs(args) || {};
}

View file

@ -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';
};

View file

@ -4,6 +4,7 @@
import EventEmitter from 'events';
import electron from 'electron';
import log from 'electron-log';
const {app} = electron;
@ -11,30 +12,30 @@ const {app} = electron;
* Monitors system idle time, listens for system events and fires status updates as needed
*/
export default class UserActivityMonitor extends EventEmitter {
constructor() {
super();
constructor() {
super();
this.isActive = true;
this.idleTime = 0;
this.lastSetActive = null;
this.systemIdleTimeIntervalID = -1;
this.isActive = true;
this.idleTime = 0;
this.lastSetActive = null;
this.systemIdleTimeIntervalID = -1;
this.config = {
updateFrequencyMs: 1 * 1000, // eslint-disable-line no-magic-numbers
inactiveThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers
statusUpdateThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers
};
}
this.config = {
updateFrequencyMs: 1 * 1000, // eslint-disable-line no-magic-numbers
inactiveThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers
statusUpdateThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers
};
}
get userIsActive() {
return this.isActive;
}
get userIsActive() {
return this.isActive;
}
get userIdleTime() {
return this.idleTime;
}
get userIdleTime() {
return this.idleTime;
}
/**
/**
* Begin monitoring system events and idle time at defined frequency
*
* @param {Object} config - overide internal configuration defaults
@ -44,86 +45,86 @@ export default class UserActivityMonitor extends EventEmitter {
* @emits {error} emitted when method is called before the app is ready
* @emits {error} emitted when this method has previously been called but not subsequently stopped
*/
startMonitoring(config = {}) {
if (!app.isReady()) {
this.emit('error', new Error('UserActivityMonitor.startMonitoring can only be called after app is ready'));
return;
startMonitoring(config = {}) {
if (!app.isReady()) {
this.emit('error', new Error('UserActivityMonitor.startMonitoring can only be called after app is ready'));
return;
}
if (this.systemIdleTimeIntervalID >= 0) {
this.emit('error', new Error('User activity monitoring is already in progress'));
return;
}
this.config = Object.assign({}, this.config, config);
this.systemIdleTimeIntervalID = setInterval(() => {
try {
this.updateIdleTime(electron.powerMonitor.getSystemIdleTime());
} catch (err) {
log.error('Error getting system idle time:', err);
}
}, this.config.updateFrequencyMs);
}
if (this.systemIdleTimeIntervalID >= 0) {
this.emit('error', new Error('User activity monitoring is already in progress'));
return;
}
this.config = Object.assign({}, this.config, config);
this.systemIdleTimeIntervalID = setInterval(() => {
try {
this.updateIdleTime(electron.powerMonitor.getSystemIdleTime());
} catch (err) {
console.log('Error getting system idle time:', err);
}
}, this.config.updateFrequencyMs);
}
/**
/**
* Stop monitoring system events and idle time
*/
stopMonitoring() {
clearInterval(this.systemIdleTimeIntervalID);
}
stopMonitoring() {
clearInterval(this.systemIdleTimeIntervalID);
}
/**
/**
* Updates internal idle time and sets internal user activity state
*
* @param {integer} idleTime
* @private
*/
updateIdleTime(idleTime) {
this.idleTime = idleTime;
if (idleTime * 1000 > this.config.inactiveThresholdMs) { // eslint-disable-line no-magic-numbers
this.setActivityState(false);
} else {
this.setActivityState(true);
updateIdleTime(idleTime) {
this.idleTime = idleTime;
if (idleTime * 1000 > this.config.inactiveThresholdMs) { // eslint-disable-line no-magic-numbers
this.setActivityState(false);
} else {
this.setActivityState(true);
}
}
}
/**
/**
* Updates user active state and conditionally triggers a status update
*
* @param {boolean} isActive
* @param {boolean} isSystemEvent indicates whether the update was triggered by a system event (log in/out, screesaver on/off etc)
* @private
*/
setActivityState(isActive = false, isSystemEvent = false) {
this.isActive = isActive;
setActivityState(isActive = false, isSystemEvent = false) {
this.isActive = isActive;
if (isSystemEvent) {
this.sendStatusUpdate(true);
return;
if (isSystemEvent) {
this.sendStatusUpdate(true);
return;
}
const now = Date.now();
if (isActive && (this.lastSetActive == null || now - this.lastSetActive >= this.config.statusUpdateThresholdMs)) {
this.sendStatusUpdate(false);
this.lastSetActive = now;
} else if (!isActive) {
this.lastSetActive = null;
}
}
const now = Date.now();
if (isActive && (this.lastSetActive == null || now - this.lastSetActive >= this.config.statusUpdateThresholdMs)) {
this.sendStatusUpdate(false);
this.lastSetActive = now;
} else if (!isActive) {
this.lastSetActive = null;
}
}
/**
/**
* Sends an update with user activity status and current system idle time
*
* @emits {status} emitted at regular, definable intervals providing an update on user active status and idle time
* @private
*/
sendStatusUpdate(isSystemEvent = false) {
this.emit('status', {
userIsActive: this.isActive,
idleTime: this.idleTime,
isSystemEvent,
});
}
sendStatusUpdate(isSystemEvent = false) {
this.emit('status', {
userIsActive: this.isActive,
idleTime: this.idleTime,
isSystemEvent,
});
}
}

View file

@ -1,11 +1,13 @@
// 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,
stripUnknown: true,
};
const defaultWindowWidth = 1000;
const defaultWindowHeight = 700;
@ -13,193 +15,193 @@ const minWindowWidth = 400;
const minWindowHeight = 240;
const argsSchema = Joi.object({
hidden: Joi.boolean(),
disableDevMode: Joi.boolean(),
dataDir: Joi.string(),
version: Joi.boolean(),
hidden: Joi.boolean(),
disableDevMode: Joi.boolean(),
dataDir: Joi.string(),
version: Joi.boolean(),
});
const boundsInfoSchema = Joi.object({
x: Joi.number().integer().default(0),
y: Joi.number().integer().default(0),
width: Joi.number().integer().min(minWindowWidth).required().default(defaultWindowWidth),
height: Joi.number().integer().min(minWindowHeight).required().default(defaultWindowHeight),
maximized: Joi.boolean().default(false),
fullscreen: Joi.boolean().default(false),
x: Joi.number().integer().default(0),
y: Joi.number().integer().default(0),
width: Joi.number().integer().min(minWindowWidth).required().default(defaultWindowWidth),
height: Joi.number().integer().min(minWindowHeight).required().default(defaultWindowHeight),
maximized: Joi.boolean().default(false),
fullscreen: Joi.boolean().default(false),
});
const appStateSchema = Joi.object({
lastAppVersion: Joi.string(),
skippedVersion: Joi.string(),
updateCheckedDate: Joi.string(),
lastAppVersion: Joi.string(),
skippedVersion: Joi.string(),
updateCheckedDate: Joi.string(),
});
const configDataSchemaV0 = Joi.object({
url: Joi.string().required(),
url: Joi.string().required(),
});
const configDataSchemaV1 = Joi.object({
version: Joi.number().min(1).default(1),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
})).default([]),
showTrayIcon: Joi.boolean().default(false),
trayIconTheme: Joi.any().allow('').valid('light', 'dark').default('light'),
minimizeToTray: Joi.boolean().default(false),
notifications: Joi.object({
flashWindow: Joi.any().valid(0, 2).default(0),
bounceIcon: Joi.boolean().default(false),
bounceIconType: Joi.any().allow('').valid('informational', 'critical').default('informational'),
}),
showUnreadBadge: Joi.boolean().default(true),
useSpellChecker: Joi.boolean().default(true),
enableHardwareAcceleration: Joi.boolean().default(true),
autostart: Joi.boolean().default(true),
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
version: Joi.number().min(1).default(1),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
})).default([]),
showTrayIcon: Joi.boolean().default(false),
trayIconTheme: Joi.any().allow('').valid('light', 'dark').default('light'),
minimizeToTray: Joi.boolean().default(false),
notifications: Joi.object({
flashWindow: Joi.any().valid(0, 2).default(0),
bounceIcon: Joi.boolean().default(false),
bounceIconType: Joi.any().allow('').valid('informational', 'critical').default('informational'),
}),
showUnreadBadge: Joi.boolean().default(true),
useSpellChecker: Joi.boolean().default(true),
enableHardwareAcceleration: Joi.boolean().default(true),
autostart: Joi.boolean().default(true),
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
});
const configDataSchemaV2 = Joi.object({
version: Joi.number().min(2).default(2),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
order: Joi.number().integer().min(0),
})).default([]),
showTrayIcon: Joi.boolean().default(false),
trayIconTheme: Joi.any().allow('').valid('light', 'dark').default('light'),
minimizeToTray: Joi.boolean().default(false),
notifications: Joi.object({
flashWindow: Joi.any().valid(0, 2).default(0),
bounceIcon: Joi.boolean().default(false),
bounceIconType: Joi.any().allow('').valid('informational', 'critical').default('informational'),
}),
showUnreadBadge: Joi.boolean().default(true),
useSpellChecker: Joi.boolean().default(true),
enableHardwareAcceleration: Joi.boolean().default(true),
autostart: Joi.boolean().default(true),
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
darkMode: Joi.boolean().default(false),
downloadLocation: Joi.string(),
version: Joi.number().min(2).default(2),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
order: Joi.number().integer().min(0),
})).default([]),
showTrayIcon: Joi.boolean().default(false),
trayIconTheme: Joi.any().allow('').valid('light', 'dark').default('light'),
minimizeToTray: Joi.boolean().default(false),
notifications: Joi.object({
flashWindow: Joi.any().valid(0, 2).default(0),
bounceIcon: Joi.boolean().default(false),
bounceIconType: Joi.any().allow('').valid('informational', 'critical').default('informational'),
}),
showUnreadBadge: Joi.boolean().default(true),
useSpellChecker: Joi.boolean().default(true),
enableHardwareAcceleration: Joi.boolean().default(true),
autostart: Joi.boolean().default(true),
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
darkMode: Joi.boolean().default(false),
downloadLocation: Joi.string(),
});
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
const certificateStoreSchema = Joi.object().pattern(
Joi.string().uri(),
Joi.object({
data: Joi.string(),
issuerName: Joi.string(),
})
Joi.string().uri(),
Joi.object({
data: Joi.string(),
issuerName: Joi.string(),
}),
);
const originPermissionsSchema = Joi.object().keys({
canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want
canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want
});
const trustedOriginsSchema = Joi.object({}).pattern(
Joi.string().uri(),
Joi.object().keys({
canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want
}),
Joi.string().uri(),
Joi.object().keys({
canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want
}),
);
const allowedProtocolsSchema = Joi.array().items(Joi.string().regex(/^[a-z-]+:$/i));
// validate bounds_info.json
export function validateArgs(data) {
return validateAgainstSchema(data, argsSchema);
return validateAgainstSchema(data, argsSchema);
}
// validate bounds_info.json
export function validateBoundsInfo(data) {
return validateAgainstSchema(data, boundsInfoSchema);
return validateAgainstSchema(data, boundsInfoSchema);
}
// validate app_state.json
export function validateAppState(data) {
return validateAgainstSchema(data, appStateSchema);
return validateAgainstSchema(data, appStateSchema);
}
// validate v.0 config.json
export function validateV0ConfigData(data) {
return validateAgainstSchema(data, configDataSchemaV0);
return validateAgainstSchema(data, configDataSchemaV0);
}
// validate v.1 config.json
export function validateV1ConfigData(data) {
if (Array.isArray(data.teams) && data.teams.length) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map(({name, url}) => {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return {name, url: updatedURL};
});
let teams = data.teams.map(({name, url}) => {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return {name, url: updatedURL};
});
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
// replace original teams
data.teams = teams;
}
return validateAgainstSchema(data, configDataSchemaV1);
// replace original teams
data.teams = teams;
}
return validateAgainstSchema(data, configDataSchemaV1);
}
export function validateV2ConfigData(data) {
if (Array.isArray(data.teams) && data.teams.length) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map(({name, url, order}) => {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return {name, url: updatedURL, order};
});
let teams = data.teams.map(({name, url, order}) => {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return {name, url: updatedURL, order};
});
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
// replace original teams
data.teams = teams;
}
return validateAgainstSchema(data, configDataSchemaV2);
// replace original teams
data.teams = teams;
}
return validateAgainstSchema(data, configDataSchemaV2);
}
// validate certificate.json
export function validateCertificateStore(data) {
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, certificateStoreSchema);
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, certificateStoreSchema);
}
// validate allowedProtocols.json
export function validateAllowedProtocols(data) {
return validateAgainstSchema(data, allowedProtocolsSchema);
return validateAgainstSchema(data, allowedProtocolsSchema);
}
export function validateTrustedOriginsStore(data) {
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, trustedOriginsSchema);
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, trustedOriginsSchema);
}
export function validateOriginPermissions(data) {
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, originPermissionsSchema);
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, originPermissionsSchema);
}
function validateAgainstSchema(data, schema) {
if (typeof data !== 'object') {
console.error(`Input 'data' is not an object we can validate: ${typeof data}`);
return false;
}
if (!schema) {
console.error('No schema provided to validate');
return false;
}
const {error, value} = schema.validate(data, defaultOptions);
if (error) {
console.error(`Validation failed due to: ${error}`);
return false;
}
return value;
if (typeof data !== 'object') {
log.error(`Input 'data' is not an object we can validate: ${typeof data}`);
return false;
}
if (!schema) {
log.error('No schema provided to validate');
return false;
}
const {error, value} = schema.validate(data, defaultOptions);
if (error) {
log.error(`Validation failed due to: ${error}`);
return false;
}
return value;
}

View file

@ -3,83 +3,85 @@
// 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 = [];
function addScheme(scheme) {
const proto = `${scheme}:`;
if (!allowedProtocols.includes(proto)) {
allowedProtocols.push(proto);
}
const proto = `${scheme}:`;
if (!allowedProtocols.includes(proto)) {
allowedProtocols.push(proto);
}
}
function init(mainWindow) {
fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => {
if (!err) {
allowedProtocols = JSON.parse(data);
allowedProtocols = Validator.validateAllowedProtocols(allowedProtocols) || [];
}
addScheme('http');
addScheme('https');
protocols.forEach((protocol) => {
if (protocol.schemes && protocol.schemes.length > 0) {
protocol.schemes.forEach(addScheme);
}
});
initDialogEvent(mainWindow);
});
}
function initDialogEvent(mainWindow) {
ipcMain.on('confirm-protocol', (event, protocol, URL) => {
if (allowedProtocols.indexOf(protocol) !== -1) {
shell.openExternal(URL);
return;
}
dialog.showMessageBox(mainWindow, {
title: 'Non http(s) protocol',
message: `${protocol} link requires an external application.`,
detail: `The requested link is ${URL} . Do you want to continue?`,
type: 'warning',
buttons: [
'Yes',
`Yes (Save ${protocol} as allowed)`,
'No',
],
cancelId: 2,
noLink: true,
}).then(({response}) => {
switch (response) {
case 1: {
allowedProtocols.push(protocol);
function handleError(err) {
if (err) {
console.error(err);
}
function init() {
fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => {
if (!err) {
allowedProtocols = JSON.parse(data);
allowedProtocols = Validator.validateAllowedProtocols(allowedProtocols) || [];
}
addScheme('http');
addScheme('https');
protocols.forEach((protocol) => {
if (protocol.schemes && protocol.schemes.length > 0) {
protocol.schemes.forEach(addScheme);
}
});
});
}
function handleDialogEvent(protocol, URL) {
if (allowedProtocols.indexOf(protocol) !== -1) {
shell.openExternal(URL);
return;
}
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',
`Yes (Save ${protocol} as allowed)`,
'No',
],
cancelId: 2,
noLink: true,
}).then(({response}) => {
switch (response) {
case 1: {
allowedProtocols.push(protocol);
function handleError(err) {
if (err) {
log.error(err);
}
}
fs.writeFile(allowedProtocolFile, JSON.stringify(allowedProtocols), handleError);
shell.openExternal(URL);
break;
}
case 0:
shell.openExternal(URL);
break;
default:
break;
}
fs.writeFile(allowedProtocolFile, JSON.stringify(allowedProtocols), handleError);
shell.openExternal(URL);
break;
}
case 0:
shell.openExternal(URL);
break;
default:
break;
}
});
});
}
export default {
init,
init,
handleDialogEvent,
};

120
src/main/appState.js Normal file
View 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
View 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();
}
}

View file

@ -4,16 +4,16 @@
import AutoLaunch from 'auto-launch';
async function upgradeAutoLaunch() {
if (process.platform === 'darwin') {
return;
}
const appLauncher = new AutoLaunch({
name: 'Mattermost',
});
const enabled = await appLauncher.isEnabled();
if (enabled) {
await appLauncher.enable();
}
if (process.platform === 'darwin') {
return;
}
const appLauncher = new AutoLaunch({
name: 'Mattermost',
});
const enabled = await appLauncher.isEnabled();
if (enabled) {
await appLauncher.enable();
}
}
export default upgradeAutoLaunch;

View file

@ -6,190 +6,190 @@ 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;
function createEventListener(win, eventName) {
return (event) => {
if (event.sender === win.webContents) {
win.emit(eventName);
}
};
return (event) => {
if (event.sender === win.webContents) {
win.emit(eventName);
}
};
}
function createUpdaterModal(parentWindow, options) {
const windowWidth = 480;
const windowHeight = 280;
const windowOptions = {
title: `${app.name} Updater`,
parent: parentWindow,
modal: true,
maximizable: false,
show: false,
width: windowWidth,
height: windowHeight,
resizable: false,
autoHideMenuBar: true,
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
};
if (process.platform === 'linux') {
windowOptions.icon = options.linuxAppIcon;
}
const windowWidth = 480;
const windowHeight = 280;
const windowOptions = {
title: `${app.name} Updater`,
parent: parentWindow,
modal: true,
maximizable: false,
show: false,
width: windowWidth,
height: windowHeight,
resizable: false,
autoHideMenuBar: true,
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
};
if (process.platform === 'linux') {
windowOptions.icon = options.linuxAppIcon;
}
const modal = new BrowserWindow(windowOptions);
modal.once('ready-to-show', () => {
modal.show();
});
let updaterURL = (global.isDev ? 'http://localhost:8080' : `file://${app.getAppPath()}`) + '/browser/updater.html';
if (options.notifyOnly) {
updaterURL += '?notifyOnly=true';
}
modal.loadURL(updaterURL);
for (const eventName of ['click-release-notes', 'click-skip', 'click-remind', 'click-install', 'click-download', 'click-cancel']) {
const listener = createEventListener(modal, eventName);
ipcMain.on(eventName, listener);
modal.on('closed', () => {
ipcMain.removeListener(eventName, listener);
const modal = new BrowserWindow(windowOptions);
modal.once('ready-to-show', () => {
modal.show();
});
}
let updaterURL = (global.isDev ? 'http://localhost:8080' : `file://${app.getAppPath()}`) + '/browser/updater.html';
return modal;
if (options.notifyOnly) {
updaterURL += '?notifyOnly=true';
}
modal.loadURL(updaterURL);
for (const eventName of ['click-release-notes', 'click-skip', 'click-remind', 'click-install', 'click-download', 'click-cancel']) {
const listener = createEventListener(modal, eventName);
ipcMain.on(eventName, listener);
modal.on('closed', () => {
ipcMain.removeListener(eventName, listener);
});
}
return modal;
}
function isUpdateApplicable(now, skippedVersion, updateInfo) {
const releaseTime = new Date(updateInfo.releaseDate).getTime();
const releaseTime = new Date(updateInfo.releaseDate).getTime();
// 48 hours after a new version is added to releases.mattermost.com, user receives a “New update is available” dialog
if (now.getTime() - releaseTime < UPDATER_INTERVAL_IN_MS) {
return false;
}
// 48 hours after a new version is added to releases.mattermost.com, user receives a “New update is available” dialog
if (now.getTime() - releaseTime < UPDATER_INTERVAL_IN_MS) {
return false;
}
// If a version was skipped, compare version.
if (skippedVersion) {
return semver.gt(updateInfo.version, skippedVersion);
}
// If a version was skipped, compare version.
if (skippedVersion) {
return semver.gt(updateInfo.version, skippedVersion);
}
return true;
return true;
}
function downloadAndInstall(cancellationToken) {
autoUpdater.on('update-downloaded', () => {
global.willAppQuit = true;
autoUpdater.quitAndInstall();
});
autoUpdater.downloadUpdate(cancellationToken);
autoUpdater.on('update-downloaded', () => {
global.willAppQuit = true;
autoUpdater.quitAndInstall();
});
autoUpdater.downloadUpdate(cancellationToken);
}
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);
}).on('update-available', (info) => {
let cancellationToken = null;
if (isUpdateApplicable(new Date(), appState.skippedVersion, info)) {
updaterModal = createUpdaterModal(mainWindow, {
linuxAppIcon: path.join(assetsDir, 'appicon.png'),
notifyOnly,
});
updaterModal.on('closed', () => {
updaterModal = null;
});
updaterModal.on('click-skip', () => {
appState.skippedVersion = info.version;
updaterModal.close();
}).on('click-remind', () => {
appState.updateCheckedDate = new Date();
setTimeout(() => { // eslint-disable-line max-nested-callbacks
autoUpdater.checkForUpdates();
autoUpdater.autoDownload = false; // To prevent upgrading on quit
const assetsDir = path.resolve(app.getAppPath(), 'assets');
autoUpdater.on('error', (err) => {
log.error('Error in autoUpdater:', err.message);
}).on('update-available', (info) => {
let cancellationToken = null;
if (isUpdateApplicable(new Date(), appState.skippedVersion, info)) {
updaterModal = createUpdaterModal(mainWindow, {
linuxAppIcon: path.join(assetsDir, 'appicon.png'),
notifyOnly,
});
updaterModal.on('closed', () => {
updaterModal = null;
});
updaterModal.on('click-skip', () => {
appState.skippedVersion = info.version;
updaterModal.close();
}).on('click-remind', () => {
appState.updateCheckedDate = new Date();
setTimeout(() => { // eslint-disable-line max-nested-callbacks
autoUpdater.checkForUpdates();
}, UPDATER_INTERVAL_IN_MS);
updaterModal.close();
}).on('click-install', () => {
updaterModal.webContents.send('start-download');
autoUpdater.signals.progress((data) => { // eslint-disable-line max-nested-callbacks
updaterModal.send('progress', Math.floor(data.percent));
log.info('progress:', data);
});
cancellationToken = new CancellationToken();
downloadAndInstall(cancellationToken);
}).on('click-download', () => {
shell.openExternal('https://about.mattermost.com/download/#mattermostApps');
}).on('click-release-notes', () => {
shell.openExternal(`https://github.com/mattermost/desktop/releases/v${info.version}`);
}).on('click-cancel', () => {
cancellationToken.cancel();
updaterModal.close();
});
updaterModal.focus();
} else if (autoUpdater.isManual) {
autoUpdater.emit('update-not-available');
}
}).on('update-not-available', () => {
if (autoUpdater.isManual) {
dialog.showMessageBox(mainWindow, {
type: 'info',
buttons: ['Close'],
title: 'Your Desktop App is up to date',
message: 'You have the latest version of the Mattermost Desktop App.',
});
}
setTimeout(() => {
autoUpdater.checkForUpdates();
}, UPDATER_INTERVAL_IN_MS);
updaterModal.close();
}).on('click-install', () => {
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);
});
cancellationToken = new CancellationToken();
downloadAndInstall(cancellationToken);
}).on('click-download', () => {
shell.openExternal('https://about.mattermost.com/download/#mattermostApps');
}).on('click-release-notes', () => {
shell.openExternal(`https://github.com/mattermost/desktop/releases/v${info.version}`);
}).on('click-cancel', () => {
cancellationToken.cancel();
updaterModal.close();
});
updaterModal.focus();
} else if (autoUpdater.isManual) {
autoUpdater.emit('update-not-available');
}
}).on('update-not-available', () => {
if (autoUpdater.isManual) {
dialog.showMessageBox(mainWindow, {
type: 'info',
buttons: ['Close'],
title: 'Your Desktop App is up to date',
message: 'You have the latest version of the Mattermost Desktop App.',
});
}
setTimeout(() => {
autoUpdater.checkForUpdates();
}, UPDATER_INTERVAL_IN_MS);
});
});
}
function shouldCheckForUpdatesOnStart(updateCheckedDate) {
if (updateCheckedDate) {
if (Date.now() - updateCheckedDate.getTime() < UPDATER_INTERVAL_IN_MS) {
return false;
if (updateCheckedDate) {
if (Date.now() - updateCheckedDate.getTime() < UPDATER_INTERVAL_IN_MS) {
return false;
}
}
}
return true;
return true;
}
function checkForUpdates(isManual = false) {
autoUpdater.isManual = isManual;
if (!updaterModal) {
autoUpdater.checkForUpdates();
}
autoUpdater.isManual = isManual;
if (!updaterModal) {
autoUpdater.checkForUpdates();
}
}
class AutoUpdaterConfig {
constructor() {
this.data = {};
}
constructor() {
this.data = {};
}
isNotifyOnly() {
if (process.platform === 'win32') {
return true;
isNotifyOnly() {
if (process.platform === 'win32') {
return true;
}
if (this.data.notifyOnly === true) {
return true;
}
return false;
}
if (this.data.notifyOnly === true) {
return true;
}
return false;
}
}
function loadConfig() {
return new AutoUpdaterConfig();
return new AutoUpdaterConfig();
}
export default {
UPDATER_INTERVAL_IN_MS,
checkForUpdates,
shouldCheckForUpdatesOnStart,
initialize,
loadConfig,
UPDATER_INTERVAL_IN_MS,
checkForUpdates,
shouldCheckForUpdatesOnStart,
initialize,
loadConfig,
};

64
src/main/badge.js Normal file
View 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);
}

View 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}`);
}
}
}
}

View file

@ -5,64 +5,64 @@
import fs from 'fs';
import urlUtils from '../utils/url';
import urlUtils from 'common/utils/url';
import * as Validator from './Validator';
function comparableCertificate(certificate) {
return {
data: certificate.data.toString(),
issuerName: certificate.issuerName,
};
return {
data: certificate.data.toString(),
issuerName: certificate.issuerName,
};
}
function areEqual(certificate0, certificate1) {
if (certificate0.data !== certificate1.data) {
return false;
}
if (certificate0.issuerName !== certificate1.issuerName) {
return false;
}
return true;
if (certificate0.data !== certificate1.data) {
return false;
}
if (certificate0.issuerName !== certificate1.issuerName) {
return false;
}
return true;
}
function CertificateStore(storeFile) {
this.storeFile = storeFile;
let storeStr;
try {
storeStr = fs.readFileSync(storeFile, 'utf-8');
const result = Validator.validateCertificateStore(storeStr);
if (!result) {
throw new Error('Provided certificate store file does not validate, using defaults instead.');
this.storeFile = storeFile;
let storeStr;
try {
storeStr = fs.readFileSync(storeFile, 'utf-8');
const result = Validator.validateCertificateStore(storeStr);
if (!result) {
throw new Error('Provided certificate store file does not validate, using defaults instead.');
}
this.data = result;
} catch (e) {
this.data = {};
}
this.data = result;
} catch (e) {
this.data = {};
}
}
CertificateStore.prototype.save = function save() {
fs.writeFileSync(this.storeFile, JSON.stringify(this.data, null, ' '));
fs.writeFileSync(this.storeFile, JSON.stringify(this.data, null, ' '));
};
CertificateStore.prototype.add = function add(targetURL, certificate) {
this.data[urlUtils.getHost(targetURL)] = comparableCertificate(certificate);
this.data[urlUtils.getHost(targetURL)] = comparableCertificate(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) {
const host = urlUtils.getHost(targetURL);
if (!this.isExisting(targetURL)) {
return false;
}
return areEqual(this.data[host], comparableCertificate(certificate));
const host = urlUtils.getHost(targetURL);
if (!this.isExisting(targetURL)) {
return false;
}
return areEqual(this.data[host], comparableCertificate(certificate));
};
export default {
load(storeFile) {
return new CertificateStore(storeFile);
},
load(storeFile) {
return new CertificateStore(storeFile);
},
};

64
src/main/contextMenu.js Normal file
View 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,
};

View file

@ -2,22 +2,23 @@
// 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}`);
});
session.cookies.flushStore().catch((err) => {
log.error(`There was a problem flushing cookies:\n${err}`);
});
}
export default function initCookieManager(session) {
// Somehow cookies are not immediately saved to disk.
// So manually flush cookie store to disk on closing the app.
// https://github.com/electron/electron/issues/8416
app.on('before-quit', () => {
flushCookiesStore(session);
});
// Somehow cookies are not immediately saved to disk.
// So manually flush cookie store to disk on closing the app.
// https://github.com/electron/electron/issues/8416
app.on('before-quit', () => {
flushCookiesStore(session);
});
app.on('browser-window-blur', () => {
flushCookiesStore(session);
});
app.on('browser-window-blur', () => {
flushCookiesStore(session);
});
}

View file

@ -1,61 +1,65 @@
// 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) {
const {net} = electron;
const request = net.request(URL);
request.setHeader('Accept-Encoding', 'gzip,deflate');
request.on('response', (response) => {
const file = getAttachmentName(response.headers);
const dialogOptions = {
defaultPath: path.join(app.getPath('downloads'), file),
};
dialog.showSaveDialog(
browserWindow,
dialogOptions
).then(
(filename) => {
if (filename) {
saveResponseBody(response, filename, callback);
}
}
).catch((err) => {
callback(err);
});
}).on('error', callback);
request.end();
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');
request.on('response', (response) => {
const file = getAttachmentName(response.headers);
const dialogOptions = {
defaultPath: path.join(app.getPath('downloads'), file),
};
dialog.showSaveDialog(
WindowManager.getMainWindow(true),
dialogOptions,
).then(
(filename) => {
if (filename) {
saveResponseBody(response, filename, callback);
}
},
).catch((err) => {
callback(err);
});
}).on('error', callback);
request.end();
}
function getAttachmentName(headers) {
if (headers['content-disposition']) {
const contentDisposition = headers['content-disposition'][0];
const matched = contentDisposition.match(/filename="(.*)"/);
if (matched) {
return path.basename(matched[1]);
if (headers['content-disposition']) {
const contentDisposition = headers['content-disposition'][0];
const matched = contentDisposition.match(/filename="(.*)"/);
if (matched) {
return path.basename(matched[1]);
}
}
}
return '';
return '';
}
function saveResponseBody(response, filename, callback) {
const output = fs.createWriteStream(filename);
output.on('close', callback);
switch (response.headers['content-encoding']) {
case 'gzip':
response.pipe(zlib.createGunzip()).pipe(output).on('error', callback);
break;
case 'deflate':
response.pipe(zlib.createInflate()).pipe(output).on('error', callback);
break;
default:
response.pipe(output).on('error', callback);
break;
}
const output = fs.createWriteStream(filename);
output.on('close', callback);
switch (response.headers['content-encoding']) {
case 'gzip':
response.pipe(zlib.createGunzip()).pipe(output).on('error', callback);
break;
case 'deflate':
response.pipe(zlib.createInflate()).pipe(output).on('error', callback);
break;
default:
response.pipe(output).on('error', callback);
break;
}
}

705
src/main/main.js Normal file
View 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 instances (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();
}

View file

@ -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;

View file

@ -3,291 +3,256 @@
// 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';
const separatorItem = {
type: 'separator',
};
import * as WindowManager from '../windows/windowManager';
const appName = app.name;
const firstMenuName = (process.platform === 'darwin') ? appName : 'File';
const template = [];
function createTemplate(config) {
const separatorItem = {
type: 'separator',
};
let platformAppMenu = process.platform === 'darwin' ? [{
label: 'About ' + appName,
role: 'about',
click() {
dialog.showMessageBox(mainWindow, {
buttons: ['OK'],
message: `${appName} Desktop ${app.getVersion()}`,
});
},
}, separatorItem, {
label: 'Preferences...',
accelerator: 'CmdOrCtrl+,',
click() {
mainWindow.loadURL(settingsURL);
},
}] : [{
label: 'Settings...',
accelerator: 'CmdOrCtrl+,',
click() {
mainWindow.loadURL(settingsURL);
},
}];
const isMac = process.platform === 'darwin';
const appName = app.name;
const firstMenuName = isMac ? appName : 'File';
const template = [];
if (config.enableServerManagement === true) {
const settingsLabel = isMac ? 'Preferences...' : 'Settings...';
let platformAppMenu = [];
if (isMac) {
platformAppMenu.push(
{
label: 'About ' + appName,
role: 'about',
},
);
platformAppMenu.push(separatorItem);
}
platformAppMenu.push({
label: 'Sign in to Another Server',
click() {
mainWindow.webContents.send('add-server');
},
});
}
platformAppMenu = platformAppMenu.concat(process.platform === 'darwin' ? [
separatorItem, {
role: 'hide',
}, {
role: 'hideothers',
}, {
role: 'unhide',
}, separatorItem, {
role: 'quit',
}] : [
separatorItem, {
role: 'quit',
accelerator: 'CmdOrCtrl+Q',
click() {
app.quit();
},
}]
);
template.push({
label: '&' + firstMenuName,
submenu: [
...platformAppMenu,
],
});
template.push({
label: '&Edit',
submenu: [{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
click() {
mainWindow.webContents.send('undo');
},
}, {
label: 'Redo',
accelerator: 'CmdOrCtrl+SHIFT+Z',
click() {
mainWindow.webContents.send('redo');
},
}, separatorItem, {
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
click() {
mainWindow.webContents.send('cut');
},
}, {
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
click() {
mainWindow.webContents.send('copy');
},
}, {
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
click() {
mainWindow.webContents.send('paste');
},
}, {
label: 'Paste and Match Style',
accelerator: 'CmdOrCtrl+SHIFT+V',
visible: process.platform === 'darwin',
click() {
mainWindow.webContents.send('paste-and-match');
},
}, {
role: 'selectall',
accelerator: 'CmdOrCtrl+A',
}],
});
const viewSubMenu = [{
label: 'Find..',
accelerator: 'CmdOrCtrl+F',
click(item, focusedWindow) {
focusedWindow.webContents.send('toggle-find');
},
}, {
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click(item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow === mainWindow) {
mainWindow.webContents.send('reload-tab');
} else {
focusedWindow.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);
}
}
},
}, {
role: 'togglefullscreen',
accelerator: process.platform === 'darwin' ? 'Ctrl+Cmd+F' : 'F11',
}, separatorItem, {
label: 'Actual Size',
accelerator: 'CmdOrCtrl+0',
click() {
mainWindow.webContents.send('zoom-reset');
},
}, {
label: 'Zoom In',
accelerator: 'CmdOrCtrl+SHIFT+=',
click() {
mainWindow.webContents.send('zoom-in');
},
}, {
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click() {
mainWindow.webContents.send('zoom-out');
},
}, separatorItem, {
label: 'Developer Tools for Application Wrapper',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
}
return 'Ctrl+Shift+I';
})(),
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.toggleDevTools();
}
},
}, {
label: 'Developer Tools for Current Server',
click() {
mainWindow.webContents.send('open-devtool');
},
}];
if (process.platform !== 'darwin') {
viewSubMenu.push(separatorItem);
viewSubMenu.push({
label: 'Toggle Dark Mode',
click() {
mainWindow.webContents.send('set-dark-mode');
},
});
}
template.push({
label: '&View',
submenu: viewSubMenu,
});
template.push({
label: '&History',
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();
}
},
}, {
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();
}
},
}],
});
const teams = config.teams;
const windowMenu = {
label: '&Window',
submenu: [{
role: 'minimize',
// empty string removes shortcut on Windows; null will default by OS
accelerator: process.platform === 'win32' ? '' : null,
}, {
role: 'close',
accelerator: 'CmdOrCtrl+W',
}, separatorItem, ...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team, i) => {
return {
label: team.name,
accelerator: `CmdOrCtrl+${i + 1}`,
label: settingsLabel,
accelerator: 'CmdOrCtrl+,',
click() {
mainWindow.show(); // for OS X
mainWindow.webContents.send('switch-tab', i);
WindowManager.showSettingsWindow();
},
};
}), separatorItem, {
label: 'Select Next Server',
accelerator: 'Ctrl+Tab',
click() {
mainWindow.webContents.send('select-next-tab');
},
enabled: (teams.length > 1),
}, {
label: 'Select Previous Server',
accelerator: 'Ctrl+Shift+Tab',
click() {
mainWindow.webContents.send('select-previous-tab');
},
enabled: (teams.length > 1),
}],
};
template.push(windowMenu);
const submenu = [];
if (config.helpLink) {
submenu.push({
label: 'Learn More...',
click() {
shell.openExternal(config.helpLink);
},
});
submenu.push(separatorItem);
}
submenu.push({
label: `Version ${app.getVersion()}`,
enabled: false,
});
template.push({label: 'Hel&p', submenu});
return template;
if (config.data.enableServerManagement === true) {
platformAppMenu.push({
label: 'Sign in to Another Server',
click() {
WindowManager.sendToRenderer(ADD_SERVER);
},
});
}
if (isMac) {
platformAppMenu = platformAppMenu.concat([
separatorItem, {
role: 'hide',
}, {
role: 'hideothers',
}, {
role: 'unhide',
}, separatorItem, {
role: 'quit',
}]);
} else {
platformAppMenu = platformAppMenu.concat([
separatorItem, {
role: 'quit',
accelerator: 'CmdOrCtrl+Q',
}]);
}
template.push({
label: '&' + firstMenuName,
submenu: [
...platformAppMenu,
],
});
template.push({
label: '&Edit',
submenu: [{
role: 'undo',
accelerator: 'CmdOrCtrl+Z',
}, {
role: 'Redo',
accelerator: 'CmdOrCtrl+SHIFT+Z',
}, separatorItem, {
role: 'cut',
accelerator: 'CmdOrCtrl+X',
}, {
role: 'copy',
accelerator: 'CmdOrCtrl+C',
}, {
role: 'paste',
accelerator: 'CmdOrCtrl+V',
}, {
role: 'pasteAndMatchStyle',
accelerator: 'CmdOrCtrl+SHIFT+V',
}, {
role: 'selectall',
accelerator: 'CmdOrCtrl+A',
}],
});
const viewSubMenu = [{
label: 'Find..',
accelerator: 'CmdOrCtrl+F',
click() {
WindowManager.openFinder();
},
}, {
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click() {
WindowManager.reload();
},
}, {
label: 'Clear Cache and Reload',
accelerator: 'Shift+CmdOrCtrl+R',
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',
}, {
role: 'zoomIn',
accelerator: 'CmdOrCtrl+SHIFT+=',
}, {
role: 'zoomOut',
accelerator: 'CmdOrCtrl+-',
}, separatorItem, {
label: 'Developer Tools for Application Wrapper',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
}
return 'Ctrl+Shift+I';
})(),
click(item, focusedWindow) {
if (focusedWindow) {
// 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() {
WindowManager.openBrowserViewDevTools();
},
}];
if (process.platform !== 'darwin' && process.platform !== 'win32') {
viewSubMenu.push(separatorItem);
viewSubMenu.push({
label: 'Toggle Dark Mode',
click() {
config.toggleDarkModeManually();
},
});
}
template.push({
label: '&View',
submenu: viewSubMenu,
});
template.push({
label: '&History',
submenu: [{
label: 'Back',
accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Alt+Left',
click: () => {
const focused = webContents.getFocusedWebContents();
if (focused.canGoBack()) {
focused.goBack();
}
},
}, {
label: 'Forward',
accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Alt+Right',
click: () => {
const focused = webContents.getFocusedWebContents();
if (focused.canGoForward()) {
focused.goForward();
}
},
}],
});
const teams = config.data.teams || [];
const windowMenu = {
label: '&Window',
submenu: [{
role: 'minimize',
// empty string removes shortcut on Windows; null will default by OS
accelerator: process.platform === 'win32' ? '' : null,
}, {
role: 'close',
accelerator: 'CmdOrCtrl+W',
}, separatorItem, ...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team, i) => {
return {
label: team.name,
accelerator: `CmdOrCtrl+${i + 1}`,
click() {
WindowManager.switchServer(team.name, true);
},
};
}), separatorItem, {
label: 'Select Next Server',
accelerator: 'Ctrl+Tab',
click() {
WindowManager.sendToRenderer(SELECT_NEXT_TAB);
},
enabled: (teams.length > 1),
}, {
label: 'Select Previous Server',
accelerator: 'Ctrl+Shift+Tab',
click() {
WindowManager.sendToRenderer(SELECT_PREVIOUS_TAB);
},
enabled: (teams.length > 1),
}],
};
template.push(windowMenu);
const submenu = [];
if (config.data.MenuhelpLink) {
submenu.push({
label: 'Learn More...',
click() {
shell.openExternal(config.data.helpLink);
},
});
submenu.push(separatorItem);
}
submenu.push({
// eslint-disable-next-line no-undef
label: `Version ${app.getVersion()} commit: ${__HASH_VERSION__}`,
enabled: false,
});
template.push({label: 'Hel&p', submenu});
return template;
}
function createMenu(mainWindow, config, isDev) {
return Menu.buildFromTemplate(createTemplate(mainWindow, config, isDev));
function createMenu(config) {
return Menu.buildFromTemplate(createTemplate(config));
}
export default {
createMenu,
createMenu,
};

View file

@ -3,59 +3,40 @@
// 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`;
const teams = config.teams;
const template = [
...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team, i) => {
return {
label: team.name,
click: () => {
showOrRestore(mainWindow);
mainWindow.webContents.send('switch-tab', i);
import * as WindowManager from '../windows/windowManager';
if (process.platform === 'darwin') {
app.dock.show();
mainWindow.focus();
}
function createTemplate(config) {
const teams = config.teams;
const template = [
...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team) => {
return {
label: team.name,
click: () => {
WindowManager.switchServer(team.name, true);
},
};
}), {
type: 'separator',
}, {
label: process.platform === 'darwin' ? 'Preferences...' : 'Settings',
click: () => {
WindowManager.showSettingsWindow();
},
}, {
type: 'separator',
}, {
role: 'quit',
},
};
}), {
type: 'separator',
}, {
label: process.platform === 'darwin' ? 'Preferences...' : 'Settings',
click: () => {
mainWindow.loadURL(settingsURL);
showOrRestore(mainWindow);
if (process.platform === 'darwin') {
app.dock.show();
mainWindow.focus();
}
},
}, {
type: 'separator',
}, {
role: 'quit',
},
];
return template;
];
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 {
createMenu,
createMenu,
};

View 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);
}
}

View 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;
}
}

View 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();
}

Some files were not shown because too many files have changed in this diff Show more