[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
209 changed files with 17905 additions and 13921 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Alert} from 'react-bootstrap';
const baseClassName = 'AutoSaveIndicator';
const leaveClassName = `${baseClassName}-Leave`;
const SAVING_STATE_SAVING = 'saving';
const SAVING_STATE_SAVED = 'saved';
const SAVING_STATE_ERROR = 'error';
const SAVING_STATE_DONE = 'done';
function getClassNameAndMessage(savingState, errorMessage) {
switch (savingState) {
case SAVING_STATE_SAVING:
return {className: baseClassName, message: 'Saving...'};
case SAVING_STATE_SAVED:
return {className: baseClassName, message: 'Saved'};
case SAVING_STATE_ERROR:
return {className: `${baseClassName}`, message: errorMessage};
case SAVING_STATE_DONE:
return {className: `${baseClassName} ${leaveClassName}`, message: 'Saved'};
default:
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
}
}
export default function AutoSaveIndicator(props) {
const {savingState, errorMessage, ...rest} = props;
const {className, message} = getClassNameAndMessage(savingState, errorMessage);
return (
<Alert
className={className}
{...rest}
bsStyle={savingState === 'error' ? 'danger' : 'info'}
>
{message}
</Alert>
);
}
AutoSaveIndicator.propTypes = {
savingState: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
};
Object.assign(AutoSaveIndicator, {
SAVING_STATE_SAVING,
SAVING_STATE_SAVED,
SAVING_STATE_ERROR,
SAVING_STATE_DONE,
});

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import {storiesOf} from '@storybook/react';
import {action} from '@storybook/addon-actions';
import {Button, ButtonToolbar} from 'react-bootstrap';
storiesOf('Button', module).
add('bsStyle', () => (
<ButtonToolbar>
<Button onClick={action('clicked default')}>{'Default'}</Button>
<Button
onClick={action('clicked primary')}
bsStyle='primary'
>{'Primary'}</Button>
<Button
onClick={action('clicked danger')}
bsStyle='danger'
>{'Danger'}</Button>
<Button
onClick={action('clicked link')}
bsStyle='link'
>{'Link'}</Button>
</ButtonToolbar>
));

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Modal} from 'react-bootstrap';
export default function DestructiveConfirmationModal(props) {
const {
title,
body,
acceptLabel,
cancelLabel,
onAccept,
onCancel,
...rest} = props;
return (
<Modal {...rest}>
<Modal.Header closeButton={true}>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
{body}
<Modal.Footer>
<Button
bsStyle='link'
onClick={onCancel}
>{cancelLabel}</Button>
<Button
bsStyle='danger'
onClick={onAccept}
>{acceptLabel}</Button>
</Modal.Footer>
</Modal>
);
}
DestructiveConfirmationModal.propTypes = {
title: PropTypes.string.isRequired,
body: PropTypes.node.isRequired,
acceptLabel: PropTypes.string.isRequired,
cancelLabel: PropTypes.string.isRequired,
onAccept: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,88 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
// ErrorCode: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
import React from 'react';
import PropTypes from 'prop-types';
import {Grid, Row, Col} from 'react-bootstrap';
export default function ErrorView(props) {
const classNames = ['container', 'ErrorView'];
if (!props.active) {
classNames.push('ErrorView-hidden');
}
return (
<Grid
id={props.id}
bsClass={classNames.join(' ')}
>
<div className='ErrorView-table'>
<div className='ErrorView-cell'>
<Row>
<Col
xs={0}
sm={1}
md={1}
lg={2}
/>
<Col
xs={12}
sm={10}
md={10}
lg={8}
>
<h2>{`Cannot connect to ${props.appName}`}</h2>
<hr/>
<p>{`We're having trouble connecting to ${props.appName}. If refreshing this page (Ctrl+R or Command+R) does not work please verify that:`}</p>
<br/>
<ul className='ErrorView-bullets' >
<li>{'Your computer is connected to the internet.'}</li>
<li>{`The ${props.appName} URL `}
<a
//onClick={handleClick}
href={props.url}
target='_blank'
rel='noreferrer'
>
{props.url}
</a>{' is correct.'}</li>
<li>{'You can reach '}
<a
// onClick={handleClick}
href={props.url}
target='_blank'
rel='noreferrer'
>
{props.errorInfo.validatedURL}
</a>{' from a browser window.'}</li>
</ul>
<br/>
<div className='ErrorView-techInfo'>
{props.errorInfo}
</div>
</Col>
<Col
xs={0}
sm={1}
md={1}
lg={2}
/>
</Row>
</div>
</div>
</Grid>
);
}
ErrorView.propTypes = {
errorInfo: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string,
active: PropTypes.bool,
appName: PropTypes.string,
};

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {Row, Button} from 'react-bootstrap';
export default class ExtraBar extends React.PureComponent {
handleBack = () => {
if (this.props.goBack) {
this.props.goBack();
}
}
render() {
let barClass = 'clear-mode';
if (!this.props.show) {
barClass = 'hidden';
} else if (this.props.darkMode) {
barClass = 'dark-mode';
}
return (
<Row
id={'extra-bar'}
className={barClass}
>
<div
className={'container-fluid'}
onClick={this.handleBack}
>
<Button
bsStyle={'link'}
bsSize={'xsmall'}
>
<span className={'backIcon fa fa-1x fa-angle-left'}/>
<span className={'backLabel'}>
{'Back'}
</span>
</Button>
</div>
</Row>
);
}
}
ExtraBar.propTypes = {
darkMode: PropTypes.bool,
goBack: PropTypes.func,
show: PropTypes.bool,
};

View File

@@ -0,0 +1,93 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import useAnimationEnd from '../../hooks/useAnimationEnd.js';
import LoadingIcon from './LoadingIcon.jsx';
const LOADING_STATE = {
INITIALIZING: 'initializing', // animation graphics are hidden
LOADING: 'loading', // animation graphics fade in and animate
LOADED: 'loaded', // animation graphics fade out
COMPLETE: 'complete', // animation graphics are removed from the DOM
};
const ANIMATION_COMPLETION_DELAY = 500;
/**
* A function component for rendering the animated MM logo loading sequence
* @param {boolean} loading - Prop that indicates whether currently loading or not
* @param {boolean} darkMode - Prop that indicates if dark mode is enabled
* @param {function} onLoadingAnimationComplete - Callback function to update when internal loading animation is complete
*/
function LoadingAnimation({
loading = false,
darkMode = false,
onLoadAnimationComplete = null},
) {
const loadingIconContainerRef = React.useRef(null);
const [animationState, setAnimationState] = React.useState(LOADING_STATE.INITIALIZING);
const [loadingAnimationComplete, setLoadingAnimationComplete] = React.useState(false);
React.useEffect(() => {
if (loading) {
setAnimationState(LOADING_STATE.LOADING);
setLoadingAnimationComplete(false);
}
// in order for the logo animation to fully complete before fading out, the LOADED state is not set until
// both the external loaded prop changes back to false and the internal loading animation is complete
if (!loading && loadingAnimationComplete) {
setAnimationState(LOADING_STATE.LOADED);
}
}, [loading]);
React.useEffect(() => {
// in order for the logo animation to fully complete before fading out, the LOADED state is not set until
// both the external loaded prop goes back to false and the internal loading animation is complete
if (!loading && loadingAnimationComplete) {
setAnimationState(LOADING_STATE.LOADED);
}
}, [loadingAnimationComplete]);
// listen for end of the css logo animation sequence
useAnimationEnd(loadingIconContainerRef, () => {
setTimeout(() => {
setLoadingAnimationComplete(true);
}, ANIMATION_COMPLETION_DELAY);
}, 'LoadingAnimation__compass-shrink');
// listen for end of final css logo fade/shrink animation sequence
useAnimationEnd(loadingIconContainerRef, () => {
if (onLoadAnimationComplete) {
onLoadAnimationComplete();
}
setAnimationState(LOADING_STATE.COMPLETE);
}, 'LoadingAnimation__shrink');
return (
<div
ref={loadingIconContainerRef}
className={classNames('LoadingAnimation', {
'LoadingAnimation--darkMode': darkMode,
'LoadingAnimation--spinning': animationState !== LOADING_STATE.INITIALIZING && animationState !== LOADING_STATE.COMPLETE,
'LoadingAnimation--loading': animationState === LOADING_STATE.LOADING && animationState !== LOADING_STATE.COMPLETE,
'LoadingAnimation--loaded': animationState === LOADING_STATE.LOADED && animationState !== LOADING_STATE.COMPLETE,
})}
>
<LoadingIcon/>
</div>
);
}
LoadingAnimation.propTypes = {
loading: PropTypes.bool,
darkMode: PropTypes.bool,
onLoadAnimationComplete: PropTypes.func,
};
export default LoadingAnimation;

View File

@@ -0,0 +1,197 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
/**
* A function component for inlining SVG code for animation logo loader
*/
function LoadingAnimation() {
return (
<svg
width='104'
height='104'
viewBox='0 0 104 104'
xmlns='http://www.w3.org/2000/svg'
>
<defs>
<linearGradient
id='LoadingAnimation__spinner-gradient'
x1='0%'
y1='72px'
x2='0%'
y2='32px'
gradientUnits='userSpaceOnUse'
>
<stop
offset='0'
className='LoadingAnimation__spinner-gradient-color'
stopOpacity='1'
/>
<stop
offset='1'
className='LoadingAnimation__spinner-gradient-color'
stopOpacity='0'
/>
</linearGradient>
<mask id='LoadingAnimation__base-wipe-mask'>
<rect
x='0'
y='0'
width='104'
height='104'
fill='white'
/>
<g className='LoadingAnimation__compass-base-mask-container'>
<circle
className='LoadingAnimation__compass-base-mask'
r='27'
cx='52'
cy='52'
fill='white'
stroke='black'
strokeWidth='54'
/>
</g>
</mask>
<mask id='LoadingAnimation__base-mask'>
<rect
x='0'
y='0'
width='104'
height='104'
fill='white'
/>
<circle
r='37'
cx='54'
cy='46'
fill='black'
/>
<g className='LoadingAnimation__compass-needle-behind-mask'>
<g transform='translate(54,46)'>
<g transform='translate(-29, -61.3)'>
<path
d='M38.5984 0C45.476 1.07762 51.9794 3.28918 57.9108 6.43722V61.1566C57.9108 77.1373 44.9364 90.1119 28.9554 90.1119C12.9744 90.1119 0 77.1373 0 61.1566C0 55.3848 1.69443 50.0063 4.60763 45.4861L38.5984 0Z'
fill='black'
/>
</g>
</g>
</g>
<g className='LoadingAnimation__compass-needle-front-mask'>
<g transform='translate(54,46)'>
<g transform='translate(-29,-61.3)'>
<path
d='M38.5984 0C45.476 1.07762 51.9794 3.28918 57.9108 6.43722V61.1566C57.9108 77.1373 44.9364 90.1119 28.9554 90.1119C12.9744 90.1119 0 77.1373 0 61.1566C0 55.3848 1.69443 50.0063 4.60763 45.4861L38.5984 0Z'
fill='black'
/>
</g>
</g>
</g>
</mask>
<mask id='LoadingAnimation__spinner-left-half-mask'>
<rect
x='0'
y='0'
width='52'
height='104'
fill='white'
/>
<circle
className='LoadingAnimation__spinner-mask'
r='20'
cx='52'
cy='52'
fill='black'
/>
</mask>
<mask id='LoadingAnimation__spinner-right-half-mask'>
<rect
x='52'
y='0'
width='52'
height='104'
fill='white'
/>
<circle
className='LoadingAnimation__spinner-mask'
r='20'
cx='52'
cy='52'
fill='black'
/>
</mask>
<mask id='LoadingAnimation__spinner-wipe-mask'>
<rect
x='0'
y='0'
width='104'
height='104'
fill='white'
/>
<g className='LoadingAnimation__spinner-mask-container'>
<circle
className='LoadingAnimation__spinner-mask'
r='27'
cx='52'
cy='52'
fill='black'
stroke='white'
strokeWidth='54'
/>
</g>
</mask>
</defs>
<g
className='LoadingAnimation__spinner-container'
mask='url(#LoadingAnimation__spinner-wipe-mask)'
>
<g className='LoadingAnimation__spinner'>
<circle
r='25'
cx='52'
cy='52'
fill='currentColor'
mask='url(#LoadingAnimation__spinner-left-half-mask)'
/>
<circle
r='25'
cx='52'
cy='52'
fill='url(#LoadingAnimation__spinner-gradient)'
mask='url(#LoadingAnimation__spinner-right-half-mask)'
/>
</g>
</g>
<g className='LoadingAnimation__compass'>
<g
className='LoadingAnimation__compass-base-container'
mask='url(#LoadingAnimation__base-wipe-mask)'
>
<circle
className='LoadingAnimation__compass-base'
r='52'
cx='52'
cy='52'
fill='currentColor'
mask='url(#LoadingAnimation__base-mask)'
/>
</g>
<g className='LoadingAnimation__compass-needle-container'>
<g className='LoadingAnimation__compass-needle'>
<g transform='translate(54,46)'>
<g transform='translate(-15,-42)'>
<path
d='M29.9539 1.4977C29.9539 0.670968 29.2827 0 28.4562 0C27.9597 0 27.5192 0.242028 27.2468 0.614415C27.216 0.656555 27.1873 0.700359 27.1609 0.745666L3.66519 32.1191C1.38202 34.7479 0 38.1803 0 41.9355C0 50.207 6.70541 56.9124 14.977 56.9124C23.2485 56.9124 29.9539 50.207 29.9539 41.9355L29.9539 41.9013V1.50252C29.9539 1.50091 29.9539 1.49931 29.9539 1.4977Z'
fill='currentColor'
/>
</g>
</g>
</g>
</g>
</g>
</svg>
);
}
export default LoadingAnimation;

View File

@@ -0,0 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './LoadingAnimation.jsx';

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import useTransitionEnd from '../hooks/useTransitionEnd.js';
import LoadingAnimation from './LoadingAnimation';
/**
* A function component for rendering the desktop app loading screen
* @param {boolean} loading - Prop that indicates whether currently loading or not
* @param {boolean} darkMode - Prop that indicates if dark mode is enabled
* @param {() => void} onFadeOutComplete - Function to call when the loading animation is completely finished
*/
function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = () => null}) {
const loadingScreenRef = React.useRef(null);
const [loadingIsComplete, setLoadingIsComplete] = React.useState(true);
const [loadAnimationIsComplete, setLoadAnimationIsComplete] = React.useState(true);
const [fadeOutIsComplete, setFadeOutIsComplete] = React.useState(true);
React.useEffect(() => {
// reset internal state if loading restarts
if (loading) {
resetState();
} else {
setLoadingIsComplete(true);
}
}, [loading]);
function handleLoadAnimationComplete() {
setLoadAnimationIsComplete(true);
}
useTransitionEnd(loadingScreenRef, React.useCallback(() => {
setFadeOutIsComplete(true);
onFadeOutComplete();
}), ['opacity']);
function loadingInProgress() {
return !(loadingIsComplete && loadAnimationIsComplete && fadeOutIsComplete);
}
function resetState() {
setLoadingIsComplete(false);
setLoadAnimationIsComplete(false);
setFadeOutIsComplete(false);
}
const loadingScreen = (
<div
ref={loadingScreenRef}
className={classNames('LoadingScreen', {
'LoadingScreen--darkMode': darkMode,
'LoadingScreen--loaded': loadingIsComplete && loadAnimationIsComplete,
})}
>
<LoadingAnimation
loading={loading}
darkMode={darkMode}
onLoadAnimationComplete={handleLoadAnimationComplete}
/>
</div>
);
return loadingInProgress() ? loadingScreen : null;
}
LoadingScreen.propTypes = {
loading: PropTypes.bool,
darkMode: PropTypes.bool,
onFadeOutComplete: PropTypes.func,
};
export default LoadingScreen;

View File

@@ -0,0 +1,453 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import os from 'os';
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import {Grid, Row} from 'react-bootstrap';
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
import {ipcRenderer} from 'electron';
import {
FOCUS_BROWSERVIEW,
MAXIMIZE_CHANGE,
DARK_MODE_CHANGE,
HISTORY,
LOAD_RETRY,
LOAD_SUCCESS,
LOAD_FAILED,
SHOW_NEW_SERVER_MODAL,
SWITCH_SERVER,
WINDOW_CLOSE,
WINDOW_MINIMIZE,
WINDOW_RESTORE,
WINDOW_MAXIMIZE,
DOUBLE_CLICK_ON_WINDOW,
PLAY_SOUND,
MODAL_OPEN,
MODAL_CLOSE,
SET_SERVER_KEY,
UPDATE_MENTIONS,
TOGGLE_BACK_BUTTON,
SELECT_NEXT_TAB,
SELECT_PREVIOUS_TAB,
ADD_SERVER,
FOCUS_THREE_DOT_MENU,
} from 'common/communication';
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
import maximizeButton from '../../assets/titlebar/chrome-maximize.svg';
import minimizeButton from '../../assets/titlebar/chrome-minimize.svg';
import closeButton from '../../assets/titlebar/chrome-close.svg';
import {playSound} from '../notificationSounds';
import TabBar from './TabBar.jsx';
import ExtraBar from './ExtraBar.jsx';
import ErrorView from './ErrorView.jsx';
const LOADING = 1;
const DONE = 2;
const RETRY = -1;
const FAILED = 0;
const NOSERVERS = -2;
export default class MainPage extends React.PureComponent {
constructor(props) {
super(props);
this.topBar = React.createRef();
this.threeDotMenu = React.createRef();
this.state = {
key: this.props.teams.findIndex((team) => team.order === 0),
sessionsExpired: {},
unreadCounts: {},
mentionCounts: {},
targetURL: '',
maximized: false,
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: LOADING, extra: null}])),
darkMode: this.props.darkMode,
};
}
getTabStatus() {
if (this.props.teams.length) {
const tab = this.props.teams[this.state.key];
if (tab) {
const tabname = tab.name;
return this.state.tabStatus.get(tabname);
}
}
return {status: NOSERVERS};
}
componentDidMount() {
// set page on retry
ipcRenderer.on(LOAD_RETRY, (_, server, retry, err, loadUrl) => {
console.log(`${server}: failed to load ${err}, but retrying`);
const status = this.state.tabStatus;
const statusValue = {
status: RETRY,
extra: {
retry,
error: err,
url: loadUrl,
},
};
status.set(server, statusValue);
this.setState({tabStatus: status});
});
ipcRenderer.on(LOAD_SUCCESS, (_, server) => {
const status = this.state.tabStatus;
status.set(server, {status: DONE});
this.setState({tabStatus: status});
});
ipcRenderer.on(LOAD_FAILED, (_, server, err, loadUrl) => {
console.log(`${server}: failed to load ${err}`);
const status = this.state.tabStatus;
const statusValue = {
status: FAILED,
extra: {
error: err,
url: loadUrl,
},
};
status.set(server, statusValue);
this.setState({tabStatus: status});
});
ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => {
this.setState({darkMode});
});
// can't switch tabs sequentially for some reason...
ipcRenderer.on(SET_SERVER_KEY, (event, key) => {
const nextIndex = this.props.teams.findIndex((team) => team.order === key);
this.handleSetServerKey(nextIndex);
});
ipcRenderer.on(SELECT_NEXT_TAB, () => {
const currentOrder = this.props.teams[this.state.key].order;
const nextOrder = ((currentOrder + 1) % this.props.teams.length);
const nextIndex = this.props.teams.findIndex((team) => team.order === nextOrder);
const team = this.props.teams[nextIndex];
this.handleSelect(team.name, nextIndex);
});
ipcRenderer.on(SELECT_PREVIOUS_TAB, () => {
const currentOrder = this.props.teams[this.state.key].order;
// js modulo operator returns a negative number if result is negative, so we have to ensure it's positive
const nextOrder = ((this.props.teams.length + (currentOrder - 1)) % this.props.teams.length);
const nextIndex = this.props.teams.findIndex((team) => team.order === nextOrder);
const team = this.props.teams[nextIndex];
this.handleSelect(team.name, nextIndex);
});
ipcRenderer.on(MAXIMIZE_CHANGE, this.handleMaximizeState);
ipcRenderer.on('enter-full-screen', () => this.handleFullScreenState(true));
ipcRenderer.on('leave-full-screen', () => this.handleFullScreenState(false));
ipcRenderer.on(ADD_SERVER, () => {
this.addServer();
});
ipcRenderer.on(PLAY_SOUND, (_event, soundName) => {
playSound(soundName);
});
ipcRenderer.on(MODAL_OPEN, () => {
this.setState({modalOpen: true});
});
ipcRenderer.on(MODAL_CLOSE, () => {
this.setState({modalOpen: false});
});
ipcRenderer.on(TOGGLE_BACK_BUTTON, (event, showExtraBar) => {
this.setState({showExtraBar});
});
ipcRenderer.on(UPDATE_MENTIONS, (_event, team, mentions, unreads, isExpired) => {
const key = this.props.teams.findIndex((server) => server.name === team);
const {unreadCounts, mentionCounts, sessionsExpired} = this.state;
const newMentionCounts = {...mentionCounts};
newMentionCounts[key] = mentions || 0;
const newUnreads = {...unreadCounts};
newUnreads[key] = unreads || false;
const expired = {...sessionsExpired};
expired[key] = isExpired || false;
this.setState({unreadCounts: newUnreads, mentionCounts: newMentionCounts, sessionsExpired: expired});
});
if (process.platform !== 'darwin') {
ipcRenderer.on(FOCUS_THREE_DOT_MENU, () => {
if (this.threeDotMenu.current) {
this.threeDotMenu.current.focus();
}
});
}
}
handleMaximizeState = (_, maximized) => {
this.setState({maximized});
}
handleFullScreenState = (isFullScreen) => {
this.setState({fullScreen: isFullScreen});
}
handleSetServerKey = (key) => {
const newKey = (this.props.teams.length + key) % this.props.teams.length;
this.setState({key: newKey});
}
handleSelect = (name, key) => {
ipcRenderer.send(SWITCH_SERVER, name);
this.handleSetServerKey(key);
}
handleDragAndDrop = async (dropResult) => {
const {removedIndex, addedIndex} = dropResult;
if (removedIndex !== addedIndex) {
const teamIndex = await this.props.moveTabs(removedIndex, addedIndex < this.props.teams.length ? addedIndex : this.props.teams.length - 1);
const name = this.props.teams[teamIndex].name;
this.handleSelect(name, teamIndex);
}
}
handleClose = (e) => {
e.stopPropagation(); // since it is our button, the event goes into MainPage's onclick event, getting focus back.
ipcRenderer.send(WINDOW_CLOSE);
}
handleMinimize = (e) => {
e.stopPropagation();
ipcRenderer.send(WINDOW_MINIMIZE);
}
handleMaximize = (e) => {
e.stopPropagation();
ipcRenderer.send(WINDOW_MAXIMIZE);
}
handleRestore = () => {
ipcRenderer.send(WINDOW_RESTORE);
}
openMenu = () => {
if (process.platform !== 'darwin') {
this.threeDotMenu.current.blur();
}
this.props.openMenu();
}
handleDoubleClick = () => {
ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW);
}
addServer = () => {
ipcRenderer.send(SHOW_NEW_SERVER_MODAL);
}
focusOnWebView = () => {
ipcRenderer.send(FOCUS_BROWSERVIEW);
}
setInputRef = (ref) => {
this.inputRef = ref;
}
render() {
const tabsRow = (
<TabBar
id='tabBar'
isDarkMode={this.state.darkMode}
teams={this.props.teams}
sessionsExpired={this.state.sessionsExpired}
unreadCounts={this.state.unreadCounts}
mentionCounts={this.state.mentionCounts}
activeKey={this.state.key}
onSelect={this.handleSelect}
onAddServer={this.addServer}
showAddServerButton={this.props.showAddServerButton}
onDrop={this.handleDragAndDrop}
tabsDisabled={this.state.modalOpen}
/>
);
let topBarClassName = 'topBar';
if (process.platform === 'darwin') {
topBarClassName += ' macOS';
}
if (this.state.darkMode) {
topBarClassName += ' darkMode';
}
if (this.state.fullScreen) {
topBarClassName += ' fullScreen';
}
let maxButton;
if (this.state.maximized) {
maxButton = (
<div
className='button restore-button'
onClick={this.handleRestore}
>
<img src={restoreButton}/>
</div>
);
} else {
maxButton = (
<div
className='button max-button'
onClick={this.handleMaximize}
>
<img src={maximizeButton}/>
</div>
);
}
let overlayGradient;
if (process.platform !== 'darwin') {
overlayGradient = (
<span className='overlay-gradient'/>
);
}
let titleBarButtons;
if (os.platform() === 'win32' && os.release().startsWith('10')) {
titleBarButtons = (
<span className='title-bar-btns'>
<div
className='button min-button'
onClick={this.handleMinimize}
>
<img src={minimizeButton}/>
</div>
{maxButton}
<div
className='button close-button'
onClick={this.handleClose}
>
<img src={closeButton}/>
</div>
</span>
);
}
const topRow = (
<Row
className={topBarClassName}
onDoubleClick={this.handleDoubleClick}
>
<div
ref={this.topBar}
className={`topBar-bg${this.state.unfocused ? ' unfocused' : ''}`}
>
<button
className='three-dot-menu'
onClick={this.openMenu}
tabIndex={0}
ref={this.threeDotMenu}
aria-label='Context menu'
>
<DotsVerticalIcon/>
</button>
{tabsRow}
{overlayGradient}
{titleBarButtons}
</div>
</Row>
);
const views = () => {
let component;
const tabStatus = this.getTabStatus();
if (!tabStatus) {
const tab = this.props.teams[this.state.key];
if (tab) {
console.error(`Not tabStatus for ${this.props.teams[this.state.key].name}`);
} else {
console.error('No tab status, tab doesn\'t exist anymore');
}
return null;
}
switch (tabStatus.status) {
case NOSERVERS: // TODO: substitute with https://mattermost.atlassian.net/browse/MM-25003
component = (
<ErrorView
id={'NoServers'}
className='errorView'
errorInfo={'No Servers configured'}
url={tabStatus.extra ? tabStatus.extra.url : ''}
active={true}
retry={null}
appName={this.props.appName}
/>);
break;
case FAILED:
component = (
<ErrorView
id={this.state.key + '-fail'}
className='errorView'
errorInfo={tabStatus.extra ? tabStatus.extra.error : null}
url={tabStatus.extra ? tabStatus.extra.url : ''}
active={true}
appName={this.props.appName}
/>);
break;
case LOADING:
case RETRY:
case DONE:
component = null;
}
return component;
};
const viewsRow = (
<Fragment>
<ExtraBar
darkMode={this.state.darkMode}
show={this.state.showExtraBar}
goBack={() => {
ipcRenderer.send(HISTORY, -1);
}}
/>
<Row>
{views()}
</Row>
</Fragment>);
return (
<div
className='MainPage'
onClick={this.focusOnWebView}
>
<Grid fluid={true}>
{topRow}
{viewsRow}
</Grid>
</div>
);
}
}
MainPage.propTypes = {
teams: PropTypes.array.isRequired,
showAddServerButton: PropTypes.bool.isRequired,
moveTabs: PropTypes.func.isRequired,
openMenu: PropTypes.func.isRequired,
darkMode: PropTypes.bool.isRequired,
appName: PropTypes.string.isRequired,
};

View File

@@ -0,0 +1,244 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
import urlUtils from 'common/utils/url';
export default class NewTeamModal extends React.PureComponent {
static defaultProps = {
restoreFocus: true,
};
constructor(props) {
super(props);
this.wasShown = false;
this.state = {
teamName: '',
teamUrl: '',
teamOrder: props.currentOrder || 0,
saveStarted: false,
};
}
initializeOnShow() {
this.setState({
teamName: this.props.team ? this.props.team.name : '',
teamUrl: this.props.team ? this.props.team.url : '',
teamIndex: this.props.team ? this.props.team.index : false,
teamOrder: this.props.team ? this.props.team.order : (this.props.currentOrder || 0),
saveStarted: false,
});
}
getTeamNameValidationError() {
if (!this.state.saveStarted) {
return null;
}
return this.state.teamName.length > 0 ? null : 'Name is required.';
}
getTeamNameValidationState() {
return this.getTeamNameValidationError() === null ? null : 'error';
}
handleTeamNameChange = (e) => {
this.setState({
teamName: e.target.value,
});
}
getTeamUrlValidationError() {
if (!this.state.saveStarted) {
return null;
}
if (this.state.teamUrl.length === 0) {
return 'URL is required.';
}
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) {
return 'URL should start with http:// or https://.';
}
if (!urlUtils.isValidURL(this.state.teamUrl.trim())) {
return 'URL is not formatted correctly.';
}
return null;
}
getTeamUrlValidationState() {
return this.getTeamUrlValidationError() === null ? null : 'error';
}
handleTeamUrlChange = (e) => {
this.setState({
teamUrl: e.target.value,
});
}
getError() {
const nameError = this.getTeamNameValidationError();
const urlError = this.getTeamUrlValidationError();
if (nameError && urlError) {
return 'Name and URL are required.';
} else if (nameError) {
return nameError;
} else if (urlError) {
return urlError;
}
return null;
}
validateForm() {
return this.getTeamNameValidationState() === null &&
this.getTeamUrlValidationState() === null;
}
save = () => {
this.setState({
saveStarted: true,
}, () => {
if (this.validateForm()) {
this.props.onSave({
url: this.state.teamUrl,
name: this.state.teamName,
index: this.state.teamIndex,
order: this.state.teamOrder,
});
}
});
}
getSaveButtonLabel() {
if (this.props.editMode) {
return 'Save';
}
return 'Add';
}
getModalTitle() {
if (this.props.editMode) {
return 'Edit Server';
}
return 'Add Server';
}
render() {
if (this.wasShown !== this.props.show && this.props.show) {
this.initializeOnShow();
}
this.wasShown = this.props.show;
return (
<Modal
bsClass='modal'
className='NewTeamModal'
show={this.props.show}
id='newServerModal'
enforceFocus={true}
onEntered={() => this.teamNameInputRef.focus()}
onHide={this.props.onClose}
restoreFocus={this.props.restoreFocus}
onKeyDown={(e) => {
switch (e.key) {
case 'Enter':
this.save();
// The add button from behind this might still be focused
e.preventDefault();
e.stopPropagation();
break;
case 'Escape':
this.props.onClose();
break;
}
}}
>
<Modal.Header>
<Modal.Title>{this.getModalTitle()}</Modal.Title>
</Modal.Header>
<Modal.Body>
<form>
<FormGroup
validationState={this.getTeamNameValidationState()}
>
<ControlLabel>{'Server Display Name'}</ControlLabel>
<FormControl
id='teamNameInput'
type='text'
value={this.state.teamName}
placeholder='Server Name'
onChange={this.handleTeamNameChange}
inputRef={(ref) => {
this.teamNameInputRef = ref;
if (this.props.setInputRef) {
this.props.setInputRef(ref);
}
}}
onClick={(e) => {
e.stopPropagation();
}}
autoFocus={true}
/>
<FormControl.Feedback/>
<HelpBlock>{'The name of the server displayed on your desktop app tab bar.'}</HelpBlock>
</FormGroup>
<FormGroup
className='NewTeamModal-noBottomSpace'
validationState={this.getTeamUrlValidationState()}
>
<ControlLabel>{'Server URL'}</ControlLabel>
<FormControl
id='teamUrlInput'
type='text'
value={this.state.teamUrl}
placeholder='https://example.com'
onChange={this.handleTeamUrlChange}
onClick={(e) => {
e.stopPropagation();
}}
/>
<FormControl.Feedback/>
<HelpBlock className='NewTeamModal-noBottomSpace'>{'The URL of your Mattermost server. Must start with http:// or https://.'}</HelpBlock>
</FormGroup>
</form>
</Modal.Body>
<Modal.Footer>
<div
className='pull-left modal-error'
>
{this.getError()}
</div>
<Button
id='cancelNewServerModal'
onClick={this.props.onClose}
>{'Cancel'}</Button>
<Button
id='saveNewServerModal'
onClick={this.save}
disabled={!this.validateForm()}
bsStyle='primary'
>{this.getSaveButtonLabel()}</Button>
</Modal.Footer>
</Modal>
);
}
}
NewTeamModal.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
team: PropTypes.object,
editMode: PropTypes.bool,
show: PropTypes.bool,
restoreFocus: PropTypes.bool,
currentOrder: PropTypes.number,
setInputRef: PropTypes.func,
};

View File

@@ -0,0 +1,36 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Modal} from 'react-bootstrap';
import DestructiveConfirmationModal from './DestructiveConfirmModal.jsx';
export default function RemoveServerModal(props) {
const {serverName, ...rest} = props;
return (
<DestructiveConfirmationModal
{...rest}
title='Remove Server'
acceptLabel='Remove'
cancelLabel='Cancel'
body={(
<Modal.Body>
<p>
{'This will remove the server from your Desktop App but will not delete any of its data' +
' - you can add the server back to the app at any time.'}
</p>
<p>
{'Confirm you wish to remove the '}<strong>{serverName}</strong>{' server?'}
</p>
</Modal.Body>
)}
/>
);
}
RemoveServerModal.propTypes = {
serverName: PropTypes.string.isRequired,
};

View File

@@ -0,0 +1,723 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import 'renderer/css/settings.css';
import React from 'react';
import PropTypes from 'prop-types';
import {Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row, Button} from 'react-bootstrap';
import {ipcRenderer} from 'electron';
import {debounce} from 'underscore';
import {GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, DOUBLE_CLICK_ON_WINDOW, GET_DOWNLOAD_LOCATION, SWITCH_SERVER, ADD_SERVER, RELOAD_CONFIGURATION} from 'common/communication';
import TeamList from './TeamList.jsx';
import AutoSaveIndicator from './AutoSaveIndicator.jsx';
const CONFIG_TYPE_SERVERS = 'servers';
const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
function backToIndex(serverName) {
ipcRenderer.send(SWITCH_SERVER, serverName);
window.close();
}
export default class SettingsPage extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
ready: false,
teams: [],
showAddTeamForm: false,
savingState: {
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
servers: AutoSaveIndicator.SAVING_STATE_DONE,
},
userOpenedDownloadDialog: false,
};
this.getConfig();
this.trayIconThemeRef = React.createRef();
this.downloadLocationRef = React.createRef();
this.showTrayIconRef = React.createRef();
this.autostartRef = React.createRef();
this.minimizeToTrayRef = React.createRef();
this.flashWindowRef = React.createRef();
this.bounceIconRef = React.createRef();
this.showUnreadBadgeRef = React.createRef();
this.useSpellCheckerRef = React.createRef();
this.enableHardwareAccelerationRef = React.createRef();
this.saveQueue = [];
}
componentDidMount() {
ipcRenderer.on(ADD_SERVER, () => {
this.setState({
showAddTeamForm: true,
});
});
ipcRenderer.on(RELOAD_CONFIGURATION, () => {
this.updateSaveState();
this.getConfig();
});
}
getConfig = () => {
ipcRenderer.invoke(GET_LOCAL_CONFIGURATION).then((config) => {
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config)});
});
}
convertConfigDataToState = (configData, currentState = {}) => {
const newState = Object.assign({}, configData);
newState.showAddTeamForm = currentState.showAddTeamForm || false;
newState.trayWasVisible = currentState.trayWasVisible || false;
if (newState.teams.length === 0 && currentState.firstRun !== false) {
newState.firstRun = false;
newState.showAddTeamForm = true;
}
newState.savingState = currentState.savingState || {
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
servers: AutoSaveIndicator.SAVING_STATE_DONE,
};
return newState;
}
saveSetting = (configType, {key, data}) => {
this.saveQueue.push({
configType,
key,
data,
});
this.updateSaveState();
this.processSaveQueue();
}
processSaveQueue = debounce(() => {
ipcRenderer.send(UPDATE_CONFIGURATION, this.saveQueue.splice(0, this.saveQueue.length));
}, 500);
updateSaveState = () => {
let queuedUpdateCounts = {
[CONFIG_TYPE_SERVERS]: 0,
[CONFIG_TYPE_APP_OPTIONS]: 0,
};
queuedUpdateCounts = this.saveQueue.reduce((updateCounts, {configType}) => {
updateCounts[configType]++;
return updateCounts;
}, queuedUpdateCounts);
const savingState = Object.assign({}, this.state.savingState);
Object.entries(queuedUpdateCounts).forEach(([configType, count]) => {
if (count > 0) {
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVING;
} else if (count === 0 && savingState[configType] === AutoSaveIndicator.SAVING_STATE_SAVING) {
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVED;
this.resetSaveState(configType);
}
});
this.setState({savingState});
}
resetSaveState = debounce((configType) => {
if (this.state.savingState[configType] !== AutoSaveIndicator.SAVING_STATE_SAVING) {
const savingState = Object.assign({}, this.state.savingState);
savingState[configType] = AutoSaveIndicator.SAVING_STATE_DONE;
this.setState({savingState});
}
}, 2000);
handleTeamsChange = (teams) => {
setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
this.setState({
showAddTeamForm: false,
teams,
});
if (teams.length === 0) {
this.setState({showAddTeamForm: true});
}
}
handleChangeShowTrayIcon = () => {
const shouldShowTrayIcon = !this.showTrayIconRef.current.props.checked;
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
this.setState({
showTrayIcon: shouldShowTrayIcon,
});
if (process.platform === 'darwin' && !shouldShowTrayIcon) {
this.setState({
minimizeToTray: false,
});
}
}
handleChangeTrayIconTheme = (theme) => {
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'trayIconTheme', data: theme});
this.setState({
trayIconTheme: theme,
});
}
handleChangeAutoStart = () => {
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.autostartRef.current.props.checked});
this.setState({
autostart: !this.autostartRef.current.props.checked,
});
}
handleChangeMinimizeToTray = () => {
const shouldMinimizeToTray = this.state.showTrayIcon && !this.minimizeToTrayRef.current.props.checked;
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'minimizeToTray', data: shouldMinimizeToTray});
this.setState({
minimizeToTray: shouldMinimizeToTray,
});
}
toggleShowTeamForm = () => {
this.setState({
showAddTeamForm: !this.state.showAddTeamForm,
});
document.activeElement.blur();
}
setShowTeamFormVisibility = (val) => {
this.setState({
showAddTeamForm: val,
});
}
handleFlashWindow = () => {
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
key: 'notifications',
data: {
...this.state.notifications,
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
},
});
this.setState({
notifications: {
...this.state.notifications,
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
},
});
}
handleBounceIcon = () => {
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
key: 'notifications',
data: {
...this.state.notifications,
bounceIcon: !this.bounceIconRef.current.props.checked,
},
});
this.setState({
notifications: {
...this.state.notifications,
bounceIcon: !this.bounceIconRef.current.props.checked,
},
});
}
handleBounceIconType = (event) => {
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
key: 'notifications',
data: {
...this.state.notifications,
bounceIconType: event.target.value,
},
});
this.setState({
notifications: {
...this.state.notifications,
bounceIconType: event.target.value,
},
});
}
handleShowUnreadBadge = () => {
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.showUnreadBadgeRef.current.props.checked});
this.setState({
showUnreadBadge: !this.showUnreadBadgeRef.current.props.checked,
});
}
handleChangeUseSpellChecker = () => {
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.useSpellCheckerRef.current.props.checked});
this.setState({
useSpellChecker: !this.useSpellCheckerRef.current.props.checked,
});
}
handleChangeEnableHardwareAcceleration = () => {
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.enableHardwareAccelerationRef.current.props.checked});
this.setState({
enableHardwareAcceleration: !this.enableHardwareAccelerationRef.current.props.checked,
});
}
saveDownloadLocation = (location) => {
this.setState({
downloadLocation: location,
});
setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'downloadLocation', data: location});
}
handleChangeDownloadLocation = (e) => {
this.saveDownloadLocation(e.target.value);
}
selectDownloadLocation = () => {
if (!this.state.userOpenedDownloadDialog) {
ipcRenderer.invoke(GET_DOWNLOAD_LOCATION, `/Users/${process.env.USER || process.env.USERNAME}/Downloads`).then((result) => this.saveDownloadLocation(result));
this.setState({userOpenedDownloadDialog: true});
}
this.setState({userOpenedDownloadDialog: false});
}
updateTeam = (index, newData) => {
const teams = this.state.teams;
teams[index] = newData;
setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
this.setState({
teams,
});
}
addServer = (team) => {
const teams = this.state.teams;
teams.push(team);
setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
this.setState({
teams,
});
}
openMenu = () => {
// @eslint-ignore
this.threeDotMenu.current.blur();
this.props.openMenu();
}
handleDoubleClick = () => {
ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, 'settings');
}
render() {
const settingsPage = {
navbar: {
backgroundColor: '#fff',
position: 'relative',
},
close: {
textDecoration: 'none',
position: 'absolute',
right: '0',
top: '5px',
fontSize: '35px',
fontWeight: 'normal',
color: '#bbb',
},
heading: {
textAlign: 'center',
fontSize: '24px',
margin: '0',
padding: '1em 0',
},
sectionHeading: {
fontSize: '20px',
margin: '0',
padding: '1em 0',
float: 'left',
},
sectionHeadingLink: {
marginTop: '24px',
display: 'inline-block',
fontSize: '15px',
},
footer: {
padding: '0.4em 0',
},
downloadLocationInput: {
marginRight: '3px',
marginTop: '8px',
width: '320px',
height: '34px',
padding: '0 12px',
borderRadius: '4px',
border: '1px solid #ccc',
fontWeight: '500',
},
downloadLocationButton: {
marginBottom: '4px',
},
container: {
paddingBottom: '40px',
},
};
const teamsRow = (
<Row>
<Col md={12}>
<TeamList
teams={this.state.teams}
showAddTeamForm={this.state.showAddTeamForm}
toggleAddTeamForm={this.toggleShowTeamForm}
setAddTeamFormVisibility={this.setShowTeamFormVisibility}
onTeamsChange={this.handleTeamsChange}
updateTeam={this.updateTeam}
addServer={this.addServer}
allowTeamEdit={this.state.enableServerManagement}
onTeamClick={(name) => {
backToIndex(name);
}}
/>
</Col>
</Row>
);
const serversRow = (
<Row>
<Col
md={10}
xs={8}
>
<h2 style={settingsPage.sectionHeading}>{'Server Management'}</h2>
<div className='IndicatorContainer'>
<AutoSaveIndicator
id='serversSaveIndicator'
savingState={this.state.savingState.servers}
errorMessage={'Can\'t save your changes. Please try again.'}
/>
</div>
</Col>
<Col
md={2}
xs={4}
>
<p className='text-right'>
<a
style={settingsPage.sectionHeadingLink}
id='addNewServer'
href='#'
onClick={this.toggleShowTeamForm}
>{'+ Add New Server'}</a>
</p>
</Col>
</Row>
);
let srvMgmt;
if (this.state.enableServerManagement === true) {
srvMgmt = (
<div>
{serversRow}
{teamsRow}
<hr/>
</div>
);
}
const options = [];
// MacOS has an option in the Dock, to set the app to autostart, so we choose to not support this option for OSX
if (process.platform === 'win32' || process.platform === 'linux') {
options.push(
<Checkbox
key='inputAutoStart'
id='inputAutoStart'
ref={this.autostartRef}
checked={this.state.autostart}
onChange={this.handleChangeAutoStart}
>
{'Start app on login'}
<HelpBlock>
{'If enabled, the app starts automatically when you log in to your machine.'}
</HelpBlock>
</Checkbox>);
}
options.push(
<Checkbox
key='inputSpellChecker'
id='inputSpellChecker'
ref={this.useSpellCheckerRef}
checked={this.state.useSpellChecker}
onChange={this.handleChangeUseSpellChecker}
>
{'Check spelling'}
<HelpBlock>
{'Highlight misspelled words in your messages.'}
{' Available for English, French, German, Portuguese, Spanish, and Dutch.'}
</HelpBlock>
</Checkbox>);
if (process.platform === 'darwin' || process.platform === 'win32') {
const TASKBAR = process.platform === 'win32' ? 'taskbar' : 'Dock';
options.push(
<Checkbox
key='inputShowUnreadBadge'
id='inputShowUnreadBadge'
ref={this.showUnreadBadgeRef}
checked={this.state.showUnreadBadge}
onChange={this.handleShowUnreadBadge}
>
{`Show red badge on ${TASKBAR} icon to indicate unread messages`}
<HelpBlock>
{`Regardless of this setting, mentions are always indicated with a red badge and item count on the ${TASKBAR} icon.`}
</HelpBlock>
</Checkbox>);
}
if (process.platform === 'win32' || process.platform === 'linux') {
options.push(
<Checkbox
key='flashWindow'
id='inputflashWindow'
ref={this.flashWindowRef}
checked={!this.state.notifications || this.state.notifications.flashWindow === 2}
onChange={this.handleFlashWindow}
>
{'Flash app window and taskbar icon when a new message is received'}
<HelpBlock>
{'If enabled, app window and taskbar icon flash for a few seconds when a new message is received.'}
</HelpBlock>
</Checkbox>);
}
if (process.platform === 'darwin') {
options.push(
<FormGroup
key='OptionsForm'
>
<Checkbox
inline={true}
key='bounceIcon'
id='inputBounceIcon'
ref={this.bounceIconRef}
checked={this.state.notifications ? this.state.notifications.bounceIcon : false}
onChange={this.handleBounceIcon}
style={{marginRight: '10px'}}
>
{'Bounce the Dock icon'}
</Checkbox>
<Radio
inline={true}
name='bounceIconType'
value='informational'
disabled={!this.state.notifications || !this.state.notifications.bounceIcon}
defaultChecked={
!this.state.notifications ||
!this.state.notifications.bounceIconType ||
this.state.notifications.bounceIconType === 'informational'
}
onChange={this.handleBounceIconType}
>
{'once'}
</Radio>
{' '}
<Radio
inline={true}
name='bounceIconType'
value='critical'
disabled={!this.state.notifications || !this.state.notifications.bounceIcon}
defaultChecked={this.state.notifications && this.state.notifications.bounceIconType === 'critical'}
onChange={this.handleBounceIconType}
>
{'until I open the app'}
</Radio>
<HelpBlock
style={{marginLeft: '20px'}}
>
{'If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.'}
</HelpBlock>
</FormGroup>,
);
}
if (process.platform === 'darwin' || process.platform === 'linux') {
options.push(
<Checkbox
key='inputShowTrayIcon'
id='inputShowTrayIcon'
ref={this.showTrayIconRef}
checked={this.state.showTrayIcon}
onChange={this.handleChangeShowTrayIcon}
>
{process.platform === 'darwin' ? `Show ${this.state.appName} icon in the menu bar` : 'Show icon in the notification area'}
<HelpBlock>
{'Setting takes effect after restarting the app.'}
</HelpBlock>
</Checkbox>);
}
if (process.platform === 'linux') {
options.push(
<FormGroup
key='trayIconTheme'
ref={this.trayIconThemeRef}
style={{marginLeft: '20px'}}
>
{'Icon theme: '}
<Radio
inline={true}
name='trayIconTheme'
value='light'
defaultChecked={this.state.trayIconTheme === 'light' || !this.state.trayIconTheme}
onChange={(event) => this.handleChangeTrayIconTheme('light', event)}
>
{'Light'}
</Radio>
{' '}
<Radio
inline={true}
name='trayIconTheme'
value='dark'
defaultChecked={this.state.trayIconTheme === 'dark'}
onChange={(event) => this.handleChangeTrayIconTheme('dark', event)}
>{'Dark'}</Radio>
</FormGroup>,
);
}
if (process.platform === 'linux') {
options.push(
<Checkbox
key='inputMinimizeToTray'
id='inputMinimizeToTray'
ref={this.minimizeToTrayRef}
disabled={!this.state.showTrayIcon || !this.state.trayWasVisible}
checked={this.state.minimizeToTray}
onChange={this.handleChangeMinimizeToTray}
>
{'Leave app running in notification area when application window is closed'}
<HelpBlock>
{'If enabled, the app stays running in the notification area after app window is closed.'}
{this.state.trayWasVisible || !this.state.showTrayIcon ? '' : ' Setting takes effect after restarting the app.'}
</HelpBlock>
</Checkbox>);
}
options.push(
<Checkbox
key='inputEnableHardwareAcceleration'
id='inputEnableHardwareAcceleration'
ref={this.enableHardwareAccelerationRef}
checked={this.state.enableHardwareAcceleration}
onChange={this.handleChangeEnableHardwareAcceleration}
>
{'Use GPU hardware acceleration'}
<HelpBlock>
{'If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.'}
{' Setting takes effect after restarting the app.'}
</HelpBlock>
</Checkbox>,
);
options.push(
<div style={settingsPage.container}>
<hr/>
<div>{'Download Location'}</div>
<input
disabled={true}
style={settingsPage.downloadLocationInput}
key='inputDownloadLocation'
id='inputDownloadLocation'
ref={this.downloadLocationRef}
onChange={this.handleChangeDownloadLocation}
value={this.state.downloadLocation}
/>
<Button
style={settingsPage.downloadLocationButton}
id='saveDownloadLocation'
onClick={this.selectDownloadLocation}
>
<span>{'Change'}</span>
</Button>
<HelpBlock>
{'Specify the folder where files will download.'}
</HelpBlock>
</div>,
);
let optionsRow = null;
if (options.length > 0) {
optionsRow = (
<Row>
<Col md={12}>
<h2 style={settingsPage.sectionHeading}>{'App Options'}</h2>
<div className='IndicatorContainer'>
<AutoSaveIndicator
id='appOptionsSaveIndicator'
savingState={this.state.savingState.appOptions}
errorMessage={'Can\'t save your changes. Please try again.'}
/>
</div>
{ options.map((opt) => (
<FormGroup key={opt.key}>
{opt}
</FormGroup>
)) }
</Col>
</Row>
);
}
let waitForIpc;
if (this.state.ready) {
waitForIpc = (
<>
{srvMgmt}
{optionsRow}
</>
);
} else {
waitForIpc = (<p>{'Loading configuration...'}</p>);
}
return (
<div
className='container-fluid'
style={{
height: '100%',
}}
>
<div
style={{
overflowY: 'auto',
height: '100%',
margin: '0 -15px',
}}
>
<Navbar
className='navbar-fixed-top'
style={settingsPage.navbar}
>
<div style={{position: 'relative'}}>
<h1 style={settingsPage.heading}>{'Settings'}</h1>
</div>
</Navbar>
<Grid
className='settingsPage'
>
{waitForIpc}
</Grid>
</div>
</div>
);
}
}
SettingsPage.propTypes = {
openMenu: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,154 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import {ipcRenderer} from 'electron';
import PropTypes from 'prop-types';
import {Nav, NavItem} from 'react-bootstrap';
import {Container, Draggable} from 'react-smooth-dnd';
import PlusIcon from 'mdi-react/PlusIcon';
import {GET_CONFIGURATION} from 'common/communication';
export default class TabBar extends React.PureComponent { // need "this"
constructor(props) {
super(props);
this.state = {
hasGPOTeams: false,
};
}
componentDidMount() {
ipcRenderer.invoke(GET_CONFIGURATION).then((config) => {
this.setState({hasGPOTeams: config.registryTeams && config.registryTeams.length > 0});
});
}
render() {
const orderedTabs = this.props.teams.concat().sort((a, b) => a.order - b.order);
const tabs = orderedTabs.map((team) => {
const index = this.props.teams.indexOf(team);
const sessionExpired = this.props.sessionsExpired[index];
const hasUnreads = this.props.unreadCounts[index];
let mentionCount = 0;
if (this.props.mentionCounts[index] > 0) {
mentionCount = this.props.mentionCounts[index];
}
let badgeDiv;
if (sessionExpired) {
badgeDiv = (
<div className='TabBar-expired'/>
);
} else if (mentionCount !== 0) {
badgeDiv = (
<div className='TabBar-badge'>
{mentionCount}
</div>
);
} else if (hasUnreads) {
badgeDiv = (
<div className='TabBar-dot'/>
);
}
const id = `teamTabItem${index}`;
const navItem = () => (
<NavItem
key={index}
id={id}
eventKey={index}
draggable={false}
ref={id}
active={this.props.activeKey === index}
activeKey={this.props.activeKey}
onMouseDown={() => {
this.props.onSelect(team.name, index);
}}
onSelect={() => {
this.props.onSelect(team.name, index);
}}
title={team.name}
disabled={this.props.tabsDisabled}
>
<div className='TabBar-tabSeperator'>
<span>
{team.name}
</span>
{ badgeDiv }
</div>
</NavItem>
);
return (
<Draggable
key={id}
render={navItem}
className='teamTabItem'
/>);
});
if (this.props.showAddServerButton === true) {
tabs.push(
<NavItem
className='TabBar-addServerButton'
key='addServerButton'
id='addServerButton'
eventKey='addServerButton'
draggable={false}
title='Add new server'
activeKey={this.props.activeKey}
onSelect={() => {
this.props.onAddServer();
}}
disabled={this.props.tabsDisabled}
>
<div className='TabBar-tabSeperator'>
<PlusIcon size={20}/>
</div>
</NavItem>,
);
}
const navContainer = (ref) => (
<Nav
ref={ref}
className={`smooth-dnd-container TabBar${this.props.isDarkMode ? ' darkMode' : ''}`}
id={this.props.id}
bsStyle='tabs'
>
{ tabs }
</Nav>
);
return (
<Container
ref={this.container}
render={navContainer}
orientation='horizontal'
lockAxis={'x'}
onDrop={this.props.onDrop}
animationDuration={300}
shouldAcceptDrop={() => {
return !this.state.hasGPOTeams && !this.props.tabsDisabled;
}}
/>
);
}
}
TabBar.propTypes = {
activeKey: PropTypes.number,
id: PropTypes.string,
isDarkMode: PropTypes.bool,
onSelect: PropTypes.func,
teams: PropTypes.array,
sessionsExpired: PropTypes.object,
unreadCounts: PropTypes.object,
mentionCounts: PropTypes.object,
showAddServerButton: PropTypes.bool,
onAddServer: PropTypes.func,
onDrop: PropTypes.func,
tabsDisabled: PropTypes.bool,
};

View File

@@ -0,0 +1,187 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {ListGroup} from 'react-bootstrap';
import TeamListItem from './TeamListItem.jsx';
import NewTeamModal from './NewTeamModal.jsx';
import RemoveServerModal from './RemoveServerModal.jsx';
export default class TeamList extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showEditTeamForm: false,
indexToRemoveServer: -1,
team: {
url: '',
name: '',
index: false,
order: props.teams.length,
},
};
}
handleTeamRemove = (index) => {
console.log(index);
const teams = this.props.teams;
const removedOrder = this.props.teams[index].order;
teams.splice(index, 1);
teams.forEach((value) => {
if (value.order > removedOrder) {
value.order--;
}
});
this.props.onTeamsChange(teams);
}
handleTeamAdd = (team) => {
const teams = this.props.teams;
// check if team already exists and then change existing team or add new one
if ((typeof team.index !== 'undefined') && teams[team.index]) {
teams[team.index].name = team.name;
teams[team.index].url = team.url;
teams[team.index].order = team.order;
} else {
teams.push(team);
}
this.setState({
showEditTeamForm: false,
team: {
url: '',
name: '',
index: false,
order: teams.length,
},
});
this.props.onTeamsChange(teams);
}
openServerRemoveModal = (indexForServer) => {
this.setState({indexToRemoveServer: indexForServer});
}
closeServerRemoveModal = () => {
this.setState({indexToRemoveServer: -1});
}
handleTeamRemovePrompt = (index) => {
return () => {
document.activeElement.blur();
this.openServerRemoveModal(index);
};
}
handleTeamEditing = (team, index) => {
return () => {
document.activeElement.blur();
this.setState({
showEditTeamForm: true,
team: {
url: team.url,
name: team.name,
index,
order: team.order,
},
});
};
}
render() {
const teamNodes = this.props.teams.map((team, i) => {
return (
<TeamListItem
key={`teamListItem_${team.name}`}
name={team.name}
url={team.url}
onTeamRemove={this.handleTeamRemovePrompt(i)}
onTeamEditing={this.handleTeamEditing(team, i)}
onTeamClick={() => this.props.onTeamClick(team.name)}
/>
);
});
const addServerForm = (
<NewTeamModal
currentOrder={this.props.teams.length}
show={this.props.showAddTeamForm || this.state.showEditTeamForm}
editMode={this.state.showEditTeamForm}
onClose={() => {
this.setState({
showEditTeamForm: false,
team: {
name: '',
url: '',
index: false,
order: this.props.teams.length,
},
});
this.props.setAddTeamFormVisibility(false);
}}
onSave={(newTeam) => {
const teamData = {
name: newTeam.name,
url: newTeam.url,
order: newTeam.order,
};
if (this.props.showAddTeamForm) {
this.props.addServer(teamData);
} else {
this.props.updateTeam(newTeam.index, teamData);
}
this.setState({
showNewTeamModal: false,
showEditTeamForm: false,
team: {
name: '',
url: '',
index: false,
order: newTeam.order + 1,
},
});
this.render();
this.props.setAddTeamFormVisibility(false);
}}
team={this.state.team}
/>);
const removeServer = this.props.teams[this.state.indexToRemoveServer];
const removeServerModal = (
<RemoveServerModal
show={this.state.indexToRemoveServer !== -1}
serverName={removeServer ? removeServer.name : ''}
onHide={this.closeServerRemoveModal}
onCancel={this.closeServerRemoveModal}
onAccept={() => {
this.handleTeamRemove(this.state.indexToRemoveServer);
this.closeServerRemoveModal();
}}
/>
);
return (
<ListGroup className='teamList'>
{ teamNodes }
{ addServerForm }
{ removeServerModal}
</ListGroup>
);
}
}
TeamList.propTypes = {
onTeamClick: PropTypes.func,
onTeamsChange: PropTypes.func,
showAddTeamForm: PropTypes.bool,
teams: PropTypes.array,
addServer: PropTypes.func,
updateTeam: PropTypes.func,
setAddTeamFormVisibility: PropTypes.func,
};

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
export default class TeamListItem extends React.PureComponent {
handleTeamRemove = () => {
this.props.onTeamRemove();
}
handleTeamEditing = () => {
this.props.onTeamEditing();
}
render() {
return (
<div className='TeamListItem list-group-item'>
<div
className='TeamListItem-left'
onClick={this.props.onTeamClick}
>
<h4 className='list-group-item-heading'>{ this.props.name }</h4>
<p className='list-group-item-text'>
{ this.props.url }
</p>
</div>
<div className='pull-right'>
<a
href='#'
onClick={this.handleTeamEditing}
>{'Edit'}</a>
{' - '}
<a
href='#'
onClick={this.handleTeamRemove}
>{'Remove'}</a>
</div>
</div>
);
}
}
TeamListItem.propTypes = {
name: PropTypes.string,
onTeamEditing: PropTypes.func,
onTeamRemove: PropTypes.func,
onTeamClick: PropTypes.func,
url: PropTypes.string,
};

View File

@@ -0,0 +1,113 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import propTypes from 'prop-types';
import {Button, Navbar, ProgressBar} from 'react-bootstrap';
function InstallButton(props) {
if (props.notifyOnly) {
return (
<Button
bsStyle='primary'
onClick={props.onClickDownload}
>{'Download Update'}</Button>
);
}
return (
<Button
bsStyle='primary'
onClick={props.onClickInstall}
>{'Install Update'}</Button>
);
}
InstallButton.propTypes = {
notifyOnly: propTypes.bool.isRequired,
onClickInstall: propTypes.func.isRequired,
onClickDownload: propTypes.func.isRequired,
};
function UpdaterPage(props) {
let footer;
if (props.isDownloading) {
footer = (
<Navbar
className='UpdaterPage-footer'
fixedBottom={true}
fluid={true}
>
<ProgressBar
active={true}
now={props.progress}
label={`${props.progress}%`}
/>
<div className='pull-right'>
<Button
onClick={props.onClickCancel}
>{'Cancel'}</Button>
</div>
</Navbar>
);
} else {
footer = (
<Navbar
className='UpdaterPage-footer'
fixedBottom={true}
fluid={true}
>
<Button
className='UpdaterPage-skipButton'
bsStyle='link'
onClick={props.onClickSkip}
>{'Skip this version'}</Button>
<div className='pull-right'>
<Button
bsStyle='link'
onClick={props.onClickRemind}
>{'Remind me in 2 days'}</Button>
<InstallButton
notifyOnly={props.notifyOnly}
onClickInstall={props.onClickInstall}
onClickDownload={props.onClickDownload}
/>
</div>
</Navbar>
);
}
return (
<div className='UpdaterPage'>
<Navbar fluid={true} >
<h1 className='UpdaterPage-heading'>{'New update is available'}</h1>
</Navbar>
<div className='container-fluid'>
<p>{`A new version of the ${props.appName} is available!`}</p>
<p>{'Read the '}
<a
href='#'
onClick={props.onClickReleaseNotes}
>{'release notes'}</a>
{' to learn more.'}
</p>
</div>
{footer}
</div>
);
}
UpdaterPage.propTypes = {
appName: propTypes.string.isRequired,
notifyOnly: propTypes.bool.isRequired,
isDownloading: propTypes.bool.isRequired,
progress: propTypes.number,
onClickInstall: propTypes.func.isRequired,
onClickDownload: propTypes.func.isRequired,
onClickReleaseNotes: propTypes.func.isRequired,
onClickRemind: propTypes.func.isRequired,
onClickSkip: propTypes.func.isRequired,
onClickCancel: propTypes.func.isRequired,
};
export default UpdaterPage;

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import {storiesOf} from '@storybook/react';
import {action} from '@storybook/addon-actions';
import UpdaterPage from '../UpdaterPage.jsx';
import '../../css/components/UpdaterPage.css';
/*
appName: propTypes.string.isRequired,
notifyOnly: propTypes.bool.isRequired,
isDownloading: propTypes.bool.isRequired,
progress: propTypes.number,
onClickInstall: propTypes.func.isRequired,
onClickDownload: propTypes.func.isRequired,
onClickReleaseNotes: propTypes.func.isRequired,
onClickRemind: propTypes.func.isRequired,
onClickSkip: propTypes.func.isRequired,
*/
const appName = 'Storybook App';
storiesOf('UpdaterPage', module).
add('Normal', () => (
<UpdaterPage
appName={appName}
notifyOnly={false}
isDownloading={false}
progress={0}
onClickInstall={action('clicked install')}
onClickReleaseNotes={action('clicked release notes')}
onClickRemind={action('clicked remind')}
onClickSkip={action('clicked skip')}
/>
)).
add('NotifyOnly', () => (
<UpdaterPage
appName={appName}
notifyOnly={true}
onClickDownload={action('clicked download')}
/>
)).
add('Downloading', () => (
<UpdaterPage
appName={appName}
isDownloading={true}
progress={0}
onClickCancel={action('clicked cancel')}
/>
));

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import {Modal, Button, Row, Col} from 'react-bootstrap';
export default class ShowCertificateModal extends React.PureComponent {
static propTypes = {
certificate: PropTypes.object,
onOk: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
certificate: props.certificate,
};
}
handleOk = () => {
this.setState({certificate: null});
this.props.onOk();
}
render() {
const certificateSection = (descriptor) => {
return (
<Fragment>
<dt className={'certificate-key'}>{descriptor}</dt>
<dd className={'certificate-section'}><span/></dd>
</Fragment>
);
};
const certificateItem = (descriptor, value) => {
const val = value ? `${value}` : <span/>;
return (
<Fragment>
<dt className={'certificate-key'}>{descriptor}</dt>
<dd className={'certificate-value'}>{val}</dd>
</Fragment>
);
};
if (this.state.certificate === null) {
return (
<Modal
bsClass='modal'
className='show-certificate'
>
<Modal.Body>
{'No certificate Selected'}
</Modal.Body>
</Modal>
);
}
const utcSeconds = (date) => {
const d = new Date(0);
d.setUTCSeconds(date);
return d;
};
const expiration = utcSeconds(this.state.certificate.validExpiry);
const creation = utcSeconds(this.state.certificate.validStart);
const dateDisplayOptions = {dateStyle: 'full', timeStyle: 'full'};
const dateLocale = 'en-US';
return (
<Modal
bsClass='modal'
className='show-certificate'
show={this.state.certificate !== null}
scrollable={'true'}
>
<Modal.Header className={'no-border'}>
<Modal.Title>{'Certificate information'}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className='details'>{'Details'}</p>
<dl>
{certificateSection('Subject Name')}
{certificateItem('Common Name', this.state.certificate.subject.commonName)}
</dl>
<dl>
{certificateSection('Issuer Name')}
{certificateItem('Common Name', this.state.certificate.issuer.commonName)}
</dl>
<dl>
{certificateItem('Serial Number', this.state.certificate.serialNumber)}
{certificateItem('Not Valid Before', creation.toLocaleString(dateLocale, dateDisplayOptions))}
{certificateItem('Not Valid After', expiration.toLocaleString(dateLocale, dateDisplayOptions))}
</dl>
<dl>
{certificateSection('Public Key Info')}
{certificateItem('Algorithm', this.state.certificate.fingerprint.split('/')[0])}
</dl>
</Modal.Body>
<Modal.Footer className={'no-border'}>
<div className='container-fluid'>
<Row>
<Col>
<Button
variant={'primary'}
onClick={this.handleOk}
className={'primary'}
>{'Close'}</Button>
</Col>
</Row>
</div>
</Modal.Footer>
</Modal>
);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import propTypes from 'prop-types';
export default function UrlDescription(props) {
if (props.url) {
return (
<div className='HoveringURL HoveringURL-left'>
<p>{props.url}</p>
</div>
);
}
}
UrlDescription.propTypes = {
url: propTypes.string,
};

View File

@@ -0,0 +1,179 @@
html {
height: 100%;
}
body {
min-height: 100%;
}
.NewTeamModal-noBottomSpace {
padding-bottom: 0px;
margin-bottom: 0px;
}
.modal {
background-color: white;
color: #333;
border-radius: 4px;
border: 1px solid;
border-color: #666;
padding: 5px;
display: block;
font-size: 14px;
line-height: 1.4;
width: 1000px;
position: absolute;
top: 25%;
left: 50%;
transform: translate(-50%, -25%);
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}
form.modalForm {
padding-left: 10px;
display: block;
color: #333;
font-size: 14px;
line-height: 1.4;
}
label {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
font-weight: 700;
}
.form-control {
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
margin-right: 10px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-webkit-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
box-sizing: border-box;
}
.form-control:focus {
border-color: #66afe9;
outline: 0;
}
.help-block {
display: block;
margin-top: 5px;
margin-bottom: 10px;
color: #737373;
}
button.modalButton {
color: #007bff;
border-color: #007bff;
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: .25rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
box-sizing: border-box;
margin-left: 0.2em;
margin-right: 0.2em;
}
button.modalButton:last-child {
margin-right: 0px;
}
button.modalButton:focus {
text-decoration: none;
}
button.modalButton:active:focus {
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
button.primary {
color: #fff;
background-color: #337ab7;
border-color: #2e6da4;
}
button.primary:focus {
background-color: #286090;
border-color: #122b40;
}
button.default {
color: #333;
background-color: #fff;
border-color: #ccc;
}
button.default:focus {
background-color: #e6e6e6;
border-color: #8c8c8c;
}
.modalHeader {
font-size: 18px;
font-weight: 500;
margin: 5px;
padding-bottom: 5px;
border-bottom: 0px;
}
.modalHeader::after {
border-bottom: 1px solid #e5e5e5;
content: '';
display: block;
margin-top: 10px;
}
.modalFooter {
margin: 5px;
padding-top: 10px;
padding-left: 10px;
padding-right: 10px;
text-align: right;
border-top: 0px;
}
.modalFooter::before {
border-top: 1px solid #e5e5e5;
content: '';
display: block;
margin-bottom: 10px;
}
.pull-left {
float: left;
}
.modal-error {
color: #a94442;
}
.has-error .form-control {
border-color: #a94442;
}

View File

@@ -0,0 +1,210 @@
.certificate-modal .modal,
.show-certificate .modal {
background-color: aliceblue;
margin-top: 40px;
}
.show-certificate .modal-dialog {
width: 800px;
}
.modal-header {
border-bottom: 0px;
}
.modal-header::after {
border-bottom: solid 1px #E5E5E5;
width: 100%;
transform: translateY(15px);
}
.modal-footer::before {
border-top: solid 1px #E5E5E5;
width: 100%;
transform: translateY(-15px);
}
.modal-body :last-child {
margin-bottom: 0;
}
.certificate-modal .col-sm-4 {
padding: 0px;
text-align: left;
}
.certificate-modal .col-sm-8 {
padding: 0px;
}
.certificate-list thead {
width: 557.89px;
}
.certificate-list thead>tr {
padding: 0px 5px;
}
.certificate-list thead>tr>th {
font-family: Helvetica Neue;
font-style: normal;
font-weight: normal;
font-size: 12px;
line-height: 18px;
padding: 3px 5px;
border-bottom: 1px solid #CCCCCC;
color: #333333;
height: 22px;
}
.certificate-list thead tr th:first-child span {
padding-left: 5px;
}
.certificate-list thead tr th span {
border-right: solid 1px #E5E5E5;
display: block;
}
.certificate-list thead tr th:last-child span {
border-right: none;
}
.certificate-list tbody tr {
user-select: none;
cursor: pointer;
color: #555555;
}
.certificate-list tbody>tr>td {
max-width: 165px;
height: 47px;
font-style: normal;
font-weight: normal;
font-size: 14px;
line-height: 17px;
padding: 15px 10px;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
.certificate-list tbody tr td:first-child {
padding-left: 15px;
max-width: 227px;
}
.certificate-list tbody tr.selected {
background: #457AB2;
color: #FFFFFF;
}
table.certificate-list {
background: #FFFFFF;
border: 1px solid #CCCCCC;
border-radius: 4px;
border-collapse: unset;
box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.0008);
height: 150px;
}
table.certificate-list:focus {
border: 1px solid #66AFE9;
box-sizing: border-box;
box-shadow: 0px 0px 8px rgba(102, 175, 233, 0.6), inset 1px 1px 0px rgba(0, 0, 0, 0.00075);
}
.show-certificate button,
.certificate-modal button {
background: #FFFFFF;
border: 1px solid #CCCCCC;
box-sizing: border-box;
border-radius: 4px;
padding: 9px 12px;
line-height: 16px;
}
.show-certificate button:disabled,
.certificate-modal button:disabled {
opacity: 0.5;
}
.show-certificate button.primary,
.certificate-modal button.primary {
background: #457AB2;
color: #FFFFFF;
border: 1px solid #2E6DA4;
}
.show-certificate button.primary:hover,
.certificate-modal button.primary:hover {
background: #659AD2;
}
.certificate-modal button.info {
color: #457AB2;
}
.certificate-modal button.info:disabled {
color: #000;
}
.show-certificate .subtitle,
.certificate-modal .subtitle {
color: #737373;
margin: 0px 0px 15px 0px;
}
.show-certificate .no-border,
.certificate-modal .no-border {
border: none;
}
.show-certificate dl {
overflow-y: auto;
}
.show-certificate dt, dd {
float: left;
margin: 5px;
}
.show-certificate dt {
width: 150px;
clear:both
}
.show-certificate dd {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
.certificate-key {
font-style: normal;
font-weight: bold;
font-size: 12px;
line-height: 15px;
color: #333333;
text-align: right;
}
.certificate-value {
padding-top: 1px;
font-style: normal;
font-weight: normal;
font-size: 12px;
line-height: 14px;
text-align: left;
color: #333333;
}
.certificate-section {
border-bottom: 1px solid #E5E5E5;
width: 598px;
height: 8px;
}
.show-certificate .details {
margin: 15px;
font-weight: bold;
font-style: normal;
font-size: 12px;
line-height: 15px;
color: #333333;
}

View File

@@ -0,0 +1,36 @@
.ErrorView {
position: absolute;
top: 32px;
right: 0px;
bottom: 0px;
left: 0px;
}
.ErrorView-hidden {
visibility: hidden;
}
.ErrorView-tableStyle {
display: table;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.ErrorView-cellStyle {
display: table-cell;
vertical-align: top;
padding-top: 2em;
}
.ErrorView-bullets {
padding-left: 15px;
line-height: 1.7;
}
.ErrorView-techInfo {
font-size: 12px;
color: #aaa;
}

View File

@@ -0,0 +1,33 @@
#extra-bar {
max-height: 76px;
transition: max-height 0.25s ease;
background-color: #f5f5f5;
-webkit-font-smoothing: antialiased;
}
#extra-bar div {
padding: 0 0.93em 0.2em;
}
#extra-bar.hidden {
max-height: 0px;
}
span.backLabel {
font-family: "Open Sans", sans-serif;
font-weight: normal;
line-height: 30px;
font-size: 14px;
padding-top: 0px;
padding-bottom: 0px;
color: #166de0;
}
span.backIcon {
margin-right: 4px;
font-size: 1.6rem;
}
.container-fluid button:first-child {
padding-left: 0px;
}

View File

@@ -0,0 +1,79 @@
.finder {
position: fixed;
top: 0px;
padding: 4px;
background: #eee;
border: 1px solid #d7d7d7;
border-top: none;
border-right: none;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
font-size: 0px;
-webkit-app-region: no-drag;
}
.finder-input-wrapper {
display: inline-block;
position: relative;
vertical-align: bottom;
}
.finder button {
border: none;
background: #f0f0f0;
outline: none;
height: 26px;
}
.finder button:hover {
background: #d2d2d2;
}
.finder-input {
border: 1px solid #d2d2d2;
border-radius: 3px;
width: 200px;
outline: none;
line-height: 24px;
font-size: 14px;
padding: 0px 35px 0px 5px;
vertical-align: baseline;
}
.finder-input:focus {
border-color: #35b5f4;
box-shadow: 0 0 1px #35b5f4;
}
.finder-progress__disabled {
display: none;
}
.finder-progress {
position: absolute;
font-size: 12px;
right: 8px;
top: 6px;
color: #7b7b7b;
}
.icon {
height: 18px;
width: 18px;
}
.finder .finder-close {
border-radius: 3px;
}
.finder-next {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
margin-right: 2px;
}
.finder-prev {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
margin-left: 2px;
}

View File

@@ -0,0 +1,30 @@
.HoveringURL {
position: fixed;
bottom: 0;
left: 0;
color: gray;
background-color: whitesmoke;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 0px;
padding-right: 16px;
padding-top: 2px;
padding-bottom: 2px;
border-top: solid thin lightgray;
pointer-events: none;
margin: 0;
}
.HoveringURL-left {
border-top-right-radius: 4px;
border-right: solid thin lightgray;
}
.HoveringURL p {
margin: 0;
font-size: small;
font-family: "Open Sans", sans-serif;
padding-left: 4px;
padding-right: 4px;
}

View File

@@ -0,0 +1,294 @@
.LoadingAnimation {
--fade-duration: 150ms;
--colour: #166de0;
--animation-initial-delay: 500ms;
--animation-start-duration: 750ms;
--animation-end-duration: 600ms;
--animation-spinner-speed: 500ms;
--animation-spinner-mask-stroke-length: 169.6;
--ease-in-cubic: cubic-bezier(0.32, 0, 0.67, 0);
--ease-in: var(--ease-in-cubic);
--ease-in-out-cubic: cubic-bezier(0.65, 0, 0.35, 1);
--ease-in-out: var(--ease-in-out-cubic);
--ease-in-out-compass-shrink: cubic-bezier(0.1, 0.25, 0.3, 1);
opacity: 0;
transform: scale3d(1, 1, 1);
}
.LoadingAnimation--darkMode {
--colour: white;
}
.LoadingAnimation g,
.LoadingAnimation rect,
.LoadingAnimation path,
.LoadingAnimation circle {
transform-origin: center center;
}
.LoadingAnimation svg {
color: var(--colour);
}
.LoadingAnimation .LoadingAnimation__spinner-gradient-color {
stop-color: var(--colour);
}
.LoadingAnimation .LoadingAnimation__spinner-mask {
transform: scale3d(1.03, 1.03, 1);
}
.LoadingAnimation .LoadingAnimation__spinner-container {
opacity: 0;
transform: scale3D(2.08, 2.08, 1) rotate3d(0, 0, 1, -10deg);
}
.LoadingAnimation .LoadingAnimation__spinner-mask-container {
transform: rotate3d(0, 0, 1, -86deg);
}
.LoadingAnimation .LoadingAnimation__spinner-mask {
stroke-dasharray: var(--animation-spinner-mask-stroke-length);
stroke-dashoffset: var(--animation-spinner-mask-stroke-length);
}
.LoadingAnimation .LoadingAnimation__compass {
opacity: 1;
transform: scale3d(1, 1, 1);
}
.LoadingAnimation .LoadingAnimation__compass-needle-container {
transform: scale3d(1, 1, 1);
}
.LoadingAnimation .LoadingAnimation__compass-needle,
.LoadingAnimation .LoadingAnimation__compass-needle-front-mask,
.LoadingAnimation .LoadingAnimation__compass-needle-behind-mask {
transform-origin: 54px 46px;
transform: rotate3d(0, 0, 1, 0deg);
}
.LoadingAnimation .LoadingAnimation__compass-base-mask-container {
transform: rotate3d(0, 0, 1, -86deg);
}
.LoadingAnimation .LoadingAnimation__compass-base-mask {
stroke-dasharray: var(--animation-spinner-mask-stroke-length);
stroke-dashoffset: var(--animation-spinner-mask-stroke-length);
}
.LoadingAnimation--loading {
--fade-in-duration: 150ms;
--fade-in-delay: 0ms;
animation:
LoadingAnimation__fade-in var(--fade-in-duration) var(--fade-in-delay) var(--ease-in) forwards;
transform: scale3d(1, 1, 1);
}
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__spinner-container {
--shrink-duration: calc(var(--animation-end-duration) * 0.5);
--shrink-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.91 + var(--animation-initial-delay));
--fade-in-duration: calc(var(--animation-end-duration) * 0.25);
--fade-in-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) * 0.24 + var(--animation-initial-delay));
animation:
LoadingAnimation__spinner-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in-out-compass-shrink) forwards,
LoadingAnimation__fade-in var(--fade-in-duration) var(--fade-in-delay) var(--ease-in) forwards;
}
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__spinner-mask {
--reveal-duration: var(--animation-end-duration);
--reveal-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
animation:
LoadingAnimation__spinner-reveal var(--reveal-duration) var(--reveal-delay) var(--ease-in) forwards;
}
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass {
--shrink-duration: calc(var(--animation-end-duration) * 0.5);
--shrink-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.91 + var(--animation-initial-delay));
animation:
LoadingAnimation__compass-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in-out-compass-shrink) forwards;
}
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-container {
--shrink-duration: calc(var(--animation-end-duration) * 0.25);
--shrink-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) - var(--animation-end-duration) * 0.25 + var(--animation-initial-delay));
animation:
LoadingAnimation__needle-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in) forwards;
}
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle {
--spin-left-duration: var(--animation-start-duration);
--spin-left-delay: var(--animation-initial-delay);
--spin-right-duration: var(--animation-end-duration);
--spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
--fade-out-duration: calc(var(--animation-end-duration) * 0.25);
--fade-out-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) - var(--animation-end-duration) * 0.25 + var(--animation-initial-delay));
animation:
LoadingAnimation__needle-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards,
LoadingAnimation__needle-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards,
LoadingAnimation__fade-out var(--fade-out-duration) var(--fade-out-delay) var(--ease-in) forwards;
}
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-behind-mask {
--spin-left-duration: var(--animation-start-duration);
--spin-left-delay: var(--animation-initial-delay);
--spin-right-duration: calc(var(--animation-end-duration) * 0.3666);
--spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
animation:
LoadingAnimation__needle-mask-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards,
LoadingAnimation__needle-mask-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards;
}
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-front-mask {
--spin-left-duration: var(--animation-start-duration);
--spin-left-delay: var(--animation-initial-delay);
--spin-right-duration: var(--animation-end-duration);
--spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
animation:
LoadingAnimation__needle-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards,
LoadingAnimation__needle-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards;
}
.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-base-mask {
--conceal-duration: var(--animation-end-duration);
--conceal-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay));
animation:
LoadingAnimation__compass-base-conceal var(--conceal-duration) var(--conceal-delay) var(--ease-in) forwards;
}
.LoadingAnimation.LoadingAnimation--spinning .LoadingAnimation__spinner {
--spin-duration: var(--animation-spinner-speed);
--spin-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.95 + var(--animation-initial-delay));
animation:
LoadingAnimation__spinner-spin var(--spin-duration) var(--spin-delay) linear infinite;
}
.LoadingAnimation--loaded {
--duration: 150ms;
--delay: 0ms;
animation:
LoadingAnimation__fade-out var(--duration) var(--delay) var(--ease-in) forwards,
LoadingAnimation__shrink var(--duration) var(--delay) var(--ease-in) forwards;
}
.LoadingAnimation--loaded .LoadingAnimation__spinner-container {
opacity: 1;
transform: scale3D(1, 1, 1) rotate3d(0, 0, 1, -10deg);
}
.LoadingAnimation--loaded .LoadingAnimation__spinner-mask {
stroke-dashoffset: 0;
}
.LoadingAnimation--loaded .LoadingAnimation__compass {
transform: scale3D(0.4166666667, 0.4166666667, 1);
}
.LoadingAnimation--loaded .LoadingAnimation__compass-needle-container {
transform: scale3d(0.35, 0.35, 1);
}
.LoadingAnimation--loaded .LoadingAnimation__compass-needle {
opacity: 0;
transform: rotate3d(0, 0, 1, 405deg);
}
.LoadingAnimation--loaded .LoadingAnimation__compass-needle-behind-mask {
transform: rotate3d(0, 0, 1, 0deg);
}
.LoadingAnimation--loaded .LoadingAnimation__compass-needle-front-mask {
transform: rotate3d(0, 0, 1, 405deg);
}
.LoadingAnimation--loaded .LoadingAnimation__compass-base-mask {
stroke-dashoffset: 0;
}
@keyframes LoadingAnimation__fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes LoadingAnimation__fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes LoadingAnimation__shrink {
0% {
transform: scale3d(1, 1, 1);
}
100% {
transform: scale3d(0.35, 0.35, 1);
}
}
@keyframes LoadingAnimation__spinner-shrink {
0% {
transform: scale3D(2.08, 2.08, 1) rotate3d(0, 0, 1, -10deg);
}
100% {
transform: scale3D(1, 1, 1) rotate3d(0, 0, 1, -10deg);
}
}
@keyframes LoadingAnimation__spinner-spin {
from {
transform: rotate3d(0, 0, 1, 0deg);
}
to {
transform: rotate3d(0, 0, 1, 359deg);
}
}
@keyframes LoadingAnimation__spinner-reveal {
0%, 5% {
stroke-dashoffset: var(--animation-spinner-mask-stroke-length);
}
95%, 100% {
stroke-dashoffset: 0;
}
}
@keyframes LoadingAnimation__needle-spin-left {
0% {
transform: rotate3d(0, 0, 1, 0deg);
}
100% {
transform: rotate3d(0, 0, 1, -20deg);
}
}
@keyframes LoadingAnimation__needle-spin-right {
0% {
transform: rotate3d(0, 0, 1, -20deg);
}
100% {
transform: rotate3d(0, 0, 1, 405deg);
}
}
@keyframes LoadingAnimation__needle-mask-spin-left {
0% {
transform: rotate3d(0, 0, 1, 0deg);
}
100% {
transform: rotate3d(0, 0, 1, -20deg);
}
}
@keyframes LoadingAnimation__needle-mask-spin-right {
0% {
transform: rotate3d(0, 0, 1, -20deg);
}
100% {
transform: rotate3d(0, 0, 1, 0deg);
}
}
@keyframes LoadingAnimation__needle-shrink {
0% {
transform: scale3d(1, 1, 1);
}
100% {
transform: scale3d(0.35, 0.35, 1);
}
}
@keyframes LoadingAnimation__compass-shrink {
0% {
transform: scale3D(1, 1, 1);
}
100% {
transform: scale3D(0.4166666667, 0.4166666667, 1);
}
}
@keyframes LoadingAnimation__compass-base-conceal {
0%, 5% {
stroke-dashoffset: var(--animation-spinner-mask-stroke-length);
}
95%, 100% {
stroke-dashoffset: 0;
}
}

View File

@@ -0,0 +1,59 @@
body {
background-color: transparent;
}
.LoadingScreen {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
vertical-align: middle;
background: white url(../../../assets/window-background.svg) no-repeat center;
background-size: cover;
opacity: 1;
visibility: visible;
z-index: 10;
overflow:hidden;
transition: opacity 150ms 0ms ease-out, visibility 150ms 0ms step-start;
}
.LoadingScreen::before, .LoadingScreen::after {
content: "";
display: block;
position: absolute;
width: 460px;
height: 460px;
opacity: 0.1;
background-image: url(../../../assets/window-background-dots.svg);
background-repeat: no-repeat;
background-position: center;
}
.LoadingScreen::before {
left: -210px;
bottom: 10px;
}
.LoadingScreen::after {
right: -80px;
top: 50%;
margin-top: -230px;
}
.LoadingScreen--darkMode {
background-color: #323639;
background-image: url(../../../assets/window-background_dark.svg);
}
.LoadingScreen--darkMode::before, .LoadingScreen--darkMode::after {
background-image: url(../../../assets/window-background-dots_dark.svg);
}
.LoadingScreen--loaded {
opacity: 0;
visibility: hidden;
transition: opacity 150ms 0ms ease-in, visibility 150ms 0ms step-end;
}

View File

@@ -0,0 +1,13 @@
/*.mainPage,.mainPage > .container-fluid, .mainPage-viewsRow {
height: 100%;
}*/
.MainPage .HoveringURL {
max-width: 95%;
position: absolute;
bottom: 0px;
}
div[id*="-permissionDialog"] {
max-width: 350px;
}

View File

@@ -0,0 +1,37 @@
.mattermostView {
text-align: center;
z-index: 1;
opacity: 1;
visibility: visible;
}
.mattermostView-hidden {
z-index: -1;
opacity: 0;
visibility: hidden;
}
.mattermostView .ErrorView {
text-align: left;
}
.mattermostView webview {
position: absolute;
top: 40px;
right: 0px;
bottom: 0px;
left: 0px;
}
.mattermostView-hidden webview {
visibility: hidden;
z-index: -1;
}
.mattermostView-error webview {
z-index: -1;
}
.allow-extra-bar webview {
top: 76px;
}

View File

@@ -0,0 +1,4 @@
.NewTeamModal-noBottomSpace {
padding-bottom: 0px;
margin-bottom: 0px;
}

View File

@@ -0,0 +1,51 @@
.PermissionRequestDialog-content{
min-width: 250px;
}
.PermissionRequestDialog-content .popover-content {
padding: 14px;
}
.PermissionRequestDialog-content > *:nth-child(1) {
margin-right: 14px;
}
.PermissionRequestDialog-content .PermissionRequestDialog-content-description {
text-indent: 0.25em;
}
.PermissionRequestDialog-content .PermissionRequestDialog-content-description .glyphicon {
margin-right: 0.5em;
}
.PermissionRequestDialog-content .PermissionRequestDialog-content-buttons {
margin-bottom: 0;
text-align: right;
}
.PermissionRequestDialog-content .PermissionRequestDialog-content-buttons > * {
margin-right: 7px;
}
.PermissionRequestDialog-content .PermissionRequestDialog-content-buttons > *:last-child {
margin-right: 0;
}
.PermissionRequestDialog-content .PermissionRequestDialog-content-close {
position:absolute;
top: 0px;
right:0px;
color: gray;
text-decoration-line: none;
}
.PermissionRequestDialog-content .PermissionRequestDialog-content-close:hover {
color: black;
}
.permission-modal .modal-dialog {
max-width: 580px;
}
.permission-modal .remove-border {
border: none;
}

View File

@@ -0,0 +1,261 @@
.TabBar {
border: none;
max-height: 36px;
flex: 1 1 auto;
display: flex !important;
flex-wrap: nowrap;
justify-content: flex-start;
-webkit-app-region: drag;
margin-top: 4px;
}
.TabBar.darkMode {
background-color: #202124;
}
.TabBar .teamTabItem span {
flex: 0 1 auto;
overflow: hidden;
white-space: nowrap;
text-align: center;
}
.TabBar>li {
-webkit-app-region: no-drag;
-webkit-user-select: none;
min-width: 48px;
}
.TabBar>li>a {
border: none;
border-radius: 0;
height: 32px;
max-height: 32px;
line-height: 16px;
margin-right: -1px;
padding: 6px 0;
color: rgba(61,60,64,0.7);
font-family: Arial;
font-size: 14px;
letter-spacing: -0.2px;
transition: 0.3s;
}
.TabBar.darkMode>li>a {
color: rgba(243,243,243,0.7);
}
.TabBar>li>a:hover {
background-color: rgba(255,255,255,0.4);
text-decoration: none;
border-radius: 6px 6px 0 0;
}
.TabBar.darkMode>li>a:hover {
background-color: rgba(50, 54, 57, 0.4);
}
.TabBar>li>a:focus {
background-color: #fff;
color: rgba(61,60,64,1);
text-decoration: none;
border-radius: 6px 6px 0 0;
}
.TabBar.darkMode>li>a:focus {
background-color: #323639;
color: rgba(243,243,243,1);
}
.TabBar>li:before, .TabBar>li:after {
position: absolute;
bottom: -1px;
width: 6px;
height: 6px;
content: "";
background-color: inherit;
z-index: 9;
flex: 0 0 6px;
}
.TabBar>li.teamTabItem.active:before, .TabBar>li.teamTabItem.smooth-dnd-ghost:before {
left: -4px;
border-bottom-right-radius: 6px;
border-right: 2px solid #fff;
border-bottom: 2px solid #fff;
z-index: 9;
}
.TabBar.darkMode>li.teamTabItem.active:before, .TabBar.darkMode>li.teamTabItem.smooth-dnd-ghost:before {
border-right: 2px solid #323639;
border-bottom: 2px solid #323639;
}
.TabBar>li.teamTabItem.active:after, .TabBar>li.teamTabItem.smooth-dnd-ghost:after {
border-bottom-left-radius: 6px;
right: -5px;
border-left: 2px solid #fff;
border-bottom: 2px solid #fff;
z-index: 9;
}
.TabBar.darkMode>li.teamTabItem.active:after, .TabBar.darkMode>li.teamTabItem.smooth-dnd-ghost:after {
border-left: 2px solid #323639;
border-bottom: 2px solid #323639;
}
.TabBar>li>a>div.TabBar-tabSeperator {
padding: 2px 16px;
max-height: 20px;
display: flex;
}
.TabBar>li.TabBar-addServerButton{
transition: none !important;
transform: none !important;
flex: 0 0 auto;
min-width: 40px;
}
.TabBar>li.TabBar-addServerButton>a{
color: rgba(61,60,64,0.7);
transition: opacity 0.3s ease-in;
}
.TabBar>li.TabBar-addServerButton svg{
margin: -2px;
}
.TabBar.darkMode>li.TabBar-addServerButton>a{
color: rgba(243,243,243,0.7);
}
.TabBar>li.teamTabItem.active>a, .TabBar>li.teamTabItem.smooth-dnd-ghost>a {
border: none;
border-radius: 6px 6px 0 0;
color: rgba(61,60,64,1);
background-color: #fff;
z-index: 9;
}
.smooth-dnd-no-user-select li.TabBar-addServerButton>a {
opacity: 0;
}
.TabBar.darkMode>li.teamTabItem.active>a, .TabBar.darkMode>li.teamTabItem.smooth-dnd-ghost>a {
color: #f3f3f3;
background-color: #323639;
}
.TabBar>li.teamTabItem:not(.active)+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
border-left: 1px solid rgba(61,60,64,0.2);
margin-left: -1px;
}
.TabBar>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
border-left: 1px solid rgba(61,60,64,0.2);
margin-left: -1px;
}
.TabBar.darkMode>li.teamTabItem:not(.active)+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
border-left: 1px solid rgba(243,243,243,0.2);
margin-left: -1px;
}
.TabBar.darkMode>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
border-left: 1px solid rgba(243,243,243,0.2);
margin-left: -1px;
}
.TabBar>li.teamTabItem:not(.active):not(.disabled):hover+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
border-left: none;
margin-left: 0px;
}
.TabBar>li.teamTabItem:not(.active):not(.disabled):hover+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
border-left: none;
margin-left: 0px;
}
.TabBar>li.teamTabItem:not(.active):not(.disabled)+.TabBar-addServerButton>a:hover>div.TabBar-tabSeperator {
border-left: none;
margin-left: 0px;
}
.TabBar>li.teamTabItem:not(.active):not(.disabled)+li.teamTabItem:not(.active)>a:hover>div.TabBar-tabSeperator {
border-left: none;
margin-left: 0px;
}
.TabBar .TabBar-addServerButton>a {
border: none;
background: transparent;
color: #999;
font-size: 10px;
line-height: 16px;
}
.TabBar.darkMode .TabBar-addServerButton>a {
color: rgba(243,243,243,0.7);
}
.TabBar .TabBar-dot {
background: #579EFF;
float: right;
height: 6px;
width: 6px;
margin-top: 5px;
margin-left: 8px;
border-radius: 4px;
flex: 0 0 6px;
}
.TabBar .TabBar-expired {
float: right;
height: 16px;
width: 16px;
margin-left: 8px;
background-image: url(../../../assets/icon-session-expired.svg);
flex: 0 0 16px;
}
.TabBar.darkMode .TabBar-expired {
filter: invert(100%);
-webkit-filter: invert(100%);
}
.TabBar .TabBar-badge {
background: #CB2431;
float: right;
color: white;
font-size: 11px;
text-align: center;
line-height: 18px;
height: 18px;
margin-left: 8px;
border-radius: 100px;
padding: 0 5px;
display: flex;
justify-content: center;
align-items: center;
font-family: "Open Sans", sans-serif;
font-weight: bold;
min-width: 18px;
margin-top: -1px;
letter-spacing: normal;
-webkit-font-smoothing: antialiased;
flex: 1 0 18px;
}
.TabBar .TabBar-badge.TabBar-badge-nomention:after {
content: "";
width: 4px;
height: 4px;
background: #fff;
display: block;
border-radius: 50%;
}
.TabBar .teamTabItem-unread {
font-weight: bold;
}

View File

@@ -0,0 +1,8 @@
.TeamListItem:hover {
background: #eee;
}
.TeamListItem-left {
display: inline-block;
width: calc(100% - 100px);
}

View File

@@ -0,0 +1,16 @@
.UpdaterPage-heading {
font-size: 20pt;
margin: 10px 0px;
}
.UpdaterPage-skipButton {
padding-left: 0px;
}
.UpdaterPage-footer {
padding: 1em 0;
}
.UpdaterPage .progress-bar {
min-width: 2em;
}

View File

@@ -0,0 +1,14 @@
@import url("ErrorView.css");
@import url("HoveringURL.css");
@import url("MainPage.css");
@import url("MattermostView.css");
@import url("NewTeamModal.css");
@import url("PermissionRequestDialog.css");
@import url("TabBar.css");
@import url("TeamListItem.css");
@import url("Finder.css");
@import url("UpdaterPage.css");
@import url("CertificateModal.css");
@import url("ExtraBar.css");
@import url("LoadingScreen.css");
@import url("LoadingAnimation.css");

252
src/renderer/css/index.css Normal file
View File

@@ -0,0 +1,252 @@
@import url("components/index.css");
@import '~font-awesome/css/font-awesome.css';
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-300.woff2') format('woff2');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-300italic.woff2') format('woff2');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-italic.woff2') format('woff2');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-600.woff2') format('woff2');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-600italic.woff2') format('woff2');
}
html {
height: 100%;
}
body {
min-height: 100%;
}
.hovering-enter {
opacity: 0.01;
}
.hovering-enter.hovering-enter-active {
opacity: 1;
transition: opacity 300ms ease-in-out;
}
.hovering-exit {
opacity: 1;
}
.hovering-exit.hovering-exit-active {
opacity: 0.01;
transition: opacity 500ms ease-in-out;
}
.has-error .control-label,
.has-error .help-block {
color: #333;
}
.modal-error {
color: #a94442;
}
.topBar {
height: 40px;
}
.topBar>.topBar-bg {
display: flex;
overflow: hidden;
height: 36px;
background-color: rgba(0,0,0,0.1);
}
.topBar>.topBar-bg.unfocused {
opacity: 0.4;
}
.topBar.darkMode {
background-color: #323639;
color: #fff;
}
.topBar.darkMode>.topBar-bg {
background-color: #202124;
}
.topBar .three-dot-menu {
font-size: 20px;
line-height: 20px;
height: 36px;
float: left;
padding-top: 5px;
border: none;
flex: 0 0 40px;
z-index: 9;
color: rgba(61,60,64,0.7);
-webkit-app-region: no-drag;
background-color: rgba(229, 229, 229, 1);
}
.topBar .three-dot-menu svg {
border-radius: 100px;
padding: 4px;
height: 28px;
width: 28px;
}
.topBar .three-dot-menu:focus {
outline: none;
}
.topBar .three-dot-menu:hover svg, .topBar .three-dot-menu:focus svg, .topBar .three-dot-menu:active svg {
outline: none;
background-color: #c8c8c8;
}
.topBar.darkMode .three-dot-menu:hover svg, .topBar.darkMode .three-dot-menu:focus svg, .topBar.darkMode .three-dot-menu:active svg {
background-color: #383A3F;
}
.topBar.macOS .three-dot-menu:hover svg, .topBar.macOS .three-dot-menu:focus svg, .topBar.macOS .three-dot-menu:active svg {
background: none !important;
}
.topBar.macOS .three-dot-menu {
flex-basis: 80px;
-webkit-app-region: drag;
}
.topBar.macOS.fullScreen .three-dot-menu {
flex-basis: 0px;
}
.topBar.macOS .three-dot-menu>svg {
display: none;
}
.topBar.darkMode .three-dot-menu {
background-color: #202124;
color: rgba(243,243,243,0.7);
}
.topBar.darkMode .title-bar-btns {
color: rgba(243,243,243,0.7);
background-color: #202124;
}
.topBar .title-bar-btns {
position: relative;
line-height: 36px;
height: 36px;
z-index: 9;
color: rgba(61,60,64,0.7);
-webkit-app-region: no-drag;
display: grid;
grid-template-columns: repeat(3, 46px);
background-color: #e5e5e5;
}
.topBar .title-bar-btns>.button {
grid-row: 1 / span 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
user-select: none;
}
.topBar.darkMode .title-bar-btns>.button:hover {
background: rgba(255,255,255,0.1);
}
.topBar.darkMode .title-bar-btns>.button:active {
background: rgba(255,255,255,0.2);
}
.topBar .title-bar-btns>.button:hover {
background: rgba(0,0,0,0.1);
}
.topBar .title-bar-btns>.button:active {
background: rgba(0,0,0,0.2);
}
.topBar .title-bar-btns>.close-button:hover {
background: #E81123 !important;
}
.topBar .title-bar-btns>.close-button:hover>img {
filter: invert(100%);
-webkit-filter: invert(100%);
opacity: 1;
}
.topBar .title-bar-btns>.close-button:active {
background: #f1707a !important;
}
.topBar .title-bar-btns>.close-button:active>img {
filter: invert(100%);
-webkit-filter: invert(100%);
}
.topBar .title-bar-btns img {
opacity: 0.7;
}
.topBar.darkMode .title-bar-btns img {
filter: invert(100%);
-webkit-filter: invert(100%);
}
.topBar .title-bar-btns>.min-button {
grid-column: 1;
}
.topBar .title-bar-btns>.max-button, .topBar .title-bar-btns>.restore-button {
grid-column: 2;
}
.topBar .title-bar-btns>.close-button {
grid-column: 3;
}
.topBar .overlay-gradient {
flex: 0 0 40px;
z-index: 9;
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #e5e5e5 100%);
-webkit-app-region: drag;
margin-top: 4px;
}
.topBar.darkMode .overlay-gradient {
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #202124 100%);
}

View File

@@ -0,0 +1,3 @@
body {
background-color: transparent;
}

View File

@@ -0,0 +1,36 @@
.CloseButton:hover span {
color: #333;
}
.IndicatorContainer {
float: left;
padding: 1em;
}
.AutoSaveIndicator {
padding: 5px 15px;
margin: 0 0 0 10px;
}
.AutoSaveIndicator.AutoSaveIndicator-Leave {
opacity: 0;
transition: opacity 1s cubic-bezier(0.19, 1, 0.22, 1);
}
.TeamListItem p {
overflow-wrap: break-word;
}
.checkbox > label {
width: 100%;
}
body {
overflow-x: hidden;
overflow-y: scroll;
height: 100%;
}
#content {
height: 100%;
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
/**
* A custom hook to implement an animationend listener on the provided ref
* @param {object} ref - A reference to a DOM element to add the listener to
* @param {function} callback - A callback function that will be run for matching animation events
* @param {string} animationName - The name of the animation to listen for
* @param {boolean} listenForEventBubbling - A parameter that when true, listens for events on the target element and
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
* ignores events bubbling up from descendent elements
*/
function useAnimationEnd(
ref,
callback,
animationName,
listenForEventBubbling = true,
) {
React.useEffect(() => {
if (!ref.current) {
return undefined;
}
function handleAnimationend(event) {
if (!listenForEventBubbling && event.target !== ref.current) {
return;
}
if (animationName && animationName !== event.animationName) {
return;
}
callback(event);
}
ref.current.addEventListener('animationend', handleAnimationend);
return () => {
ref.current.removeEventListener('animationend', handleAnimationend);
};
}, [ref, callback, animationName, listenForEventBubbling]);
}
export default useAnimationEnd;

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
/**
* A custom hook to implement a transitionend listener on the provided ref
* @param {object} ref - A reference to a DOM element to add the listener to
* @param {function} callback - A callback function that will be run for matching animation events
* @param {array} properties - An array of css property strings to listen for
* @param {boolean} listenForEventBubbling - A parameter that when true, listens for events on the target element and
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
* ignores events bubbling up from descendent elements
*/
function useTransitionend(
ref,
callback,
properties,
listenForEventBubbling = true,
) {
React.useEffect(() => {
if (!ref.current) {
return undefined;
}
function handleTransitionEnd(event) {
if (!listenForEventBubbling && event.target !== ref.current) {
return;
}
if (properties && typeof properties === 'object') {
const property = properties.find(
(propertyName) => propertyName === event.propertyName,
);
if (property) {
callback(event);
}
return;
}
callback(event);
}
ref.current.addEventListener('transitionend', handleTransitionEnd);
return () => {
if (!ref.current) {
return;
}
ref.current.removeEventListener(
'transitionend',
handleTransitionEnd,
);
};
}, [ref, callback, properties, listenForEventBubbling]);
}
export default useTransitionend;

10
src/renderer/index.html Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app" />
</body>
</html>

133
src/renderer/index.jsx Normal file
View File

@@ -0,0 +1,133 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/index.css';
if (process.env.NODE_ENV === 'production') {
window.eval = global.eval = () => { // eslint-disable-line no-multi-assign, no-eval
throw new Error('Sorry, Mattermost does not support window.eval() for security reasons.');
};
} else if (module.hot) {
module.hot.accept();
}
import React from 'react';
import ReactDOM from 'react-dom';
import {ipcRenderer} from 'electron';
import {GET_CONFIGURATION, UPDATE_TEAMS, QUIT, RELOAD_CONFIGURATION} from 'common/communication';
import MainPage from './components/MainPage.jsx';
class Root extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
}
async componentDidMount() {
await this.setInitialConfig();
ipcRenderer.on('synchronize-config', () => {
this.reloadConfig();
});
ipcRenderer.on(RELOAD_CONFIGURATION, () => {
this.reloadConfig();
});
// Deny drag&drop navigation in mainWindow.
// Drag&drop is allowed in webview of index.html.
document.addEventListener('dragover', (event) => event.preventDefault());
document.addEventListener('drop', (event) => event.preventDefault());
}
setInitialConfig = async () => {
const config = await this.requestConfig(true);
this.setState({config});
}
moveTabs = async (originalOrder, newOrder) => {
const teams = this.state.config.teams.concat();
const tabOrder = teams.map((team, index) => {
return {
index,
order: team.order,
};
}).sort((a, b) => (a.order - b.order));
const team = tabOrder.splice(originalOrder, 1);
tabOrder.splice(newOrder, 0, team[0]);
let teamIndex;
tabOrder.forEach((t, order) => {
if (order === newOrder) {
teamIndex = t.index;
}
teams[t.index].order = order;
});
await this.teamConfigChange(teams);
return teamIndex;
};
teamConfigChange = async (updatedTeams, callback) => {
const updatedConfig = await ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams);
await this.reloadConfig();
if (callback) {
callback(updatedConfig);
}
};
reloadConfig = async () => {
const config = await this.requestConfig();
this.setState({config});
};
requestConfig = async (exitOnError) => {
// todo: should we block?
try {
const configRequest = await ipcRenderer.invoke(GET_CONFIGURATION);
return configRequest;
} catch (err) {
console.log(`there was an error with the config: ${err}`);
if (exitOnError) {
ipcRenderer.send(QUIT, `unable to load configuration: ${err}`, err.stack);
}
}
return null;
};
openMenu = () => {
if (process.platform !== 'darwin') {
ipcRenderer.send('open-app-menu');
}
}
render() {
const {config} = this.state;
if (!config) {
return null;
}
return (
<MainPage
teams={config.teams}
showAddServerButton={config.enableServerManagement}
moveTabs={this.moveTabs}
openMenu={this.openMenu}
darkMode={config.darkMode}
appName={config.appName}
/>
);
}
}
ipcRenderer.invoke('get-app-version').then(({name, version}) => {
// eslint-disable-next-line no-undef
console.log(`Starting ${name} v${version} commit: ${__HASH_VERSION__}`);
});
ReactDOM.render(
<Root/>,
document.getElementById('app'),
);

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
import SelectCertificateModal from './certificateModal.jsx';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
import 'renderer/css/components/CertificateModal.css';
const handleCancel = () => {
window.postMessage({type: MODAL_CANCEL}, window.location.href);
};
const handleSelect = (cert) => {
window.postMessage({type: MODAL_RESULT, data: {cert}}, window.location.href);
};
const getCertInfo = () => {
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
};
const start = async () => {
ReactDOM.render(
<SelectCertificateModal
onSelect={handleSelect}
onCancel={handleCancel}
getCertInfo={getCertInfo}
/>,
document.getElementById('app'),
);
};
start();

View File

@@ -0,0 +1,182 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import {Modal, Button, Table, Row, Col} from 'react-bootstrap';
import {MODAL_INFO} from 'common/communication';
import ShowCertificateModal from '../../components/showCertificateModal.jsx';
export default class SelectCertificateModal extends React.PureComponent {
static propTypes = {
onSelect: PropTypes.func.isRequired,
onCancel: PropTypes.func,
getCertInfo: PropTypes.func,
}
constructor(props) {
super(props);
this.state = {
selectedIndex: null,
showCertificate: null,
};
}
componentDidMount() {
window.addEventListener('message', this.handleCertInfoMessage);
this.props.getCertInfo();
}
componentWillUnmount() {
window.removeEventListener('message', this.handleCertInfoMessage);
}
handleCertInfoMessage = (event) => {
switch (event.data.type) {
case MODAL_INFO: {
const {url, list} = event.data.data;
this.setState({url, list});
break;
}
default:
break;
}
}
selectfn = (index) => {
return (() => {
this.setState({selectedIndex: index});
});
};
renderCert = (cert, index) => {
const issuer = (cert.issuerName || (cert.issuer && cert.issuer.commonName) || '');
const subject = (cert.subjectName || (cert.subject && cert.subject.commonName) || '');
const serial = cert.serialNumber || '';
return (
<tr
key={`cert-${index}`}
onClick={this.selectfn(index)}
className={this.state.selectedIndex === index ? 'selected' : ''}
>
<td
title={subject}
>{subject}</td>
<td
title={issuer}
>{issuer}</td>
<td
title={serial}
>{serial}</td>
</tr>);
};
renderCerts = (certificateList) => {
if (certificateList) {
const certs = certificateList.map(this.renderCert);
return (
<Fragment>
{certs}
</Fragment>
);
}
return (<Fragment><tr/><tr><td/><td>{'No certificates available'}</td><td/></tr></Fragment>);
}
getSelectedCert = () => {
return this.state.selectedIndex === null ? null : this.state.list[this.state.selectedIndex];
};
handleOk = () => {
const cert = this.getSelectedCert();
if (cert !== null) {
this.props.onSelect(cert);
}
}
handleCertificateInfo = () => {
const certificate = this.getSelectedCert();
this.setState({showCertificate: certificate});
}
certificateInfoClose = () => {
this.setState({showCertificate: null});
}
render() {
if (this.state.showCertificate) {
return (
<ShowCertificateModal
certificate={this.state.showCertificate}
onOk={this.certificateInfoClose}
/>
);
}
return (
<Modal
bsClass='modal'
className='certificate-modal'
show={Boolean(this.state.list && this.state.url)}
>
<Modal.Header>
<Modal.Title >{'Select a certificate'}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className={'subtitle'}>{`Select a certificate to authenticate yourself to ${this.state.url}`}</p>
<Table
striped={true}
hover={true}
size={'sm'}
responsive={true}
className='certificate-list'
tabIndex={1}
>
<thead>
<tr>
<th><span className={'divider'}>{'Subject'}</span></th>
<th><span className={'divider'}>{'Issuer'}</span></th>
<th>{'Serial'}</th>
</tr>
</thead>
<tbody>
{this.renderCerts(this.state.list)}
<tr/* this is to correct table height without affecting real rows *//>
</tbody>
</Table>
</Modal.Body>
<Modal.Footer className={'no-border'}>
<div className={'container-fluid'}>
<Row>
<Col sm={4}>
<Button
variant={'info'}
disabled={this.state.selectedIndex === null}
onClick={this.handleCertificateInfo}
className={'info'}
>{'Certificate Information'}</Button>
</Col>
<Col sm={8}>
<Button
onClick={this.props.onCancel}
variant={'secondary'}
className={'secondary'}
>{'Cancel'}</Button>
<Button
variant={'primary'}
onClick={this.handleOk}
disabled={this.state.selectedIndex === null}
className={'primary'}
>{'OK'}</Button>
</Col>
</Row>
</div>
</Modal.Footer>
</Modal>
);
}
}

View File

@@ -0,0 +1,178 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
export default class Finder extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
foundInPage: false,
searchTxt: '',
};
}
componentDidMount() {
this.searchInput.focus();
// synthetic events are not working all that reliably for touch bar with esc keys
this.searchInput.addEventListener('keyup', this.handleKeyEvent);
}
static getDerivedStateFromProps(props, state) {
if (state.searchTxt) {
return {
foundInPage: Boolean(props.matches),
matches: `${props.activeMatchOrdinal}/${props.matches}`,
};
}
return {matches: '0/0'};
}
findNext = () => {
this.props.findInPage(this.state.searchTxt, {
forward: true,
findNext: true,
});
};
find = (keyword) => {
this.props.stopFindInPage('clearSelection');
if (keyword) {
this.props.findInPage(keyword);
} else {
this.setState({
matches: '0/0',
});
}
};
findPrev = () => {
this.props.findInPage(this.state.searchTxt, {forward: false, findNext: true});
}
searchTxt = (event) => {
this.setState({searchTxt: event.target.value});
this.find(event.target.value);
}
handleKeyEvent = (event) => {
if (event.code === 'Escape') {
this.close();
} else if (event.code === 'Enter') {
this.findNext();
}
}
close = () => {
this.searchInput.removeEventListener('keyup', this.handleKeyEvent);
this.props.stopFindInPage('clearSelection');
this.props.close();
}
render() {
return (
<div
id='finder'
onClick={this.props.focus}
>
<div className='finder'>
<div className='finder-input-wrapper'>
<input
className='finder-input'
placeholder=''
value={this.state.searchTxt}
onChange={this.searchTxt}
ref={(input) => {
this.searchInput = input;
}}
/>
<span className={this.state.foundInPage ? 'finder-progress' : 'finder-progress finder-progress__disabled'}>{this.state.matches}</span>
</div>
<button
className='finder-prev'
onClick={this.findPrev}
disabled={!this.state.searchTxt}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
className='icon'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<polyline points='18 15 12 9 6 15'/>
</svg>
</button>
<button
className='finder-next'
onClick={this.findNext}
disabled={!this.state.searchTxt}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
className='icon arrow-up'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<polyline points='6 9 12 15 18 9'/>
</svg>
</button>
<button
className='finder-close'
onClick={this.close}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
className='icon'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<line
x1='18'
y1='6'
x2='6'
y2='18'
/>
<line
x1='6'
y1='6'
x2='18'
y2='18'
/>
</svg>
</button>
</div>
</div>
);
}
}
Finder.propTypes = {
close: PropTypes.func,
findInPage: PropTypes.func,
stopFindInPage: PropTypes.func,
activeMatchOrdinal: PropTypes.number,
matches: PropTypes.number,
focus: PropTypes.func,
};

View File

@@ -0,0 +1,73 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import {FIND_IN_PAGE, STOP_FIND_IN_PAGE, CLOSE_FINDER, FOUND_IN_PAGE, FOCUS_FINDER} from 'common/communication.js';
import Finder from './finder.jsx';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
import 'renderer/css/components/Finder.css';
const closeFinder = () => {
window.postMessage({type: CLOSE_FINDER}, window.location.href);
};
const findInPage = (searchText, options) => {
window.postMessage({type: FIND_IN_PAGE, data: {searchText, options}}, window.location.href);
};
const stopFindInPage = (action) => {
window.postMessage({type: STOP_FIND_IN_PAGE, data: action}, window.location.href);
};
const focusFinder = () => {
window.postMessage({type: FOCUS_FINDER}, window.location.href);
};
class FinderRoot extends React.PureComponent {
constructor() {
super();
this.state = {};
}
componentDidMount() {
window.addEventListener('message', this.handleMessageEvent);
}
componentWillUnmount() {
window.removeEventListener('message', this.handleMessageEvent);
}
handleMessageEvent = (event) => {
if (event.data.type === FOUND_IN_PAGE) {
this.setState({
activeMatchOrdinal: event.data.data.activeMatchOrdinal,
matches: event.data.data.matches,
});
}
}
render() {
return (
<Finder
activeMatchOrdinal={this.state.activeMatchOrdinal}
matches={this.state.matches}
close={closeFinder}
focus={focusFinder}
findInPage={findInPage}
stopFindInPage={stopFindInPage}
/>
);
}
}
const start = async () => {
ReactDOM.render(
<FinderRoot/>,
document.getElementById('app'),
);
};
start();

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication.js';
import LoadingScreen from '../../components/LoadingScreen.jsx';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
import 'renderer/css/components/LoadingAnimation.css';
import 'renderer/css/components/LoadingScreen.css';
class LoadingScreenRoot extends React.PureComponent {
constructor() {
super();
this.state = {
showLoadingScreen: true,
darkMode: false,
};
}
componentDidMount() {
window.postMessage({type: GET_LOADING_SCREEN_DATA}, window.location.href);
window.addEventListener('message', this.handleMessageEvent);
}
componentWillUnmount() {
window.removeEventListener('message', this.handleMessageEvent);
}
handleMessageEvent = (event) => {
if (event.data.type === RECEIVED_LOADING_SCREEN_DATA) {
this.setState({
darkMode: event.data.data.darkMode,
});
}
if (event.data.type === TOGGLE_LOADING_SCREEN_VISIBILITY) {
this.setState({
showLoadingScreen: event.data.data,
});
}
}
onFadeOutComplete = () => {
window.postMessage({type: LOADING_SCREEN_ANIMATION_FINISHED}, window.location.href);
}
render() {
return (
<LoadingScreen
loading={this.state.showLoadingScreen}
darkMode={this.state.darkMode}
onFadeOutComplete={this.onFadeOutComplete}
/>
);
}
}
const start = async () => {
ReactDOM.render(
<LoadingScreenRoot/>,
document.getElementById('app'),
);
};
start();

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
import LoginModal from './loginModal.jsx';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
const handleLoginCancel = (request) => {
window.postMessage({type: MODAL_CANCEL, data: {request}}, window.location.href);
};
const handleLogin = (request, username, password) => {
window.postMessage({type: MODAL_RESULT, data: {request, username, password}}, window.location.href);
};
const getAuthInfo = () => {
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
};
const start = async () => {
ReactDOM.render(
<LoginModal
onLogin={handleLogin}
onCancel={handleLoginCancel}
getAuthInfo={getAuthInfo}
/>,
document.getElementById('app'),
);
};
start();

View File

@@ -0,0 +1,156 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Col, ControlLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap';
import {MODAL_INFO} from 'common/communication';
import urlUtils from 'common/utils/url';
export default class LoginModal extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
request: null,
authInfo: null,
};
}
componentDidMount() {
window.addEventListener('message', this.handleAuthInfoMessage);
this.props.getAuthInfo();
}
componentWillUnmount() {
window.removeEventListener('message', this.handleAuthInfoMessage);
}
handleAuthInfoMessage = (event) => {
switch (event.data.type) {
case MODAL_INFO: {
const {request, authInfo} = event.data.data;
this.setState({request, authInfo});
break;
}
default:
break;
}
}
handleSubmit = (event) => {
event.preventDefault();
this.props.onLogin(this.state.request, this.state.username, this.state.password);
this.setState({
username: '',
password: '',
request: null,
authInfo: null,
});
}
handleCancel = (event) => {
event.preventDefault();
this.props.onCancel(this.state.request);
this.setState({
username: '',
password: '',
request: null,
authInfo: null,
});
}
setUsername = (e) => {
this.setState({username: e.target.value});
}
setPassword = (e) => {
this.setState({password: e.target.value});
}
render() {
let theServer = '';
if (!(this.state.request && this.state.authInfo)) {
theServer = '';
} else if (this.state.authInfo.isProxy) {
theServer = `The proxy ${this.state.authInfo.host}:${this.state.authInfo.port}`;
} else {
const tmpURL = urlUtils.parseURL(this.state.request.url);
theServer = `The server ${tmpURL.protocol}//${tmpURL.host}`;
}
const message = `${theServer} requires a username and password.`;
return (
<Modal show={Boolean(this.state.request && this.state.authInfo)}>
<Modal.Header>
<Modal.Title>{'Authentication Required'}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>
{ message }
</p>
<Form
horizontal={true}
onSubmit={this.handleSubmit}
>
<FormGroup>
<Col
componentClass={ControlLabel}
sm={2}
>{'User Name'}</Col>
<Col sm={10}>
<FormControl
type='text'
placeholder='User Name'
onChange={this.setUsername}
value={this.state.username}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Col>
</FormGroup>
<FormGroup>
<Col
componentClass={ControlLabel}
sm={2}
>{'Password'}</Col>
<Col sm={10}>
<FormControl
type='password'
placeholder='Password'
onChange={this.setPassword}
value={this.state.password}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Col>
</FormGroup>
<FormGroup>
<Col sm={12}>
<div className='pull-right'>
<Button
type='submit'
bsStyle='primary'
>{'Login'}</Button>
{ ' ' }
<Button onClick={this.handleCancel}>{'Cancel'}</Button>
</div>
</Col>
</FormGroup>
</Form>
</Modal.Body>
</Modal>
);
}
}
LoginModal.propTypes = {
onCancel: PropTypes.func,
onLogin: PropTypes.func,
getAuthInfo: PropTypes.func,
};

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
import React from 'react';
import ReactDOM from 'react-dom';
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication.js';
import NewTeamModal from '../../components/NewTeamModal.jsx'; //'./addServer.jsx';
const onClose = () => {
window.postMessage({type: MODAL_CANCEL}, window.location.href);
};
const onSave = (data) => {
window.postMessage({type: MODAL_RESULT, data}, window.location.href);
};
const start = async () => {
ReactDOM.render(
<NewTeamModal
onClose={onClose}
onSave={onSave}
editMode={false}
show={true}
url={decodeURIComponent(urlParams.get('url'))}
/>,
document.getElementById('app'),
);
};
start();

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication.js';
import PermissionModal from './permissionModal.jsx';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
const handleDeny = () => {
window.postMessage({type: MODAL_CANCEL}, window.location.href);
};
const handleGrant = () => {
window.postMessage({type: MODAL_RESULT}, window.location.href);
};
const getPermissionInfo = () => {
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
};
const openExternalLink = (protocol, url) => {
window.postMessage({type: MODAL_SEND_IPC_MESSAGE, data: {type: 'confirm-protocol', args: [protocol, url]}}, window.location.href);
};
const start = async () => {
ReactDOM.render(
<PermissionModal
getPermissionInfo={getPermissionInfo}
handleDeny={handleDeny}
handleGrant={handleGrant}
openExternalLink={openExternalLink}
/>,
document.getElementById('app'),
);
};
start();

View File

@@ -0,0 +1,109 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Modal, Button} from 'react-bootstrap';
import PropTypes from 'prop-types';
import urlUtil from 'common/utils/url';
import {MODAL_INFO} from 'common/communication';
import {PERMISSION_DESCRIPTION} from 'common/permissions';
export default class PermissionModal extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
window.addEventListener('message', this.handlePermissionInfoMessage);
this.props.getPermissionInfo();
}
componentWillUnmount() {
window.removeEventListener('message', this.handlePermissionInfoMessage);
}
handlePermissionInfoMessage = (event) => {
switch (event.data.type) {
case MODAL_INFO: {
const {url, permission} = event.data.data;
this.setState({url, permission});
break;
}
default:
break;
}
}
getModalTitle() {
return `${PERMISSION_DESCRIPTION[this.state.permission]} Required`;
}
getModalBody() {
const {url, permission} = this.state;
const originDisplay = url ? urlUtil.getHost(url) : 'unknown origin';
const originLink = url ? originDisplay : '';
const click = (e) => {
e.preventDefault();
let parseUrl;
try {
parseUrl = urlUtil.parseURL(originLink);
this.props.openExternalLink(parseUrl.protocol, originLink);
} catch (err) {
console.error(`invalid url ${originLink} supplied to externallink: ${err}`);
}
};
return (
<div>
<p>
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission]}.`}
</p>
<p>
<span>{'This request originated from '}</span>
<a onClick={click}>{originDisplay}</a>
</p>
</div>
);
}
render() {
return (
<Modal
bsClass='modal'
className='permission-modal'
show={Boolean(this.state.url && this.state.permission)}
id='requestPermissionModal'
enforceFocus={true}
>
<Modal.Header>
<Modal.Title>{this.getModalTitle()}</Modal.Title>
</Modal.Header>
<Modal.Body>
{this.getModalBody()}
</Modal.Body>
<Modal.Footer className={'remove-border'}>
<div>
<Button
onClick={this.props.handleDeny}
>{'Cancel'}</Button>
<Button
bsStyle='primary'
onClick={this.props.handleGrant}
>{'Accept'}</Button>
</div>
</Modal.Footer>
</Modal>
);
}
}
PermissionModal.propTypes = {
handleDeny: PropTypes.func,
handleGrant: PropTypes.func,
getPermissionInfo: PropTypes.func,
openExternalLink: PropTypes.func,
};

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<body>
<div id="app" />
</body>
</html>

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import 'renderer/css/components/HoveringURL.css';
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
import React from 'react';
import ReactDOM from 'react-dom';
import UrlDescription from '../../components/urlDescription.jsx';
const start = async () => {
ReactDOM.render(
<UrlDescription
url={decodeURIComponent(urlParams.get('url'))}
/>,
document.getElementById('app'),
);
};
start();

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {throttle} from 'underscore';
import ding from 'static/sounds/ding.mp3';
import bing from 'static/sounds/bing.mp3';
import crackle from 'static/sounds/crackle.mp3';
import down from 'static/sounds/down.mp3';
import hello from 'static/sounds/hello.mp3';
import ripple from 'static/sounds/ripple.mp3';
import upstairs from 'static/sounds/upstairs.mp3';
export const DEFAULT_WIN7 = 'Ding';
const notificationSounds = new Map([
[DEFAULT_WIN7, ding],
['Bing', bing],
['Crackle', crackle],
['Down', down],
['Hello', hello],
['Ripple', ripple],
['Upstairs', upstairs],
]);
export const playSound = throttle((soundName) => {
if (soundName) {
const audio = new Audio(notificationSounds.get(soundName));
audio.play();
}
}, 3000, {trailing: false});

43
src/renderer/settings.jsx Normal file
View File

@@ -0,0 +1,43 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import {ipcRenderer} from 'electron';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/index.css';
import 'renderer/css/settings.css';
if (process.env.NODE_ENV === 'production') {
window.eval = global.eval = () => { // eslint-disable-line no-multi-assign, no-eval
throw new Error('Sorry, Mattermost does not support window.eval() for security reasons.');
};
} else if (module.hot) {
module.hot.accept();
}
import React from 'react';
import ReactDOM from 'react-dom';
import SettingsPage from './components/SettingsPage.jsx';
function openMenu() {
if (process.platform !== 'darwin') {
ipcRenderer.send('open-app-menu');
}
}
const start = async () => {
ReactDOM.render(
<SettingsPage
openMenu={openMenu}
/>,
document.getElementById('app'),
);
};
// Deny drag&drop navigation in mainWindow.
document.addEventListener('dragover', (event) => event.preventDefault());
document.addEventListener('drop', (event) => event.preventDefault());
start();

13
src/renderer/updater.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Updater</title>
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="css/components/index.css">
</head>
<body>
<div id="content"></div>
<script src="updater_bundle.js"></script>
</body>
</html>

157
src/renderer/updater.jsx Normal file
View File

@@ -0,0 +1,157 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import url from 'url';
import React from 'react';
import ReactDOM from 'react-dom';
import propTypes from 'prop-types';
import {ipcRenderer, remote} from 'electron';
import UpdaterPage from './components/UpdaterPage.jsx';
const thisURL = url.parse(location.href, true);
const notifyOnly = thisURL.query.notifyOnly === 'true';
class UpdaterPageContainer extends React.PureComponent {
constructor(props) {
super(props);
this.state = props.initialState;
}
getTabWebContents() {
return remote.webContents.getFocusedWebContents();
}
componentDidMount() {
ipcRenderer.on('start-download', () => {
this.setState({
isDownloading: true,
});
});
ipcRenderer.on('progress', (event, progress) => {
this.setState({
progress,
});
});
ipcRenderer.on('zoom-in', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
if (activeTabWebContents.zoomLevel >= 9) {
return;
}
activeTabWebContents.zoomLevel += 1;
});
ipcRenderer.on('zoom-out', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
if (activeTabWebContents.zoomLevel <= -8) {
return;
}
activeTabWebContents.zoomLevel -= 1;
});
ipcRenderer.on('zoom-reset', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
activeTabWebContents.zoomLevel = 0;
});
ipcRenderer.on('undo', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
activeTabWebContents.undo();
});
ipcRenderer.on('redo', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
activeTabWebContents.redo();
});
ipcRenderer.on('cut', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
activeTabWebContents.cut();
});
ipcRenderer.on('copy', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
activeTabWebContents.copy();
});
ipcRenderer.on('paste', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
activeTabWebContents.paste();
});
ipcRenderer.on('paste-and-match', () => {
const activeTabWebContents = this.getTabWebContents();
if (!activeTabWebContents) {
return;
}
activeTabWebContents.pasteAndMatchStyle();
});
}
render() {
return (
<UpdaterPage
appName={`${remote.app.name} Desktop App`}
notifyOnly={this.props.notifyOnly}
{...this.state}
onClickReleaseNotes={() => {
ipcRenderer.send('click-release-notes');
}}
onClickSkip={() => {
ipcRenderer.send('click-skip');
}}
onClickRemind={() => {
ipcRenderer.send('click-remind');
}}
onClickInstall={() => {
ipcRenderer.send('click-install');
}}
onClickDownload={() => {
ipcRenderer.send('click-download');
}}
onClickCancel={() => {
ipcRenderer.send('click-cancel');
}}
/>
);
}
}
UpdaterPageContainer.propTypes = {
notifyOnly: propTypes.bool,
initialState: propTypes.object,
};
ReactDOM.render(
<UpdaterPageContainer
notifyOnly={notifyOnly}
initialState={{isDownloading: false, progress: 0}}
/>,
document.getElementById('content'),
);