Improved knockout observables management to prevent memory leaks

This commit is contained in:
djmaze 2020-10-26 12:54:03 +01:00
parent b165a1de4f
commit 3eb6ab1ef7
46 changed files with 1020 additions and 1013 deletions

View file

@ -1,5 +1,4 @@
import { ComposeType, FolderType } from 'Common/Enums';
import { pString } from 'Common/Utils';
const
tpl = document.createElement('template'),
@ -303,13 +302,13 @@ export function folderListOptionsBuilder(
bSep = true;
aList.forEach(oItem => {
// if (oItem.subScribed() || !oItem.existen || bBuildUnvisible)
// if (oItem.subscribed() || !oItem.exists || bBuildUnvisible)
if (
(oItem.subScribed() || !oItem.existen || bBuildUnvisible) &&
(oItem.selectable || oItem.hasSubScribedSubfolders())
(oItem.subscribed() || !oItem.exists || bBuildUnvisible) &&
(oItem.selectable || oItem.hasSubscribedSubfolders())
) {
if (fVisibleCallback ? fVisibleCallback(oItem) : true) {
if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubScribedSubfolders()) {
if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubscribedSubfolders()) {
aResult.push({
id: oItem.fullNameRaw,
name:
@ -327,7 +326,7 @@ export function folderListOptionsBuilder(
}
}
if (oItem.subScribed() && oItem.subFolders().length) {
if (oItem.subscribed() && oItem.subFolders().length) {
aResult = aResult.concat(
folderListOptionsBuilder(
[],
@ -349,17 +348,14 @@ export function folderListOptionsBuilder(
}
/**
* Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
* @param {Object|Array} objectOrObjects
* @returns {void}
*/
export function delegateRunOnDestroy(objectOrObjects) {
if (objectOrObjects) {
if (isArray(objectOrObjects)) {
objectOrObjects.forEach(item => delegateRunOnDestroy(item));
} else {
objectOrObjects.onDestroy && objectOrObjects.onDestroy();
}
}
objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
obj => obj.onDestroy && obj.onDestroy()
);
}
/**
@ -448,14 +444,6 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
};
}
/**
* @param {string} color
* @returns {boolean}
*/
export function isTransparent(color) {
return 'rgba(0, 0, 0, 0)' === color || 'transparent' === color;
}
/**
* @param {string} mailToUrl
* @param {Function} PopupComposeViewModel
@ -524,8 +512,8 @@ export function mailToHelper(mailToUrl, PopupComposeViewModel) {
to,
cc,
bcc,
null == params.subject ? null : pString(decodeURIComponent(params.subject)),
null == params.body ? null : plainToHtml(pString(decodeURIComponent(params.body)))
null == params.subject ? null : decodeURIComponent(params.subject),
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
]);
return true;

15
dev/External/ko.js vendored
View file

@ -262,4 +262,19 @@ ko.observable.fn.deleteAccessHelper = function() {
return this;
};
ko.addObservablesTo = (target, observables) => {
Object.entries(observables).forEach(([key, value]) => target[key] = ko.observable(value) );
/*
Object.entries(observables).forEach(([key, value]) =>
target[key] = Array.isArray(value) ? ko.observableArray(value) : ko.observable(value)
);
*/
};
ko.addComputablesTo = (target, computables) =>
Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn));
ko.addSubscribablesTo = (target, subscribables) =>
Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn));
export default ko;

View file

@ -1,5 +1,5 @@
function disposeOne(disposable) {
function dispose(disposable) {
if (disposable && 'function' === typeof disposable.dispose) {
disposable.dispose();
}
@ -22,7 +22,7 @@ function typeCast(curValue, newValue) {
}
export class AbstractModel {
disposables = [];
subscribables = [];
constructor() {
/*
@ -32,27 +32,30 @@ export class AbstractModel {
*/
}
addObservables(obj) {
Object.entries(obj).forEach(([key, value]) => this[key] = ko.observable(value) );
/*
Object.entries(obj).forEach(([key, value]) =>
this[key] = Array.isArray(value) ? ko.observableArray(value) : ko.observable(value)
);
*/
addObservables(observables) {
ko.addObservablesTo(this, observables);
}
addComputables(obj) {
Object.entries(obj).forEach(([key, fn]) => this[key] = ko.computed(fn) );
addComputables(computables) {
ko.addComputablesTo(this, computables);
}
addSubscribables(obj) {
Object.entries(obj).forEach(([key, fn]) => this.disposables.push( this[key].subscribe(fn) ) );
addSubscribables(subscribables) {
Object.entries(subscribables).forEach(([key, fn]) => this.subscribables.push( this[key].subscribe(fn) ) );
}
/** Called by delegateRunOnDestroy */
onDestroy() {
this.disposables.forEach(disposeOne);
/** clear ko.subscribe */
this.subscribables.forEach(dispose);
/** clear object entries */
Object.entries(this).forEach(([key, value]) => {
disposeOne(value);
/** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value;
arr && arr.onDestroy && value.onDestroy();
/** destroy ko.observable/ko.computed? */
dispose(value);
/** clear object value */
this[key] = null;
});
}

View file

@ -56,4 +56,16 @@ export class AbstractViewNext {
return this.viewModelDom.querySelector(selectors);
}
addObservables(observables) {
ko.addObservablesTo(this, observables);
}
addComputables(computables) {
ko.addComputablesTo(this, computables);
}
addSubscribables(subscribables) {
ko.addSubscribablesTo(this, subscribables);
}
}

View file

@ -10,6 +10,10 @@ export class AbstractCollectionModel extends Array
super();
}
onDestroy() {
this.forEach(item => item.onDestroy && item.onDestroy());
}
/**
* @static
* @param {FetchJson} json

View file

@ -20,7 +20,7 @@ class FolderModel extends AbstractModel {
this.interval = 0;
this.selectable = false;
this.existen = true;
this.exists = true;
this.addObservables({
name: '',
@ -29,7 +29,7 @@ class FolderModel extends AbstractModel {
focused: false,
selected: false,
edited: false,
subScribed: true,
subscribed: true,
checkable: false,
deleteAccess: false,
@ -54,10 +54,6 @@ class FolderModel extends AbstractModel {
const folder = super.reviveFromJson(json);
if (folder) {
folder.deep = json.FullNameRaw.split(folder.delimiter).length - 1;
folder.selectable = !!json.IsSelectable;
folder.existen = !!json.IsExists;
folder.subScribed(!!json.IsSubscribed);
folder.messageCountAll = ko.computed({
read: folder.privateMessageCountAll,
@ -87,26 +83,26 @@ class FolderModel extends AbstractModel {
isInbox: () => FolderType.Inbox === folder.type(),
hasSubScribedSubfolders:
hasSubscribedSubfolders:
() =>
!!folder.subFolders().find(
oFolder => (oFolder.subScribed() || oFolder.hasSubScribedSubfolders()) && !oFolder.isSystemFolder()
oFolder => (oFolder.subscribed() || oFolder.hasSubscribedSubfolders()) && !oFolder.isSystemFolder()
),
canBeEdited: () => FolderType.User === folder.type() && folder.existen && folder.selectable,
canBeEdited: () => FolderType.User === folder.type() && folder.exists && folder.selectable,
visible: () => {
const isSubScribed = folder.subScribed(),
isSubFolders = folder.hasSubScribedSubfolders();
const isSubscribed = folder.subscribed(),
isSubFolders = folder.hasSubscribedSubfolders();
return isSubScribed || (isSubFolders && (!folder.existen || !folder.selectable));
return isSubscribed || (isSubFolders && (!folder.exists || !folder.selectable));
},
isSystemFolder: () => FolderType.User !== folder.type(),
hidden: () => {
const isSystem = folder.isSystemFolder(),
isSubFolders = folder.hasSubScribedSubfolders();
isSubFolders = folder.hasSubscribedSubfolders();
return (isSystem && !isSubFolders) || (!folder.selectable && !isSubFolders);
},
@ -137,7 +133,7 @@ class FolderModel extends AbstractModel {
selectableForFolderList: () => !folder.isSystemFolder() && folder.selectable,
canBeSubScribed: () => !folder.isSystemFolder() && folder.selectable,
canBeSubscribed: () => !folder.isSystemFolder() && folder.selectable,
canBeChecked: () => !folder.isSystemFolder() && folder.selectable,
@ -221,9 +217,9 @@ class FolderModel extends AbstractModel {
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
hasSubScribedUnreadMessagesSubfolders: () =>
hasSubscribedUnreadMessagesSubfolders: () =>
!!folder.subFolders().find(
folder => folder.hasUnreadMessages() || folder.hasSubScribedUnreadMessagesSubfolders()
folder => folder.hasUnreadMessages() || folder.hasSubscribedUnreadMessagesSubfolders()
)
});
@ -246,7 +242,7 @@ class FolderModel extends AbstractModel {
* @returns {string}
*/
collapsedCss() {
return 'e-collapsed-sign ' + (this.hasSubScribedSubfolders()
return 'e-collapsed-sign ' + (this.hasSubscribedSubfolders()
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
: 'icon-none'
);

View file

@ -120,13 +120,13 @@ class FoldersUserSettings {
subscribeFolder(folder) {
Local.set(ClientSideKeyName.FoldersLashHash, '');
Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, true);
folder.subScribed(true);
folder.subscribed(true);
}
unSubscribeFolder(folder) {
Local.set(ClientSideKeyName.FoldersLashHash, '');
Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, false);
folder.subScribed(false);
folder.subscribed(false);
}
checkableTrueFolder(folder) {

View file

@ -26,9 +26,6 @@ class ThemesUserSettings {
this.themeTrigger = ko.observable(SaveSettingsStep.Idle).extend({ throttle: 100 });
this.iTimer = 0;
this.oThemeAjaxRequest = null;
this.theme.subscribe((value) => {
this.themesObjects().forEach(theme => {
theme.selected(value === theme.name);

View file

@ -160,9 +160,9 @@ class FolderUserStore {
folder &&
inboxFolderName !== folder.fullNameRaw &&
folder.selectable &&
folder.existen &&
folder.exists &&
timeout > folder.interval &&
(folder.isSystemFolder() || (folder.subScribed() && folder.checkable()))
(folder.isSystemFolder() || (folder.subscribed() && folder.checkable()))
) {
timeouts.push([folder.interval, folder.fullNameRaw]);
}

View file

@ -23,29 +23,33 @@ class LoginAdminView extends AbstractViewNext {
this.hideSubmitButton = appSettingsGet('hideSubmitButton');
this.login = ko.observable('');
this.password = ko.observable('');
this.addObservables({
login: '',
password: '',
this.loginError = ko.observable(false);
this.passwordError = ko.observable(false);
loginError: false,
passwordError: false,
formHidden: false,
submitRequest: false,
submitError: ''
});
this.loginErrorAnimation = ko.observable(false).extend({ 'falseTimeout': 500 });
this.passwordErrorAnimation = ko.observable(false).extend({ 'falseTimeout': 500 });
this.formHidden = ko.observable(false);
this.formError = ko.computed(() => this.loginErrorAnimation() || this.passwordErrorAnimation());
this.login.subscribe(() => this.loginError(false));
this.addSubscribables({
login: () => this.loginError(false),
this.password.subscribe(() => this.passwordError(false));
password: () => this.passwordError(false),
this.loginError.subscribe(v => this.loginErrorAnimation(!!v));
loginError: v => this.loginErrorAnimation(!!v),
this.passwordError.subscribe(v => this.passwordErrorAnimation(!!v));
this.submitRequest = ko.observable(false);
this.submitError = ko.observable('');
passwordError: v => this.passwordErrorAnimation(!!v)
});
}
@command((self) => !self.submitRequest())

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { StorageResultType, Notification } from 'Common/Enums';
import { getNotification } from 'Common/Translator';
@ -16,27 +14,25 @@ class AccountPopupView extends AbstractViewNext {
constructor() {
super();
this.isNew = ko.observable(true);
this.addObservables({
isNew: true,
this.email = ko.observable('');
this.password = ko.observable('');
email: '',
password: '',
this.emailError = ko.observable(false);
this.passwordError = ko.observable(false);
emailError: false,
passwordError: false,
this.email.subscribe(() => {
this.emailError(false);
submitRequest: false,
submitError: '',
submitErrorAdditional: '',
emailFocus: false
});
this.password.subscribe(() => {
this.passwordError(false);
});
this.email.subscribe(() => this.emailError(false));
this.submitRequest = ko.observable(false);
this.submitError = ko.observable('');
this.submitErrorAdditional = ko.observable('');
this.emailFocus = ko.observable(false);
this.password.subscribe(() => this.passwordError(false));
}
@command((self) => !self.submitRequest())

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import PgpStore from 'Stores/User/Pgp';
import { popup, command } from 'Knoin/Knoin';
@ -13,13 +11,15 @@ class AddOpenPgpKeyPopupView extends AbstractViewNext {
constructor() {
super();
this.key = ko.observable('');
this.key.error = ko.observable(false);
this.key.errorMessage = ko.observable('');
this.addObservables({
key: '',
keyError: false,
keyErrorMessage: ''
});
this.key.subscribe(() => {
this.key.error(false);
this.key.errorMessage('');
this.keyError(false);
this.keyErrorMessage('');
});
}
@ -35,10 +35,10 @@ class AddOpenPgpKeyPopupView extends AbstractViewNext {
keyTrimmed = keyTrimmed.replace(/[\r]+/g, '').replace(/[\n]{2,}/g, '\n\n');
}
this.key.error(!keyTrimmed);
this.key.errorMessage('');
this.keyError(!keyTrimmed);
this.keyErrorMessage('');
if (!openpgpKeyring || this.key.error()) {
if (!openpgpKeyring || this.keyError()) {
return false;
}
@ -58,8 +58,8 @@ class AddOpenPgpKeyPopupView extends AbstractViewNext {
}
if (err) {
this.key.error(true);
this.key.errorMessage(err && err[0] ? '' + err[0] : '');
this.keyError(true);
this.keyErrorMessage(err && err[0] ? '' + err[0] : '');
console.log(err);
}
}
@ -75,7 +75,7 @@ class AddOpenPgpKeyPopupView extends AbstractViewNext {
rl.app.reloadOpenPgpKeys();
if (this.key.error()) {
if (this.keyError()) {
return false;
}
@ -85,8 +85,8 @@ class AddOpenPgpKeyPopupView extends AbstractViewNext {
clearPopup() {
this.key('');
this.key.error(false);
this.key.errorMessage('');
this.keyError(false);
this.keyErrorMessage('');
}
onShow() {

View file

@ -15,17 +15,19 @@ class AdvancedSearchPopupView extends AbstractViewNext {
constructor() {
super();
this.fromFocus = ko.observable(false);
this.addObservables({
fromFocus: false,
this.from = ko.observable('');
this.to = ko.observable('');
this.subject = ko.observable('');
this.text = ko.observable('');
this.selectedDateValue = ko.observable(-1);
from: '',
to: '',
subject: '',
text: '',
selectedDateValue: -1,
this.hasAttachment = ko.observable(false);
this.starred = ko.observable(false);
this.unseen = ko.observable(false);
hasAttachment: false,
starred: false,
unseen: false
});
this.selectedDates = ko.computed(() => {
translatorTrigger();

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { KeyState } from 'Common/Enums';
import { i18n } from 'Common/Translator';
@ -14,9 +12,11 @@ class AskPopupView extends AbstractViewNext {
constructor() {
super();
this.askDesc = ko.observable('');
this.yesButton = ko.observable('');
this.noButton = ko.observable('');
this.addObservables({
askDesc: '',
yesButton: '',
noButton: ''
});
this.fYesAction = null;
this.fNoAction = null;

View file

@ -132,150 +132,86 @@ class ComposePopupView extends AbstractViewNext {
this.capaOpenPGP = PgpStore.capaOpenPGP;
this.identitiesDropdownTrigger = ko.observable(false);
this.identities = IdentityStore.identities;
this.to = ko.observable('');
this.to.focused = ko.observable(false);
this.cc = ko.observable('');
this.cc.focused = ko.observable(false);
this.bcc = ko.observable('');
this.bcc.focused = ko.observable(false);
this.replyTo = ko.observable('');
this.replyTo.focused = ko.observable(false);
this.addObservables({
identitiesDropdownTrigger: false,
to: '',
toFocused: false,
cc: '',
ccFocused: false,
bcc: '',
bccFocused: false,
replyTo: '',
replyToFocused: false,
subject: '',
subjectFocused: false,
isHtml: false,
requestDsn: false,
requestReadReceipt: false,
markAsImportant: false,
sendError: false,
sendSuccessButSaveError: false,
savedError: false,
sendErrorDesc: '',
savedErrorDesc: '',
savedTime: 0,
emptyToError: false,
attachmentsInProcessError: false,
attachmentsInErrorError: false,
showCc: false,
showBcc: false,
showReplyTo: false,
draftFolder: '',
draftUid: '',
sending: false,
saving: false,
attachmentsPlace: false,
composeUploaderButton: null,
composeUploaderDropPlace: null,
dragAndDropEnabled: false,
attacheMultipleAllowed: false,
addAttachmentEnabled: false,
composeEditorArea: null,
currentIdentity: this.identities()[0] ? this.identities()[0] : null
});
// this.to.subscribe((v) => console.log(v));
ko.computed(() => {
switch (true) {
case this.to.focused():
case this.toFocused():
this.sLastFocusedField = 'to';
break;
case this.cc.focused():
case this.ccFocused():
this.sLastFocusedField = 'cc';
break;
case this.bcc.focused():
case this.bccFocused():
this.sLastFocusedField = 'bcc';
break;
// no default
}
}).extend({ notify: 'always' });
this.subject = ko.observable('');
this.subject.focused = ko.observable(false);
this.isHtml = ko.observable(false);
this.requestDsn = ko.observable(false);
this.requestReadReceipt = ko.observable(false);
this.markAsImportant = ko.observable(false);
this.sendError = ko.observable(false);
this.sendSuccessButSaveError = ko.observable(false);
this.savedError = ko.observable(false);
this.sendButtonSuccess = ko.computed(() => !this.sendError() && !this.sendSuccessButSaveError());
this.sendErrorDesc = ko.observable('');
this.savedErrorDesc = ko.observable('');
this.sendError.subscribe(value => !value && this.sendErrorDesc(''));
this.savedError.subscribe(value => !value && this.savedErrorDesc(''));
this.sendSuccessButSaveError.subscribe(value => !value && this.savedErrorDesc(''));
this.savedTime = ko.observable(0);
this.savedTimeText = ko.computed(() =>
this.savedTime()
? i18n('COMPOSE/SAVED_TIME', { 'TIME': this.savedTime().format('LT') })
: ''
);
this.emptyToError = ko.observable(false);
this.emptyToErrorTooltip = ko.computed(() => (this.emptyToError() ? i18n('COMPOSE/EMPTY_TO_ERROR_DESC') : ''));
this.attachmentsInProcessError = ko.observable(false);
this.attachmentsInErrorError = ko.observable(false);
this.attachmentsErrorTooltip = ko.computed(() => {
let result = '';
switch (true) {
case this.attachmentsInProcessError():
result = i18n('COMPOSE/ATTACHMENTS_UPLOAD_ERROR_DESC');
break;
case this.attachmentsInErrorError():
result = i18n('COMPOSE/ATTACHMENTS_ERROR_DESC');
break;
// no default
}
return result;
});
this.showCc = ko.observable(false);
this.showBcc = ko.observable(false);
this.showReplyTo = ko.observable(false);
this.cc.subscribe((value) => {
if (false === this.showCc() && value.length) {
this.showCc(true);
}
});
this.bcc.subscribe((value) => {
if (false === this.showBcc() && value.length) {
this.showBcc(true);
}
});
this.replyTo.subscribe((value) => {
if (false === this.showReplyTo() && value.length) {
this.showReplyTo(true);
}
});
this.draftFolder = ko.observable('');
this.draftUid = ko.observable('');
this.sending = ko.observable(false);
this.saving = ko.observable(false);
this.attachments = ko.observableArray([]);
this.attachmentsInProcess = ko.computed(() => this.attachments().filter(item => item && !item.complete()));
this.attachmentsInReady = ko.computed(() => this.attachments().filter(item => item && item.complete()));
this.attachmentsInError = ko.computed(() => this.attachments().filter(item => item && item.error()));
this.attachmentsCount = ko.computed(() => this.attachments().length);
this.attachmentsInErrorCount = ko.computed(() => this.attachmentsInError().length);
this.attachmentsInProcessCount = ko.computed(() => this.attachmentsInProcess().length);
this.isDraftFolderMessage = ko.computed(() => this.draftFolder() && this.draftUid());
this.attachmentsPlace = ko.observable(false);
this.attachmentsInErrorCount.subscribe((value) => {
if (0 === value) {
this.attachmentsInErrorError(false);
}
});
this.composeUploaderButton = ko.observable(null);
this.composeUploaderDropPlace = ko.observable(null);
this.dragAndDropEnabled = ko.observable(false);
this.dragAndDropOver = ko.observable(false).extend({ throttle: 1 });
this.dragAndDropVisible = ko.observable(false).extend({ throttle: 1 });
this.attacheMultipleAllowed = ko.observable(false);
this.addAttachmentEnabled = ko.observable(false);
this.composeEditorArea = ko.observable(null);
this.identities = IdentityStore.identities;
this.identitiesOptions = ko.computed(() =>
IdentityStore.identities().map(item => ({
'item': item,
'optValue': item.id(),
'optText': item.formattedName()
}))
);
this.currentIdentity = ko.observable(this.identities()[0] ? this.identities()[0] : null);
this.currentIdentity.extend({
toggleSubscribe: [
@ -291,24 +227,105 @@ class ComposePopupView extends AbstractViewNext {
]
});
this.currentIdentityView = ko.computed(() => {
const item = this.currentIdentity();
return item ? item.formattedName() : 'unknown';
this.bDisabeCloseOnEsc = true;
this.sDefaultKeyScope = KeyState.Compose;
this.tryToClosePopup = this.tryToClosePopup.debounce(200);
this.iTimer = 0;
this.addComputables({
sendButtonSuccess: () => !this.sendError() && !this.sendSuccessButSaveError(),
savedTimeText: () =>
this.savedTime() ? i18n('COMPOSE/SAVED_TIME', { 'TIME': this.savedTime().format('LT') }) : '',
emptyToErrorTooltip: () => (this.emptyToError() ? i18n('COMPOSE/EMPTY_TO_ERROR_DESC') : ''),
attachmentsErrorTooltip: () => {
let result = '';
switch (true) {
case this.attachmentsInProcessError():
result = i18n('COMPOSE/ATTACHMENTS_UPLOAD_ERROR_DESC');
break;
case this.attachmentsInErrorError():
result = i18n('COMPOSE/ATTACHMENTS_ERROR_DESC');
break;
// no default
}
return result;
},
attachmentsInProcess: () => this.attachments().filter(item => item && !item.complete()),
attachmentsInReady: () => this.attachments().filter(item => item && item.complete()),
attachmentsInError: () => this.attachments().filter(item => item && item.error()),
attachmentsCount: () => this.attachments().length,
attachmentsInErrorCount: () => this.attachmentsInError().length,
attachmentsInProcessCount: () => this.attachmentsInProcess().length,
isDraftFolderMessage: () => this.draftFolder() && this.draftUid(),
identitiesOptions: () =>
IdentityStore.identities().map(item => ({
'item': item,
'optValue': item.id(),
'optText': item.formattedName()
})),
currentIdentityView: () => {
const item = this.currentIdentity();
return item ? item.formattedName() : 'unknown';
},
canBeSentOrSaved: () => !this.sending() && !this.saving()
});
this.to.subscribe((value) => {
if (this.emptyToError() && value.length) {
this.emptyToError(false);
this.addSubscribables({
sendError: value => !value && this.sendErrorDesc(''),
savedError: value => !value && this.savedErrorDesc(''),
sendSuccessButSaveError: value => !value && this.savedErrorDesc(''),
cc: value => {
if (false === this.showCc() && value.length) {
this.showCc(true);
}
},
bcc: value => {
if (false === this.showBcc() && value.length) {
this.showBcc(true);
}
},
replyTo: value => {
if (false === this.showReplyTo() && value.length) {
this.showReplyTo(true);
}
},
attachmentsInErrorCount: value => {
if (0 === value) {
this.attachmentsInErrorError(false);
}
},
to: value => {
if (this.emptyToError() && value.length) {
this.emptyToError(false);
}
},
attachmentsInProcess: value => {
if (this.attachmentsInProcessError() && Array.isNotEmpty(value)) {
this.attachmentsInProcessError(false);
}
}
});
this.attachmentsInProcess.subscribe(value => {
if (this.attachmentsInProcessError() && Array.isNotEmpty(value)) {
this.attachmentsInProcessError(false);
}
});
this.canBeSentOrSaved = ko.computed(() => !this.sending() && !this.saving());
this.resizeObserver = new ResizeObserver(this.resizerTrigger.throttle(50).bind(this));
setInterval(() => {
if (
@ -323,15 +340,6 @@ class ComposePopupView extends AbstractViewNext {
this.saveCommand();
}
}, 120000);
this.bDisabeCloseOnEsc = true;
this.sDefaultKeyScope = KeyState.Compose;
this.tryToClosePopup = this.tryToClosePopup.debounce(200);
this.iTimer = 0;
this.resizeObserver = new ResizeObserver(this.resizerTrigger.throttle(50).bind(this));
}
getMessageRequestParams(sSaveFolder)
@ -677,7 +685,7 @@ class ComposePopupView extends AbstractViewNext {
this.bSkipNextHide = false;
this.to.focused(false);
this.toFocused(false);
rl.route.on();
@ -1120,9 +1128,9 @@ class ComposePopupView extends AbstractViewNext {
// rl.settings.app('mobile') ||
setTimeout(() => {
if (!this.to()) {
this.to.focused(true);
this.toFocused(true);
} else if (this.oEditor) {
if (!this.to.focused()) {
if (!this.toFocused()) {
this.oEditor.focus();
}
}
@ -1222,10 +1230,7 @@ class ComposePopupView extends AbstractViewNext {
if (attachment) {
this.attachments.remove(attachment);
delegateRunOnDestroy(attachment);
if (oJua) {
oJua.cancel(id);
}
oJua && oJua.cancel(id);
}
};
}

View file

@ -25,57 +25,59 @@ class ComposeOpenPgpPopupView extends AbstractViewNext {
this.publicKeysOptionsCaption = i18n('PGP_NOTIFICATIONS/ADD_A_PUBLICK_KEY');
this.privateKeysOptionsCaption = i18n('PGP_NOTIFICATIONS/SELECT_A_PRIVATE_KEY');
this.notification = ko.observable('');
this.addObservables({
notification: '',
this.sign = ko.observable(false);
this.encrypt = ko.observable(false);
sign: false,
encrypt: false,
this.password = ko.observable('');
password: '',
this.text = ko.observable('');
this.selectedPrivateKey = ko.observable(null);
this.selectedPublicKey = ko.observable(null);
text: '',
selectedPrivateKey: null,
selectedPublicKey: null,
this.signKey = ko.observable(null);
signKey: null,
submitRequest: false
});
this.encryptKeys = ko.observableArray([]);
this.encryptKeysView = ko.computed(
() => this.encryptKeys().map(oKey => (oKey ? oKey.key : null)).filter(v => v)
);
this.addComputables({
encryptKeysView: () => this.encryptKeys().map(oKey => (oKey ? oKey.key : null)).filter(v => v),
this.privateKeysOptions = ko.computed(() => {
const opts = PgpStore.openpgpkeysPrivate().map((oKey, iIndex) => {
if (this.signKey() && this.signKey().key.id === oKey.id) {
return null;
}
return oKey.users.map(user => ({
'id': oKey.guid,
'name': '(' + oKey.id.substr(KEY_NAME_SUBSTR).toUpperCase() + ') ' + user,
'key': oKey,
'class': iIndex % 2 ? 'odd' : 'even'
}));
});
privateKeysOptions: () => {
const opts = PgpStore.openpgpkeysPrivate().map((oKey, iIndex) => {
if (this.signKey() && this.signKey().key.id === oKey.id) {
return null;
}
return oKey.users.map(user => ({
'id': oKey.guid,
'name': '(' + oKey.id.substr(KEY_NAME_SUBSTR).toUpperCase() + ') ' + user,
'key': oKey,
'class': iIndex % 2 ? 'odd' : 'even'
}));
});
return opts.flat().filter(v => v);
return opts.flat().filter(v => v);
},
publicKeysOptions: () => {
const opts = PgpStore.openpgpkeysPublic().map((oKey, index) => {
if (this.encryptKeysView().includes(oKey)) {
return null;
}
return oKey.users.map(user => ({
'id': oKey.guid,
'name': '(' + oKey.id.substr(KEY_NAME_SUBSTR).toUpperCase() + ') ' + user,
'key': oKey,
'class': index % 2 ? 'odd' : 'even'
}));
});
return opts.flat().filter(v => v);
}
});
this.publicKeysOptions = ko.computed(() => {
const opts = PgpStore.openpgpkeysPublic().map((oKey, index) => {
if (this.encryptKeysView().includes(oKey)) {
return null;
}
return oKey.users.map(user => ({
'id': oKey.guid,
'name': '(' + oKey.id.substr(KEY_NAME_SUBSTR).toUpperCase() + ') ' + user,
'key': oKey,
'class': index % 2 ? 'odd' : 'even'
}));
});
return opts.flat().filter(v => v);
});
this.submitRequest = ko.observable(false);
this.resultCallback = null;
this.selectedPrivateKey.subscribe((value) => {

View file

@ -46,81 +46,36 @@ class ContactsPopupView extends AbstractViewNext {
this.allowContactsSync = ContactStore.allowContactsSync;
this.enableContactsSync = ContactStore.enableContactsSync;
this.search = ko.observable('');
this.contactsCount = ko.observable(0);
this.contacts = ContactStore.contacts;
this.addObservables({
search: '',
contactsCount: 0,
this.currentContact = ko.observable(null);
currentContact: null,
this.importUploaderButton = ko.observable(null);
importUploaderButton: null,
this.contactsPage = ko.observable(1);
this.contactsPageCount = ko.computed(() =>
Math.max(1, Math.ceil(this.contactsCount() / CONTACTS_PER_PAGE))
);
contactsPage: 1,
this.contactsPaginator = ko.computed(computedPaginatorHelper(this.contactsPage, this.contactsPageCount));
emptySelection: true,
viewClearSearch: false,
this.emptySelection = ko.observable(true);
this.viewClearSearch = ko.observable(false);
viewID: '',
viewReadOnly: false,
this.viewID = ko.observable('');
this.viewReadOnly = ko.observable(false);
this.viewProperties = ko.observableArray([]);
viewSaveTrigger: SaveSettingsStep.Idle,
this.viewSaveTrigger = ko.observable(SaveSettingsStep.Idle);
viewSaving: false,
this.viewPropertiesNames = ko.computed(() =>
this.viewProperties().filter(
property => [ContactPropertyType.FirstName, ContactPropertyType.LastName].includes(property.type())
)
);
this.viewPropertiesOther = ko.computed(() =>
this.viewProperties().filter(property => [ContactPropertyType.Nick].includes(property.type()))
);
this.viewPropertiesEmails = ko.computed(() =>
this.viewProperties().filter(property => ContactPropertyType.Email === property.type())
);
this.viewPropertiesWeb = ko.computed(() =>
this.viewProperties().filter(property => ContactPropertyType.Web === property.type())
);
this.viewHasNonEmptyRequiredProperties = ko.computed(() => {
const names = this.viewPropertiesNames(),
emails = this.viewPropertiesEmails(),
fFilter = property => !!trim(property.value());
return !!(names.find(fFilter) || emails.find(fFilter));
watchDirty: false,
watchHash: false
});
this.viewPropertiesPhones = ko.computed(() =>
this.viewProperties().filter(property => ContactPropertyType.Phone === property.type())
);
this.contacts = ContactStore.contacts;
this.viewPropertiesEmailsNonEmpty = ko.computed(() =>
this.viewPropertiesNames().filter(property => !!trim(property.value()))
);
this.viewProperties = ko.observableArray([]);
const propertyFocused = property => !trim(property.value()) && !property.focused();
this.viewPropertiesEmailsEmptyAndOnFocused = ko.computed(() =>
this.viewPropertiesEmails().filter(propertyFocused)
);
this.viewPropertiesPhonesEmptyAndOnFocused = ko.computed(() =>
this.viewPropertiesPhones().filter(propertyFocused)
);
this.viewPropertiesWebEmptyAndOnFocused = ko.computed(() =>
this.viewPropertiesWeb().filter(propertyFocused)
);
this.viewPropertiesOtherEmptyAndOnFocused = ko.computed(() =>
this.viewPropertiesOther().filter(propertyFocused)
);
/*
// Somehow this is broken now when calling addNewProperty
const fFastClearEmptyListHelper = list => {
@ -129,33 +84,18 @@ class ContactsPopupView extends AbstractViewNext {
delegateRunOnDestroy(list);
}
};
this.viewPropertiesEmailsEmptyAndOnFocused.subscribe(fFastClearEmptyListHelper);
this.viewPropertiesPhonesEmptyAndOnFocused.subscribe(fFastClearEmptyListHelper);
this.viewPropertiesWebEmptyAndOnFocused.subscribe(fFastClearEmptyListHelper);
this.viewPropertiesOtherEmptyAndOnFocused.subscribe(fFastClearEmptyListHelper);
this.addSubscribables({
viewPropertiesEmailsEmptyAndOnFocused: fFastClearEmptyListHelper,
viewPropertiesPhonesEmptyAndOnFocused: fFastClearEmptyListHelper,
viewPropertiesWebEmptyAndOnFocused: fFastClearEmptyListHelper,
viewPropertiesOtherEmptyAndOnFocused: fFastClearEmptyListHelper
});
*/
this.viewSaving = ko.observable(false);
this.useCheckboxesInList = SettingsStore.useCheckboxesInList;
this.search.subscribe(() => this.reloadContactList());
this.contactsChecked = ko.computed(() => this.contacts().filter(item => item.checked()));
this.contactsCheckedOrSelected = ko.computed(() => {
const checked = this.contactsChecked(),
selected = this.currentContact();
return selected
? checked.concat([selected]).unique()
: checked;
});
this.contactsCheckedOrSelectedUids = ko.computed(() =>
this.contactsCheckedOrSelected().map(contact => contact.id)
);
this.selector = new Selector(
this.contacts,
this.currentContact,
@ -177,11 +117,6 @@ class ContactsPopupView extends AbstractViewNext {
this.bDropPageAfterDelete = false;
this.watchDirty = ko.observable(false);
this.watchHash = ko.observable(false);
this.viewHash = ko.computed(() => '' + this.viewProperties().map(oItem => oItem.value()).join(''));
// this.saveCommandDebounce = _.debounce(this.saveCommand.bind(this), 1000);
this.viewHash.subscribe(() => {
@ -191,6 +126,61 @@ class ContactsPopupView extends AbstractViewNext {
});
this.sDefaultKeyScope = KeyState.ContactList;
this.addComputables({
contactsPageCount: () => Math.max(1, Math.ceil(this.contactsCount() / CONTACTS_PER_PAGE)),
contactsPaginator: computedPaginatorHelper(this.contactsPage, this.contactsPageCount),
viewPropertiesNames: () =>
this.viewProperties().filter(
property => [ContactPropertyType.FirstName, ContactPropertyType.LastName].includes(property.type())
),
viewPropertiesOther: () =>
this.viewProperties().filter(property => [ContactPropertyType.Nick].includes(property.type())),
viewPropertiesEmails: () =>
this.viewProperties().filter(property => ContactPropertyType.Email === property.type()),
viewPropertiesWeb: () => this.viewProperties().filter(property => ContactPropertyType.Web === property.type()),
viewHasNonEmptyRequiredProperties: () => {
const names = this.viewPropertiesNames(),
emails = this.viewPropertiesEmails(),
fFilter = property => !!trim(property.value());
return !!(names.find(fFilter) || emails.find(fFilter));
},
viewPropertiesPhones: () =>
this.viewProperties().filter(property => ContactPropertyType.Phone === property.type()),
viewPropertiesEmailsNonEmpty: () => this.viewPropertiesNames().filter(property => !!trim(property.value())),
viewPropertiesEmailsEmptyAndOnFocused: () => this.viewPropertiesEmails().filter(propertyFocused),
viewPropertiesPhonesEmptyAndOnFocused: () => this.viewPropertiesPhones().filter(propertyFocused),
viewPropertiesWebEmptyAndOnFocused: () => this.viewPropertiesWeb().filter(propertyFocused),
viewPropertiesOtherEmptyAndOnFocused: () => this.viewPropertiesOther().filter(propertyFocused),
contactsChecked: () => this.contacts().filter(item => item.checked()),
contactsCheckedOrSelected: () => {
const checked = this.contactsChecked(),
selected = this.currentContact();
return selected
? checked.concat([selected]).unique()
: checked;
},
contactsCheckedOrSelectedUids: () => this.contactsCheckedOrSelected().map(contact => contact.id),
viewHash: () => '' + this.viewProperties().map(oItem => oItem.value()).join('')
});
}
@command()

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { StorageResultType, ServerSecure, Notification } from 'Common/Enums';
import { pInt, pString } from 'Common/Utils';
import { i18n } from 'Common/Translator';
@ -19,154 +17,155 @@ class DomainPopupView extends AbstractViewNext {
constructor() {
super();
this.edit = ko.observable(false);
this.saving = ko.observable(false);
this.savingError = ko.observable('');
this.page = ko.observable('main');
this.sieveSettings = ko.observable(false);
this.addObservables({
edit: false,
saving: false,
savingError: '',
page: 'main',
sieveSettings: false,
this.testing = ko.observable(false);
this.testingDone = ko.observable(false);
this.testingImapError = ko.observable(false);
this.testingSieveError = ko.observable(false);
this.testingSmtpError = ko.observable(false);
this.testingImapErrorDesc = ko.observable('');
this.testingSieveErrorDesc = ko.observable('');
this.testingSmtpErrorDesc = ko.observable('');
testing: false,
testingDone: false,
testingImapError: false,
testingSieveError: false,
testingSmtpError: false,
testingImapErrorDesc: '',
testingSieveErrorDesc: '',
testingSmtpErrorDesc: '',
this.testingImapError.subscribe(value => value || this.testingImapErrorDesc(''));
imapServerFocus: false,
sieveServerFocus: false,
smtpServerFocus: false,
this.testingSieveError.subscribe(value => value || this.testingSieveErrorDesc(''));
name: '',
this.testingSmtpError.subscribe(value => value || this.testingSmtpErrorDesc(''));
imapServer: '',
imapPort: '143',
imapSecure: ServerSecure.None,
imapShortLogin: false,
useSieve: false,
sieveAllowRaw: false,
sieveServer: '',
sievePort: '4190',
sieveSecure: ServerSecure.None,
smtpServer: '',
smtpPort: '25',
smtpSecure: ServerSecure.None,
smtpShortLogin: false,
smtpAuth: true,
smtpPhpMail: false,
whiteList: '',
aliasName: '',
this.imapServerFocus = ko.observable(false);
this.sieveServerFocus = ko.observable(false);
this.smtpServerFocus = ko.observable(false);
enableSmartPorts: false
});
this.name = ko.observable('');
this.addSubscribables({
testingImapError: value => value || this.testingImapErrorDesc(''),
testingSieveError: value => value || this.testingSieveErrorDesc(''),
testingSmtpError: value => value || this.testingSmtpErrorDesc(''),
this.imapServer = ko.observable('');
this.imapPort = ko.observable('143');
this.imapSecure = ko.observable(ServerSecure.None);
this.imapShortLogin = ko.observable(false);
this.useSieve = ko.observable(false);
this.sieveAllowRaw = ko.observable(false);
this.sieveServer = ko.observable('');
this.sievePort = ko.observable('4190');
this.sieveSecure = ko.observable(ServerSecure.None);
this.smtpServer = ko.observable('');
this.smtpPort = ko.observable('25');
this.smtpSecure = ko.observable(ServerSecure.None);
this.smtpShortLogin = ko.observable(false);
this.smtpAuth = ko.observable(true);
this.smtpPhpMail = ko.observable(false);
this.whiteList = ko.observable('');
this.aliasName = ko.observable('');
page: () => this.sieveSettings(false),
this.enableSmartPorts = ko.observable(false);
// smart form improvements
imapServerFocus: value =>
value && this.name() && !this.imapServer() && this.imapServer(this.name().replace(/[.]?[*][.]?/g, '')),
this.allowSieve = ko.computed(() => CapaAdminStore.filters() && CapaAdminStore.sieve());
sieveServerFocus: value =>
value && this.imapServer() && !this.sieveServer() && this.sieveServer(this.imapServer()),
this.headerText = ko.computed(() => {
const name = this.name(),
aliasName = this.aliasName();
smtpServerFocus: value => value && this.imapServer() && !this.smtpServer()
&& this.smtpServer(this.imapServer().replace(/imap/gi, 'smtp')),
let result = '';
if (this.edit()) {
result = i18n('POPUPS_DOMAIN/TITLE_EDIT_DOMAIN', { 'NAME': name });
if (aliasName) {
result += ' ← ' + aliasName;
imapSecure: value => {
if (this.enableSmartPorts()) {
const port = pInt(this.imapPort());
switch (pString(value)) {
case '0':
case '2':
if (993 === port) {
this.imapPort('143');
}
break;
case '1':
if (143 === port) {
this.imapPort('993');
}
break;
// no default
}
}
} else {
result = name
? i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN_WITH_NAME', { 'NAME': name })
: i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN');
}
},
return result;
});
this.domainDesc = ko.computed(() => {
const name = this.name();
return !this.edit() && name ? i18n('POPUPS_DOMAIN/NEW_DOMAIN_DESC', { 'NAME': '*@' + name }) : '';
});
this.domainIsComputed = ko.computed(() => {
const usePhpMail = this.smtpPhpMail(),
allowSieve = this.allowSieve(),
useSieve = this.useSieve();
return (
this.name() &&
this.imapServer() &&
this.imapPort() &&
(allowSieve && useSieve ? this.sieveServer() && this.sievePort() : true) &&
((this.smtpServer() && this.smtpPort()) || usePhpMail)
);
});
this.canBeTested = ko.computed(() => !this.testing() && this.domainIsComputed());
this.canBeSaved = ko.computed(() => !this.saving() && this.domainIsComputed());
this.page.subscribe(() => this.sieveSettings(false));
// smart form improvements
this.imapServerFocus.subscribe(value =>
value && this.name() && !this.imapServer() && this.imapServer(this.name().replace(/[.]?[*][.]?/g, ''))
);
this.sieveServerFocus.subscribe(value =>
value && this.imapServer() && !this.sieveServer() && this.sieveServer(this.imapServer())
);
this.smtpServerFocus.subscribe(value =>
value && this.imapServer() && !this.smtpServer() && this.smtpServer(this.imapServer().replace(/imap/gi, 'smtp'))
);
this.imapSecure.subscribe(value => {
if (this.enableSmartPorts()) {
const port = pInt(this.imapPort());
switch (pString(value)) {
case '0':
case '2':
if (993 === port) {
this.imapPort('143');
}
break;
case '1':
if (143 === port) {
this.imapPort('993');
}
break;
// no default
smtpSecure: value => {
if (this.enableSmartPorts()) {
const port = pInt(this.smtpPort());
switch (pString(value)) {
case '0':
if (465 === port || 587 === port) {
this.smtpPort('25');
}
break;
case '1':
if (25 === port || 587 === port) {
this.smtpPort('465');
}
break;
case '2':
if (25 === port || 465 === port) {
this.smtpPort('587');
}
break;
// no default
}
}
}
});
this.smtpSecure.subscribe(value => {
if (this.enableSmartPorts()) {
const port = pInt(this.smtpPort());
switch (pString(value)) {
case '0':
if (465 === port || 587 === port) {
this.smtpPort('25');
}
break;
case '1':
if (25 === port || 587 === port) {
this.smtpPort('465');
}
break;
case '2':
if (25 === port || 465 === port) {
this.smtpPort('587');
}
break;
// no default
this.addComputables({
allowSieve: () => CapaAdminStore.filters() && CapaAdminStore.sieve(),
headerText: () => {
const name = this.name(),
aliasName = this.aliasName();
let result = '';
if (this.edit()) {
result = i18n('POPUPS_DOMAIN/TITLE_EDIT_DOMAIN', { 'NAME': name });
if (aliasName) {
result += ' ← ' + aliasName;
}
} else {
result = name
? i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN_WITH_NAME', { 'NAME': name })
: i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN');
}
}
return result;
},
domainDesc: () => {
const name = this.name();
return !this.edit() && name ? i18n('POPUPS_DOMAIN/NEW_DOMAIN_DESC', { 'NAME': '*@' + name }) : '';
},
domainIsComputed: () => {
const usePhpMail = this.smtpPhpMail(),
allowSieve = this.allowSieve(),
useSieve = this.useSieve();
return (
this.name() &&
this.imapServer() &&
this.imapPort() &&
(allowSieve && useSieve ? this.sieveServer() && this.sievePort() : true) &&
((this.smtpServer() && this.smtpPort()) || usePhpMail)
);
},
canBeTested: () => !this.testing() && this.domainIsComputed(),
canBeSaved: () => !this.saving() && this.domainIsComputed()
});
}

View file

@ -18,18 +18,18 @@ class DomainAliasPopupView extends AbstractViewNext {
constructor() {
super();
this.saving = ko.observable(false);
this.savingError = ko.observable('');
this.addObservables({
saving: false,
savingError: '',
this.name = ko.observable('');
name: '',
this.alias = ko.observable('');
alias: ''
});
this.domains = DomainStore.domainsWithoutAliases;
this.domainsOptions = ko.computed(() =>
this.domains().map(item => ({ optValue: item.name, optText: item.name }))
);
this.domainsOptions = ko.computed(() => this.domains().map(item => ({ optValue: item.name, optText: item.name })));
this.canBeSaved = ko.computed(() => !this.saving() && this.name() && this.alias());

View file

@ -18,24 +18,21 @@ class FilterPopupView extends AbstractViewNext {
constructor() {
super();
this.isNew = ko.observable(true);
this.addObservables({
isNew: true,
filter: null,
allowMarkAsRead: false,
selectedFolderValue: ''
});
this.modules = FilterStore.modules;
this.fTrueCallback = null;
this.filter = ko.observable(null);
this.allowMarkAsRead = ko.observable(false);
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.folderSelectList = FolderStore.folderMenuForFilters;
this.selectedFolderValue = ko.observable('');
this.selectedFolderValue.subscribe(() => {
if (this.filter()) {
this.filter().actionValueError(false);
}
});
this.selectedFolderValue.subscribe(() => this.filter() && this.filter().actionValueError(false));
this.actionTypeOptions = ko.observableArray([]);
this.fieldOptions = ko.observableArray([]);

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { StorageResultType, Notification } from 'Common/Enums';
import { i18n, getNotification } from 'Common/Translator';
import { setFolderHash } from 'Common/Cache';
@ -19,23 +17,25 @@ class FolderClearPopupView extends AbstractViewNext {
constructor() {
super();
this.selectedFolder = ko.observable(null);
this.clearingProcess = ko.observable(false);
this.clearingError = ko.observable('');
this.folderFullNameForClear = ko.computed(() => {
const folder = this.selectedFolder();
return folder ? folder.printableFullName() : '';
this.addObservables({
selectedFolder: null,
clearingProcess: false,
clearingError: ''
});
this.folderNameForClear = ko.computed(() => {
const folder = this.selectedFolder();
return folder ? folder.localName() : '';
});
this.addComputables({
folderFullNameForClear: () => {
const folder = this.selectedFolder();
return folder ? folder.printableFullName() : '';
},
this.dangerDescHtml = ko.computed(() =>
i18n('POPUPS_CLEAR_FOLDER/DANGER_DESC_HTML_1', { 'FOLDER': this.folderNameForClear() })
);
folderNameForClear: () => {
const folder = this.selectedFolder();
return folder ? folder.localName() : '';
},
dangerDescHtml: () => i18n('POPUPS_CLEAR_FOLDER/DANGER_DESC_HTML_1', { 'FOLDER': this.folderNameForClear() })
});
}
@command((self) => {

View file

@ -20,10 +20,12 @@ class FolderCreateView extends AbstractViewNext {
constructor() {
super();
this.folderName = ko.observable('');
this.folderName.focused = ko.observable(false);
this.addObservables({
folderName: '',
folderNameFocused: false,
this.selectedParentValue = ko.observable(UNUSED_OPTION_VALUE);
selectedParentValue: UNUSED_OPTION_VALUE
});
this.parentFolderSelectList = ko.computed(() => {
const top = [],
@ -66,7 +68,7 @@ class FolderCreateView extends AbstractViewNext {
clearPopup() {
this.folderName('');
this.selectedParentValue('');
this.folderName.focused(false);
this.folderNameFocused(false);
}
onShow() {
@ -75,7 +77,7 @@ class FolderCreateView extends AbstractViewNext {
onShowWithDelay() {
// rl.settings.app('mobile') ||
this.folderName.focused(true);
this.folderNameFocused(true);
}
}

View file

@ -79,11 +79,13 @@ class FolderSystemPopupView extends AbstractViewNext {
fSaveSystemFolders();
};
FolderStore.sentFolder.subscribe(fCallback);
FolderStore.draftFolder.subscribe(fCallback);
FolderStore.spamFolder.subscribe(fCallback);
FolderStore.trashFolder.subscribe(fCallback);
FolderStore.archiveFolder.subscribe(fCallback);
ko.addSubscribablesTo(FolderStore, {
sentFolder: fCallback,
draftFolder: fCallback,
spamFolder: fCallback,
trashFolder: fCallback,
archiveFolder: fCallback
});
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
}

View file

@ -26,25 +26,27 @@ class IdentityPopupView extends AbstractViewNext {
super();
this.id = '';
this.edit = ko.observable(false);
this.owner = ko.observable(false);
this.addObservables({
edit: false,
owner: false,
emailFocused: false,
name: '',
replyToFocused: false,
bccFocused: false,
signature: '',
signatureInsertBefore: false,
showBcc: false,
showReplyTo: false,
submitRequest: false,
submitError: ''
});
this.email = ko.observable('').validateEmail();
this.email.focused = ko.observable(false);
this.name = ko.observable('');
this.replyTo = ko.observable('').validateEmail();
this.replyTo.focused = ko.observable(false);
this.bcc = ko.observable('').validateEmail();
this.bcc.focused = ko.observable(false);
this.signature = ko.observable('');
this.signatureInsertBefore = ko.observable(false);
this.showBcc = ko.observable(false);
this.showReplyTo = ko.observable(false);
this.submitRequest = ko.observable(false);
this.submitError = ko.observable('');
this.bcc.subscribe((value) => {
if (false === this.showBcc() && value.length) {
@ -71,19 +73,19 @@ class IdentityPopupView extends AbstractViewNext {
if (this.email.hasError()) {
if (!this.owner()) {
this.email.focused(true);
this.emailFocused(true);
}
return false;
}
if (this.replyTo.hasError()) {
this.replyTo.focused(true);
this.replyToFocused(true);
return false;
}
if (this.bcc.hasError()) {
this.bcc.focused(true);
this.bccFocused(true);
return false;
}
@ -163,7 +165,7 @@ class IdentityPopupView extends AbstractViewNext {
onShowWithDelay() {
if (!this.owner()/* && !rl.settings.app('mobile')*/) {
this.email.focused(true);
this.emailFocused(true);
}
}

View file

@ -29,9 +29,7 @@ class LanguagesPopupView extends AbstractViewNext {
}));
});
this.langs.subscribe(() => {
this.setLanguageSelection();
});
this.langs.subscribe(() => this.setLanguageSelection());
}
languageTooltipName(language) {

View file

@ -14,17 +14,16 @@ class MessageOpenPgpPopupView extends AbstractViewNext {
constructor() {
super();
this.notification = ko.observable('');
this.selectedKey = ko.observable(null);
this.addObservables({
notification: '',
selectedKey: null,
password: '',
submitRequest: false
});
this.privateKeys = ko.observableArray([]);
this.password = ko.observable('');
this.resultCallback = null;
this.submitRequest = ko.observable(false);
this.sDefaultKeyScope = KeyState.PopupMessageOpenPGP;
}

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { pInt } from 'Common/Utils';
import PgpStore from 'Stores/User/Pgp';
@ -15,20 +13,20 @@ class NewOpenPgpKeyPopupView extends AbstractViewNext {
constructor() {
super();
this.email = ko.observable('');
this.email.focus = ko.observable('');
this.email.error = ko.observable(false);
this.addObservables({
email: '',
emailFocus: '',
emailError: false,
this.name = ko.observable('');
this.password = ko.observable('');
this.keyBitLength = ko.observable(2048);
name: '',
password: '',
keyBitLength: 2048,
this.submitRequest = ko.observable(false);
this.submitError = ko.observable('');
this.email.subscribe(() => {
this.email.error(false);
submitRequest: false,
submitError: ''
});
this.email.subscribe(() => this.emailError(false));
}
@command()
@ -36,8 +34,8 @@ class NewOpenPgpKeyPopupView extends AbstractViewNext {
const userId = {},
openpgpKeyring = PgpStore.openpgpKeyring;
this.email.error(!this.email().trim());
if (!openpgpKeyring || this.email.error()) {
this.emailError(!this.email().trim());
if (!openpgpKeyring || this.emailError()) {
return false;
}
@ -95,7 +93,7 @@ class NewOpenPgpKeyPopupView extends AbstractViewNext {
this.password('');
this.email('');
this.email.error(false);
this.emailError(false);
this.keyBitLength(2048);
this.submitError('');
@ -106,7 +104,7 @@ class NewOpenPgpKeyPopupView extends AbstractViewNext {
}
onShowWithDelay() {
this.email.focus(true);
this.emailFocus(true);
}
}

View file

@ -18,10 +18,11 @@ class PluginPopupView extends AbstractViewNext {
this.onPluginSettingsUpdateResponse = this.onPluginSettingsUpdateResponse.bind(this);
this.saveError = ko.observable('');
this.name = ko.observable('');
this.readme = ko.observable('');
this.addObservables({
saveError: '',
name: '',
readme: ''
});
this.configures = ko.observableArray([]);

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { StorageResultType, Notification } from 'Common/Enums';
import { getNotification } from 'Common/Translator';
import { HtmlEditor } from 'Common/HtmlEditor';
@ -19,38 +17,37 @@ class TemplatePopupView extends AbstractViewNext {
super();
this.editor = null;
this.signatureDom = ko.observable(null);
this.id = ko.observable('');
this.addObservables({
signatureDom: null,
this.name = ko.observable('');
this.name.error = ko.observable(false);
this.name.focus = ko.observable(false);
id: '',
this.body = ko.observable('');
this.body.loading = ko.observable(false);
this.body.error = ko.observable(false);
name: '',
nameError: false,
nameFocus: false,
this.name.subscribe(() => {
this.name.error(false);
body: '',
bodyLoading: false,
bodyError: false,
submitRequest: false,
submitError: ''
});
this.body.subscribe(() => {
this.body.error(false);
});
this.name.subscribe(() => this.nameError(false));
this.submitRequest = ko.observable(false);
this.submitError = ko.observable('');
this.body.subscribe(() => this.bodyError(false));
}
@command((self) => !self.submitRequest())
addTemplateCommand() {
this.populateBodyFromEditor();
this.name.error(!this.name().trim());
this.body.error(!this.body().trim() || ':HTML:' === this.body().trim());
this.nameError(!this.name().trim());
this.bodyError(!this.body().trim() || ':HTML:' === this.body().trim());
if (this.name.error() || this.body.error()) {
if (this.nameError() || this.bodyError()) {
return false;
}
@ -82,11 +79,11 @@ class TemplatePopupView extends AbstractViewNext {
this.id('');
this.name('');
this.name.error(false);
this.nameError(false);
this.body('');
this.body.loading(false);
this.body.error(false);
this.bodyLoading(false);
this.bodyError(false);
this.submitRequest(false);
this.submitError('');
@ -125,11 +122,11 @@ class TemplatePopupView extends AbstractViewNext {
if (template.populated) {
this.editorSetBody(this.body());
} else {
this.body.loading(true);
this.body.error(false);
this.bodyLoading(true);
this.bodyError(false);
Remote.templateGetById((result, data) => {
this.body.loading(false);
this.bodyLoading(false);
if (
StorageResultType.Success === result &&
@ -141,10 +138,10 @@ class TemplatePopupView extends AbstractViewNext {
template.populated = true;
this.body(template.body);
this.body.error(false);
this.bodyError(false);
} else {
this.body('');
this.body.error(true);
this.bodyError(true);
}
this.editorSetBody(this.body());
@ -156,7 +153,7 @@ class TemplatePopupView extends AbstractViewNext {
}
onShowWithDelay() {
this.name.focus(true);
this.nameFocus(true);
}
}

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { Capa, StorageResultType } from 'Common/Enums';
import { pString } from 'Common/Utils';
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
@ -17,68 +15,72 @@ class TwoFactorConfigurationPopupView extends AbstractViewNext {
constructor() {
super();
this.lock = ko.observable(false);
this.addObservables({
lock: false,
processing: false,
clearing: false,
secreting: false,
viewUser: '',
twoFactorStatus: false,
twoFactorTested: false,
viewSecret: '',
viewBackupCodes: '',
viewUrlTitle: '',
viewUrl: '',
viewEnable_: false
});
this.capaTwoFactor = rl.settings.capa(Capa.TwoFactor);
this.processing = ko.observable(false);
this.clearing = ko.observable(false);
this.secreting = ko.observable(false);
this.viewUser = ko.observable('');
this.twoFactorStatus = ko.observable(false);
this.twoFactorTested = ko.observable(false);
this.viewSecret = ko.observable('');
this.viewBackupCodes = ko.observable('');
this.viewUrlTitle = ko.observable('');
this.viewUrl = ko.observable('');
this.viewEnable_ = ko.observable(false);
this.viewEnable = ko.computed({
read: this.viewEnable_,
write: (value) => {
value = !!value;
if (value && this.twoFactorTested()) {
this.viewEnable_(value);
Remote.enableTwoFactor((result, data) => {
if (StorageResultType.Success !== result || !data || !data.Result) {
this.viewEnable_(false);
}
}, true);
} else {
if (!value) {
this.addComputables({
viewEnable: {
read: this.viewEnable_,
write: (value) => {
value = !!value;
if (value && this.twoFactorTested()) {
this.viewEnable_(value);
}
Remote.enableTwoFactor((result, data) => {
if (StorageResultType.Success !== result || !data || !data.Result) {
this.viewEnable_(false);
Remote.enableTwoFactor((result, data) => {
if (StorageResultType.Success !== result || !data || !data.Result) {
this.viewEnable_(false);
}
}, true);
} else {
if (!value) {
this.viewEnable_(value);
}
}, false);
Remote.enableTwoFactor((result, data) => {
if (StorageResultType.Success !== result || !data || !data.Result) {
this.viewEnable_(false);
}
}, false);
}
}
}
});
},
this.viewTwoFactorEnableTooltip = ko.computed(() => {
translatorTrigger();
return this.twoFactorTested() || this.viewEnable_()
? ''
: i18n('POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_TEST_BEFORE_DESC');
});
viewTwoFactorEnableTooltip: () => {
translatorTrigger();
return this.twoFactorTested() || this.viewEnable_()
? ''
: i18n('POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_TEST_BEFORE_DESC');
},
this.viewTwoFactorStatus = ko.computed(() => {
translatorTrigger();
return i18n(
this.twoFactorStatus()
? 'POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_CONFIGURED_DESC'
: 'POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC'
);
});
viewTwoFactorStatus: () => {
translatorTrigger();
return i18n(
this.twoFactorStatus()
? 'POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_CONFIGURED_DESC'
: 'POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC'
);
},
this.twoFactorAllowedEnable = ko.computed(() => this.viewEnable() || this.twoFactorTested());
twoFactorAllowedEnable: () => this.viewEnable() || this.twoFactorTested()
});
this.onResult = this.onResult.bind(this);
this.onShowSecretResult = this.onShowSecretResult.bind(this);

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { StorageResultType } from 'Common/Enums';
import Remote from 'Remote/User/Fetch';
@ -15,13 +13,15 @@ class TwoFactorTestPopupView extends AbstractViewNext {
constructor() {
super();
this.code = ko.observable('');
this.code.focused = ko.observable(false);
this.code.status = ko.observable(null);
this.addObservables({
code: '',
codeFocused: false,
codeStatus: null,
testing: false
});
this.koTestedTrigger = null;
this.testing = ko.observable(false);
}
@command((self) => self.code() && !self.testing())
@ -29,9 +29,9 @@ class TwoFactorTestPopupView extends AbstractViewNext {
this.testing(true);
Remote.testTwoFactor((result, data) => {
this.testing(false);
this.code.status(StorageResultType.Success === result && data && !!data.Result);
this.codeStatus(StorageResultType.Success === result && data && !!data.Result);
if (this.koTestedTrigger && this.code.status()) {
if (this.koTestedTrigger && this.codeStatus()) {
this.koTestedTrigger(true);
}
}, this.code());
@ -39,8 +39,8 @@ class TwoFactorTestPopupView extends AbstractViewNext {
clearPopup() {
this.code('');
this.code.focused(false);
this.code.status(null);
this.codeFocused(false);
this.codeStatus(null);
this.testing(false);
this.koTestedTrigger = null;
@ -54,7 +54,7 @@ class TwoFactorTestPopupView extends AbstractViewNext {
onShowWithDelay() {
// rl.settings.app('mobile') ||
this.code.focused(true);
this.codeFocused(true);
}
}

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { KeyState } from 'Common/Enums';
import { popup } from 'Knoin/Knoin';
@ -13,8 +11,10 @@ class ViewOpenPgpKeyPopupView extends AbstractViewNext {
constructor() {
super();
this.key = ko.observable('');
this.keyDom = ko.observable(null);
this.addObservables({
key: '',
keyDom: null
});
this.sDefaultKeyScope = KeyState.PopupViewOpenPGP;
}

View file

@ -37,17 +37,31 @@ class LoginUserView extends AbstractViewNext {
this.hideSubmitButton = Settings.app('hideSubmitButton');
this.welcome = ko.observable(!!Settings.get('UseLoginWelcomePage'));
this.addObservables({
welcome: !!Settings.get('UseLoginWelcomePage'),
email: '',
password: '',
signMe: false,
additionalCode: '',
this.email = ko.observable('');
this.password = ko.observable('');
this.signMe = ko.observable(false);
emailError: false,
passwordError: false,
this.additionalCode = ko.observable('');
this.additionalCode.error = ko.observable(false);
this.additionalCode.errorAnimation = ko.observable(false).extend({ falseTimeout: 500 });
this.additionalCode.visibility = ko.observable(false);
this.additionalCodeSignMe = ko.observable(false);
formHidden: false,
submitRequest: false,
submitError: '',
submitErrorAddidional: '',
langRequest: false,
additionalCodeError: false,
additionalCodeSignMe: false,
additionalCodeVisibility: false,
signMeType: LoginSignMeType.Unused
});
this.additionalCodeErrorAnimation = ko.observable(false).extend({ falseTimeout: 500 });
this.logoImg = (Settings.get('LoginLogo')||'').trim();
this.loginDescription = (Settings.get('LoginDescription')||'').trim();
@ -58,61 +72,50 @@ class LoginUserView extends AbstractViewNext {
this.forgotPasswordLinkUrl = Settings.app('forgotPasswordLinkUrl');
this.registrationLinkUrl = Settings.app('registrationLinkUrl');
this.emailError = ko.observable(false);
this.passwordError = ko.observable(false);
this.emailErrorAnimation = ko.observable(false).extend({ falseTimeout: 500 });
this.passwordErrorAnimation = ko.observable(false).extend({ falseTimeout: 500 });
this.formHidden = ko.observable(false);
this.addComputables({
formError:
() =>
this.emailErrorAnimation() ||
this.passwordErrorAnimation() ||
(this.additionalCodeVisibility() && this.additionalCodeErrorAnimation()),
this.formError = ko.computed(
() =>
this.emailErrorAnimation() ||
this.passwordErrorAnimation() ||
(this.additionalCode.visibility() && this.additionalCode.errorAnimation())
);
languageFullName: () => convertLangName(this.language()),
this.email.subscribe(() => {
this.emailError(false);
this.additionalCode('');
this.additionalCode.visibility(false);
signMeVisibility: () => LoginSignMeType.Unused !== this.signMeType()
});
this.password.subscribe(() => this.passwordError(false));
this.addSubscribables({
email: () => {
this.emailError(false);
this.additionalCode('');
this.additionalCodeVisibility(false);
},
this.additionalCode.subscribe(() => this.additionalCode.error(false));
password: () => this.passwordError(false),
this.additionalCode.visibility.subscribe(() => this.additionalCode.error(false));
additionalCode: () => this.additionalCodeError(false),
additionalCodeError: bV => this.additionalCodeErrorAnimation(!!bV),
additionalCodeVisibility: () => this.additionalCodeError(false),
this.emailError.subscribe(bV => this.emailErrorAnimation(!!bV));
emailError: bV => this.emailErrorAnimation(!!bV),
this.passwordError.subscribe(bV => this.passwordErrorAnimation(!!bV));
passwordError: bV => this.passwordErrorAnimation(!!bV),
this.additionalCode.error.subscribe(bV => this.additionalCode.errorAnimation(!!bV));
submitError: value => value || this.submitErrorAddidional(''),
this.submitRequest = ko.observable(false);
this.submitError = ko.observable('');
this.submitErrorAddidional = ko.observable('');
this.submitError.subscribe(value => value || this.submitErrorAddidional(''));
signMeType: iValue => this.signMe(LoginSignMeType.DefaultOn === iValue)
});
this.allowLanguagesOnLogin = !!Settings.get('AllowLanguagesOnLogin');
this.langRequest = ko.observable(false);
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
this.bSendLanguage = false;
this.languageFullName = ko.computed(() => convertLangName(this.language()));
this.signMeType = ko.observable(LoginSignMeType.Unused);
this.signMeType.subscribe(iValue => this.signMe(LoginSignMeType.DefaultOn === iValue));
this.signMeVisibility = ko.computed(() => LoginSignMeType.Unused !== this.signMeType());
if (Settings.get('AdditionalLoginError') && !this.submitError()) {
this.submitError(Settings.get('AdditionalLoginError'));
}
@ -128,10 +131,10 @@ class LoginUserView extends AbstractViewNext {
this.passwordError(false);
let error;
if (this.additionalCode.visibility()) {
this.additionalCode.error(false);
if (this.additionalCodeVisibility()) {
this.additionalCodeError(false);
if (!this.additionalCode().trim()) {
this.additionalCode.error(true);
this.additionalCodeError(true);
error = '.inputAdditionalCode';
}
}
@ -157,7 +160,7 @@ class LoginUserView extends AbstractViewNext {
if (oData.Result) {
if (oData.TwoFactorAuth) {
this.additionalCode('');
this.additionalCode.visibility(true);
this.additionalCodeVisibility(true);
this.submitRequest(false);
setTimeout(() => this.querySelector('.inputAdditionalCode').focus(), 100);
@ -192,8 +195,8 @@ class LoginUserView extends AbstractViewNext {
sLoginPassword,
!!this.signMe(),
this.bSendLanguage ? this.language() : '',
this.additionalCode.visibility() ? this.additionalCode() : '',
this.additionalCode.visibility() ? !!this.additionalCodeSignMe() : false
this.additionalCodeVisibility() ? this.additionalCode() : '',
this.additionalCodeVisibility() ? !!this.additionalCodeSignMe() : false
);
Local.set(ClientSideKeyName.LastSignMe, this.signMe() ? '-1-' : '-0-');

View file

@ -108,97 +108,87 @@ class MessageListMailBoxUserView extends AbstractViewNext {
this.userUsageSize = QuotaStore.usage;
this.userUsageProc = QuotaStore.percentage;
this.moveDropdownTrigger = ko.observable(false);
this.moreDropdownTrigger = ko.observable(false);
this.addObservables({
moveDropdownTrigger: false,
moreDropdownTrigger: false,
dragOverArea: null,
dragOverBodyArea: null,
inputMessageListSearchFocus: false
});
// append drag and drop
this.dragOver = ko.observable(false).extend({ 'throttle': 1 });
this.dragOverEnter = ko.observable(false).extend({ 'throttle': 1 });
this.dragOverArea = ko.observable(null);
this.dragOverBodyArea = ko.observable(null);
this.messageListItemTemplate = ko.computed(() =>
this.mobile || Layout.SidePreview === SettingsStore.layout()
? 'MailMessageListItem'
: 'MailMessageListItemNoPreviewPane'
);
this.messageListSearchDesc = ko.computed(() => {
const value = MessageStore.messageListEndSearch();
return value ? i18n('MESSAGE_LIST/SEARCH_RESULT_FOR', { 'SEARCH': value }) : '';
});
this.messageListPaginator = ko.computed(
computedPaginatorHelper(MessageStore.messageListPage, MessageStore.messageListPageCount)
);
this.checkAll = ko.computed({
read: () => 0 < MessageStore.messageListChecked().length,
write: (value) => {
value = !!value;
MessageStore.messageList().forEach(message => message.checked(value));
}
});
this.inputMessageListSearchFocus = ko.observable(false);
this.sLastSearchValue = '';
this.inputProxyMessageListSearch = ko.computed({
read: this.mainMessageListSearch,
write: value => this.sLastSearchValue = value
this.addComputables({
messageListItemTemplate: () =>
this.mobile || Layout.SidePreview === SettingsStore.layout()
? 'MailMessageListItem'
: 'MailMessageListItemNoPreviewPane',
messageListSearchDesc: () => {
const value = MessageStore.messageListEndSearch();
return value ? i18n('MESSAGE_LIST/SEARCH_RESULT_FOR', { 'SEARCH': value }) : ''
},
messageListPaginator: computedPaginatorHelper(MessageStore.messageListPage, MessageStore.messageListPageCount),
checkAll: {
read: () => 0 < MessageStore.messageListChecked().length,
write: (value) => {
value = !!value;
MessageStore.messageList().forEach(message => message.checked(value));
}
},
inputProxyMessageListSearch: {
read: this.mainMessageListSearch,
write: value => this.sLastSearchValue = value
},
isIncompleteChecked: () => {
const c = MessageStore.messageListChecked().length;
return c && MessageStore.messageList().length > c;
},
hasMessages: () => 0 < this.messageList().length,
hasCheckedOrSelectedLines: () => 0 < this.messageListCheckedOrSelected().length,
isSpamFolder: () => FolderStore.spamFolder() === this.messageListEndFolder() && FolderStore.spamFolder(),
isSpamDisabled: () => UNUSED_OPTION_VALUE === FolderStore.spamFolder(),
isTrashFolder: () => FolderStore.trashFolder() === this.messageListEndFolder() && FolderStore.trashFolder(),
isDraftFolder: () => FolderStore.draftFolder() === this.messageListEndFolder() && FolderStore.draftFolder(),
isSentFolder: () => FolderStore.sentFolder() === this.messageListEndFolder() && FolderStore.sentFolder(),
isArchiveFolder: () => FolderStore.archiveFolder() === this.messageListEndFolder() && FolderStore.archiveFolder(),
isArchiveDisabled: () => UNUSED_OPTION_VALUE === FolderStore.archiveFolder(),
isArchiveVisible: () => !this.isArchiveFolder() && !this.isArchiveDisabled() && !this.isDraftFolder(),
isSpamVisible: () =>
!this.isSpamFolder() && !this.isSpamDisabled() && !this.isDraftFolder() && !this.isSentFolder(),
isUnSpamVisible: () =>
this.isSpamFolder() && !this.isSpamDisabled() && !this.isDraftFolder() && !this.isSentFolder(),
mobileCheckedStateShow: () => this.mobile ? 0 < MessageStore.messageListChecked().length : true,
mobileCheckedStateHide: () => this.mobile ? !MessageStore.messageListChecked().length : true,
messageListFocused: () => Focused.MessageList === AppStore.focusedState()
});
this.isIncompleteChecked = ko.computed(() => {
const c = MessageStore.messageListChecked().length;
return c && MessageStore.messageList().length > c;
});
this.hasMessages = ko.computed(() => 0 < this.messageList().length);
this.hasCheckedOrSelectedLines = ko.computed(() => 0 < this.messageListCheckedOrSelected().length);
this.isSpamFolder = ko.computed(
() => FolderStore.spamFolder() === this.messageListEndFolder() && FolderStore.spamFolder()
);
this.isSpamDisabled = ko.computed(() => UNUSED_OPTION_VALUE === FolderStore.spamFolder());
this.isTrashFolder = ko.computed(
() => FolderStore.trashFolder() === this.messageListEndFolder() && FolderStore.trashFolder()
);
this.isDraftFolder = ko.computed(
() => FolderStore.draftFolder() === this.messageListEndFolder() && FolderStore.draftFolder()
);
this.isSentFolder = ko.computed(
() => FolderStore.sentFolder() === this.messageListEndFolder() && FolderStore.sentFolder()
);
this.isArchiveFolder = ko.computed(
() => FolderStore.archiveFolder() === this.messageListEndFolder() && FolderStore.archiveFolder()
);
this.isArchiveDisabled = ko.computed(() => UNUSED_OPTION_VALUE === FolderStore.archiveFolder());
this.isArchiveVisible = ko.computed(
() => !this.isArchiveFolder() && !this.isArchiveDisabled() && !this.isDraftFolder()
);
this.isSpamVisible = ko.computed(
() => !this.isSpamFolder() && !this.isSpamDisabled() && !this.isDraftFolder() && !this.isSentFolder()
);
this.isUnSpamVisible = ko.computed(
() => this.isSpamFolder() && !this.isSpamDisabled() && !this.isDraftFolder() && !this.isSentFolder()
);
// this.messageListChecked = MessageStore.messageListChecked;
this.mobileCheckedStateShow = ko.computed(() => this.mobile ? 0 < MessageStore.messageListChecked().length : true);
this.mobileCheckedStateHide = ko.computed(() => this.mobile ? !MessageStore.messageListChecked().length : true);
this.messageListFocused = ko.computed(() => Focused.MessageList === AppStore.focusedState());
this.canBeMoved = this.hasCheckedOrSelectedLines;

View file

@ -16,7 +16,7 @@ import {
import { $htmlCL, leftPanelDisabled, keyScopeReal, moveAction } from 'Common/Globals';
import { inFocus } from 'Common/Utils';
import { mailToHelper, isTransparent } from 'Common/UtilsUser';
import { mailToHelper } from 'Common/UtilsUser';
import Audio from 'Common/Audio';
@ -40,6 +40,10 @@ import { AbstractViewNext } from 'Knoin/AbstractViewNext';
const Settings = rl.settings;
function isTransparent(color) {
return 'rgba(0, 0, 0, 0)' === color || 'transparent' === color;
}
@view({
name: 'View/User/MailBox/MessageView',
type: ViewType.Right,
@ -67,7 +71,14 @@ class MessageViewMailBoxUserView extends AbstractViewNext {
this.oHeaderDom = null;
this.oMessageScrollerDom = null;
this.bodyBackgroundColor = ko.observable('');
this.addObservables({
bodyBackgroundColor: '',
showAttachmnetControls: false,
downloadAsZipLoading: false,
lastReplyAction_: '',
showFullInfo: '1' === Local.get(ClientSideKeyName.MessageHeaderFullInfo),
moreDropdownTrigger: false
});
this.pswp = null;
@ -103,50 +114,12 @@ class MessageViewMailBoxUserView extends AbstractViewNext {
this.messageListOfThreadsLoading = ko.observable(false).extend({ rateLimit: 1 });
this.highlightUnselectedAttachments = ko.observable(false).extend({ falseTimeout: 2000 });
this.showAttachmnetControls = ko.observable(false);
this.showAttachmnetControlsState = v => Local.set(ClientSideKeyName.MessageAttachmentControls, !!v);
this.allowAttachmnetControls = ko.computed(
() => this.attachmentsActions().length && Settings.capa(Capa.AttachmentsActions)
);
this.downloadAsZipAllowed = ko.computed(
() => this.attachmentsActions().includes('zip') && this.allowAttachmnetControls()
);
this.downloadAsZipLoading = ko.observable(false);
this.downloadAsZipError = ko.observable(false).extend({ falseTimeout: 7000 });
this.showAttachmnetControls.subscribe(v => this.message()
&& this.message().attachments().forEach(item => item && item.checked(!!v))
);
this.lastReplyAction_ = ko.observable('');
this.lastReplyAction = ko.computed({
read: this.lastReplyAction_,
write: value => this.lastReplyAction_(
[ComposeType.Reply, ComposeType.ReplyAll, ComposeType.Forward].includes(value)
? ComposeType.Reply
: value
)
});
this.lastReplyAction(Local.get(ClientSideKeyName.LastReplyAction) || ComposeType.Reply);
this.lastReplyAction_.subscribe(value => Local.set(ClientSideKeyName.LastReplyAction, value));
this.showFullInfo = ko.observable('1' === Local.get(ClientSideKeyName.MessageHeaderFullInfo));
this.moreDropdownTrigger = ko.observable(false);
this.messageDomFocused = ko.observable(false).extend({ rateLimit: 0 });
this.messageVisibility = ko.computed(() => !this.messageLoadingThrottle() && !!this.message());
this.message.subscribe(message => (!message) && MessageStore.selectorMessageSelected(null));
this.canBeRepliedOrForwarded = ko.computed(() => !this.isDraftFolder() && this.messageVisibility());
// commands
this.replyCommand = createCommandReplyHelper(ComposeType.Reply);
this.replyAllCommand = createCommandReplyHelper(ComposeType.ReplyAll);
@ -162,97 +135,85 @@ class MessageViewMailBoxUserView extends AbstractViewNext {
// viewer
this.viewBodyTopValue = ko.observable(0);
this.viewFolder = '';
this.viewUid = '';
this.viewHash = '';
this.viewSubject = ko.observable('');
this.viewFromShort = ko.observable('');
this.viewFromDkimData = ko.observable(['none', '']);
this.viewToShort = ko.observable('');
this.viewFrom = ko.observable('');
this.viewTo = ko.observable('');
this.viewCc = ko.observable('');
this.viewBcc = ko.observable('');
this.viewReplyTo = ko.observable('');
this.viewTimeStamp = ko.observable(0);
this.viewSize = ko.observable('');
this.viewLineAsCss = ko.observable('');
this.viewViewLink = ko.observable('');
this.viewUnsubscribeLink = ko.observable('');
this.viewDownloadLink = ko.observable('');
this.viewIsImportant = ko.observable(false);
this.viewIsFlagged = ko.observable(false);
this.viewFromDkimVisibility = ko.computed(() => 'none' !== this.viewFromDkimData()[0]);
this.viewFromDkimStatusIconClass = ko.computed(() => {
switch (this.viewFromDkimData()[0]) {
case 'none':
return 'icon-none iconcolor-display-none';
case 'pass':
return 'icon-ok iconcolor-green';
default:
return 'icon-warning-alt iconcolor-red';
}
this.addObservables({
viewBodyTopValue: 0,
viewSubject: '',
viewFromShort: '',
viewFromDkimData: ['none', ''],
viewToShort: '',
viewFrom: '',
viewTo: '',
viewCc: '',
viewBcc: '',
viewReplyTo: '',
viewTimeStamp: 0,
viewSize: '',
viewLineAsCss: '',
viewViewLink: '',
viewUnsubscribeLink: '',
viewDownloadLink: '',
viewIsImportant: false,
viewIsFlagged: false
});
this.viewFromDkimStatusTitle = ko.computed(() => {
const status = this.viewFromDkimData();
if (Array.isNotEmpty(status)) {
if (status[0]) {
return status[1] || 'DKIM: ' + status[0];
}
}
this.addSubscribables({
showAttachmnetControls: v => this.message()
&& this.message().attachments().forEach(item => item && item.checked(!!v)),
return '';
});
lastReplyAction_: value => Local.set(ClientSideKeyName.LastReplyAction, value),
this.messageActiveDom.subscribe(dom => this.bodyBackgroundColor(this.detectDomBackgroundColor(dom)), this);
messageActiveDom: dom => this.bodyBackgroundColor(this.detectDomBackgroundColor(dom)),
this.message.subscribe((message) => {
this.messageActiveDom(null);
message: message => {
this.messageActiveDom(null);
if (message) {
this.showAttachmnetControls(false);
if (Local.get(ClientSideKeyName.MessageAttachmentControls)) {
setTimeout(() => {
this.showAttachmnetControls(true);
}, 50);
}
if (message) {
this.showAttachmnetControls(false);
if (Local.get(ClientSideKeyName.MessageAttachmentControls)) {
setTimeout(() => {
this.showAttachmnetControls(true);
}, 50);
}
if (this.viewHash !== message.hash) {
this.scrollMessageToTop();
}
this.viewFolder = message.folder;
this.viewUid = message.uid;
this.viewHash = message.hash;
this.viewSubject(message.subject());
this.viewFromShort(message.fromToLine(true, true));
this.viewFromDkimData(message.fromDkimData());
this.viewToShort(message.toToLine(true, true));
this.viewFrom(message.fromToLine(false));
this.viewTo(message.toToLine(false));
this.viewCc(message.ccToLine(false));
this.viewBcc(message.bccToLine(false));
this.viewReplyTo(message.replyToToLine(false));
this.viewTimeStamp(message.dateTimeStampInUTC());
this.viewSize(message.friendlySize());
this.viewLineAsCss(message.lineAsCss());
this.viewViewLink(message.viewLink());
this.viewUnsubscribeLink(message.getFirstUnsubsribeLink());
this.viewDownloadLink(message.downloadLink());
this.viewIsImportant(message.isImportant());
this.viewIsFlagged(message.isFlagged());
} else {
MessageStore.selectorMessageSelected(null);
this.viewFolder = '';
this.viewUid = '';
this.viewHash = '';
if (this.viewHash !== message.hash) {
this.scrollMessageToTop();
}
},
this.viewFolder = message.folder;
this.viewUid = message.uid;
this.viewHash = message.hash;
this.viewSubject(message.subject());
this.viewFromShort(message.fromToLine(true, true));
this.viewFromDkimData(message.fromDkimData());
this.viewToShort(message.toToLine(true, true));
this.viewFrom(message.fromToLine(false));
this.viewTo(message.toToLine(false));
this.viewCc(message.ccToLine(false));
this.viewBcc(message.bccToLine(false));
this.viewReplyTo(message.replyToToLine(false));
this.viewTimeStamp(message.dateTimeStampInUTC());
this.viewSize(message.friendlySize());
this.viewLineAsCss(message.lineAsCss());
this.viewViewLink(message.viewLink());
this.viewUnsubscribeLink(message.getFirstUnsubsribeLink());
this.viewDownloadLink(message.downloadLink());
this.viewIsImportant(message.isImportant());
this.viewIsFlagged(message.isFlagged());
} else {
this.viewFolder = '';
this.viewUid = '';
this.viewHash = '';
this.scrollMessageToTop();
}
fullScreenMode: value => $htmlCL.toggle('rl-message-fullscreen', value)
});
this.message.viewTrigger.subscribe(() => {
@ -260,13 +221,55 @@ class MessageViewMailBoxUserView extends AbstractViewNext {
message ? this.viewIsFlagged(message.isFlagged()) : this.viewIsFlagged(false);
});
this.fullScreenMode.subscribe(value => $htmlCL.toggle('rl-message-fullscreen', value));
this.addComputables({
allowAttachmnetControls: () => this.attachmentsActions().length && Settings.capa(Capa.AttachmentsActions),
this.messageFocused = ko.computed(() => Focused.MessageView === AppStore.focusedState());
downloadAsZipAllowed: () => this.attachmentsActions().includes('zip') && this.allowAttachmnetControls(),
this.messageListAndMessageViewLoading = ko.computed(
() => MessageStore.messageListCompleteLoadingThrottle() || MessageStore.messageLoadingThrottle()
);
lastReplyAction: {
read: this.lastReplyAction_,
write: value => this.lastReplyAction_(
[ComposeType.Reply, ComposeType.ReplyAll, ComposeType.Forward].includes(value)
? ComposeType.Reply
: value
)
},
messageVisibility: () => !this.messageLoadingThrottle() && !!this.message(),
canBeRepliedOrForwarded: () => !this.isDraftFolder() && this.messageVisibility(),
viewFromDkimVisibility: () => 'none' !== this.viewFromDkimData()[0],
viewFromDkimStatusIconClass:() => {
switch (this.viewFromDkimData()[0]) {
case 'none':
return 'icon-none iconcolor-display-none';
case 'pass':
return 'icon-ok iconcolor-green';
default:
return 'icon-warning-alt iconcolor-red';
}
},
viewFromDkimStatusTitle:() => {
const status = this.viewFromDkimData();
if (Array.isNotEmpty(status)) {
if (status[0]) {
return status[1] || 'DKIM: ' + status[0];
}
}
return '';
},
messageFocused: () => Focused.MessageView === AppStore.focusedState(),
messageListAndMessageViewLoading:
() => MessageStore.messageListCompleteLoadingThrottle() || MessageStore.messageLoadingThrottle()
});
this.lastReplyAction(Local.get(ClientSideKeyName.LastReplyAction) || ComposeType.Reply);
addEventListener('mailbox.message-view.toggle-full-screen', () => this.toggleFullScreen());

View file

@ -30,7 +30,7 @@ class Folder implements \JsonSerializable
/**
* @var bool
*/
private $bExisten;
private $bExists;
/**
* @var bool
@ -50,7 +50,7 @@ class Folder implements \JsonSerializable
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
*/
function __construct(\MailSo\Imap\Folder $oImapFolder, bool $bSubscribed = true, bool $bExisten = true)
function __construct(\MailSo\Imap\Folder $oImapFolder, bool $bSubscribed = true, bool $bExists = true)
{
$this->oImapFolder = $oImapFolder;
$this->oSubFolders = null;
@ -66,7 +66,7 @@ class Folder implements \JsonSerializable
}
$this->bSubscribed = $bSubscribed;
$this->bExisten = $bExisten;
$this->bExists = $bExists;
}
/**
@ -164,12 +164,12 @@ class Folder implements \JsonSerializable
public function IsExists() : bool
{
return $this->bExisten;
return $this->bExists;
}
public function IsSelectable() : bool
{
return $this->IsExists() && $this->oImapFolder->IsSelectable();
return $this->bExists && $this->oImapFolder->IsSelectable();
}
/**
@ -236,9 +236,9 @@ class Folder implements \JsonSerializable
'FullNameRaw' => $this->FullNameRaw(),
'Delimiter' => (string) $this->Delimiter(),
'HasVisibleSubFolders' => $this->HasVisibleSubFolders(),
'IsSubscribed' => $this->IsSubscribed(),
'IsExists' => $this->IsExists(),
'IsSelectable' => $this->IsSelectable(),
'Subscribed' => $this->bSubscribed,
'Exists' => $this->bExists,
'Selectable' => $this->IsSelectable(),
'Flags' => $this->FlagsLowerCase()
);
}

View file

@ -59,7 +59,7 @@
</div>
</div>
<div class="controls"
data-bind="visible: additionalCode.visibility(), css: {'error': additionalCode.error, 'animated': additionalCode.errorAnimation}">
data-bind="visible: additionalCodeVisibility(), css: {'error': additionalCodeError, 'animated': additionalCodeErrorAnimation}">
<div class="input-append">
<input type="text" class="i18n input-block-level inputAdditionalCode" autocomplete="off"
autocorrect="off" autocapitalize="off" spellcheck="false" style="padding-right: 35px;"
@ -71,7 +71,7 @@
</div>
</div>
<div class="controls plugin-mark-Login-BottomControlGroup"
data-bind="visible: additionalCode.visibility()">
data-bind="visible: additionalCodeVisibility()">
<div class="additionalCodeSignMeLabel" data-bind="component: {
name: 'CheckboxSimple',
params: {

View file

@ -1,6 +1,6 @@
<div class="e-item" data-bind="visible: visible, css: { 'i-am-inbox-wrapper': isInbox }">
<a class="e-link" data-bind="dropmessages: $data,
css: { 'i-am-inbox': isInbox, 'selected': selected() && !isSystemFolder(), 'selectable': selectableForFolderList, 'hidden' : hidden, 'print-count': hasUnreadMessages, 'unread-sub': hasSubScribedUnreadMessagesSubfolders, 'system': isSystemFolder, 'anim-action-class': actionBlink }">
css: { 'i-am-inbox': isInbox, 'selected': selected() && !isSystemFolder(), 'selectable': selectableForFolderList, 'hidden' : hidden, 'print-count': hasUnreadMessages, 'unread-sub': hasSubscribedUnreadMessagesSubfolders, 'system': isSystemFolder, 'anim-action-class': actionBlink }">
<span class="badge pull-right count" data-bind="text: printableUnreadCount"></span>
<i data-bind="css: collapsedCss()"></i>
<span class="focused-poiner"></span>

View file

@ -7,9 +7,9 @@
</h3>
</div>
<div class="modal-body">
<div class="alert" data-bind="visible: key.error() && key.errorMessage(), text: key.errorMessage"></div>
<div class="alert" data-bind="visible: keyError() && keyErrorMessage(), text: keyErrorMessage"></div>
<div class="form-horizontal">
<div class="control-group" data-bind="css: {'error': key.error}">
<div class="control-group" data-bind="css: {'error': keyError}">
<textarea class="inputKey input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: key"></textarea>
</div>
</div>

View file

@ -111,7 +111,7 @@
</label>
</div>
<div class="e-cell e-value">
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: to, emailsTagsFocus: to.focused, autoCompleteSource: emailsSource" />
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: to, emailsTagsFocus: toFocused, autoCompleteSource: emailsSource" />
</div>
</div>
<div class="e-row cc-row" data-bind="visible: showCc">
@ -143,7 +143,7 @@
<span class="i18n" data-i18n="COMPOSE/TITLE_SUBJECT"></span>
</div>
<div class="e-cell e-value">
<input type="text" size="70" autocomplete="off" data-bind="textInput: subject, hasFocus: subject.focused" />
<input type="text" size="70" autocomplete="off" data-bind="textInput: subject, hasFocus: subjectFocused" />
</div>
</div>
<div class="e-row">

View file

@ -15,7 +15,7 @@
</label>
<div class="controls">
<input type="text" class="uiInput inputName" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="textInput: folderName, hasfocus: folderName.focused, onEnter: createFolderCommand" />
data-bind="textInput: folderName, hasfocus: folderNameFocused, onEnter: createFolderCommand" />
</div>
</div>
<div class="control-group">

View file

@ -19,7 +19,7 @@
<div class="controls">
<input type="email" class="inputEmail input-xlarge" autofocus=""
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: email, onEnter: addOrEditIdentityCommand, hasfocus: email.focused" />
data-bind="value: email, onEnter: addOrEditIdentityCommand, hasfocus: emailFocused" />
</div>
</div>
<div class="control-group" data-bind="visible: owner">
@ -41,7 +41,7 @@
<div class="controls">
<input type="text" class="inputReplyTo input-xlarge"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: replyTo, onEnter: addOrEditIdentityCommand, hasfocus: replyTo.focused" />
data-bind="value: replyTo, onEnter: addOrEditIdentityCommand, hasfocus: replyToFocused" />
</div>
</div>
<div class="control-group" data-bind="visible: showBcc, css: {'error': bcc.hasError}">
@ -49,7 +49,7 @@
<div class="controls">
<input type="text" class="inputBcc input-xlarge"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: bcc, onEnter: addOrEditIdentityCommand, hasfocus: bcc.focused" />
data-bind="value: bcc, onEnter: addOrEditIdentityCommand, hasfocus: bccFocused" />
</div>
</div>
<div class="control-group" data-bind="visible: !showReplyTo() || !showBcc()">

View file

@ -13,12 +13,12 @@
<span data-bind="text: submitError"></span>
</div>
<br />
<div class="control-group" data-bind="css: {'error': email.error}">
<div class="control-group" data-bind="css: {'error': emailError}">
<label class="i18n control-label" data-i18n="POPUPS_GENERATE_OPEN_PGP_KEYS/LABEL_EMAIL"></label>
<div class="controls">
<input type="email" class="inputEmail input-large"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: email, hasfocus: email.focus" />
data-bind="value: email, hasfocus: emailFocus" />
</div>
</div>
<div class="control-group">

View file

@ -14,17 +14,17 @@
<span data-bind="text: submitError"></span>
</div>
<br />
<div class="control-group" data-bind="css: {'error': name.error}">
<div class="control-group" data-bind="css: {'error': nameError}">
<label class="i18n control-label" data-i18n="POPUPS_ADD_TEMPLATE/LABEL_NAME"></label>
<div class="controls">
<input type="text" class="inputName input-xlarge" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="textInput: name, onEnter: addTemplateCommand, hasfocus: name.focus" />
data-bind="textInput: name, onEnter: addTemplateCommand, hasfocus: nameFocus" />
</div>
</div>
</div>
<hr />
<div class="form-horizontal">
<div class="control-group" data-bind="css: {'error': body.error}">
<div class="control-group" data-bind="css: {'error': bodyError}">
<div class="e-template-place" data-bind="initDom: signatureDom"></div>
</div>
</div>

View file

@ -16,14 +16,14 @@
<div class="controls">
<input type="text" class="uiInput inputName"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="textInput: code, hasfocus: code.focused, onEnter: testCodeCommand" />
data-bind="textInput: code, hasfocus: codeFocused, onEnter: testCodeCommand" />
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn" data-bind="command: testCodeCommand, css: { 'btn-success': true === code.status(), 'btn-danger': false === code.status() }">
<i data-bind="css: {'icon-ok': !testing(), 'icon-spinner animated': testing(), 'icon-white': true === code.status() || false === code.status() }"></i>
<a class="btn" data-bind="command: testCodeCommand, css: { 'btn-success': true === codeStatus(), 'btn-danger': false === codeStatus() }">
<i data-bind="css: {'icon-ok': !testing(), 'icon-spinner animated': testing(), 'icon-white': true === codeStatus() || false === codeStatus() }"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n="POPUPS_TWO_FACTOR_TEST/BUTTON_TEST"></span>
</a>

View file

@ -18,20 +18,20 @@
</span>
</td>
<td class="subscribe-folder-parent">
<span class="unsubscribe-folder" data-bind="visible: canBeSubScribed() && !subScribed(), click: function(oFolder) { $root.subscribeFolder(oFolder); }">
<span class="unsubscribe-folder" data-bind="visible: canBeSubscribed() && !subscribed(), click: function(oFolder) { $root.subscribeFolder(oFolder); }">
<i class="icon-eye"></i>
</span>
<span class="subscribe-folder" data-bind="visible: canBeSubScribed() && subScribed(), click: function(oFolder) { $root.unSubscribeFolder(oFolder); }">
<span class="subscribe-folder" data-bind="visible: canBeSubscribed() && subscribed(), click: function(oFolder) { $root.unSubscribeFolder(oFolder); }">
<i class="icon-eye"></i>
</span>
</td>
<td class="check-folder-parent" data-bind="visible: $root.displaySpecSetting">
<span class="uncheck-folder" data-bind="visible: canBeChecked() && subScribed() && !checkable(), click: function(oFolder) { $root.checkableTrueFolder(oFolder); }">
<span class="uncheck-folder" data-bind="visible: canBeChecked() && subscribed() && !checkable(), click: function(oFolder) { $root.checkableTrueFolder(oFolder); }">
<i class="icon-check-mark-circle-two"></i>
</span>
<span class="check-folder" data-bind="visible: canBeChecked() && subScribed() && checkable(), click: function(oFolder) { $root.checkableFalseFolder(oFolder); }">
<span class="check-folder" data-bind="visible: canBeChecked() && subscribed() && checkable(), click: function(oFolder) { $root.checkableFalseFolder(oFolder); }">
<i class="icon-check-mark-circle-two"></i>
</span>
</td>
</tr>
<!-- ko template: { name: 'SettingsFolderItem', foreach: subFolders } --><!-- /ko -->
<!-- ko template: { name: 'SettingsFolderItem', foreach: subFolders } --><!-- /ko -->