mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-07 00:17:03 +03:00
Testing van be done at https://snappymail.eu/demo/
This commit is contained in:
parent
0ec37b7e90
commit
542d9c91e9
10 changed files with 215 additions and 270 deletions
|
|
@ -26,9 +26,8 @@ RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||||
Header set X-Content-Type-Options "nosniff"
|
Header set X-Content-Type-Options "nosniff"
|
||||||
Header set X-Frame-Options "DENY"
|
Header set X-Frame-Options "DENY"
|
||||||
Header set X-XSS-Protection "1; mode=block"
|
Header set X-XSS-Protection "1; mode=block"
|
||||||
</IfModule>
|
Header set Service-Worker-Allowed "/"
|
||||||
|
|
||||||
<IfModule mod_headers.c>
|
|
||||||
RewriteCond %{HTTP:Accept-encoding} br
|
RewriteCond %{HTTP:Accept-encoding} br
|
||||||
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
||||||
RewriteRule "^(.+)" "$1\.br" [L,T=text/javascript,QSA]
|
RewriteRule "^(.+)" "$1\.br" [L,T=text/javascript,QSA]
|
||||||
|
|
|
||||||
26
README.md
26
README.md
|
|
@ -64,6 +64,7 @@ This fork of RainLoop has the following changes:
|
||||||
* Split Admin specific JavaScript code from User code
|
* Split Admin specific JavaScript code from User code
|
||||||
* JSON reviver
|
* JSON reviver
|
||||||
* Better memory garbage collection management
|
* Better memory garbage collection management
|
||||||
|
* Added serviceworker for Notifications
|
||||||
|
|
||||||
### Removal of old JavaScript
|
### Removal of old JavaScript
|
||||||
|
|
||||||
|
|
@ -107,24 +108,25 @@ Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill
|
||||||
|
|
||||||
RainLoop 1.14 vs SnappyMail
|
RainLoop 1.14 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|----------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.130.942 | 652.764 |
|
|admin.js |2.130.942 | 652.023 |
|
||||||
|app.js |4.184.455 |2.319.847 |
|
|app.js |4.184.455 |2.310.715 |
|
||||||
|boot.js | 671.522 | 5.285 |
|
|boot.js | 671.522 | 5.285 |
|
||||||
|libs.js | 647.614 | 235.271 |
|
|libs.js | 647.614 | 235.271 |
|
||||||
|polyfills.js | 325.834 | 0 |
|
|polyfills.js | 325.834 | 0 |
|
||||||
|TOTAL |7.960.367 |3.213.167 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|
|TOTAL |7.960.367 |3.203.579 |
|
||||||
|
|
||||||
|js/min/* |RainLoop |Snappy |Rain gzip |gzip |brotli |
|
|js/min/* |RainLoop |Snappy |Rain gzip |gzip |brotli |
|
||||||
|--------------- |--------: |--------: |--------: |--------: |--------: |
|
|--------------- |--------: |--------: |--------: |--------: |--------: |
|
||||||
|admin.min.js | 252.147 | 90.573 | 73.657 | 23.735 | 20.767 |
|
|admin.min.js | 252.147 | 90.470 | 73.657 | 23.707 | 20.738 |
|
||||||
|app.min.js | 511.202 | 312.734 |140.462 | 83.425 | 67.822 |
|
|app.min.js | 511.202 | 310.166 |140.462 | 83.178 | 67.672 |
|
||||||
|boot.min.js | 66.007 | 2.918 | 22.567 | 1.500 | 1.275 |
|
|boot.min.js | 66.007 | 2.918 | 22.567 | 1.500 | 1.275 |
|
||||||
|libs.min.js | 572.545 | 130.767 |176.720 | 47.288 | 42.043 |
|
|libs.min.js | 572.545 | 130.767 |176.720 | 47.288 | 42.043 |
|
||||||
|polyfills.min.js | 32.452 | 0 | 11.312 | 0 | 0 |
|
|polyfills.min.js | 32.452 | 0 | 11.312 | 0 | 0 |
|
||||||
|TOTAL |1.434.353 | 536.992 |424.718 |155.948 |131.907 |
|
|TOTAL |1.434.353 | 534.321 |424.718 |155.673 |131.728 |
|
||||||
|TOTAL (no admin) |1.182.206 | 446.419 |351.061 |132.213 |111.140 |
|
|TOTAL (no admin) |1.182.206 | 443.851 |351.061 |131.966 |110.990 |
|
||||||
|
|
||||||
For a user its around 62% smaller and faster than traditional RainLoop.
|
For a user its around 62% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,128 @@
|
||||||
import * as Links from 'Common/Links';
|
import * as Links from 'Common/Links';
|
||||||
|
|
||||||
class Audio {
|
let notificator = null,
|
||||||
notificator = null;
|
player = null,
|
||||||
player = null;
|
canPlay = type => player && !!player.canPlayType(type).replace('no', ''),
|
||||||
|
|
||||||
supported = false;
|
audioCtx = AudioContext || window.webkitAudioContext,
|
||||||
supportedMp3 = false;
|
|
||||||
supportedOgg = false;
|
|
||||||
supportedWav = false;
|
|
||||||
supportedNotification = false;
|
|
||||||
|
|
||||||
constructor() {
|
play = (url, name) => {
|
||||||
this.player = this.createNewObject();
|
if (player) {
|
||||||
|
player.src = url;
|
||||||
// Safari can't play without user interaction
|
player.play();
|
||||||
this.supported = !!this.player && !!this.player.play;
|
name = name.trim();
|
||||||
if (this.supported && this.player && this.player.canPlayType) {
|
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'}));
|
||||||
this.supportedMp3 = !!this.player.canPlayType('audio/mpeg;').replace(/no/, '');
|
|
||||||
this.supportedWav = !!this.player.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
|
|
||||||
this.supportedOgg = !!this.player.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
|
|
||||||
this.supportedNotification = this.supported && this.supportedMp3;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
if (!this.player || (!this.supportedMp3 && !this.supportedOgg && !this.supportedWav)) {
|
createNewObject = () => {
|
||||||
this.supported = false;
|
|
||||||
this.supportedMp3 = false;
|
|
||||||
this.supportedOgg = false;
|
|
||||||
this.supportedWav = false;
|
|
||||||
this.supportedNotification = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.supported && this.player) {
|
|
||||||
const stopFn = () => this.stop();
|
|
||||||
|
|
||||||
this.player.addEventListener('ended', stopFn);
|
|
||||||
this.player.addEventListener('error', stopFn);
|
|
||||||
|
|
||||||
addEventListener('audio.api.stop', stopFn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewObject() {
|
|
||||||
try {
|
try {
|
||||||
const player = window.Audio ? new Audio() : null;
|
const player = new Audio;
|
||||||
if (player && player.canPlayType && player.pause && player.play) {
|
if (player.canPlayType && player.pause && player.play) {
|
||||||
player.preload = 'none';
|
player.preload = 'none';
|
||||||
player.loop = false;
|
player.loop = false;
|
||||||
player.autoplay = false;
|
player.autoplay = false;
|
||||||
player.muted = false;
|
player.muted = false;
|
||||||
|
return player;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
return player;
|
console.error(e);
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// The AudioContext is not allowed to start.
|
||||||
|
// It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu
|
||||||
|
// Setup listeners to attempt an unlock
|
||||||
|
unlockEvents = [
|
||||||
|
'click','dblclick',
|
||||||
|
'contextmenu',
|
||||||
|
'auxclick',
|
||||||
|
'mousedown','mouseup',
|
||||||
|
'pointerup',
|
||||||
|
'touchstart','touchend',
|
||||||
|
'keydown','keyup'
|
||||||
|
],
|
||||||
|
unlock = () => {
|
||||||
|
if (audioCtx) {
|
||||||
|
console.log('AudioContext ' + audioCtx.state);
|
||||||
|
audioCtx.resume();
|
||||||
|
}
|
||||||
|
unlockEvents.forEach(type => document.removeEventListener(type, unlock, true));
|
||||||
|
// setTimeout(()=>Audio.playNotification(1),1);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (audioCtx) {
|
||||||
|
audioCtx = audioCtx ? new audioCtx : null;
|
||||||
|
audioCtx.onstatechange = unlock;
|
||||||
|
}
|
||||||
|
unlockEvents.forEach(type => document.addEventListener(type, unlock, true));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browsers can't play without user interaction
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SMAudio = new class {
|
||||||
|
supported = false;
|
||||||
|
supportedMp3 = false;
|
||||||
|
supportedOgg = false;
|
||||||
|
supportedWav = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
player || (player = createNewObject());
|
||||||
|
|
||||||
|
this.supported = !!player;
|
||||||
|
if (player) {
|
||||||
|
this.supportedMp3 = !!canPlay('audio/mpeg;');
|
||||||
|
this.supportedWav = !!canPlay('audio/wav; codecs="1"');
|
||||||
|
this.supportedOgg = !!canPlay('audio/ogg; codecs="vorbis"');
|
||||||
|
|
||||||
|
const stopFn = () => this.pause();
|
||||||
|
player.addEventListener('ended', stopFn);
|
||||||
|
player.addEventListener('error', stopFn);
|
||||||
|
addEventListener('audio.api.stop', stopFn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
paused() {
|
paused() {
|
||||||
return this.supported ? !!this.player.paused : true;
|
return !player || player.paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this.supported && this.player.pause) {
|
this.pause();
|
||||||
this.player.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchEvent(new CustomEvent('audio.stop'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
this.stop();
|
player && player.pause();
|
||||||
}
|
dispatchEvent(new CustomEvent('audio.stop'));
|
||||||
|
|
||||||
clearName(name = '', ext = '') {
|
|
||||||
name = name.trim();
|
|
||||||
if (ext && '.' + ext === name.toLowerCase().substr((ext.length + 1) * -1)) {
|
|
||||||
name = name.substr(0, name.length - 4).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return name || 'audio';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playMp3(url, name) {
|
playMp3(url, name) {
|
||||||
if (this.supported && this.supportedMp3) {
|
this.supportedMp3 && play(url, name);
|
||||||
this.player.src = url;
|
|
||||||
this.player.play();
|
|
||||||
|
|
||||||
dispatchEvent(new CustomEvent('audio.start', {detail:this.clearName(name, 'mp3')}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playOgg(url, name) {
|
playOgg(url, name) {
|
||||||
if (this.supported && this.supportedOgg) {
|
this.supportedOgg && play(url, name);
|
||||||
this.player.src = url;
|
|
||||||
this.player.play();
|
|
||||||
|
|
||||||
name = this.clearName(name, 'oga');
|
|
||||||
name = this.clearName(name, 'ogg');
|
|
||||||
|
|
||||||
dispatchEvent(new CustomEvent('audio.start', {detail:name}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playWav(url, name) {
|
playWav(url, name) {
|
||||||
if (this.supported && this.supportedWav) {
|
this.supportedWav && play(url, name);
|
||||||
this.player.src = url;
|
|
||||||
this.player.play();
|
|
||||||
|
|
||||||
dispatchEvent(new CustomEvent('audio.start', {detail:this.clearName(name, 'wav')}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playNotification() {
|
playNotification(silent) {
|
||||||
if (this.supported && this.supportedMp3) {
|
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||||
if (!this.notificator) {
|
if (!notificator) {
|
||||||
this.notificator = this.createNewObject();
|
notificator = createNewObject();
|
||||||
this.notificator.src = Links.sound('new-mail.mp3');
|
notificator.src = Links.sound('new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
|
||||||
}
|
}
|
||||||
|
if (notificator) {
|
||||||
if (this.notificator && this.notificator.play) {
|
notificator.volume = silent ? 0.01 : 1;
|
||||||
this.notificator.play();
|
notificator.play();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No audio: ' + audioCtx.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default new Audio();
|
export default SMAudio;
|
||||||
|
|
|
||||||
|
|
@ -197,16 +197,6 @@ export const MessageSelectAction = {
|
||||||
Unflagged: 6
|
Unflagged: 6
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
export const DesktopNotification = {
|
|
||||||
Allowed: 0,
|
|
||||||
NotAllowed: 1,
|
|
||||||
Denied: 2,
|
|
||||||
NotSupported: 9
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,9 @@ class GeneralUserSettings {
|
||||||
this.layout = SettingsStore.layout;
|
this.layout = SettingsStore.layout;
|
||||||
this.usePreviewPane = SettingsStore.usePreviewPane;
|
this.usePreviewPane = SettingsStore.usePreviewPane;
|
||||||
|
|
||||||
this.soundNotificationIsSupported = NotificationStore.soundNotificationIsSupported;
|
|
||||||
this.enableSoundNotification = NotificationStore.enableSoundNotification;
|
this.enableSoundNotification = NotificationStore.enableSoundNotification;
|
||||||
|
|
||||||
this.enableDesktopNotification = NotificationStore.enableDesktopNotification;
|
this.enableDesktopNotification = NotificationStore.enableDesktopNotification;
|
||||||
this.isDesktopNotificationSupported = NotificationStore.isDesktopNotificationSupported;
|
|
||||||
this.isDesktopNotificationDenied = NotificationStore.isDesktopNotificationDenied;
|
this.isDesktopNotificationDenied = NotificationStore.isDesktopNotificationDenied;
|
||||||
|
|
||||||
this.showImages = SettingsStore.showImages;
|
this.showImages = SettingsStore.showImages;
|
||||||
|
|
@ -96,6 +94,10 @@ class GeneralUserSettings {
|
||||||
NotificationStore.playSoundNotification(true);
|
NotificationStore.playSoundNotification(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testSystemNotification() {
|
||||||
|
NotificationStore.displayDesktopNotification('SnappyMail', 'Test notification', { 'Folder': '', 'Uid': '' });
|
||||||
|
}
|
||||||
|
|
||||||
onBuild() {
|
onBuild() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const f0 = settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this),
|
const f0 = settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this),
|
||||||
|
|
@ -157,10 +159,6 @@ class GeneralUserSettings {
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
|
||||||
this.enableDesktopNotification.valueHasMutated();
|
|
||||||
}
|
|
||||||
|
|
||||||
selectLanguage() {
|
selectLanguage() {
|
||||||
showScreenPopup(require('View/Popup/Languages'), [this.language, this.languages(), LanguageStore.userLanguage()]);
|
showScreenPopup(require('View/Popup/Languages'), [this.language, this.languages(), LanguageStore.userLanguage()]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
clearNewMessageCache
|
clearNewMessageCache
|
||||||
} from 'Common/Cache';
|
} from 'Common/Cache';
|
||||||
|
|
||||||
import { mailBox, notificationMailIcon } from 'Common/Links';
|
import { mailBox } from 'Common/Links';
|
||||||
import { i18n, getNotification } from 'Common/Translator';
|
import { i18n, getNotification } from 'Common/Translator';
|
||||||
|
|
||||||
import { EmailCollectionModel } from 'Model/EmailCollection';
|
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||||
|
|
@ -245,7 +245,6 @@ class MessageUserStore {
|
||||||
const len = newMessages.length;
|
const len = newMessages.length;
|
||||||
if (3 < len) {
|
if (3 < len) {
|
||||||
NotificationStore.displayDesktopNotification(
|
NotificationStore.displayDesktopNotification(
|
||||||
notificationMailIcon(),
|
|
||||||
AccountStore.email(),
|
AccountStore.email(),
|
||||||
i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
|
i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
|
||||||
'COUNT': len
|
'COUNT': len
|
||||||
|
|
@ -255,7 +254,6 @@ class MessageUserStore {
|
||||||
} else {
|
} else {
|
||||||
newMessages.forEach(item => {
|
newMessages.forEach(item => {
|
||||||
NotificationStore.displayDesktopNotification(
|
NotificationStore.displayDesktopNotification(
|
||||||
notificationMailIcon(),
|
|
||||||
EmailCollectionModel.reviveFromJson(item.From).toString(),
|
EmailCollectionModel.reviveFromJson(item.From).toString(),
|
||||||
item.Subject,
|
item.Subject,
|
||||||
{ 'Folder': item.Folder, 'Uid': item.Uid }
|
{ 'Folder': item.Folder, 'Uid': item.Uid }
|
||||||
|
|
|
||||||
|
|
@ -1,154 +1,96 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { DesktopNotification } from 'Common/Enums';
|
|
||||||
import Audio from 'Common/Audio';
|
import Audio from 'Common/Audio';
|
||||||
|
import * as Links from 'Common/Links';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Might not work due to the new ServiceWorkerRegistration.showNotification
|
||||||
|
*/
|
||||||
|
const HTML5Notification = window.Notification ? Notification : null,
|
||||||
|
HTML5NotificationStatus = () => (HTML5Notification && HTML5Notification.permission) || 'denied',
|
||||||
|
dispatchMessage = data => {
|
||||||
|
focus();
|
||||||
|
if (data.Folder && data.Uid) {
|
||||||
|
dispatchEvent(new CustomEvent('mailbox.message.show', {detail:data}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let DesktopNotifications = false,
|
||||||
|
WorkerNotifications = navigator.serviceWorker;
|
||||||
|
|
||||||
|
// Are Notifications supported in the service worker?
|
||||||
|
if (WorkerNotifications && ServiceWorkerRegistration && ServiceWorkerRegistration.prototype.showNotification) {
|
||||||
|
console.log('ServiceWorker supported');
|
||||||
|
/* Listen for close requests from the ServiceWorker */
|
||||||
|
WorkerNotifications.addEventListener('message', event => {
|
||||||
|
const obj = JSON.parse(event.data);
|
||||||
|
obj && 'notificationclick' === obj.action && dispatchMessage(obj.data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
WorkerNotifications = null;
|
||||||
|
console.log('WorkerNotifications not supported');
|
||||||
|
}
|
||||||
|
|
||||||
class NotificationUserStore {
|
class NotificationUserStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.enableSoundNotification = ko.observable(false);
|
this.enableSoundNotification = ko.observable(false);
|
||||||
this.soundNotificationIsSupported = ko.observable(false);
|
|
||||||
|
|
||||||
this.allowDesktopNotification = ko.observable(false);
|
this.enableDesktopNotification = ko.observable(false)/*.extend({ notify: 'always' })*/;
|
||||||
|
|
||||||
this.desktopNotificationPermissions = ko
|
this.isDesktopNotificationDenied = ko.observable('denied' === HTML5NotificationStatus());
|
||||||
.computed(() => {
|
|
||||||
this.allowDesktopNotification();
|
|
||||||
|
|
||||||
let result = DesktopNotification.NotSupported;
|
this.enableDesktopNotification.subscribe(value => {
|
||||||
|
DesktopNotifications = !!value;
|
||||||
const NotificationClass = this.notificationClass();
|
if (value && HTML5Notification && 'granted' !== HTML5Notification.permission) {
|
||||||
if (NotificationClass && NotificationClass.permission) {
|
HTML5Notification.requestPermission(() =>
|
||||||
switch (NotificationClass.permission.toLowerCase()) {
|
this.isDesktopNotificationDenied('denied' === HTML5Notification.permission)
|
||||||
case 'granted':
|
);
|
||||||
result = DesktopNotification.Allowed;
|
}
|
||||||
break;
|
});
|
||||||
case 'denied':
|
|
||||||
result = DesktopNotification.Denied;
|
|
||||||
break;
|
|
||||||
case 'default':
|
|
||||||
result = DesktopNotification.NotAllowed;
|
|
||||||
break;
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
} else if (window.webkitNotifications && window.webkitNotifications.checkPermission) {
|
|
||||||
result = window.webkitNotifications.checkPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
})
|
|
||||||
.extend({ notify: 'always' });
|
|
||||||
|
|
||||||
this.enableDesktopNotification = ko
|
|
||||||
.computed({
|
|
||||||
read: () =>
|
|
||||||
this.allowDesktopNotification() && DesktopNotification.Allowed === this.desktopNotificationPermissions(),
|
|
||||||
write: (value) => {
|
|
||||||
if (value) {
|
|
||||||
const NotificationClass = this.notificationClass(),
|
|
||||||
permission = this.desktopNotificationPermissions();
|
|
||||||
|
|
||||||
if (NotificationClass && DesktopNotification.Allowed === permission) {
|
|
||||||
this.allowDesktopNotification(true);
|
|
||||||
} else if (NotificationClass && DesktopNotification.NotAllowed === permission) {
|
|
||||||
NotificationClass.requestPermission(() => {
|
|
||||||
this.allowDesktopNotification.valueHasMutated();
|
|
||||||
|
|
||||||
if (DesktopNotification.Allowed === this.desktopNotificationPermissions()) {
|
|
||||||
if (this.allowDesktopNotification()) {
|
|
||||||
this.allowDesktopNotification.valueHasMutated();
|
|
||||||
} else {
|
|
||||||
this.allowDesktopNotification(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.allowDesktopNotification()) {
|
|
||||||
this.allowDesktopNotification(false);
|
|
||||||
} else {
|
|
||||||
this.allowDesktopNotification.valueHasMutated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.allowDesktopNotification(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.allowDesktopNotification(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.extend({ notify: 'always' });
|
|
||||||
|
|
||||||
if (!this.enableDesktopNotification.valueHasMutated) {
|
|
||||||
this.enableDesktopNotification.valueHasMutated = () => {
|
|
||||||
this.allowDesktopNotification.valueHasMutated();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isDesktopNotificationSupported = ko.computed(
|
|
||||||
() => DesktopNotification.NotSupported !== this.desktopNotificationPermissions()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.isDesktopNotificationDenied = ko.computed(
|
|
||||||
() =>
|
|
||||||
DesktopNotification.NotSupported === this.desktopNotificationPermissions() ||
|
|
||||||
DesktopNotification.Denied === this.desktopNotificationPermissions()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.initNotificationPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
initNotificationPlayer() {
|
|
||||||
if (Audio && Audio.supportedNotification) {
|
|
||||||
this.soundNotificationIsSupported(true);
|
|
||||||
} else {
|
|
||||||
this.enableSoundNotification(false);
|
|
||||||
this.soundNotificationIsSupported(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used with SoundNotification setting
|
||||||
|
*/
|
||||||
playSoundNotification(skipSetting) {
|
playSoundNotification(skipSetting) {
|
||||||
if (Audio && Audio.supportedNotification && (skipSetting ? true : this.enableSoundNotification())) {
|
if (skipSetting ? true : this.enableSoundNotification()) {
|
||||||
Audio.playNotification();
|
Audio.playNotification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
displayDesktopNotification(imageSrc, title, text, messageData) {
|
/**
|
||||||
if (this.enableDesktopNotification()) {
|
* Used with DesktopNotifications setting
|
||||||
const NotificationClass = this.notificationClass(),
|
*/
|
||||||
notification = NotificationClass
|
displayDesktopNotification(title, text, messageData, imageSrc) {
|
||||||
? new NotificationClass(title, {
|
if (DesktopNotifications && 'granted' === HTML5NotificationStatus()) {
|
||||||
body: text,
|
const options = {
|
||||||
icon: imageSrc
|
body: text,
|
||||||
})
|
icon: imageSrc || Links.notificationMailIcon(),
|
||||||
: null;
|
data: messageData
|
||||||
|
};
|
||||||
if (notification) {
|
if (WorkerNotifications) {
|
||||||
if (notification.show) {
|
// Service-Worker-Allowed HTTP header to allow the scope.
|
||||||
notification.show();
|
WorkerNotifications.register('/serviceworker.js')
|
||||||
}
|
// WorkerNotifications.register(Links.staticPrefix('js/serviceworker.js'), {scope:'/'})
|
||||||
|
.then(() =>
|
||||||
if (messageData) {
|
WorkerNotifications.ready.then(registration =>
|
||||||
notification.onclick = () => {
|
/* Show the notification */
|
||||||
focus();
|
registration
|
||||||
|
.showNotification(title, options)
|
||||||
if (messageData.Folder && messageData.Uid) {
|
.then(() =>
|
||||||
dispatchEvent(new CustomEvent('mailbox.message.show', {detail:messageData}));
|
registration.getNotifications().then((/*notifications*/) => {
|
||||||
}
|
/* Send an empty message so the Worker knows who the client is */
|
||||||
};
|
registration.active.postMessage('');
|
||||||
}
|
})
|
||||||
|
)
|
||||||
setTimeout(
|
)
|
||||||
(function(localNotifications) {
|
)
|
||||||
return () => {
|
.catch(e => console.error(e));
|
||||||
if (localNotifications.cancel) {
|
} else {
|
||||||
localNotifications.cancel();
|
const notification = new HTML5Notification(title, options);
|
||||||
} else if (localNotifications.close) {
|
notification.show && notification.show();
|
||||||
localNotifications.close();
|
notification.onclick = messageData ? () => dispatchMessage(messageData) : null;
|
||||||
}
|
setTimeout(() => notification.close(), 7000);
|
||||||
};
|
|
||||||
})(notification),
|
|
||||||
7000
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,13 +99,6 @@ class NotificationUserStore {
|
||||||
this.enableSoundNotification(!!rl.settings.get('SoundNotification'));
|
this.enableSoundNotification(!!rl.settings.get('SoundNotification'));
|
||||||
this.enableDesktopNotification(!!rl.settings.get('DesktopNotifications'));
|
this.enableDesktopNotification(!!rl.settings.get('DesktopNotifications'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {*|null}
|
|
||||||
*/
|
|
||||||
notificationClass() {
|
|
||||||
return window.Notification && Notification.requestPermission ? Notification : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NotificationUserStore();
|
export default new NotificationUserStore();
|
||||||
|
|
|
||||||
15
dev/serviceworker.js
Normal file
15
dev/serviceworker.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
self.addEventListener('message', event => self.client = event.source);
|
||||||
|
|
||||||
|
const fn = event => {
|
||||||
|
self.client.postMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
data: event.notification.data,
|
||||||
|
action: event.type
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.onnotificationclose = fn;
|
||||||
|
self.onnotificationclick = fn;
|
||||||
|
|
@ -114,13 +114,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-horizontal hide-on-mobile" data-bind="visible: isDesktopNotificationSupported() || soundNotificationIsSupported()">
|
<div class="form-horizontal">
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<span class="i18n i18n-animation" data-i18n="SETTINGS_GENERAL/LABEL_NOTIFICATIONS"></span>
|
<span class="i18n i18n-animation" data-i18n="SETTINGS_GENERAL/LABEL_NOTIFICATIONS"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div data-bind="visible: isDesktopNotificationSupported">
|
<div>
|
||||||
<div data-bind="component: {
|
<div data-bind="component: {
|
||||||
name: 'Checkbox',
|
name: 'Checkbox',
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -131,11 +131,12 @@
|
||||||
}
|
}
|
||||||
}"></div>
|
}"></div>
|
||||||
|
|
||||||
<span data-bind="visible: isDesktopNotificationDenied">
|
<span data-bind="visible: isDesktopNotificationDenied" class="i18n" style="color: #999" data-i18n="SETTINGS_GENERAL/LABEL_CHROME_NOTIFICATION_DESC_DENIED"></span>
|
||||||
<span class="i18n" style="color: #999" data-i18n="SETTINGS_GENERAL/LABEL_CHROME_NOTIFICATION_DESC_DENIED"></span>
|
<span data-bind="click: testSystemNotification" style="color:green;cursor:pointer">
|
||||||
|
<i class="icon-right-dir iconsize20"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div data-bind="visible: soundNotificationIsSupported">
|
<div>
|
||||||
<div data-bind="component: {
|
<div data-bind="component: {
|
||||||
name: 'Checkbox',
|
name: 'Checkbox',
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -144,7 +145,7 @@
|
||||||
inline: true
|
inline: true
|
||||||
}
|
}
|
||||||
}"></div>
|
}"></div>
|
||||||
|
|
||||||
<span data-bind="click: testSoundNotification" style="color:green;cursor:pointer">
|
<span data-bind="click: testSoundNotification" style="color:green;cursor:pointer">
|
||||||
<i class="icon-right-dir iconsize20"></i>
|
<i class="icon-right-dir iconsize20"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,13 @@ const jsBoot = () => {
|
||||||
.pipe(gulp.dest('snappymail/v/' + config.devVersion + '/static/js'));
|
.pipe(gulp.dest('snappymail/v/' + config.devVersion + '/static/js'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ServiceWorker
|
||||||
|
const jsServiceWorker = () => {
|
||||||
|
return gulp
|
||||||
|
.src('dev/serviceworker.js')
|
||||||
|
.pipe(gulp.dest('snappymail/v/' + config.devVersion + '/static/js'));
|
||||||
|
};
|
||||||
|
|
||||||
// libs
|
// libs
|
||||||
const jsLibs = () => {
|
const jsLibs = () => {
|
||||||
const src = config.paths.js.libs.src;
|
const src = config.paths.js.libs.src;
|
||||||
|
|
@ -113,7 +120,7 @@ const jsLint = () =>
|
||||||
.pipe(eslint.failAfterError());
|
.pipe(eslint.failAfterError());
|
||||||
|
|
||||||
const jsState1 = gulp.series(jsLint);
|
const jsState1 = gulp.series(jsLint);
|
||||||
const jsState3 = gulp.parallel(jsBoot, jsLibs, jsApp, jsAdmin);
|
const jsState3 = gulp.parallel(jsBoot, jsServiceWorker, jsLibs, jsApp, jsAdmin);
|
||||||
const jsState2 = gulp.series(jsClean, webpack, jsState3, jsMin);
|
const jsState2 = gulp.series(jsClean, webpack, jsState3, jsMin);
|
||||||
|
|
||||||
exports.jsLint = jsLint;
|
exports.jsLint = jsLint;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue