Compare commits

...

254 Commits

Author SHA1 Message Date
Akash Nimare
411150fd72 i18n: Add test i18n framework. 2018-03-01 02:29:57 +05:30
Akash Nimare
d673d5b76c 🎉 v1.8.2. 2018-02-26 19:18:23 +05:30
Akash Nimare
2bf88aa912 webview: Remove loading indicator once webview is loaded. 2018-02-26 18:36:25 +05:30
Akash Nimare
c9f179a2ae node: Enforce LTS node v8.0.0. 2018-02-26 18:28:16 +05:30
Akash Nimare
c11089027c left-sidebar: Align action container properly. 2018-02-26 18:09:42 +05:30
Abhigyan Khaund
5c45ab7b66 fix: New organization link overlapping existing servers.
Fixes: #427.
2018-02-26 17:28:59 +05:30
Akash Nimare
6205ca0aca Update shortcut for opening webapp keyboard shortcuts. 2018-02-24 01:27:28 +05:30
Abhigyan Khaund
b83e2dd428 settings-tab: Do not create a sidebar tab rather highlight bottom settings icon. (#422)
Creating the new functional tab for setting tab was unnecessary as we already have a sticky setting icon at the bottom. This PR adds a functionality to highlight that settings icon instead of creating the new one.

Fixes: #418.
2018-02-23 19:03:30 +05:30
Akash Nimare
fd421a62d2 webview: Improve flickering of webview on first load. 2018-02-23 02:03:18 +05:30
Abhigyan Khaund
fa85241c79 design: Only show the back button when needed.
Fixes: #421.
2018-02-23 00:53:23 +05:30
Akash Nimare
b163c237b6 Do not auto-reload app when system comes back from sleep.
Improves #412, #312.
2018-02-22 20:55:55 +05:30
Abhigyan Khaund
5351ee10fa auto-updates: Show a notification when a new update available [Linux].
[Linux] - This PR adds a feature to notify the user whenever a new update is available.

Fixes: #398.
2018-02-22 16:55:45 +05:30
Priyank P
d48b6ae80d reconnect: Check wheather internet is working before reloading. (#415)
When the online event is triggered check whether the internet is actually working or not.
Commonly on windows, it turns out that internet takes couple of seconds to boot up after
connecting to the internet or in some cases, this might be they have to sign in to internet service
portal in order to access the internet.
2018-02-21 16:56:01 +05:30
Akash Nimare
2d07d40c92 activetab: Fix broken last active tab. (#420)
This fixes an issue where the server tabs were not loading
according to the last active tab. This used to load last server
no matter what the last used server is.

Fixes - #416.
2018-02-18 17:52:26 +05:30
Akash Nimare
55ae71c00c travis: Fix travis not failing on linting errors. 2018-02-15 19:58:08 +05:30
cPhost
0c18bb5226 menu.js: Fix linting error. 2018-02-15 19:55:37 +05:30
Abhigyan Khaund
6d213c86a8 design: Add back button in left-sidebar.
Fixes: #208.
2018-02-14 14:01:35 +05:30
Akash Nimare
0c1cd96ed7 Remove transition property from checkbox input.
That delay can cause the setting button to not change/switch properly.
2018-02-13 17:23:39 +05:30
Priyank P
d77b1b5960 electron-connect: Disable electron-connect when it's run from npm start.
Improves gulp dev script.
2018-02-13 00:07:44 +05:30
Akash Nimare
2c6c0c8c8d Update electron to v1.8.2. 2018-02-11 22:36:32 +05:30
Akash Nimare
84bf59d32f Update package-lock.json files. 2018-02-11 22:08:50 +05:30
Priyank P
256c352fb9 windows: Add tools support with batch scripts.
Add tools support with batch scripts.

Fixes - #397.
2018-02-09 18:19:09 +05:30
Akash Nimare
da28589c17 menu: Add menu-item to reveal app logs in file manager #405. 2018-02-03 02:04:36 +05:30
Akash Nimare
c452ee2ef6 menu: Improve help menu. 2018-02-03 01:37:13 +05:30
Abhigyan Khaund
3582aa4694 menu: Rename Settings to Desktop App Settings. 2018-02-02 17:07:12 +05:30
akashnimare
7f7eee2455 setting-page: design improvemnets. 2018-01-31 21:36:39 +05:30
akashnimare
c716b8f233 notification: Refactor code for bot mention in reply. 2018-01-31 01:34:46 +05:30
Abhigyan Khaund
4a40c75127 notifications: Adds bots mention support to reply option. (#395)
Fixes: #391.
2018-01-31 01:26:11 +05:30
Balaji
239631a2b6 tools: Add scripts for review Pull Requests. (#399)
* tools: Add script to fetch-pull-request.

Improves #397.

* tools: Add script to fetch-rebase-pull-request.

Improves #397.
2018-01-30 17:11:10 +05:30
Abhigyan Khaund
89d1344e2f preference page: Add a Loading indication for new server button. (#401)
Change the text of "Add" button to "Adding..." when a user clicks on Add button for adding new server.

Fixes: #396.
2018-01-29 23:53:10 +05:30
akashnimare
1948ba2cc3 menu: Fix Zoom In shortcut.
This is a temporary fix. Ideally, 'CmdOrCtrl+Plus' works on all
the platforms but because of https://github.com/electron/electron/issues/6731
it converts accelerator to 'Ctrl+Shift+Plus'.
2018-01-27 19:56:27 +05:30
Abhigyan Khaund
b8da7dd6ee gulp-dev: Add nodeman to dev script and fix renderer reload on changes.
Fixes: #368.
2018-01-23 00:29:15 +05:30
akashnimare
4a0efb7301 Code refactoring. 2018-01-23 00:05:15 +05:30
Priyank P
aedd95259d preference: Only toggle the state if element is present.
Fixes #393.
2018-01-22 22:38:01 +05:30
Priyank P
c8d7a79877 design: Improve preference design. 2018-01-17 18:34:24 +05:30
akashnimare
6e6db42b54 v1.8.1 2018-01-17 02:16:04 +05:30
akashnimare
db79284fbb fix: Don't import appId from package file #386. 2018-01-17 01:54:20 +05:30
akashnimare
2434f06655 release: 🎉 new-release v1.8.0. 2018-01-15 20:59:28 +05:30
akashnimare
1d611d3382 setting-page: Update placeholder for adding new server page.
Improves #340.
2018-01-15 20:55:51 +05:30
akashnimare
a746194e9e help-menu: Add a space between app name and version. 2018-01-15 18:45:45 +05:30
akashnimare
7cc13f7a26 Update dependencies.
Updated:
electron - 1.7.10
electron-builder - 19.53.6
electron-updater - 2.18.2
2018-01-15 17:46:33 +05:30
Akash Nimare
6a9bb152a0 Update features section. 2018-01-14 01:11:18 +05:30
Priyank P
8b6dcd355f notification: Add reply option to notifications for macOS.
This PR adds reply option to notifications of macOS using
`node-mac-notifier` and then post the reply for to the webapp.
It also fixes an issue that even though the app is focused the server that sent
the notification did not focus. And it also adds parsing for mentioning. This also
refactors code for notification.

Fixes: #284, #381.
2018-01-14 00:04:34 +05:30
Priyank P
91742a5770 silent: Reflect changes in webview for silent option. (#380)
* silent: Reflect changes in webview for mute/silent option.

This silent the webview incase silent option is toggled, and
by default silent the webview when its create if needed.

Fixes: #380.
2018-01-11 18:46:39 +05:30
akashnimare
fb74251a2c performance: Disable hardware acceleration to decrease the load on GPU.
Adding this experimental electron api to see if it makes any difference
in performance.

Improves #213.
2018-01-08 02:10:43 +05:30
akashnimare
a920720f91 setting-page: Improve add new server page #340. 2018-01-07 15:10:21 +05:30
Priyank P
aa8e99b7a6 domain-util: Fix checkDomain, so it checks all error codes. (#370)
This fixes an issue where if server send non 404 error code such
as 403 forbidden we marked them as Zulip server even though they are
not, now it checks for 400 error range.
2018-01-07 00:16:52 +05:30
akashnimare
e23f8aaa58 shortcut: Rename Zoom In keyboard shortcut. 2018-01-06 16:30:59 +05:30
Akash Nimare
5c3208d44c settings: Add a setting option to start the app in background. (#366)
Fixes #314.
2017-12-29 12:37:53 +05:30
akashnimare
c0b57bbe2b settings: Set default value of flashtaskbar setting[Windows]. 2017-12-28 00:29:16 +05:30
Priyank P
afe4e8901b github templates: Add pull request template and update issue template. (#365) 2017-12-27 23:15:02 +05:30
Priyank P
231e7fd9c2 preferences page: Reflect changes in the preference page. (#362)
This updated the setting page if the sidebar was toggled using a shortcut.
This also updates the setting page if the tray was toggled using menu.

Fixes: #304.
2017-12-27 21:06:32 +05:30
Akash Nimare
a0d898a5b7 Merge pull request #363 from cPhost/logger
logger-util: Code refactoring and better logs design.
2017-12-27 20:45:13 +05:30
cPhost
1abf62555c logger-util: code refactoring and better logs design.
This imporves logging and refactors most of the code.
This also renames console-util to logger-util.
2017-12-26 22:03:05 -05:00
Akash Nimare
6befcbaa8f Merge pull request #361 from cPhost/focus-app
notifications: Focus app when a notification is triggered.
2017-12-27 01:08:32 +05:30
cPhost
e56a01049b notifications: Focus app when a notification is triggered.
This PR adds a feature of showing app window when the notification are clicked
Fixes: #358
2017-12-26 14:30:33 -05:00
akashnimare
72cb8459ff code refactoring. 2017-12-26 01:02:42 +05:30
Akash Nimare
0b83b22206 Merge pull request #353 from cPhost/fix-errors
default-util: Fix log dir errors.
2017-12-26 00:21:35 +05:30
Akash Nimare
267d25e5c4 Merge pull request #357 from cPhost/img-fix
Add default icon if the server image is not available.
2017-12-25 23:43:35 +05:30
cPhost
8401f8f5ce server icon: Load default icon if org icon is not avalible. 2017-12-25 12:17:45 -05:00
cPhost
c4a7264f34 console: Fix errors where Logs dir can't be created. 2017-12-25 09:50:52 -05:00
akashnimare
9d081ecd5a Reload full app on system hibernation.
Improves #312.
2017-12-20 02:33:08 +05:30
akashnimare
dc6582fa82 Logs: Unused Logs dir. 2017-12-19 03:42:42 +05:30
akashnimare
3b412672c6 Logs: Fix an issue where Logs dir don't get init properly. 2017-12-19 03:40:24 +05:30
Akash Nimare
04083bfa81 Merge pull request #352 from zulip/cPhost-handle-domainjson
Handle corrupted config files.
Improves #310.
2017-12-19 02:33:39 +05:30
akashnimare
562e82d2f1 test: Create logs dir on app startup. 2017-12-19 01:22:50 +05:30
cPhost
3b014e0715 settings util: delete settings.json file in case it is corrupted. 2017-12-17 15:17:08 -05:00
cPhost
13178ebc8f domain util: delete domain.json file in case it is corrupted. 2017-12-17 15:16:55 -05:00
cPhost
08693bf105 console: require app form remote if required 2017-12-17 15:00:04 -05:00
akashnimare
d7a0b63d62 test: fix failing e2e test on travis 2017-12-13 20:05:07 +05:30
akashnimare
a193ecf229 design: update new server page #340. 2017-12-13 18:54:31 +05:30
akashnimare
31f04754a4 design: fixed positioning of create-new-org link 2017-12-11 16:08:23 +05:30
akashnimare
ae7374475f design: Update UI for server section #340 2017-12-11 15:42:50 +05:30
akashnimare
7697d5d698 electron: update electron to v1.7.9 #213 2017-12-11 15:09:56 +05:30
akashnimare
d3e1b5de45 update app configs 2017-12-06 19:46:24 +05:30
Akash Nimare
9efa6191f7 Merge pull request #349 from zulip/improve-debian-installer
linux: Improve debian installer
2017-12-06 18:17:57 +05:30
akashnimare
d86797d2fc debian: replace bash shebang with '#!/usr/bin/env bash" for portability.
According to the debian config rules '#!/usr/bin/env bash' violates
the quality standard which isn't allowed.

More info - https://github.com/Microsoft/vscode/issues/35638
2017-12-05 23:58:26 +05:30
akashnimare
23eef7edb0 debian: remove app settings on uninstall 2017-12-05 22:15:18 +05:30
akashnimare
a1d5a35ccf debian: Remove config files when user uninstalls app 2017-12-05 21:01:26 +05:30
akashnimare
62e8dfe180 Rename afterRemove script to debian uninstaller 2017-12-05 20:54:39 +05:30
Akash Nimare
8322054984 Merge pull request #347 from cPhost/deb
debian-installer: add apt repo for debian
2017-12-05 18:42:15 +05:30
Akash Nimare
13ae6f07e9 Add comment for adding apt repo 2017-12-05 18:41:06 +05:30
cPhost
dab92be54c debian installer: remove the apt repo after unistallation 2017-12-05 07:29:45 -05:00
cPhost
fe9c66d8c2 debian-installer: add apt repo for debian 2017-12-05 07:29:34 -05:00
akashnimare
43b4d511dc init default settings properly fixes #348 2017-12-04 17:19:25 +05:30
akashnimare
aa5a47ad53 Make app window thinner fixes #332 2017-12-04 15:19:43 +05:30
Akash Nimare
ce27f92900 Merge pull request #346 from cPhost/about-page
about-page: add links for zulip-electron and license.
2017-12-04 14:54:13 +05:30
cPhost
7be051bb6e about-page: center everything, add links for zulip-electron, license 2017-12-03 18:02:34 -05:00
Akash Nimare
c2a01adabe Merge pull request #345 from cPhost/logger
logger: add console helper to log for both file and console
2017-12-04 01:13:07 +05:30
cPhost
770926e6eb logger: add console helper to log for both file and console 2017-12-02 11:58:28 -05:00
Akash Nimare
10ef627f59 Add notifications troubleshooting 2017-12-01 20:43:52 +05:30
akashnimare
4adba0f4b4 Update electron-builder to v19.46.4 2017-11-24 17:00:03 +05:30
akashnimare
ed590c26e3 Added whitelist zulip server 2017-11-23 22:38:28 +05:30
akashnimare
6d10291a87 🎉 v1.7.0 2017-11-22 20:10:59 +05:30
akashnimare
3fe3a3da85 Update electron to v1.6.15 2017-11-22 16:21:39 +05:30
akashnimare
ba64438a99 Improve Add Server page for new users [WIP] #340 2017-11-22 14:39:39 +05:30
Akash Nimare
bcc27894c4 Merge pull request #337 from SimplyAhmazing/setup-e2e-tests
setup e2e tests
2017-11-22 13:28:31 +05:30
Akash Nimare
0dd0f593d1 Merge pull request #338 from sroy8091/issue_#336
Better error message on invalid server fixes #336
2017-11-21 21:08:08 +05:30
Sumit Roy
f5e9342f78 fail to connect message #336 2017-11-21 11:31:42 +05:30
simplyahmazing
083ccdf229 remove unused imports 2017-11-20 11:14:22 -05:00
simplyahmazing
1261786db2 remove unused dependencies 2017-11-20 11:06:05 -05:00
simplyahmazing
7f567f55c3 setup e2e tests 2017-11-19 16:48:21 -05:00
akashnimare
dcd2abca6e v1.6.0-beta 2017-11-16 00:32:32 +05:30
Akash Nimare
2fb9efb981 Merge pull request #335 from zulip/electron-shortcut-fix
Remove electron-localshortcut completely
2017-11-15 16:56:42 +05:30
akashnimare
7245b6a110 Move electron-debug to devDependencies 2017-11-15 04:59:59 +05:30
akashnimare
bcb8ffb55f Remove electron-LocalShortcut completely
This commit removes the usage of "electron-localshortcut" completely.
Now, we rely on only menu accelerators for shortcuts. It's risky to register a local shortcuts in the app window
either using electronLocalShortcut or globalShortcut as the registered shortcuts could interfare with
OS global shortcuts which is very frustrating. This fixes #317 once and for all.
2017-11-15 03:39:51 +05:30
akashnimare
77094596a5 Use electron-debug in development only WIP #317
electron-debug hijacks the CMD/CTRL+R and reloads the whole app,
whereas we only need to reload the current server. Removed those commands from
electronLocalShortcuts as well as they are already registered in menu items.
2017-11-15 03:07:36 +05:30
Akash Nimare
06ad44bdd7 Merge pull request #333 from zulip/appimage-autoupdates
Add AppImage autoupdates fixes #333
2017-11-11 20:12:24 +05:30
akashnimare
e719ba139c Update electron-builder to latest version 2017-11-11 20:10:53 +05:30
akashnimare
9853e9226c Better tray icon for retina display, fixes #330 2017-11-08 12:26:52 +05:30
akashnimare
f2c76b5ca3 Enable auto-update on Linux (AppImage) 2017-11-04 01:31:45 +05:30
Akash Nimare
e6dbff995b Merge pull request #327 from zulip/taskbar-setting-option
Setting option for Windows taskbar flash fixes #299
2017-11-03 00:09:58 +05:30
akashnimare
4578d4a5f7 typo in setting 2017-11-03 00:06:34 +05:30
akashnimare
4b895a2312 Don't show flash taskbar setting on Linux/macOS 2017-11-02 23:54:38 +05:30
akashnimare
53c0428a3a Add setting to control Windows taskbar flashing #299 2017-11-02 20:58:35 +05:30
akashnimare
0a1866abb5 Show Detailed error message on invaild Zulip server #325 2017-11-01 17:59:00 +05:30
Akash Nimare
ce862a4890 Merge pull request #326 from zulip/remove-python-version-file
Remove python version file

Fixes failing tests on Travis Linux
2017-10-31 20:55:17 +05:30
simplyahmazing
1b1ad2cd61 remove .python-version file from repo 2017-10-28 20:06:56 -04:00
simplyahmazing
ead7a06308 ignore .python-version files 2017-10-28 20:06:26 -04:00
akashnimare
6659dd5097 Update electron-builder & updater to latest 2017-10-26 11:54:55 +05:30
akashnimare
ed1f0f6d5b Update electron-builder & updater to latest 2017-10-24 18:38:00 +05:30
akashnimare
79acf8a6e1 Add option to remove app settings
Menu item "Reset app settings" now remove all the configurations/settings files related to app.
Previously it used to remove only window-state.json. This helps a bit in #310.
2017-10-24 18:29:35 +05:30
akashnimare
8e0033f03e Handle certificate issuer error
Fixes, #316
2017-10-21 01:47:16 +05:30
Akash Nimare
9144c2630d Merge pull request #322 from zulip/spellchecker-osx-fix
Spellchecker Improvements
2017-10-20 23:52:24 +05:30
akashnimare
fae05fc3b1 Initialize default app settings
Settings are initialized only when user clicks on General/Server/Network section settings
In case, user doesn't visit these section, those values set to be null automatically.
This fix makes sure the default settings are correctly set to either true or false.
2017-10-18 21:38:48 +05:30
akashnimare
73603a4fd2 Add settings to disable/enable spellchecker 2017-10-18 21:36:01 +05:30
akashnimare
a498ffc7d6 Update spellchecker to v1.1.2 2017-10-18 04:17:51 +05:30
akashnimare
7afcf13401 Re-write and improve spellchecker class
Rewrote the Spellchecker class so that we can have better control
over the context menu and spellchecker.
2017-10-18 04:14:02 +05:30
akashnimare
89a292559d Set English as default language for spellchecker on Linux/Windows 2017-10-17 21:57:51 +05:30
akashnimare
be14517caf Set server language for spellchecker on macOS
Ideally spellchecker should detect the language, but on macOS, it fails to auto-detect the lanugage user is typing in
that's why we need to mention it explicitly. We set this language with the help of the default language of the server.
2017-10-17 00:29:50 +05:30
akashnimare
3b6c5ae532 🎉 v1.5.0 2017-10-11 12:47:17 +05:30
akashnimare
40e3ed0f2f Reload current view properly [WIP] 2017-10-10 16:35:43 +05:30
Akash Nimare
5d988858b0 Merge pull request #313 from YJDave/tooltip
Add tooltip for add server button
2017-10-10 15:40:24 +05:30
YJDave
3a974136a3 Tooltip for add organization icon 2017-10-07 14:08:26 +05:30
akashnimare
6ed5a5309c Load correct Active tab
We need to deactivate the tab if it doesn't match with previously loaded active tab.
2017-10-06 02:59:16 +05:30
akashnimare
80c37fabb8 Enable badge api on macOS only
app.dock.setBadge() is supported on macOS only. Added an extra check so that it doesn't throw errors on Win + Linux.
2017-10-05 05:34:16 +05:30
akashnimare
79366e19df Remove unnecessary logging 2017-10-05 05:28:26 +05:30
akashnimare
f409bb0449 Handle reload event correctly
Added the functionality to remember the last active tab. Previously, we used to load the first tab no matter what.
Also, when user adds a new server the same server will be activated.
Reloading the app will now reload the current view/server only.

Fixes #311, #308
2017-10-05 05:21:34 +05:30
akashnimare
45bdde951f Add a red circle over dock icon for PMs
This will show a small red circle over the dock icon.
This is to notify user that they have PMs in unread messages.
2017-10-03 03:29:52 +05:30
akashnimare
6b627780f0 Fetch correct organization icon from server_settings API
Some Zulip Servers use absolute URL for server icon whereas others use relative URL.
I have added an extra check to handle both the cases. Improves #308.
2017-10-03 00:11:41 +05:30
akashnimare
6f67553da5 update electron to v1.6.14 2017-10-02 22:01:28 +05:30
Akash Nimare
2e710a9322 Merge pull request #309 from zulip/org-server-icon
Show server-info on hovering the server-icons
2017-10-01 03:29:29 +05:30
akashnimare
91f3afa8fe Show server-info on hovering the server-icons 2017-10-01 02:51:50 +05:30
akashnimare
f784345495 Fix sidebar tooltip
This commit fixes an issue which was caused by the recent changes in left-sidebar styling.
Due to transform property the tooltip of action-buttons (reload, setting) was hidden on hover.
2017-10-01 02:06:08 +05:30
akashnimare
67da435154 Fix a typo in base notification 2017-09-30 03:34:19 +05:30
akashnimare
c89733610d Fix desktop notification control setting
This commit fixes a bug which was caused by calling the notification constructor without the args (title, opts etc).
2017-09-30 03:26:59 +05:30
akashnimare
8f272a67b5 Update desktop app installation link 2017-09-29 17:37:36 +05:30
Akash Nimare
f6c4a76138 Merge pull request #307 from aklap/change-menu-label
Rename menu label Zulip Desktop as About Zulip.
2017-09-27 18:01:22 +05:30
Alexis La Porte
b90a4c5254 Rename menu label Zulip Desktop as About Zulip.
This fix changes the menu label 'Zulip Desktop' to 'About Zulip' for clarity, ('About' is more descriptive of the content in the view associated with the label). Also to have the menu conform to convention regarding naming menu labels for Mac OS applications.

Fixes #306
2017-09-27 03:41:10 -04:00
akashnimare
a06e09e565 update electron-builder to v19.29.1 2017-09-22 15:28:33 +05:30
akashnimare
ad5bef821e UI: Remove sidebar fluctuation on switching the toggle button
Fixes #301
2017-09-21 00:48:08 +05:30
Akash Nimare
58bbd7bf30 Merge pull request #302 from zulip/revert-301-ui_enchancements
Revert "Removed the sidebar fluctuation on switching the toggle button"
2017-09-20 23:18:59 +05:30
Akash Nimare
90d080dc96 Revert "Removed the sidebar fluctuation on switching the toggle button" 2017-09-20 23:18:35 +05:30
Akash Nimare
ad3fcf585e Merge pull request #301 from Shipragupta14/ui_enchancements
Removed the sidebar fluctuation on switching the toggle button
2017-09-20 02:31:53 +05:30
Shipragupta14
4b8f216bab Removed the sidebar fluctuation on switching the toggle button 2017-09-20 02:03:39 +05:30
Akash Nimare
e620e0c428 Merge pull request #294 from ihsavru/master
Improve UI/UX of setting page
2017-09-19 17:11:18 +05:30
ihsavru
50b3151b5d corrected typos 2017-09-19 15:52:03 +05:30
ihsavru
0c32756485 changed button and switch colour 2017-09-19 10:44:39 +05:30
ihsavru
0c0835e364 changed shortcuts UI 2017-09-17 23:31:53 +05:30
ihsavru
9e962a5c44 improved toggle switches 2017-09-17 11:54:40 +05:30
ihsavru
a218f7ea64 change toggle buttons 2017-09-17 10:45:09 +05:30
ihsavru
13a7f7475a added toggle switches 2017-09-16 19:09:12 +05:30
ihsavru
48b17a1549 made settings page responsive 2017-09-16 17:39:07 +05:30
akashnimare
653598fd9e add a re-install script 2017-09-16 01:54:55 +05:30
Akash Nimare
ddbc282f49 Merge pull request #297 from cedricium/create-new-org
Added 'Create New Organization' link to the Settings page
2017-09-15 21:17:15 +05:30
Cedricium
992d92b06d Changes made based on review
Removed unnecessary comments and changed the 'Save' button in `new-server-form.js`
to 'Add'.
2017-09-15 08:19:39 -07:00
Cedricium
45867ef15e Made changes based on @rishig recommendations
Changes include:

- increasing the font size of the 'Create new organization' link
- adding more bottom margin to the link
- aligning the link text with the external navigation icon
2017-09-14 20:49:40 -07:00
Cedricium
6572c90d49 'Create New Organization' added to Settings page
This fixes #281, which will allow users to open an external link in their default
browser to create a new organization on zulipchat.com.
2017-09-14 15:13:31 -07:00
akashnimare
1ed0011c88 Added crash reported fixes #295 2017-09-13 01:24:02 +05:30
Akash Nimare
6dd79b205c Merge pull request #293 from cedricium/shortcut-settings
Adding keyboard shortcuts to Settings page
2017-09-11 20:23:11 +05:30
Cedricium
538c18fa90 Shorten 'Keyboard Shortcuts' to just 'Shortcuts' 2017-09-11 07:39:19 -07:00
Cedricium
29e347c511 List application-specific shortcuts only 2017-09-10 14:07:04 -07:00
Cedricium
ad37a5e0a6 Changed 'Ctrl/Cmd' to appropriate user OS key
If Windows or Linux, variable `userOSKey` will be 'Ctrl'. For Macs, `userOSKey`
will be '⌘' and these values will show up in place of the previous 'Ctrl/Cmd' keys.
2017-09-10 00:32:06 -07:00
Cedricium
352b775e27 Added all keyboard shortcuts
Finished adding all keyboard shortcuts to the Settings page. Styled the tables
such that they are uniform with their columns being the same
width.

At the bottom of the 'Keyboard Shortcuts' settings page, a link to the
complete keyboard shortcuts documentation
(https://chat.zulip.org/help/keyboard-shortcuts) was also added.
2017-09-09 22:39:34 -07:00
Cedricium
38cec25680 Adding keyboard shortcuts in Settings
This is the initial pass at adding keyboard shortcuts to the Settings page. In
this commit, the `ShortcutsSection` class has been created and is applied to a
newly-added 'Keyboard Shortcuts' nav item. The template for ShortcutsSection
is essentially multiple settings cards containing one table of keyboard short-
cuts organized by their underlying functionality.

The HTML `<kbd>` tag was defined in preference.css which styles the element to
look like a keyboard key, similar to StackOverflow or GitHub.
2017-09-09 19:11:19 -07:00
Akash Nimare
f77ab92202 Merge pull request #287 from vbNETonIce/patch-1
Windows set up instructions
2017-09-08 16:51:20 +05:30
Akash Nimare
e843a29316 Update Windows.md 2017-09-08 16:50:59 +05:30
akashnimare
85837242e7 Add back tray icon on windows #289 2017-09-08 05:17:14 +05:30
akashnimare
9f6da5712e Add show/hide desktop notification setting #192 2017-09-08 04:27:15 +05:30
akashnimare
158685a869 code refactor for mouse events 2017-09-07 22:50:20 +05:30
Akash Nimare
288b1cb3f2 Merge pull request #290 from zulip/clear-settings
Added Clear app settings menu item
2017-09-07 22:36:43 +05:30
akashnimare
e24a966d48 Add shortcut for clearing app data 2017-09-07 03:34:30 +05:30
akashnimare
306feb2eff reset window position 2017-09-07 03:29:04 +05:30
akashnimare
f426c932b0 Relaunch app on clearing app data 2017-09-06 16:23:56 +05:30
Akash Nimare
26172d8508 Merge pull request #288 from vbNETonIce/patch-2
tiny change in wording of new reset data option
2017-09-06 16:07:17 +05:30
vbNETonIce
99da0a338f tiny change in wording of new reset data option 2017-09-06 12:36:20 +02:00
akashnimare
9599249b31 Add reset app settings menu item #286 2017-09-06 15:23:22 +05:30
vbNETonIce
4bdd2564b7 Windows set up instructions 2017-09-06 08:57:13 +02:00
Akash Nimare
4dcf22a53c Merge pull request #285 from zulip/reset-data-setting
Add reset app data setting #192
2017-09-06 04:08:37 +05:30
akashnimare
0dc97648a0 code refactor 2017-09-06 03:54:25 +05:30
akashnimare
787f097cf3 style reset data button 2017-09-06 03:42:39 +05:30
akashnimare
5481d55c66 remove clear cache menu items 2017-09-06 03:26:56 +05:30
akashnimare
2a052b2c38 Added reset app data functionality [WIP] 2017-09-06 03:15:23 +05:30
akashnimare
3ed253d2e1 🎉 v1.4.0 2017-09-04 20:12:58 +05:30
akashnimare
5f672fe404 updated dependencies 2017-09-04 20:04:02 +05:30
akashnimare
10372787ac updated dependencies 2017-08-31 01:52:23 +05:30
akashnimare
bcabb615b4 clear app data setting option [WIP] 2017-08-29 04:26:16 +05:30
Akash Nimare
68acf2ec64 Merge pull request #282 from zulip/auto-launch
Added auto launch Zulip on system startup setting option #192
2017-08-29 03:32:59 +05:30
akashnimare
afd24035f4 Don't run start at login script in dev mode 2017-08-29 03:28:07 +05:30
akashnimare
4f28f6b935 Add open at login setting [WIP] 2017-08-29 01:47:19 +05:30
akashnimare
52bd600690 Detailed certificate warning message #126 2017-08-28 23:50:15 +05:30
Akash Nimare
c7ce2a8a99 updated placeholder text
organization >> server
2017-08-28 20:19:36 +05:30
akashnimare
9760b1bf98 Add start app at login template 2017-08-28 17:11:27 +05:30
akashnimare
d579c267f0 code refactor 2017-08-28 16:34:33 +05:30
Akash Nimare
21f01d268a Merge pull request #280 from zulip/badge-setting
Add an option to show/hide badges and taskbar icons #192
2017-08-27 05:13:19 +05:30
akashnimare
33782e0492 disable text-selection in setting page 2017-08-27 05:00:18 +05:30
akashnimare
8d20568b7a Show badge on app load by default 2017-08-27 04:50:44 +05:30
akashnimare
06f38e92ce refactor code 2017-08-27 04:28:03 +05:30
akashnimare
855e96e40e refactoring code for badge/taskbar icon [WIP] 2017-08-27 03:58:08 +05:30
akashnimare
bb68720ab7 add badge-setting constructor [WIP] 2017-08-27 01:50:13 +05:30
akashnimare
044d007a0c Hide/Show badge setting option [WIP] 2017-08-26 05:28:59 +05:30
akashnimare
4ea95fe8e8 Relaunch app when new updated has been downloaded fixes #279 2017-08-26 03:18:06 +05:30
Akash Nimare
d17c685e4d Document windows portable installer fixes #148 2017-08-25 18:26:56 +05:30
Akash Nimare
bb174da59a Merge pull request #278 from geeeeeeeeek/feature/connect-through-proxy
Feature/connect through proxy
2017-08-25 18:15:36 +05:30
Akash Nimare
a028b80adb Add proxy rules examples 2017-08-25 18:08:39 +05:30
akashnimare
3683511e60 updated windows build config 2017-08-24 05:08:14 +05:30
Akash Nimare
bf359db7f4 Updated download instructions 2017-08-24 04:53:43 +05:30
Akash Nimare
949d786f1f installation instructions [WIP] 2017-08-24 04:49:25 +05:30
Zhongyi Tong
5cd0c1ca4e Do not show alert after turning off the proxy. 2017-08-21 11:10:01 -04:00
Zhongyi Tong
86674991c1 Fix incorrect base component. 2017-08-21 11:06:59 -04:00
Zhongyi Tong
8492cda092 Add placeholder for proxy inputs. 2017-08-20 23:04:40 -04:00
Zhongyi Tong
647303c001 Implement proxy. 2017-08-20 23:00:50 -04:00
Zhongyi Tong
fe34f8adad Finish setting section for proxy. 2017-08-20 21:59:31 -04:00
Zhongyi Tong
f142a2eb4e Move help.md outside app folder. 2017-08-20 16:59:48 -04:00
Zhongyi Tong
43eaa3dd63 Initialize user guide. 2017-08-20 16:58:31 -04:00
Zhongyi Tong
a3142713f7 Initialize setting section for proxy. 2017-08-20 16:35:39 -04:00
Zhongyi Tong
876936125a Refactor general setting section. 2017-08-20 16:00:10 -04:00
Zhongyi Tong
7261fb6cef Update settings layout. 2017-08-20 15:21:13 -04:00
akashnimare
fba52e6dff code refactoring 2017-08-20 03:51:31 +05:30
akashnimare
23ac347fb9 Additional app categories [Linux] 2017-08-19 20:52:14 +05:30
akashnimare
082aebb1e0 Add app category [Linux] fixes #277 2017-08-19 20:49:44 +05:30
akashnimare
3b18dc4df0 improve auto-updates logging
From now on, all the logs will be saved within the Zulip application directory.
2017-08-19 02:21:50 +05:30
akashnimare
39d30b92fa Fixing beta updates [WIP] #269 2017-08-18 20:47:33 +05:30
Akash Nimare
733209e04e Merge pull request #274 from geeeeeeeeek/feature/load-all-tabs-on-startup
Load all servers on app start-up.
2017-08-18 15:25:59 +05:30
Zhongyi Tong
a68d697fc5 Fix linting. 2017-08-17 14:16:12 -04:00
Zhongyi Tong
e52ece57df Load all servers on app start-up. 2017-08-17 14:14:23 -04:00
akashnimare
65681c7da9 Add pre-commit test 2017-08-17 16:40:20 +05:30
akashnimare
52b6209905 Allow spacing between curly braces [XO Linter] 2017-08-17 16:27:34 +05:30
akashnimare
0e37925418 Handle crash events 2017-08-17 16:23:18 +05:30
Akash Nimare
1a31d2e431 Merge pull request #273 from geeeeeeeeek/issue/new-server-icon-cache
Issue/new server icon cache
2017-08-16 23:52:58 +05:30
Zhongyi Tong
c2fab2c7bc Fix duplicate checking. 2017-08-16 13:22:59 -04:00
Zhongyi Tong
965b55ba5f Fix regression of avoiding duplicated servers. 2017-08-16 12:48:31 -04:00
akashnimare
2c7bb36f8e Submit new server form when Enter key is pressed 2017-08-16 04:48:41 +05:30
Zhongyi Tong
48b829b771 Lazy update server settings on reload. 2017-08-15 17:45:38 -04:00
Zhongyi Tong
f13c28db73 Generate stored icon path using url hash. 2017-08-15 17:42:40 -04:00
Akash Nimare
19157c77e4 Merge pull request #272 from Anshuman-Verma/es6-index.js
Use ES6 destructuring for grabbing elements
2017-08-15 00:10:51 +05:30
Akash Nimare
161cd80d38 fixed linting errors 2017-08-14 23:07:17 +05:30
Anshuman
65db4c8f39 Use ES6 destructuring for grabbing elements 2017-08-13 23:04:48 +05:30
akashnimare
d600d3a6ed clear autoupdate dialog message 2017-08-12 17:38:50 +05:30
akashnimare
5fdd971966 Check for duplicate domains 2017-08-11 03:26:34 +05:30
Akash Nimare
d77d39d6ad Merge pull request #270 from geeeeeeeeek/issue/refine-add-server
Issue/refine add server
2017-08-10 15:54:45 +05:30
Akash Nimare
bf35db26bd fixed linting errors 2017-08-10 15:44:25 +05:30
Zhongyi Tong
4d8c8e08a0 Shorten loading animation of functional tabs. 2017-08-10 13:04:07 +08:00
Zhongyi Tong
b34681ece8 Show new-server form by default. 2017-08-10 13:00:25 +08:00
80 changed files with 14475 additions and 650 deletions

8
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,8 @@
---
<!-- Please Include: -->
- **Operating System**:
- [ ] Windows
- [ ] Linux/Ubutnu
- [ ] macOS
- **Clear steps to reproduce the issue**:
- **Relevant error messages and/or screenshots**:

16
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
<!--
Remove the fields that are not appropriate
Please include:
-->
**What's this PR do?**
**Any background context you want to provide?**
**Screenshots?**
**You have tested this PR on:**
- [ ] Windows
- [ ] Linux/Ubuntu
- [ ] macOS

7
.gitignore vendored
View File

@@ -23,4 +23,9 @@ yarn-error.log*
# miscellaneous
.idea
config.gypi
config.gypi
# Test generated files
tests/package.json
.python-version

View File

@@ -1 +0,0 @@
2.7.9

View File

@@ -18,8 +18,9 @@ node_js:
- '6'
before_install:
- npm install -g gulp
- npm install
- ./scripts/travis-xvfb.sh
- npm install -g gulp
- npm install
cache:
directories:
@@ -33,4 +34,4 @@ notifications:
urls:
- https://zulip.org/zulipbot/travis
on_success: always
on_failure: always
on_failure: always

View File

@@ -1,7 +0,0 @@
---
Please include:
- `Operating System`
- `Clear steps to reproduce the issue`
- `Relevant error messages and/or screenshots`

View File

@@ -8,23 +8,16 @@ Desktop client for Zulip. Available for Mac, Linux and Windows.
<img src="http://i.imgur.com/ChzTq4F.png"/>
# Download
You can download the latest version from the [Releases](https://github.com/zulip/zulip-electron/releases/latest) page.
Please see [installation guide](https://zulipchat.com/help/desktop-app-install-guide).
# Features
* Sign in to multiple teams
* Native desktop Notifications
* SpellChecker
* Desktop Notifications with inline reply support
* Multilanguage SpellChecker
* OSX/Win/Linux installers
* Automatic Updates (macOS/Windows)
* Automatic Updates (macOS/Windows/Linux)
* Keyboard shortcuts
Description | Keys
-----------------------| -----------------------
Default shortcuts | <kbd>Cmd/Ctrl</kbd> <kbd>k</kbd>
Manage Zulip Servers | <kbd>Cmd/Ctrl</kbd> <kbd>,</kbd>
Back | <kbd>Cmd/Ctrl</kbd> <kbd>[</kbd>
Forward | <kbd>Cmd/Ctrl</kbd> <kbd>]</kbd>
# Development
Please see our [development guide](./development.md) to get started and run app locally.

View File

@@ -1,30 +1,52 @@
'use strict';
const {app, dialog} = require('electron');
const {autoUpdater} = require('electron-updater');
const { app, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
function appUpdater() {
// Don't initiate auto-updates in development
if (isDev) {
return;
}
if (process.platform === 'linux' && !process.env.APPIMAGE) {
const { linuxUpdateNotification } = require('./linuxupdater');
linuxUpdateNotification();
return;
}
// Create Logs directory
const LogsDir = `${app.getPath('userData')}/Logs`;
// Log whats happening
const log = require('electron-log');
log.transports.file.file = `${LogsDir}/updates.log`;
log.transports.file.level = 'info';
autoUpdater.logger = log;
// Handle auto updates for beta/pre releases
autoUpdater.allowPrerelease = ConfigUtil.getConfigItem('betaUpdate') || false;
// Ask the user if update is available
// eslint-disable-next-line no-unused-vars
autoUpdater.on('update-downloaded', (event, info) => {
autoUpdater.on('update-downloaded', event => {
// Ask user to update the app
dialog.showMessageBox({
type: 'question',
buttons: ['Install and Relaunch', 'Later'],
buttons: ['Install and Relaunch', 'Install Later'],
defaultId: 0,
message: 'A new version of ' + app.getName() + ' has been downloaded',
message: `A new update ${event.version} has been downloaded`,
detail: 'It will be installed the next time you restart the application'
}, response => {
if (response === 0) {
setTimeout(() => autoUpdater.quitAndInstall(), 1);
setTimeout(() => {
autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}, 1000);
}
});
});

View File

@@ -0,0 +1,16 @@
'use strict';
const { crashReporter } = require('electron');
const crashHandler = () => {
crashReporter.start({
productName: 'zulip-electron',
companyName: 'Kandra Labs, Inc.',
submitURL: 'https://zulip-sentry.herokuapp.com/crashreport',
autoSubmit: true
});
};
module.exports = {
crashHandler
};

View File

@@ -1,19 +1,28 @@
'use strict';
const path = require('path');
const electron = require('electron');
const {app} = require('electron');
const ipc = require('electron').ipcMain;
const electronLocalshortcut = require('electron-localshortcut');
const isDev = require('electron-is-dev');
const windowStateKeeper = require('electron-window-state');
const isDev = require('electron-is-dev');
const appMenu = require('./menu');
const {appUpdater} = require('./autoupdater');
const { appUpdater } = require('./autoupdater');
const { crashHandler } = require('./crash-reporter');
const { setAutoLaunch } = require('./startup');
const { app, ipcMain } = electron;
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
// Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
// in development mode
if (isDev) {
require('electron-debug')();
}
// Prevent window being garbage collected
let mainWindow;
let badgeCount;
let isQuitting = false;
@@ -34,10 +43,6 @@ if (isAlreadyRunning) {
return app.quit();
}
function isWindowsOrmacOS() {
return process.platform === 'darwin' || process.platform === 'win32';
}
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
const iconPath = () => {
@@ -50,6 +55,10 @@ function createMainWindow() {
defaultWidth: 1000,
defaultHeight: 600
});
// Let's keep the window position global so that we can access it in other process
global.mainWindowState = mainWindowState;
const win = new electron.BrowserWindow({
// This settings needs to be saved in config
title: 'Zulip',
@@ -58,8 +67,8 @@ function createMainWindow() {
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 600,
minHeight: 500,
minWidth: 300,
minHeight: 400,
webPreferences: {
plugins: true,
allowDisplayingInsecureContent: true,
@@ -73,7 +82,11 @@ function createMainWindow() {
});
win.once('ready-to-show', () => {
win.show();
if (ConfigUtil.getConfigItem('startMinimized')) {
win.minimize();
} else {
win.show();
}
});
win.loadURL(mainURL);
@@ -89,9 +102,6 @@ function createMainWindow() {
win.hide();
}
}
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
win.setTitle('Zulip');
@@ -119,21 +129,8 @@ function createMainWindow() {
return win;
}
function registerLocalShortcuts(page) {
// Somehow, reload action cannot be overwritten by the menu item
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
page.send('reload-viewer');
});
// Also adding these shortcuts because some users might want to use it instead of CMD/Left-Right
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
page.send('back');
});
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
page.send('forward');
});
}
// Decrease load on GPU (experimental)
app.disableHardwareAcceleration();
// eslint-disable-next-line max-params
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
@@ -141,11 +138,6 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba
callback(true);
});
app.on('window-all-closed', () => {
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
app.on('activate', () => {
if (!mainWindow) {
mainWindow = createMainWindow();
@@ -160,39 +152,47 @@ app.on('ready', () => {
const page = mainWindow.webContents;
registerLocalShortcuts(page);
page.on('dom-ready', () => {
mainWindow.show();
});
page.once('did-frame-finish-load', () => {
const checkOS = isWindowsOrmacOS();
if (checkOS && !isDev) {
// Initate auto-updates on MacOS and Windows
appUpdater();
if (ConfigUtil.getConfigItem('startMinimized')) {
mainWindow.minimize();
} else {
mainWindow.show();
}
});
electron.powerMonitor.on('resume', () => {
page.send('reload-viewer');
page.once('did-frame-finish-load', () => {
// Initate auto-updates on MacOS and Windows
appUpdater();
crashHandler();
});
ipc.on('focus-app', () => {
// Temporarily remove this event
// electron.powerMonitor.on('resume', () => {
// mainWindow.reload();
// page.send('destroytray');
// });
ipcMain.on('focus-app', () => {
mainWindow.show();
});
ipc.on('quit-app', () => {
ipcMain.on('quit-app', () => {
app.quit();
});
// Reload full app not just webview, useful in debugging
ipc.on('reload-full-app', () => {
ipcMain.on('reload-full-app', () => {
mainWindow.reload();
page.send('destroytray');
});
ipc.on('toggle-app', () => {
ipcMain.on('clear-app-settings', () => {
global.mainWindowState.unmanage(mainWindow);
app.relaunch();
app.exit();
});
ipcMain.on('toggle-app', () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
@@ -200,57 +200,39 @@ app.on('ready', () => {
}
});
ipc.on('update-badge', (event, messageCount) => {
if (process.platform === 'darwin') {
app.setBadgeCount(messageCount);
}
if (process.platform === 'win32') {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(true);
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {
page.send('render-taskbar-icon', messageCount);
}
}
ipcMain.on('toggle-badge-option', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
ipcMain.on('update-badge', (event, messageCount) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
page.send('tray', messageCount);
});
ipc.on('update-taskbar-icon', (event, data, text) => {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
ipcMain.on('update-taskbar-icon', (event, data, text) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
});
ipc.on('forward-message', (event, listener, ...params) => {
ipcMain.on('forward-message', (event, listener, ...params) => {
page.send(listener, ...params);
});
ipc.on('update-menu', (event, props) => {
ipcMain.on('update-menu', (event, props) => {
appMenu.setMenu(props);
});
ipc.on('register-server-tab-shortcut', (event, index) => {
electronLocalshortcut.register(mainWindow, `CommandOrControl+${index}`, () => {
// Array index == Shown index - 1
page.send('switch-server-tab', index - 1);
});
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
setAutoLaunch(AutoLaunchValue);
});
ipc.on('local-shortcuts', (event, enable) => {
if (enable) {
registerLocalShortcuts(page);
} else {
electronLocalshortcut.unregisterAll(mainWindow);
}
});
});
app.on('will-quit', () => {
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
app.on('before-quit', () => {
isQuitting = true;
});
// Send crash reports
process.on('uncaughtException', err => {
console.error(err);
console.error(err.stack);
});

42
app/main/linuxupdater.js Normal file
View File

@@ -0,0 +1,42 @@
const { app } = require('electron');
const { Notification } = require('electron');
const request = require('request');
const semver = require('semver');
const ConfigUtil = require('../renderer/js/utils/config-util');
const LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
function linuxUpdateNotification() {
let url = 'https://api.github.com/repos/zulip/zulip-electron/releases';
url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
const options = {
url,
headers: {'User-Agent': 'request'}
};
request(options, (error, response, body) => {
if (error) {
console.log('Error:', error);
return;
}
if (response.statusCode < 400) {
const data = JSON.parse(body);
const latestVersion = ConfigUtil.getConfigItem('betaUpdate') ? data[0].tag_name : data.tag_name;
if (semver.gt(latestVersion, app.getVersion())) {
const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
if (notified === null) {
new Notification({title: 'Zulip Update', body: 'A new version ' + latestVersion + ' is available. Please update using your package manager.'}).show();
LinuxUpdateUtil.setUpdateItem(latestVersion, true);
}
}
} else {
console.log('Status:', response.statusCode);
}
});
}
module.exports = {
linuxUpdateNotification
};

View File

@@ -1,11 +1,17 @@
'use strict';
const os = require('os');
const {dialog, app, shell, BrowserWindow, Menu} = require('electron');
const path = require('path');
const { app, shell, BrowserWindow, Menu } = require('electron');
const fs = require('fs-extra');
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
const appName = app.getName();
const i18n = require(__dirname + '/../translations/i18n');
class AppMenu {
getHistorySubmenu() {
return [{
@@ -33,7 +39,7 @@ class AppMenu {
accelerator: 'CommandOrControl+R',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('reload-viewer');
AppMenu.sendAction('reload-current-viewer');
}
}
}, {
@@ -50,7 +56,7 @@ class AppMenu {
role: 'togglefullscreen'
}, {
label: 'Zoom In',
accelerator: 'CommandOrControl+=',
accelerator: process.platform === 'darwin' ? 'Command+Plus' : 'Control+=',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('zoomIn');
@@ -86,9 +92,9 @@ class AppMenu {
accelerator: 'CommandOrControl+S',
click(item, focusedWindow) {
if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('show-sidebar');
const newValue = !ConfigUtil.getConfigItem('showSidebar');
focusedWindow.webContents.send('toggle-sidebar', newValue);
ConfigUtil.setConfigItem('show-sidebar', newValue);
ConfigUtil.setConfigItem('showSidebar', newValue);
}
}
}, {
@@ -111,26 +117,33 @@ class AppMenu {
}
getHelpSubmenu() {
return [{
label: `${appName} Website`,
click() {
shell.openExternal('https://zulipchat.com/help/');
}
}, {
label: `${appName + 'Desktop'} - ${app.getVersion()}`,
enabled: false
}, {
label: 'Report an Issue...',
click() {
const body = `
return [
{
label: `${appName + ' Desktop-'} v${app.getVersion()}`,
enabled: false
},
{
label: `${appName} Help`,
click() {
shell.openExternal('https://zulipchat.com/help/');
}
}, {
label: 'Show App Logs',
click() {
shell.openItem(app.getPath('userData'));
}
}, {
label: 'Report an Issue...',
click() {
const body = `
<!-- Please succinctly describe your issue and steps to reproduce it. -->
-
${app.getName()} ${app.getVersion()}
Electron ${process.versions.electron}
${process.platform} ${process.arch} ${os.release()}`;
shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
}
}];
shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
}
}];
}
getWindowSubmenu(tabs, activeTabIndex) {
@@ -146,6 +159,11 @@ class AppMenu {
type: 'separator'
});
for (let i = 0; i < tabs.length; i++) {
// Do not add functional tab settings to list of windows in menu bar
if (tabs[i].props.role === 'function' && tabs[i].webview.props.name === 'Settings') {
continue;
}
initialSubmenu.push({
label: tabs[i].webview.props.name,
accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`,
@@ -155,7 +173,7 @@ class AppMenu {
AppMenu.sendAction('switch-server-tab', tabs[i].props.index);
}
},
type: 'radio'
type: 'checkbox'
});
}
}
@@ -164,12 +182,12 @@ class AppMenu {
}
getDarwinTpl(props) {
const {tabs, activeTabIndex} = props;
const { tabs, activeTabIndex } = props;
return [{
label: `${app.getName()}`,
submenu: [{
label: 'Zulip Desktop',
label: 'About Zulip',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
@@ -178,7 +196,7 @@ class AppMenu {
}, {
type: 'separator'
}, {
label: 'Settings',
label: 'Desktop App Settings',
accelerator: 'Cmd+,',
click(item, focusedWindow) {
if (focusedWindow) {
@@ -187,7 +205,7 @@ class AppMenu {
}
}, {
label: 'Keyboard Shortcuts',
accelerator: 'Cmd+K',
accelerator: 'Cmd+Shift+K',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('shortcut');
@@ -196,9 +214,10 @@ class AppMenu {
}, {
type: 'separator'
}, {
label: 'Clear Cache',
label: 'Reset App Settings',
accelerator: 'Command+Shift+D',
click() {
AppMenu.clearCache();
AppMenu.resetAppSettings();
}
}, {
label: 'Log Out',
@@ -229,15 +248,15 @@ class AppMenu {
}, {
label: 'Edit',
submenu: [{
role: 'undo'
role: 'undo', label: i18n._('Undo')
}, {
role: 'redo'
role: 'redo', label: i18n._('Redo')
}, {
type: 'separator'
}, {
role: 'cut'
role: 'cut', label: i18n._('Cut')
}, {
role: 'copy'
role: 'copy', label: i18n._('Copy')
}, {
role: 'paste'
}, {
@@ -263,12 +282,12 @@ class AppMenu {
}
getOtherTpl(props) {
const {tabs, activeTabIndex} = props;
const { tabs, activeTabIndex } = props;
return [{
label: 'File',
submenu: [{
label: 'Zulip Desktop',
label: 'About Zulip',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
@@ -277,7 +296,7 @@ class AppMenu {
}, {
type: 'separator'
}, {
label: 'Settings',
label: 'Desktop App Settings',
accelerator: 'Ctrl+,',
click(item, focusedWindow) {
if (focusedWindow) {
@@ -288,7 +307,7 @@ class AppMenu {
type: 'separator'
}, {
label: 'Keyboard Shortcuts',
accelerator: 'Ctrl+K',
accelerator: 'Ctrl+Shift+K',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('shortcut');
@@ -297,9 +316,10 @@ class AppMenu {
}, {
type: 'separator'
}, {
label: 'Clear Cache',
label: 'Reset App Settings',
accelerator: 'Ctrl+Shift+D',
click() {
AppMenu.clearCache();
AppMenu.resetAppSettings();
}
}, {
label: 'Log Out',
@@ -316,11 +336,11 @@ class AppMenu {
accelerator: 'Ctrl+Q'
}]
}, {
label: 'Edit',
label: i18n.__('Edit'),
submenu: [{
role: 'undo'
role: 'undo', label: i18n.__('Undo')
}, {
role: 'redo'
role: 'redo', label: i18n.__('Redo')
}, {
type: 'separator'
}, {
@@ -363,11 +383,21 @@ class AppMenu {
win.webContents.send(action, ...params);
}
static clearCache() {
const win = BrowserWindow.getAllWindows()[0];
const ses = win.webContents.session;
ses.clearCache(() => {
dialog.showMessageBox({type: 'info', buttons: [], message: 'Cache cleared!'});
static resetAppSettings() {
// We save App's settings/configurations in following files
const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];
settingFiles.forEach(settingFileName => {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
fs.access(getSettingFilesPath, error => {
if (error) {
console.log(error);
} else {
fs.unlink(getSettingFilesPath, () => {
AppMenu.sendAction('clear-app-data');
});
}
});
});
}

34
app/main/startup.js Normal file
View File

@@ -0,0 +1,34 @@
'use strict';
const { app } = require('electron');
const AutoLaunch = require('auto-launch');
const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
const setAutoLaunch = AutoLaunchValue => {
// Don't run this in development
if (isDev) {
return;
}
// On Mac, work around a bug in auto-launch where it opens a Terminal window
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
const appPath = process.platform === 'darwin' ? app.getPath('exe').replace(/\.app\/Content.*/, '.app') : undefined; // Use the default
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
path: appPath,
isHidden: false
});
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
if (autoLaunchOption) {
ZulipAutoLauncher.enable();
} else {
ZulipAutoLauncher.disable();
}
};
module.exports = {
setAutoLaunch
};

1380
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,13 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.3.0-beta",
"version": "1.8.2",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"email": "<svnitakash@gmail.com>",
"copyright": "©2017 Kandra Labs, Inc.",
"copyright": "Kandra Labs, Inc.",
"author": {
"name": "Kandra Labs, Inc.",
"email": "svnitakash@gmail.com"
"email": "support@zulipchat.com"
},
"repository": {
"type": "git",
@@ -27,15 +26,19 @@
"InstantMessaging"
],
"dependencies": {
"electron-debug": "1.4.0",
"auto-launch": "5.0.1",
"electron-is-dev": "0.3.0",
"electron-localshortcut": "2.0.2",
"electron-log": "2.2.7",
"electron-spellchecker": "1.2.0",
"electron-updater": "2.8.5",
"electron-spellchecker": "1.1.2",
"electron-updater": "2.18.2",
"electron-window-state": "4.1.1",
"is-online": "7.0.0",
"node-json-db": "0.7.3",
"request": "2.81.0",
"wurl": "2.5.0",
"electron-window-state": "4.1.1"
"semver": "5.4.1",
"wurl": "2.5.0"
},
"optionalDependencies": {
"node-mac-notifier": "0.0.13"
}
}

View File

@@ -7,25 +7,37 @@
<body>
<div class="about">
<img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version">version ?.?.?</p>
<p class="detail" id="version">v?.?.?</p>
<div class="maintenance-info">
<p class="detail maintainer">Maintained by Zulip</p>
<p class="detail license">Available under the Apache License</p>
<a class="bug" onclick="linkInBrowser()" href="#">Found bug?</a>
<p class="detail maintainer">
Maintained by <a onclick="linkInBrowser('website')">Zulip</a>
</p>
<p class="detail license">
Available under the <a onclick="linkInBrowser('license')">Apache 2.0 License</a>
</p>
<a class="bug" onclick="linkInBrowser('bug')" href="#">Found bug?</a>
</div>
</div>
<script>
const app = require('electron').remote.app;
const version_tag = document.getElementById('version');
version_tag.innerHTML = 'version ' + app.getVersion();
function linkInBrowser(event) {
const shell = require('electron').shell;
const url = "https://github.com/zulip/zulip-electron/issues/new?body=Please describe your issue and steps to reproduce it."
const { app } = require('electron').remote;
const { shell } = require('electron');
const version_tag = document.querySelector('#version');
version_tag.innerHTML = 'v' + app.getVersion();
function linkInBrowser(type) {
let url;
switch (type) {
case 'website':
url = "https://zulipchat.com";
break;
case 'license':
url = "https://github.com/zulip/zulip-electron/blob/master/LICENSE";
break;
default:
url = 'https://github.com/zulip/zulip-electron/issues/new?body=' +
'%3C!--Please%20describe%20your%20issue%20and%20steps%20to%20reproduce%20it.--%3E';
}
shell.openExternal(url);
}
</script>

View File

@@ -1,21 +1,23 @@
body {
background: #fafafa;
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-webkit-font-smoothing: subpixel-antialiased;
}
.logo {
display: block;
margin: 0 auto;
margin: -40px auto;
}
#version {
color: #aaa;
font-size: 0.9em;
color: #444343;
font-size: 1.3em;
padding-top: 40px;
}
.about {
margin-top: 50px;
margin: 25vh auto;
height: 25vh;
text-align: center;
}
@@ -42,9 +44,9 @@ body {
}
.maintenance-info {
cursor: pointer;
position: absolute;
width: 100%;
bottom: 20px;
left: 0px;
color: #444;
}
@@ -52,7 +54,6 @@ body {
.maintenance-info p {
margin: 0;
font-size: 1em;
width: 100%;
}
@@ -71,3 +72,11 @@ body {
.maintenance-info .bug:hover {
background-color: #32a692;
}
p.detail a {
color: #355f4c;
}
p.detail a:hover {
text-decoration: underline;
}

View File

@@ -13,12 +13,12 @@ body {
#content {
display: flex;
height: 100%;
background: #eee url(../img/ic_loading.gif) no-repeat;
background: #fff url(../img/ic_loading.gif) no-repeat;
background-size: 60px 60px;
background-position: center;
}
#sidebar {
.toggle-sidebar {
background: #222c31;
width: 54px;
padding: 27px 0 20px 0;
@@ -27,6 +27,21 @@ body {
flex-direction: column;
-webkit-app-region: drag;
overflow: hidden;
transition: all 0.5s ease;
}
.toggle-sidebar div {
transition: all 0.5s ease-out;
}
.sidebar-hide {
width: 0;
transition: all 0.8s ease;
}
.sidebar-hide div {
transform: translateX(-100%);
transition: all 0.6s ease-out;
}
@font-face {
@@ -38,8 +53,8 @@ body {
/*******************
* Left Sidebar *
*******************/
* Left Sidebar *
*******************/
#tabs-container {
display: flex;
@@ -66,11 +81,18 @@ body {
text-rendering: optimizeLegibility;
}
#actions-container {
display: flex;
flex-direction: column;
position: fixed;
bottom: 0;
width: inherit;
}
.action-button {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
padding: 12px;
}
.action-button:hover {
@@ -86,6 +108,26 @@ body {
color: #98a9b3;
}
.action-button.disable {
opacity: 0.6;
}
.action-button.disable:hover {
cursor: not-allowed;
}
.action-button.disable:hover i {
color: #6c8592;
}
.action-button.active {
background-color: rgba(255, 255, 255, 0.25);
}
.action-button.active i {
color: #eee;
}
.tab:first-child {
margin-top: 8px;
}
@@ -197,8 +239,8 @@ body {
/*******************
* Webview Area *
*******************/
* Webview Area *
*******************/
#webviews-container {
display: flex;
@@ -208,7 +250,7 @@ body {
webview {
opacity: 1;
transition: opacity 0.3s ease-in;
/* transition: opacity 0.3s ease-in; */
flex-grow: 1;
}
@@ -231,6 +273,7 @@ webview:focus {
/* Tooltip styling */
#back-tooltip,
#reload-tooltip,
#setting-tooltip {
font-family: sans-serif;
@@ -247,6 +290,7 @@ webview:focus {
font-size: 14px;
}
#back-tooltip:after,
#reload-tooltip:after,
#setting-tooltip:after {
content: " ";
@@ -258,6 +302,33 @@ webview:focus {
right: 68px;
}
#add-server-tooltip,
.server-tooltip {
font-family: 'arial';
background: #222c31;
left: 56px;
padding: 10px 20px;
position: fixed;
margin-top: 8px;
z-index: 5000 !important;
color: #fff;
border-radius: 4px;
text-align: center;
width: max-content;
font-size: 14px;
}
#add-server-tooltip:after,
.server-tooltip:after {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid #222c31;
position: absolute;
top: 10px;
left: -5px;
}
#collapse-button {
bottom: 30px;
left: 20px;
@@ -285,7 +356,9 @@ webview:focus {
display: none !important;
}
/* Full screen Popup container */
.popup .popuptext {
visibility: hidden;
background-color: #555;

View File

@@ -3,43 +3,95 @@ body {
height: 100%;
margin: 0;
cursor: default;
user-select: none;
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
color: #333;
background: #efefef;
}
kbd {
display: inline-block;
border: 1px solid #ccc ;
border-radius: 3px;
font-size: 15px;
font-family: Courier New, Courier, monospace;
font-weight: bold;
white-space: nowrap;
background-color: #f7f7f7;
color: #333;
margin: 0 0.1em;
padding: 0.3em 0.8em;
text-shadow: 0 1px 0 #fff;
line-height: 1.4;
}
table,
th,
td {
border-collapse: collapse;
color: #383430;
}
table {
width: 100%;
margin-top: 18px;
margin-bottom: 18px;
}
table tr:nth-child(even) {
background-color: #fafafa;
}
table tr:nth-child(odd) {
background-color: #fff;
}
td {
padding: 5px;
}
td:nth-child(odd) {
text-align: right;
width: 50%;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
}
@font-face {
font-family: 'Montserrat';
src: url(../fonts/Montserrat-Regular.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
/* Preferred icon size */
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
/* Preferred icon size */
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
}
#content {
display: flex;
height: 100%;
font-family: sans-serif;
font-family: 'Montserrat';
}
#sidebar {
@@ -77,7 +129,9 @@ body {
#settings-header {
font-size: 22px;
color: #5c6166;
color: #222c31;
font-weight: bold;
text-transform: uppercase;
}
#settings-container {
@@ -88,15 +142,21 @@ body {
}
#new-server-container {
margin: 20px 0;
opacity: 1;
transition: opacity 0.3s;
}
.title {
padding: 4px 0 6px 0;
font-weight: 500;
color: #222c31;
}
.page-title {
color: #222c31;
font-size: 15px;
font-weight: bold;
color: #1e1e1e;
padding: 4px 0 6px 0;
}
.sub-title {
@@ -130,19 +190,35 @@ img.server-info-icon {
cursor: pointer;
}
.server-info-url {
.setting-input-key {
font-size: 14px;
height: 27px;
line-height: 27px;
font-weight: bold;
background: transparent;
flex-wrap: nowrap;
margin-right: 10px;
}
.setting-input-value {
flex-grow: 1;
font-size: 14px;
height: 24px;
border: none;
border-bottom: #ededed 1px solid;
height: 22px;
border-radius: 3px;
padding: 7px;
border: #ededed 2px solid;
outline-width: 0;
background: transparent;
max-width: 500px;
}
.server-info-value:focus {
border-bottom: #388E3C 1px solid;
.setting-input-value:focus {
border: #7cb980 2px solid;
border-radius: 3px;
}
.setting-block {
width: 100%;
}
.actions-container {
@@ -158,14 +234,13 @@ img.server-info-icon {
display: flex;
align-items: center;
padding: 0 10px;
border-radius: 2px;
margin-right: 10px;
}
.action i {
margin-right: 5px;
font-size: 18px;
line-height: 27px;
line-height: 26px;
}
.settings-pane {
@@ -186,12 +261,18 @@ img.server-info-icon {
.settings-card {
display: flex;
padding: 16px 30px;
flex-wrap: wrap;
padding: 12px 30px;
margin: 10px 0 20px 0;
background: #fff;
border-radius: 2px;
width: 540px;
box-shadow: 1px 2px 4px #bcbcbc;
width: 70%;
transition: all 0.2s;
}
.settings-card:hover {
border-left: 8px solid #bcbcbc;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),
0 2px 0px 0px rgba(0,0,0,0.12);
}
.hidden {
@@ -200,15 +281,19 @@ img.server-info-icon {
}
.red {
color: #ef5350;
background: #ffebee;
border: 1px solid #ef5350;
color: #ffffff;
background: #ef5350;
padding: 3px;
padding-right: 10px;
padding-left: 10px;
}
.green {
color: #388E3C;
background: #E8F5E9;
border: 1px solid #388E3C;
.blue {
color: #ffffff;
background: #4EBFAC;
padding: 3px;
padding-right: 10px;
padding-left: 10px;
}
.grey {
@@ -219,8 +304,10 @@ img.server-info-icon {
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin: 6px;
}
.code {
@@ -231,4 +318,98 @@ i.open-tab-button {
padding: 0 5px;
font-size: 18px;
cursor: pointer;
}
.reset-data-button {
display: inline-block;
border: none;
padding: 10px;
width: 120px;
cursor: pointer;
font-size: 13px;
transition: background-color 0.2s ease;
text-decoration: none;
}
.reset-data-button:hover {
background-color: #3c9f8d;
color: #fff;
}
#server-info-container {
min-height: calc(100% - 260px);
}
#create-organization-container {
font-size: 1.15em;
}
#create-organization-container i {
position: relative;
top: 3px;
}
#open-create-org-link {
color: #666;
cursor: pointer;
text-decoration: none;
}
#open-create-org-link:hover {
color: #005580;
;
text-decoration: underline;
}
.toggle {
position: absolute;
margin-left: -9999px;
visibility: hidden;
}
.toggle+label {
display: block;
position: relative;
cursor: pointer;
outline: none;
user-select: none;
}
input.toggle-round+label {
padding: 2px;
width: 50px;
height: 25px;
background-color: #dddddd;
border-radius: 25px;
}
input.toggle-round+label:before,
input.toggle-round+label:after {
display: block;
position: absolute;
top: 2px;
left: 2px;
bottom: 2px;
content: "";
}
input.toggle-round+label:before {
right: 2px;
background-color: #f1f1f1;
border-radius: 25px;
}
input.toggle-round+label:after {
width: 25px;
height: 25px;
background-color: #fff;
border-radius: 100%;
}
input.toggle-round:checked+label:before {
background-color: #4EBFAC;
}
input.toggle-round:checked+label:after {
margin-left: 25px;
}

Binary file not shown.

View File

@@ -4,7 +4,7 @@ const Tab = require(__dirname + '/../components/tab.js');
class FunctionalTab extends Tab {
template() {
return `<div class="tab functional-tab">
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button">
<i class="material-icons">close</i>
</div>
@@ -16,10 +16,11 @@ class FunctionalTab extends Tab {
init() {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$el);
this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0];
this.registerListeners();
if (this.props.name !== 'Settings') {
this.props.$root.appendChild(this.$el);
this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0];
this.registerListeners();
}
}
registerListeners() {

View File

@@ -7,7 +7,8 @@ const {ipcRenderer} = require('electron');
class ServerTab extends Tab {
template() {
return `<div class="tab">
return `<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none"></div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src='${this.props.icon}'/>
@@ -49,7 +50,8 @@ class ServerTab extends Tab {
shortcutText = `Ctrl+${shownIndex}`;
}
ipcRenderer.send('register-server-tab-shortcut', shownIndex);
// Array index == Shown index - 1
ipcRenderer.send('switch-server-tab', shownIndex - 1);
return shortcutText;
}

View File

@@ -21,6 +21,8 @@ class Tab extends BaseComponent {
registerListeners() {
this.$el.addEventListener('click', this.props.onClick);
this.$el.addEventListener('mouseover', this.props.onHover);
this.$el.addEventListener('mouseout', this.props.onHoverOut);
}
isLoading() {

View File

@@ -4,12 +4,14 @@ const path = require('path');
const fs = require('fs');
const DomainUtil = require(__dirname + '/../utils/domain-util.js');
const ConfigUtil = require(__dirname + '/../utils/config-util.js');
const SystemUtil = require(__dirname + '/../utils/system-util.js');
const LinkUtil = require(__dirname + '/../utils/link-util.js');
const {shell} = require('electron').remote;
const { shell, app } = require('electron').remote;
const BaseComponent = require(__dirname + '/../components/base.js');
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
class WebView extends BaseComponent {
constructor(props) {
super();
@@ -24,10 +26,12 @@ class WebView extends BaseComponent {
template() {
return `<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
webpreferences="allowRunningInsecureContent, javascript=yes">
</webview>`;
}
@@ -41,7 +45,7 @@ class WebView extends BaseComponent {
registerListeners() {
this.$el.addEventListener('new-window', event => {
const {url} = event;
const { url } = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
if (LinkUtil.isInternal(domainPrefix, url) || url === (domainPrefix + '/')) {
@@ -53,19 +57,49 @@ class WebView extends BaseComponent {
}
});
if (shouldSilentWebview) {
this.$el.addEventListener('dom-ready', () => {
this.$el.setAudioMuted(true);
});
}
this.$el.addEventListener('page-title-updated', event => {
const {title} = event;
const { title } = event;
this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange();
});
this.$el.addEventListener('did-navigate-in-page', event => {
const isSettingPage = event.url.includes('renderer/preference.html');
if (isSettingPage) {
return;
}
this.canGoBackButton();
});
this.$el.addEventListener('did-navigate', () => {
this.canGoBackButton();
});
this.$el.addEventListener('page-favicon-updated', event => {
const { favicons } = event;
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
// This api is only supported on macOS
app.dock.setBadge('●');
}
});
this.$el.addEventListener('dom-ready', () => {
this.$el.classList.add('onload');
if (this.props.role === 'server') {
this.$el.classList.add('onload');
}
this.show();
});
this.$el.addEventListener('did-fail-load', event => {
const {errorDescription} = event;
const { errorDescription } = event;
const hasConnectivityErr = (SystemUtil.connectivityERR.indexOf(errorDescription) >= 0);
if (hasConnectivityErr) {
console.error('error', errorDescription);
@@ -96,11 +130,13 @@ class WebView extends BaseComponent {
this.$el.classList.remove('disabled');
setTimeout(() => {
this.$el.classList.remove('onload');
if (this.props.role === 'server') {
this.$el.classList.remove('onload');
}
}, 1000);
this.focus();
this.loading = false;
this.props.onTitleChange(this.$el.getTitle());
this.props.onTitleChange();
// Injecting preload css in webview to override some css rules
this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8'));
}
@@ -159,6 +195,15 @@ class WebView extends BaseComponent {
}
}
canGoBackButton() {
const $backButton = document.querySelector('#actions-container #back-action');
if (this.$el.canGoBack()) {
$backButton.classList.remove('disable');
} else {
$backButton.classList.add('disable');
}
}
forward() {
if (this.$el.canGoForward()) {
this.$el.goForward();

View File

@@ -1,13 +1,17 @@
'use strict';
require(__dirname + '/js/tray.js');
const {ipcRenderer} = require('electron');
const { ipcRenderer, remote } = require('electron');
const isDev = require('electron-is-dev');
const { session } = remote;
require(__dirname + '/js/tray.js');
const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
const WebView = require(__dirname + '/js/components/webview.js');
const ServerTab = require(__dirname + '/js/components/server-tab.js');
const FunctionalTab = require(__dirname + '/js/components/functional-tab.js');
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
const ReconnectUtil = require(__dirname + '/js/utils/reconnect-util.js');
class ServerManagerView {
constructor() {
@@ -18,9 +22,14 @@ class ServerManagerView {
this.$reloadButton = $actionsContainer.querySelector('#reload-action');
this.$settingsButton = $actionsContainer.querySelector('#settings-action');
this.$webviewsContainer = document.getElementById('webviews-container');
this.$backButton = $actionsContainer.querySelector('#back-action');
this.$addServerTooltip = document.getElementById('add-server-tooltip');
this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip');
this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip');
this.$serverIconTooltip = document.getElementsByClassName('server-tooltip');
this.$backTooltip = $actionsContainer.querySelector('#back-tooltip');
this.$sidebar = document.getElementById('sidebar');
this.$fullscreenPopup = document.getElementById('fullscreen-popup');
@@ -30,17 +39,73 @@ class ServerManagerView {
this.activeTabIndex = -1;
this.tabs = [];
this.functionalTabs = {};
this.tabIndex = 0;
}
init() {
this.initSidebar();
this.initTabs();
this.initActions();
this.registerIpcs();
this.loadProxy().then(() => {
this.initSidebar();
this.initTabs();
this.initActions();
this.registerIpcs();
this.initDefaultSettings();
});
}
loadProxy() {
return new Promise(resolve => {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
if (proxyEnabled) {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
}, resolve);
} else {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: '',
proxyRules: '',
proxyBypassRules: ''
}, resolve);
}
});
}
// Settings are initialized only when user clicks on General/Server/Network section settings
// In case, user doesn't visit these section, those values set to be null automatically
// This will make sure the default settings are correctly set to either true or false
initDefaultSettings() {
// Default settings which should be respected
const settingOptions = {
trayIcon: true,
useProxy: false,
showSidebar: true,
badgeOption: true,
startAtLogin: false,
startMinimized: false,
enableSpellchecker: true,
showNotification: true,
betaUpdate: false,
silent: false,
lastActiveTab: 0
};
// Platform specific settings
if (process.platform === 'win32') {
// Only available on Windows
settingOptions.flashTaskbarOnMessage = true;
}
for (const i in settingOptions) {
if (ConfigUtil.getConfigItem(i) === null) {
ConfigUtil.setConfigItem(i, settingOptions[i]);
}
}
}
initSidebar() {
const showSidebar = ConfigUtil.getConfigItem('show-sidebar', true);
const showSidebar = ConfigUtil.getConfigItem('showSidebar', true);
this.toggleSidebar(showSidebar);
}
@@ -49,25 +114,33 @@ class ServerManagerView {
if (servers.length > 0) {
for (let i = 0; i < servers.length; i++) {
this.initServer(servers[i], i);
DomainUtil.updateSavedServer(servers[i].url, i);
this.activateTab(i);
}
this.activateTab(0);
// Open last active tab
this.activateTab(ConfigUtil.getConfigItem('lastActiveTab'));
// Remove focus from the settings icon at sidebar bottom
this.$settingsButton.classList.remove('active');
} else {
this.openSettings('Servers');
}
ipcRenderer.send('local-shortcuts', true);
}
initServer(server, index) {
const tabIndex = this.getTabIndex();
this.tabs.push(new ServerTab({
role: 'server',
icon: server.icon,
$root: this.$tabsContainer,
onClick: this.activateTab.bind(this, index),
onClick: this.activateLastTab.bind(this, index),
index,
tabIndex,
onHover: this.onHover.bind(this, index, server.alias),
onHoverOut: this.onHoverOut.bind(this, index),
webview: new WebView({
$root: this.$webviewsContainer,
index,
tabIndex,
url: server.url,
name: server.alias,
isActive: () => {
@@ -91,20 +164,47 @@ class ServerManagerView {
this.$settingsButton.addEventListener('click', () => {
this.openSettings('General');
});
this.$reloadButton.addEventListener('mouseover', () => {
this.$reloadTooltip.removeAttribute('style');
this.$backButton.addEventListener('click', () => {
this.tabs[this.activeTabIndex].webview.back();
});
this.$reloadButton.addEventListener('mouseout', () => {
this.$reloadTooltip.style.display = 'none';
const $serverImgs = document.querySelectorAll('.server-icons');
$serverImgs.forEach($serverImg => {
$serverImg.addEventListener('error', () => {
$serverImg.src = 'img/icon.png';
});
});
this.$settingsButton.addEventListener('mouseover', () => {
this.$settingsTooltip.removeAttribute('style');
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip);
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
this.sidebarHoverEvent(this.$backButton, this.$backTooltip);
}
getTabIndex() {
const currentIndex = this.tabIndex;
this.tabIndex++;
return currentIndex;
}
sidebarHoverEvent(SidebarButton, SidebarTooltip) {
SidebarButton.addEventListener('mouseover', () => {
SidebarTooltip.removeAttribute('style');
});
this.$settingsButton.addEventListener('mouseout', () => {
this.$settingsTooltip.style.display = 'none';
SidebarButton.addEventListener('mouseout', () => {
SidebarTooltip.style.display = 'none';
});
}
onHover(index, serverName) {
this.$serverIconTooltip[index].innerHTML = serverName;
this.$serverIconTooltip[index].removeAttribute('style');
}
onHoverOut(index) {
this.$serverIconTooltip[index].style.display = 'none';
}
openFunctionalTab(tabProps) {
if (this.functionalTabs[tabProps.name] !== undefined) {
this.activateTab(this.functionalTabs[tabProps.name]);
@@ -113,16 +213,20 @@ class ServerManagerView {
this.functionalTabs[tabProps.name] = this.tabs.length;
const tabIndex = this.getTabIndex();
this.tabs.push(new FunctionalTab({
role: 'function',
materialIcon: tabProps.materialIcon,
name: tabProps.name,
$root: this.$tabsContainer,
index: this.functionalTabs[tabProps.name],
tabIndex,
onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]),
onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]),
webview: new WebView({
$root: this.$webviewsContainer,
index: this.functionalTabs[tabProps.name],
tabIndex,
url: tabProps.url,
name: tabProps.name,
isActive: () => {
@@ -134,7 +238,6 @@ class ServerManagerView {
preload: false
})
}));
this.activateTab(this.functionalTabs[tabProps.name]);
}
@@ -144,6 +247,7 @@ class ServerManagerView {
materialIcon: 'settings',
url: `file://${__dirname}/preference.html#${nav}`
});
this.$settingsButton.classList.add('active');
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
}
@@ -163,8 +267,15 @@ class ServerManagerView {
});
}
activateLastTab(index) {
// Open last active tab
ConfigUtil.setConfigItem('lastActiveTab', index);
// Open all the tabs in background
this.activateTab(index);
}
activateTab(index, hideOldTab = true) {
if (this.tabs[index].webview.loading) {
if (!this.tabs[index]) {
return;
}
@@ -172,10 +283,19 @@ class ServerManagerView {
if (this.activeTabIndex === index) {
return;
} else if (hideOldTab) {
// If old tab is functional tab Settings, remove focus from the settings icon at sidebar bottom
if (this.tabs[this.activeTabIndex].props.role === 'function' && this.tabs[this.activeTabIndex].props.name === 'Settings') {
this.$settingsButton.classList.remove('active');
}
this.tabs[this.activeTabIndex].deactivate();
}
}
try {
this.tabs[index].webview.canGoBackButton();
} catch (err) {
}
this.activeTabIndex = index;
this.tabs[index].activate();
@@ -183,6 +303,27 @@ class ServerManagerView {
tabs: this.tabs,
activeTabIndex: this.activeTabIndex
});
ipcRenderer.on('toggle-sidebar', (event, state) => {
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-sidebar', state);
});
ipcRenderer.on('toogle-silent', (event, state) => {
const webviews = document.querySelectorAll('webview');
webviews.forEach(webview => {
try {
webview.setAudioMuted(state);
} catch (err) {
// webview is not ready yet
webview.addEventListener('dom-ready', () => {
webview.isAudioMuted();
});
}
});
});
}
destroyTab(name, index) {
@@ -210,16 +351,24 @@ class ServerManagerView {
// Clear DOM elements
this.$tabsContainer.innerHTML = '';
this.$webviewsContainer.innerHTML = '';
// Destroy shortcuts
ipcRenderer.send('local-shortcuts', false);
}
reloadView() {
// Save and remember the index of last active tab so that we can use it later
const lastActiveTab = this.tabs[this.activeTabIndex].props.index;
ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab);
// Destroy the current view and re-initiate it
this.destroyView();
this.initTabs();
}
// This will trigger when pressed CTRL/CMD + R [WIP]
// It won't reload the current view properly when you add/delete a server.
reloadCurrentView() {
this.$reloadButton.click();
}
updateBadge() {
let messageCountAll = 0;
for (let i = 0; i < this.tabs.length; i++) {
@@ -235,9 +384,9 @@ class ServerManagerView {
toggleSidebar(show) {
if (show) {
this.$sidebar.classList.remove('hidden');
this.$sidebar.classList.remove('sidebar-hide');
} else {
this.$sidebar.classList.add('hidden');
this.$sidebar.classList.add('sidebar-hide');
}
}
@@ -270,14 +419,28 @@ class ServerManagerView {
ipcRenderer.on('open-about', this.openAbout.bind(this));
ipcRenderer.on('reload-viewer', this.reloadView.bind(this));
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
ipcRenderer.on('reload-current-viewer', this.reloadCurrentView.bind(this));
ipcRenderer.on('hard-reload', () => {
ipcRenderer.send('reload-full-app');
});
ipcRenderer.on('clear-app-data', () => {
ipcRenderer.send('clear-app-settings');
});
ipcRenderer.on('switch-server-tab', (event, index) => {
this.activateTab(index);
this.activateLastTab(index);
});
ipcRenderer.on('reload-proxy', (event, showAlert) => {
this.loadProxy().then(() => {
if (showAlert) {
alert('Proxy settings saved!');
}
});
});
ipcRenderer.on('toggle-sidebar', (event, show) => {
@@ -293,6 +456,18 @@ class ServerManagerView {
this.$fullscreenPopup.classList.remove('show');
});
ipcRenderer.on('focus-webview-with-id', (event, webviewId) => {
const webviews = document.querySelectorAll('webview');
webviews.forEach(webview => {
const currentId = webview.getWebContents().id;
const tabId = webview.getAttribute('data-tab-id');
const concurrentTab = document.querySelector(`div[data-tab-id="${tabId}"]`);
if (currentId === webviewId) {
concurrentTab.click();
}
});
});
ipcRenderer.on('render-taskbar-icon', (event, messageCount) => {
// Create a canvas from unread messagecounts
function createOverlayIcon(messageCount) {
@@ -326,9 +501,24 @@ class ServerManagerView {
window.onload = () => {
const serverManagerView = new ServerManagerView();
const reconnectUtil = new ReconnectUtil(serverManagerView);
serverManagerView.init();
window.addEventListener('online', () => {
serverManagerView.reloadView();
reconnectUtil.pollInternetAndReload();
});
window.addEventListener('offline', () => {
reconnectUtil.clearState();
console.log('No internet connection, you are offline.');
});
// only start electron-connect (auto reload on change) when its ran
// from `npm run dev` or `gulp dev` and not from `npm start` when
// app is started `npm start` main process's proces.argv will have
// `--no-electron-connect`
const mainProcessArgv = remote.getGlobal('process').argv;
if (isDev && !mainProcessArgv.includes('--no-electron-connect')) {
const electronConnect = require('electron-connect');
electronConnect.client.create();
}
};

View File

@@ -1,22 +0,0 @@
'use strict';
const {remote} = require('electron');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
const app = remote.app;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId('org.zulip.zulip-electron');
const NativeNotification = window.Notification;
class SilentNotification extends NativeNotification {
constructor(title, opts) {
opts.silent = ConfigUtil.getConfigItem('silent') || false;
super(title, opts);
}
}
window.Notification = SilentNotification;

View File

@@ -0,0 +1,100 @@
'use strict';
const { ipcRenderer } = require('electron');
const url = require('url');
const MacNotifier = require('node-mac-notifier');
const ConfigUtil = require('../utils/config-util');
const {
appId, customReply, focusCurrentServer, parseReply, setupReply
} = require('./helpers');
let replyHandler;
let clickHandler;
class DarwinNotification {
constructor(title, opts) {
const silent = ConfigUtil.getConfigItem('silent') || false;
const { host, protocol } = location;
const { icon } = opts;
const profilePic = url.resolve(`${protocol}//${host}`, icon);
this.tag = opts.tag;
const notification = new MacNotifier(title, Object.assign(opts, {
bundleId: appId,
canReply: true,
silent,
icon: profilePic
}));
notification.addEventListener('click', () => {
// focus to the server who sent the
// notification if not focused already
if (clickHandler) {
clickHandler();
}
focusCurrentServer();
ipcRenderer.send('focus-app');
});
notification.addEventListener('reply', this.notificationHandler);
}
static requestPermission() {
return; // eslint-disable-line no-useless-return
}
// Override default Notification permission
static get permission() {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
}
set onreply(handler) {
replyHandler = handler;
}
get onreply() {
return replyHandler;
}
set onclick(handler) {
clickHandler = handler;
}
get onclick() {
return clickHandler;
}
// not something that is common or
// used by zulip server but added to be
// future proff.
addEventListener(event, handler) {
if (event === 'click') {
clickHandler = handler;
}
if (event === 'reply') {
replyHandler = handler;
}
}
notificationHandler({ response }) {
response = parseReply(response);
focusCurrentServer();
setupReply(this.tag);
if (replyHandler) {
replyHandler(response);
return;
}
customReply(response);
}
// method specific to notification api
// used by zulip
close() {
return; // eslint-disable-line no-useless-return
}
}
module.exports = DarwinNotification;

View File

@@ -0,0 +1,31 @@
'use strict';
const { ipcRenderer } = require('electron');
const ConfigUtil = require('../utils/config-util');
const { focusCurrentServer } = require('./helpers');
const NativeNotification = window.Notification;
class BaseNotification extends NativeNotification {
constructor(title, opts) {
opts.silent = true;
super(title, opts);
this.addEventListener('click', () => {
// focus to the server who sent the
// notification if not focused already
focusCurrentServer();
ipcRenderer.send('focus-app');
});
}
static requestPermission() {
return; // eslint-disable-line no-useless-return
}
// Override default Notification permission
static get permission() {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
}
}
module.exports = BaseNotification;

View File

@@ -0,0 +1,149 @@
const { remote } = require('electron');
// Do not change this
const appId = 'org.zulip.zulip-electron';
const botsList = [];
let botsListLoaded = false;
// this function load list of bots from the server
// sync=True for a synchronous getJSON request
// in case botsList isn't already completely loaded when required in parseRely
function loadBots(sync = false) {
const { $ } = window;
botsList.length = 0;
if (sync) {
$.ajaxSetup({async: false});
}
$.getJSON('/json/users')
.done(data => {
const members = data.members;
members.forEach(membersRow => {
if (membersRow.is_bot) {
const bot = `@${membersRow.full_name}`;
const mention = `@**${bot.replace(/^@/, '')}**`;
botsList.push([bot, mention]);
}
});
botsListLoaded = true;
})
.fail(error => {
console.log('Request failed: ', error.responseText);
console.log('Request status: ', error.statusText);
});
if (sync) {
$.ajaxSetup({async: true});
}
}
function checkElements(...elements) {
let status = true;
elements.forEach(element => {
if (element === null || element === undefined) {
status = false;
}
});
return status;
}
function customReply(reply) {
// server does not support notification reply yet.
const buttonSelector = '.messagebox #send_controls button[type=submit]';
const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content';
const textarea = document.querySelector('#compose-textarea');
const messagebox = document.querySelector(messageboxSelector);
const sendButton = document.querySelector(buttonSelector);
// sanity check for old server versions
const elementsExists = checkElements(textarea, messagebox, sendButton);
if (!elementsExists) {
return;
}
textarea.value = reply;
messagebox.click();
sendButton.click();
}
const currentWindow = remote.getCurrentWindow();
const webContents = remote.getCurrentWebContents();
const webContentsId = webContents.id;
// this function will focus the server that sent
// the notification. Main function implemented in main.js
function focusCurrentServer() {
currentWindow.send('focus-webview-with-id', webContentsId);
}
// this function parses the reply from to notification
// making it easier to reply from notification eg
// @username in reply will be converted to @**username**
// #stream in reply will be converted to #**stream**
// bot mentions are not yet supported
function parseReply(reply) {
const usersDiv = document.querySelectorAll('#user_presences li');
const streamHolder = document.querySelectorAll('#stream_filters li');
const users = [];
const streams = [];
usersDiv.forEach(userRow => {
const anchor = userRow.querySelector('span a');
if (anchor !== null) {
const user = `@${anchor.textContent.trim()}`;
const mention = `@**${user.replace(/^@/, '')}**`;
users.push([user, mention]);
}
});
streamHolder.forEach(stream => {
const streamAnchor = stream.querySelector('div a');
if (streamAnchor !== null) {
const streamName = `#${streamAnchor.textContent.trim()}`;
const streamMention = `#**${streamName.replace(/^#/, '')}**`;
streams.push([streamName, streamMention]);
}
});
users.forEach(([user, mention]) => {
if (reply.includes(user)) {
const regex = new RegExp(user, 'g');
reply = reply.replace(regex, mention);
}
});
streams.forEach(([stream, streamMention]) => {
const regex = new RegExp(stream, 'g');
reply = reply.replace(regex, streamMention);
});
// If botsList isn't completely loaded yet, make a synchronous getJSON request for list
if (botsListLoaded === false) {
loadBots(true);
}
// Iterate for every bot name and replace in reply
// @botname with @**botname**
botsList.forEach(([bot, mention]) => {
if (reply.includes(bot)) {
const regex = new RegExp(bot, 'g');
reply = reply.replace(regex, mention);
}
});
reply = reply.replace(/\\n/, '\n');
return reply;
}
function setupReply(id) {
const { narrow } = window;
narrow.by_subject(id, { trigger: 'notification' });
}
module.exports = {
appId,
checkElements,
customReply,
parseReply,
setupReply,
focusCurrentServer,
loadBots
};

View File

@@ -0,0 +1,27 @@
'use strict';
const {
remote: { app }
} = require('electron');
const DefaultNotification = require('./default-notification');
const { appId, loadBots } = require('./helpers');
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId(appId);
window.Notification = DefaultNotification;
if (process.platform === 'darwin') {
const DarwinNotification = require('./darwin-notifications');
window.Notification = DarwinNotification;
}
window.addEventListener('load', () => {
// Call this function only when user is logged in
// eslint-disable-next-line no-undef, camelcase
if (page_params.realm_uri) {
loadBots();
}
});

View File

@@ -0,0 +1,63 @@
'use strict';
const electron = require('electron');
const { app } = require('electron');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
let instance = null;
class BadgeSettings {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
}
showBadgeCount(messageCount, mainWindow) {
if (process.platform === 'darwin') {
app.setBadgeCount(messageCount);
}
if (process.platform === 'win32') {
this.updateOverlayIcon(messageCount, mainWindow);
}
}
hideBadgeCount(mainWindow) {
if (process.platform === 'darwin') {
app.setBadgeCount(0);
}
if (process.platform === 'win32') {
mainWindow.setOverlayIcon(null, '');
}
}
updateBadge(badgeCount, mainWindow) {
if (ConfigUtil.getConfigItem('badgeOption', true)) {
this.showBadgeCount(badgeCount, mainWindow);
} else {
this.hideBadgeCount(mainWindow);
}
}
updateOverlayIcon(messageCount, mainWindow) {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {
mainWindow.webContents.send('render-taskbar-icon', messageCount);
}
}
updateTaskbarIcon(data, text, mainWindow) {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
}
}
module.exports = new BadgeSettings();

View File

@@ -0,0 +1,46 @@
'use strict';
const {ipcRenderer} = require('electron');
const BaseComponent = require(__dirname + '/../../components/base.js');
class BaseSection extends BaseComponent {
generateSettingOption(props) {
const {$element, value, clickHandler} = props;
$element.innerHTML = '';
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value));
$element.appendChild($optionControl);
$optionControl.addEventListener('click', clickHandler);
}
generateOptionTemplate(settingOption) {
if (settingOption) {
return `
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" checked>
<label></label>
</div>
</div>
`;
} else {
return `
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox">
<label></label>
</div>
</div>
`;
}
}
reloadApp() {
ipcRenderer.send('forward-message', 'reload-viewer');
}
}
module.exports = BaseSection;

View File

@@ -0,0 +1,37 @@
'use strict';
const BaseComponent = require(__dirname + '/../../components/base.js');
const shell = require('electron').shell;
class CreateOrganziation extends BaseComponent {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="setting-row">
<div class="setting-description">
<span id="open-create-org-link">Or create a new organization on zulipchat.com<i class="material-icons open-tab-button">open_in_new</i></span>
</div>
<div class="setting-control"></div>
</div>
`;
}
init() {
this.props.$root.innerHTML = this.template();
this.openCreateNewOrgExternalLink();
}
openCreateNewOrgExternalLink() {
const link = 'https://zulipchat.com/beta/';
const externalCreateNewOrgEl = document.getElementById('open-create-org-link');
externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link);
});
}
}
module.exports = CreateOrganziation;

View File

@@ -1,11 +1,14 @@
'use strict';
const path = require('path');
const { ipcRenderer, remote } = require('electron');
const fs = require('fs-extra');
const {ipcRenderer} = require('electron');
const BaseComponent = require(__dirname + '/../../components/base.js');
const { app, dialog } = remote;
const currentBrowserWindow = remote.getCurrentWindow();
const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
class GeneralSection extends BaseComponent {
class GeneralSection extends BaseSection {
constructor(props) {
super();
this.props = props;
@@ -13,144 +16,241 @@ class GeneralSection extends BaseComponent {
template() {
return `
<div class="settings-pane" id="server-settings-pane">
<div class="title">Tray Options</div>
<div id="tray-option-settings" class="settings-card">
<div class="setting-row">
<div class="settings-pane">
<div class="title">Appearance</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="tray-option">
<div class="setting-description">Show app icon in system tray</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">App Updates</div>
<div id="betaupdate-option-settings" class="settings-card">
<div class="setting-row">
<div class="setting-description">Get beta updates</div>
<div class="setting-row" id="sidebar-option">
<div class="setting-description">Show sidebar (<span class="code">Cmd Or Ctrl+S</span>)</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="badge-option">
<div class="setting-description">Show app unread badge</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
<div class="setting-description">Flash taskbar on new message</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Desktop Notification</div>
<div id="silent-option-settings" class="settings-card">
<div class="setting-row">
<div class="setting-description">Mute all sounds from Zulip (requires reload)</div>
<div class="settings-card">
<div class="setting-row" id="show-notification-option">
<div class="setting-description">Show Desktop Notifications</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="silent-option">
<div class="setting-description">Mute all sounds from Zulip</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">User Interface</div>
<div id="ui-option-settings" class="settings-card">
<div class="setting-row" id="sidebar-option">
<div class="setting-description">Show sidebar (<span class="code">CmdOrCtrl+S</span>)</div>
<div class="title">App Updates</div>
<div class="settings-card">
<div class="setting-row" id="betaupdate-option">
<div class="setting-description">Get beta updates</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Functionality</div>
<div class="settings-card">
<div class="setting-row" id="startAtLogin-option">
<div class="setting-description">Start app at login</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="start-minimize-option">
<div class="setting-description">Always start minimized</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="enable-spellchecker-option">
<div class="setting-description">Enable Spellchecker (requires restart)</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Reset Application Data</div>
<div class="settings-card">
<div class="setting-row" id="resetdata-option">
<div class="setting-description">This will delete all application data including all added accounts and preferences
</div>
<button class="reset-data-button blue">Reset App Data</button>
</div>
</div>
</div>
`;
}
settingsOptionTemplate(settingOption) {
if (settingOption) {
return `
<div class="action green">
<span>On</span>
</div>
`;
} else {
return `
<div class="action red">
<span>Off</span>
</div>
`;
init() {
this.props.$root.innerHTML = this.template();
this.updateTrayOption();
this.updateBadgeOption();
this.updateSilentOption();
this.updateUpdateOption();
this.updateSidebarOption();
this.updateStartAtLoginOption();
this.updateResetDataOption();
this.showDesktopNotification();
this.enableSpellchecker();
this.minimizeOnStart();
// Platform specific settings
// Flashing taskbar on Windows
if (process.platform === 'win32') {
this.updateFlashTaskbar();
}
}
trayOptionTemplate(trayOption) {
this.settingsOptionTemplate(trayOption);
}
updateOptionTemplate(updateOption) {
this.settingsOptionTemplate(updateOption);
}
silentOptionTemplate(silentOption) {
this.settingsOptionTemplate(silentOption);
}
sidebarToggleTemplate(toggleOption) {
this.settingsOptionTemplate(toggleOption);
}
init() {
this.props.$root.innerHTML = this.template();
this.initTrayOption();
this.initUpdateOption();
this.initSilentOption();
this.initSidebarToggle();
}
initTrayOption() {
this.$trayOptionSettings = document.querySelector('#tray-option-settings .setting-control');
this.$trayOptionSettings.innerHTML = '';
const trayOption = ConfigUtil.getConfigItem('trayIcon', true);
const $trayOption = this.generateNodeFromTemplate(this.settingsOptionTemplate(trayOption));
this.$trayOptionSettings.appendChild($trayOption);
$trayOption.addEventListener('click', () => {
const newValue = !ConfigUtil.getConfigItem('trayIcon');
ConfigUtil.setConfigItem('trayIcon', newValue);
ipcRenderer.send('forward-message', 'toggletray');
this.initTrayOption();
updateTrayOption() {
this.generateSettingOption({
$element: document.querySelector('#tray-option .setting-control'),
value: ConfigUtil.getConfigItem('trayIcon', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('trayIcon');
ConfigUtil.setConfigItem('trayIcon', newValue);
ipcRenderer.send('forward-message', 'toggletray');
this.updateTrayOption();
}
});
}
initUpdateOption() {
this.$updateOptionSettings = document.querySelector('#betaupdate-option-settings .setting-control');
this.$updateOptionSettings.innerHTML = '';
const updateOption = ConfigUtil.getConfigItem('betaUpdate', false);
const $updateOption = this.generateNodeFromTemplate(this.settingsOptionTemplate(updateOption));
this.$updateOptionSettings.appendChild($updateOption);
$updateOption.addEventListener('click', () => {
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
ConfigUtil.setConfigItem('betaUpdate', newValue);
this.initUpdateOption();
updateBadgeOption() {
this.generateSettingOption({
$element: document.querySelector('#badge-option .setting-control'),
value: ConfigUtil.getConfigItem('badgeOption', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('badgeOption');
ConfigUtil.setConfigItem('badgeOption', newValue);
ipcRenderer.send('toggle-badge-option', newValue);
this.updateBadgeOption();
}
});
}
initSilentOption() {
this.$silentOptionSettings = document.querySelector('#silent-option-settings .setting-control');
this.$silentOptionSettings.innerHTML = '';
const silentOption = ConfigUtil.getConfigItem('silent', false);
const $silentOption = this.generateNodeFromTemplate(this.settingsOptionTemplate(silentOption));
this.$silentOptionSettings.appendChild($silentOption);
$silentOption.addEventListener('click', () => {
const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue);
this.initSilentOption();
updateFlashTaskbar() {
this.generateSettingOption({
$element: document.querySelector('#flash-taskbar-option .setting-control'),
value: ConfigUtil.getConfigItem('flashTaskbarOnMessage', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('flashTaskbarOnMessage');
ConfigUtil.setConfigItem('flashTaskbarOnMessage', newValue);
this.updateFlashTaskbar();
}
});
}
initSidebarToggle() {
this.$sidebarOptionSettings = document.querySelector('#ui-option-settings #sidebar-option .setting-control');
this.$sidebarOptionSettings.innerHTML = '';
const sidebarOption = ConfigUtil.getConfigItem('show-sidebar', true);
const $sidebarOption = this.generateNodeFromTemplate(this.settingsOptionTemplate(sidebarOption));
this.$sidebarOptionSettings.appendChild($sidebarOption);
$sidebarOption.addEventListener('click', () => {
const newValue = !ConfigUtil.getConfigItem('show-sidebar');
ConfigUtil.setConfigItem('show-sidebar', newValue);
ipcRenderer.send('forward-message', 'toggle-sidebar', newValue);
this.initSidebarToggle();
updateUpdateOption() {
this.generateSettingOption({
$element: document.querySelector('#betaupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('betaUpdate', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
ConfigUtil.setConfigItem('betaUpdate', newValue);
this.updateUpdateOption();
}
});
}
handleServerInfoChange() {
ipcRenderer.send('forward-message', 'reload-viewer');
updateSilentOption() {
this.generateSettingOption({
$element: document.querySelector('#silent-option .setting-control'),
value: ConfigUtil.getConfigItem('silent', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption();
currentBrowserWindow.send('toogle-silent', newValue);
}
});
}
showDesktopNotification() {
this.generateSettingOption({
$element: document.querySelector('#show-notification-option .setting-control'),
value: ConfigUtil.getConfigItem('showNotification', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showNotification', true);
ConfigUtil.setConfigItem('showNotification', newValue);
this.showDesktopNotification();
}
});
}
updateSidebarOption() {
this.generateSettingOption({
$element: document.querySelector('#sidebar-option .setting-control'),
value: ConfigUtil.getConfigItem('showSidebar', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showSidebar');
ConfigUtil.setConfigItem('showSidebar', newValue);
ipcRenderer.send('forward-message', 'toggle-sidebar', newValue);
this.updateSidebarOption();
}
});
}
updateStartAtLoginOption() {
this.generateSettingOption({
$element: document.querySelector('#startAtLogin-option .setting-control'),
value: ConfigUtil.getConfigItem('startAtLogin', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('startAtLogin');
ConfigUtil.setConfigItem('startAtLogin', newValue);
ipcRenderer.send('toggleAutoLauncher', newValue);
this.updateStartAtLoginOption();
}
});
}
enableSpellchecker() {
this.generateSettingOption({
$element: document.querySelector('#enable-spellchecker-option .setting-control'),
value: ConfigUtil.getConfigItem('enableSpellchecker', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('enableSpellchecker');
ConfigUtil.setConfigItem('enableSpellchecker', newValue);
this.enableSpellchecker();
}
});
}
clearAppDataDialog() {
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
const getAppPath = path.join(app.getPath('appData'), app.getName());
dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure',
detail: clearAppDataMessage
}, response => {
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
}
});
}
updateResetDataOption() {
const resetDataButton = document.querySelector('#resetdata-option .reset-data-button');
resetDataButton.addEventListener('click', () => {
this.clearAppDataDialog();
});
}
minimizeOnStart() {
this.generateSettingOption({
$element: document.querySelector('#start-minimize-option .setting-control'),
value: ConfigUtil.getConfigItem('startMinimized', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('startMinimized');
ConfigUtil.setConfigItem('startMinimized', newValue);
this.minimizeOnStart();
}
});
}
}
module.exports = GeneralSection;

View File

@@ -8,7 +8,7 @@ class PreferenceNav extends BaseComponent {
this.props = props;
this.navItems = ['General', 'Servers'];
this.navItems = ['General', 'Network', 'Servers', 'Shortcuts'];
this.init();
}

View File

@@ -0,0 +1,102 @@
'use strict';
const {ipcRenderer} = require('electron');
const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
class NetworkSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="settings-pane">
<div class="title">Proxy</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="use-proxy-option">
<div class="setting-description">Connect servers through a proxy</div>
<div class="setting-control"></div>
</div>
<div class="setting-block">
<div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">PAC script</span>
<input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/>
</div>
<div class="setting-row" id="proxy-rules-option">
<span class="setting-input-key">Proxy rules</span>
<input class="setting-input-value" placeholder="e.g. http=foopy:80;ftp=foopy2"/>
</div>
<div class="setting-row" id="proxy-bypass-option">
<span class="setting-input-key">Proxy bypass rules</span>
<input class="setting-input-value" placeholder="e.g. foobar.com"/>
</div>
<div class="setting-row">
<div class="action blue" id="proxy-save-action">
<i class="material-icons">check_box</i>
<span>Save</span>
</div>
</div>
</div>
</div>
</div>
`;
}
init() {
this.props.$root.innerHTML = this.template();
this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
this.$proxySaveAction = document.getElementById('proxy-save-action');
this.$settingBlock = this.props.$root.querySelector('.setting-block');
this.initProxyOption();
this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
this.$proxyRules.value = ConfigUtil.getConfigItem('proxyRules', '');
this.$proxyBypass.value = ConfigUtil.getConfigItem('proxyBypass', '');
this.$proxySaveAction.addEventListener('click', () => {
ConfigUtil.setConfigItem('proxyPAC', this.$proxyPAC.value);
ConfigUtil.setConfigItem('proxyRules', this.$proxyRules.value);
ConfigUtil.setConfigItem('proxyBypass', this.$proxyBypass.value);
ipcRenderer.send('forward-message', 'reload-proxy', true);
});
}
initProxyOption() {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
this.toggleProxySettings(proxyEnabled);
this.updateProxyOption();
}
toggleProxySettings(option) {
if (option) {
this.$settingBlock.classList.remove('hidden');
} else {
this.$settingBlock.classList.add('hidden');
}
}
updateProxyOption() {
this.generateSettingOption({
$element: document.querySelector('#use-proxy-option .setting-control'),
value: ConfigUtil.getConfigItem('useProxy', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('useProxy');
ConfigUtil.setConfigItem('useProxy', newValue);
this.toggleProxySettings(newValue);
if (newValue === false) {
// Reload proxy if the proxy is turned off
ipcRenderer.send('forward-message', 'reload-proxy', false);
}
this.updateProxyOption();
}
});
}
}
module.exports = NetworkSection;

View File

@@ -11,15 +11,16 @@ class NewServerForm extends BaseComponent {
template() {
return `
<div class="settings-card" style="border: solid 1px #4CAF50;">
<div class="settings-card">
<div class="server-info-right">
<div class="title">URL of Zulip organization</div>
<div class="server-info-row">
<input class="server-info-url" autofocus placeholder="Enter the url of your Zulip server..."/>
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or chat.your-organization.com"/>
</div>
<div class="server-info-row">
<div class="action green server-save-action">
<i class="material-icons">check_box</i>
<span>Save</span>
<div class="action blue server-save-action">
<i class="material-icons">add_box</i>
<span>Add</span>
</div>
</div>
</div>
@@ -38,18 +39,31 @@ class NewServerForm extends BaseComponent {
this.props.$root.innerHTML = '';
this.props.$root.appendChild(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.server-info-url')[0];
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0];
}
submitFormHandler() {
this.$saveServerButton.children[1].innerHTML = 'Adding...';
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
DomainUtil.addDomain(serverConf).then(() => {
this.props.onChange(this.props.index);
});
}, errorMessage => {
this.$saveServerButton.children[1].innerHTML = 'Add';
alert(errorMessage);
});
}
initActions() {
this.$saveServerButton.addEventListener('click', () => {
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
DomainUtil.addDomain(serverConf).then(() => {
this.props.onChange(this.props.index);
});
}, errorMessage => {
alert(errorMessage);
});
this.submitFormHandler();
});
this.$newServerUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode;
// Submit form when Enter key is pressed
if (EnterkeyCode === 13) {
this.submitFormHandler();
}
});
}
}

View File

@@ -1,11 +1,13 @@
'use strict';
const BaseComponent = require(__dirname + '/js/components/base.js');
const {ipcRenderer} = require('electron');
const { ipcRenderer } = require('electron');
const Nav = require(__dirname + '/js/pages/preference/nav.js');
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js');
const NetworkSection = require(__dirname + '/js/pages/preference/network-section.js');
const ShortcutsSection = require(__dirname + '/js/pages/preference/shortcuts-section.js');
class PreferenceView extends BaseComponent {
constructor() {
@@ -49,16 +51,45 @@ class PreferenceView extends BaseComponent {
});
break;
}
case 'Network': {
this.section = new NetworkSection({
$root: this.$settingsContainer
});
break;
}
case 'Shortcuts': {
this.section = new ShortcutsSection({
$root: this.$settingsContainer
});
break;
}
default: break;
}
this.section.init();
window.location.hash = `#${navItem}`;
}
// Handle toggling and reflect changes in preference page
handleToggle(elementName, state) {
const inputSelector = `#${elementName} .action .switch input`;
const input = document.querySelector(inputSelector);
if (input) {
input.checked = state;
}
}
registerIpcs() {
ipcRenderer.on('switch-settings-nav', (event, navItem) => {
this.handleNavigation(navItem);
});
ipcRenderer.on('toggle-sidebar', (event, state) => {
this.handleToggle('sidebar-option', state);
});
ipcRenderer.on('toggletray', (event, state) => {
this.handleToggle('tray-option', state);
});
}
}

View File

@@ -20,10 +20,10 @@ class ServerInfoForm extends BaseComponent {
<div class="server-info-right">
<div class="server-info-row">
<span class="server-info-alias">${this.props.server.alias}</span>
<i class="material-icons open-tab-button">open_in_new</i>
<i class="material-icons open-tab-button">open_in_new</i>
</div>
<div class="server-info-row">
<input class="server-info-url" disabled value="${this.props.server.url}"/>
<input class="setting-input-value" disabled value="${this.props.server.url}"/>
</div>
<div class="server-info-row">
<div class="action red server-delete-action">

View File

@@ -1,13 +1,12 @@
'use strict';
const {ipcRenderer} = require('electron');
const BaseComponent = require(__dirname + '/../../components/base.js');
const BaseSection = require(__dirname + '/base-section.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const ServerInfoForm = require(__dirname + '/server-info-form.js');
const NewServerForm = require(__dirname + '/new-server-form.js');
const CreateOrganziation = require(__dirname + '/create-new-org.js');
class ServersSection extends BaseComponent {
class ServersSection extends BaseSection {
constructor(props) {
super();
this.props = props;
@@ -16,23 +15,17 @@ class ServersSection extends BaseComponent {
template() {
return `
<div class="settings-pane" id="server-settings-pane">
<div class="title">Manage Servers</div>
<div class="actions-container">
<div class="action green" id="new-server-action">
<i class="material-icons">add_box</i>
<span>Add Server</span>
</div>
</div>
<div id="new-server-container" class="hidden"></div>
<div class="sub-title" id="existing-servers"></div>
<div class="page-title">Register or login to a Zulip organization to get started</div>
<div id="new-server-container"></div>
<div class="title" id="existing-servers"></div>
<div id="server-info-container"></div>
<div id="create-organization-container"></div>
</div>
`;
}
init() {
this.initServers();
this.initActions();
}
initServers() {
@@ -45,37 +38,36 @@ class ServersSection extends BaseComponent {
this.$newServerContainer = document.getElementById('new-server-container');
this.$newServerButton = document.getElementById('new-server-action');
this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!';
this.$serverInfoContainer.innerHTML = servers.length ? '' : '';
// Show Existing servers if servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing servers';
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing organizations';
this.initNewServerForm();
this.$createOrganizationContainer = document.getElementById('create-organization-container');
this.initCreateNewOrganization();
for (let i = 0; i < servers.length; i++) {
new ServerInfoForm({
$root: this.$serverInfoContainer,
server: servers[i],
index: i,
onChange: this.handleServerInfoChange.bind(this)
onChange: this.reloadApp
}).init();
}
}
initCreateNewOrganization() {
new CreateOrganziation({
$root: this.$createOrganizationContainer
}).init();
}
initNewServerForm() {
new NewServerForm({
$root: this.$newServerContainer,
onChange: this.handleServerInfoChange.bind(this)
onChange: this.reloadApp
}).init();
}
initActions() {
this.$newServerContainer.classList.remove('hidden');
this.$newServerButton.classList.remove('green');
this.$newServerButton.classList.add('grey');
}
handleServerInfoChange() {
ipcRenderer.send('forward-message', 'reload-viewer');
}
}
module.exports = ServersSection;

View File

@@ -0,0 +1,311 @@
'use strict';
const BaseSection = require(__dirname + '/base-section.js');
class ShortcutsSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
templateMac() {
const userOSKey = '⌘';
return `
<div class="settings-pane">
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>,</kbd></td>
<td>Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
<td>Reset App Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>L</kbd></td>
<td>Log Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Zulip</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Others</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Q</kbd></td>
<td>Quit Zulip</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Undo</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Redo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>X</kbd></td>
<td>Cut</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>C</kbd></td>
<td>Copy</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste and Match Style</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
<td>Select All</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
<td>Find</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>G</kbd></td>
<td>Find Next</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
<td>Emoji & Symbols</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Reload</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Hard Reload</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
<td>Enter Full Screen</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>+</kbd></td>
<td>Zoom In</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>-</kbd></td>
<td>Zoom Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>0</kbd></td>
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>I</kbd></td>
<td>Toggle DevTools for Zulip App</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>U</kbd></td>
<td>Toggle DevTools for Active Tab</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>←</kbd></td>
<td>Back</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>→</kbd></td>
<td>Forward</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>M</kbd></td>
<td>Minimize</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>W</kbd></td>
<td>Close</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
templateWinLin() {
const userOSKey = 'Ctrl';
return `
<div class="settings-pane">
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>,</kbd></td>
<td>Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
<td>Log Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Q</kbd></td>
<td>Quit Zulip</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Z</kbd></td>
<td>Undo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Y</kbd></td>
<td>Redo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>X</kbd></td>
<td>Cut</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>C</kbd></td>
<td>Copy</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>V</kbd></td>
<td>Paste</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd></td>
<td>Paste and Match Style</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>A</kbd></td>
<td>Select All</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>R</kbd></td>
<td>Reload</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
<td>Hard Reload</td>
</tr>
<tr>
<td><kbd>F11</kbd></td>
<td>Toggle Full Screen</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>=</kbd></td>
<td>Zoom In</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>-</kbd></td>
<td>Zoom Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>0</kbd></td>
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
<td>Toggle DevTools for Zulip App</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
<td>Toggle DevTools for Active Tab</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
<td>Back</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
<td>Forward</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>M</kbd></td>
<td>Minimize</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>W</kbd></td>
<td>Close</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
init() {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
}
}
module.exports = ShortcutsSection;

View File

@@ -1,7 +1,10 @@
'use strict';
const {ipcRenderer} = require('electron');
const {spellChecker} = require('./spellchecker');
const { ipcRenderer } = require('electron');
const SetupSpellChecker = require('./spellchecker');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
// eslint-disable-next-line import/no-unassigned-import
require('./notification');
@@ -32,11 +35,29 @@ process.once('loaded', () => {
// To prevent failing this script on linux we need to load it after the document loaded
document.addEventListener('DOMContentLoaded', () => {
// Init spellchecker
spellChecker();
// Get the default language of the server
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, camelcase
if (serverLanguage) {
// Set spellcheker language
ConfigUtil.setConfigItem('spellcheckerLanguage', serverLanguage);
// Init spellchecker
SetupSpellChecker.init();
}
// redirect users to network troubleshooting page
document.querySelector('.restart_get_events_button').addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
getRestartButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
});
// Clean up spellchecker events after you navigate away from this page;
// otherwise, you may experience errors
window.addEventListener('beforeunload', () => {
SetupSpellChecker.unsubscribeSpellChecker();
});

View File

@@ -1,29 +1,56 @@
'use strict';
const {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} = require('electron-spellchecker');
const { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } = require('electron-spellchecker');
function spellChecker() {
// Implement spellcheck using electron api
window.spellCheckHandler = new SpellCheckHandler();
window.spellCheckHandler.attachToInput();
const ConfigUtil = require(__dirname + '/utils/config-util.js');
// Start off as US English
window.spellCheckHandler.switchLanguage('en-US');
class SetupSpellChecker {
init() {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
this.enableSpellChecker();
}
this.enableContextMenu();
}
const contextMenuBuilder = new ContextMenuBuilder(window.spellCheckHandler);
const contextMenuListener = new ContextMenuListener(info => {
contextMenuBuilder.showPopupMenu(info);
});
enableSpellChecker() {
try {
this.SpellCheckHandler = new SpellCheckHandler();
} catch (err) {
console.log(err);
}
}
// Clean up events after you navigate away from this page;
// otherwise, you may experience errors
window.addEventListener('beforeunload', () => {
// eslint-disable-next-line no-undef
spellCheckHandler.unsubscribe();
contextMenuListener.unsubscribe();
});
enableContextMenu() {
if (this.SpellCheckHandler) {
this.SpellCheckHandler.attachToInput();
const userLanguage = ConfigUtil.getConfigItem('spellcheckerLanguage');
// eslint-disable-next-line no-unused-expressions
process.platform === 'darwin' ?
// On macOS, spellchecker fails to auto-detect the lanugage user is typing in
// that's why we need to mention it explicitly
this.SpellCheckHandler.switchLanguage(userLanguage) :
// On Linux and Windows, spellchecker can automatically detects the language the user is typing in
// and silently switches on the fly; thus we can start off as US English
this.SpellCheckHandler.switchLanguage('en-US');
}
const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
this.contextMenuListener = new ContextMenuListener(info => {
contextMenuBuilder.showPopupMenu(info);
});
}
unsubscribeSpellChecker() {
// eslint-disable-next-line no-undef
if (this.SpellCheckHandler) {
this.SpellCheckHandler.unsubscribe();
}
if (this.contextMenuListener) {
this.contextMenuListener.unsubscribe();
}
}
}
module.exports = {
spellChecker
};
module.exports = new SetupSpellChecker();

View File

@@ -180,11 +180,10 @@ ipcRenderer.on('tray', (event, arg) => {
if (!window.tray) {
return;
}
// We don't want to create tray from unread messages on windows and macOS since these systems already have dock badges and taskbar overlay icon.
if (process.platform === 'linux') {
// We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === 'linux' || process.platform === 'win32') {
if (arg === 0) {
unread = arg;
// Message Count // console.log("message count is zero.");
window.tray.setImage(iconPath());
window.tray.setToolTip('No unread messages');
} else {
@@ -198,15 +197,18 @@ ipcRenderer.on('tray', (event, arg) => {
});
function toggleTray() {
let state;
if (window.tray) {
state = false;
window.tray.destroy();
if (window.tray.isDestroyed()) {
window.tray = null;
}
ConfigUtil.setConfigItem('trayIcon', false);
} else {
state = true;
createTray();
if (process.platform === 'linux') {
if (process.platform === 'linux' || process.platform === 'win32') {
renderNativeImage(unread).then(image => {
window.tray.setImage(image);
window.tray.setToolTip(unread + ' unread messages');
@@ -214,6 +216,10 @@ function toggleTray() {
}
ConfigUtil.setConfigItem('trayIcon', true);
}
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggletray', state);
}
ipcRenderer.on('toggletray', toggleTray);

View File

@@ -1,16 +1,29 @@
'use strict';
const fs = require('fs');
const path = require('path');
const process = require('process');
const JsonDB = require('node-json-db');
const Logger = require('./logger-util');
const logger = new Logger({
file: 'config-util.log',
timestamp: true
});
let instance = null;
let dialog = null;
let app = null;
/* To make the util runnable in both main and renderer process */
if (process.type === 'renderer') {
app = require('electron').remote.app;
const remote = require('electron').remote;
dialog = remote.dialog;
app = remote.app;
} else {
app = require('electron').app;
const electron = require('electron');
dialog = electron.dialog;
app = electron.app;
}
class ConfigUtil {
@@ -26,6 +39,7 @@ class ConfigUtil {
}
getConfigItem(key, defaultValue = null) {
this.reloadDB();
const value = this.db.getData('/')[key];
if (value === undefined) {
this.setConfigItem(key, defaultValue);
@@ -46,7 +60,22 @@ class ConfigUtil {
}
reloadDB() {
this.db = new JsonDB(app.getPath('userData') + '/settings.json', true, true);
const settingsJsonPath = path.join(app.getPath('userData'), '/settings.json');
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered error while saving current settings.'
);
logger.error('Error while JSON parsing settings.json: ');
logger.error(err);
}
}
this.db = new JsonDB(settingsJsonPath, true, true);
}
}

View File

@@ -0,0 +1,31 @@
const fs = require('fs');
let app = null;
let setupCompleted = false;
if (process.type === 'renderer') {
app = require('electron').remote.app;
} else {
app = require('electron').app;
}
const zulipDir = app.getPath('userData');
const logDir = `${zulipDir}/Logs/`;
const initSetUp = () => {
// if it is the first time the app is running
// create zulip dir in userData folder to
// avoid errors
if (!setupCompleted) {
if (!fs.existsSync(zulipDir)) {
fs.mkdirSync(zulipDir);
}
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
setupCompleted = true;
}
};
module.exports = {
initSetUp
};

View File

@@ -1,10 +1,16 @@
'use strict';
const {app, dialog} = require('electron').remote;
const { app, dialog } = require('electron').remote;
const fs = require('fs');
const path = require('path');
const JsonDB = require('node-json-db');
const request = require('request');
const Logger = require('./logger-util');
const logger = new Logger({
file: `domain-util.log`,
timestamp: true
});
let instance = null;
@@ -45,6 +51,11 @@ class DomainUtil {
return this.db.getData(`/domains[${index}]`);
}
updateDomain(index, server) {
this.reloadDB();
this.db.push(`/domains[${index}]`, server, true);
}
addDomain(server) {
return new Promise(resolve => {
if (server.icon) {
@@ -73,11 +84,25 @@ class DomainUtil {
this.reloadDB();
}
checkDomain(domain) {
const hasPrefix = (domain.indexOf('http') === 0);
if (!hasPrefix) {
domain = (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
// Check if domain is already added
duplicateDomain(domain) {
domain = this.formatUrl(domain);
const servers = this.getDomains();
for (const i in servers) {
if (servers[i].url === domain) {
return true;
}
}
return false;
}
checkDomain(domain, silent = false) {
if (!silent && this.duplicateDomain(domain)) {
// Do not check duplicate in silent mode
return Promise.reject('This server has been added.');
}
domain = this.formatUrl(domain);
const checkDomain = domain + '/static/audio/zulip.ogg';
@@ -90,35 +115,62 @@ class DomainUtil {
return new Promise((resolve, reject) => {
request(checkDomain, (error, response) => {
const certsError =
['Error: self signed certificate',
'Error: unable to verify the first certificate'
[
'Error: self signed certificate',
'Error: unable to verify the first certificate',
'Error: unable to get local issuer certificate'
];
if (!error && response.statusCode !== 404) {
// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];
// make sure that error is a error or string not undefined
// so validation does not throw error.
error = error || '';
if (!error && response.statusCode < 400) {
// Correct
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
}, () => {
resolve(serverConf);
});
} else if (certsError.indexOf(error.toString()) >= 0) {
dialog.showMessageBox({
type: 'question',
buttons: ['Yes', 'No'],
defaultId: 0,
message: `Do you trust certificate from ${domain}? \n ${error}`
}, response => {
if (response === 0) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
}, () => {
resolve(serverConf);
});
} else {
reject('Untrusted Certificate.');
}
});
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError.indexOf(error.toString()) >= 0) {
if (silent) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
}, () => {
resolve(serverConf);
});
} else {
const certErrorMessage = `Do you trust certificate from ${domain}? \n ${error}`;
const certErrorDetail = `The server you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\n Unless you have a good reason to believe otherwise, you should not proceed.
\n You can click here if you'd like to proceed with the connection.`;
dialog.showMessageBox({
type: 'warning',
buttons: ['Yes', 'No'],
defaultId: 0,
message: certErrorMessage,
detail: certErrorDetail
}, response => {
if (response === 0) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
}, () => {
resolve(serverConf);
});
} else {
reject('Untrusted Certificate.');
}
});
}
} else {
reject('Not a valid Zulip server');
const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \
\n(1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server`;
reject(invalidZulipServerError);
}
});
});
@@ -132,7 +184,9 @@ class DomainUtil {
const data = JSON.parse(response.body);
if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
resolve({
icon: data.realm_uri + data.realm_icon,
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri,
alias: data.realm_name
});
@@ -146,14 +200,8 @@ class DomainUtil {
saveServerIcon(url) {
// The save will always succeed. If url is invalid, downgrade to default icon.
const dir = `${app.getPath('userData')}/server-icons`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return new Promise(resolve => {
const filePath = `${dir}/${new Date().getMilliseconds()}${path.extname(url).split('?')[0]}`;
const filePath = this.generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(url).on('response', response => {
@@ -175,8 +223,63 @@ class DomainUtil {
});
}
updateSavedServer(url, index) {
// Does not promise successful update
this.checkDomain(url, true).then(newServerConf => {
this.saveServerIcon(newServerConf.icon).then(localIconUrl => {
newServerConf.icon = localIconUrl;
this.updateDomain(index, newServerConf);
this.reloadDB();
});
});
}
reloadDB() {
this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
const domainJsonPath = path.join(app.getPath('userData'), '/domain.json');
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(
'Error saving new organization',
'There seems to be error while saving new organization, ' +
'you may have to re-add your previous organizations back.'
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
}
}
this.db = new JsonDB(domainJsonPath, true, true);
}
generateFilePath(url) {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let len = url.length;
while (len) {
hash = (hash * 33) ^ url.charCodeAt(--len);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
}
formatUrl(domain) {
const hasPrefix = (domain.indexOf('http') === 0);
if (hasPrefix) {
return domain;
} else {
return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
}
}
}

View File

@@ -0,0 +1,74 @@
'use strict';
const fs = require('fs');
const path = require('path');
const process = require('process');
const remote =
process.type === 'renderer' ? require('electron').remote : require('electron');
const JsonDB = require('node-json-db');
const Logger = require('./logger-util');
const logger = new Logger({
file: 'linux-update-util.log',
timestamp: true
});
/* To make the util runnable in both main and renderer process */
const { dialog, app } = remote;
let instance = null;
class LinuxUpdateUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB();
return instance;
}
getUpdateItem(key, defaultValue = null) {
this.reloadDB();
const value = this.db.getData('/')[key];
if (value === undefined) {
this.setUpdateItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
}
setUpdateItem(key, value) {
this.db.push(`/${key}`, value, true);
this.reloadDB();
}
removeUpdateItem(key) {
this.db.delete(`/${key}`);
this.reloadDB();
}
reloadDB() {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/updates.json');
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
'Error saving update notifications.',
'We encountered error while saving update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(err);
}
}
this.db = new JsonDB(linuxUpdateJsonPath, true, true);
}
}
module.exports = new LinuxUpdateUtil();

View File

@@ -0,0 +1,87 @@
const NodeConsole = require('console').Console;
const fs = require('fs');
const isDev = require('electron-is-dev');
const { initSetUp } = require('./default-util');
initSetUp();
let app = null;
if (process.type === 'renderer') {
app = require('electron').remote.app;
} else {
app = require('electron').app;
}
const browserConsole = console;
const logDir = `${app.getPath('userData')}/Logs`;
class Logger {
constructor(opts = {}) {
let {
timestamp = true,
file = 'console.log',
level = true,
logInDevMode = false
} = opts;
file = `${logDir}/${file}`;
if (timestamp === true) {
timestamp = this.getTimestamp;
}
const fileStream = fs.createWriteStream(file, { flags: 'a' });
const nodeConsole = new NodeConsole(fileStream);
this.nodeConsole = nodeConsole;
this.timestamp = timestamp;
this.level = level;
this.logInDevMode = logInDevMode;
this.setUpConsole();
}
_log(type, ...args) {
const {
nodeConsole, timestamp, level, logInDevMode
} = this;
let nodeConsoleLog;
/* eslint-disable no-fallthrough */
switch (true) {
case typeof timestamp === 'function':
args.unshift(timestamp() + ' |\t');
case (level !== false):
args.unshift(type.toUpperCase() + ' |');
case isDev || logInDevMode:
nodeConsoleLog = nodeConsole[type] || nodeConsole.log;
nodeConsoleLog.apply(null, args);
default: break;
}
/* eslint-enable no-fallthrough */
browserConsole[type].apply(null, args);
}
setUpConsole() {
for (const type in browserConsole) {
this.setupConsoleMethod(type);
}
}
setupConsoleMethod(type) {
this[type] = (...args) => {
this._log(type, ...args);
};
}
getTimestamp() {
const date = new Date();
const timestamp =
`${date.getMonth()}/${date.getDate()} ` +
`${date.getMinutes()}:${date.getSeconds()}`;
return timestamp;
}
}
module.exports = Logger;

View File

@@ -0,0 +1,52 @@
const isOnline = require('is-online');
class ReconnectUtil {
constructor(serverManagerView) {
this.serverManagerView = serverManagerView;
this.alreadyReloaded = false;
}
clearState() {
this.alreadyReloaded = false;
}
pollInternetAndReload() {
const pollInterval = setInterval(() => {
this._checkAndReload()
.then(status => {
if (status) {
this.alreadyReloaded = true;
clearInterval(pollInterval);
}
});
}, 1500);
}
_checkAndReload() {
return new Promise(resolve => {
if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition
isOnline()
.then(online => {
if (online) {
if (!this.alreadyReloaded) {
this.serverManagerView.reloadView();
}
console.log('You\'re back online.');
return resolve(true);
}
console.log('There is no internet connection, try checking network cables, modem and router.');
const errMsgHolder = document.querySelector('#description');
errMsgHolder.innerHTML = `
<div>You internet connection does't seem to work properly!</div>
</div>Verify that it works and then click try again.</div>`;
return resolve(false);
});
} else {
return resolve(true);
}
});
}
}
module.exports = ReconnectUtil;

View File

@@ -13,13 +13,14 @@
<div class="popup">
<span class="popuptext hidden" id="fullscreen-popup"></span>
</div>
<div id="sidebar">
<div id="sidebar" class="toggle-sidebar">
<div id="view-controls-container">
<div id="tabs-container"></div>
<div id="add-tab" class="tab functional-tab">
<div class="server-tab" id="add-action">
<i class="material-icons">add</i>
</div>
<span id="add-server-tooltip" style="display:none">Add organization</span>
</div>
</div>
<div id="actions-container">
@@ -27,6 +28,10 @@
<i class="material-icons md-48">refresh</i>
<span id="reload-tooltip" style="display:none">Reload</span>
</div>
<div class="action-button disable" id="back-action">
<i class="material-icons md-48">arrow_back</i>
<span id="back-tooltip" style="display:none">Go Back</span>
</div>
<div class="action-button" id="settings-action">
<i class="material-icons md-48">settings</i>
<span id="setting-tooltip" style="display:none">Settings</span>
@@ -39,5 +44,4 @@
</div>
</body>
<script src="js/main.js"></script>
</html>

View File

@@ -12,5 +12,5 @@
<div id="settings-container"></div>
</div>
</body>
<script src="js/pages/preference/preference.js"></script>
<script src="js/pages/preference/preference.js"></script>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

4
app/translations/de.json Normal file
View File

@@ -0,0 +1,4 @@
{
"Edit": "Redigera",
"Undo": "Ångra"
}

4
app/translations/en.json Normal file
View File

@@ -0,0 +1,4 @@
{
"Edit": "Edit",
"Undo": "Undo"
}

34
app/translations/i18n.js Normal file
View File

@@ -0,0 +1,34 @@
const path = require('path');
const electron = require('electron');
const fs = require('fs');
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
class i18n {
constructor() {
this.app = electron.app ? electron.app : electron.remote.app;
this.appLanguage = ConfigUtil.getConfigItem('spellcheckerLanguage');
this.appLocale = path.join(__dirname, this.appLanguage + '.json');
if (fs.existsSync(this.appLocale)) {
this.loadedLanguage = JSON.parse(fs.readFileSync(this.appLocale, 'utf8'));
console.log('translate into', this.appLanguage);
console.log(this.loadedLanguage);
} else {
this.loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, 'en.json'), 'utf8'));
console.log('no lang found');
console.log(this.loadedLanguage);
}
}
_(phrase) {
let translation = this.loadedLanguage[phrase];
if (translation === undefined) {
translation = phrase;
}
return translation;
}
}
// eslint-disable-next-line new-cap
module.exports = new i18n();

View File

@@ -17,7 +17,8 @@ install:
- npm install
- npm install -g gulp
build: off
build: off
test_script:
- npm run test
- npm run test-e2e

View File

@@ -1 +1,58 @@
** Windows Set up instructions **
## Prerequisites
* [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node.js](https://nodejs.org) >= v6.9.0
* [python](https://www.python.org/downloads/release/python-2713/) (v2.7.x recommended)
* [node-gyp](https://github.com/nodejs/node-gyp#installation) (installed via powershell)
## System specific dependencies
* use only 32bit or 64bit for all of the installers, do not mix architectures
* install using default settings
* open Windows Powershell as Admin
```powershell
C:\Windows\system32> npm install --global --production windows-build-tools
```
## Installation
Clone the source locally:
```sh
$ git clone https://github.com/zulip/zulip-electron
$ cd zulip-electron
```
Install project dependencies:
```sh
$ npm install
```
Start the app:
```sh
$ npm start
```
Start and watch changes
```sh
$ npm run dev
```
### Making a release
To package app into an installer use command:
```
npm run pack
npm run dist
```
It will start the packaging process. The ready for distribution file (e.g. dmg, windows installer, deb package) will be outputted to the `dist` directory.
# Troubleshooting
If you have any problems running the app please see the [most common issues](./troubleshooting.md).

View File

@@ -1,9 +1,10 @@
'use strict';
const gulp = require('gulp');
const mocha = require('gulp-mocha');
const electron = require('electron-connect').server.create({
verbose: true
});
const tape = require('gulp-tape');
const tapColorize = require('tap-colorize');
gulp.task('dev', () => {
// Start browser process
@@ -13,7 +14,7 @@ gulp.task('dev', () => {
// Reload renderer process
gulp.watch('app/renderer/css/*.css', ['reload:renderer']);
gulp.watch('app/renderer/*.html', ['reload:renderer']);
gulp.watch('app/renderer/js/*.js', ['reload:renderer']);
gulp.watch('app/renderer/js/**/*.js', ['reload:renderer']);
});
gulp.task('restart:browser', done => {
@@ -28,9 +29,11 @@ gulp.task('reload:renderer', done => {
done();
});
// Test app using mocha+spectron
gulp.task('test', () => {
return gulp.src('tests/index.js').pipe(mocha());
gulp.task('test-e2e', () => {
return gulp.src('tests/*.js')
.pipe(tape({
reporter: tapColorize()
}));
});
gulp.task('default', ['dev', 'test']);
gulp.task('default', ['dev', 'test-e2e']);

105
help.md Normal file
View File

@@ -0,0 +1,105 @@
# User Guide
> Welcome! This guide will walk you through the basics of using Zulip Desktop.
## Get Zulip Desktop
## Connect to a Server
### Connect through a proxy
It's possible to connect to your server through a proxy.
You can enter the proxy settings in the `Network` section of App Settings.
There are three fields provided:
* `PAC script` - The URL associated with the PAC file.
* `Proxy rules` - Rules indicating which proxies to use.
* `Proxy bypass rules` - Rules indicating which URLs should
bypass the proxy settings.
For a typical setup where internet access is required to use an HTTP proxy,
but URLs on the local network should be accessed directly, configure as follows:
`Proxy rules = proxy.example.com`
Your HTTP proxy server
`Proxy bypass rules = *.example.com;10.0.0.0/8`
Directly connect to your own domain and private IP subnet
for more complex setups, read below to configure complex proxy rules and proxy bypass rules.
### Sets the proxy settings.
When `PAC script` and `Proxy rules` are provided together, the `Proxy rules`
option is ignored and `PAC script` configuration is applied.
The `Proxy rules` has to follow the rules below:
```
proxyRules = schemeProxies[";"<schemeProxies>]
schemeProxies = [<urlScheme>"="]<proxyURIList>
urlScheme = "http" | "https" | "ftp" | "socks"
proxyURIList = <proxyURL>[","<proxyURIList>]
proxyURL = [<proxyScheme>"://"]<proxyHost>[":"<proxyPort>]
```
For example:
* `http=foopy:80;ftp=foopy2` - Use HTTP proxy `foopy:80` for `http://` URLs, and
HTTP proxy `foopy2:80` for `ftp://` URLs.
* `foopy:80` - Use HTTP proxy `foopy:80` for all URLs.
* `foopy:80,bar,direct://` - Use HTTP proxy `foopy:80` for all URLs, failing
over to `bar` if `foopy:80` is unavailable, and after that using no proxy.
* `socks4://foopy` - Use SOCKS v4 proxy `foopy:1080` for all URLs.
* `http=foopy,socks5://bar.com` - Use HTTP proxy `foopy` for http URLs, and fail
over to the SOCKS5 proxy `bar.com` if `foopy` is unavailable.
* `http=foopy,direct://` - Use HTTP proxy `foopy` for http URLs, and use no
proxy if `foopy` is unavailable.
* `http=foopy;socks=foopy2` - Use HTTP proxy `foopy` for http URLs, and use
`socks4://foopy2` for all other URLs.
The `Proxy bypass rules` is a comma separated list of rules described below:
* `[ URL_SCHEME "://" ] HOSTNAME_PATTERN [ ":" <port> ]`
Match all hostnames that match the pattern HOSTNAME_PATTERN.
Examples:
"foobar.com", "*foobar.com", "*.foobar.com", "*foobar.com:99",
"https://x.*.y.com:99"
* `"." HOSTNAME_SUFFIX_PATTERN [ ":" PORT ]`
Match a particular domain suffix.
Examples:
".google.com", ".com", "http://.google.com"
* `[ SCHEME "://" ] IP_LITERAL [ ":" PORT ]`
Match URLs which are IP address literals.
Examples:
"127.0.1", "[0:0::1]", "[::1]", "http://[::1]:99"
* `IP_LITERAL "/" PREFIX_LENGHT_IN_BITS`
Match any URL that is to an IP literal that falls between the
given range. IP range is specified using CIDR notation.
Examples:
"192.168.1.1/16", "fefe:13::abc/33".
* `<local>`
Match local addresses. The meaning of `<local>` is whether the
host matches one of: "127.0.0.1", "::1", "localhost".
## Change App Preferences
## Reporting an Issue

68
how-to-install.md Normal file
View File

@@ -0,0 +1,68 @@
# How to install
**Note:** If you download from the [releases page](https://github.com/zulip/zulip-electron/releases), be careful what version you pick. Releases that end with `-beta` are beta releases and the rest are stable.
- **beta:** these releases are the right balance between getting new features early while staying away from nasty bugs.
- **stable:** these releases are more thoroughly tested; they receive new features later, but there's a lower chance that things will go wrong.
[LR]: https://github.com/zulip/zulip-electron/releases
## OS X
**DMG or zip**:
1. Download [Zulip-x.x.x.dmg][LR] or [Zulip-x.x.x-mac.zip][LR]
2. Open or unzip the file and drag the app into the `Applications` folder
3. Done! The app will update automatically
**Using brew**:
1. Run `brew cask install zulip` in your terminal
2. The app will be installed in your `Applications`
3. Done! The app will update automatically (you can also use `brew update && brew upgrade zulip`)
## Windows
**Installer (recommended)**:
1. Download [Zulip-Web-Setup-x.x.x.exe][LR]
2. Run the installer, wait until it finishes
3. Done! The app will update automatically
**Portable**:
1. Download [zulip-x.x.x-arch.nsis.7z][LR] [*here arch = ia32 (32-bit), x64 (64-bit)*]
2. Extract the zip wherever you want (e.g. a flash drive) and run the app from there
## Linux
**Ubuntu, Debian 8+ (deb package)**:
1. Download [Zulip-x.x.x-amd64.deb][LR]
2. Double click and install, or run `dpkg -i Zulip-x.x.x-amd64.deb` in the terminal
3. Start the app with your app launcher or by running `zulip` in a terminal
4. Done! The app will NOT update automatically, but you can still check for updates
**Other distros (Fedora, CentOS, Arch Linux etc)** :
1. Download Zulip-x.x.x-x86_64.AppImage[LR]
2. Make it executable using chmod a+x Zulip-x.x.x-x86_64.AppImage
3. Start the app with your app launcher
**You can also use `apt-get` (recommended)**:
* First download our signing key to make sure the deb you download is correct:
```
sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9
```
* Add the repo to your apt source list :
```
echo "deb https://dl.bintray.com/zulip/debian/ beta main" |
sudo tee -a /etc/apt/sources.list.d/zulip.list
```
* Now install the client :
```
sudo apt-get update
sudo apt-get install zulip
```

9854
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,14 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.3.0-beta",
"version": "1.8.2",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"email": "<svnitakash@gmail.com>",
"copyright": "©2017 Kandra Labs, Inc.",
"copyright": "Kandra Labs, Inc.",
"author": {
"name": "Kandra Labs, Inc.",
"email": "svnitakash@gmail.com"
"email": "support@zulipchat.com"
},
"repository": {
"type": "git",
@@ -18,17 +17,24 @@
"bugs": {
"url": "https://github.com/zulip/zulip-electron/issues"
},
"engines": {
"node": ">=8.0.0"
},
"scripts": {
"start": "electron app --disable-http-cache",
"start": "electron app --disable-http-cache --no-electron-connect",
"reinstall": "./tools/reinstall-node-modules",
"postinstall": "electron-builder install-app-deps",
"test": "xo",
"dev": "gulp dev",
"test-e2e": "gulp test-e2e",
"dev": "gulp dev & nodemon --watch app/main --watch app/renderer --exec 'npm test' -e html,css,js",
"pack": "electron-builder --dir",
"dist": "electron-builder",
"mas": "electron-builder --mac mas",
"build:win": "electron-builder --win nsis-web --ia32 --x64",
"travis": "cd ./scripts && ./travis-build-test.sh"
},
"pre-commit": [
"test"
],
"build": {
"appId": "org.zulip.zulip-electron",
"asar": true,
@@ -42,7 +48,7 @@
"category": "public.app-category.productivity"
},
"linux": {
"category": "",
"category": "Chat;GNOME;GTK;Network;InstantMessaging",
"packageCategory": "GNOME;GTK;Network;InstantMessaging",
"description": "Zulip Desktop Client for Linux",
"target": [
@@ -53,7 +59,9 @@
"maintainer": "Akash Nimare <svnitakash@gmail.com>"
},
"deb": {
"synopsis": "Zulip Desktop App"
"synopsis": "Zulip Desktop App",
"afterInstall": "./scripts/debian-add-repo.sh",
"afterRemove": "./scripts/debian-uninstaller.sh"
},
"dmg": {
"background": "build/appdmg.png",
@@ -74,7 +82,15 @@
]
},
"win": {
"target": "nsis",
"target": [
{
"target": "nsis-web",
"arch": [
"x64",
"ia32"
]
}
],
"icon": "build/icon.ico",
"publisherName": "Kandra Labs, Inc."
},
@@ -94,15 +110,20 @@
],
"devDependencies": {
"assert": "1.4.1",
"cp-file": "^5.0.0",
"devtron": "1.4.0",
"electron-builder": "19.19.1",
"electron": "1.6.11",
"electron": "1.8.2",
"electron-builder": "19.53.6",
"electron-connect": "0.6.2",
"electron-debug": "1.4.0",
"gulp": "3.9.1",
"gulp-mocha": "4.3.1",
"chai-as-promised": "7.1.1",
"chai": "4.1.1",
"gulp-tape": "0.0.9",
"is-ci": "^1.0.10",
"nodemon": "^1.14.11",
"pre-commit": "1.2.2",
"spectron": "3.7.2",
"tap-colorize": "^1.2.0",
"tape": "^4.8.0",
"xo": "0.18.2"
},
"xo": {
@@ -119,9 +140,14 @@
"rules": {
"max-lines": [
"warn",
500
{
"max": 500,
"skipBlankLines": true,
"skipComments": true
}
],
"no-warning-comments": 0,
"object-curly-spacing": 0,
"capitalized-comments": 0,
"no-else-return": 0,
"no-path-concat": 0,
@@ -143,4 +169,4 @@
"mocha"
]
}
}
}

View File

@@ -0,0 +1,10 @@
#!/bin/bash
# This script runs when user install the debian package
# Install apt repository source list if it does not exist
if ! grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep zulip.list; then
sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9
echo "deb https://dl.bintray.com/zulip/debian/ stable main" | \
sudo tee -a /etc/apt/sources.list.d/zulip.list;
fi

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# This script runs when user uninstall the debian package.
# It will remove all the config files and anything which was added by the app.
# Remove apt repository source list when user uninstalls Zulip app
if grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep zulip.list; then
sudo apt-key del 69AD12704E71A4803DCA3A682424BE5AE9BD10D9;
sudo rm /etc/apt/sources.list.d/zulip.list;
fi
# Get the root user
if [ $SUDO_USER ];
then getSudoUser=$SUDO_USER;
else getSudoUser=`whoami`;
fi
# Get the path for Zulip's desktop entry which is created by auto-launch script
getDesktopEntry=/home/$getSudoUser/.config/autostart/zulip.desktop;
# Remove desktop entry if exists
if [ -f $getDesktopEntry ]; then
sudo rm $getDesktopEntry;
fi
# App directory which contains all the config, setting files
appDirectory=/home/$getSudoUser/.config/Zulip/;
if [ -d $appDirectory ]; then
sudo rm -rf $appDirectory;
fi

View File

@@ -1,10 +1,20 @@
#!/usr/bin/env bash
# exit script if fails
set -e;
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
export {no_proxy,NO_PROXY}="127.0.0.1,localhost"
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
sleep 3
export {no_proxy,NO_PROXY}="127.0.0.1,localhost"
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
sleep 3
echo 'Travis Screen Resolution:'
xdpyinfo | grep dimensions
fi
npm run test
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
npm run test-e2e
fi

5
scripts/travis-xvfb.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16
fi

7
tests/config.js Normal file
View File

@@ -0,0 +1,7 @@
const path = require('path')
const TEST_APP_PRODUCT_NAME = 'ZulipTest'
module.exports = {
TEST_APP_PRODUCT_NAME
}

View File

@@ -1,81 +1,13 @@
const assert = require('assert')
const Application = require('spectron').Application
const chai = require('chai')
const { expect } = chai
const chaiAsPromised = require('chai-as-promised')
chai.should()
chai.use(chaiAsPromised)
describe('application launch', function () {
this.timeout(15000)
beforeEach(function () {
this.app = new Application({
path: require('electron'),
args: [__dirname + '/../app/renderer/main.html']
})
return this.app.start()
})
beforeEach(function () {
chaiAsPromised.transferPromiseness = this.app.transferPromiseness
})
afterEach(function () {
if (this.app && this.app.isRunning()) {
return this.app.stop()
}
})
it('shows an initial window', function () {
return this.app.client.waitUntilWindowLoaded(5000)
.getWindowCount().should.eventually.equal(2)
.browserWindow.isMinimized().should.eventually.be.false
.browserWindow.isDevToolsOpened().should.eventually.be.false
.browserWindow.isVisible().should.eventually.be.true
.browserWindow.isFocused().should.eventually.be.true
.browserWindow.getBounds().should.eventually.have.property('width').and.be.above(0)
.browserWindow.getBounds().should.eventually.have.property('height').and.be.above(0)
})
it('sets up a default organization', function () {
let app = this.app
let self = this
app.client.execute(() => {
window.confirm = function () { return true }
})
function createOrg (client, name, url, winIndex) {
return client
// Focus on settings webview
.then(switchToWebviewAtIndex.bind(null, self.app.client, winIndex))
.pause(1000) // wait for settings to load
// Fill settings form
.click('#new-server-action')
.setValue('input[id="server-info-name"]', name)
.setValue('input[id="server-info-url"]', url)
.click('#save-server-action')
.pause(500) // Need to pause while server verification takes place
.then(() => app.browserWindow.reload())
.pause(1500) // Wait for webview of org to load
}
function switchToWebviewAtIndex(client, index) {
return client
.windowHandles()
.then(function (session) {
this.window(session.value[index])
})
}
return this.app.client.waitUntilWindowLoaded(5000)
.then(() => createOrg(self.app.client, 'Zulip 1', 'chat.zulip.org', 1))
.then(switchToWebviewAtIndex.bind(null, self.app.client, 0))
.click('#add-action > i').pause(500)
.then(switchToWebviewAtIndex.bind(null, self.app.client, 2))
.then(() => createOrg(self.app.client, 'Zulip 2', 'chat.zulip.org', 2))
})
})
const test = require('tape')
const setup = require('./setup')
test('app runs', function (t) {
t.timeoutAfter(10e3)
setup.resetTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => app.client.windowByIndex(1)) // focus on webview
.then(() => app.client.waitForExist('//*[@id="new-server-container"]/div/div/div[2]/input'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})

99
tests/setup.js Normal file
View File

@@ -0,0 +1,99 @@
const Application = require('spectron').Application
const cpFile = require('cp-file')
const fs = require('fs')
const isCI = require('is-ci')
const mkdirp = require('mkdirp')
const path = require('path')
const rimraf = require('rimraf')
const config = require('./config')
module.exports = {
createApp,
endTest,
waitForLoad,
wait,
resetTestDataDir
}
// Runs Zulip Desktop.
// Returns a promise that resolves to a Spectron Application once the app has loaded.
// Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
function createApp (t) {
generateTestAppPackageJson()
return new Application({
path: path.join(__dirname, '..', 'node_modules', '.bin',
'electron' + (process.platform === 'win32' ? '.cmd' : '')),
args: [path.join(__dirname)], // Ensure this dir has a package.json file with a 'main' entry piont
env: {NODE_ENV: 'test'},
waitTimeout: 10e3
})
}
// Generates package.json for test app
// Reads app package.json and updates the productName to config.TEST_APP_PRODUCT_NAME
// We do this so that the app integration doesn't doesn't share the same appDataDir as the dev application
function generateTestAppPackageJson () {
let packageJson = require(path.join(__dirname, '../package.json'))
packageJson.productName = config.TEST_APP_PRODUCT_NAME
packageJson.main = '../app/main'
const testPackageJsonPath = path.join(__dirname, 'package.json')
fs.writeFileSync(testPackageJsonPath, JSON.stringify(packageJson, null, ' '), 'utf-8')
}
// Starts the app, waits for it to load, returns a promise
function waitForLoad (app, t, opts) {
if (!opts) opts = {}
return app.start().then(function () {
return app.client.waitUntilWindowLoaded()
})
.then(function() {
return app.client.pause(2000);
})
.then(function () {
return app.webContents.getTitle()
}).then(function (title) {
t.equal(title, 'Zulip', 'html title')
})
}
// Returns a promise that resolves after 'ms' milliseconds. Default: 1 second
function wait (ms) {
if (ms === undefined) ms = 1000 // Default: wait long enough for the UI to update
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms)
})
}
// Quit the app, end the test, either in success (!err) or failure (err)
function endTest (app, t, err) {
return app.stop().then(function () {
t.end(err)
})
}
function getAppDataDir () {
let base
if (process.platform === 'darwin') {
base = path.join(process.env.HOME, 'Library', 'Application Support')
} else if (process.platform === 'linux') {
base = process.env.XDG_CONFIG_HOME ?
process.env.XDG_CONFIG_HOME : path.join(process.env.HOME, '.config')
} else if (process.platform === 'win32') {
base = process.env.APPDATA
} else {
console.log('Could not detect app data dir base. Exiting...')
process.exit(1)
}
console.log('Detected App Data Dir base:', base)
return path.join(base, config.TEST_APP_PRODUCT_NAME)
}
// Resets the test directory, containing domain.json, window-state.json, etc
function resetTestDataDir () {
appDataDir = getAppDataDir()
rimraf.sync(appDataDir)
rimraf.sync(path.join(__dirname, 'package.json'))
}

View File

@@ -0,0 +1,19 @@
const test = require('tape')
const setup = require('./setup')
test('add-organization', function (t) {
t.timeoutAfter(50e3)
setup.resetTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => app.client.windowByIndex(1)) // focus on webview
.then(() => app.client.setValue('.setting-input-value', 'chat.zulip.org'))
.then(() => app.client.click('.server-save-action'))
.then(() => setup.wait(5000))
.then(() => app.client.windowByIndex(0)) // Switch focus back to main win
.then(() => app.client.windowByIndex(1)) // Switch focus back to org webview
.then(() => app.client.waitForExist('//*[@id="id_username"]'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})

16
tools/fetch-pull-request Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
set -e
set -x
if ! git diff-index --quiet HEAD; then
set +x
echo "There are uncommitted changes:"
git status --short
echo "Doing nothing to avoid losing your work."
exit 1
fi
request_id="$1"
remote=${2:-"upstream"}
git fetch "$remote" "pull/$request_id/head"
git checkout -B "review-original-${request_id}"
git reset --hard FETCH_HEAD

View File

@@ -0,0 +1,23 @@
@echo off
git diff-index --quiet HEAD
if %ERRORLEVEL% NEQ 0 (
echo "There are uncommitted changes:"
git status --short
echo "Doing nothing to avoid losing your work."
exit /B 1
)
if "%~1"=="" (
echo "Error you must specify the PR number"
)
if "%~2"=="" (
set remote="upstream"
) else (
set remote=%2
)
set request_id="%1"
git fetch "%remote%" "pull/%request_id%/head"
git checkout -B "review-%request_id%"
git reset --hard FETCH_HEAD

17
tools/fetch-rebase-pull-request Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
set -e
set -x
if ! git diff-index --quiet HEAD; then
set +x
echo "There are uncommitted changes:"
git status --short
echo "Doing nothing to avoid losing your work."
exit 1
fi
request_id="$1"
remote=${2:-"upstream"}
git fetch "$remote" "pull/$request_id/head"
git checkout -B "review-${request_id}" $remote/master
git reset --hard FETCH_HEAD
git pull --rebase

View File

@@ -0,0 +1,24 @@
@echo off
git diff-index --quiet HEAD
if %errorlevel% neq 0 (
echo "There are uncommitted changes:"
git status --short
echo "Doing nothing to avoid losing your work."
exit \B 1
)
if "%~1"=="" (
echo "Error you must specify the PR number"
)
if "%~2"=="" (
set remote="upstream"
) else (
set remote=%2
)
set request_id="%1"
git fetch "%remote%" "pull/%request_id%/head"
git checkout -B "review-%request_id%" %remote%/master
git reset --hard FETCH_HEAD
git pull --rebase

View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -e
set -x
echo "Removing node_modules and app/node_modules"
rm -rf node_modules
rm -rf app/node_modules
echo "node_modules removed reinstalling npm packages"
npm i

View File

@@ -0,0 +1,8 @@
@echo off
echo "Removing node_modules and app/node_modules"
rmdir /s /q node_modules
rmdir /s /q app/node_modules
echo "node_modules removed reinstalling npm packages"
npm i

View File

@@ -5,7 +5,11 @@
* Electron is more or less Chrome, you can get developer tools using `CMD+ALT+I`
### Error : ChecksumMismatchError
- Try deleteing `node_modules` && `app/node_modules` directories. Re-install dependencies using `npm install`.
- Try deleteing `node_modules` && `app/node_modules` directories. Re-install dependencies using `npm install`
### Error : Module version mismatch. Expected 50, got 51
- Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly.
- Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly
### Error: Desktop Notifications not working
- Make sure the **Show Desktop Notifications** setting option is set to be true
- Check your OS notifications center settings