diff --git a/.htaccess b/.htaccess index 0c2db6f7e..49e532bc1 100644 --- a/.htaccess +++ b/.htaccess @@ -1,6 +1,8 @@ -RewriteEngine On -# Redirect cPanel -RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301] + + RewriteEngine On + # Redirect cPanel + RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301] + ExpiresActive On diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2cda2472..b712239f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ 1. Install node.js - `https://nodejs.org/download/` 2. Install yarn - `https://yarnpkg.com/en/docs/install` 3. Install gulp - `npm install gulp -g` -4. Fork snappymail - `https://github.com/the-djmaze/snappymail/issues/new#fork-destination-box` +4. Fork snappymail from https://github.com/the-djmaze/snappymail 5. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail` 6. `cd snappymail` 7. Install install all dependencies - `yarn install` diff --git a/README.md b/README.md index ac12eb8b9..f2d17cdc2 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ This fork of RainLoop has the following changes: * Replaced old Sabre library * AddressBook Contacts support MySQL/MariaDB utf8mb4 * Prevent Google FLoC +* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks ### Removal of old JavaScript @@ -119,25 +120,25 @@ RainLoop 1.15 vs SnappyMail |js/* |RainLoop |Snappy | |--------------- |--------: |--------: | -|admin.js |2.158.025 | 91.664 | -|app.js |4.215.733 | 453.928 | +|admin.js |2.158.025 | 91.364 | +|app.js |4.215.733 | 454.661 | |boot.js | 672.433 | 3.147 | -|libs.js | 647.679 | 218.466 | +|libs.js | 647.679 | 218.491 | |polyfills.js | 325.908 | 0 | |serviceworker.js | 0 | 285 | -|TOTAL |8.019.778 | 767.490 | +|TOTAL |8.019.778 | 767.663 | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |--------------- |--------: |--------: |------: |------: |--------: |--------: | -|admin.min.js | 255.514 | 47.908 | 73.899 | 14.313 | 60.674 | 12.787 | -|app.min.js | 516.000 | 233.532 |140.430 | 68.577 |110.657 | 57.878 | +|admin.min.js | 255.514 | 47.527 | 73.899 | 14.263 | 60.674 | 12.750 | +|app.min.js | 516.000 | 233.755 |140.430 | 68.663 |110.657 | 58.026 | |boot.min.js | 66.456 | 1.751 | 22.553 | 1.025 | 20.043 | 858 | -|libs.min.js | 574.626 | 106.529 |177.280 | 38.607 |151.855 | 34.567 | +|libs.min.js | 574.626 | 106.543 |177.280 | 38.613 |151.855 | 34.543 | |polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 | -|TOTAL |1.445.204 | 389.720 |425.477 |122.522 |353.301 |106.090 | -|TOTAL (no admin) |1.189.690 | 341.812 |351.061 |108.209 |292.627 | 93.303 | +|TOTAL |1.445.204 | 389.576 |425.477 |122.564 |353.301 |106.177 | +|TOTAL (no admin) |1.189.690 | 342.049 |351.061 |108.301 |292.627 | 93.427 | -For a user its around 68% smaller and faster than traditional RainLoop. +For a user its around 69% smaller and faster than traditional RainLoop. |OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |--------------- |--------: |--------: |------: |------: |--------: |--------: | @@ -173,13 +174,13 @@ For a user its around 68% smaller and faster than traditional RainLoop. |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | -|------------ |-------: |-------: |------: |------: |--------: | -|app.css | 340.334 | 100.825 | 46,959 | 18.002 | 15.540 | -|app.min.css | 274.791 | 82.684 | 39.618 | 16.138 | 14.234 | -|boot.css | | 1.326 | | 664 | 545 | -|boot.min.css | | 1.071 | | 590 | 474 | -|admin.css | | 42.217 | | 8.714 | 7.602 | -|admin.min.css | | 33.576 | | 7.680 | 6.810 | +|------------ |-------: |------: |------: |------: |--------: | +|app.css | 340.334 | 98.499 | 46,959 | 17.842 | 15.378 | +|app.min.css | 274.791 | 81.603 | 39.618 | 15.982 | 14.056 | +|boot.css | | 1.326 | | 664 | 545 | +|boot.min.css | | 1.071 | | 590 | 474 | +|admin.css | | 40.516 | | 8.481 | 7.401 | +|admin.min.css | | 32.041 | | 7.447 | 6.610 | ### Squire vs CKEditor diff --git a/assets/sounds/alert.mp3 b/assets/sounds/alert.mp3 new file mode 100644 index 000000000..f1779dfe9 Binary files /dev/null and b/assets/sounds/alert.mp3 differ diff --git a/assets/sounds/alert.ogg b/assets/sounds/alert.ogg new file mode 100644 index 000000000..e848b3f42 Binary files /dev/null and b/assets/sounds/alert.ogg differ diff --git a/assets/sounds/ping.mp3 b/assets/sounds/ping.mp3 new file mode 100644 index 000000000..52885ece8 Binary files /dev/null and b/assets/sounds/ping.mp3 differ diff --git a/assets/sounds/ping.ogg b/assets/sounds/ping.ogg new file mode 100644 index 000000000..6620ded87 Binary files /dev/null and b/assets/sounds/ping.ogg differ diff --git a/dev/App/User.js b/dev/App/User.js index 82f319fa2..2d0802b08 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -38,7 +38,6 @@ import { } from 'Common/Cache'; import { - userBackground, mailBox, root, openPgpWorkerJs, @@ -81,6 +80,8 @@ import { ComposePopupView } from 'View/Popup/Compose'; import { FolderSystemPopupView } from 'View/Popup/FolderSystem'; import { AskPopupView } from 'View/Popup/Ask'; +import { timeToNode } from 'Common/Momentor'; + // Every 5 minutes const refreshFolders = 300000; @@ -110,16 +111,6 @@ class AppUser extends AbstractApp { lastTime = currentTime; }, interval); - if (SettingsGet('UserBackgroundHash')) { - setTimeout(() => { - const img = userBackground(SettingsGet('UserBackgroundHash')); - if (img) { - $htmlCL.add('UserBackground'); - doc.body.style.backgroundImage = "url("+img+")"; - } - }, 1000); - } - const fn = (ev=>$htmlCL.toggle('rl-ctrl-key-pressed', ev.ctrlKey)).debounce(500); ['keydown','keyup'].forEach(t => doc.addEventListener(t, fn)); @@ -1020,7 +1011,14 @@ class AppUser extends AbstractApp { this.hideLoading(); } - setInterval(() => dispatchEvent(new CustomEvent('reload-time')), 60000); + setInterval(this.reloadTime(), 60000); + } + + reloadTime() + { + setTimeout(() => + doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element)) + , 1) } showMessageComposer(params = []) diff --git a/dev/Common/Audio.js b/dev/Common/Audio.js index d6d9f5aeb..26660d695 100644 --- a/dev/Common/Audio.js +++ b/dev/Common/Audio.js @@ -1,5 +1,5 @@ import * as Links from 'Common/Links'; -import { doc } from 'Common/Globals'; +import { doc, SettingsGet } from 'Common/Globals'; let notificator = null, player = null, @@ -106,11 +106,11 @@ export const SMAudio = new class { playNotification(silent) { if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) { - if (!notificator) { - notificator = createNewObject(); - notificator.src = Links.staticLink('sounds/new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg')); - } + notificator = notificator || createNewObject(); if (notificator) { + notificator.src = Links.staticLink('sounds/' + + SettingsGet('NotificationSound') + + (this.supportedMp3 ? '.mp3' : '.ogg')); notificator.volume = silent ? 0.01 : 1; notificator.play(); } diff --git a/dev/Common/Links.js b/dev/Common/Links.js index dc726351f..b5edfa99f 100644 --- a/dev/Common/Links.js +++ b/dev/Common/Links.js @@ -70,14 +70,6 @@ export function change(email) { return serverRequest('Change') + encodeURIComponent(email) + '/'; } -/** - * @param {string} hash - * @returns {string} - */ -export function userBackground(hash) { - return serverRequestRaw('UserBackground', hash); -} - /** * @param {string} lang * @param {boolean} isAdmin diff --git a/dev/Common/Momentor.js b/dev/Common/Momentor.js index 6da0a88ce..aa6efed35 100644 --- a/dev/Common/Momentor.js +++ b/dev/Common/Momentor.js @@ -1,4 +1,3 @@ -import { doc } from 'Common/Globals'; import { i18n } from 'Common/Translator'; export function timestampToString(timeStampInUTC, formatStr) { @@ -53,8 +52,3 @@ export function timeToNode(element, time) { console.error(e); } } - -addEventListener('reload-time', () => setTimeout(() => - doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element)) - , 1) -); diff --git a/dev/Common/Selector.js b/dev/Common/Selector.js index 109b30135..bd01d203a 100644 --- a/dev/Common/Selector.js +++ b/dev/Common/Selector.js @@ -1,6 +1,15 @@ import ko from 'ko'; import { isArray } from 'Common/Utils'; +/* + oCallbacks: + ItemSelect + MiddleClick + AutoSelect + ItemGetUid + UpOrDown +*/ + export class Selector { /** * @param {koProperty} koList @@ -209,11 +218,9 @@ export class Selector { itemSelected(item) { if (this.isListChecked()) { - if (!item) { - (this.oCallbacks.onItemSelect || (()=>{}))(item || null); - } + item || (this.oCallbacks.ItemSelect || (()=>{}))(null); } else if (item) { - (this.oCallbacks.onItemSelect || (()=>{}))(item); + (this.oCallbacks.ItemSelect || (()=>{}))(item); } } @@ -226,20 +233,32 @@ export class Selector { this.oContentScrollable = contentScrollable; if (contentScrollable) { + let getItem = selector => { + let el = event.target.closestWithin(selector, contentScrollable); + return el ? ko.dataFor(el) : null; + }; + contentScrollable.addEventListener('click', event => { let el = event.target.closestWithin(this.sItemSelector, contentScrollable); el && this.actionClick(ko.dataFor(el), event); - el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable); - if (el) { - const item = ko.dataFor(el); + const item = getItem(this.sItemCheckedSelector); + if (item) { + if (event.shiftKey) { + this.actionClick(item, event); + } else { + this.focusedItem(item); + item.checked(!item.checked()); + } + } + }); + + contentScrollable.addEventListener('auxclick', event => { + if (1 == event.button) { + const item = getItem(this.sItemSelector); if (item) { - if (event.shiftKey) { - this.actionClick(item, event); - } else { - this.focusedItem(item); - item.checked(!item.checked()); - } + this.focusedItem(item); + (this.oCallbacks.MiddleClick || (()=>{}))(item); } } }); @@ -271,7 +290,7 @@ export class Selector { * @returns {boolean} */ autoSelect() { - return !!(this.oCallbacks.onAutoSelect || (()=>true))(); + return !!(this.oCallbacks.AutoSelect || (()=>true))(); } /** @@ -281,7 +300,7 @@ export class Selector { getItemUid(item) { let uid = ''; - const getItemUidCallback = this.oCallbacks.onItemGetUid || null; + const getItemUidCallback = this.oCallbacks.ItemGetUid || null; if (getItemUidCallback && item) { uid = getItemUidCallback(item); } @@ -312,7 +331,7 @@ export class Selector { } else if (++i < listLen) { result = list[i]; } - result || (this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey); + result || (this.oCallbacks.UpOrDown || (()=>true))('ArrowUp' === sEventKey); } else if ('Home' === sEventKey) { result = list[0]; } else if ('End' === sEventKey) { diff --git a/dev/Common/Translator.js b/dev/Common/Translator.js index ba9728276..dd2d81bb6 100644 --- a/dev/Common/Translator.js +++ b/dev/Common/Translator.js @@ -140,7 +140,7 @@ export function reload(admin, language) { // reload the data if (init()) { i18nToNodes(doc); - dispatchEvent(new CustomEvent('reload-time')); + admin || rl.app.reloadTime(); trigger(!trigger()); } script.remove(); diff --git a/dev/Knoin/AbstractViews.js b/dev/Knoin/AbstractViews.js index 42df779c0..4affd299e 100644 --- a/dev/Knoin/AbstractViews.js +++ b/dev/Knoin/AbstractViews.js @@ -38,9 +38,6 @@ class AbstractView { keyScope(this.sCurrentScope); } - cancelCommand() {} - closeCommand() {} - querySelector(selectors) { return this.viewModelDom.querySelector(selectors); } @@ -68,7 +65,10 @@ export class AbstractViewPopup extends AbstractView this.sDefaultScope = Scope[name]; } } - +/* + cancelCommand() {} + closeCommand() {} +*/ /** * @returns {void} */ @@ -76,7 +76,7 @@ export class AbstractViewPopup extends AbstractView addEventListener('keydown', event => { if (event && this.modalVisibility && this.modalVisibility()) { if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) { - this.cancelCommand && this.cancelCommand(); + this.cancelCommand(); return false; } else if ('Backspace' == event.key && !inFocus()) { return false; diff --git a/dev/Model/Message.js b/dev/Model/Message.js index a5b4b0dd1..9e31a2515 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -524,13 +524,6 @@ export class MessageModel extends AbstractModel { } } - storeDataInDom() { - if (this.body) { - this.body.rlIsHtml = !!this.isHtml(); - this.body.rlHasImages = !!this.hasImages(); - } - } - fetchDataFromDom() { if (this.body) { this.isHtml(!!this.body.rlIsHtml); diff --git a/dev/Remote/Admin/Fetch.js b/dev/Remote/Admin/Fetch.js index 5d0922ac6..dc0ded270 100644 --- a/dev/Remote/Admin/Fetch.js +++ b/dev/Remote/Admin/Fetch.js @@ -38,13 +38,6 @@ class RemoteAdminFetch extends AbstractFetchRemote { }); } - /** - * @param {?Function} fCallback - */ - pluginList(fCallback) { - this.defaultRequest(fCallback, 'AdminPluginList'); - } - /** * @param {?Function} fCallback */ diff --git a/dev/Screen/Admin/Settings.js b/dev/Screen/Admin/Settings.js index 31a1f64bd..4b47b4db6 100644 --- a/dev/Screen/Admin/Settings.js +++ b/dev/Screen/Admin/Settings.js @@ -7,7 +7,6 @@ import { DomainsAdminSettings } from 'Settings/Admin/Domains'; import { LoginAdminSettings } from 'Settings/Admin/Login'; import { ContactsAdminSettings } from 'Settings/Admin/Contacts'; import { SecurityAdminSettings } from 'Settings/Admin/Security'; -import { PluginsAdminSettings } from 'Settings/Admin/Plugins'; import { PackagesAdminSettings } from 'Settings/Admin/Packages'; import { AboutAdminSettings } from 'Settings/Admin/About'; import { BrandingAdminSettings } from 'Settings/Admin/Branding'; @@ -38,7 +37,6 @@ export class SettingsAdminScreen extends AbstractSettingsScreen { [BrandingAdminSettings, 'Branding'], [ContactsAdminSettings, 'Contacts'], [SecurityAdminSettings, 'Security'], - [PluginsAdminSettings, 'Plugins'], [PackagesAdminSettings, 'Packages'], [AboutAdminSettings, 'About'], ].forEach(item => diff --git a/dev/Settings/Admin/Packages.js b/dev/Settings/Admin/Packages.js index f2d04f6c1..6c1e9ab19 100644 --- a/dev/Settings/Admin/Packages.js +++ b/dev/Settings/Admin/Packages.js @@ -6,6 +6,10 @@ import { getNotification } from 'Common/Translator'; import { PackageAdminStore } from 'Stores/Admin/Package'; import Remote from 'Remote/Admin/Fetch'; +import { showScreenPopup } from 'Knoin/Knoin'; +import { PluginPopupView } from 'View/Popup/Plugin'; +import { SettingsGet } from 'Common/Globals'; + export class PackagesAdminSettings { constructor() { this.packagesError = ko.observable(''); @@ -13,24 +17,42 @@ export class PackagesAdminSettings { this.packages = PackageAdminStore; this.packagesCurrent = ko.computed(() => - PackageAdminStore.filter(item => item && item.installed && !item.compare) + PackageAdminStore.filter(item => item && item.installed && !item.canBeUpdated) ); this.packagesAvailableForUpdate = ko.computed(() => - PackageAdminStore.filter(item => item && item.installed && !!item.compare) + PackageAdminStore.filter(item => item && item.installed && !!item.canBeUpdated) ); this.packagesAvailableForInstallation = ko.computed(() => PackageAdminStore.filter(item => item && !item.installed) ); this.visibility = ko.computed(() => (PackageAdminStore.loading() ? 'visible' : 'hidden')); + + this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins')); + this.enabledPlugins.subscribe(value => + Remote.saveAdminConfig(null, { + EnabledPlugins: value ? 1 : 0 + }) + ); } onShow() { this.packagesError(''); } - onBuild() { + onBuild(oDom) { PackageAdminStore.fetch(); + + oDom.addEventListener('click', event => { + // configurePlugin + let el = event.target.closestWithin('.package-configure', oDom), + data = el ? ko.dataFor(el) : 0; + data && Remote.plugin((iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]), data.name) + // disablePlugin + el = event.target.closestWithin('.package-active', oDom); + data = el ? ko.dataFor(el) : 0; + data && this.disablePlugin(data); + }); } requestHelper(packageToRequest, install) { @@ -70,4 +92,20 @@ export class PackagesAdminSettings { Remote.packageInstall(this.requestHelper(packageToInstall, true), packageToInstall); } } + + disablePlugin(plugin) { + let b = !plugin.enabled(); + plugin.enabled(b); + Remote.pluginDisable((iError, data) => { + if (iError) { + this.packagesError( + (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) + ? data.ErrorMessage + : getNotification(iError) + ); + } +// PackageAdminStore.fetch(); + }, plugin.name, !b); + } + } diff --git a/dev/Settings/Admin/Plugins.js b/dev/Settings/Admin/Plugins.js deleted file mode 100644 index 6ea4f4e05..000000000 --- a/dev/Settings/Admin/Plugins.js +++ /dev/null @@ -1,63 +0,0 @@ -import ko from 'ko'; - -import { Notification } from 'Common/Enums'; -import { SettingsGet } from 'Common/Globals'; -import { getNotification } from 'Common/Translator'; - -import { showScreenPopup } from 'Knoin/Knoin'; - -import { PluginAdminStore } from 'Stores/Admin/Plugin'; - -import Remote from 'Remote/Admin/Fetch'; - -import { PluginPopupView } from 'View/Popup/Plugin'; - -export class PluginsAdminSettings -{ - constructor() { - this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins')); - - this.plugins = PluginAdminStore; - this.pluginsError = PluginAdminStore.error; - - this.enabledPlugins.subscribe(value => - Remote.saveAdminConfig(null, { - EnabledPlugins: value ? 1 : 0 - }) - ); - } - - disablePlugin(plugin) { - let b = !plugin.disabled(); - plugin.disabled(b); - Remote.pluginDisable((iError, data) => { - if (iError) { - PluginAdminStore.error( - (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) - ? data.ErrorMessage - : getNotification(iError) - ); - } - PluginAdminStore.fetch(); - }, plugin.name, b); - } - - configurePlugin(plugin) { - Remote.plugin((iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]), plugin.name); - } - - onBuild(oDom) { - oDom.addEventListener('click', event => { - let el = event.target.closestWithin('.configure-plugin-action', oDom); - el && ko.dataFor(el) && this.configurePlugin(ko.dataFor(el)); - - el = event.target.closestWithin('.disabled-plugin', oDom); - el && ko.dataFor(el) && this.disablePlugin(ko.dataFor(el)); - }); - } - - onShow() { - PluginAdminStore.error(''); - PluginAdminStore.fetch(); - } -} diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js index ed49b0af6..9df238ca7 100644 --- a/dev/Settings/User/General.js +++ b/dev/Settings/User/General.js @@ -32,6 +32,8 @@ export class GeneralUserSettings { this.layout = SettingsUserStore.layout; this.enableSoundNotification = NotificationUserStore.enableSoundNotification; + this.notificationSound = ko.observable(SettingsGet('NotificationSound')); + this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds')); this.enableDesktopNotification = NotificationUserStore.enableDesktopNotification; this.isDesktopNotificationDenied = NotificationUserStore.isDesktopNotificationDenied; @@ -115,6 +117,10 @@ export class GeneralUserSettings { enableDesktopNotification: value => Remote.saveSetting('DesktopNotifications', value ? 1 : 0), enableSoundNotification: value => Remote.saveSetting('SoundNotification', value ? 1 : 0), + notificationSound: value => { + Remote.saveSetting('NotificationSound', value); + rl.settings.set('NotificationSound', value); + }, replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0), diff --git a/dev/Settings/User/Themes.js b/dev/Settings/User/Themes.js index bc5d9ee69..2a0b82531 100644 --- a/dev/Settings/User/Themes.js +++ b/dev/Settings/User/Themes.js @@ -2,9 +2,9 @@ import ko from 'ko'; import { SaveSettingsStep, UploadErrorCode, Capa } from 'Common/Enums'; import { changeTheme, convertThemeName } from 'Common/Utils'; -import { userBackground, themePreviewLink, serverRequest } from 'Common/Links'; +import { themePreviewLink, serverRequest } from 'Common/Links'; import { i18n } from 'Common/Translator'; -import { doc, $htmlCL, Settings } from 'Common/Globals'; +import { Settings } from 'Common/Globals'; import { ThemeStore } from 'Stores/Theme'; @@ -38,16 +38,6 @@ export class ThemesUserSettings { Theme: value }); }); - - this.background.hash.subscribe((value) => { - if (!value) { - $htmlCL.remove('UserBackground'); - doc.body.removeAttribute('style'); - } else { - $htmlCL.add('UserBackground'); - doc.body.style.backgroundImage = "url("+userBackground(value)+")"; - } - }); } onBuild() { diff --git a/dev/Stores/Admin/Package.js b/dev/Stores/Admin/Package.js index 2753dea47..bc824527f 100644 --- a/dev/Stores/Admin/Package.js +++ b/dev/Stores/Admin/Package.js @@ -8,35 +8,34 @@ PackageAdminStore.real = ko.observable(true); PackageAdminStore.loading = ko.observable(false); +//PackageAdminStore.error = ko.observable(''); + PackageAdminStore.fetch = () => { PackageAdminStore.loading(true); Remote.packagesList((iError, data) => { PackageAdminStore.loading(false); - if (!iError) { + if (iError) { + PackageAdminStore.real(false); + } else { PackageAdminStore.real(!!data.Result.Real); - let list = []; const loading = {}; - PackageAdminStore.forEach(item => { if (item && item.loading()) { loading[item.file] = item; } }); + let list = []; if (isArray(data.Result.List)) { - list = data.Result.List.map(item => { - if (item) { - item.loading = ko.observable(loading[item.file] !== undefined); - return 'core' === item.type && !item.canBeInstalled ? null : item; - } - return null; - }).filter(v => v); + list = data.Result.List.filter(v => v).map(item => { + item.loading = ko.observable(loading[item.file] !== undefined); + item.enabled = ko.observable(item.enabled); + return item; + }); } PackageAdminStore(list); - } else { - PackageAdminStore.real(false); } }); }; diff --git a/dev/Stores/Admin/Plugin.js b/dev/Stores/Admin/Plugin.js deleted file mode 100644 index 6b4bc50e2..000000000 --- a/dev/Stores/Admin/Plugin.js +++ /dev/null @@ -1,24 +0,0 @@ -import ko from 'ko'; -import Remote from 'Remote/Admin/Fetch'; - -export const PluginAdminStore = ko.observableArray(); - -PluginAdminStore.loading = ko.observable(false); - -PluginAdminStore.error = ko.observable(''); - -PluginAdminStore.fetch = () => { - PluginAdminStore.loading(true); - Remote.pluginList((iError, data) => { - PluginAdminStore.loading(false); - if (!iError) { - PluginAdminStore( - data.Result.map(item => ({ - name: item.Name, - disabled: ko.observable(!item.Enabled), - configured: ko.observable(!!item.Configured) - })) - ); - } - }); -}; diff --git a/dev/Stores/Theme.js b/dev/Stores/Theme.js index 3a11d63c9..95f17f13a 100644 --- a/dev/Stores/Theme.js +++ b/dev/Stores/Theme.js @@ -1,6 +1,7 @@ import ko from 'ko'; -import { $htmlCL, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals'; +import { doc, $htmlCL, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals'; import { isArray } from 'Common/Utils'; +import { serverRequestRaw } from 'Common/Links'; export const ThemeStore = { themes: ko.observableArray(), @@ -25,3 +26,13 @@ export const ThemeStore = { ThemeStore.theme = ko.observable('').extend({ limitedList: ThemeStore.themes }); ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value)); + +ThemeStore.userBackgroundHash.subscribe(value => { + if (value) { + $htmlCL.add('UserBackground'); + doc.body.style.backgroundImage = "url("+serverRequestRaw('UserBackground', value)+")"; + } else { + $htmlCL.remove('UserBackground'); + doc.body.removeAttribute('style'); + } +}); diff --git a/dev/Stores/User/Message.js b/dev/Stores/User/Message.js index 1a570831e..f36fe51d6 100644 --- a/dev/Stores/User/Message.js +++ b/dev/Stores/User/Message.js @@ -2,7 +2,7 @@ import ko from 'ko'; import { Scope, Notification } from 'Common/Enums'; import { MessageSetAction } from 'Common/EnumsUser'; -import { doc, createElement, elementById } from 'Common/Globals'; +import { doc, $htmlCL, createElement, elementById } from 'Common/Globals'; import { isNonEmptyArray, pInt, pString, addObservablesTo, addSubscribablesTo } from 'Common/Utils'; import { plainToHtml } from 'Common/UtilsUser'; @@ -186,6 +186,8 @@ export const MessageUserStore = new class { } }, + listLoadingAnimation: value => $htmlCL.toggle('list-loading', value), + listLoading: value => this.listCompleteLoading(value || this.listIsNotCompleted()), @@ -220,7 +222,7 @@ export const MessageUserStore = new class { } }); - this.purgeMessageBodyCacheThrottle = this.purgeMessageBodyCache.throttle(30000); + this.purgeMessageBodyCache = this.purgeMessageBodyCache.throttle(30000); } purgeMessageBodyCache() { @@ -428,16 +430,12 @@ export const MessageUserStore = new class { ); } - setMessage(data, cached) { + setMessage(data, cached, oMessage) { let isNew = false, - body = null, json = data && data.Result, - id = '', - plain = '', - resultHtml = '', messagesDom = this.messagesBodiesDom(), selectedMessage = this.selectorMessageSelected(), - message = this.message(); + message = oMessage || this.message(); if ( json && @@ -447,7 +445,7 @@ export const MessageUserStore = new class { ) { const threads = message.threads(); if (message.uid !== json.Uid && 1 < threads.length && threads.includes(json.Uid)) { - message = MessageModel.reviveFromJson(json); + message = oMessage ? null : MessageModel.reviveFromJson(json); if (message) { message.threads(threads); MessageFlagsCache.initMessage(message); @@ -460,7 +458,7 @@ export const MessageUserStore = new class { } if (message && message.uid === json.Uid) { - this.messageError(''); + oMessage || this.messageError(''); if (cached) { delete json.IsSeen; @@ -475,7 +473,8 @@ export const MessageUserStore = new class { addRequestedMessage(message.folder, message.uid); if (messagesDom) { - id = 'rl-mgs-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''); + let body = null, + id = 'rl-mgs-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''); const textBody = elementById(id); if (textBody) { @@ -483,7 +482,9 @@ export const MessageUserStore = new class { message.fetchDataFromDom(); messagesDom.append(textBody); } else { - let isHtml = !!json.Html; + let isHtml = !!json.Html, + plain = '', + resultHtml = '
';
 						if (isHtml) {
 							resultHtml = json.Html.toString();
 							if (SettingsUserStore.removeColors()) {
@@ -508,8 +509,6 @@ export const MessageUserStore = new class {
 							} else {
 								resultHtml = '
' + resultHtml + '
'; } - } else { - resultHtml = '
' + resultHtml + '
'; } body = Element.fromHTML('