Compare commits

..

44 Commits

Author SHA1 Message Date
akashnimare
9e3cfbd887 echo commit range on travis. 2018-01-30 22:46:52 +05:30
cPhost
720f42ca80 ci: Add commit linter and clean up code. 2018-01-30 11:07:41 -05:00
cPhost
e18ba5dab6 gitlint: Setup commit linter.
This will setup a commit hook that will check the commit msg run
`npm run lint-commits` to check all the commits. Additionaly setup
git hooks by running `npm run setup-gitlint-hooks`.
2018-01-30 08:58:19 -05:00
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
49 changed files with 991 additions and 237 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

3
.gitignore vendored
View File

@@ -26,7 +26,6 @@ yarn-error.log*
config.gypi
# Test generated files
tests/e2e/package.json
coverage
tests/package.json
.python-version

View File

@@ -28,7 +28,13 @@ cache:
- app/node_modules
script:
- npm run travis
- echo $TRAVIS_COMMIT_RANGE
- echo ${TRAVIS_COMMIT_RANGE/.../..}
- echo "test"
- git log -4
- node ./tools/gitlint --ci-mode
- npm run travis
notifications:
webhooks:
urls:

View File

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

View File

@@ -12,19 +12,12 @@ Please see [installation guide](https://zulipchat.com/help/desktop-app-install-g
# 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,5 +1,4 @@
'use strict';
const fs = require('fs');
const { app, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
const isDev = require('electron-is-dev');
@@ -15,10 +14,6 @@ function appUpdater() {
// Create Logs directory
const LogsDir = `${app.getPath('userData')}/Logs`;
if (!fs.existsSync(LogsDir)) {
fs.mkdirSync(LogsDir);
}
// Log whats happening
const log = require('electron-log');

View File

@@ -12,6 +12,7 @@ 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
// in development mode
@@ -81,7 +82,11 @@ function createMainWindow() {
});
win.once('ready-to-show', () => {
win.show();
if (ConfigUtil.getConfigItem('startMinimized')) {
win.minimize();
} else {
win.show();
}
});
win.loadURL(mainURL);
@@ -124,6 +129,9 @@ function createMainWindow() {
return win;
}
// Decrease load on GPU (experimental)
app.disableHardwareAcceleration();
// eslint-disable-next-line max-params
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
event.preventDefault();
@@ -145,7 +153,11 @@ app.on('ready', () => {
const page = mainWindow.webContents;
page.on('dom-ready', () => {
mainWindow.show();
if (ConfigUtil.getConfigItem('startMinimized')) {
mainWindow.minimize();
} else {
mainWindow.show();
}
});
page.once('did-frame-finish-load', () => {
@@ -155,7 +167,8 @@ app.on('ready', () => {
});
electron.powerMonitor.on('resume', () => {
page.send('reload-viewer');
mainWindow.reload();
page.send('destroytray');
});
ipcMain.on('focus-app', () => {

View File

@@ -54,7 +54,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');
@@ -121,7 +121,7 @@ class AppMenu {
shell.openExternal('https://zulipchat.com/help/');
}
}, {
label: `${appName + 'Desktop'} - ${app.getVersion()}`,
label: `${appName + ' Desktop'} - ${app.getVersion()}`,
enabled: false
}, {
label: 'Report an Issue...',

31
app/package-lock.json generated
View File

@@ -101,6 +101,12 @@
"tweetnacl": "0.14.5"
}
},
"bindings": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz",
"integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==",
"optional": true
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
@@ -318,6 +324,12 @@
"resolved": "https://registry.npmjs.org/event-kit/-/event-kit-2.4.0.tgz",
"integrity": "sha512-ZXd9jxUoc/f/zdLdR3OUcCzT84WnpaNWefquLyE125akIC90sDs8S3T/qihliuVsaj7Osc0z8lLL2fjooE9Z4A=="
},
"event-target-shim": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz",
"integrity": "sha1-qG5e5r2qFgVEddp5fM3fDFVphJE=",
"optional": true
},
"extend": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@@ -622,6 +634,25 @@
"mkdirp": "0.5.1"
}
},
"node-mac-notifier": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/node-mac-notifier/-/node-mac-notifier-0.0.13.tgz",
"integrity": "sha1-1kt27RgfR5XURFui060Nb3KY9+I=",
"optional": true,
"requires": {
"bindings": "1.3.0",
"event-target-shim": "1.1.1",
"node-uuid": "1.4.8"
},
"dependencies": {
"node-uuid": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
"integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=",
"optional": true
}
}
},
"oauth-sign": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.7.0",
"version": "1.8.1",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.",
@@ -30,10 +30,13 @@
"electron-is-dev": "0.3.0",
"electron-log": "2.2.7",
"electron-spellchecker": "1.1.2",
"electron-updater": "2.18.2",
"electron-window-state": "4.1.1",
"electron-updater": "2.16.2",
"node-json-db": "0.7.3",
"request": "2.81.0",
"wurl": "2.5.0"
},
"optionalDependencies": {
"node-mac-notifier": "0.0.13"
}
}

View File

@@ -263,7 +263,13 @@ img.server-info-icon {
margin: 10px 0 20px 0;
background: #fff;
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 {

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>

View File

@@ -7,7 +7,7 @@ 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">

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, app } = require('electron').remote;
const BaseComponent = require(__dirname + '/../components/base.js');
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
class WebView extends BaseComponent {
constructor(props) {
super();
@@ -24,6 +26,7 @@ class WebView extends BaseComponent {
template() {
return `<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity
@@ -54,6 +57,12 @@ 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;
this.badgeCount = this.getBadgeCount(title);

View File

@@ -1,69 +0,0 @@
const NodeConsole = require('console').Console;
const fs = require('fs');
const { app } = require('electron').remote;
const isDev = require('electron-is-dev');
const browserConsole = console;
const logDir = `${app.getPath('userData')}/Logs`;
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
function customConsole(opts, type, ...args) {
const { nodeConsole, timestamp } = opts;
if (timestamp) {
args.unshift(timestamp());
}
if (!isDev) {
const nodeConsoleLog = nodeConsole[type] || nodeConsole.log;
nodeConsoleLog.apply(null, args);
}
browserConsole[type].apply(null, args);
}
function getTimestamp() {
const date = new Date();
const timestamp =
`${date.getMonth()}/${date.getDate()} ` +
`${date.getMinutes()}:${date.getSeconds()}`;
return timestamp;
}
function setConsoleProto(type) {
Object.defineProperty(this, type, {
value(...args) {
const { timestamp, nodeConsole } = this;
const opts = {
timestamp,
nodeConsole
};
customConsole.apply(null, [].concat(opts, type, args));
}
});
}
class Console {
constructor(opts = {}) {
let { timestamp, file } = opts;
file = `${logDir}/${file || 'console.log'}`;
if (timestamp === true) {
timestamp = getTimestamp;
}
const fileStream = fs.createWriteStream(file);
const nodeConsole = new NodeConsole(fileStream);
this.nodeConsole = nodeConsole;
this.timestamp = timestamp;
this.setUpConsole();
}
setUpConsole() {
for (const type in browserConsole) {
setConsoleProto.call(this, type);
}
}
}
module.exports = Console;

View File

@@ -1,10 +1,10 @@
'use strict';
require(__dirname + '/js/tray.js');
const { ipcRenderer, remote } = require('electron');
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');
@@ -35,6 +35,7 @@ class ServerManagerView {
this.activeTabIndex = -1;
this.tabs = [];
this.functionalTabs = {};
this.tabIndex = 0;
}
init() {
@@ -77,6 +78,7 @@ class ServerManagerView {
showSidebar: true,
badgeOption: true,
startAtLogin: false,
startMinimized: false,
enableSpellchecker: true,
showNotification: true,
betaUpdate: false,
@@ -84,6 +86,13 @@ class ServerManagerView {
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]);
@@ -112,17 +121,20 @@ class ServerManagerView {
}
initServer(server, index) {
const tabIndex = this.getTabIndex();
this.tabs.push(new ServerTab({
role: 'server',
icon: server.icon,
$root: this.$tabsContainer,
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: () => {
@@ -147,11 +159,24 @@ class ServerManagerView {
this.openSettings('General');
});
const $serverImgs = document.querySelectorAll('.server-icons');
$serverImgs.forEach($serverImg => {
$serverImg.addEventListener('error', () => {
$serverImg.src = 'img/icon.png';
});
});
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip);
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
}
getTabIndex() {
const currentIndex = this.tabIndex;
this.tabIndex++;
return currentIndex;
}
sidebarHoverEvent(SidebarButton, SidebarTooltip) {
SidebarButton.addEventListener('mouseover', () => {
SidebarTooltip.removeAttribute('style');
@@ -178,16 +203,19 @@ class ServerManagerView {
this.functionalTabs[tabProps.name] = this.tabs.length;
const tabIndex = this.getTabIndex();
this.tabs.push(new FunctionalTab({
role: 'function',
materialIcon: tabProps.materialIcon,
$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: () => {
@@ -255,6 +283,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) {
@@ -387,6 +436,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) {

View File

@@ -1,30 +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 baseNotification extends NativeNotification {
constructor(title, opts) {
opts.silent = ConfigUtil.getConfigItem('silent') || false;
super(title, opts);
}
static requestPermission() {
return; // eslint-disable-line no-useless-return
}
// Override default Notification permission
static get permission() {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
}
}
window.Notification = baseNotification;

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,102 @@
const { remote } = require('electron');
// Do not change this
const appId = 'org.zulip.zulip-electron';
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);
});
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
};

View File

@@ -0,0 +1,18 @@
'use strict';
const {
remote: { app }
} = require('electron');
const DefaultNotification = require('./default-notification');
const { appId } = 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;
}

View File

@@ -1,11 +1,10 @@
'use strict';
const path = require('path');
const { ipcRenderer } = require('electron');
const { app, dialog } = require('electron').remote;
const { ipcRenderer, remote } = require('electron');
const fs = require('fs-extra');
const { app, dialog } = remote;
const currentBrowserWindow = remote.getCurrentWindow();
const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
@@ -61,10 +60,14 @@ class GeneralSection extends BaseSection {
<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 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">
@@ -89,6 +92,7 @@ class GeneralSection extends BaseSection {
this.updateResetDataOption();
this.showDesktopNotification();
this.enableSpellchecker();
this.minimizeOnStart();
// Platform specific settings
// Flashing taskbar on Windows
@@ -155,6 +159,7 @@ class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption();
currentBrowserWindow.send('toogle-silent', newValue);
}
});
}
@@ -234,6 +239,18 @@ class GeneralSection extends BaseSection {
});
}
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

@@ -13,9 +13,9 @@ class NewServerForm extends BaseComponent {
return `
<div class="settings-card">
<div class="server-info-right">
<div class="title">URL of your Zulip organization</div>
<div class="title">URL of Zulip organization</div>
<div class="server-info-row">
<input class="setting-input-value" autofocus placeholder="acme.zulipchat.com"/>
<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 blue server-save-action">
@@ -43,11 +43,13 @@ class NewServerForm extends BaseComponent {
}
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);
});
}

View File

@@ -69,10 +69,27 @@ class PreferenceView extends BaseComponent {
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

@@ -197,13 +197,16 @@ 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' || process.platform === 'win32') {
renderNativeImage(unread).then(image => {
@@ -213,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 {
@@ -47,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

@@ -5,6 +5,12 @@ 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;
@@ -93,8 +99,7 @@ class DomainUtil {
checkDomain(domain, silent = false) {
if (!silent && this.duplicateDomain(domain)) {
// Do not check duplicate in silent mode
alert('This server has been added.');
return;
return Promise.reject('This server has been added.');
}
domain = this.formatUrl(domain);
@@ -115,11 +120,16 @@ class DomainUtil {
'Error: unable to verify the first certificate',
'Error: unable to get local issuer certificate'
];
// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];
if (!error && response.statusCode !== 404) {
// 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);
@@ -159,7 +169,7 @@ class DomainUtil {
}
} else {
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(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);
}
});
@@ -225,7 +235,23 @@ class DomainUtil {
}
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) {

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

@@ -41,4 +41,13 @@
</body>
<script src="js/main.js"></script>
<!-- To trigger electron.reload on changes in renderer from gulp -->
<script>
window.addEventListener('load', () => {
const isDev = require('electron-is-dev');
if (isDev) {
require('electron-connect').client.create();
}
});
</script>
</html>

View File

@@ -20,4 +20,6 @@ install:
build: off
test_script:
- npm run test-all
- node ./tools/gitlint --ci-mode
- npm run test
- npm run test-e2e

View File

@@ -14,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 => {
@@ -30,7 +30,7 @@ gulp.task('reload:renderer', done => {
});
gulp.task('test-e2e', () => {
return gulp.src('tests/e2e/*.js')
return gulp.src('tests/*.js')
.pipe(tape({
reporter: tapColorize()
}));

View File

@@ -1,21 +0,0 @@
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
browsers: ['Electron'],
preprocessors: {
'**/*.js': ['electron']
},
files: [
{pattern: './karma.shim.js', watched: true, included: true, served: true},
{pattern: './tests/unit/*.js', watched: true, included: true, served: true},
{pattern: './app/renderer/**/*.js', watched: true, included: false, served: true}
],
reporters: ['mocha'],
client: {
captureConsole: true,
useIframe: false
},
singleRun: true
});
};

View File

@@ -1,5 +0,0 @@
window.require = window.parent.require;
window.process = window.parent.process;
window.__dirname = window.parent.__dirname;
require('module').globalPaths.push('./node_modules');
require('module').globalPaths.push('./app/renderer');

39
package-lock.json generated
View File

@@ -1561,9 +1561,9 @@
"dev": true
},
"electron": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.6.15.tgz",
"integrity": "sha1-w07FRIa39Jpm21jG8koJKEFPrqc=",
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.9.tgz",
"integrity": "sha1-rdVOn4+D7QL2UZ7BATX2mLGTNs8=",
"dev": true,
"requires": {
"@types/node": "7.0.48",
@@ -4762,6 +4762,34 @@
"inherits": "2.0.1"
}
},
"nodemon": {
"version": "1.14.11",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.14.11.tgz",
"integrity": "sha512-323uPopdzYcyDR2Ze1UOLF9zocwoQEyGPiKaLm/Y8Mbfjylt/YueAJUVHqox+vgG8TqZqZApcHv5lmUvrn/KQw==",
"dev": true,
"requires": {
"chokidar": "2.0.0",
"debug": "3.1.0",
"ignore-by-default": "1.0.1",
"minimatch": "3.0.4",
"pstree.remy": "1.1.0",
"semver": "5.4.1",
"touch": "3.1.0",
"undefsafe": "2.0.1",
"update-notifier": "2.3.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
}
},
"node-abi": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.1.2.tgz",
@@ -5801,11 +5829,6 @@
"resolve-from": "1.0.1"
}
},
"resemblejs": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/resemblejs/-/resemblejs-2.5.0.tgz",
"integrity": "sha512-j2p4H8jpxch3bG9SOoap4tWbNZ4hNGrl54y7WJSwoKBQM3ZBhVyaFEYqzlv06dY1I4Pns/M8Ten6R6+/jTjycA=="
},
"resolve": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.7.0",
"version": "1.8.1",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
@@ -21,18 +21,18 @@
"start": "electron app --disable-http-cache",
"reinstall": "rm -rf node_modules; rm -rf app/node_modules; npm install",
"postinstall": "electron-builder install-app-deps",
"lint": "xo",
"test": "xo",
"test-e2e": "gulp test-e2e",
"test-unit": "karma start karma.conf.js",
"test-all": "xo && npm run test-unit && npm run test-e2e",
"dev": "gulp dev",
"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",
"setup-gitlint-hooks": "node ./tools/gitlint/setup-gitlint-hook",
"lint-commits": "node ./tools/gitlint --all-commits",
"travis": "cd ./scripts && ./travis-build-test.sh"
},
"pre-commit": [
"lint"
"test"
],
"build": {
"appId": "org.zulip.zulip-electron",
@@ -109,26 +109,21 @@
],
"devDependencies": {
"assert": "1.4.1",
"chalk": "^2.3.0",
"cp-file": "^5.0.0",
"devtron": "1.4.0",
"electron-builder": "19.46.4",
"electron": "1.7.9",
"electron": "1.7.10",
"electron-builder": "19.53.6",
"electron-connect": "0.6.2",
"electron-debug": "1.4.0",
"gulp": "3.9.1",
"gulp-tape": "0.0.9",
"is-ci": "^1.0.10",
"jasmine": "^2.8.0",
"karma": "^1.7.1",
"karma-electron": "^5.2.2",
"karma-electron-launcher": "^0.2.0",
"karma-jasmine": "^1.1.0",
"karma-mocha-reporter": "^2.2.5",
"nodemon": "^1.14.11",
"pre-commit": "1.2.2",
"spectron": "3.7.2",
"tap-colorize": "^1.2.0",
"tape": "^4.8.0",
"watchify": "^3.9.0",
"xo": "0.18.2"
},
"xo": {
@@ -159,11 +154,16 @@
"import/no-extraneous-dependencies": 0,
"no-prototype-builtins": 0
}
},
{
"files": "scripts/gitlint/*.js",
"rules": {
"unicorn/no-process-exit": 1
}
}
],
"ignore": [
"tests/e2e/*.js",
"tests/unit/*"
"tests/*.js"
],
"envs": [
"node",

View File

@@ -10,14 +10,8 @@ if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
xdpyinfo | grep dimensions
fi
# macOS
# Run all the tests
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
npm run test-all
fi
npm run test
# Linux
# Only run linting test on Linux
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
npm run lint
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
npm run test-e2e
fi

View File

@@ -22,7 +22,7 @@ module.exports = {
function createApp (t) {
generateTestAppPackageJson()
return new Application({
path: path.join(__dirname,'..', '..', 'node_modules', '.bin',
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'},
@@ -34,9 +34,9 @@ function createApp (t) {
// 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'))
let packageJson = require(path.join(__dirname, '../package.json'))
packageJson.productName = config.TEST_APP_PRODUCT_NAME
packageJson.main = '../../app/main'
packageJson.main = '../app/main'
const testPackageJsonPath = path.join(__dirname, 'package.json')
fs.writeFileSync(testPackageJsonPath, JSON.stringify(packageJson, null, ' '), 'utf-8')

View File

@@ -1,20 +0,0 @@
const ConfigUtil = require('js/utils/config-util.js');
const SetupSpellChecker = require('js/spellchecker')
describe('test spell checker', function () {
// enable spellchecker settings
ConfigUtil.setConfigItem('enableSpellchecker', true);
ConfigUtil.setConfigItem('spellcheckerLanguage', 'en');
SetupSpellChecker.init() // re-initialize after setting update
const spellCheckHandler = SetupSpellChecker.SpellCheckHandler
it('mark misspelled word', function () {
expect(spellCheckHandler.isMisspelled('helpe')).toBe(true)
})
it('verify properly spelled word', function () {
expect(spellCheckHandler.isMisspelled('help')).toBe(false)
})
})

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

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

31
tools/gitlint/ci.js Normal file
View File

@@ -0,0 +1,31 @@
const {run} = require('./helpers');
module.exports = () => {
if (process.argv.TRAVIS !== undefined) {
return travis();
}
return appveyor();
};
function travis() {
if (!process.env.TRAVIS_PULL_REQUEST) {
// Building against master last commit
return run('git log -1 HEAD');
}
const cmd = `git log ${process.env.TRAVIS_COMMIT_RANGE}`;
const commitRange = run(`git diff --name-only ${process.env.TRAVIS_COMMIT_RANGE}`);
process.stdout.write(`COMMIT_RANGE: ${commitRange}`, 'utf8');
return run(cmd);
}
function appveyor() {
if (!process.env.APPVEYOR_PULL_REQUEST_NUMBER) {
return run('git log -1 HEAD');
}
const cmd =
`git log origin/master...${process.env.APPVEYOR_PULL_REQUEST_HEAD_COMMIT}`;
return run(cmd);
}

91
tools/gitlint/helpers.js Normal file
View File

@@ -0,0 +1,91 @@
const {spawnSync} = require('child_process');
const chalk = require('chalk');
const commitMsgRegex = /[A-Z]+.*\.$/;
const isFullCommitRegex = /(\w|\W){1,}:\s{1}/;
const fullCommitRegex = /(\w|\W){1,}:\s{1}[A-Z]+.*\.$/;
function run(script) {
script = script.split(' ');
const cmd = script.splice(0, 1)[0];
const args = script;
const output = spawnSync(cmd, args, {
cwd: process.cwd(),
encoding: 'utf8',
windowsHide: true
}).stdout;
return output;
}
function garbageCollect(a) {
a.forEach((content, index) => {
if (content === '' || content === undefined) {
a.splice(index, 1);
}
});
return a;
}
function getAllCommits(output) {
output = output.split('\ncommits');
if (!output.length > 1) {
exports.error('There are no commits to lint.');
process.exit(1);
}
output = garbageCollect(output);
output.forEach((commit, index) => {
output[index] = 'commit' + commit;
});
return output;
}
function parseCommit(output) {
output = output.split('\n\n');
let commit = output[0].replace('commit ', '');
commit = commit.replace(/\n.*/g, '');
let commitHash = commit.split('');
commitHash = commitHash.slice(commitHash.length - 7);
commitHash = commitHash.join('');
const fullCommit = output[1].split('\n');
const commitMsg = fullCommit[0];
let lintingStatus = commitMsgRegex.test(commitMsg);
lintingStatus = (commitMsg.length <= 72);
if (lintingStatus && isFullCommitRegex(commitMsg)) {
lintingStatus = fullCommitRegex.test(commitMsg);
}
const result = {
failed: !lintingStatus,
commitHash
};
return result;
}
function logSuccess() {
console.log(chalk`{green commit linter:} commit linter passed.`);
process.exit(0);
}
function error(...args) {
args.unshift(chalk.red('ERROR! '));
console.error.apply(this, args);
}
function warn() {
// console.error(chalk`{yellow ${msg}}`);
}
module.exports = {
run,
getAllCommits,
parseCommit,
logSuccess,
error,
warn
};

44
tools/gitlint/index.js Normal file
View File

@@ -0,0 +1,44 @@
const helpers = require('./helpers');
const getCICmd = require('./ci');
let checkAllCommits = false;
let ciMode = false;
if (process.argv[2]) {
checkAllCommits = process.argv[2].includes('-a');
ciMode = process.argv[2] === '--ci-mode';
}
let cmd;
if (ciMode) {
cmd = getCICmd();
} else {
cmd =
checkAllCommits ? 'git log upstream/master...HEAD' : 'git log -1 HEAD';
}
const commits = helpers.run(cmd);
const commitsArray = helpers.getAllCommits(commits);
let lintFailed = false;
commitsArray.forEach(commit => {
const res = helpers.parseCommit(commit);
if (res.failed) {
const {commitHash} = res;
helpers.error(`commit ${commitHash} does not pass our commit style.`);
lintFailed = true;
} else {
helpers.logSuccess('Commit[s] follow the zulip-electron commit rules.');
}
});
if (lintFailed) {
helpers.warn(`
commit msg linting failed
-------------------------------
Reasons it does not have:
a) capitial latter at start of message
b) period at the end of commit or
c) Commit msg length is more than 72 charaters
`);
helpers.warn('Run with --no-verify flag to skip the commit-linter');
process.exit(1);
}

View File

@@ -0,0 +1,73 @@
// This script sets up the pre-push
// git hook which will be used to lint
// commits
const fs = require('fs');
const path = require('path');
const gitHooks = '../../.git/hooks';
const postCommitPath = path.resolve(__dirname, `${gitHooks}/post-commit`);
const prePushPath = path.resolve(__dirname, `${gitHooks}/pre-push`);
function scriptTemplate(cmds) {
cmds = cmds.join('');
const script = [
'#!/bin/sh',
'set -e',
'echo running gitlint...',
cmds,
'exit $?'
];
return script.join('\n');
}
const postCommitFile = scriptTemplate`node scripts/gitlint`;
const prePushFile = scriptTemplate`node scripts/gitlint --all-commits`;
function writeAndChmod(file, data) {
fs.writeFile(file, data, err => {
if (err) {
throw err;
}
fs.chmod(file, '777', err => {
if (err) {
const msg =
'chmod post-commit, pre-push hooks, at .git/hooks 0777 so they work!';
console.error(msg);
}
});
});
}
[postCommitPath, prePushPath].forEach((file, index) => {
fs.open(file, 'w+', err => {
if (err && err.code !== 'EEXIST') {
throw err;
}
const data = index === 0 ? postCommitFile : prePushFile;
writeAndChmod(file, data);
});
});
// Remove .sample files since
// sometimes the hooks do not work
const postCommitSampleFile = `${postCommitPath}.sample`;
const prePushSampleFile = `${prePushPath}.sample`;
function removeSampleFile(file) {
fs.unlink(file, err => {
if (err) {
throw err;
}
});
}
[postCommitSampleFile, prePushSampleFile].forEach(file => {
fs.exists(file, exists => {
if (exists) {
removeSampleFile(file);
}
});
});