Prevent memory leaks in *Model

This commit is contained in:
djmaze 2020-10-25 11:46:58 +01:00
parent 48a9a03762
commit d7a4639d6b
22 changed files with 221 additions and 225 deletions

View file

@ -5,50 +5,52 @@ function disposeOne(disposable) {
}
}
function typeCast(current, value) {
switch (typeof current)
function typeCast(curValue, newValue) {
switch (typeof curValue)
{
case 'boolean': return !!value;
case 'number': return isFinite(value) ? parseFloat(value) : 0;
case 'string': return null != value ? '' + value : '';
case 'boolean': return !!newValue;
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
case 'string': return null != newValue ? '' + newValue : '';
case 'object':
if (current.constructor.reviveFromJson) {
return current.constructor.reviveFromJson(value) || undefined;
if (curValue.constructor.reviveFromJson) {
return curValue.constructor.reviveFromJson(newValue) || undefined;
}
if (!Array.isArray(current) || !Array.isArray(value))
if (!Array.isArray(curValue) || !Array.isArray(newValue))
return undefined;
}
return value;
return newValue;
}
export class AbstractModel {
disposables = [];
/**
* @param {string} modelName = ''
*/
constructor() {
/*
if (new.target === AbstractModel) {
throw new Error("Can't instantiate AbstractModel!");
}
this.sModelName = new.target.name;
*/
}
regDisposables(value) {
if (Array.isArray(value)) {
value.forEach(item => this.disposables.push(item));
} else if (value) {
this.disposables.push(value);
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)
);
*/
}
addSubscribables(obj) {
Object.entries(obj).forEach(([key, fn]) => this.disposables.push( this[key].subscribe(fn) ) );
}
onDestroy() {
if (Array.isArray(this.disposables)) {
this.disposables.forEach(disposeOne);
}
Object.values(this).forEach(disposeOne);
Object.entries(this).forEach(([key, value]) => {
disposeOne(value);
this[key] = null;
});
}
/**

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { change } from 'Common/Links';
import { AbstractModel } from 'Knoin/AbstractModel';
@ -15,10 +13,11 @@ class AccountModel extends AbstractModel {
this.email = email;
this.count = ko.observable(count);
this.deleteAccess = ko.observable(false);
this.canBeDeleted = ko.observable(!!canBeDelete);
this.addObservables({
count: count,
deleteAccess: false,
canBeDeleted: !!canBeDelete
});
this.canBeEdit = this.canBeDeleted;
}

View file

@ -24,16 +24,18 @@ class ComposeAttachmentModel extends AbstractModel {
this.contentLocation = contentLocation;
this.fromMessage = false;
this.fileName = ko.observable(fileName);
this.size = ko.observable(size);
this.tempName = ko.observable('');
this.addObservables({
fileName: fileName,
size: size,
tempName: '',
this.progress = ko.observable(0);
this.error = ko.observable('');
this.waiting = ko.observable(true);
this.uploading = ko.observable(false);
this.enabled = ko.observable(true);
this.complete = ko.observable(false);
progress: 0,
error: '',
waiting: true,
uploading: false,
enabled: true,
complete: false
});
this.progressText = ko.computed(() => {
const p = this.progress();
@ -56,15 +58,6 @@ class ComposeAttachmentModel extends AbstractModel {
this.mimeType = ko.computed(() => File.getContentType(this.fileName()));
this.fileExt = ko.computed(() => File.getExtension(this.fileName()));
this.regDisposables([
this.progressText,
this.progressStyle,
this.title,
this.friendlySize,
this.mimeType,
this.fileExt
]);
}
static fromAttachment(item)

View file

@ -1,5 +1,3 @@
import ko from 'ko';
import { ContactPropertyModel } from 'Model/ContactProperty';
import { ContactPropertyType } from 'Common/Enums';
@ -14,10 +12,12 @@ class ContactModel extends AbstractModel {
this.properties = [];
this.readOnly = false;
this.focused = ko.observable(false);
this.selected = ko.observable(false);
this.checked = ko.observable(false);
this.deleted = ko.observable(false);
this.addObservables({
focused: false,
selected: false,
checked: false,
deleted: false
});
}
/**

View file

@ -17,12 +17,14 @@ class ContactPropertyModel extends AbstractModel {
constructor(type = ContactPropertyType.Unknown, typeStr = '', value = '', focused = false, placeholder = '') {
super();
this.type = ko.observable(pInt(type));
this.typeStr = ko.observable(pString(typeStr));
this.focused = ko.observable(!!focused);
this.value = ko.observable(pString(value));
this.addObservables({
type: pInt(type),
typeStr: pString(typeStr),
focused: !!focused,
value: pString(value),
this.placeholder = ko.observable(placeholder);
placeholder: placeholder
});
this.placeholderValue = ko.computed(() => {
const v = this.placeholder();
@ -30,8 +32,6 @@ class ContactPropertyModel extends AbstractModel {
});
this.largeValue = ko.computed(() => ContactPropertyType.Note === this.type());
this.regDisposables([this.placeholderValue, this.largeValue]);
}
toJSON() {

View file

@ -15,43 +15,39 @@ class FilterModel extends AbstractModel {
constructor() {
super();
this.enabled = ko.observable(true);
this.id = '';
this.name = ko.observable('');
this.name.error = ko.observable(false);
this.name.focused = ko.observable(false);
this.addObservables({
enabled: true,
deleteAccess: false,
canBeDeleted: true,
this.conditions = ko.observableArray([]);
this.conditionsType = ko.observable(FilterRulesType.Any);
name: '',
nameError: false,
nameFocused: false,
conditionsType: FilterRulesType.Any,
// Actions
this.actionValue = ko.observable('');
this.actionValue.error = ko.observable(false);
actionValue: '',
actionValueError: false,
this.actionValueSecond = ko.observable('');
this.actionValueThird = ko.observable('');
actionValueSecond: '',
actionValueThird: '',
this.actionValueFourth = ko.observable('');
this.actionValueFourth.error = ko.observable(false);
actionValueFourth: '',
actionValueFourthError: false,
this.actionMarkAsRead = ko.observable(false);
actionMarkAsRead: false,
this.actionKeep = ko.observable(true);
this.actionNoStop = ko.observable(false);
actionKeep: true,
actionNoStop: false,
this.actionType = ko.observable(FiltersAction.MoveTo);
this.actionType.subscribe(() => {
this.actionValue('');
this.actionValue.error(false);
this.actionValueSecond('');
this.actionValueThird('');
this.actionValueFourth('');
this.actionValueFourth.error(false);
actionType: FiltersAction.MoveTo
});
this.conditions = ko.observableArray([]);
const fGetRealFolderName = (folderFullNameRaw) => {
const folder = getFolderFromCacheList(folderFullNameRaw);
return folder ? folder.fullName.replace('.' === folder.delimiter ? /\./ : /[\\/]+/, ' / ') : folderFullNameRaw;
@ -115,14 +111,18 @@ class FilterModel extends AbstractModel {
return result;
});
this.regDisposables(this.name.subscribe(sValue => this.name.error(!sValue)));
this.regDisposables(this.actionValue.subscribe(sValue => this.actionValue.error(!sValue)));
this.regDisposables([this.actionNoStop, this.actionTemplate]);
this.deleteAccess = ko.observable(false);
this.canBeDeleted = ko.observable(true);
this.addSubscribables({
name: sValue => this.nameError(!sValue),
actionValue: sValue => this.actionValueError(!sValue),
actionType: () => {
this.actionValue('');
this.actionValueError(false);
this.actionValueSecond('');
this.actionValueThird('');
this.actionValueFourth('');
this.actionValueFourthError(false);
}
});
}
generateID() {
@ -131,15 +131,13 @@ class FilterModel extends AbstractModel {
verify() {
if (!this.name()) {
this.name.error(true);
this.nameError(true);
return false;
}
if (this.conditions().length) {
if (this.conditions().find(cond => cond && !cond.verify())) {
if (this.conditions().length && this.conditions().find(cond => cond && !cond.verify())) {
return false;
}
}
if (!this.actionValue()) {
if ([
@ -149,13 +147,13 @@ class FilterModel extends AbstractModel {
FiltersAction.Vacation
].includes(this.actionType())
) {
this.actionValue.error(true);
this.actionValueError(true);
return false;
}
}
if (FiltersAction.Forward === this.actionType() && !this.actionValue().includes('@')) {
this.actionValue.error(true);
this.actionValueError(true);
return false;
}
@ -164,12 +162,12 @@ class FilterModel extends AbstractModel {
this.actionValueFourth() &&
!this.actionValueFourth().includes('@')
) {
this.actionValueFourth.error(true);
this.actionValueFourthError(true);
return false;
}
this.name.error(false);
this.actionValue.error(false);
this.nameError(false);
this.actionValueError(false);
return true;
}
@ -240,7 +238,7 @@ class FilterModel extends AbstractModel {
filter.enabled(this.enabled());
filter.name(this.name());
filter.name.error(this.name.error());
filter.nameError(this.nameError());
filter.conditionsType(this.conditionsType());
@ -249,7 +247,7 @@ class FilterModel extends AbstractModel {
filter.actionType(this.actionType());
filter.actionValue(this.actionValue());
filter.actionValue.error(this.actionValue.error());
filter.actionValueError(this.actionValueError());
filter.actionValueSecond(this.actionValueSecond());
filter.actionValueThird(this.actionValueThird());

View file

@ -8,13 +8,15 @@ class FilterConditionModel extends AbstractModel {
constructor() {
super();
this.field = ko.observable(FilterConditionField.From);
this.type = ko.observable(FilterConditionType.Contains);
this.value = ko.observable('');
this.value.error = ko.observable(false);
this.addObservables({
field: FilterConditionField.From,
type: FilterConditionType.Contains,
value: '',
valueError: false,
this.valueSecond = ko.observable('');
this.valueSecond.error = ko.observable(false);
valueSecond: '',
valueSecondError: false
});
this.template = ko.computed(() => {
let template = '';
@ -33,22 +35,22 @@ class FilterConditionModel extends AbstractModel {
return template;
}, this);
this.field.subscribe(() => {
this.addSubscribables({
field: () => {
this.value('');
this.valueSecond('');
}
});
this.regDisposables([this.template]);
}
verify() {
if (!this.value()) {
this.value.error(true);
this.valueError(true);
return false;
}
if (FilterConditionField.Header === this.field() && !this.valueSecond()) {
this.valueSecond.error(true);
this.valueSecondError(true);
return false;
}

View file

@ -11,7 +11,6 @@ class FolderModel extends AbstractModel {
constructor() {
super();
this.name = ko.observable('');
this.fullName = '';
this.fullNameRaw = '';
this.fullNameHash = '';
@ -23,23 +22,27 @@ class FolderModel extends AbstractModel {
this.selectable = false;
this.existen = true;
this.type = ko.observable(FolderType.User);
this.addObservables({
name: '',
type: FolderType.User,
focused: false,
selected: false,
edited: false,
subScribed: true,
checkable: false,
deleteAccess: false,
nameForEdit: '',
privateMessageCountAll: 0,
privateMessageCountUnread: 0,
collapsedPrivate: true
});
this.focused = ko.observable(false);
this.selected = ko.observable(false);
this.edited = ko.observable(false);
this.subScribed = ko.observable(true);
this.checkable = ko.observable(false);
this.subFolders = ko.observableArray(new FolderCollectionModel);
this.deleteAccess = ko.observable(false);
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
this.nameForEdit = ko.observable('');
this.privateMessageCountAll = ko.observable(0);
this.privateMessageCountUnread = ko.observable(0);
this.collapsedPrivate = ko.observable(true);
}
/**
@ -230,15 +233,16 @@ class FolderModel extends AbstractModel {
)
);
// subscribe
folder.name.subscribe(value => folder.nameForEdit(value));
folder.addSubscribables({
name: value => folder.nameForEdit(value),
folder.edited.subscribe(value => value && folder.nameForEdit(folder.name()));
edited: value => value && folder.nameForEdit(folder.name()),
folder.messageCountUnread.subscribe((unread) => {
messageCountUnread: unread => {
if (FolderType.Inbox === folder.type()) {
dispatchEvent(new CustomEvent('mailbox.inbox-unread-count', {detail:unread}));
}
}
});
}
return folder;

View file

@ -10,17 +10,20 @@ class IdentityModel extends AbstractModel {
constructor(id, email) {
super();
this.id = ko.observable(id || '');
this.email = ko.observable(email);
this.name = ko.observable('');
this.addObservables({
id: id || '',
email: email,
name: '',
this.replyTo = ko.observable('');
this.bcc = ko.observable('');
replyTo: '',
bcc: '',
this.signature = ko.observable('');
this.signatureInsertBefore = ko.observable(false);
signature: '',
signatureInsertBefore: false,
deleteAccess: false
});
this.deleteAccess = ko.observable(false);
this.canBeDeleted = ko.computed(() => !!this.id());
}

View file

@ -31,57 +31,56 @@ class MessageModel extends AbstractModel {
this._reset();
this.subject = ko.observable('');
this.subjectPrefix = ko.observable('');
this.subjectSuffix = ko.observable('');
this.size = ko.observable(0);
this.dateTimeStampInUTC = ko.observable(0);
this.priority = ko.observable(MessagePriority.Normal);
this.addObservables({
subject: '',
subjectPrefix: '',
subjectSuffix: '',
size: 0,
dateTimeStampInUTC: 0,
priority: MessagePriority.Normal,
this.senderEmailsString = ko.observable('');
this.senderClearEmailsString = ko.observable('');
senderEmailsString: '',
senderClearEmailsString: '',
this.newForAnimation = ko.observable(false);
newForAnimation: false,
this.deleted = ko.observable(false);
this.isDeleted = ko.observable(false);
this.isUnseen = ko.observable(false);
this.isFlagged = ko.observable(false);
this.isAnswered = ko.observable(false);
this.isForwarded = ko.observable(false);
this.isReadReceipt = ko.observable(false);
deleted: false,
isDeleted: false,
isUnseen: false,
isFlagged: false,
isAnswered: false,
isForwarded: false,
isReadReceipt: false,
this.focused = ko.observable(false);
this.selected = ko.observable(false);
this.checked = ko.observable(false);
this.hasAttachments = ko.observable(false);
this.attachmentsSpecData = ko.observableArray([]);
focused: false,
selected: false,
checked: false,
hasAttachments: false,
isHtml: false,
hasImages: false,
isPgpSigned: false,
isPgpEncrypted: false,
pgpSignedVerifyStatus: SignedVerifyStatus.None,
pgpSignedVerifyUser: '',
readReceipt: '',
hasUnseenSubMessage: false,
hasFlaggedSubMessage: false
});
this.attachmentIconClass = ko.computed(() =>
File.getCombinedIconClass(this.hasAttachments() ? this.attachmentsSpecData() : [])
);
this.isHtml = ko.observable(false);
this.hasImages = ko.observable(false);
this.attachments = ko.observable(new AttachmentCollectionModel);
this.isPgpSigned = ko.observable(false);
this.isPgpEncrypted = ko.observable(false);
this.pgpSignedVerifyStatus = ko.observable(SignedVerifyStatus.None);
this.pgpSignedVerifyUser = ko.observable('');
this.priority = ko.observable(MessagePriority.Normal);
this.readReceipt = ko.observable('');
this.hasUnseenSubMessage = ko.observable(false);
this.hasFlaggedSubMessage = ko.observable(false);
this.attachments = ko.observableArray(new AttachmentCollectionModel);
this.attachmentsSpecData = ko.observableArray([]);
this.threads = ko.observableArray([]);
this.threadsLen = ko.computed(() => this.threads().length);
this.isImportant = ko.computed(() => MessagePriority.High === this.priority());
this.regDisposables([this.attachmentIconClass, this.threadsLen, this.isImportant]);
}
_reset() {

View file

@ -55,10 +55,9 @@ class ContactsPopupView extends AbstractViewNext {
this.importUploaderButton = ko.observable(null);
this.contactsPage = ko.observable(1);
this.contactsPageCount = ko.computed(() => {
const iPage = Math.ceil(this.contactsCount() / CONTACTS_PER_PAGE);
return 0 >= iPage ? 1 : iPage;
});
this.contactsPageCount = ko.computed(() =>
Math.max(1, Math.ceil(this.contactsCount() / CONTACTS_PER_PAGE))
);
this.contactsPaginator = ko.computed(computedPaginatorHelper(this.contactsPage, this.contactsPageCount));
@ -104,10 +103,7 @@ class ContactsPopupView extends AbstractViewNext {
this.viewPropertiesNames().filter(property => !!trim(property.value()))
);
const propertyFocused = (property) => {
const focused = property.focused();
return !trim(property.value()) && !focused;
};
const propertyFocused = property => !trim(property.value()) && !property.focused();
this.viewPropertiesEmailsEmptyAndOnFocused = ko.computed(() =>
this.viewPropertiesEmails().filter(propertyFocused)
@ -117,7 +113,9 @@ class ContactsPopupView extends AbstractViewNext {
this.viewPropertiesPhones().filter(propertyFocused)
);
this.viewPropertiesWebEmptyAndOnFocused = ko.computed(() => this.viewPropertiesWeb().filter(propertyFocused));
this.viewPropertiesWebEmptyAndOnFocused = ko.computed(() =>
this.viewPropertiesWeb().filter(propertyFocused)
);
this.viewPropertiesOtherEmptyAndOnFocused = ko.computed(() =>
this.viewPropertiesOther().filter(propertyFocused)
@ -141,9 +139,7 @@ class ContactsPopupView extends AbstractViewNext {
this.useCheckboxesInList = SettingsStore.useCheckboxesInList;
this.search.subscribe(() => {
this.reloadContactList();
});
this.search.subscribe(() => this.reloadContactList());
this.contactsChecked = ko.computed(() => this.contacts().filter(item => item.checked()));
@ -170,14 +166,14 @@ class ContactsPopupView extends AbstractViewNext {
'.e-contact-item.focused'
);
this.selector.on('onItemSelect', (contact) => {
this.populateViewContact(contact ? contact : null);
this.selector.on('onItemSelect', contact => {
this.populateViewContact(contact || null);
if (!contact) {
this.emptySelection(true);
}
});
this.selector.on('onItemGetUid', (contact) => (contact ? contact.generateUid() : ''));
this.selector.on('onItemGetUid', contact => contact ? contact.generateUid() : '');
this.bDropPageAfterDelete = false;

View file

@ -33,7 +33,7 @@ class FilterPopupView extends AbstractViewNext {
this.selectedFolderValue.subscribe(() => {
if (this.filter()) {
this.filter().actionValue.error(false);
this.filter().actionValueError(false);
}
});
@ -157,13 +157,13 @@ class FilterPopupView extends AbstractViewNext {
this.isNew(!bEdit);
if (!bEdit && oFilter) {
oFilter.name.focused(true);
oFilter.nameFocused(true);
}
}
onShowWithDelay() {
if (this.isNew() && this.filter()/* && !rl.settings.app('mobile')*/) {
this.filter().name.focused(true);
this.filter().nameFocused(true);
}
}
}

View file

@ -11,10 +11,10 @@
<div class="row filter" data-bind="with: filter, i18nInit: filter">
<div class="span9" data-bind="i18nInit: true">
<div class="control-group" data-bind="css: {'error': name.error}">
<div class="control-group" data-bind="css: {'error': nameError}">
<div class="controls">
<input type="text" class="i18n span5"
data-bind="value: name, hasFocus: name.focused"
data-bind="value: name, hasFocus: nameFocused"
autocorrect="off" autocapitalize="off" spellcheck="false"
data-i18n="[placeholder]POPUPS_FILTER/FILTER_NAME"
/>

View file

@ -1,4 +1,4 @@
<div class="control-group" data-bind="css: {'error': actionValue.error}" style="margin-bottom: 0">
<div class="control-group" data-bind="css: {'error': actionValueError}" style="margin-bottom: 0">
<div class="controls">
<div data-bind="component: {
name: 'Checkbox',

View file

@ -1,4 +1,4 @@
<div class="control-group" data-bind="css: {'error': actionValue.error}">
<div class="control-group" data-bind="css: {'error': actionValueError}">
<div class="controls">
<input type="text" class="span3 i18n" data-bind="value: actionValue"
data-i18n="[placeholder]POPUPS_FILTER/EMAIL_LABEL" />

View file

@ -1,4 +1,4 @@
<div class="control-group" data-bind="css: {'error': actionValue.error}">
<div class="control-group" data-bind="css: {'error': actionValueError}">
<div class="controls">
<select class="span3" data-bind="options: $root.folderSelectList, value: $root.selectedFolderValue,
optionsText: 'name', optionsValue: 'id', optionsAfterRender: $root.defaultOptionsAfterRender"></select>

View file

@ -1,4 +1,4 @@
<div class="control-group" data-bind="css: {'error': actionValue.error}" style="margin-bottom: 0">
<div class="control-group" data-bind="css: {'error': actionValueError}" style="margin-bottom: 0">
<div class="controls">
<div data-bind="component: {
name: 'Checkbox',

View file

@ -1,4 +1,4 @@
<div class="control-group" data-bind="css: {'error': actionValue.error}" style="margin-bottom: 0">
<div class="control-group" data-bind="css: {'error': actionValueError}" style="margin-bottom: 0">
<div class="controls">
<textarea class="span5 i18n" data-bind="value: actionValue" style="height: 100px;"
data-i18n="[placeholder]POPUPS_FILTER/REJECT_MESSAGE_LABEL"></textarea>

View file

@ -11,7 +11,7 @@
}"></div>
</div>
</div>
<div class="control-group" data-bind="css: {'error': actionValueFourth.error}" style="margin-bottom: 0">
<div class="control-group" data-bind="css: {'error': actionValueFourthError}" style="margin-bottom: 0">
<div class="controls">
<input type="text" class="span5 i18n" data-bind="value: actionValueFourth"
data-i18n="[placeholder]POPUPS_FILTER/VACATION_RECIPIENTS_LABEL" />
@ -26,7 +26,7 @@
data-i18n="[placeholder]POPUPS_FILTER/VACATION_SUBJECT_LABEL" />
</div>
</div>
<div class="control-group" data-bind="css: {'error': actionValue.error}" style="margin-bottom: 0">
<div class="control-group" data-bind="css: {'error': actionValueError}" style="margin-bottom: 0">
<div class="controls">
<textarea class="span5 i18n" data-bind="value: actionValue" style="height: 100px;"
data-i18n="[placeholder]POPUPS_FILTER/VACATION_MESSAGE_LABEL"></textarea>

View file

@ -1,4 +1,4 @@
<div class="control-group" data-bind="css: {'error': value.error}" style="margin-bottom: 0">
<div class="control-group" data-bind="css: {'error': valueError}" style="margin-bottom: 0">
<select class="span3" data-bind="options: $root.fieldOptions, value: field, optionsText: 'name', optionsValue: 'id'"></select>
&nbsp;
<select class="span2" data-bind="options: $root.typeOptions, value: type, optionsText: 'name', optionsValue: 'id'"></select>

View file

@ -1,4 +1,4 @@
<div class="control-group" data-bind="css: {'error': value.error}" style="margin-bottom: 0">
<div class="control-group" data-bind="css: {'error': valueError}" style="margin-bottom: 0">
<select class="span2" data-bind="options: $root.fieldOptions, value: field, optionsText: 'name', optionsValue: 'id'"></select>
&nbsp;
<input class="span2" type="text" data-bind="value: valueSecond" />

View file

@ -1,4 +1,4 @@
<div class="control-group" data-bind="css: {'error': value.error}" style="margin-bottom: 0">
<div class="control-group" data-bind="css: {'error': valueError}" style="margin-bottom: 0">
<select class="span3" data-bind="options: $root.fieldOptions, value: field, optionsText: 'name', optionsValue: 'id'"></select>
&nbsp;
<select class="span2" data-bind="options: $root.typeOptionsSize, value: type, optionsText: 'name', optionsValue: 'id'"></select>