Compare commits
	
		
			18 Commits
		
	
	
		
			tray-icon
			...
			git-linter
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 9e3cfbd887 | ||
|  | 720f42ca80 | ||
|  | e18ba5dab6 | ||
|  | 239631a2b6 | ||
|  | 89d1344e2f | ||
|  | 1948ba2cc3 | ||
|  | b8da7dd6ee | ||
|  | 4a0efb7301 | ||
|  | aedd95259d | ||
|  | c8d7a79877 | ||
|  | 6e6db42b54 | ||
|  | db79284fbb | ||
|  | 2434f06655 | ||
|  | 1d611d3382 | ||
|  | a746194e9e | ||
|  | 7cc13f7a26 | ||
|  | 6a9bb152a0 | ||
|  | 8b6dcd355f | 
| @@ -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: | ||||
|   | ||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -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. | ||||
|  | ||||
|   | ||||
| @@ -54,7 +54,7 @@ class AppMenu { | ||||
| 			role: 'togglefullscreen' | ||||
| 		}, { | ||||
| 			label: 'Zoom In', | ||||
| 			accelerator: 'CommandOrControl+Plus', | ||||
| 			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
									
									
									
								
							
							
						
						| @@ -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", | ||||
|   | ||||
| @@ -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" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -26,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 | ||||
|   | ||||
| @@ -35,6 +35,7 @@ class ServerManagerView { | ||||
| 		this.activeTabIndex = -1; | ||||
| 		this.tabs = []; | ||||
| 		this.functionalTabs = {}; | ||||
| 		this.tabIndex = 0; | ||||
| 	} | ||||
|  | ||||
| 	init() { | ||||
| @@ -120,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: () => { | ||||
| @@ -167,6 +171,12 @@ class ServerManagerView { | ||||
| 		this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip); | ||||
| 	} | ||||
|  | ||||
| 	getTabIndex() { | ||||
| 		const currentIndex = this.tabIndex; | ||||
| 		this.tabIndex++; | ||||
| 		return currentIndex; | ||||
| 	} | ||||
|  | ||||
| 	sidebarHoverEvent(SidebarButton, SidebarTooltip) { | ||||
| 		SidebarButton.addEventListener('mouseover', () => { | ||||
| 			SidebarTooltip.removeAttribute('style'); | ||||
| @@ -193,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: () => { | ||||
| @@ -423,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) { | ||||
|   | ||||
| @@ -1,34 +0,0 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const { remote, ipcRenderer } = require('electron'); | ||||
|  | ||||
| const ConfigUtil = require(__dirname + '/utils/config-util.js'); | ||||
|  | ||||
| const { app } = remote; | ||||
|  | ||||
| // 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); | ||||
|  | ||||
| 		this.addEventListener('click', () => { | ||||
| 			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'; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| window.Notification = baseNotification; | ||||
|  | ||||
							
								
								
									
										100
									
								
								app/renderer/js/notification/darwin-notifications.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										31
									
								
								app/renderer/js/notification/default-notification.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										102
									
								
								app/renderer/js/notification/helpers.js
									
									
									
									
									
										Normal 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 | ||||
| }; | ||||
							
								
								
									
										18
									
								
								app/renderer/js/notification/index.js
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @@ -15,7 +15,7 @@ class NewServerForm extends BaseComponent { | ||||
| 				<div class="server-info-right"> | ||||
| 					<div class="title">URL of Zulip organization</div> | ||||
| 					<div class="server-info-row"> | ||||
| 						<input class="setting-input-value" autofocus placeholder="acme.zulipchat.com or chat.acme.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); | ||||
| 		}); | ||||
| 	} | ||||
|   | ||||
| @@ -69,21 +69,26 @@ 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) => { | ||||
| 			const inputSelector = '#sidebar-option .action .switch input'; | ||||
| 			const input = document.querySelector(inputSelector); | ||||
| 			input.checked = state; | ||||
| 			this.handleToggle('sidebar-option', state); | ||||
| 		}); | ||||
|  | ||||
| 		ipcRenderer.on('toggletray', (event, state) => { | ||||
| 			const inputSelector = '#tray-option .action .switch input'; | ||||
| 			const input = document.querySelector(inputSelector); | ||||
| 			input.checked = state; | ||||
| 			this.handleToggle('tray-option', state); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -3,111 +3,105 @@ const path = require('path'); | ||||
|  | ||||
| const electron = require('electron'); | ||||
|  | ||||
| const { ipcRenderer, remote } = electron; | ||||
| const {ipcRenderer, remote} = electron; | ||||
|  | ||||
| const { Tray, Menu, BrowserWindow } = remote; | ||||
| const {Tray, Menu, nativeImage, BrowserWindow} = remote; | ||||
|  | ||||
| const APP_ICON = path.join(__dirname, '../../resources/', 'f'); | ||||
| const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray'); | ||||
|  | ||||
| const ConfigUtil = require(__dirname + '/utils/config-util.js'); | ||||
|  | ||||
| const iconPath = unreadCount => { | ||||
| const iconPath = () => { | ||||
| 	if (process.platform === 'linux') { | ||||
| 		return APP_ICON + 'linux.png'; | ||||
| 	} | ||||
| 	if (!unreadCount) { | ||||
| 		return path.join(__dirname, '../../resources/tray', 'trayosx@2x.png'); | ||||
| 	} | ||||
| 	if (unreadCount > 99) { | ||||
| 		return APP_ICON + (process.platform === 'win32' ? 'win.ico' : `/favicon-infinite.png`); | ||||
| 	} | ||||
| 	return APP_ICON + (process.platform === 'win32' ? 'win.ico' : `/favicon-${unreadCount}.png`); | ||||
| 	return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'osx.png'); | ||||
| }; | ||||
|  | ||||
| let unread = 0; | ||||
|  | ||||
| // const trayIconSize = () => { | ||||
| // 	switch (process.platform) { | ||||
| // 		case 'darwin': | ||||
| // 			return 20; | ||||
| // 		case 'win32': | ||||
| // 			return 100; | ||||
| // 		case 'linux': | ||||
| // 			return 100; | ||||
| // 		default: return 80; | ||||
| // 	} | ||||
| // }; | ||||
| const trayIconSize = () => { | ||||
| 	switch (process.platform) { | ||||
| 		case 'darwin': | ||||
| 			return 20; | ||||
| 		case 'win32': | ||||
| 			return 100; | ||||
| 		case 'linux': | ||||
| 			return 100; | ||||
| 		default: return 80; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| //  Default config for Icon we might make it OS specific if needed like the size | ||||
| // const config = { | ||||
| // 	pixelRatio: window.devicePixelRatio, | ||||
| // 	unreadCount: 0, | ||||
| // 	showUnreadCount: true, | ||||
| // 	unreadColor: '#000000', | ||||
| // 	readColor: '#000000', | ||||
| // 	unreadBackgroundColor: '#B9FEEA', | ||||
| // 	readBackgroundColor: '#B9FEEA', | ||||
| // 	size: trayIconSize(), | ||||
| // 	thick: process.platform === 'win32' | ||||
| // }; | ||||
| const config = { | ||||
| 	pixelRatio: window.devicePixelRatio, | ||||
| 	unreadCount: 0, | ||||
| 	showUnreadCount: true, | ||||
| 	unreadColor: '#000000', | ||||
| 	readColor: '#000000', | ||||
| 	unreadBackgroundColor: '#B9FEEA', | ||||
| 	readBackgroundColor: '#B9FEEA', | ||||
| 	size: trayIconSize(), | ||||
| 	thick: process.platform === 'win32' | ||||
| }; | ||||
|  | ||||
| // const renderCanvas = function (arg) { | ||||
| // 	config.unreadCount = arg; | ||||
| const renderCanvas = function (arg) { | ||||
| 	config.unreadCount = arg; | ||||
|  | ||||
| // 	return new Promise(resolve => { | ||||
| // 		const SIZE = config.size * config.pixelRatio; | ||||
| // 		const PADDING = SIZE * 0.05; | ||||
| // 		const CENTER = SIZE / 2; | ||||
| // 		const HAS_COUNT = config.showUnreadCount && config.unreadCount; | ||||
| // 		const color = config.unreadCount ? config.unreadColor : config.readColor; | ||||
| // 		const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor; | ||||
| 	return new Promise(resolve => { | ||||
| 		const SIZE = config.size * config.pixelRatio; | ||||
| 		const PADDING = SIZE * 0.05; | ||||
| 		const CENTER = SIZE / 2; | ||||
| 		const HAS_COUNT = config.showUnreadCount && config.unreadCount; | ||||
| 		const color = config.unreadCount ? config.unreadColor : config.readColor; | ||||
| 		const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor; | ||||
|  | ||||
| // 		const canvas = document.createElement('canvas'); | ||||
| // 		canvas.width = SIZE; | ||||
| // 		canvas.height = SIZE; | ||||
| // 		const ctx = canvas.getContext('2d'); | ||||
| 		const canvas = document.createElement('canvas'); | ||||
| 		canvas.width = SIZE; | ||||
| 		canvas.height = SIZE; | ||||
| 		const ctx = canvas.getContext('2d'); | ||||
|  | ||||
| // 		// Circle | ||||
| // 		// If (!config.thick || config.thick && HAS_COUNT) { | ||||
| // 		ctx.beginPath(); | ||||
| // 		ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false); | ||||
| // 		ctx.fillStyle = backgroundColor; | ||||
| // 		ctx.fill(); | ||||
| // 		ctx.lineWidth = SIZE / (config.thick ? 10 : 20); | ||||
| // 		ctx.strokeStyle = backgroundColor; | ||||
| // 		ctx.stroke(); | ||||
| // 		// Count or Icon | ||||
| // 		if (HAS_COUNT) { | ||||
| // 			ctx.fillStyle = color; | ||||
| // 			ctx.textAlign = 'center'; | ||||
| // 			if (config.unreadCount > 99) { | ||||
| // 				ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`; | ||||
| // 				ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15)); | ||||
| // 			} else if (config.unreadCount < 10) { | ||||
| // 				ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; | ||||
| // 				ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.20)); | ||||
| // 			} else { | ||||
| // 				ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; | ||||
| // 				ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.15)); | ||||
| // 			} | ||||
| 		// Circle | ||||
| 		// If (!config.thick || config.thick && HAS_COUNT) { | ||||
| 		ctx.beginPath(); | ||||
| 		ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false); | ||||
| 		ctx.fillStyle = backgroundColor; | ||||
| 		ctx.fill(); | ||||
| 		ctx.lineWidth = SIZE / (config.thick ? 10 : 20); | ||||
| 		ctx.strokeStyle = backgroundColor; | ||||
| 		ctx.stroke(); | ||||
| 		// Count or Icon | ||||
| 		if (HAS_COUNT) { | ||||
| 			ctx.fillStyle = color; | ||||
| 			ctx.textAlign = 'center'; | ||||
| 			if (config.unreadCount > 99) { | ||||
| 				ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`; | ||||
| 				ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15)); | ||||
| 			} else if (config.unreadCount < 10) { | ||||
| 				ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; | ||||
| 				ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.20)); | ||||
| 			} else { | ||||
| 				ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; | ||||
| 				ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.15)); | ||||
| 			} | ||||
|  | ||||
| // 			resolve(canvas); | ||||
| // 		} | ||||
| // 	}); | ||||
| // }; | ||||
| // /** | ||||
| //  * Renders the tray icon as a native image | ||||
| //  * @param arg: Unread count | ||||
| //  * @return the native image | ||||
| //  */ | ||||
| // const renderNativeImage = function (arg) { | ||||
| // 	return Promise.resolve() | ||||
| // 		.then(() => iconPath(arg)) | ||||
| // 	// .then(canvas => { | ||||
| // 	// 	const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPng(); | ||||
| // 	// 	return Promise.resolve(nativeImage.createFromBuffer(pngData, config.pixelRatio)); | ||||
| // 	// }); | ||||
| // }; | ||||
| 			resolve(canvas); | ||||
| 		} | ||||
| 	}); | ||||
| }; | ||||
| /** | ||||
|  * Renders the tray icon as a native image | ||||
|  * @param arg: Unread count | ||||
|  * @return the native image | ||||
|  */ | ||||
| const renderNativeImage = function (arg) { | ||||
| 	return Promise.resolve() | ||||
| 		.then(() => renderCanvas(arg)) | ||||
| 		.then(canvas => { | ||||
| 			const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPng(); | ||||
| 			return Promise.resolve(nativeImage.createFromBuffer(pngData, config.pixelRatio)); | ||||
| 		}); | ||||
| }; | ||||
|  | ||||
| function sendAction(action) { | ||||
| 	const win = BrowserWindow.getAllWindows()[0]; | ||||
| @@ -187,17 +181,17 @@ ipcRenderer.on('tray', (event, arg) => { | ||||
| 		return; | ||||
| 	} | ||||
| 	// We don't want to create tray from unread messages on macOS since it already has dock badges. | ||||
| 	if (process.platform === 'darwin' || process.platform === 'win32') { | ||||
| 	if (process.platform === 'linux' || process.platform === 'win32') { | ||||
| 		if (arg === 0) { | ||||
| 			unread = arg; | ||||
| 			window.tray.setImage(iconPath()); | ||||
| 			window.tray.setToolTip('No unread messages'); | ||||
| 		} else { | ||||
| 			unread = arg; | ||||
| 			// renderNativeImage(arg).then(image => { | ||||
| 			window.tray.setImage(iconPath(arg)); | ||||
| 			window.tray.setToolTip(arg + ' unread messages'); | ||||
| 			// }); | ||||
| 			renderNativeImage(arg).then(image => { | ||||
| 				window.tray.setImage(image); | ||||
| 				window.tray.setToolTip(arg + ' unread messages'); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| @@ -214,11 +208,11 @@ function toggleTray() { | ||||
| 	} else { | ||||
| 		state = true; | ||||
| 		createTray(); | ||||
| 		if (process.platform === 'darwin' || process.platform === 'win32') { | ||||
| 			// renderNativeImage(unread).then(image => { | ||||
| 			window.tray.setImage(iconPath()); | ||||
| 			window.tray.setToolTip(unread + ' unread messages'); | ||||
| 			// }); | ||||
| 		if (process.platform === 'linux' || process.platform === 'win32') { | ||||
| 			renderNativeImage(unread).then(image => { | ||||
| 				window.tray.setImage(image); | ||||
| 				window.tray.setToolTip(unread + ' unread messages'); | ||||
| 			}); | ||||
| 		} | ||||
| 		ConfigUtil.setConfigItem('trayIcon', true); | ||||
| 	} | ||||
|   | ||||
| @@ -99,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); | ||||
|   | ||||
| @@ -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> | ||||
| Before Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB |