mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-09-20 07:35:55 +08:00
Testing van be done at https://snappymail.eu/demo/
This commit is contained in:
parent
0ec37b7e90
commit
542d9c91e9
|
@ -26,9 +26,8 @@ RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
|||
Header set X-Content-Type-Options "nosniff"
|
||||
Header set X-Frame-Options "DENY"
|
||||
Header set X-XSS-Protection "1; mode=block"
|
||||
</IfModule>
|
||||
Header set Service-Worker-Allowed "/"
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
RewriteCond %{HTTP:Accept-encoding} br
|
||||
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
||||
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
|
||||
* JSON reviver
|
||||
* Better memory garbage collection management
|
||||
* Added serviceworker for Notifications
|
||||
|
||||
### 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
|
||||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|----------- |--------: |--------: |
|
||||
|admin.js |2.130.942 | 652.764 |
|
||||
|app.js |4.184.455 |2.319.847 |
|
||||
|boot.js | 671.522 | 5.285 |
|
||||
|libs.js | 647.614 | 235.271 |
|
||||
|polyfills.js | 325.834 | 0 |
|
||||
|TOTAL |7.960.367 |3.213.167 |
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.130.942 | 652.023 |
|
||||
|app.js |4.184.455 |2.310.715 |
|
||||
|boot.js | 671.522 | 5.285 |
|
||||
|libs.js | 647.614 | 235.271 |
|
||||
|polyfills.js | 325.834 | 0 |
|
||||
|serviceworker.js | 0 | 285 |
|
||||
|TOTAL |7.960.367 |3.203.579 |
|
||||
|
||||
|js/min/* |RainLoop |Snappy |Rain gzip |gzip |brotli |
|
||||
|--------------- |--------: |--------: |--------: |--------: |--------: |
|
||||
|admin.min.js | 252.147 | 90.573 | 73.657 | 23.735 | 20.767 |
|
||||
|app.min.js | 511.202 | 312.734 |140.462 | 83.425 | 67.822 |
|
||||
|admin.min.js | 252.147 | 90.470 | 73.657 | 23.707 | 20.738 |
|
||||
|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 |
|
||||
|libs.min.js | 572.545 | 130.767 |176.720 | 47.288 | 42.043 |
|
||||
|polyfills.min.js | 32.452 | 0 | 11.312 | 0 | 0 |
|
||||
|TOTAL |1.434.353 | 536.992 |424.718 |155.948 |131.907 |
|
||||
|TOTAL (no admin) |1.182.206 | 446.419 |351.061 |132.213 |111.140 |
|
||||
|TOTAL |1.434.353 | 534.321 |424.718 |155.673 |131.728 |
|
||||
|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.
|
||||
|
||||
|
|
|
@ -1,128 +1,128 @@
|
|||
import * as Links from 'Common/Links';
|
||||
|
||||
class Audio {
|
||||
notificator = null;
|
||||
player = null;
|
||||
let notificator = null,
|
||||
player = null,
|
||||
canPlay = type => player && !!player.canPlayType(type).replace('no', ''),
|
||||
|
||||
supported = false;
|
||||
supportedMp3 = false;
|
||||
supportedOgg = false;
|
||||
supportedWav = false;
|
||||
supportedNotification = false;
|
||||
audioCtx = AudioContext || window.webkitAudioContext,
|
||||
|
||||
constructor() {
|
||||
this.player = this.createNewObject();
|
||||
|
||||
// Safari can't play without user interaction
|
||||
this.supported = !!this.player && !!this.player.play;
|
||||
if (this.supported && this.player && this.player.canPlayType) {
|
||||
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;
|
||||
play = (url, name) => {
|
||||
if (player) {
|
||||
player.src = url;
|
||||
player.play();
|
||||
name = name.trim();
|
||||
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'}));
|
||||
}
|
||||
},
|
||||
|
||||
if (!this.player || (!this.supportedMp3 && !this.supportedOgg && !this.supportedWav)) {
|
||||
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() {
|
||||
createNewObject = () => {
|
||||
try {
|
||||
const player = window.Audio ? new Audio() : null;
|
||||
if (player && player.canPlayType && player.pause && player.play) {
|
||||
const player = new Audio;
|
||||
if (player.canPlayType && player.pause && player.play) {
|
||||
player.preload = 'none';
|
||||
player.loop = false;
|
||||
player.autoplay = false;
|
||||
player.muted = false;
|
||||
return player;
|
||||
}
|
||||
|
||||
return player;
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
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() {
|
||||
return this.supported ? !!this.player.paused : true;
|
||||
return !player || player.paused;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.supported && this.player.pause) {
|
||||
this.player.pause();
|
||||
}
|
||||
|
||||
dispatchEvent(new CustomEvent('audio.stop'));
|
||||
this.pause();
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.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';
|
||||
player && player.pause();
|
||||
dispatchEvent(new CustomEvent('audio.stop'));
|
||||
}
|
||||
|
||||
playMp3(url, name) {
|
||||
if (this.supported && this.supportedMp3) {
|
||||
this.player.src = url;
|
||||
this.player.play();
|
||||
|
||||
dispatchEvent(new CustomEvent('audio.start', {detail:this.clearName(name, 'mp3')}));
|
||||
}
|
||||
this.supportedMp3 && play(url, name);
|
||||
}
|
||||
|
||||
playOgg(url, name) {
|
||||
if (this.supported && this.supportedOgg) {
|
||||
this.player.src = url;
|
||||
this.player.play();
|
||||
|
||||
name = this.clearName(name, 'oga');
|
||||
name = this.clearName(name, 'ogg');
|
||||
|
||||
dispatchEvent(new CustomEvent('audio.start', {detail:name}));
|
||||
}
|
||||
this.supportedOgg && play(url, name);
|
||||
}
|
||||
|
||||
playWav(url, name) {
|
||||
if (this.supported && this.supportedWav) {
|
||||
this.player.src = url;
|
||||
this.player.play();
|
||||
|
||||
dispatchEvent(new CustomEvent('audio.start', {detail:this.clearName(name, 'wav')}));
|
||||
}
|
||||
this.supportedWav && play(url, name);
|
||||
}
|
||||
|
||||
playNotification() {
|
||||
if (this.supported && this.supportedMp3) {
|
||||
if (!this.notificator) {
|
||||
this.notificator = this.createNewObject();
|
||||
this.notificator.src = Links.sound('new-mail.mp3');
|
||||
playNotification(silent) {
|
||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||
if (!notificator) {
|
||||
notificator = createNewObject();
|
||||
notificator.src = Links.sound('new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
|
||||
}
|
||||
|
||||
if (this.notificator && this.notificator.play) {
|
||||
this.notificator.play();
|
||||
if (notificator) {
|
||||
notificator.volume = silent ? 0.01 : 1;
|
||||
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
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const DesktopNotification = {
|
||||
Allowed: 0,
|
||||
NotAllowed: 1,
|
||||
Denied: 2,
|
||||
NotSupported: 9
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
|
|
|
@ -30,11 +30,9 @@ class GeneralUserSettings {
|
|||
this.layout = SettingsStore.layout;
|
||||
this.usePreviewPane = SettingsStore.usePreviewPane;
|
||||
|
||||
this.soundNotificationIsSupported = NotificationStore.soundNotificationIsSupported;
|
||||
this.enableSoundNotification = NotificationStore.enableSoundNotification;
|
||||
|
||||
this.enableDesktopNotification = NotificationStore.enableDesktopNotification;
|
||||
this.isDesktopNotificationSupported = NotificationStore.isDesktopNotificationSupported;
|
||||
this.isDesktopNotificationDenied = NotificationStore.isDesktopNotificationDenied;
|
||||
|
||||
this.showImages = SettingsStore.showImages;
|
||||
|
@ -96,6 +94,10 @@ class GeneralUserSettings {
|
|||
NotificationStore.playSoundNotification(true);
|
||||
}
|
||||
|
||||
testSystemNotification() {
|
||||
NotificationStore.displayDesktopNotification('SnappyMail', 'Test notification', { 'Folder': '', 'Uid': '' });
|
||||
}
|
||||
|
||||
onBuild() {
|
||||
setTimeout(() => {
|
||||
const f0 = settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this),
|
||||
|
@ -157,10 +159,6 @@ class GeneralUserSettings {
|
|||
}, 50);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
this.enableDesktopNotification.valueHasMutated();
|
||||
}
|
||||
|
||||
selectLanguage() {
|
||||
showScreenPopup(require('View/Popup/Languages'), [this.language, this.languages(), LanguageStore.userLanguage()]);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
clearNewMessageCache
|
||||
} from 'Common/Cache';
|
||||
|
||||
import { mailBox, notificationMailIcon } from 'Common/Links';
|
||||
import { mailBox } from 'Common/Links';
|
||||
import { i18n, getNotification } from 'Common/Translator';
|
||||
|
||||
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||
|
@ -245,7 +245,6 @@ class MessageUserStore {
|
|||
const len = newMessages.length;
|
||||
if (3 < len) {
|
||||
NotificationStore.displayDesktopNotification(
|
||||
notificationMailIcon(),
|
||||
AccountStore.email(),
|
||||
i18n('MESSAGE_LIST/NEW_MESSAGE_NOTIFICATION', {
|
||||
'COUNT': len
|
||||
|
@ -255,7 +254,6 @@ class MessageUserStore {
|
|||
} else {
|
||||
newMessages.forEach(item => {
|
||||
NotificationStore.displayDesktopNotification(
|
||||
notificationMailIcon(),
|
||||
EmailCollectionModel.reviveFromJson(item.From).toString(),
|
||||
item.Subject,
|
||||
{ 'Folder': item.Folder, 'Uid': item.Uid }
|
||||
|
|
|
@ -1,154 +1,96 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { DesktopNotification } from 'Common/Enums';
|
||||
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 {
|
||||
constructor() {
|
||||
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
|
||||
.computed(() => {
|
||||
this.allowDesktopNotification();
|
||||
this.isDesktopNotificationDenied = ko.observable('denied' === HTML5NotificationStatus());
|
||||
|
||||
let result = DesktopNotification.NotSupported;
|
||||
|
||||
const NotificationClass = this.notificationClass();
|
||||
if (NotificationClass && NotificationClass.permission) {
|
||||
switch (NotificationClass.permission.toLowerCase()) {
|
||||
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);
|
||||
}
|
||||
this.enableDesktopNotification.subscribe(value => {
|
||||
DesktopNotifications = !!value;
|
||||
if (value && HTML5Notification && 'granted' !== HTML5Notification.permission) {
|
||||
HTML5Notification.requestPermission(() =>
|
||||
this.isDesktopNotificationDenied('denied' === HTML5Notification.permission)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used with SoundNotification setting
|
||||
*/
|
||||
playSoundNotification(skipSetting) {
|
||||
if (Audio && Audio.supportedNotification && (skipSetting ? true : this.enableSoundNotification())) {
|
||||
if (skipSetting ? true : this.enableSoundNotification()) {
|
||||
Audio.playNotification();
|
||||
}
|
||||
}
|
||||
|
||||
displayDesktopNotification(imageSrc, title, text, messageData) {
|
||||
if (this.enableDesktopNotification()) {
|
||||
const NotificationClass = this.notificationClass(),
|
||||
notification = NotificationClass
|
||||
? new NotificationClass(title, {
|
||||
body: text,
|
||||
icon: imageSrc
|
||||
})
|
||||
: null;
|
||||
|
||||
if (notification) {
|
||||
if (notification.show) {
|
||||
notification.show();
|
||||
}
|
||||
|
||||
if (messageData) {
|
||||
notification.onclick = () => {
|
||||
focus();
|
||||
|
||||
if (messageData.Folder && messageData.Uid) {
|
||||
dispatchEvent(new CustomEvent('mailbox.message.show', {detail:messageData}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setTimeout(
|
||||
(function(localNotifications) {
|
||||
return () => {
|
||||
if (localNotifications.cancel) {
|
||||
localNotifications.cancel();
|
||||
} else if (localNotifications.close) {
|
||||
localNotifications.close();
|
||||
}
|
||||
};
|
||||
})(notification),
|
||||
7000
|
||||
);
|
||||
/**
|
||||
* Used with DesktopNotifications setting
|
||||
*/
|
||||
displayDesktopNotification(title, text, messageData, imageSrc) {
|
||||
if (DesktopNotifications && 'granted' === HTML5NotificationStatus()) {
|
||||
const options = {
|
||||
body: text,
|
||||
icon: imageSrc || Links.notificationMailIcon(),
|
||||
data: messageData
|
||||
};
|
||||
if (WorkerNotifications) {
|
||||
// Service-Worker-Allowed HTTP header to allow the scope.
|
||||
WorkerNotifications.register('/serviceworker.js')
|
||||
// WorkerNotifications.register(Links.staticPrefix('js/serviceworker.js'), {scope:'/'})
|
||||
.then(() =>
|
||||
WorkerNotifications.ready.then(registration =>
|
||||
/* Show the notification */
|
||||
registration
|
||||
.showNotification(title, options)
|
||||
.then(() =>
|
||||
registration.getNotifications().then((/*notifications*/) => {
|
||||
/* Send an empty message so the Worker knows who the client is */
|
||||
registration.active.postMessage('');
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.catch(e => console.error(e));
|
||||
} else {
|
||||
const notification = new HTML5Notification(title, options);
|
||||
notification.show && notification.show();
|
||||
notification.onclick = messageData ? () => dispatchMessage(messageData) : null;
|
||||
setTimeout(() => notification.close(), 7000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,13 +99,6 @@ class NotificationUserStore {
|
|||
this.enableSoundNotification(!!rl.settings.get('SoundNotification'));
|
||||
this.enableDesktopNotification(!!rl.settings.get('DesktopNotifications'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {*|null}
|
||||
*/
|
||||
notificationClass() {
|
||||
return window.Notification && Notification.requestPermission ? Notification : null;
|
||||
}
|
||||
}
|
||||
|
||||
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 class="form-horizontal hide-on-mobile" data-bind="visible: isDesktopNotificationSupported() || soundNotificationIsSupported()">
|
||||
<div class="form-horizontal">
|
||||
<div class="legend">
|
||||
<span class="i18n i18n-animation" data-i18n="SETTINGS_GENERAL/LABEL_NOTIFICATIONS"></span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<div data-bind="visible: isDesktopNotificationSupported">
|
||||
<div>
|
||||
<div data-bind="component: {
|
||||
name: 'Checkbox',
|
||||
params: {
|
||||
|
@ -131,11 +131,12 @@
|
|||
}
|
||||
}"></div>
|
||||
|
||||
<span data-bind="visible: isDesktopNotificationDenied">
|
||||
<span class="i18n" style="color: #999" data-i18n="SETTINGS_GENERAL/LABEL_CHROME_NOTIFICATION_DESC_DENIED"></span>
|
||||
<span data-bind="visible: isDesktopNotificationDenied" 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>
|
||||
</div>
|
||||
<div data-bind="visible: soundNotificationIsSupported">
|
||||
<div>
|
||||
<div data-bind="component: {
|
||||
name: 'Checkbox',
|
||||
params: {
|
||||
|
@ -144,7 +145,7 @@
|
|||
inline: true
|
||||
}
|
||||
}"></div>
|
||||
|
||||
|
||||
<span data-bind="click: testSoundNotification" style="color:green;cursor:pointer">
|
||||
<i class="icon-right-dir iconsize20"></i>
|
||||
</span>
|
||||
|
|
|
@ -30,6 +30,13 @@ const jsBoot = () => {
|
|||
.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
|
||||
const jsLibs = () => {
|
||||
const src = config.paths.js.libs.src;
|
||||
|
@ -113,7 +120,7 @@ const jsLint = () =>
|
|||
.pipe(eslint.failAfterError());
|
||||
|
||||
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);
|
||||
|
||||
exports.jsLint = jsLint;
|
||||
|
|
Loading…
Reference in a new issue