Refactor converse-desktop and bump to Converse 9

This removes all AngularJS code

Angular version 1 is unmaintained and largely obsolete since years.

Additionally, I'd rather re-use the conventions and libraries from
Converse instead of having different ones in converse-desktop.

This means we're losing some functionality, such as the app settings
and the about modal.

This is unfortunate, but ideally (non-Electron) app settings should
be implemented in Converse itself and not in this repo.
This commit is contained in:
JC Brand 2021-12-01 11:54:41 +01:00
parent cc3dae61ad
commit 1d19fba89f
45 changed files with 193 additions and 1028 deletions

22
.eslintrc.js Normal file
View File

@ -0,0 +1,22 @@
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"globals": {
"__dirname": true,
"converse": true,
"exports": true,
"module": true,
"process": true,
"require": true,
},
"rules": {
"prefer-const": "error",
}
};

16
Makefile Normal file
View File

@ -0,0 +1,16 @@
BIN ?= ./node_modules/.bin/
ESLINT ?= ./node_modules/.bin/eslint
node_modules: package.json package-lock.json
npm i
build: node_modules
$(BIN)/electron-rebuild
serve: build
npm start
.PHONY: eslint
eslint: node_modules
$(ESLINT) *.js
$(ESLINT) app/**/*.js

View File

@ -1,10 +0,0 @@
let angApp = require(__dirname+'/../init')
angApp.controller('AboutController', function($scope, AppStateService, AppInfo) {
$scope.appInfo = AppInfo
$scope.closeAbout = () => {
AppStateService.set(AppStateService.APP_STATE_DEFAULT)
}
})

View File

@ -1,47 +0,0 @@
let angApp = require(__dirname+'/../init')
angApp.controller('DefaultController', function($scope, $timeout, $http, AppInfo) {
$scope.appInfo = AppInfo
let getUpdateInfo = () => {
$http({
url: $scope.appInfo.APP_RELEASES_CHECK_URL,
method: 'GET'
}).then((response) => {
let releaseTag = response.data[0].tag_name
let releaseVersion = parseInt(releaseTag.replace(/v|\./g, ''))
let appVersion = parseInt($scope.appInfo.APP_VERSION.replace(/v|\./g, ''))
if (appVersion < releaseVersion ) {
$scope.checkingForUpdate = 'updateAvailable'
}
else {
$scope.checkingForUpdate = 'latest'
}
}).catch((error) => {
$scope.checkingForUpdate = 'checkErr'
})
}
let checkForUpdate = (timeout = 5000) => {
$scope.checkingForUpdate = 'inProgress'
$timeout(() => {
getUpdateInfo()
}, timeout)
}
let checkForUpdateDelayed = (timeout = 5000) => {
$timeout(() => {
checkForUpdate()
}, timeout)
}
checkForUpdateDelayed()
$scope.checkRetry = ($event) => {
$event.preventDefault()
checkForUpdate()
}
})

View File

@ -1,7 +0,0 @@
let angApp = require(__dirname+'/../init')
angApp.controller('FooterController', function($scope, AppInfo) {
$scope.appInfo = AppInfo
})

View File

@ -1,21 +0,0 @@
let angApp = require(__dirname+'/../init')
angApp.controller('LoginController', function($scope, DesktopService, CredentialsService) {
$scope.help = {}
$scope.showHelp = (item) => {
$scope.help[item] = typeof $scope.help[item] === 'undefined' ? true : !$scope.help[item];
}
$scope.addAccountAndLoginAction = () => {
CredentialsService.addCredentials($scope.credentials.connectionManager,
$scope.credentials.login,
$scope.credentials.password
)
DesktopService.getCredentialsAndLogin()
$scope.accountForm.$setPristine()
$scope.accountForm.$setUntouched()
$scope.credentials = {}
}
})

View File

@ -1,40 +0,0 @@
let angApp = require(__dirname+'/../init')
angApp.controller('SettingsController', function ($scope, $rootScope, AppStateService, SettingsService) {
let formInitialized = false
$scope.settingsChanged = false
$scope.settingsSaved = false
const settingsSetPristine = () => {
$scope.settingsChanged = false
formInitialized = false
}
$scope.closeSettings = () => {
$scope.settings = SettingsService.loadAll()
settingsSetPristine()
AppStateService.set(AppStateService.APP_STATE_DEFAULT)
}
$scope.saveSettings = () => {
$scope.settingsSaved = true
SettingsService.saveAll($scope.settings)
settingsSetPristine()
}
$scope.restartApp = () => {
$rootScope.$broadcast('app:restart')
}
$scope.settings = SettingsService.loadAll()
$scope.$watch("settings", () => {
if (!formInitialized) {
formInitialized = true
} else {
$scope.settingsChanged = true
}
}, true)
})

View File

@ -0,0 +1,24 @@
const { addCredentials } = require('../credentials.js');
const { getCredentials, removeCredentials } = require('../credentials.js');
converse.plugins.add('converse-desktop-credentials', {
initialize () {
const { _converse } = this;
const { api } = _converse;
api.listen.on('afterResourceBinding', () => {
if (_converse.connection.pass) {
addCredentials(
converse.connectionManager,
_converse.bare_jid,
_converse.connection.pass
);
}
});
api.listen.on('logout', () => {
getCredentials().then((result) => removeCredentials(result.login))
});
}
});

49
app/credentials.js Normal file
View File

@ -0,0 +1,49 @@
/* global require, module */
const settings = require('electron-settings');
const keytar = require('keytar')
function addCredentials (connectionManager, login, password) {
const xmppService = login.split('@').pop()
settings.setSync('connectionManager', connectionManager)
settings.setSync('login', login)
keytar.setPassword(xmppService, login, password)
}
function getCredentials () {
const credentials = {}
credentials.login = settings.getSync('login')
return new Promise((resolve) => {
if (credentials.login) {
credentials.connectionManager = settings.getSync('connectionManager')
credentials.xmppService = credentials.login.split('@').pop()
let password = keytar.getPassword(credentials.xmppService, credentials.login)
password.then((result) => {
credentials.password = result
resolve(credentials)
})
} else {
resolve({});
}
});
}
function removeCredentials (login) {
const xmppService = login.split('@').pop()
const passwordDelete = keytar.deletePassword(xmppService, login)
return new Promise((resolve, reject) => {
passwordDelete.then(() => {
settings.unsetSync('login')
settings.unsetSync('connectionManager')
resolve()
}, (error) => {
reject(error)
})
})
}
module.exports = {
addCredentials,
getCredentials,
removeCredentials
}

View File

@ -1,14 +0,0 @@
const angular = require('angular')
let angApp = angular.module('app', [])
angApp.constant('AppInfo', {
APP_NAME: 'Converse Desktop',
APP_VERSION: 'v0.1.0',
APP_HOME: 'https://github.com/conversejs/converse-desktop',
APP_RELEASES_CHECK_URL: 'https://api.github.com/repos/conversejs/converse-desktop/releases',
APP_RELEASES_URL: 'https://github.com/conversejs/converse-desktop/releases',
CONVERSE_VERSION: 'v7.0.4',
ELECTRON_VERSION: '11.2.3'
});
module.exports = angApp

View File

@ -1,22 +0,0 @@
let angApp = require(__dirname + '/../init')
angApp.factory('AppStateService', [ '$rootScope', ($rootScope) => {
let stateService = {}
stateService.APP_STATE_LOGIN = 'login'
stateService.APP_STATE_DEFAULT = 'default'
stateService.APP_STATE_SETTINGS = 'settings'
stateService.APP_STATE_ABOUT = 'about'
stateService.set = (state) => {
stateService.previousState = typeof stateService.state !== 'undefined' ?
stateService.state : stateService.APP_STATE_DEFAULT
stateService.state = state
$rootScope.$broadcast('app:state:changed', stateService.state)
}
stateService.set(stateService.APP_STATE_DEFAULT)
return stateService
}])

View File

@ -1,54 +0,0 @@
let angApp = require(__dirname+'/../init')
angApp.factory('CredentialsService', () => {
const keytar = require('keytar')
const settings = require('electron-settings')
let credentialsService = {}
credentialsService.getCredentials = () => {
let credentials = {}
credentials.login = settings.getSync('login')
let promise = new Promise((resolve, reject) => {
if (credentials.login) {
credentials.connectionManager = settings.getSync('connectionManager')
credentials.xmppService = credentials.login.split('@').pop()
let password = keytar.getPassword(credentials.xmppService, credentials.login)
password.then((result) => {
credentials.password = result
resolve(credentials)
})
}
else {
reject(Error('No login stored'))
}
})
return promise
}
credentialsService.addCredentials = (connectionManager, login, password) => {
let xmppService = login.split('@').pop()
settings.setSync('connectionManager', connectionManager)
settings.setSync('login', login)
keytar.setPassword(xmppService, login, password)
}
credentialsService.removeCredentials = (login) => {
let xmppService = login.split('@').pop()
passwordDelete = keytar.deletePassword(xmppService, login)
let promise = new Promise((resolve, reject) => {
passwordDelete.then((result) => {
settings.unsetSync('login')
settings.unsetSync('connectionManager')
resolve()
}, (error) => {
reject(error)
})
})
return promise
}
return credentialsService
})

View File

@ -1,96 +0,0 @@
let angApp = require(__dirname + '/../init')
const desktopPlugin = require(__dirname +'/../../libs/converse.js/converse-desktop/desktop-plugin')
angApp.factory('DesktopService', (
$window, $timeout, CredentialsService, SystemService, AppStateService,
SettingsService, XmppHelperService
) => {
let desktopService = {}
desktopService._notifyMessage = () => {
SystemService.playAudio()
SystemService.showEnvelope()
}
desktopService._hideNotifyMessage = () => {
SystemService.hideEnvelope()
}
desktopService.logout = () => {
let credentials = CredentialsService.getCredentials()
credentials.then((result) => {
let remove = CredentialsService.removeCredentials(result.login)
console.log('Remove credentials on logout')
remove.then(() => {
AppStateService.set(AppStateService.APP_STATE_LOGIN)
})
})
}
desktopService.initConverse = (connectionManager, login, password) => {
AppStateService.set(AppStateService.APP_STATE_DEFAULT) // Always set to default state before init
desktopPlugin.register(login)
let lang = navigator.language
let allowBookmarks = SettingsService.get('allowBookmarks')
let omemoDefault = SettingsService.get('omemoDefault')
let xmppResource = XmppHelperService.getResourceFromJid(login)
if (!xmppResource) {
xmppResource = '.' + (Math.random().toString(36)+'00000000000000000').slice(2, 7) // Generate 5 char unique str
login = login + '/converseDesktop'+xmppResource
}
let conversejsParams = {
assets_path: './node_modules/converse.js/dist/',
allow_bookmarks: allowBookmarks,
auto_login: true,
auto_reconnect: true,
// debug: true,
i18n: lang,
jid: login,
omemo_default: omemoDefault,
password: password,
play_sounds: false,
priority: 50,
view_mode: 'embedded',
whitelisted_plugins: ['converseDesktop'],
}
if (connectionManager.startsWith('ws')) {
conversejsParams.websocket_url = connectionManager
} else {
conversejsParams.bosh_service_url = connectionManager
}
$timeout(() => {
converse.initialize(conversejsParams)
}, 50)
}
desktopService.getCredentialsAndLogin = () => {
let credentials = CredentialsService.getCredentials()
credentials.then((result) => {
desktopService.initConverse(result.connectionManager, result.login, result.password)
}, (error) => {
AppStateService.set(AppStateService.APP_STATE_LOGIN)
})
}
desktopService.chatToOpen = null
$window.document.addEventListener('conversejs-logout', function (e) {
desktopService.logout()
})
$window.document.addEventListener('conversejs-unread', function (e) {
let sender = e.detail
desktopService.chatToOpen = sender
desktopService._notifyMessage()
})
$window.document.addEventListener('conversejs-no-unread', function (e) {
desktopService._hideNotifyMessage()
})
return desktopService
})

View File

@ -1,109 +0,0 @@
let angApp = require(__dirname+'/../init')
angApp.factory('SettingsService', () => {
let settingsService = {}
const electronSettings = require('electron-settings')
const settings = {
converseDesktop: {
runMinimized: {
default: false,
title: 'Run minimized',
hint: 'Default: false. Whether run Converse Desktop minimized to tray or not.'
},
minimizeOnClose: {
default: false,
title: 'Minimize on close',
hint: 'Default: false. Minimize or close Converse Desktop window.'
},
preserveWindowSize: {
default: true,
title: 'Preserve window size',
hint: 'Default: true, 800x600 otherwise.'
},
preserveWindowPosition: {
default: true,
title: 'Preserve window position',
hint: 'Default: true, screen center otherwise.'
}
},
conversejs: {
allowBookmarks: {
default: false,
title: 'Allow server bookmarks',
hint: 'Default: false. Enables/disables chatroom bookmarks functionality.'
},
omemoDefault: {
default: false,
title: 'Use OMEMO encryption by default',
hint: 'Default: false. Use OMEMO encryption by default when the chat supports it.'
}
}
}
const iterateSettings = (callback, settingsObj) => {
if(typeof settingsObj === "undefined") {
settingsObj = settings
}
angular.forEach(settingsObj, (value, key) => {
let settingsList = settingsObj[key]
angular.forEach(settingsList, (value, key) => {
let itemDefault = settingsList[key]['default']
callback(key, itemDefault, settingsList)
})
})
}
// Callback
// TODO: replace with promise?
const saveDefault = (key, value) => {
if (!electronSettings.hasSync(key)) {
electronSettings.setSync(key, value)
}
}
// Callback
const save = (key, defaultValue, settingsList) => {
let value = settingsList[key]['value']
electronSettings.setSync(key, value)
}
// Callback
const loadAll = (key, defaultValue, settingsList) => {
if (!electronSettings.hasSync(key)) {
settingsList[key]['value'] = defaultValue
}
settingsList[key]['value'] = electronSettings.getSync(key)
}
/**
* SettingsService
*/
settingsService.initDefaults = () => {
iterateSettings(saveDefault)
// Logout for versions with BOSH only
if (electronSettings.hasSync('bosh')) {
electronSettings.unsetSync('bosh')
electronSettings.unsetSync('login')
}
}
settingsService.get = (key) => {
return electronSettings.getSync(key)
}
settingsService.loadAll = () => {
let currentSettings = angular.copy(settings)
iterateSettings(loadAll, currentSettings)
return currentSettings
}
settingsService.saveAll = (currentSettings) => {
iterateSettings(save, currentSettings)
}
return settingsService
})

View File

@ -1,28 +0,0 @@
let angApp = require(__dirname + '/../init')
angApp.factory('SystemService', () => {
const remote = require('electron').remote
let systemService = {}
systemService.playAudio = () => {
var audio = new Audio(__dirname + '/../../resources/sounds/graceful.ogg')
audio.play()
}
systemService.showEnvelope = () => {
remote.require('./main').trayService.showEnvelope()
}
systemService.hideEnvelope = () => {
remote.require('./main').trayService.hideEnvelope()
}
systemService.reloadWindow = () => {
remote.getCurrentWindow().reload()
}
return systemService
})

View File

@ -1,25 +0,0 @@
let angApp = require(__dirname + '/../init')
angApp.factory('XmppHelperService', [() => {
let xmppHelperService = {}
/**
* Use function copy from Strophe js lib because converse.js Strophe library
* is under private _api and unavailable before converse.js is initialized.
* This function is used _before_ converse.js is initialized.
*
* Get the resource portion of a JID String.
* @param {string} jid A JID.
* @return {string | null} A String containing the resource.
*/
xmppHelperService.getResourceFromJid = (jid) => {
if (!jid) { return null }
const s = jid.split("/")
if (s.length < 2) { return null }
s.splice(0, 1)
return s.join('/')
}
return xmppHelperService
}])

View File

@ -1,35 +0,0 @@
<div class="page-about" ng-controller="AboutController">
<div class="about-card">
<div class="about-card__content">
<div class="about-card__logo">
<img src="./resources/images/logo.png" srcset="./resources/images/logo@2x.png 2x" alt=""
class="chimeverse-branding__img" />
</div>
<div class="about-card__description">
<h3 class="about__title">About {{appInfo.APP_NAME}} {{appInfo.APP_VERSION}}</h3>
<div class="about__description">Jabber/XMPP client based on Converse.js and Electron</div>
<div>&nbsp;</div>
<div class="about__converse-version">
Version of <a href="https://conversejs.org/" target="_blank">converse.js</a> is {{appInfo.CONVERSE_VERSION}}
</div>
<div class="about__electron-version">
Version of <a href="https://www.electronjs.org/" target="_blank">electron</a> is {{appInfo.ELECTRON_VERSION}}
</div>
<div>&nbsp;</div>
<div class="about__thanks">
Thanks to
<a href="https://github.com/nick-denry/Chimeverse/graphs/contributors" target="_blank">all contributors</a>
</div>
<div>&nbsp;</div>
</div>
</div>
<div class="about__action">
<button class="about__close-button" ng-click="closeAbout()">OK</button>
</div>
<div>&nbsp;</div>
<div class="about__copyright">Copyright &copy; 2019-2020 Nick Denry</div>
</div>
</div>
<ng-include src="'./app/views/shared/_footer.html'"></ng-include>

View File

@ -1,24 +0,0 @@
<div class="page-default" ng-controller="DefaultController">
<div class="chimeverse-branding noselect">
<img src="./resources/images/logo.png" srcset="./resources/images/logo@2x.png 2x" alt=""
class="chimeverse-branding__img" />
<h3 class="chimeverse-branding__header">{{appInfo.APP_NAME}}</h3>
<div class="chimeverse-branding__version">
<span>{{appInfo.APP_VERSION}} </span>
<span class="chimeverse-branding__update">
<span class="update__checking" ng-show="checkingForUpdate == 'inProgress'">checking for update...</span>
<span class="update__error" ng-show="checkingForUpdate == 'checkErr'">
&gt;_&lt; check for update failed <a href="#" ng-click="checkRetry($event)">retry</a>
</span>
<span class="update__latest" ng-show="checkingForUpdate == 'latest'">latest version</span>
<a class="update__available" href="{{appInfo.APP_RELEASES_URL}}" target="_blank"
ng-show="checkingForUpdate == 'updateAvailable'">
Update available
</a>
</span>
</div>
</div>
<div class="conversejs-adoption">
<div id="conversejs"></div>
</div>
</div>

View File

@ -1,43 +0,0 @@
<div class="login-form">
<h3 class="login-form__title">Welcome to Converse Desktop</h3>
<div class="login-form__description">Jabber/XMPP client based on Converse.js and Electron</div>
<div class="login-form__card">
<form name="accountForm" ng-controller="LoginController">
<div class="login-form__wrapper">
<div class="login-form__input-group">
<span class="group-prepend oi" data-glyph="link-intact"></span>
<input ng-model="credentials.connectionManager" class="login-form__input login-form__input--with-append" type="text" placeholder="Connection manager url" required>
<span class="group-append--backgrounded oi" data-glyph="info" ng-click="showHelp('connectionManager')"></span>
</div>
<div class="login-form__input-help" ng-show="help.connectionManager == true"">
<div>
Should be a
<a href="https://m.conversejs.org/docs/html/setup.html#bosh-section" target="_blank">BOSH service</a>
or a
<a href="https://m.conversejs.org/docs/html/setup.html#websocket-section" target="_blank">Websocket</a>
URL
</div>
</div>
</div>
<div class="login-form__wrapper">
<div class="login-form__input-group">
<span class="group-prepend oi" data-glyph="person"></span>
<input ng-model="credentials.login" class="login-form__input" type="text" placeholder="jid@jabber.org" required>
</div>
</div>
<div class="login-form__wrapper">
<div class="login-form__input-group">
<span class="group-prepend oi" data-glyph="lock-locked"></span>
<input ng-model="credentials.password" class="login-form__input" type="password" placeholder="Password" required>
</div>
</div>
<div class="login-form__wrapper">
<button class="login-form__button" ng-click="addAccountAndLoginAction()" ng-disabled="accountForm.$invalid" disabled>Add account</button>
</div>
<div class="login-form__credentials-message">
Credentials stored via <a href="https://atom.github.io/node-keytar/" target="_blank">Keychain</a>
</div>
</form>
</div>
</div>
<ng-include src="'./app/views/shared/_footer.html'"></ng-include>

View File

@ -1,28 +0,0 @@
<div class="settings-page" ng-controller="SettingsController">
<form name="settignsForm">
<h3 class="settings-page__title">Chimeverse settings</h3>
<div class="form-item" ng-repeat="(key, item) in settings.converseDesktop">
<label>
<input type="checkbox" name="{{key}}" ng-model="item.value" /> {{item.title}}
</label>
<div class="form-item__hint">{{item.hint}}</div>
</div>
<h3 class="settings-page__title">Converse.js settings</h3>
<div class="form-item" ng-repeat="(key, item) in settings.conversejs">
<label>
<input type="checkbox" name="{{key}}" ng-model="item.value" /> {{item.title}}
</label>
<div class="form-item__hint">{{item.hint}}</div>
</div>
<div class="form-item" ng-show="settingsSaved">
<a href="#" ng-click="restartApp()" class="form-item__restart-app">
Click here to restart the app and apply your changes
</a>
</div>
<div class="form-actions">
<button class="form-item__save-button" ng-class="{'active': settingsChanged == true}" ng-click="saveSettings()">Save</button>
<button class="form-item__cancel-button" ng-click="closeSettings()">Cancel</button>
</div>
</form>
</div>
<ng-include src="'./app/views/shared/_footer.html'"></ng-include>

View File

@ -1,4 +0,0 @@
<div class="page__footer" ng-controller="FooterController">
<span class="footer__version">{{appInfo.APP_NAME}} {{appInfo.APP_VERSION}}.</span>
<a class="github-button" href="{{appInfo.APP_HOME}}" data-icon="octicon-star" aria-label="Star nick-denry/Chimeverse on GitHub">Star</a>
</div>

View File

@ -7,28 +7,17 @@
<link rel="stylesheet" type="text/css" media="screen" href="node_modules/converse.js/dist/converse.min.css">
<link rel="stylesheet" type="text/css" href="resources/css/app.css">
<link rel="stylesheet" type="text/css" href="resources/css/_footer.css">
<link rel="stylesheet" type="text/css" href="resources/css/page-about.css">
<link rel="stylesheet" type="text/css" href="resources/css/page-default.css">
<link rel="stylesheet" type="text/css" href="resources/css/page-login.css">
<link rel="stylesheet" type="text/css" href="resources/css/page-settings.css">
</head>
<base href="./">
<body ng-app="app">
<div class="main-background"></div>
<div class="main-window" ng-controller="AppController" ng-cloak>
<div ng-show="state == 'default'">
<ng-include src="'./app/views/default/page.html'"></ng-include>
</div>
<div ng-show="state == 'login'">
<ng-include src="'./app/views/login/page.html'"></ng-include>
</div>
<div ng-show="state == 'about'">
<ng-include src="'./app/views/about/page.html'"></ng-include>
</div>
<div ng-show="state == 'settings'">
<ng-include src="'./app/views/settings/page.html'"></ng-include>
<body class="converse-fullscreen" ng-app="app">
<div class="main-window">
<div class="page-default">
<div id="conversejs-bg"></div>
</div>
</div>
<!--
@see comment https://github.com/signalapp/libsignal-protocol-javascript/issues/6#issuecomment-247208665
-->
@ -37,27 +26,15 @@
delete window.require
</script>
<!-- Place libsignal at libs dir as it's no more distributed with converse.js -->
<script src="libs/converse.js/3rdparty/libsignal-protocol.js"></script>
<script src="./3rdparty/libsignal-protocol.js"></script>
<script>
window.require = window.nodeRequire
delete window.nodeRequire
</script>
<script>
// You can also require other files to run in this process
require('./node_modules/converse.js/dist/emojis.js')
require('./node_modules/converse.js/dist/converse.js')
require('./renderer.js')
</script>
<script>
function loadJs(url) {
var script = document.createElement('script')
script.src = url
script.setAttribute('async', 'true')
document.documentElement.firstChild.appendChild(script)
}
setTimeout(function () {
loadJs("./node_modules/github-buttons/dist/buttons.min.js")
}, 500)
require('./setup.js')
</script>
</body>
</html>
</html>

View File

@ -1,67 +0,0 @@
let desktopPlugin = {}
desktopPlugin.register = (login) => {
converse.plugins.add('converseDesktop', {
initialize: (event) => {
let _converse = event.properties._converse
let Strophe = converse.env.Strophe
/**
* Check if message stanza has some body payload
* @param {*} stanzaNodes
*/
let isBodyMessage = (stanzaNodes) => {
let result = false
Object.keys(stanzaNodes).some((key) => {
if (stanzaNodes[key].nodeName == 'body') {
result = true
}
})
return result
}
Promise.all([
_converse.api.waitUntil('rosterContactsFetched'),
_converse.api.waitUntil('chatBoxesFetched')
]).then(() => {
_converse.api.listen.on('logout', () => {
let event = new CustomEvent('conversejs-logout')
document.dispatchEvent(event)
})
_converse.api.listen.on('message', (data) => {
// Display notifications only for "payloaded" messages
if (isBodyMessage(data.stanza.childNodes)) {
let sender = data.stanza.attributes.from.nodeValue
let senderJid = Strophe.getBareJidFromJid(sender)
let loginJid = Strophe.getBareJidFromJid(login)
if (senderJid != loginJid) {
console.log(senderJid)
let event = new CustomEvent('conversejs-unread', {detail: senderJid})
document.dispatchEvent(event)
}
}
})
_converse.api.listen.on('chatBoxFocused', () => {
let event = new CustomEvent('conversejs-no-unread')
document.dispatchEvent(event)
//chimeverseService._hideNotifyMessage()
})
window.document.addEventListener('converse-force-logout', function (e) {
console.log('Get converse-force-logout event')
console.log('Logout form plugin')
_converse.api.user.logout()
//chimeverseService.logout()
})
window.document.addEventListener('conversejs-open-chat', function (e) {
let chatToOpen = e.detail
console.log('Get open-unread-chat event: '+chatToOpen)
if (chatToOpen !== null) {
_converse.api.chats.open(chatToOpen)
}
})
})
}
})
}
module.exports = desktopPlugin

62
main.js
View File

@ -8,7 +8,7 @@ let mainWindow
// Require other app modules
const trayService = require(__dirname+'/modules/tray-service')
const menuService = require(__dirname+'/modules/menu-service')
const settingsService = require(__dirname+'/modules/settings-service')
// const settingsService = require(__dirname+'/modules/settings-service')
const isMac = process.platform === 'darwin'
const isWin = process.platform === 'win32'
@ -23,41 +23,19 @@ function initApp() {
function createWindow () {
// Main window options
let mainWindowOptions = {
width: 800,
height: 600,
minWidth: 780,
minHeight: 560,
const mainWindowOptions = {
zoomToPageWidth: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true
}
}
// Load app settings
let runMinimized = settingsService.get('runMinimized')
if (runMinimized) {
mainWindowOptions.show = !runMinimized
}
let preserveWindowSize = settingsService.get('preserveWindowSize')
if (preserveWindowSize) {
let width = settingsService.get('windowWidth')
let height = settingsService.get('windowHeight')
if (width) mainWindowOptions.width = width
if (height) mainWindowOptions.height = height
}
let preserveWindowPosition = settingsService.get('preserveWindowPosition')
if (preserveWindowPosition) {
let windowX = settingsService.get('windowX')
let windowY = settingsService.get('windowY')
if (windowX) mainWindowOptions.x = windowX
if (windowY) mainWindowOptions.y = windowY
},
icon: './resources/images/logo.png',
}
// Create the browser window.
mainWindow = new BrowserWindow(mainWindowOptions)
mainWindow.maximize();
// and load the index.html of the app.
mainWindow.loadFile('index.html')
@ -72,35 +50,15 @@ function createWindow () {
// mainWindow.webContents.openDevTools()
// Before close
let minimizeOnClose = settingsService.get('minimizeOnClose')
// const minimizeOnClose = settingsService.get('minimizeOnClose');
const minimizeOnClose = false; // XXX: this doesn't seem to work
if (minimizeOnClose) {
mainWindow.on('close', (e) => {
if (!app.isQuitting) {
e.preventDefault()
mainWindow.hide()
}
})
}
// Save window size
if (preserveWindowSize) {
mainWindow.on('resize', (e) => {
let newSize = mainWindow.getSize()
let width = newSize[0]
let height = newSize[1]
settingsService.set('windowWidth', width)
settingsService.set('windowHeight', height)
})
}
// Save window position
if (preserveWindowPosition !== 'undefined') {
mainWindow.on('move', (e) => {
let newPosition = mainWindow.getPosition()
let windowX = newPosition[0]
let windowY = newPosition[1]
settingsService.set('windowX', windowX)
settingsService.set('windowY', windowY)
return false;
})
}
@ -115,7 +73,7 @@ function createWindow () {
}
// Handle restart
ipcMain.on('app-restart', (evt, arg) => {
ipcMain.on('app-restart', () => {
app.isQuitting = true
app.relaunch()
app.exit()

View File

@ -1,61 +1,24 @@
/**
* Module for Menu functions.
*/
const {app, Menu, BrowserWindow} = require('electron')
let menuService = {}
const menuService = {}
menuService.createMenu = () => {
const isMac = process.platform === 'darwin'
const about = {
label: 'About Converse Desktop',
click: () => {
// @see https://github.com/electron/electron/issues/16558#issuecomment-484460276
// let activeWindow = BrowserWindow.getFocusedWindow()
let activeWindow = BrowserWindow.getAllWindows()[0]
activeWindow.show()
activeWindow.webContents.send('about-page-event')
}
}
const application = {
label: 'Converse Desktop',
submenu: [
... isMac ? [about] : [],
{
label: 'Reconnect',
accelerator: 'CmdOrCtrl+R',
click: () => {
let activeWindow = BrowserWindow.getAllWindows()[0]
const activeWindow = BrowserWindow.getAllWindows()[0]
activeWindow.show()
activeWindow.reload()
}
},
{
label: 'Force logout',
accelerator: 'CmdOrCtrl+D',
click: () => {
let activeWindow = BrowserWindow.getAllWindows()[0]
activeWindow.show()
activeWindow.webContents.send('force-logout-event')
}
},
{
type: 'separator',
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => {
let activeWindow = BrowserWindow.getAllWindows()[0]
activeWindow.show()
activeWindow.webContents.send('preferences-event')
}
},
{
type: 'separator',
},
@ -110,12 +73,11 @@ menuService.createMenu = () => {
const help = {
label: 'Help',
submenu: [
... !isMac ? [about] : [],
{
label: 'Debug info',
accelerator: 'F12',
click: () => {
let activeWindow = BrowserWindow.getAllWindows()[0]
const activeWindow = BrowserWindow.getAllWindows()[0]
activeWindow.webContents.openDevTools()
}
}

View File

@ -7,7 +7,7 @@ const electronSettings = require('electron-settings')
let settingsService = {}
settingsService.get = (itemKey) => {
settingValue = electronSettings.getSync(itemKey)
const settingValue = electronSettings.getSync(itemKey)
if (typeof settingValue === 'undefined' || settingValue === null) {
return false
}
@ -18,4 +18,4 @@ settingsService.set = (itemKey, settingValue) => {
electronSettings.setSync(itemKey, settingValue)
}
module.exports = settingsService
module.exports = settingsService

View File

@ -9,17 +9,15 @@ const path = require('path')
let trayServiceWindow = null
let tray = null
let trayService = {}
const trayService = {}
let getTrayServiceIcon = (iconName = 'icon') => {
const getTrayServiceIcon = (iconName = 'icon') => {
let iconImage = ''
if (process.platform === 'darwin') {
iconImage = iconName+'Template'
}
else if (process.platform === 'win32') {
} else if (process.platform === 'win32') {
iconImage = iconName+'-16x16'
}
else {
} else {
iconImage = iconName+'-48x48'
}
return path.join(__dirname, '/../resources/images/' + iconImage + '.png')
@ -27,12 +25,12 @@ let getTrayServiceIcon = (iconName = 'icon') => {
trayService.initTray = (window) => {
trayServiceWindow = window
let iconPath = getTrayServiceIcon()
const iconPath = getTrayServiceIcon()
tray = new Tray(iconPath)
tray.setToolTip('Converse Desktop')
tray.on('click', function() {
// Sent open-related-chat event only on click
let activeWindow = BrowserWindow.getAllWindows()[0]
const activeWindow = BrowserWindow.getAllWindows()[0]
activeWindow.webContents.send('open-unread-chat')
trayService.hideEnvelope()
trayServiceWindow.show()
@ -40,12 +38,12 @@ trayService.initTray = (window) => {
}
trayService.showEnvelope = () => {
let iconPath = getTrayServiceIcon('envelope')
const iconPath = getTrayServiceIcon('envelope')
tray.setImage(iconPath)
}
trayService.hideEnvelope = () => {
let iconPath = getTrayServiceIcon()
const iconPath = getTrayServiceIcon()
tray.setImage(iconPath)
}

View File

@ -20,16 +20,16 @@
"Electron",
"OMEMO"
],
"author": "Nick Denry <nick@denry.ru>",
"license": "MPL-2.0",
"devDependencies": {
"electron": "11.2.3",
"electron-builder": "^22.9.1",
"electron-packager": "^15.2.0"
"electron-packager": "^15.2.0",
"electron-rebuild": "^3.2.5",
"eslint": "^8.4.1"
},
"dependencies": {
"angular": "^1.7.9",
"converse.js": "7.0.4",
"converse.js": "conversejs/converse.js#6ee3ddbc8c41608cf56d86566d6460bb423892b0",
"electron-settings": "^4.0.2",
"github-buttons": "^2.8.0",
"keytar": "^7.3.0",
@ -59,7 +59,7 @@
},
"linux": {
"artifactName": "converse_desktop-${version}_${arch}.${ext}",
"icon": "resources/images/logo@2x.png",
"icon": "resources/images/logo.png",
"target": [
"deb",
"tar.gz"

View File

@ -1,64 +0,0 @@
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// All of the Node.js APIs are available in this process.
var angApp = require('./app/init')
require('./app/services/credentials-service')
require('./app/services/settings-service')
require('./app/services/system-service')
require('./app/services/app-state-service')
require('./app/services/xmpp-helper-service')
require('./app/services/desktop-service')
require('./app/controllers/settings-controller')
require('./app/controllers/login-controller')
require('./app/controllers/default-controller')
require('./app/controllers/about-controller')
require('./app/controllers/footer-controller')
angApp.controller('AppController', function ($scope, $timeout, DesktopService, SettingsService, AppStateService) {
const { ipcRenderer } = require('electron')
// Menu force logout event
ipcRenderer.on('force-logout-event', () => {
DesktopService.logout()
let event = new CustomEvent("converse-force-logout") // Dispatch to the plugin
document.dispatchEvent(event)
})
// Menu settings event
ipcRenderer.on('preferences-event', () => {
AppStateService.set(AppStateService.APP_STATE_SETTINGS)
})
// Menu about event
ipcRenderer.on('about-page-event', () => {
AppStateService.set(AppStateService.APP_STATE_ABOUT)
})
// Menu about event
ipcRenderer.on('open-unread-chat', () => {
let event = new CustomEvent('conversejs-open-chat', {detail: DesktopService.chatToOpen})
document.dispatchEvent(event)
})
AppStateService.set(AppStateService.APP_STATE_DEFAULT)
$scope.$on('app:state:changed', (event, data) => {
// @see https://docs.angularjs.org/error/$rootScope/inprog
$timeout(() => {
$scope.state = data
console.log('Switch to the "' + $scope.state +'" state')
}, 0)
})
$scope.$on('app:restart', (event, data) => {
ipcRenderer.send('app-restart')
})
SettingsService.initDefaults()
DesktopService.getCredentialsAndLogin()
})

View File

@ -20,16 +20,6 @@ a {
color: #777;
}
.main-background {
background: #f8f8f8;
height: 100%;
left: 0px;
position: fixed;
top: 0px;
width: 100%;
z-index: -1;
}
.conversejs-adoption {
background: transparent;
pointer-events: none;

View File

@ -1,39 +0,0 @@
.page-about {
color: #777;
left: 50%;
position: absolute;
top: 47%;
transform: translate(-50%, -50%);
}
.about-card {
background: #fff;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
margin-top: 30px;
min-width: 400px;
padding: 30px;
}
.about-card__content {
display: flex;
}
.about__action {
text-align: center;
}
.about__close-button {
border: 0px solid #ddd;
background: rgb(5,93,228);
background: linear-gradient(0deg, rgba(5,93,228,1) 0%, rgba(76,145,255,1) 100%);
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 18px;
outline: none;
padding: 10px 30px;
}
.about__copyright {
text-align: center;
}

View File

@ -1,100 +0,0 @@
.login-form {
color: #777;
left: 50%;
position: absolute;
top: 47%;
transform: translate(-50%, -50%);
}
.login-form__title {
font-size: 28px;
text-align: center;
font-weight: normal;
margin: 0px 0px 15px 0px;
}
.login-form__description {
text-align: center;
}
.login-form__card {
background: #fff;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
margin-top: 30px;
padding: 30px;
}
.login-form__wrapper {
margin-bottom: 15px;;
}
.login-form__input-group {
border: 1px solid #ddd;
border-radius: 0px;
padding: 0px 0px 0px 30px;
position: relative;
}
.login-form__input-group:active,
.login-form__input-group:focus,
.login-form__input-group:hover {
border: 1px solid #aaa;
}
.login-form__input-group .group-prepend {
position: absolute;
left: 15px;
top: 12px;
}
.login-form__input-group .group-append--backgrounded {
background: #f2f2f2;
cursor: pointer;
padding: 12px;
position: absolute;
right: 0px;
top: 0 px;
}
.login-form__input {
border: 0px;
color: #222;
font-size: 18px;
outline: none;
padding: 10px;
width: 290px;
}
.login-form__input::placeholder { /* Most modern browsers support this now. */
color: #ccc;
}
.login-form__input--with-append {
width: 260px;
}
.login-form__input-help {
font-size: 13px;
margin: 4px;
}
.login-form__button {
border: 1px solid #ddd;
background: rgb(5,93,228);
background: linear-gradient(0deg, rgba(5,93,228,1) 0%, rgba(76,145,255,1) 100%);
color: #fff;
display: inline-block;
font-size: 18px;
outline: none;
padding: 10px 30px;
width: 100%;
}
.login-form__button:disabled {
background: #eee;
}
.login-form__credentials-message {
font-size: 14px;
text-align: center;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 B

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

46
setup.js Normal file
View File

@ -0,0 +1,46 @@
require('./app/converse-plugins/desktop-credentials.js');
const{ getCredentials } = require('./app/credentials.js')
async function initialize () {
let websocket_url, bosh_service_url;
const { connectionManager, login, password } = await getCredentials()
converse.connectionManager = connectionManager;
if (connectionManager?.startsWith('ws')) {
websocket_url = connectionManager
} else if (connectionManager?.startsWith('http')) {
bosh_service_url = connectionManager
} else {
bosh_service_url = 'https://conversejs.org/http-bind/';
}
converse.plugins.add('converse-debug', {
initialize () {
const { _converse } = this;
window._converse = _converse;
}
});
converse.initialize({
assets_path: './node_modules/converse.js/dist/',
auto_login: login && password,
bosh_service_url,
i18n: navigator.language,
jid: login,
loglevel: 'debug',
muc_respect_autojoin: true,
muc_show_logs_before_join: true,
password: password,
play_sounds: false,
priority: 50,
prune_messages_above: 250,
theme: 'concord',
view_mode: 'fullscreen',
websocket_url,
whitelisted_plugins: ['converse-debug', 'converse-desktop-credentials'],
});
}
initialize();