diff --git a/README.md b/README.md index a5b2a32fa..5c10548df 100644 --- a/README.md +++ b/README.md @@ -139,23 +139,25 @@ RainLoop 1.15 vs SnappyMail |js/* |RainLoop |Snappy | |--------------- |--------: |--------: | -|admin.js |2.158.025 | 79.040 | -|app.js |4.215.733 | 431.641 | +|admin.js |2.158.025 | 79.018 | +|app.js |4.215.733 | 407.680 | |boot.js | 672.433 | 1.996 | |libs.js | 647.679 | 200.131 | +|sieve.js | 0 | 30.809 | |polyfills.js | 325.908 | 0 | |serviceworker.js | 0 | 285 | -|TOTAL |8.019.778 | 713.093 | +|TOTAL |8.019.778 | 719.919 | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |--------------- |--------: |--------: |------: |------: |--------: |--------: | -|admin.min.js | 255.514 | 39.272 | 73.899 | 13.076 | 60.674 | 11.727 | -|app.min.js | 516.000 | 207.038 |140.430 | 66.265 |110.657 | 56.788 | +|admin.min.js | 255.514 | 39.256 | 73.899 | 13.076 | 60.674 | 11.702 | +|app.min.js | 516.000 | 194.277 |140.430 | 62.348 |110.657 | 53.485 | |boot.min.js | 66.456 | 1.230 | 22.553 | 768 | 20.043 | 619 | |libs.min.js | 574.626 | 96.201 |177.280 | 35.522 |151.855 | 31.746 | +|sieve.min.js | 0 | 15.009 | 0 | 5.228 | 0 | 4.702 | |polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 | -|TOTAL |1.445.204 | 343.741 |425.477 |115.631 |353.301 |100.880 | -|TOTAL (no admin) |1.189.690 | 304.469 |351.061 |102.555 |292.627 | 89.153 | +|TOTAL |1.445.204 | 345.973 |425.477 |116.942 |353.301 |102.254 | +|TOTAL (no admin) |1.189.690 | 306.717 |351.061 |103.866 |292.627 | 90.552 | For a user its around 69% smaller and faster than traditional RainLoop. diff --git a/dev/Common/Folders.js b/dev/Common/Folders.js index 836d5cc90..1a9ff9287 100644 --- a/dev/Common/Folders.js +++ b/dev/Common/Folders.js @@ -67,8 +67,8 @@ fetchFolderInformation = (fCallback, folder, list = []) => { /** * @param {Array=} aDisabled * @param {Array=} aHeaderLines - * @param {Function=} fDisableCallback * @param {Function=} fRenameCallback + * @param {Function=} fDisableCallback * @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView * @returns {Array} */ diff --git a/dev/Knoin/AbstractViews.js b/dev/Knoin/AbstractViews.js index 587e6021a..783108649 100644 --- a/dev/Knoin/AbstractViews.js +++ b/dev/Knoin/AbstractViews.js @@ -29,7 +29,7 @@ class AbstractView { /* onBuild() {} - onBeforeShow() {} // Happens before: hidden = false + beforeShow() {} // Happens before: hidden = false onShow() {} // Happens after: hidden = false onHide() {} */ @@ -73,7 +73,7 @@ export class AbstractViewPopup extends AbstractView onClose() {} /* - onBeforeShow() {} // Happens before showModal() + beforeShow() {} // Happens before showModal() onShow() {} // Happens after showModal() afterShow() {} // Happens after showModal() animation transitionend onHide() {} // Happens before animation transitionend @@ -112,7 +112,7 @@ export class AbstractViewSettings { /* onBuild(viewModelDom) {} - onBeforeShow() {} + beforeShow() {} onShow() {} onHide() {} viewModelDom diff --git a/dev/Knoin/Knoin.js b/dev/Knoin/Knoin.js index c27c75752..a029fd3df 100644 --- a/dev/Knoin/Knoin.js +++ b/dev/Knoin/Knoin.js @@ -214,7 +214,7 @@ const vmScreen.onShow && vmScreen.onShow(); forEachViewModel(vmScreen, (vm, dom) => { - vm.onBeforeShow && vm.onBeforeShow(); + vm.beforeShow && vm.beforeShow(); dom.hidden = false; vm.onShow && vm.onShow(); autofocus(dom); @@ -243,7 +243,7 @@ export const if (vm) { params = params || []; - vm.onBeforeShow && vm.onBeforeShow(...params); + vm.beforeShow && vm.beforeShow(...params); vm.modalVisible(true); diff --git a/dev/Screen/AbstractSettings.js b/dev/Screen/AbstractSettings.js index 09ae5d585..fd00ca9ff 100644 --- a/dev/Screen/AbstractSettings.js +++ b/dev/Screen/AbstractSettings.js @@ -69,7 +69,7 @@ export class AbstractSettingsScreen extends AbstractScreen { this.oCurrentSubScreen = settingsScreen; // show - settingsScreen.onBeforeShow && settingsScreen.onBeforeShow(); + settingsScreen.beforeShow && settingsScreen.beforeShow(); settingsScreen.viewModelDom.hidden = false; settingsScreen.onShow && settingsScreen.onShow(); diff --git a/dev/Screen/User/Settings.js b/dev/Screen/User/Settings.js index b3b81cb61..00f19ab67 100644 --- a/dev/Screen/User/Settings.js +++ b/dev/Screen/User/Settings.js @@ -21,8 +21,6 @@ import { SystemDropDownUserView } from 'View/User/SystemDropDown'; import { SettingsMenuUserView } from 'View/User/Settings/Menu'; import { SettingsPaneUserView } from 'View/User/Settings/Pane'; -//import { staticLink } from 'Common/Links'; - export class SettingsUserScreen extends AbstractSettingsScreen { constructor() { super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]); @@ -41,7 +39,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen { if (SettingsCapa('Sieve')) { views.push(UserSettingsFilters); -// rl.loadScript(staticLink('js/sieve.js')).then(() => 0).catch(e => console.error(e)); } if (SettingsCapa('AutoLogout') || SettingsCapa('OpenPGP') || SettingsCapa('GnuPG')) { diff --git a/dev/Settings/Admin/Config.js b/dev/Settings/Admin/Config.js index 909efbc9e..0a7bd3037 100644 --- a/dev/Settings/Admin/Config.js +++ b/dev/Settings/Admin/Config.js @@ -9,7 +9,7 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ { this.config = ko.observableArray(); } - onBeforeShow() { + beforeShow() { Remote.request('AdminSettingsGet', (iError, data) => { if (!iError) { const cfg = [], diff --git a/dev/Settings/User/Filters.js b/dev/Settings/User/Filters.js index cc807fd4b..f14a41434 100644 --- a/dev/Settings/User/Filters.js +++ b/dev/Settings/User/Filters.js @@ -1,97 +1,44 @@ -import { getNotification } from 'Common/Translator'; -import { forEachObjectValue } from 'Common/Utils'; import { addObservablesTo } from 'External/ko'; +import { staticLink } from 'Common/Links'; +import { FolderUserStore } from 'Stores/User/Folder'; -import { SieveUserStore } from 'Stores/User/Sieve'; -import Remote from 'Remote/User/Fetch'; - -import { SieveScriptModel } from 'Model/SieveScript'; - -import { showScreenPopup } from 'Knoin/Knoin'; - -import { SieveScriptPopupView } from 'View/Popup/SieveScript'; - +//export class UserSettingsFilters /*extends AbstractViewSettings*/ { export class UserSettingsFilters /*extends AbstractViewSettings*/ { constructor() { - this.scripts = SieveUserStore.scripts; - this.loading = ko.observable(false).extend({ debounce: 200 }); - + this.scripts = ko.observableArray(); + this.loading = ko.observable(true).extend({ debounce: 200 }); addObservablesTo(this, { serverError: false, serverErrorDesc: '' }); + rl.loadScript(staticLink('js/sieve.js')).then(() => { + const Sieve = window.Sieve; + Sieve.folderList = FolderUserStore.folderList; + Sieve.serverError.subscribe(value => this.serverError(value)); + Sieve.serverErrorDesc.subscribe(value => this.serverErrorDesc(value)); + Sieve.loading.subscribe(value => this.loading(value)); + Sieve.scripts.subscribe(value => this.scripts(value)); + Sieve.updateList(); + }).catch(e => console.error(e)); + this.scriptForDeletion = ko.observable(null).askDeleteHelper(); } - setError(text) { - this.serverError(true); - this.serverErrorDesc(text); - } - - updateList() { - if (!this.loading()) { - this.loading(true); - this.serverError(false); - - Remote.request('Filters', (iError, data) => { - this.loading(false); - this.scripts([]); - - if (iError) { - SieveUserStore.capa([]); - this.setError(getNotification(iError)); - } else { - SieveUserStore.capa(data.Result.Capa); -/* - this.scripts( - data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v) - ); -*/ - forEachObjectValue(data.Result.Scripts, value => { - value = SieveScriptModel.reviveFromJson(value); - value && this.scripts.push(value) - }); - } - }); - } - } - addScript() { - showScreenPopup(SieveScriptPopupView, [new SieveScriptModel()]); + this.editScript(); } editScript(script) { - showScreenPopup(SieveScriptPopupView, [script]); + window.Sieve.ScriptView.showModal(script ? [script] : null); } deleteScript(script) { - this.serverError(false); - Remote.request('FiltersScriptDelete', - (iError, data) => { - if (iError) { - this.setError((data && data.ErrorMessageAdditional) || getNotification(iError)); - } else { - this.scripts.remove(script); - } - }, - {name:script.name()} - ); + window.Sieve.deleteScript(script); } toggleScript(script) { - let name = script.active() ? '' : script.name(); - this.serverError(false); - Remote.request('FiltersScriptActivate', - (iError, data) => { - if (iError) { - this.setError((data && data.ErrorMessageAdditional) || iError) - } else { - this.scripts.forEach(script => script.active(script.name() === name)); - } - }, - {name:name} - ); + window.Sieve.toggleScript(script); } onBuild(oDom) { @@ -103,6 +50,6 @@ export class UserSettingsFilters /*extends AbstractViewSettings*/ { } onShow() { - this.updateList(); + window.Sieve && window.Sieve.updateList(); } } diff --git a/dev/Settings/User/Sieve.js b/dev/Settings/User/Sieve.js deleted file mode 100644 index 5c2724aaf..000000000 --- a/dev/Settings/User/Sieve.js +++ /dev/null @@ -1,73 +0,0 @@ -import { getNotification } from 'Common/Translator'; - -import Remote from 'Remote/User/Fetch'; - -//export class UserSettingsFilters /*extends AbstractViewSettings*/ { -export class UserSettingsSieve /*extends AbstractViewSettings*/ { - constructor() { - const Sieve = window.Sieve; - this.scripts = Sieve.scripts; - this.loading = Sieve.loading; - this.serverError = Sieve.serverError; - this.serverErrorDesc = Sieve.serverErrorDesc; - this.scriptForDeletion = ko.observable(null).askDeleteHelper(); - } - - setError(text) { - this.serverError(true); - this.serverErrorDesc(text); - } - - updateList() { - window.Sieve.updateList(); - } - - addScript() { - window.Sieve.ScriptView.showModal(); - } - - editScript(script) { - window.Sieve.ScriptView.showModal([script]); - } - - deleteScript(script) { - this.serverError(false); - Remote.request('FiltersScriptDelete', - (iError, data) => { - if (iError) { - this.setError((data && data.ErrorMessageAdditional) || getNotification(iError)); - } else { - this.scripts.remove(script); - } - }, - {name:script.name()} - ); - } - - toggleScript(script) { - let name = script.active() ? '' : script.name(); - this.serverError(false); - Remote.request('FiltersScriptActivate', - (iError, data) => { - if (iError) { - this.setError((data && data.ErrorMessageAdditional) || iError) - } else { - this.scripts.forEach(script => script.active(script.name() === name)); - } - }, - {name:name} - ); - } - - onBuild(oDom) { - oDom.addEventListener('click', event => { - const el = event.target.closestWithin('.script-item .e-action', oDom), - script = el && ko.dataFor(el); - script && this.editScript(script); - }); - } - - onShow() { - this.updateList(); - } -} diff --git a/dev/Sieve/Model/Abstract.js b/dev/Sieve/Model/Abstract.js new file mode 100644 index 000000000..4df08e039 --- /dev/null +++ b/dev/Sieve/Model/Abstract.js @@ -0,0 +1,121 @@ +import { forEachObjectValue, forEachObjectEntry, koComputable } from 'Sieve/Utils'; + +function typeCast(curValue, newValue) { + if (null != curValue) { + switch (typeof curValue) + { + case 'boolean': return 0 != newValue && !!newValue; + case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0; + case 'string': return null != newValue ? '' + newValue : ''; + case 'object': + if (curValue.constructor.reviveFromJson) { + return curValue.constructor.reviveFromJson(newValue); + } + if (Array.isArray(curValue) && !Array.isArray(newValue)) + return []; + } + } + return newValue; +} + +export class AbstractModel { + constructor() { +/* + if (new.target === AbstractModel) { + throw new Error("Can't instantiate AbstractModel!"); + } +*/ + this.disposables = []; + } + + addObservables(observables) { + forEachObjectEntry(observables, (key, value) => + this[key] || (this[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value)) + ); + } + + addComputables(computables) { + forEachObjectEntry(computables, (key, fn) => this[key] = koComputable(fn)); + } + + addSubscribables(subscribables) { + forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) ); + } + + /** Called by delegateRunOnDestroy */ + onDestroy() { + /** dispose ko subscribables */ + this.disposables.forEach(disposable => { + disposable && typeof disposable.dispose === 'function' && disposable.dispose(); + }); + /** clear object entries */ +// forEachObjectEntry(this, (key, value) => { + forEachObjectValue(this, value => { + /** clear CollectionModel */ + let arr = ko.isObservableArray(value) ? value() : value; + arr && arr.onDestroy && arr.onDestroy(); + /** destroy ko.observable/ko.computed? */ +// dispose(value); + /** clear object value */ +// this[key] = null; // TODO: issue with Contacts view + }); +// this.disposables = []; + } + + /** + * @static + * @param {FetchJson} json + * @returns {boolean} + */ + static validJson(json) { + return !!(json && ('Object/'+this.name.replace('Model', '') === json['@Object'])); + } + + /** + * @static + * @param {FetchJson} json + * @returns {*Model} + */ + static reviveFromJson(json) { + let obj = this.validJson(json) ? new this() : null; + obj && obj.revivePropertiesFromJson(json); + return obj; + } + + revivePropertiesFromJson(json) { + let model = this.constructor; + if (!model.validJson(json)) { + return false; + } + forEachObjectEntry(json, (key, value) => { + if ('@' !== key[0]) try { + key = key[0].toLowerCase() + key.slice(1); + switch (typeof this[key]) + { + case 'function': + if (ko.isObservable(this[key])) { + this[key](typeCast(this[key](), value)); +// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived'); + } +// else console.log(model.name + '.' + key + ' is a function'); + break; + case 'boolean': + case 'number': + case 'object': + case 'string': + this[key] = typeCast(this[key], value); + break; + // fall through + case 'undefined': + default: +// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived'); + } + } catch (e) { + console.log(model.name + '.' + key); + console.error(e); + } + }); + return true; + } + +} diff --git a/dev/Model/Filter.js b/dev/Sieve/Model/Filter.js similarity index 82% rename from dev/Model/Filter.js rename to dev/Sieve/Model/Filter.js index defaeac09..809e0030d 100644 --- a/dev/Model/Filter.js +++ b/dev/Sieve/Model/Filter.js @@ -1,13 +1,11 @@ -import { koArrayWithDestroy } from 'External/ko'; +import { koArrayWithDestroy } from 'Sieve/Utils'; -import { arrayLength, pString } from 'Common/Utils'; -import { i18n } from 'Common/Translator'; -import { getFolderFromCacheList } from 'Common/Cache'; +//import { getFolderFromCacheList } from 'Common/Cache'; -import { AccountUserStore } from 'Stores/User/Account'; +//import { AccountUserStore } from 'Stores/User/Account'; -import { FilterConditionModel } from 'Model/FilterCondition'; -import { AbstractModel } from 'Knoin/AbstractModel'; +import { FilterConditionModel } from 'Sieve/Model/FilterCondition'; +import { AbstractModel } from 'Sieve/Model/Abstract'; /** * @enum {string} @@ -66,9 +64,10 @@ export class FilterModel extends AbstractModel { this.conditions = koArrayWithDestroy(); - const fGetRealFolderName = (folderFullName) => { - const folder = getFolderFromCacheList(folderFullName); - return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullName; + const fGetRealFolderName = folderFullName => { +// const folder = getFolderFromCacheList(folderFullName); +// return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullName; + return folderFullName; }; this.addComputables({ @@ -78,23 +77,23 @@ export class FilterModel extends AbstractModel { switch (this.actionType()) { case FilterAction.MoveTo: - result = i18n(root + 'MOVE_TO', { + result = rl.i18n(root + 'MOVE_TO', { FOLDER: fGetRealFolderName(actionValue) }); break; case FilterAction.Forward: - result = i18n(root + 'FORWARD_TO', { + result = rl.i18n(root + 'FORWARD_TO', { EMAIL: actionValue }); break; case FilterAction.Vacation: - result = i18n(root + 'VACATION_MESSAGE'); + result = rl.i18n(root + 'VACATION_MESSAGE'); break; case FilterAction.Reject: - result = i18n(root + 'REJECT'); + result = rl.i18n(root + 'REJECT'); break; case FilterAction.Discard: - result = i18n(root + 'DISCARD'); + result = rl.i18n(root + 'DISCARD'); break; // no default } @@ -213,7 +212,7 @@ export class FilterModel extends AbstractModel { } setRecipients() { - this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', ')); +// this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', ')); } /** @@ -224,16 +223,10 @@ export class FilterModel extends AbstractModel { static reviveFromJson(json) { const filter = super.reviveFromJson(json); if (filter) { - filter.id = pString(json.ID); - - filter.conditions([]); - - if (arrayLength(json.Conditions)) { - filter.conditions( - json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v) - ); - } - + filter.id = filter.id ? '' + filter.id : ''; + filter.conditions( + json.Conditions ? json.Conditions.map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v) : [] + ); filter.actionKeep(0 != json.Keep); filter.actionNoStop(0 == json.Stop); filter.actionMarkAsRead(1 == json.MarkAsRead); diff --git a/dev/Model/FilterCondition.js b/dev/Sieve/Model/FilterCondition.js similarity index 95% rename from dev/Model/FilterCondition.js rename to dev/Sieve/Model/FilterCondition.js index 5bc7cd858..5ee0806aa 100644 --- a/dev/Model/FilterCondition.js +++ b/dev/Sieve/Model/FilterCondition.js @@ -1,6 +1,6 @@ -import { koComputable } from 'External/ko'; +import { koComputable } from 'Sieve/Utils'; -import { AbstractModel } from 'Knoin/AbstractModel'; +import { AbstractModel } from 'Sieve/Model/Abstract'; /** * @enum {string} diff --git a/dev/Model/SieveScript.js b/dev/Sieve/Model/Script.js similarity index 94% rename from dev/Model/SieveScript.js rename to dev/Sieve/Model/Script.js index a30af59a2..2314b27bf 100644 --- a/dev/Model/SieveScript.js +++ b/dev/Sieve/Model/Script.js @@ -1,7 +1,6 @@ -import { AbstractModel } from 'Knoin/AbstractModel'; -import { FilterModel } from 'Model/Filter'; -import { arrayLength, pString, b64EncodeJSON } from 'Common/Utils'; -import { koArrayWithDestroy } from 'External/ko'; +import { AbstractModel } from 'Sieve/Model/Abstract'; +import { FilterModel } from 'Sieve/Model/Filter'; +import { koArrayWithDestroy } from 'Sieve/Utils'; const SIEVE_FILE_NAME = 'rainloop.user'; @@ -153,12 +152,12 @@ function filtersToSieveScript(filters) subject = ':subject ' + quote(StripSpaces(paramValue)) + ' '; } - paramValue = pString(filter.actionValueThird()).trim(); + paramValue = (filter.actionValueThird() || '').trim(); if (paramValue.length) { days = Math.max(1, parseInt(paramValue, 10)); } - paramValue = pString(filter.actionValueFourth()).trim() + paramValue = (filter.actionValueFourth() || '').trim() if (paramValue.length) { paramValue = paramValue.split(',').map(email => email.trim().length ? quote(email) : '' @@ -214,7 +213,7 @@ function filtersToSieveScript(filters) '/*', 'BEGIN:FILTER:' + filter.id, 'BEGIN:HEADER', - b64EncodeJSON(filter.toJson()).match(split).join(eol) + 'END:HEADER', + btoa(unescape(encodeURIComponent(JSON.stringify(filter.toJson())))).match(split).join(eol) + 'END:HEADER', '*/', filter.enabled() ? '' : '/* @Filter is disabled ', filterToString(filter, require), @@ -317,7 +316,7 @@ export class SieveScriptModel extends AbstractModel if (script) { if (script.allowFilters()) { script.filters( - arrayLength(json.filters) + Array.isArray(json.filters) && json.filters.length ? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v) : sieveScriptToFilters(script.body()) ); diff --git a/dev/Sieve/Utils.js b/dev/Sieve/Utils.js index c458cc1fa..52c101b58 100644 --- a/dev/Sieve/Utils.js +++ b/dev/Sieve/Utils.js @@ -1,4 +1,89 @@ +import { SieveScriptModel } from 'Sieve/Model/Script'; export const + // import { i18n } from 'Common/Translator'; + i18n = rl.i18n, + + // import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils'; + forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn), + forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)), + + // import { koArrayWithDestroy } from 'External/ko'; + // With this we don't need delegateRunOnDestroy + koArrayWithDestroy = data => { + data = ko.observableArray(data); + data.subscribe(changes => + changes.forEach(item => + 'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy() + ) + , data, 'arrayChange'); + return data; + }, + + // import { koComputable } from 'External/ko'; + koComputable = fn => ko.computed(fn, {'pure':true}), + arrayToString = (arr, separator) => - arr.map(item => item.toString ? item.toString() : item).join(separator); + arr.map(item => item.toString ? item.toString() : item).join(separator), +/* + getNotificationMessage = code => { + let key = getKeyByValue(Notification, code); + return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : ''; + rl.i18n('NOTIFICATIONS/') + }, + getNotification = (code, message = '', defCode = 0) => { + code = parseInt(code, 10) || 0; + if (Notification.ClientViewError === code && message) { + return message; + } + + return getNotificationMessage(code) + || getNotificationMessage(parseInt(defCode, 10)) + || ''; + }, +*/ + getNotification = code => 'ERROR ' + code, + + Remote = rl.app.Remote, + + // capabilities + capa = ko.observableArray(), + + // Sieve scripts SieveScriptModel + scripts = koArrayWithDestroy(), + + loading = ko.observable(false), + serverError = ko.observable(false), + serverErrorDesc = ko.observable(''), + setError = text => { + serverError(true); + serverErrorDesc(text); + }, + + updateList = () => { + if (!loading()) { + loading(true); + serverError(false); + + Remote.request('Filters', (iError, data) => { + loading(false); + scripts([]); + + if (iError) { + capa([]); + setError(getNotification(iError)); + } else { + capa(data.Result.Capa); +/* + scripts( + data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v) + ); +*/ + forEachObjectValue(data.Result.Scripts, value => { + value = SieveScriptModel.reviveFromJson(value); + value && scripts.push(value) + }); + } + }); + } + }; diff --git a/dev/View/Popup/Filter.js b/dev/Sieve/View/Filter.js similarity index 58% rename from dev/View/Popup/Filter.js rename to dev/Sieve/View/Filter.js index e77639987..9c3a5493b 100644 --- a/dev/View/Popup/Filter.js +++ b/dev/Sieve/View/Filter.js @@ -1,19 +1,77 @@ -import ko from 'ko'; -import { koComputable } from 'External/ko'; +import { FilterAction } from 'Sieve/Model/Filter'; +import { FilterConditionField, FilterConditionType } from 'Sieve/Model/FilterCondition'; +/* +import { SettingsUserStore } from 'Stores/User/Settings'; +*/ -import { FilterAction } from 'Model/Filter'; -import { FilterConditionField, FilterConditionType } from 'Model/FilterCondition'; -import { SettingsGet } from 'Common/Globals'; -import { defaultOptionsAfterRender } from 'Common/Utils'; -import { i18n, initOnStartOrLangChange } from 'Common/Translator'; +import { + capa, + i18n, + koComputable +} from 'Sieve/Utils'; -import { SieveUserStore } from 'Stores/User/Sieve'; +const + // import { defaultOptionsAfterRender } from 'Common/Utils'; + defaultOptionsAfterRender = (domItem, item) => + domItem && item && undefined !== item.disabled + && domItem.classList.toggle('disabled', domItem.disabled = item.disabled), -import { AbstractViewPopup } from 'Knoin/AbstractViews'; + // import { folderListOptionsBuilder } from 'Common/Folders'; + /** + * @param {Array=} aDisabled + * @param {Array=} aHeaderLines + * @param {Function=} fRenameCallback + * @returns {Array} + */ + folderListOptionsBuilder = ( + aDisabled, + aHeaderLines, + fRenameCallback + ) => { + const + aResult = [], + sDeepPrefix = '\u00A0\u00A0\u00A0', + showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/, -import { folderListOptionsBuilder } from 'Common/Folders'; + foldersWalk = folders => { + folders.forEach(oItem => { + if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) { + aResult.push({ + id: oItem.fullName, + name: + sDeepPrefix.repeat(oItem.deep) + + fRenameCallback(oItem), + system: false, + disabled: !oItem.selectable() || aDisabled.includes(oItem.fullName) + }); + } -export class FilterPopupView extends AbstractViewPopup { + if (oItem.subFolders.length) { + foldersWalk(oItem.subFolders()); + } + }); + }; + + + fRenameCallback = fRenameCallback || (oItem => oItem.name()); + Array.isArray(aDisabled) || (aDisabled = []); + + Array.isArray(aHeaderLines) && aHeaderLines.forEach(line => + aResult.push({ + id: line[0], + name: line[1], + system: false, + disabled: false + }) + ); + + // FolderUserStore.folderList() + foldersWalk(window.Sieve.folderList() || []); + + return aResult; + }; + +export class FilterPopupView extends rl.pluginPopupView { constructor() { super('Filter'); @@ -27,7 +85,7 @@ export class FilterPopupView extends AbstractViewPopup { this.defaultOptionsAfterRender = defaultOptionsAfterRender; this.folderSelectList = koComputable(() => folderListOptionsBuilder( - [SettingsGet('SieveAllowFileintoInbox') ? '' : 'INBOX'], + [rl.settings.get('SieveAllowFileintoInbox') ? '' : 'INBOX'], [['', '']], item => item ? item.localName() : '' ) @@ -39,9 +97,7 @@ export class FilterPopupView extends AbstractViewPopup { key => this[key] = ko.observableArray() ); - initOnStartOrLangChange(this.populateOptions.bind(this)); - - SieveUserStore.capa.subscribe(this.populateOptions, this); + this.populateOptions(); } saveFilter() { @@ -77,11 +133,10 @@ export class FilterPopupView extends AbstractViewPopup { // this.actionTypeOptions.push({id: FilterAction.None, // name: i18n('GLOBAL/NONE')}); - const modules = SieveUserStore.capa; - if (modules) { - this.allowMarkAsRead(modules.includes('imap4flags')); + if (capa) { + this.allowMarkAsRead(capa.includes('imap4flags')); - if (modules.includes('fileinto')) { + if (capa.includes('fileinto')) { this.actionTypeOptions.push({ id: FilterAction.MoveTo, name: i18nFilter('ACTION_MOVE_TO') @@ -92,22 +147,22 @@ export class FilterPopupView extends AbstractViewPopup { }); } - if (modules.includes('reject')) { + if (capa.includes('reject')) { this.actionTypeOptions.push({ id: FilterAction.Reject, name: i18nFilter('ACTION_REJECT') }); } - if (modules.includes('vacation')) { + if (capa.includes('vacation')) { this.actionTypeOptions.push({ id: FilterAction.Vacation, name: i18nFilter('ACTION_VACATION_MESSAGE') }); } - if (modules.includes('body')) { + if (capa.includes('body')) { this.fieldOptions.push({ id: FilterConditionField.Body, name: i18nFilter('FIELD_BODY') }); } - if (modules.includes('regex')) { + if (capa.includes('regex')) { this.typeOptions.push({ id: FilterConditionType.Regex, name: 'Regex' }); } } @@ -129,7 +184,8 @@ export class FilterPopupView extends AbstractViewPopup { this.filter().removeCondition(oConditionToDelete); } - onShow(oFilter, fTrueCallback, bEdit) { + beforeShow(oFilter, fTrueCallback, bEdit) { +// onShow(oFilter, fTrueCallback, bEdit) { this.isNew(!bEdit); this.fTrueCallback = fTrueCallback; @@ -138,6 +194,8 @@ export class FilterPopupView extends AbstractViewPopup { this.selectedFolderValue(oFilter.actionValue()); bEdit || oFilter.nameFocused(true); + + this.populateOptions(); } afterShow() { diff --git a/dev/View/Popup/SieveScript.js b/dev/Sieve/View/Script.js similarity index 70% rename from dev/View/Popup/SieveScript.js rename to dev/Sieve/View/Script.js index 67dc8d829..b6b8cf64c 100644 --- a/dev/View/Popup/SieveScript.js +++ b/dev/Sieve/View/Script.js @@ -1,24 +1,22 @@ -import ko from 'ko'; +import { FilterModel } from 'Sieve/Model/Filter'; +import { SieveScriptModel } from 'Sieve/Model/Script'; -import { getNotification, i18nToNodes } from 'Common/Translator'; -import { addObservablesTo } from 'External/ko'; - -import Remote from 'Remote/User/Fetch'; -import { FilterModel } from 'Model/Filter'; -import { SieveUserStore } from 'Stores/User/Sieve'; - -import { showScreenPopup } from 'Knoin/Knoin'; -import { AbstractViewPopup } from 'Knoin/AbstractViews'; - -import { FilterPopupView } from 'View/Popup/Filter'; +import { FilterPopupView } from 'Sieve/View/Filter'; //import { parseScript } from 'Sieve/Parser'; -export class SieveScriptPopupView extends AbstractViewPopup { +import { + capa, + scripts, + getNotification, + Remote +} from 'Sieve/Utils'; + +export class SieveScriptPopupView extends rl.pluginPopupView { constructor() { super('SieveScript'); - addObservablesTo(this, { + this.addObservables({ saveError: false, saveErrorText: '', rawActive: false, @@ -26,7 +24,7 @@ export class SieveScriptPopupView extends AbstractViewPopup { script: null }); - this.sieveCapabilities = SieveUserStore.capa.join(' '); + this.sieveCapabilities = capa.join(' '); this.saving = false; this.filterForDeletion = ko.observable(null).askDeleteHelper(); @@ -40,7 +38,7 @@ export class SieveScriptPopupView extends AbstractViewPopup { return; } - if (!script.exists() && SieveUserStore.scripts.find(item => item.name() === script.name())) { + if (!script.exists() && scripts.find(item => item.name() === script.name())) { script.nameError(true); return; } @@ -60,7 +58,7 @@ export class SieveScriptPopupView extends AbstractViewPopup { self.saveError(true); self.saveErrorText((data && data.ErrorMessageAdditional) || getNotification(iError)); } else { - script.exists() || SieveUserStore.scripts.push(script); + script.exists() || scripts.push(script); script.exists(true); script.hasChanges(false); } @@ -78,7 +76,7 @@ export class SieveScriptPopupView extends AbstractViewPopup { /* this = SieveScriptModel */ const filter = new FilterModel(); filter.generateID(); - showScreenPopup(FilterPopupView, [ + FilterPopupView.showModal([ filter, () => this.filters.push(filter) ]); @@ -86,13 +84,14 @@ export class SieveScriptPopupView extends AbstractViewPopup { editFilter(filter) { const clonedFilter = filter.cloneSelf(); - showScreenPopup(FilterPopupView, [ + FilterPopupView.showModal([ clonedFilter, () => { const script = this.script(), filters = script.filters(), index = filters.indexOf(filter); if (-1 < index) { +// script.filters.splice(index, 1, clonedFilter); filters[index] = clonedFilter; script.filters(filters); } @@ -118,7 +117,9 @@ export class SieveScriptPopupView extends AbstractViewPopup { }); } - onShow(oScript) { + beforeShow(oScript) { +// onShow(oScript) { + oScript = oScript || new SieveScriptModel(); let raw = !oScript.allowFilters(); this.script(oScript); this.rawActive(raw); @@ -132,9 +133,4 @@ export class SieveScriptPopupView extends AbstractViewPopup { console.log(tree.join('\r\n')); */ } - - afterShow() { - // Sometimes not everything is translated, try again - i18nToNodes(this.viewModelDom); - } } diff --git a/dev/View/Popup/Languages.js b/dev/View/Popup/Languages.js index 483203d5c..ff62f63e3 100644 --- a/dev/View/Popup/Languages.js +++ b/dev/View/Popup/Languages.js @@ -36,7 +36,7 @@ export class LanguagesPopupView extends AbstractViewPopup { this.languages().forEach(item => item.selected(item.key === currentLang)); } - onBeforeShow() { + beforeShow() { this.fLang = null; this.userLanguage(''); diff --git a/dev/sieve.js b/dev/sieve.js index 3a9ac47c4..dcf266d30 100644 --- a/dev/sieve.js +++ b/dev/sieve.js @@ -1,206 +1,56 @@ -import { parseScript } from 'Sieve/Parser'; +import { + capa, + scripts, + loading, + serverError, + serverErrorDesc, + setError, + updateList, + getNotification, + Remote +} from 'Sieve/Utils'; -import { FilterModel } from 'Model/Filter'; -import { SieveScriptModel } from 'Model/SieveScript'; -import { FilterPopupView } from 'View/Popup/Filter'; - -//import { getNotification, i18nToNodes } from 'Common/Translator'; -import { forEachObjectValue } from 'Common/Utils'; -import { koArrayWithDestroy } from 'External/ko'; +import { SieveScriptPopupView } from 'Sieve/View/Script'; // SieveUserStore -const -/* - getNotificationMessage = code => { - let key = getKeyByValue(Notification, code); - return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : ''; - rl.i18n('NOTIFICATIONS/') - }, - getNotification = (code, message = '', defCode = 0) => { - code = parseInt(code, 10) || 0; - if (Notification.ClientViewError === code && message) { - return message; - } +window.Sieve = { + capa: capa, + scripts: scripts, + setError: setError, + updateList: updateList, + loading: loading, + serverError: serverError, + serverErrorDesc: serverErrorDesc, + ScriptView: SieveScriptPopupView, - return getNotificationMessage(code) - || getNotificationMessage(parseInt(defCode, 10)) - || ''; - }, -*/ - getNotification = code => 'ERROR ' + code, + folderList: null, - Remote = rl.app.Remote, - - Sieve = { - // capabilities - capa: ko.observableArray(), - - // Sieve scripts SieveScriptModel - scripts: koArrayWithDestroy(), - - parseScript: parseScript, - - setError: text => { - Sieve.serverError(true); - Sieve.serverErrorDesc(text); - }, - updateList: () => { - if (!Sieve.loading()) { - Sieve.loading(true); - Sieve.serverError(false); - - Remote.request('Filters', (iError, data) => { - Sieve.loading(false); - Sieve.scripts([]); - - if (iError) { - Sieve.capa([]); - Sieve.setError(getNotification(iError)); - } else { - Sieve.capa(data.Result.Capa); - /* - Sieve.scripts( - data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v) - ); - */ - forEachObjectValue(data.Result.Scripts, value => { - value = SieveScriptModel.reviveFromJson(value); - value && Sieve.scripts.push(value) - }); - } - }); - } - }, - - loading: ko.observable(false).extend({ debounce: 200 }), - serverError: ko.observable(false), - serverErrorDesc: ko.observable('') - }; - -Sieve.ScriptView = class SieveScriptPopupView extends rl.pluginPopupView { - constructor() { - super('SieveScript'); - - this.addObservables({ - saveError: false, - saveErrorText: '', - rawActive: false, - allowToggle: false, - script: null - }); - - this.sieveCapabilities = Sieve.capa.join(' '); - this.saving = false; - - this.filterForDeletion = ko.observable(null).askDeleteHelper(); - } - - saveScript() { - let self = this, - script = self.script(); - if (!self.saving/* && script.hasChanges()*/) { - if (!script.verify()) { - return; - } - - if (!script.exists() && Sieve.scripts.find(item => item.name() === script.name())) { - script.nameError(true); - return; - } - - self.saving = true; - self.saveError(false); - - if (self.allowToggle()) { - script.body(script.filtersToRaw()); - } - - Remote.request('FiltersScriptSave', - (iError, data) => { - self.saving = false; - - if (iError) { - self.saveError(true); - self.saveErrorText((data && data.ErrorMessageAdditional) || getNotification(iError)); - } else { - script.exists() || Sieve.scripts.push(script); - script.exists(true); - script.hasChanges(false); - } - }, - script.toJson() - ); - } - } - - deleteFilter(filter) { - this.script().filters.remove(filter); - } - - addFilter() { - /* this = SieveScriptModel */ - const filter = new FilterModel(); - filter.generateID(); - FilterPopupView.showModal([ - filter, - () => this.filters.push(filter) - ]); - } - - editFilter(filter) { - const clonedFilter = filter.cloneSelf(); - FilterPopupView.showModal([ - clonedFilter, - () => { - const script = this.script(), - filters = script.filters(), - index = filters.indexOf(filter); - if (-1 < index) { - filters[index] = clonedFilter; - script.filters(filters); + deleteScript: script => { + serverError(false); + Remote.request('FiltersScriptDelete', + (iError, data) => { + if (iError) { + setError((data && data.ErrorMessageAdditional) || getNotification(iError)); + } else { + scripts.remove(script); } }, - true - ]); + {name:script.name()} + ); + }, + + toggleScript(script) { + let name = script.active() ? '' : script.name(); + serverError(false); + Remote.request('FiltersScriptActivate', + (iError, data) => { + if (iError) { + setError((data && data.ErrorMessageAdditional) || iError) + } else { + scripts.forEach(script => script.active(script.name() === name)); + } + }, + {name:name} + ); } - - toggleFiltersRaw() { - let script = this.script(), notRaw = !this.rawActive(); - if (notRaw) { - script.body(script.filtersToRaw()); - script.hasChanges(script.hasChanges()); - } - this.rawActive(notRaw); - } - - onBuild(oDom) { - oDom.addEventListener('click', event => { - const el = event.target.closestWithin('td.e-action', oDom), - filter = el && ko.dataFor(el); - filter && this.editFilter(filter); - }); - } - - onShow(oScript) { - oScript = oScript || new SieveScriptModel(); - let raw = !oScript.allowFilters(); - this.script(oScript); - this.rawActive(raw); - this.allowToggle(!raw); - this.saveError(false); - -/* - // TODO: Sieve GUI - let tree = parseScript(oScript.body(), oScript.name()); - console.dir(tree); - console.log(tree.join('\r\n')); -*/ - } - - afterShow() { - // Sometimes not everything is translated, try again -// i18nToNodes(this.viewModelDom); - } -} - -window.Sieve = Sieve; +}; diff --git a/plugins/README.md b/plugins/README.md index a8e78ba73..d430a1caa 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -21,7 +21,7 @@ class PluginPopupView extends rl.pluginPopupView onBuild(dom) {} // Happens before showModal() - onBeforeShow(...params) {} + beforeShow(...params) {} // Happens after showModal() onShow(...params) {} // Happens after showModal() animation transitionend diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsFilter.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsFilter.html index 0427046ac..dd0a526bd 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsFilter.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsFilter.html @@ -5,44 +5,39 @@ -