mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-25 18:19:22 +03:00
Merge branch 'master' into plugin-hooks
# Conflicts: # dev/Common/Plugins.js # dev/Knoin/Knoin.js # dev/Remote/AbstractFetch.js # dev/View/User/Login.js
This commit is contained in:
commit
827cfa5051
892 changed files with 110819 additions and 48675 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import * as Links from 'Common/Links';
|
||||
import { doc } from 'Common/Globals';
|
||||
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
|
||||
|
||||
let notificator = null,
|
||||
player = null,
|
||||
|
|
@ -12,7 +12,7 @@ let notificator = null,
|
|||
player.src = url;
|
||||
player.play();
|
||||
name = name.trim();
|
||||
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'}));
|
||||
fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -73,8 +73,7 @@ export const SMAudio = new class {
|
|||
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
|
||||
if (player) {
|
||||
const stopFn = () => this.pause();
|
||||
player.addEventListener('ended', stopFn);
|
||||
player.addEventListener('error', stopFn);
|
||||
addEventsListener(player, ['ended','error'], stopFn);
|
||||
addEventListener('audio.api.stop', stopFn);
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +88,7 @@ export const SMAudio = new class {
|
|||
|
||||
pause() {
|
||||
player && player.pause();
|
||||
dispatchEvent(new CustomEvent('audio.stop'));
|
||||
fireEvent('audio.stop');
|
||||
}
|
||||
|
||||
playMp3(url, name) {
|
||||
|
|
@ -106,11 +105,11 @@ export const SMAudio = new class {
|
|||
|
||||
playNotification(silent) {
|
||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||
if (!notificator) {
|
||||
notificator = createNewObject();
|
||||
notificator.src = Links.staticLink('sounds/new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
|
||||
}
|
||||
notificator = notificator || createNewObject();
|
||||
if (notificator) {
|
||||
notificator.src = Links.staticLink('sounds/'
|
||||
+ SettingsGet('NotificationSound')
|
||||
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
||||
notificator.volume = silent ? 0.01 : 1;
|
||||
notificator.play();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,174 +1,157 @@
|
|||
import { MessageSetAction } from 'Common/EnumsUser';
|
||||
import { isNonEmptyArray, pInt } from 'Common/Utils';
|
||||
import { isArray } from 'Common/Utils';
|
||||
|
||||
let FOLDERS_CACHE = {},
|
||||
FOLDERS_NAME_CACHE = {},
|
||||
FOLDERS_HASH_CACHE = {},
|
||||
FOLDERS_UID_NEXT_CACHE = {},
|
||||
MESSAGE_FLAGS_CACHE = {},
|
||||
NEW_MESSAGE_CACHE = {},
|
||||
REQUESTED_MESSAGE_CACHE = {},
|
||||
inboxFolderName = 'INBOX';
|
||||
|
||||
const REQUESTED_MESSAGE_CACHE = {};
|
||||
export const
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clearCache = () => {
|
||||
FOLDERS_CACHE = {};
|
||||
FOLDERS_NAME_CACHE = {};
|
||||
MESSAGE_FLAGS_CACHE = {};
|
||||
NEW_MESSAGE_CACHE = {};
|
||||
REQUESTED_MESSAGE_CACHE = {};
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
export function clear() {
|
||||
FOLDERS_CACHE = {};
|
||||
FOLDERS_NAME_CACHE = {};
|
||||
FOLDERS_HASH_CACHE = {};
|
||||
FOLDERS_UID_NEXT_CACHE = {};
|
||||
MESSAGE_FLAGS_CACHE = {};
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @returns {string}
|
||||
*/
|
||||
getMessageKey = (folderFullName, uid) => `${folderFullName}#${uid}`,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} uid
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getMessageKey(folderFullNameRaw, uid) {
|
||||
return `${folderFullNameRaw}#${uid}`;
|
||||
}
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
*/
|
||||
addRequestedMessage = (folder, uid) => REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true,
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
*/
|
||||
export function addRequestedMessage(folder, uid) {
|
||||
REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true;
|
||||
}
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasRequestedMessage(folder, uid) {
|
||||
return true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)];
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
*/
|
||||
addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} uid
|
||||
*/
|
||||
export function addNewMessageCache(folderFullNameRaw, uid) {
|
||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true;
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
*/
|
||||
hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
|
||||
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
|
||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} uid
|
||||
*/
|
||||
export function hasNewMessageAndRemoveFromCache(folderFullNameRaw, uid) {
|
||||
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
|
||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clearNewMessageCache = () => NEW_MESSAGE_CACHE = {},
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
export function clearNewMessageCache() {
|
||||
NEW_MESSAGE_CACHE = {};
|
||||
}
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderInboxName = () => inboxFolderName,
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFolderInboxName() {
|
||||
return inboxFolderName;
|
||||
}
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
setFolderInboxName = name => inboxFolderName = name,
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function setFolderInboxName(name) {
|
||||
inboxFolderName = name;
|
||||
}
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderFullName = folderHash =>
|
||||
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
|
||||
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFolderFullNameRaw(folderHash) {
|
||||
return folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '';
|
||||
}
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @param {string} folderFullName
|
||||
* @param {?FolderModel} folder
|
||||
*/
|
||||
setFolder = folder => {
|
||||
folder.hash = '';
|
||||
FOLDERS_CACHE[folder.fullName] = folder;
|
||||
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {?FolderModel} folder
|
||||
*/
|
||||
export function setFolder(folderHash, folderFullNameRaw, folder) {
|
||||
FOLDERS_CACHE[folderFullNameRaw] = folder;
|
||||
FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderHash = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFolderHash(folderFullNameRaw) {
|
||||
return folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '';
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} folderHash
|
||||
*/
|
||||
setFolderHash = (folderFullName, folderHash) =>
|
||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} folderHash
|
||||
*/
|
||||
export function setFolderHash(folderFullNameRaw, folderHash) {
|
||||
if (folderFullNameRaw) {
|
||||
FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderUidNext = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFolderUidNext(folderFullNameRaw) {
|
||||
return folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
|
||||
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
|
||||
: '';
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uidNext
|
||||
*/
|
||||
setFolderUidNext = (folderFullName, uidNext) =>
|
||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} uidNext
|
||||
*/
|
||||
export function setFolderUidNext(folderFullNameRaw, uidNext) {
|
||||
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext;
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {?FolderModel}
|
||||
*/
|
||||
getFolderFromCacheList = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @returns {?FolderModel}
|
||||
*/
|
||||
export function getFolderFromCacheList(folderFullNameRaw) {
|
||||
return folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
*/
|
||||
export function removeFolderFromCacheList(folderFullNameRaw) {
|
||||
delete FOLDERS_CACHE[folderFullNameRaw];
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
*/
|
||||
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
|
||||
|
||||
export class MessageFlagsCache
|
||||
{
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @param {string} flag
|
||||
* @returns {bool}
|
||||
*/
|
||||
static hasFlag(folderFullName, uid, flag) {
|
||||
return MESSAGE_FLAGS_CACHE[folderFullName]
|
||||
&& MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||
&& MESSAGE_FLAGS_CACHE[folderFullName][uid].includes(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @returns {?Array}
|
||||
*/
|
||||
static getFor(folderFullName, uid) {
|
||||
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||
: null;
|
||||
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -176,11 +159,13 @@ export class MessageFlagsCache
|
|||
* @param {string} uid
|
||||
* @param {Array} flagsCache
|
||||
*/
|
||||
static setFor(folderFullName, uid, flagsCache) {
|
||||
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||
static setFor(folderFullName, uid, flags) {
|
||||
if (isArray(flags)) {
|
||||
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||
}
|
||||
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
|
||||
}
|
||||
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -196,39 +181,24 @@ export class MessageFlagsCache
|
|||
static initMessage(message) {
|
||||
if (message) {
|
||||
const uid = message.uid,
|
||||
flags = this.getFor(message.folder, uid);
|
||||
flags = this.getFor(message.folder, uid),
|
||||
thread = message.threads;
|
||||
|
||||
if (flags && flags.length) {
|
||||
message.isFlagged(!!flags[1]);
|
||||
|
||||
if (!message.isSimpleMessage) {
|
||||
message.isUnseen(!!flags[0]);
|
||||
message.isAnswered(!!flags[2]);
|
||||
message.isForwarded(!!flags[3]);
|
||||
message.isReadReceipt(!!flags[4]);
|
||||
message.isDeleted(!!flags[5]);
|
||||
}
|
||||
if (isArray(flags)) {
|
||||
message.flags(flags);
|
||||
}
|
||||
|
||||
if (message.threads.length) {
|
||||
const unseenSubUid = message.threads.find(sSubUid => {
|
||||
if (uid !== sSubUid) {
|
||||
const subFlags = this.getFor(message.folder, sSubUid);
|
||||
return subFlags && subFlags.length && !!subFlags[0];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (thread.length) {
|
||||
const unseenSubUid = thread.find(iSubUid =>
|
||||
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
|
||||
);
|
||||
|
||||
const flaggedSubUid = message.threads.find(sSubUid => {
|
||||
if (uid !== sSubUid) {
|
||||
const subFlags = this.getFor(message.folder, sSubUid);
|
||||
return subFlags && subFlags.length && !!subFlags[1];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const flaggedSubUid = thread.find(iSubUid =>
|
||||
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
|
||||
);
|
||||
|
||||
message.hasUnseenSubMessage(unseenSubUid && 0 < pInt(unseenSubUid));
|
||||
message.hasFlaggedSubMessage(flaggedSubUid && 0 < pInt(flaggedSubUid));
|
||||
message.hasUnseenSubMessage(!!unseenSubUid);
|
||||
message.hasFlaggedSubMessage(!!flaggedSubUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,25 +208,7 @@ export class MessageFlagsCache
|
|||
*/
|
||||
static store(message) {
|
||||
if (message) {
|
||||
this.setFor(message.folder, message.uid, [
|
||||
message.isUnseen(),
|
||||
message.isFlagged(),
|
||||
message.isAnswered(),
|
||||
message.isForwarded(),
|
||||
message.isReadReceipt(),
|
||||
message.isDeleted()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
* @param {Array} flags
|
||||
*/
|
||||
static storeByFolderAndUid(folder, uid, flags) {
|
||||
if (isNonEmptyArray(flags)) {
|
||||
this.setFor(folder, uid, flags);
|
||||
this.setFor(message.folder, message.uid, message.flags());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -266,33 +218,30 @@ export class MessageFlagsCache
|
|||
* @param {number} setAction
|
||||
*/
|
||||
static storeBySetAction(folder, uid, setAction) {
|
||||
let unread = 0;
|
||||
const flags = this.getFor(folder, uid);
|
||||
let flags = this.getFor(folder, uid) || [];
|
||||
const
|
||||
unread = flags.includes('\\seen') ? 0 : 1,
|
||||
add = item => flags.includes(item) || flags.push(item),
|
||||
remove = item => flags = flags.filter(flag => flag != item);
|
||||
|
||||
if (isNonEmptyArray(flags)) {
|
||||
if (flags[0]) {
|
||||
unread = 1;
|
||||
}
|
||||
|
||||
switch (setAction) {
|
||||
case MessageSetAction.SetSeen:
|
||||
flags[0] = false;
|
||||
break;
|
||||
case MessageSetAction.UnsetSeen:
|
||||
flags[0] = true;
|
||||
break;
|
||||
case MessageSetAction.SetFlag:
|
||||
flags[1] = true;
|
||||
break;
|
||||
case MessageSetAction.UnsetFlag:
|
||||
flags[1] = false;
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
this.setFor(folder, uid, flags);
|
||||
switch (setAction) {
|
||||
case MessageSetAction.SetSeen:
|
||||
add('\\seen');
|
||||
break;
|
||||
case MessageSetAction.UnsetSeen:
|
||||
remove('\\seen');
|
||||
break;
|
||||
case MessageSetAction.SetFlag:
|
||||
add('\\flagged');
|
||||
break;
|
||||
case MessageSetAction.UnsetFlag:
|
||||
remove('\\flagged');
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
this.setFor(folder, uid, flags);
|
||||
|
||||
return unread;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export const MESSAGES_PER_PAGE_VALUES = [10, 20, 30, 50, 100];
|
||||
|
||||
export const UNUSED_OPTION_VALUE = '__UNUSE__';
|
||||
|
|
|
|||
|
|
@ -1,85 +1,53 @@
|
|||
/* eslint quote-props: 0 */
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const Capa = {
|
||||
OpenPGP: 'OPEN_PGP',
|
||||
Prefetch: 'PREFETCH',
|
||||
Composer: 'COMPOSER',
|
||||
Contacts: 'CONTACTS',
|
||||
Reload: 'RELOAD',
|
||||
Search: 'SEARCH',
|
||||
SearchAdv: 'SEARCH_ADV',
|
||||
MessageActions: 'MESSAGE_ACTIONS',
|
||||
MessageListActions: 'MESSAGELIST_ACTIONS',
|
||||
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
|
||||
DangerousActions: 'DANGEROUS_ACTIONS',
|
||||
Settings: 'SETTINGS',
|
||||
Help: 'HELP',
|
||||
Themes: 'THEMES',
|
||||
UserBackground: 'USER_BACKGROUND',
|
||||
Sieve: 'SIEVE',
|
||||
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
|
||||
Templates: 'TEMPLATES',
|
||||
AutoLogout: 'AUTOLOGOUT',
|
||||
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
|
||||
Identities: 'IDENTITIES'
|
||||
};
|
||||
export const
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const Scope = {
|
||||
All: 'all',
|
||||
None: 'none',
|
||||
Contacts: 'Contacts',
|
||||
Scope = {
|
||||
MessageList: 'MessageList',
|
||||
FolderList: 'FolderList',
|
||||
MessageView: 'MessageView',
|
||||
Compose: 'Compose',
|
||||
Settings: 'Settings',
|
||||
Menu: 'Menu',
|
||||
ComposeOpenPgp: 'ComposeOpenPgp',
|
||||
MessageOpenPgp: 'MessageOpenPgp',
|
||||
ViewOpenPgpKey: 'ViewOpenPgpKey',
|
||||
KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
|
||||
Ask: 'Ask'
|
||||
};
|
||||
Settings: 'Settings'
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const UploadErrorCode = {
|
||||
UploadErrorCode = {
|
||||
Normal: 0,
|
||||
FileIsTooBig: 1,
|
||||
FilePartiallyUploaded: 2,
|
||||
NoFileUploaded: 3,
|
||||
MissingTempFolder: 4,
|
||||
OnSavingFile: 5,
|
||||
FilePartiallyUploaded: 3,
|
||||
NoFileUploaded: 4,
|
||||
MissingTempFolder: 6,
|
||||
OnSavingFile: 7,
|
||||
FileType: 98,
|
||||
Unknown: 99
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const SaveSettingsStep = {
|
||||
SaveSettingsStep = {
|
||||
Animate: -2,
|
||||
Idle: -1,
|
||||
TrueResult: 1,
|
||||
FalseResult: 0
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const Notification = {
|
||||
Notification = {
|
||||
RequestError: 1,
|
||||
RequestAborted: 2,
|
||||
|
||||
// Global
|
||||
InvalidToken: 101,
|
||||
AuthError: 102,
|
||||
|
||||
// User
|
||||
ConnectionError: 104,
|
||||
DomainNotAllowed: 109,
|
||||
AccountNotAllowed: 110,
|
||||
|
|
@ -110,20 +78,15 @@ export const Notification = {
|
|||
CantDeleteNonEmptyFolder: 405,
|
||||
|
||||
// CantSaveSettings: 501,
|
||||
CantSavePluginSettings: 502,
|
||||
|
||||
DomainAlreadyExists: 601,
|
||||
|
||||
CantInstallPackage: 701,
|
||||
CantDeletePackage: 702,
|
||||
InvalidPluginPackage: 703,
|
||||
UnsupportedPluginPackage: 704,
|
||||
|
||||
DemoSendMessageError: 750,
|
||||
DemoAccountError: 751,
|
||||
|
||||
AccountAlreadyExists: 801,
|
||||
AccountDoesNotExist: 802,
|
||||
AccountSwitchFailed: 803,
|
||||
|
||||
MailServerError: 901,
|
||||
ClientViewError: 902,
|
||||
|
|
@ -134,5 +97,12 @@ export const Notification = {
|
|||
// JsonTimeout: 953,
|
||||
|
||||
UnknownNotification: 998,
|
||||
UnknownError: 999
|
||||
UnknownError: 999,
|
||||
|
||||
// Admin
|
||||
CantInstallPackage: 701,
|
||||
CantDeletePackage: 702,
|
||||
InvalidPluginPackage: 703,
|
||||
UnsupportedPluginPackage: 704,
|
||||
CantSavePluginSettings: 705
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,14 +4,28 @@
|
|||
* @enum {number}
|
||||
*/
|
||||
export const FolderType = {
|
||||
Inbox: 10,
|
||||
SentItems: 11,
|
||||
Draft: 12,
|
||||
Trash: 13,
|
||||
Spam: 14,
|
||||
Archive: 15,
|
||||
NotSpam: 80,
|
||||
User: 99
|
||||
User: 0,
|
||||
Inbox: 1,
|
||||
Sent: 2,
|
||||
Drafts: 3,
|
||||
Spam: 4, // JUNK
|
||||
Trash: 5,
|
||||
Archive: 6,
|
||||
NotSpam: 80
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FolderMetadataKeys = {
|
||||
// RFC 5464
|
||||
Comment: '/private/comment',
|
||||
CommentShared: '/shared/comment',
|
||||
// RFC 6154
|
||||
SpecialUse: '/private/specialuse',
|
||||
// Kolab
|
||||
KolabFolderType: '/private/vendor/kolab/folder-type',
|
||||
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -34,13 +48,13 @@ export const FolderSortMode = {
|
|||
* @enum {string}
|
||||
*/
|
||||
export const ComposeType = {
|
||||
Empty: 'empty',
|
||||
Reply: 'reply',
|
||||
ReplyAll: 'replyall',
|
||||
Forward: 'forward',
|
||||
ForwardAsAttachment: 'forward-as-attachment',
|
||||
Draft: 'draft',
|
||||
EditAsNew: 'editasnew'
|
||||
Empty: 0,
|
||||
Reply: 1,
|
||||
ReplyAll: 2,
|
||||
Forward: 3,
|
||||
ForwardAsAttachment: 4,
|
||||
Draft: 5,
|
||||
EditAsNew: 6
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -58,19 +72,14 @@ export const SetSystemFoldersNotification = {
|
|||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const ClientSideKeyName = {
|
||||
FoldersLashHash: 0,
|
||||
MessagesInboxLastHash: 1,
|
||||
MailBoxListSize: 2,
|
||||
ExpandedFolders: 3,
|
||||
FolderListSize: 4,
|
||||
MessageListSize: 5,
|
||||
LastReplyAction: 6,
|
||||
LastSignMe: 7,
|
||||
ComposeLastIdentityID: 8,
|
||||
MessageHeaderFullInfo: 9,
|
||||
MessageAttachmentControls: 10
|
||||
};
|
||||
export const
|
||||
ClientSideKeyNameExpandedFolders = 3,
|
||||
ClientSideKeyNameFolderListSize = 4,
|
||||
ClientSideKeyNameMessageListSize = 5,
|
||||
ClientSideKeyNameLastReplyAction = 6,
|
||||
ClientSideKeyNameLastSignMe = 7,
|
||||
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
||||
ClientSideKeyNameMessageAttachmentControls = 10;
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint key-spacing: 0 */
|
||||
/* eslint quote-props: 0 */
|
||||
|
||||
import { isNonEmptyArray } from 'Common/Utils';
|
||||
import { arrayLength } from 'Common/Utils';
|
||||
|
||||
const
|
||||
cache = {},
|
||||
|
|
@ -9,6 +9,7 @@ const
|
|||
msOffice = app+'vnd.openxmlformats-officedocument.',
|
||||
openDoc = app+'vnd.oasis.opendocument.',
|
||||
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
|
||||
lowerCase = text => text.toLowerCase().trim(),
|
||||
|
||||
exts = {
|
||||
eml: 'message/rfc822',
|
||||
|
|
@ -123,13 +124,13 @@ export const FileInfo = {
|
|||
* @returns {string}
|
||||
*/
|
||||
getExtension: fileName => {
|
||||
fileName = fileName.toLowerCase().trim();
|
||||
fileName = lowerCase(fileName);
|
||||
const result = fileName.split('.').pop();
|
||||
return result === fileName ? '' : result;
|
||||
},
|
||||
|
||||
getContentType: fileName => {
|
||||
fileName = fileName.toLowerCase().trim();
|
||||
fileName = lowerCase(fileName);
|
||||
if ('winmail.dat' === fileName) {
|
||||
return app + 'ms-tnef';
|
||||
}
|
||||
|
|
@ -158,8 +159,8 @@ export const FileInfo = {
|
|||
* @returns {string}
|
||||
*/
|
||||
getType: (ext, mimeType) => {
|
||||
ext = ext.toLowerCase().trim();
|
||||
mimeType = mimeType.toLowerCase().trim().replace('csv/plain', 'text/csv');
|
||||
ext = lowerCase(ext);
|
||||
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
|
||||
|
||||
let key = ext + mimeType;
|
||||
if (cache[key]) {
|
||||
|
|
@ -249,10 +250,10 @@ export const FileInfo = {
|
|||
* @param {string} sFileType
|
||||
* @returns {string}
|
||||
*/
|
||||
getCombinedIconClass: data => {
|
||||
if (isNonEmptyArray(data)) {
|
||||
getAttachmentsIconClass: data => {
|
||||
if (arrayLength(data)) {
|
||||
let icons = data
|
||||
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '')
|
||||
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
|
||||
.validUnique();
|
||||
|
||||
return (icons && 1 === icons.length && 'icon-file' !== icons[0])
|
||||
|
|
|
|||
259
dev/Common/Folders.js
Normal file
259
dev/Common/Folders.js
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
import {
|
||||
MessageFlagsCache,
|
||||
setFolderHash,
|
||||
getFolderHash,
|
||||
getFolderInboxName,
|
||||
getFolderFromCacheList,
|
||||
getFolderUidNext
|
||||
} from 'Common/Cache';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export const
|
||||
|
||||
sortFolders = folders => {
|
||||
try {
|
||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
folders.sort((a, b) =>
|
||||
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
* @param {string} folder
|
||||
* @param {Array=} list = []
|
||||
*/
|
||||
fetchFolderInformation = (fCallback, folder, list = []) => {
|
||||
let fetch = !arrayLength(list);
|
||||
const uids = [];
|
||||
|
||||
if (!fetch) {
|
||||
list.forEach(messageListItem => {
|
||||
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
|
||||
uids.push(messageListItem.uid);
|
||||
}
|
||||
|
||||
if (messageListItem.threads.length) {
|
||||
messageListItem.threads.forEach(uid => {
|
||||
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
|
||||
uids.push(uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
fetch = uids.length;
|
||||
}
|
||||
|
||||
if (fetch) {
|
||||
Remote.request('FolderInformation', fCallback, {
|
||||
Folder: folder,
|
||||
FlagsUids: uids,
|
||||
UidNext: getFolderUidNext(folder) // Used to check for new messages
|
||||
});
|
||||
} else if (SettingsUserStore.useThreads()) {
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Array=} aDisabled
|
||||
* @param {Array=} aHeaderLines
|
||||
* @param {Function=} fRenameCallback
|
||||
* @param {Function=} fDisableCallback
|
||||
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
|
||||
* @returns {Array}
|
||||
*/
|
||||
folderListOptionsBuilder = (
|
||||
aDisabled,
|
||||
aHeaderLines,
|
||||
fRenameCallback,
|
||||
fDisableCallback,
|
||||
bNoSelectSelectable,
|
||||
aList = FolderUserStore.folderList()
|
||||
) => {
|
||||
const
|
||||
aResult = [],
|
||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||
// FolderSystemPopupView should always be true
|
||||
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
||||
|
||||
foldersWalk = folders => {
|
||||
folders.forEach(oItem => {
|
||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||
aResult.push({
|
||||
id: oItem.fullName,
|
||||
name:
|
||||
sDeepPrefix.repeat(oItem.deep) +
|
||||
fRenameCallback(oItem),
|
||||
system: false,
|
||||
disabled: !bNoSelectSelectable && (
|
||||
!oItem.selectable() ||
|
||||
aDisabled.includes(oItem.fullName) ||
|
||||
fDisableCallback(oItem))
|
||||
});
|
||||
}
|
||||
|
||||
if (oItem.subFolders.length) {
|
||||
foldersWalk(oItem.subFolders());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
fDisableCallback = fDisableCallback || (() => false);
|
||||
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
||||
isArray(aDisabled) || (aDisabled = []);
|
||||
|
||||
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
||||
aResult.push({
|
||||
id: line[0],
|
||||
name: line[1],
|
||||
system: false,
|
||||
disabled: false
|
||||
})
|
||||
);
|
||||
|
||||
foldersWalk(aList);
|
||||
|
||||
return aResult;
|
||||
},
|
||||
|
||||
// Every 5 minutes
|
||||
refreshFoldersInterval = 300000,
|
||||
|
||||
/**
|
||||
* @param {boolean=} boot = false
|
||||
*/
|
||||
folderInformationMultiply = (boot = false) => {
|
||||
const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
|
||||
if (arrayLength(folders)) {
|
||||
Remote.request('FolderInformationMultiply', (iError, oData) => {
|
||||
if (!iError && arrayLength(oData.Result)) {
|
||||
const utc = Date.now();
|
||||
oData.Result.forEach(item => {
|
||||
const hash = getFolderHash(item.Folder),
|
||||
folder = getFolderFromCacheList(item.Folder);
|
||||
|
||||
if (folder) {
|
||||
folder.expires = utc;
|
||||
|
||||
setFolderHash(item.Folder, item.Hash);
|
||||
|
||||
folder.messageCountAll(item.MessageCount);
|
||||
|
||||
let unreadCountChange = folder.messageCountUnread() !== item.MessageUnseenCount;
|
||||
|
||||
folder.messageCountUnread(item.MessageUnseenCount);
|
||||
|
||||
if (unreadCountChange) {
|
||||
MessageFlagsCache.clearFolder(folder.fullName);
|
||||
}
|
||||
|
||||
if (!hash || item.Hash !== hash) {
|
||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
}
|
||||
} else if (unreadCountChange
|
||||
&& folder.fullName === FolderUserStore.currentFolderFullName()
|
||||
&& MessagelistUserStore.length) {
|
||||
rl.app.folderInformation(folder.fullName, MessagelistUserStore());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (boot) {
|
||||
setTimeout(() => folderInformationMultiply(true), 2000);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
Folders: folders
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
moveOrDeleteResponseHelper = (iError, oData) => {
|
||||
if (iError) {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
alert(getNotification(iError));
|
||||
} else if (FolderUserStore.currentFolder()) {
|
||||
if (2 === arrayLength(oData.Result)) {
|
||||
setFolderHash(oData.Result[0], oData.Result[1]);
|
||||
} else {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
}
|
||||
MessagelistUserStore.reload(!MessagelistUserStore.length);
|
||||
}
|
||||
},
|
||||
|
||||
messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
|
||||
const
|
||||
sSpamFolder = FolderUserStore.spamFolder(),
|
||||
isSpam = sSpamFolder === toFolderFullName,
|
||||
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
|
||||
|
||||
Remote.request('MessageMove',
|
||||
moveOrDeleteResponseHelper,
|
||||
{
|
||||
FromFolder: fromFolderFullName,
|
||||
ToFolder: toFolderFullName,
|
||||
Uids: uidsForMove.join(','),
|
||||
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
|
||||
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
||||
},
|
||||
null,
|
||||
'',
|
||||
['MessageList']
|
||||
);
|
||||
},
|
||||
|
||||
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
|
||||
Remote.request('MessageDelete',
|
||||
moveOrDeleteResponseHelper,
|
||||
{
|
||||
Folder: sFromFolderFullName,
|
||||
Uids: aUidForRemove.join(',')
|
||||
},
|
||||
null,
|
||||
'',
|
||||
['MessageList']
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} sFromFolderFullName
|
||||
* @param {Array} aUidForMove
|
||||
* @param {string} sToFolderFullName
|
||||
* @param {boolean=} bCopy = false
|
||||
*/
|
||||
moveMessagesToFolder = (sFromFolderFullName, aUidForMove, sToFolderFullName, bCopy) => {
|
||||
if (sFromFolderFullName !== sToFolderFullName && arrayLength(aUidForMove)) {
|
||||
const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
|
||||
oToFolder = getFolderFromCacheList(sToFolderFullName);
|
||||
|
||||
if (oFromFolder && oToFolder) {
|
||||
if (bCopy) {
|
||||
Remote.request('MessageCopy', null, {
|
||||
FromFolder: oFromFolder.fullName,
|
||||
ToFolder: oToFolder.fullName,
|
||||
Uids: aUidForMove.join(',')
|
||||
});
|
||||
} else {
|
||||
messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, aUidForMove);
|
||||
}
|
||||
|
||||
MessagelistUserStore.removeMessagesFromList(oFromFolder.fullName, aUidForMove, oToFolder.fullName, bCopy);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
@ -1,58 +1,78 @@
|
|||
import ko from 'ko';
|
||||
import { Scope } from 'Common/Enums';
|
||||
|
||||
export const doc = document;
|
||||
let keyScopeFake = 'all';
|
||||
|
||||
export const $htmlCL = doc.documentElement.classList;
|
||||
export const
|
||||
ScopeMenu = 'Menu',
|
||||
|
||||
export const elementById = id => doc.getElementById(id);
|
||||
doc = document,
|
||||
|
||||
export const Settings = rl.settings;
|
||||
export const SettingsGet = rl.settings.get;
|
||||
$htmlCL = doc.documentElement.classList,
|
||||
|
||||
export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 });
|
||||
elementById = id => doc.getElementById(id),
|
||||
|
||||
export const moveAction = ko.observable(false);
|
||||
export const leftPanelDisabled = ko.observable(false);
|
||||
exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
||||
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
|
||||
|
||||
export const createElement = (name, attr) => {
|
||||
let el = doc.createElement(name);
|
||||
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
|
||||
return el;
|
||||
};
|
||||
Settings = rl.settings,
|
||||
SettingsGet = Settings.get,
|
||||
SettingsCapa = Settings.capa,
|
||||
|
||||
dropdowns = [],
|
||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
||||
|
||||
moveAction = ko.observable(false),
|
||||
leftPanelDisabled = ko.observable(false),
|
||||
|
||||
createElement = (name, attr) => {
|
||||
let el = doc.createElement(name);
|
||||
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
|
||||
return el;
|
||||
},
|
||||
|
||||
fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})),
|
||||
|
||||
formFieldFocused = () => doc.activeElement && doc.activeElement.matches('input,textarea'),
|
||||
|
||||
addShortcut = (...args) => shortcuts.add(...args),
|
||||
|
||||
registerShortcut = (keys, modifiers, scopes, method) =>
|
||||
addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
|
||||
|
||||
addEventsListener = (element, events, fn, options) =>
|
||||
events.forEach(event => element.addEventListener(event, fn, options)),
|
||||
|
||||
addEventsListeners = (element, events) =>
|
||||
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
|
||||
|
||||
// keys / shortcuts
|
||||
keyScopeReal = ko.observable('all'),
|
||||
keyScope = value => {
|
||||
if (!value) {
|
||||
return keyScopeFake;
|
||||
}
|
||||
if (ScopeMenu !== value) {
|
||||
keyScopeFake = value;
|
||||
if (dropdownVisibility()) {
|
||||
value = ScopeMenu;
|
||||
}
|
||||
}
|
||||
keyScopeReal(value);
|
||||
shortcuts.setScope(value);
|
||||
};
|
||||
|
||||
dropdownVisibility.subscribe(value => {
|
||||
if (value) {
|
||||
keyScope(ScopeMenu);
|
||||
} else if (ScopeMenu === shortcuts.getScope()) {
|
||||
keyScope(keyScopeFake);
|
||||
}
|
||||
});
|
||||
|
||||
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
|
||||
leftPanelDisabled.subscribe(value => {
|
||||
value && moveAction() && moveAction(false);
|
||||
$htmlCL.toggle('rl-left-panel-disabled', value);
|
||||
});
|
||||
|
||||
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
|
||||
|
||||
// keys
|
||||
export const keyScopeReal = ko.observable(Scope.All);
|
||||
|
||||
export const keyScope = (()=>{
|
||||
let keyScopeFake = Scope.All;
|
||||
dropdownVisibility.subscribe(value => {
|
||||
if (value) {
|
||||
keyScope(Scope.Menu);
|
||||
} else if (Scope.Menu === shortcuts.getScope()) {
|
||||
keyScope(keyScopeFake);
|
||||
}
|
||||
});
|
||||
return ko.computed({
|
||||
read: () => keyScopeFake,
|
||||
write: value => {
|
||||
if (Scope.Menu !== value) {
|
||||
keyScopeFake = value;
|
||||
if (dropdownVisibility()) {
|
||||
value = Scope.Menu;
|
||||
}
|
||||
}
|
||||
|
||||
keyScopeReal(value);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
keyScopeReal.subscribe(value => shortcuts.setScope(value));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { createElement, SettingsGet } from 'Common/Globals';
|
||||
import { forEachObjectEntry, pInt } from 'Common/Utils';
|
||||
import { proxy } from 'Common/Links';
|
||||
|
||||
const
|
||||
tpl = createElement('template'),
|
||||
htmlre = /[&<>"']/g,
|
||||
htmlmap = {
|
||||
'&': '&',
|
||||
|
|
@ -6,17 +11,489 @@ const
|
|||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
},
|
||||
|
||||
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
|
||||
|
||||
// Strip utm_* tracking
|
||||
stripTracking = text => text.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1').replace(/&&+/, '');
|
||||
|
||||
export const
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]),
|
||||
|
||||
/**
|
||||
* Clears the Message Html for viewing
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
cleanHtml = (html, contentLocationUrls, removeColors) => {
|
||||
const
|
||||
debug = false, // Config()->Get('debug', 'enable', false);
|
||||
useProxy = !!SettingsGet('UseLocalProxyForExternalImages'),
|
||||
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
|
||||
|
||||
result = {
|
||||
hasExternals: false,
|
||||
foundCIDs: [],
|
||||
foundContentLocationUrls: []
|
||||
},
|
||||
|
||||
// convert body attributes to CSS
|
||||
tasks = {
|
||||
link: value => {
|
||||
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
|
||||
tpl.content.querySelectorAll('a').forEach(node => node.style.color = value)
|
||||
}
|
||||
},
|
||||
text: (value, node) => node.style.color = value,
|
||||
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
|
||||
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
|
||||
bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
|
||||
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
|
||||
},
|
||||
allowedAttributes = [
|
||||
// defaults
|
||||
'name',
|
||||
'dir', 'lang', 'style', 'title',
|
||||
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
|
||||
'border', 'bordercolor', 'charset', 'direction',
|
||||
// a
|
||||
'download', 'hreflang',
|
||||
// body
|
||||
'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
|
||||
// col
|
||||
'align', 'valign',
|
||||
// font
|
||||
'color', 'face', 'size',
|
||||
// hr
|
||||
'noshade',
|
||||
// img
|
||||
'hspace', 'sizes', 'srcset', 'vspace',
|
||||
// meter
|
||||
'low', 'high', 'optimum', 'value',
|
||||
// ol
|
||||
'reversed', 'start',
|
||||
// table
|
||||
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
|
||||
// th
|
||||
'abbr', 'scope',
|
||||
// td
|
||||
'colspan', 'rowspan', 'headers'
|
||||
],
|
||||
disallowedTags = [
|
||||
'HEAD','STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
|
||||
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
|
||||
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
|
||||
],
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG','O:P','TABLE'
|
||||
];
|
||||
|
||||
tpl.innerHTML = html
|
||||
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
|
||||
.replace(/<!doctype[^>]*>/gi, '')
|
||||
.replace(/<\?xml[^>]*\?>/gi, '')
|
||||
// Not supported by <template> element
|
||||
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
||||
.replace(/<\/?(html|head)[^>]*>/gi, '')
|
||||
.trim();
|
||||
html = '';
|
||||
|
||||
// \MailSo\Base\HtmlUtils::ClearComments()
|
||||
// https://github.com/the-djmaze/snappymail/issues/187
|
||||
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
|
||||
while (nodeIterator.nextNode()) {
|
||||
nodeIterator.referenceNode.remove();
|
||||
}
|
||||
|
||||
tpl.content.querySelectorAll('*').forEach(oElement => {
|
||||
const name = oElement.tagName,
|
||||
oStyle = oElement.style;
|
||||
|
||||
// \MailSo\Base\HtmlUtils::ClearTags()
|
||||
if (disallowedTags.includes(name)
|
||||
|| 'none' == oStyle.display
|
||||
|| 'hidden' == oStyle.visibility
|
||||
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
|
||||
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
||||
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
|
||||
// || ('0' === oStyle.opacity
|
||||
|| (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim() && !oElement.querySelector('img')))
|
||||
) {
|
||||
oElement.remove();
|
||||
return;
|
||||
}
|
||||
// if (['CENTER','FORM'].includes(name)) {
|
||||
if ('FORM' === name || 'O:P' === name) {
|
||||
replaceWithChildren(oElement);
|
||||
return;
|
||||
}
|
||||
/*
|
||||
// Idea to allow CSS
|
||||
if ('STYLE' === name) {
|
||||
msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
|
||||
oElement.textContent = oElement.textContent
|
||||
.replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
|
||||
.replace(/(background-)color:[^};]+/g, '');
|
||||
return;
|
||||
}
|
||||
*/
|
||||
const aAttrsForRemove = [],
|
||||
hasAttribute = name => oElement.hasAttribute(name),
|
||||
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
|
||||
setAttribute = (name, value) => oElement.setAttribute(name, value),
|
||||
delAttribute = name => oElement.removeAttribute(name);
|
||||
|
||||
if ('mail-body' === oElement.className) {
|
||||
forEachObjectEntry(tasks, (name, cb) => {
|
||||
if (hasAttribute(name)) {
|
||||
cb(getAttribute(name), oElement);
|
||||
delAttribute(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (oElement.hasAttributes()) {
|
||||
let i = oElement.attributes.length;
|
||||
while (i--) {
|
||||
let sAttrName = oElement.attributes[i].name.toLowerCase();
|
||||
if (!allowedAttributes.includes(sAttrName)) {
|
||||
delAttribute(sAttrName);
|
||||
aAttrsForRemove.push(sAttrName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
if ('TABLE' === name) {
|
||||
if (hasAttribute('width')) {
|
||||
oStyle.width = getAttribute('width');
|
||||
delAttribute('width');
|
||||
}
|
||||
value = oStyle.width;
|
||||
if (value && !value.includes('%')) {
|
||||
oStyle.maxWidth = value;
|
||||
oStyle.width = '100%';
|
||||
}
|
||||
}
|
||||
|
||||
else if ('A' === name) {
|
||||
value = oElement.href;
|
||||
if (!/^([a-z]+):/i.test(value)) {
|
||||
setAttribute('data-x-broken-href', value);
|
||||
delAttribute('href');
|
||||
} else {
|
||||
oElement.href = stripTracking(value);
|
||||
setAttribute('target', '_blank');
|
||||
setAttribute('rel', 'external nofollow noopener noreferrer');
|
||||
}
|
||||
setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
// SVG xlink:href
|
||||
/*
|
||||
if (hasAttribute('xlink:href')) {
|
||||
delAttribute('xlink:href');
|
||||
}
|
||||
*/
|
||||
|
||||
let skipStyle = false;
|
||||
if (hasAttribute('src')) {
|
||||
value = getAttribute('src');
|
||||
delAttribute('src');
|
||||
|
||||
if (detectHiddenImages
|
||||
&& 'IMG' === name
|
||||
&& (('' != getAttribute('height') && 3 > pInt(getAttribute('height')))
|
||||
|| ('' != getAttribute('width') && 3 > pInt(getAttribute('width')))
|
||||
|| [
|
||||
'email.microsoftemail.com/open',
|
||||
'github.com/notifications/beacon/',
|
||||
'mandrillapp.com/track/open',
|
||||
'list-manage.com/track/open'
|
||||
].filter(uri => value.toLowerCase().includes(uri)).length
|
||||
)) {
|
||||
skipStyle = true;
|
||||
setAttribute('style', 'display:none');
|
||||
setAttribute('data-x-hidden-src', value);
|
||||
}
|
||||
else if (contentLocationUrls[value])
|
||||
{
|
||||
setAttribute('data-x-src-location', value);
|
||||
result.foundContentLocationUrls.push(value);
|
||||
}
|
||||
else if ('cid:' === value.slice(0, 4))
|
||||
{
|
||||
setAttribute('data-x-src-cid', value.slice(4));
|
||||
result.foundCIDs.push(value.slice(4));
|
||||
}
|
||||
else if (/^https?:\/\//i.test(value) || '//' === value.slice(0, 2))
|
||||
{
|
||||
setAttribute('data-x-src', useProxy ? proxy(value) : value);
|
||||
result.hasExternals = true;
|
||||
}
|
||||
else if ('data:image/' === value.slice(0, 11))
|
||||
{
|
||||
setAttribute('src', value);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAttribute('data-x-broken-src', value);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAttribute('background')) {
|
||||
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
|
||||
delAttribute('background');
|
||||
}
|
||||
|
||||
if (hasAttribute('bgcolor')) {
|
||||
oStyle.backgroundColor = getAttribute('bgcolor');
|
||||
delAttribute('bgcolor');
|
||||
}
|
||||
|
||||
if (hasAttribute('color')) {
|
||||
oStyle.color = getAttribute('color');
|
||||
delAttribute('color');
|
||||
}
|
||||
|
||||
if (!skipStyle) {
|
||||
/*
|
||||
if ('fixed' === oStyle.position) {
|
||||
oStyle.position = 'absolute';
|
||||
}
|
||||
*/
|
||||
oStyle.removeProperty('behavior');
|
||||
oStyle.removeProperty('cursor');
|
||||
oStyle.removeProperty('min-width');
|
||||
|
||||
const urls = {
|
||||
cid: [], // 'data-x-style-cid'
|
||||
remote: [], // 'data-x-style-url'
|
||||
broken: [] // 'data-x-broken-style-src'
|
||||
};
|
||||
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
|
||||
if (oStyle[property]) {
|
||||
let value = oStyle[property],
|
||||
found = value.match(/url\s*\(([^)]+)\)/gi);
|
||||
if (found) {
|
||||
oStyle[property] = null;
|
||||
found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
|
||||
let lowerUrl = found.toLowerCase();
|
||||
if ('cid:' === lowerUrl.slice(0, 4)) {
|
||||
found = found.slice(4);
|
||||
urls.cid[property] = found
|
||||
result.foundCIDs.push(found);
|
||||
} else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
|
||||
result.hasExternals = true;
|
||||
urls.remote[property] = useProxy ? proxy(found) : found;
|
||||
} else if ('data:image/' === lowerUrl.slice(0, 11)) {
|
||||
oStyle[property] = value;
|
||||
} else {
|
||||
urls.broken[property] = found;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// oStyle.removeProperty('background-image');
|
||||
// oStyle.removeProperty('list-style-image');
|
||||
|
||||
if (urls.cid.length) {
|
||||
setAttribute('data-x-style-cid', JSON.stringify(urls.cid));
|
||||
}
|
||||
if (urls.remote.length) {
|
||||
setAttribute('data-x-style-url', JSON.stringify(urls.remote));
|
||||
}
|
||||
if (urls.broken.length) {
|
||||
setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
|
||||
}
|
||||
|
||||
if (11 > pInt(oStyle.fontSize)) {
|
||||
oStyle.removeProperty('font-size');
|
||||
}
|
||||
|
||||
// Removes background and color
|
||||
// Many e-mails incorrectly only define one, not both
|
||||
// And in dark theme mode this kills the readability
|
||||
if (removeColors) {
|
||||
oStyle.removeProperty('background-color');
|
||||
oStyle.removeProperty('background-image');
|
||||
oStyle.removeProperty('color');
|
||||
}
|
||||
|
||||
// Drop Microsoft Office style properties
|
||||
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
|
||||
}
|
||||
|
||||
if (debug && aAttrsForRemove.length) {
|
||||
setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
|
||||
}
|
||||
});
|
||||
|
||||
// return tpl.content.firstChild;
|
||||
result.html = tpl.innerHTML.trim();
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @returns {string}
|
||||
*/
|
||||
htmlToPlain = html => {
|
||||
const
|
||||
hr = '⎯'.repeat(64),
|
||||
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
|
||||
blockquotes = node => {
|
||||
let bq;
|
||||
while ((bq = node.querySelector('blockquote'))) {
|
||||
// Convert child blockquote first
|
||||
blockquotes(bq);
|
||||
// Convert blockquote
|
||||
// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '> ');
|
||||
// replaceWithChildren(bq);
|
||||
bq.replaceWith(
|
||||
'\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
html = html
|
||||
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
|
||||
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
|
||||
.replace(/\r?\n/g, '')
|
||||
.replace(/\s+/gm, ' ');
|
||||
|
||||
while (/<(div|tr)[\s>]/i.test(html)) {
|
||||
html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
||||
}
|
||||
while (/<\/(div|tr)[\s>]/i.test(html)) {
|
||||
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
||||
}
|
||||
|
||||
tpl.innerHTML = html
|
||||
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
||||
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||
|
||||
// Convert line-breaks
|
||||
forEach('br', br => br.replaceWith('\n'));
|
||||
|
||||
// lines
|
||||
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
|
||||
|
||||
// headings
|
||||
forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
|
||||
|
||||
// paragraphs
|
||||
forEach('p', node => {
|
||||
node.prepend('\n\n');
|
||||
if ('' == node.textContent.trim()) {
|
||||
node.remove();
|
||||
} else {
|
||||
node.after('\n\n');
|
||||
}
|
||||
});
|
||||
|
||||
// proper indenting and numbering of (un)ordered lists
|
||||
forEach('ol,ul', node => {
|
||||
let prefix = '',
|
||||
parent = node,
|
||||
ordered = 'OL' == node.tagName,
|
||||
i = 0;
|
||||
while (parent && parent.parentNode && parent.parentNode.closest) {
|
||||
parent = parent.parentNode.closest('ol,ul');
|
||||
parent && (prefix = ' ' + prefix);
|
||||
}
|
||||
node.querySelectorAll(':scope > li').forEach(li => {
|
||||
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
|
||||
});
|
||||
node.prepend('\n\n');
|
||||
node.after('\n\n');
|
||||
});
|
||||
|
||||
// Convert anchors
|
||||
forEach('a', a => {
|
||||
let txt = a.textContent, href = a.href;
|
||||
return a.replaceWith((txt.trim() == href ? txt : txt + ' ' + href + ' '));
|
||||
});
|
||||
|
||||
// Bold
|
||||
forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
|
||||
// Italic
|
||||
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
||||
|
||||
// Blockquotes must be last
|
||||
blockquotes(tpl.content);
|
||||
|
||||
return (tpl.content.textContent || '').replace(/\n{3,}/gm, '\n\n').trim();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} plain
|
||||
* @param {boolean} findEmailAndLinksInText = false
|
||||
* @returns {string}
|
||||
*/
|
||||
plainToHtml = plain => {
|
||||
plain = stripTracking(plain)
|
||||
.toString()
|
||||
.replace(/\r/g, '')
|
||||
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
|
||||
|
||||
let bIn = false,
|
||||
bDo = true,
|
||||
bStart = true,
|
||||
aNextText = [],
|
||||
aText = plain.split('\n');
|
||||
|
||||
do {
|
||||
bDo = false;
|
||||
aNextText = [];
|
||||
aText.forEach(sLine => {
|
||||
bStart = '>' === sLine.slice(0, 1);
|
||||
if (bStart && !bIn) {
|
||||
bDo = true;
|
||||
bIn = true;
|
||||
aNextText.push('~~~blockquote~~~');
|
||||
aNextText.push(sLine.slice(1));
|
||||
} else if (!bStart && bIn) {
|
||||
if (sLine) {
|
||||
bIn = false;
|
||||
aNextText.push('~~~/blockquote~~~');
|
||||
aNextText.push(sLine);
|
||||
} else {
|
||||
aNextText.push(sLine);
|
||||
}
|
||||
} else if (bStart && bIn) {
|
||||
aNextText.push(sLine.slice(1));
|
||||
} else {
|
||||
aNextText.push(sLine);
|
||||
}
|
||||
});
|
||||
|
||||
if (bIn) {
|
||||
bIn = false;
|
||||
aNextText.push('~~~/blockquote~~~');
|
||||
}
|
||||
|
||||
aText = aNextText;
|
||||
} while (bDo);
|
||||
|
||||
return aText.join('\n')
|
||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
|
||||
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
|
||||
.replace(/\n/g, '<br>');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
export function encodeHtml(text) {
|
||||
return (text && text.toString ? text.toString() : ''+text).replace(htmlre, m => htmlmap[m]);
|
||||
}
|
||||
|
||||
class HtmlEditor {
|
||||
export class HtmlEditor {
|
||||
/**
|
||||
* @param {Object} element
|
||||
* @param {Function=} onBlur
|
||||
|
|
@ -24,25 +501,37 @@ class HtmlEditor {
|
|||
* @param {Function=} onModeChange
|
||||
*/
|
||||
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
|
||||
this.editor;
|
||||
this.blurTimer = 0;
|
||||
|
||||
this.__resizable = false;
|
||||
this.__inited = false;
|
||||
|
||||
this.onBlur = onBlur;
|
||||
this.onReady = onReady;
|
||||
this.onModeChange = onModeChange;
|
||||
|
||||
this.element = element;
|
||||
if (element) {
|
||||
let editor;
|
||||
|
||||
this.resize = (() => {
|
||||
try {
|
||||
this.editor && this.__resizable && this.editor.resize(element.clientWidth, element.clientHeight);
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
}).throttle(100);
|
||||
onReady = onReady ? [onReady] : [];
|
||||
this.onReady = fn => onReady.push(fn);
|
||||
const readyCallback = () => {
|
||||
this.editor = editor;
|
||||
this.onReady = fn => fn();
|
||||
onReady.forEach(fn => fn());
|
||||
};
|
||||
|
||||
this.init();
|
||||
if (rl.createWYSIWYG) {
|
||||
editor = rl.createWYSIWYG(element, readyCallback);
|
||||
}
|
||||
if (!editor) {
|
||||
editor = new SquireUI(element);
|
||||
setTimeout(readyCallback, 1);
|
||||
}
|
||||
|
||||
editor.on('blur', () => this.blurTrigger());
|
||||
editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
|
||||
editor.on('mode', () => {
|
||||
this.blurTrigger();
|
||||
this.onModeChange && this.onModeChange(!this.isPlain());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
blurTrigger() {
|
||||
|
|
@ -70,9 +559,9 @@ class HtmlEditor {
|
|||
* @returns {void}
|
||||
*/
|
||||
clearCachedSignature() {
|
||||
this.editor && this.editor.execCommand('insertSignature', {
|
||||
this.onReady(() => this.editor.execCommand('insertSignature', {
|
||||
clearCache: true
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -82,75 +571,66 @@ class HtmlEditor {
|
|||
* @returns {void}
|
||||
*/
|
||||
setSignature(signature, html, insertBefore = false) {
|
||||
this.editor && this.editor.execCommand('insertSignature', {
|
||||
this.onReady(() => this.editor.execCommand('insertSignature', {
|
||||
isHtml: html,
|
||||
insertBefore: insertBefore,
|
||||
signature: signature
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} wrapIsHtml = false
|
||||
* @returns {string}
|
||||
*/
|
||||
getData(wrapIsHtml = false) {
|
||||
getData() {
|
||||
let result = '';
|
||||
if (this.editor) {
|
||||
try {
|
||||
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
|
||||
result = this.editor.__plain.getRawData();
|
||||
} else {
|
||||
result = wrapIsHtml
|
||||
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
|
||||
this.editor.getData() +
|
||||
'</div>'
|
||||
: this.editor.getData();
|
||||
result = this.editor.getData();
|
||||
}
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} wrapIsHtml = false
|
||||
* @returns {string}
|
||||
*/
|
||||
getDataWithHtmlMark(wrapIsHtml = false) {
|
||||
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml);
|
||||
getDataWithHtmlMark() {
|
||||
return (this.isHtml() ? ':HTML:' : '') + this.getData();
|
||||
}
|
||||
|
||||
modeWysiwyg() {
|
||||
try {
|
||||
this.editor && this.editor.setMode('wysiwyg');
|
||||
} catch (e) { console.error(e); }
|
||||
this.onReady(() => this.editor.setMode('wysiwyg'));
|
||||
}
|
||||
modePlain() {
|
||||
try {
|
||||
this.editor && this.editor.setMode('plain');
|
||||
} catch (e) { console.error(e); }
|
||||
this.onReady(() => this.editor.setMode('plain'));
|
||||
}
|
||||
|
||||
setHtmlOrPlain(text) {
|
||||
if (':HTML:' === text.substr(0, 6)) {
|
||||
this.setHtml(text.substr(6));
|
||||
if (':HTML:' === text.slice(0, 6)) {
|
||||
this.setHtml(text.slice(6));
|
||||
} else {
|
||||
this.setPlain(text);
|
||||
}
|
||||
}
|
||||
|
||||
setData(mode, data) {
|
||||
if (this.editor && this.__inited) {
|
||||
this.onReady(() => {
|
||||
const editor = this.editor;
|
||||
this.clearCachedSignature();
|
||||
try {
|
||||
this.editor.setMode(mode);
|
||||
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
|
||||
this.editor.__plain.setRawData(data);
|
||||
editor.setMode(mode);
|
||||
if (this.isPlain() && editor.plugins.plain && editor.__plain) {
|
||||
editor.__plain.setRawData(data);
|
||||
} else {
|
||||
this.editor.setData(data);
|
||||
editor.setData(data);
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setHtml(html) {
|
||||
|
|
@ -161,46 +641,8 @@ class HtmlEditor {
|
|||
this.setData('plain', txt);
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.element && !this.editor) {
|
||||
const onReady = () => {
|
||||
if (this.editor.removeMenuItem) {
|
||||
this.editor.removeMenuItem('cut');
|
||||
this.editor.removeMenuItem('copy');
|
||||
this.editor.removeMenuItem('paste');
|
||||
}
|
||||
|
||||
this.__resizable = true;
|
||||
this.__inited = true;
|
||||
|
||||
this.resize();
|
||||
|
||||
this.onReady && this.onReady();
|
||||
};
|
||||
|
||||
if (rl.createWYSIWYG) {
|
||||
this.editor = rl.createWYSIWYG(this.element, onReady);
|
||||
}
|
||||
if (!this.editor) {
|
||||
this.editor = new SquireUI(this.element);
|
||||
setTimeout(onReady,1);
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.on('blur', () => this.blurTrigger());
|
||||
this.editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
|
||||
this.editor.on('mode', () => {
|
||||
this.blurTrigger();
|
||||
this.onModeChange && this.onModeChange(!this.isPlain());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
try {
|
||||
this.editor && this.editor.focus();
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
this.onReady(() => this.editor.focus());
|
||||
}
|
||||
|
||||
hasFocus() {
|
||||
|
|
@ -212,14 +654,15 @@ class HtmlEditor {
|
|||
}
|
||||
|
||||
blur() {
|
||||
try {
|
||||
this.editor && this.editor.focusManager.blur(true);
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
this.onReady(() => this.editor.focusManager.blur(true));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setHtml('');
|
||||
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
|
||||
}
|
||||
}
|
||||
|
||||
export { HtmlEditor, HtmlEditor as default };
|
||||
rl.Utils = {
|
||||
htmlToPlain: htmlToPlain,
|
||||
plainToHtml: plainToHtml
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,191 +1,140 @@
|
|||
import { pString, pInt } from 'Common/Utils';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { Settings } from 'Common/Globals';
|
||||
|
||||
const
|
||||
ROOT = './',
|
||||
HASH_PREFIX = '#/',
|
||||
SERVER_PREFIX = './?',
|
||||
VERSION = Settings.app('version'),
|
||||
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
|
||||
VERSION_PREFIX = () => Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
|
||||
|
||||
getHash = () => SettingsGet('AuthAccountHash') || '0';
|
||||
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export const SUB_QUERY_PREFIX = '&q[]=';
|
||||
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
|
||||
|
||||
/**
|
||||
* @param {string=} startupUrl
|
||||
* @returns {string}
|
||||
*/
|
||||
export function root(startupUrl = '') {
|
||||
return HASH_PREFIX + pString(startupUrl);
|
||||
}
|
||||
export const
|
||||
SUB_QUERY_PREFIX = '&q[]=',
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function logoutLink() {
|
||||
return (rl.adminArea() && !Settings.app('adminHostUse'))
|
||||
? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
|
||||
: ROOT;
|
||||
}
|
||||
/**
|
||||
* @param {string=} startupUrl
|
||||
* @returns {string}
|
||||
*/
|
||||
root = () => HASH_PREFIX,
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {string} hash
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serverRequestRaw(type, hash, customSpecSuffix) {
|
||||
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||
+ (null == customSpecSuffix ? getHash() : customSpecSuffix) + '/'
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
logoutLink = () => adminPath() ? prefix() : './',
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {string} hash
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
serverRequestRaw = (type, hash) =>
|
||||
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||
+ '0/' // AuthAccountHash ?
|
||||
+ (type
|
||||
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
||||
: '')
|
||||
;
|
||||
}
|
||||
: ''),
|
||||
|
||||
/**
|
||||
* @param {string} download
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
export function attachmentDownload(download, customSpecSuffix) {
|
||||
return serverRequestRaw('Download', download, customSpecSuffix);
|
||||
}
|
||||
/**
|
||||
* @param {string} download
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
attachmentDownload = (download, customSpecSuffix) =>
|
||||
serverRequestRaw('Download', download, customSpecSuffix),
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serverRequest(type) {
|
||||
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
|
||||
}
|
||||
proxy = url =>
|
||||
SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
/*
|
||||
return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
|
||||
'Rnd' => \md5(\microtime(true)),
|
||||
'Token' => Utils::GetConnectionToken(),
|
||||
'Url' => $sUrl
|
||||
)).'/';
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} email
|
||||
* @returns {string}
|
||||
*/
|
||||
export function change(email) {
|
||||
return serverRequest('Change') + encodeURIComponent(email) + '/';
|
||||
}
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {string}
|
||||
*/
|
||||
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
||||
|
||||
/**
|
||||
* @param {string} hash
|
||||
* @returns {string}
|
||||
*/
|
||||
export function userBackground(hash) {
|
||||
return serverRequestRaw('UserBackground', hash);
|
||||
}
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {boolean} isAdmin
|
||||
* @returns {string}
|
||||
*/
|
||||
langLink = (lang, isAdmin) =>
|
||||
SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/',
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {boolean} isAdmin
|
||||
* @returns {string}
|
||||
*/
|
||||
export function langLink(lang, isAdmin) {
|
||||
return SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/';
|
||||
}
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
staticLink = path => VERSION_PREFIX() + 'static/' + path,
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
export function staticLink(path) {
|
||||
return VERSION_PREFIX + 'static/' + path;
|
||||
}
|
||||
/**
|
||||
* @param {string} theme
|
||||
* @returns {string}
|
||||
*/
|
||||
themePreviewLink = theme => {
|
||||
let prefix = VERSION_PREFIX();
|
||||
if ('@custom' === theme.slice(-7)) {
|
||||
theme = theme.slice(0, theme.length - 7).trim();
|
||||
prefix = Settings.app('webPath') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function openPgpJs() {
|
||||
return staticLink('js/min/openpgp.min.js');
|
||||
}
|
||||
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function openPgpWorkerJs() {
|
||||
return staticLink('js/min/openpgp.worker.min.js');
|
||||
}
|
||||
/**
|
||||
* @param {string} inboxFolderName = 'INBOX'
|
||||
* @returns {string}
|
||||
*/
|
||||
mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName,
|
||||
|
||||
/**
|
||||
* @param {string} theme
|
||||
* @returns {string}
|
||||
*/
|
||||
export function themePreviewLink(theme) {
|
||||
let prefix = VERSION_PREFIX;
|
||||
if ('@custom' === theme.substr(-7)) {
|
||||
theme = theme.substr(0, theme.length - 7).trim();
|
||||
prefix = Settings.app('webPath') || '';
|
||||
}
|
||||
/**
|
||||
* @param {string=} screenName = ''
|
||||
* @returns {string}
|
||||
*/
|
||||
settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''),
|
||||
|
||||
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
|
||||
}
|
||||
/**
|
||||
* @param {string=} screenName
|
||||
* @returns {string}
|
||||
*/
|
||||
admin = screenName => HASH_PREFIX + (
|
||||
'AdminDomains' == screenName ? 'domains'
|
||||
: 'AdminSecurity' == screenName ? 'security'
|
||||
: ''
|
||||
),
|
||||
|
||||
/**
|
||||
* @param {string} inboxFolderName = 'INBOX'
|
||||
* @returns {string}
|
||||
*/
|
||||
export function mailbox(inboxFolderName = 'INBOX') {
|
||||
return HASH_PREFIX + 'mailbox/' + inboxFolderName;
|
||||
}
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {number=} page = 1
|
||||
* @param {string=} search = ''
|
||||
* @param {number=} threadUid = 0
|
||||
* @returns {string}
|
||||
*/
|
||||
mailBox = (folder, page, search, threadUid) => {
|
||||
let result = [HASH_PREFIX + 'mailbox'];
|
||||
|
||||
/**
|
||||
* @param {string=} screenName = ''
|
||||
* @returns {string}
|
||||
*/
|
||||
export function settings(screenName = '') {
|
||||
return HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');
|
||||
}
|
||||
if (folder) {
|
||||
result.push(folder + (threadUid ? '~' + threadUid : ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} screenName
|
||||
* @returns {string}
|
||||
*/
|
||||
export function admin(screenName) {
|
||||
let result = HASH_PREFIX;
|
||||
switch (screenName) {
|
||||
case 'AdminDomains':
|
||||
result += 'domains';
|
||||
break;
|
||||
case 'AdminSecurity':
|
||||
result += 'security';
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
page = pInt(page, 1);
|
||||
if (1 < page) {
|
||||
result.push('p' + page);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {number=} page = 1
|
||||
* @param {string=} search = ''
|
||||
* @param {string=} threadUid = ''
|
||||
* @returns {string}
|
||||
*/
|
||||
export function mailBox(folder, page = 1, search = '', threadUid = '') {
|
||||
page = pInt(page, 1);
|
||||
search = pString(search);
|
||||
|
||||
let result = HASH_PREFIX + 'mailbox/';
|
||||
|
||||
if (folder) {
|
||||
const resultThreadUid = pInt(threadUid);
|
||||
result += encodeURI(folder) + (0 < resultThreadUid ? '~' + resultThreadUid : '');
|
||||
}
|
||||
|
||||
if (1 < page) {
|
||||
result = result.replace(/\/+$/, '') + '/p' + page;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
result = result.replace(/\/+$/, '') + '/' + encodeURI(search);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
search = pString(search);
|
||||
if (search) {
|
||||
result.push(encodeURI(search));
|
||||
}
|
||||
|
||||
return result.join('/');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
import { doc } from 'Common/Globals';
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
||||
export function timestampToString(timeStampInUTC, formatStr) {
|
||||
const now = Date.now(),
|
||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||
|
||||
if (31536000000 < time) {
|
||||
const m = new Date(time);
|
||||
switch (formatStr) {
|
||||
case 'FROMNOW':
|
||||
return m.fromNow();
|
||||
case 'SHORT': {
|
||||
if (4 >= (now - time) / 3600000)
|
||||
return m.fromNow();
|
||||
const mt = m.getTime(), date = new Date,
|
||||
dt = date.setHours(0,0,0,0);
|
||||
if (mt > dt)
|
||||
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
||||
if (mt > dt - 86400000)
|
||||
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
||||
if (date.getFullYear() === m.getFullYear())
|
||||
return m.format('d M');
|
||||
return m.format('LL');
|
||||
}
|
||||
case 'FULL':
|
||||
return m.format('LLL');
|
||||
default:
|
||||
return m.format(formatStr);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function timeToNode(element, time) {
|
||||
try {
|
||||
time = time || (Date.parse(element.dateTime) / 1000);
|
||||
if (time) {
|
||||
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
||||
|
||||
let key = element.dataset.momentFormat;
|
||||
if (key) {
|
||||
element.textContent = timestampToString(time, key);
|
||||
}
|
||||
|
||||
if ((key = element.dataset.momentFormatTitle)) {
|
||||
element.title = timestampToString(time, key);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// prevent knockout crashes
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener('reload-time', () => setTimeout(() =>
|
||||
doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
|
||||
, 1)
|
||||
);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||
import { isArray, isFunction } from 'Common/Utils';
|
||||
|
||||
const SIMPLE_HOOKS = {},
|
||||
|
|
@ -36,7 +37,7 @@ export function runHook(name, args = []) {
|
|||
* @param {?number=} timeout
|
||||
*/
|
||||
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
|
||||
rl.app && rl.app.Remote.defaultRequest(callback, 'Plugin' + action, parameters, timeout);
|
||||
rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -78,3 +79,5 @@ rl.pluginSettingsGet = (pluginSection, name) => {
|
|||
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
|
||||
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
|
||||
};
|
||||
|
||||
rl.pluginPopupView = AbstractViewPopup;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import ko from 'ko';
|
||||
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
/*
|
||||
oCallbacks:
|
||||
ItemSelect
|
||||
MiddleClick
|
||||
AutoSelect
|
||||
ItemGetUid
|
||||
UpOrDown
|
||||
*/
|
||||
|
||||
export class Selector {
|
||||
/**
|
||||
|
|
@ -19,8 +30,8 @@ export class Selector {
|
|||
sItemFocusedSelector
|
||||
) {
|
||||
this.list = koList;
|
||||
this.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
|
||||
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
|
||||
this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
|
||||
this.isListChecked = koComputable(() => 0 < this.listChecked().length);
|
||||
|
||||
this.focusedItem = koFocusedItem || ko.observable(null);
|
||||
this.selectedItem = koSelectedItem || ko.observable(null);
|
||||
|
|
@ -209,11 +220,9 @@ export class Selector {
|
|||
|
||||
itemSelected(item) {
|
||||
if (this.isListChecked()) {
|
||||
if (!item) {
|
||||
(this.oCallbacks.onItemSelect || (()=>{}))(item || null);
|
||||
}
|
||||
item || (this.oCallbacks.ItemSelect || (()=>0))(null);
|
||||
} else if (item) {
|
||||
(this.oCallbacks.onItemSelect || (()=>{}))(item);
|
||||
(this.oCallbacks.ItemSelect || (()=>0))(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,13 +235,17 @@ export class Selector {
|
|||
this.oContentScrollable = contentScrollable;
|
||||
|
||||
if (contentScrollable) {
|
||||
contentScrollable.addEventListener('click', event => {
|
||||
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||
el && this.actionClick(ko.dataFor(el), event);
|
||||
let getItem = selector => {
|
||||
let el = event.target.closestWithin(selector, contentScrollable);
|
||||
return el ? ko.dataFor(el) : null;
|
||||
};
|
||||
|
||||
el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable);
|
||||
if (el) {
|
||||
const item = ko.dataFor(el);
|
||||
addEventsListeners(contentScrollable, {
|
||||
click: event => {
|
||||
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||
el && this.actionClick(ko.dataFor(el), event);
|
||||
|
||||
const item = getItem(this.sItemCheckedSelector);
|
||||
if (item) {
|
||||
if (event.shiftKey) {
|
||||
this.actionClick(item, event);
|
||||
|
|
@ -241,26 +254,33 @@ export class Selector {
|
|||
item.checked(!item.checked());
|
||||
}
|
||||
}
|
||||
},
|
||||
auxclick: event => {
|
||||
if (1 == event.button) {
|
||||
const item = getItem(this.sItemSelector);
|
||||
if (item) {
|
||||
this.focusedItem(item);
|
||||
(this.oCallbacks.MiddleClick || (()=>0))(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
shortcuts.add('enter,open', '', keyScope, () => {
|
||||
registerShortcut('enter,open', '', keyScope, () => {
|
||||
const focused = this.focusedItem();
|
||||
if (focused && !focused.selected()) {
|
||||
this.actionClick(focused);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
shortcuts.add('arrowup,arrowdown', 'meta', keyScope, () => false);
|
||||
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
|
||||
|
||||
shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => {
|
||||
addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
|
||||
this.newSelectPosition(event.key, true);
|
||||
return false;
|
||||
});
|
||||
shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
||||
registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
||||
this.newSelectPosition(event.key, false);
|
||||
return false;
|
||||
});
|
||||
|
|
@ -271,7 +291,7 @@ export class Selector {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
autoSelect() {
|
||||
return !!(this.oCallbacks.onAutoSelect || (()=>true))();
|
||||
return !!(this.oCallbacks.AutoSelect || (()=>1))();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -281,7 +301,7 @@ export class Selector {
|
|||
getItemUid(item) {
|
||||
let uid = '';
|
||||
|
||||
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
|
||||
const getItemUidCallback = this.oCallbacks.ItemGetUid || null;
|
||||
if (getItemUidCallback && item) {
|
||||
uid = getItemUidCallback(item);
|
||||
}
|
||||
|
|
@ -312,7 +332,7 @@ export class Selector {
|
|||
} else if (++i < listLen) {
|
||||
result = list[i];
|
||||
}
|
||||
result || (this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey);
|
||||
result || (this.oCallbacks.UpOrDown || (()=>0))('ArrowUp' === sEventKey);
|
||||
} else if ('Home' === sEventKey) {
|
||||
result = list[0];
|
||||
} else if ('End' === sEventKey) {
|
||||
|
|
|
|||
|
|
@ -2,158 +2,211 @@ import ko from 'ko';
|
|||
import { Notification, UploadErrorCode } from 'Common/Enums';
|
||||
import { langLink } from 'Common/Links';
|
||||
import { doc, createElement } from 'Common/Globals';
|
||||
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
||||
|
||||
let I18N_DATA = window.snappymailI18N || {};
|
||||
let I18N_DATA = {};
|
||||
|
||||
export const trigger = ko.observable(false);
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Object=} valueList
|
||||
* @param {string=} defaulValue
|
||||
* @returns {string}
|
||||
*/
|
||||
export function i18n(key, valueList, defaulValue) {
|
||||
let path = key.split('/');
|
||||
if (!I18N_DATA[path[0]] || !path[1]) {
|
||||
return defaulValue || key;
|
||||
}
|
||||
let result = I18N_DATA[path[0]][path[1]] || defaulValue || key;
|
||||
if (valueList) {
|
||||
Object.entries(valueList).forEach(([key, value]) => {
|
||||
result = result.replace('%' + key + '%', value);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const i18nToNode = element => {
|
||||
const key = element.dataset.i18n;
|
||||
if (key) {
|
||||
if ('[' === key.substr(0, 1)) {
|
||||
switch (key.substr(0, 6)) {
|
||||
case '[html]':
|
||||
element.innerHTML = i18n(key.substr(6));
|
||||
break;
|
||||
case '[place':
|
||||
element.placeholder = i18n(key.substr(13));
|
||||
break;
|
||||
case '[title':
|
||||
element.title = i18n(key.substr(7));
|
||||
break;
|
||||
// no default
|
||||
const
|
||||
i18nToNode = element => {
|
||||
const key = element.dataset.i18n;
|
||||
if (key) {
|
||||
if ('[' === key.slice(0, 1)) {
|
||||
switch (key.slice(0, 6)) {
|
||||
case '[html]':
|
||||
element.innerHTML = i18n(key.slice(6));
|
||||
break;
|
||||
case '[place':
|
||||
element.placeholder = i18n(key.slice(13));
|
||||
break;
|
||||
case '[title':
|
||||
element.title = i18n(key.slice(7));
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
} else {
|
||||
element.textContent = i18n(key);
|
||||
}
|
||||
} else {
|
||||
element.textContent = i18n(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
init = () => {
|
||||
if (rl.I18N) {
|
||||
I18N_DATA = rl.I18N;
|
||||
Date.defineRelativeTimeFormat(rl.relativeTime || {});
|
||||
rl.I18N = null;
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
|
||||
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
||||
|
||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} elements
|
||||
* @param {boolean=} animate = false
|
||||
*/
|
||||
export function i18nToNodes(element) {
|
||||
setTimeout(() =>
|
||||
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
|
||||
, 1);
|
||||
}
|
||||
export const
|
||||
trigger = ko.observable(false),
|
||||
|
||||
/**
|
||||
* @param {Function} startCallback
|
||||
* @param {Function=} langCallback = null
|
||||
*/
|
||||
export function initOnStartOrLangChange(startCallback, langCallback = null) {
|
||||
startCallback && startCallback();
|
||||
startCallback && trigger.subscribe(startCallback);
|
||||
langCallback && trigger.subscribe(langCallback);
|
||||
}
|
||||
|
||||
function getNotificationMessage(code) {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
if (key) {
|
||||
key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
|
||||
return I18N_DATA.NOTIFICATIONS[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} code
|
||||
* @param {*=} message = ''
|
||||
* @param {*=} defCode = null
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getNotification(code, message = '', defCode = 0) {
|
||||
code = parseInt(code, 10) || 0;
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return getNotificationMessage(code)
|
||||
|| getNotificationMessage(parseInt(defCode, 10))
|
||||
|| '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUploadErrorDescByCode(code) {
|
||||
let result = 'UNKNOWN';
|
||||
code = parseInt(code, 10) || 0;
|
||||
switch (code) {
|
||||
case UploadErrorCode.FileIsTooBig:
|
||||
case UploadErrorCode.FilePartiallyUploaded:
|
||||
case UploadErrorCode.NoFileUploaded:
|
||||
case UploadErrorCode.MissingTempFolder:
|
||||
case UploadErrorCode.OnSavingFile:
|
||||
case UploadErrorCode.FileType:
|
||||
result = i18nKey(getKeyByValue(UploadErrorCode, code));
|
||||
break;
|
||||
}
|
||||
return i18n('UPLOAD/ERROR_' + result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} admin
|
||||
* @param {string} language
|
||||
*/
|
||||
export function reload(admin, language) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = createElement('script');
|
||||
script.onload = () => {
|
||||
// reload the data
|
||||
if (window.snappymailI18N) {
|
||||
I18N_DATA = window.snappymailI18N;
|
||||
i18nToNodes(doc);
|
||||
dispatchEvent(new CustomEvent('reload-time'));
|
||||
trigger(!trigger());
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Object=} valueList
|
||||
* @param {string=} defaulValue
|
||||
* @returns {string}
|
||||
*/
|
||||
i18n = (key, valueList, defaulValue) => {
|
||||
let result = defaulValue || key;
|
||||
if (key) {
|
||||
let path = key.split('/');
|
||||
if (I18N_DATA[path[0]] && path[1]) {
|
||||
result = I18N_DATA[path[0]][path[1]] || result;
|
||||
}
|
||||
window.snappymailI18N = null;
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
||||
script.src = langLink(language, admin);
|
||||
// script.async = true;
|
||||
doc.head.append(script);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (valueList) {
|
||||
forEachObjectEntry(valueList, (key, value) => {
|
||||
result = result.replace('%' + key + '%', value);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} language
|
||||
* @param {boolean=} isEng = false
|
||||
* @returns {string}
|
||||
*/
|
||||
export function convertLangName(language, isEng = false) {
|
||||
return i18n(
|
||||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
||||
null,
|
||||
language
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {Object} elements
|
||||
* @param {boolean=} animate = false
|
||||
*/
|
||||
i18nToNodes = element =>
|
||||
setTimeout(() =>
|
||||
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
|
||||
, 1),
|
||||
|
||||
timestampToString = (timeStampInUTC, formatStr) => {
|
||||
const now = Date.now(),
|
||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||
|
||||
if (31536000000 < time) {
|
||||
const m = new Date(time);
|
||||
switch (formatStr) {
|
||||
case 'FROMNOW':
|
||||
return m.fromNow();
|
||||
case 'SHORT': {
|
||||
if (4 >= (now - time) / 3600000)
|
||||
return m.fromNow();
|
||||
const mt = m.getTime(), date = new Date,
|
||||
dt = date.setHours(0,0,0,0);
|
||||
if (mt > dt)
|
||||
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
||||
if (mt > dt - 86400000)
|
||||
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
||||
if (date.getFullYear() === m.getFullYear())
|
||||
return m.format('d M');
|
||||
return m.format('LL');
|
||||
}
|
||||
case 'FULL':
|
||||
return m.format('LLL');
|
||||
default:
|
||||
return m.format(formatStr);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
timeToNode = (element, time) => {
|
||||
try {
|
||||
if (time) {
|
||||
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
||||
} else {
|
||||
time = Date.parse(element.dateTime) / 1000;
|
||||
}
|
||||
|
||||
let key = element.dataset.momentFormat;
|
||||
if (key) {
|
||||
element.textContent = timestampToString(time, key);
|
||||
}
|
||||
|
||||
if ((key = element.dataset.momentFormatTitle)) {
|
||||
element.title = timestampToString(time, key);
|
||||
}
|
||||
} catch (e) {
|
||||
// prevent knockout crashes
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
reloadTime = () => setTimeout(() =>
|
||||
doc.querySelectorAll('time').forEach(element => timeToNode(element))
|
||||
, 1),
|
||||
|
||||
/**
|
||||
* @param {Function} startCallback
|
||||
* @param {Function=} langCallback = null
|
||||
*/
|
||||
initOnStartOrLangChange = (startCallback, langCallback = null) => {
|
||||
startCallback && startCallback();
|
||||
startCallback && trigger.subscribe(startCallback);
|
||||
langCallback && trigger.subscribe(langCallback);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} code
|
||||
* @param {*=} message = ''
|
||||
* @param {*=} defCode = null
|
||||
* @returns {string}
|
||||
*/
|
||||
getNotification = (code, message = '', defCode = 0) => {
|
||||
code = parseInt(code, 10) || 0;
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return getNotificationMessage(code)
|
||||
|| getNotificationMessage(parseInt(defCode, 10))
|
||||
|| '';
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {*} code
|
||||
* @returns {string}
|
||||
*/
|
||||
getUploadErrorDescByCode = code => {
|
||||
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
|
||||
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {boolean} admin
|
||||
* @param {string} language
|
||||
*/
|
||||
translatorReload = (admin, language) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const script = createElement('script');
|
||||
script.onload = () => {
|
||||
// reload the data
|
||||
if (init()) {
|
||||
i18nToNodes(doc);
|
||||
admin || reloadTime();
|
||||
trigger(!trigger());
|
||||
}
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
||||
script.src = langLink(language, admin);
|
||||
// script.async = true;
|
||||
doc.head.append(script);
|
||||
}),
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} language
|
||||
* @param {boolean=} isEng = false
|
||||
* @returns {string}
|
||||
*/
|
||||
convertLangName = (language, isEng = false) =>
|
||||
i18n(
|
||||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
||||
null,
|
||||
language
|
||||
);
|
||||
|
||||
init();
|
||||
|
|
|
|||
|
|
@ -1,107 +1,50 @@
|
|||
import { SaveSettingsStep } from 'Common/Enums';
|
||||
import { doc, createElement, elementById } from 'Common/Globals';
|
||||
|
||||
export const
|
||||
isArray = Array.isArray,
|
||||
isNonEmptyArray = array => isArray(array) && array.length,
|
||||
isFunction = v => typeof v === 'function';
|
||||
|
||||
/**
|
||||
* @param {*} value
|
||||
* @param {number=} defaultValue = 0
|
||||
* @returns {number}
|
||||
*/
|
||||
export function pInt(value, defaultValue = 0) {
|
||||
value = parseInt(value, 10);
|
||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function pString(value) {
|
||||
return null != value ? '' + value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function inFocus() {
|
||||
try {
|
||||
return doc.activeElement && doc.activeElement.matches(
|
||||
'input,textarea,.cke_editable'
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} theme
|
||||
* @returns {string}
|
||||
*/
|
||||
export const convertThemeName = theme => {
|
||||
if ('@custom' === theme.substr(-7)) {
|
||||
theme = theme.substr(0, theme.length - 7).trim();
|
||||
}
|
||||
|
||||
return theme
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} domOption
|
||||
* @param {object} item
|
||||
* @returns {void}
|
||||
*/
|
||||
export function defaultOptionsAfterRender(domItem, item) {
|
||||
if (item && undefined !== item.disabled && domItem) {
|
||||
domItem.classList.toggle('disabled', domItem.disabled = item.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} koTrigger
|
||||
* @param {mixed} context
|
||||
* @returns {mixed}
|
||||
*/
|
||||
export function settingsSaveHelperSimpleFunction(koTrigger, context) {
|
||||
return iError => {
|
||||
koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
|
||||
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
|
||||
};
|
||||
}
|
||||
import { elementById } from 'Common/Globals';
|
||||
|
||||
let __themeTimer = 0,
|
||||
__themeJson = null;
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {function=} themeTrigger = noop
|
||||
* @returns {void}
|
||||
*/
|
||||
export function changeTheme(value, themeTrigger = ()=>{}) {
|
||||
const themeLink = elementById('app-theme-link'),
|
||||
clearTimer = () => {
|
||||
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
|
||||
__themeJson = null;
|
||||
};
|
||||
export const
|
||||
isArray = Array.isArray,
|
||||
arrayLength = array => isArray(array) && array.length,
|
||||
isFunction = v => typeof v === 'function',
|
||||
pString = value => null != value ? '' + value : '',
|
||||
|
||||
let themeStyle = elementById('app-theme-style'),
|
||||
url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
|
||||
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||
|
||||
if (url) {
|
||||
url = url.toString()
|
||||
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
|
||||
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
|
||||
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
|
||||
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
|
||||
|
||||
if ('Json/' !== url.substr(-5)) {
|
||||
url += 'Json/';
|
||||
}
|
||||
pInt = (value, defaultValue = 0) => {
|
||||
value = parseInt(value, 10);
|
||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
||||
},
|
||||
|
||||
convertThemeName = theme => theme
|
||||
.replace(/@custom$/, '')
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
||||
.trim(),
|
||||
|
||||
defaultOptionsAfterRender = (domItem, item) =>
|
||||
domItem && item && undefined !== item.disabled
|
||||
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
|
||||
|
||||
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
||||
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
|
||||
/* // Without deprecated 'unescape':
|
||||
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
|
||||
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
||||
)),
|
||||
*/
|
||||
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
|
||||
changeTheme = (value, themeTrigger = ()=>0) => {
|
||||
const themeStyle = elementById('app-theme-style'),
|
||||
clearTimer = () => {
|
||||
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
|
||||
__themeJson = null;
|
||||
},
|
||||
url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
|
||||
|
||||
clearTimeout(__themeTimer);
|
||||
|
||||
|
|
@ -117,36 +60,12 @@ export function changeTheme(value, themeTrigger = ()=>{}) {
|
|||
}
|
||||
rl.fetchJSON(url, init)
|
||||
.then(data => {
|
||||
if (data && isArray(data) && 2 === data.length) {
|
||||
if (themeLink && !themeStyle) {
|
||||
themeStyle = createElement('style');
|
||||
themeStyle.id = 'app-theme-style';
|
||||
themeLink.after(themeStyle);
|
||||
themeLink.remove();
|
||||
}
|
||||
|
||||
if (themeStyle) {
|
||||
themeStyle.textContent = data[1];
|
||||
themeStyle.dataset.href = url;
|
||||
themeStyle.dataset.theme = data[0];
|
||||
}
|
||||
|
||||
if (2 === arrayLength(data)) {
|
||||
themeStyle.textContent = data[1];
|
||||
themeTrigger(SaveSettingsStep.TrueResult);
|
||||
}
|
||||
})
|
||||
.then(clearTimer, clearTimer);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
export function addObservablesTo(target, observables) {
|
||||
Object.entries(observables).forEach(([key, value]) =>
|
||||
target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) );
|
||||
}
|
||||
|
||||
export function addComputablesTo(target, computables) {
|
||||
Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn));
|
||||
}
|
||||
|
||||
export function addSubscribablesTo(target, subscribables) {
|
||||
Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn));
|
||||
}
|
||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
||||
|
|
|
|||
|
|
@ -1,341 +1,46 @@
|
|||
import { ComposeType, FolderType } from 'Common/EnumsUser';
|
||||
import { MessageFlagsCache, addRequestedMessage } from 'Common/Cache';
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility } from 'Common/Globals';
|
||||
import { plainToHtml } from 'Common/Html';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { encodeHtml } from 'Common/Html';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { createElement } from 'Common/Globals';
|
||||
import { MessageModel } from 'Model/Message';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import * as Local from 'Storage/Client';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export const
|
||||
|
||||
dropdownsDetectVisibility = (() =>
|
||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||
).debounce(50),
|
||||
|
||||
/**
|
||||
* @param {(string|number)} value
|
||||
* @param {boolean=} includeZero = true
|
||||
* @param {string} link
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPosNumeric(value, includeZero = true) {
|
||||
return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {number=} len = 100
|
||||
* @returns {string}
|
||||
*/
|
||||
function splitPlainText(text, len = 100) {
|
||||
let prefix = '',
|
||||
subText = '',
|
||||
result = text,
|
||||
spacePos = 0,
|
||||
newLinePos = 0;
|
||||
|
||||
while (result.length > len) {
|
||||
subText = result.substr(0, len);
|
||||
spacePos = subText.lastIndexOf(' ');
|
||||
newLinePos = subText.lastIndexOf('\n');
|
||||
|
||||
if (-1 !== newLinePos) {
|
||||
spacePos = newLinePos;
|
||||
}
|
||||
|
||||
if (-1 === spacePos) {
|
||||
spacePos = len;
|
||||
}
|
||||
|
||||
prefix += subText.substr(0, spacePos) + '\n';
|
||||
result = result.substr(spacePos + 1);
|
||||
download = (link, name = "") => {
|
||||
if (ThemeStore.isMobile()) {
|
||||
open(link, '_self');
|
||||
focus();
|
||||
} else {
|
||||
const oLink = createElement('a');
|
||||
oLink.href = link;
|
||||
oLink.target = '_blank';
|
||||
oLink.download = name;
|
||||
doc.body.appendChild(oLink).click();
|
||||
oLink.remove();
|
||||
}
|
||||
|
||||
return prefix + result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @returns {string}
|
||||
*/
|
||||
export function htmlToPlain(html) {
|
||||
let pos = 0,
|
||||
limit = 0,
|
||||
iP1 = 0,
|
||||
iP2 = 0,
|
||||
iP3 = 0,
|
||||
text = '';
|
||||
|
||||
const convertBlockquote = (blockquoteText) => {
|
||||
blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
|
||||
return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
|
||||
args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
|
||||
);
|
||||
};
|
||||
|
||||
const convertDivs = (...args) => {
|
||||
if (args && 1 < args.length) {
|
||||
let divText = args[1].trim();
|
||||
if (divText.length) {
|
||||
divText = divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs);
|
||||
divText = '\n' + divText.trim() + '\n';
|
||||
}
|
||||
|
||||
return divText;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const
|
||||
tpl = createElement('template'),
|
||||
convertPre = (...args) =>
|
||||
args && 1 < args.length
|
||||
? args[1]
|
||||
.toString()
|
||||
.replace(/[\n]/gm, '<br/>')
|
||||
.replace(/[\r]/gm, '')
|
||||
: '',
|
||||
fixAttibuteValue = (...args) => (args && 1 < args.length ? '' + args[1] + encodeHtml(args[2]) : ''),
|
||||
convertLinks = (...args) => (args && 1 < args.length ? args[1].trim() : '');
|
||||
|
||||
tpl.innerHTML = html
|
||||
.replace(/<p[^>]*><\/p>/gi, '')
|
||||
.replace(/<pre[^>]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre)
|
||||
.replace(/[\s]+/gm, ' ')
|
||||
.replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue)
|
||||
.replace(/<br[^>]*>/gim, '\n')
|
||||
.replace(/<\/h[\d]>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n\n')
|
||||
.replace(/<ul[^>]*>/gim, '\n')
|
||||
.replace(/<\/ul>/gi, '\n')
|
||||
.replace(/<li[^>]*>/gim, ' * ')
|
||||
.replace(/<\/li>/gi, '\n')
|
||||
.replace(/<\/td>/gi, '\n')
|
||||
.replace(/<\/tr>/gi, '\n')
|
||||
.replace(/<hr[^>]*>/gim, '\n_______________________________\n\n')
|
||||
.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs)
|
||||
.replace(/<blockquote[^>]*>/gim, '\n__bq__start__\n')
|
||||
.replace(/<\/blockquote>/gim, '\n__bq__end__\n')
|
||||
.replace(/<a [^>]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks)
|
||||
.replace(/<\/div>/gi, '\n')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/<[^>]*>/gm, '');
|
||||
|
||||
text = splitPlainText(tpl.content.textContent
|
||||
.replace(/\n[ \t]+/gm, '\n')
|
||||
.replace(/[\n]{3,}/gm, '\n\n')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/&/gi, '&')
|
||||
);
|
||||
|
||||
pos = 0;
|
||||
limit = 800;
|
||||
|
||||
while (0 < limit) {
|
||||
--limit;
|
||||
iP1 = text.indexOf('__bq__start__', pos);
|
||||
if (-1 < iP1) {
|
||||
iP2 = text.indexOf('__bq__start__', iP1 + 5);
|
||||
iP3 = text.indexOf('__bq__end__', iP1 + 5);
|
||||
|
||||
if ((-1 === iP2 || iP3 < iP2) && iP1 < iP3) {
|
||||
text = text.substr(0, iP1) + convertBlockquote(text.substring(iP1 + 13, iP3)) + text.substr(iP3 + 11);
|
||||
pos = 0;
|
||||
} else if (-1 < iP2 && iP2 < iP3) {
|
||||
pos = iP2 - 1;
|
||||
} else {
|
||||
pos = 0;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return text.replace(/__bq__start__|__bq__end__/gm, '').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} plain
|
||||
* @param {boolean} findEmailAndLinksInText = false
|
||||
* @returns {string}
|
||||
*/
|
||||
export function plainToHtml(plain) {
|
||||
plain = plain.toString().replace(/\r/g, '');
|
||||
plain = plain.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
|
||||
|
||||
let bIn = false,
|
||||
bDo = true,
|
||||
bStart = true,
|
||||
aNextText = [],
|
||||
aText = plain.split('\n');
|
||||
|
||||
do {
|
||||
bDo = false;
|
||||
aNextText = [];
|
||||
aText.forEach(sLine => {
|
||||
bStart = '>' === sLine.substr(0, 1);
|
||||
if (bStart && !bIn) {
|
||||
bDo = true;
|
||||
bIn = true;
|
||||
aNextText.push('~~~blockquote~~~');
|
||||
aNextText.push(sLine.substr(1));
|
||||
} else if (!bStart && bIn) {
|
||||
if (sLine) {
|
||||
bIn = false;
|
||||
aNextText.push('~~~/blockquote~~~');
|
||||
aNextText.push(sLine);
|
||||
} else {
|
||||
aNextText.push(sLine);
|
||||
}
|
||||
} else if (bStart && bIn) {
|
||||
aNextText.push(sLine.substr(1));
|
||||
} else {
|
||||
aNextText.push(sLine);
|
||||
}
|
||||
});
|
||||
|
||||
if (bIn) {
|
||||
bIn = false;
|
||||
aNextText.push('~~~/blockquote~~~');
|
||||
}
|
||||
|
||||
aText = aNextText;
|
||||
} while (bDo);
|
||||
|
||||
return aText.join('\n')
|
||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(/~~~blockquote~~~[\s]*/g, '<blockquote>')
|
||||
.replace(/[\s]*~~~\/blockquote~~~/g, '</blockquote>')
|
||||
.replace(/\n/g, '<br/>');
|
||||
}
|
||||
|
||||
rl.Utils = {
|
||||
htmlToPlain: htmlToPlain,
|
||||
plainToHtml: plainToHtml
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array} aSystem
|
||||
* @param {Array} aList
|
||||
* @param {Array=} aDisabled
|
||||
* @param {Array=} aHeaderLines
|
||||
* @param {Function=} fDisableCallback
|
||||
* @param {Function=} fRenameCallback
|
||||
* @param {boolean=} bSystem
|
||||
* @param {boolean=} bBuildUnvisible
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function folderListOptionsBuilder(
|
||||
aSystem,
|
||||
aList,
|
||||
aDisabled,
|
||||
aHeaderLines,
|
||||
fDisableCallback,
|
||||
fRenameCallback,
|
||||
bSystem,
|
||||
bBuildUnvisible
|
||||
) {
|
||||
let /**
|
||||
* @type {?FolderModel}
|
||||
*/
|
||||
bSep = false,
|
||||
aResult = [];
|
||||
|
||||
const sDeepPrefix = '\u00A0\u00A0\u00A0';
|
||||
|
||||
bBuildUnvisible = undefined === bBuildUnvisible ? false : !!bBuildUnvisible;
|
||||
bSystem = null == bSystem ? 0 < aSystem.length : bSystem;
|
||||
fDisableCallback = null != fDisableCallback ? fDisableCallback : null;
|
||||
fRenameCallback = null != fRenameCallback ? fRenameCallback : null;
|
||||
|
||||
if (!isArray(aDisabled)) {
|
||||
aDisabled = [];
|
||||
}
|
||||
|
||||
if (!isArray(aHeaderLines)) {
|
||||
aHeaderLines = [];
|
||||
}
|
||||
|
||||
aHeaderLines.forEach(line => {
|
||||
aResult.push({
|
||||
id: line[0],
|
||||
name: line[1],
|
||||
system: false,
|
||||
dividerbar: false,
|
||||
disabled: false
|
||||
});
|
||||
});
|
||||
|
||||
bSep = true;
|
||||
aSystem.forEach(oItem => {
|
||||
aResult.push({
|
||||
id: oItem.fullNameRaw,
|
||||
name: fRenameCallback ? fRenameCallback(oItem) : oItem.name(),
|
||||
system: true,
|
||||
dividerbar: bSep,
|
||||
disabled:
|
||||
!oItem.selectable ||
|
||||
aDisabled.includes(oItem.fullNameRaw) ||
|
||||
(fDisableCallback ? fDisableCallback(oItem) : false)
|
||||
});
|
||||
bSep = false;
|
||||
});
|
||||
|
||||
bSep = true;
|
||||
aList.forEach(oItem => {
|
||||
// if (oItem.subscribed() || !oItem.exists || bBuildUnvisible)
|
||||
if (
|
||||
(oItem.subscribed() || !oItem.exists || bBuildUnvisible) &&
|
||||
(oItem.selectable || oItem.hasSubscribedSubfolders())
|
||||
) {
|
||||
if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubscribedSubfolders()) {
|
||||
aResult.push({
|
||||
id: oItem.fullNameRaw,
|
||||
name:
|
||||
sDeepPrefix.repeat(oItem.deep + 1) +
|
||||
(fRenameCallback ? fRenameCallback(oItem) : oItem.name()),
|
||||
system: false,
|
||||
dividerbar: bSep,
|
||||
disabled:
|
||||
!oItem.selectable ||
|
||||
aDisabled.includes(oItem.fullNameRaw) ||
|
||||
(fDisableCallback ? fDisableCallback(oItem) : false)
|
||||
});
|
||||
bSep = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (oItem.subscribed() && oItem.subFolders.length) {
|
||||
aResult = aResult.concat(
|
||||
folderListOptionsBuilder(
|
||||
[],
|
||||
oItem.subFolders(),
|
||||
aDisabled,
|
||||
[],
|
||||
fDisableCallback,
|
||||
fRenameCallback,
|
||||
bSystem,
|
||||
bBuildUnvisible
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
|
||||
* @param {Object|Array} objectOrObjects
|
||||
* @returns {void}
|
||||
*/
|
||||
export function delegateRunOnDestroy(objectOrObjects) {
|
||||
objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
|
||||
obj => obj.onDestroy && obj.onDestroy()
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {function}
|
||||
*/
|
||||
export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
||||
computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||
return () => {
|
||||
const currentPage = koCurrentPage(),
|
||||
pageCount = koPageCount(),
|
||||
|
|
@ -395,13 +100,13 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
|||
if (3 === prev) {
|
||||
fAdd(2, false);
|
||||
} else if (3 < prev) {
|
||||
fAdd(Math.round((prev - 1) / 2), false, '...');
|
||||
fAdd(Math.round((prev - 1) / 2), false, '…');
|
||||
}
|
||||
|
||||
if (pageCount - 2 === next) {
|
||||
fAdd(pageCount - 1, true);
|
||||
} else if (pageCount - 2 > next) {
|
||||
fAdd(Math.round((pageCount + next) / 2), true, '...');
|
||||
fAdd(Math.round((pageCount + next) / 2), true, '…');
|
||||
}
|
||||
|
||||
// first and last
|
||||
|
|
@ -416,81 +121,253 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
|||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} mailToUrl
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function mailToHelper(mailToUrl) {
|
||||
if (
|
||||
mailToUrl &&
|
||||
'mailto:' ===
|
||||
mailToUrl
|
||||
.toString()
|
||||
.substr(0, 7)
|
||||
.toLowerCase()
|
||||
) {
|
||||
mailToUrl = mailToUrl.toString().substr(7);
|
||||
mailToHelper = mailToUrl => {
|
||||
if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) {
|
||||
mailToUrl = mailToUrl.slice(7).split('?');
|
||||
|
||||
let to = [],
|
||||
cc = null,
|
||||
bcc = null,
|
||||
params = {};
|
||||
|
||||
const email = mailToUrl.replace(/\?.+$/, ''),
|
||||
query = mailToUrl.replace(/^[^?]*\?/, '');
|
||||
|
||||
query.split('&').forEach(temp => {
|
||||
temp = temp.split('=');
|
||||
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
|
||||
});
|
||||
|
||||
if (undefined !== params.to) {
|
||||
to = EmailModel.parseEmailLine(decodeURIComponent(email + ',' + params.to));
|
||||
to = Object.values(
|
||||
to.reduce((result, value) => {
|
||||
if (value) {
|
||||
if (result[value.email]) {
|
||||
if (!result[value.email].name) {
|
||||
result[value.email] = value;
|
||||
}
|
||||
} else {
|
||||
result[value.email] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, {})
|
||||
);
|
||||
} else {
|
||||
to = EmailModel.parseEmailLine(email);
|
||||
}
|
||||
|
||||
if (undefined !== params.cc) {
|
||||
cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc));
|
||||
}
|
||||
|
||||
if (undefined !== params.bcc) {
|
||||
bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc));
|
||||
}
|
||||
const
|
||||
email = mailToUrl[0],
|
||||
params = new URLSearchParams(mailToUrl[1]),
|
||||
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
|
||||
|
||||
showMessageComposer([
|
||||
ComposeType.Empty,
|
||||
null,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
null == params.subject ? null : decodeURIComponent(params.subject),
|
||||
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
|
||||
params.get('to')
|
||||
? Object.values(
|
||||
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
|
||||
if (value) {
|
||||
if (result[value.email]) {
|
||||
if (!result[value.email].name) {
|
||||
result[value.email] = value;
|
||||
}
|
||||
} else {
|
||||
result[value.email] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, {})
|
||||
)
|
||||
: EmailModel.parseEmailLine(email),
|
||||
toEmailModel(params.get('cc')),
|
||||
toEmailModel(params.get('bcc')),
|
||||
params.get('subject'),
|
||||
plainToHtml(params.get('body') || '')
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
export function showMessageComposer(params = [])
|
||||
showMessageComposer = (params = []) =>
|
||||
{
|
||||
rl.app.showMessageComposer(params);
|
||||
}
|
||||
},
|
||||
|
||||
initFullscreen = (el, fn) =>
|
||||
{
|
||||
let event = 'fullscreenchange';
|
||||
if (!el.requestFullscreen && el.webkitRequestFullscreen) {
|
||||
el.requestFullscreen = el.webkitRequestFullscreen;
|
||||
event = 'webkit'+event;
|
||||
}
|
||||
if (el.requestFullscreen) {
|
||||
el.addEventListener(event, fn);
|
||||
return el;
|
||||
}
|
||||
},
|
||||
|
||||
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
||||
{
|
||||
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
||||
target.removeAttribute('style');
|
||||
source.removeAttribute('style');
|
||||
}
|
||||
// source.classList.toggle('resizable', mode);
|
||||
if (mode) {
|
||||
const length = Local.get(sClientSideKeyName+mode);
|
||||
if (!source.layoutResizer) {
|
||||
const resizer = createElement('div', {'class':'resizer'}),
|
||||
size = {},
|
||||
store = () => {
|
||||
if ('Width' == resizer.mode) {
|
||||
target.style.left = source.offsetWidth + 'px';
|
||||
Local.set(resizer.key+resizer.mode, source.offsetWidth);
|
||||
} else {
|
||||
target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
|
||||
Local.set(resizer.key+resizer.mode, source.offsetHeight);
|
||||
}
|
||||
},
|
||||
cssint = s => {
|
||||
let value = getComputedStyle(source, null)[s].replace('px', '');
|
||||
if (value.includes('%')) {
|
||||
value = source.parentElement['offset'+resizer.mode]
|
||||
* value.replace('%', '') / 100;
|
||||
}
|
||||
return parseFloat(value);
|
||||
};
|
||||
source.layoutResizer = resizer;
|
||||
source.append(resizer);
|
||||
resizer.addEventListener('mousedown', {
|
||||
handleEvent: function(e) {
|
||||
if ('mousedown' == e.type) {
|
||||
const lmode = resizer.mode.toLowerCase();
|
||||
e.preventDefault();
|
||||
size.pos = ('width' == lmode) ? e.pageX : e.pageY;
|
||||
size.min = cssint('min-'+lmode);
|
||||
size.max = cssint('max-'+lmode);
|
||||
size.org = cssint(lmode);
|
||||
addEventListener('mousemove', this);
|
||||
addEventListener('mouseup', this);
|
||||
} else if ('mousemove' == e.type) {
|
||||
const lmode = resizer.mode.toLowerCase(),
|
||||
length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
|
||||
if (length >= size.min && length <= size.max ) {
|
||||
source.style[lmode] = length + 'px';
|
||||
source.observer || store();
|
||||
}
|
||||
} else if ('mouseup' == e.type) {
|
||||
removeEventListener('mousemove', this);
|
||||
removeEventListener('mouseup', this);
|
||||
}
|
||||
}
|
||||
});
|
||||
source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
|
||||
}
|
||||
source.layoutResizer.mode = mode;
|
||||
source.layoutResizer.key = sClientSideKeyName;
|
||||
source.observer && source.observer.observe(source, { box: 'border-box' });
|
||||
if (length) {
|
||||
source.style[mode] = length + 'px';
|
||||
}
|
||||
} else {
|
||||
source.observer && source.observer.disconnect();
|
||||
}
|
||||
},
|
||||
|
||||
populateMessageBody = (oMessage, preload) => {
|
||||
if (oMessage) {
|
||||
preload || MessageUserStore.hideMessageBodies();
|
||||
preload || MessageUserStore.loading(true);
|
||||
Remote.message((iError, oData/*, bCached*/) => {
|
||||
if (iError) {
|
||||
if (Notification.RequestAborted !== iError && !preload) {
|
||||
MessageUserStore.message(null);
|
||||
MessageUserStore.error(getNotification(iError));
|
||||
}
|
||||
} else {
|
||||
oMessage = preload ? oMessage : null;
|
||||
let
|
||||
isNew = false,
|
||||
json = oData && oData.Result,
|
||||
message = oMessage || MessageUserStore.message();
|
||||
|
||||
if (
|
||||
json &&
|
||||
MessageModel.validJson(json) &&
|
||||
message &&
|
||||
message.folder === json.Folder
|
||||
) {
|
||||
const threads = message.threads(),
|
||||
messagesDom = MessageUserStore.bodiesDom();
|
||||
if (!oMessage && message.uid != json.Uid && threads.includes(json.Uid)) {
|
||||
message = MessageModel.reviveFromJson(json);
|
||||
if (message) {
|
||||
message.threads(threads);
|
||||
MessageFlagsCache.initMessage(message);
|
||||
|
||||
// Set clone
|
||||
MessageUserStore.message(MessageModel.fromMessageListItem(message));
|
||||
message = MessageUserStore.message();
|
||||
|
||||
isNew = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (message && message.uid == json.Uid) {
|
||||
oMessage || MessageUserStore.error('');
|
||||
/*
|
||||
if (bCached) {
|
||||
delete json.Flags;
|
||||
}
|
||||
*/
|
||||
isNew || message.revivePropertiesFromJson(json);
|
||||
addRequestedMessage(message.folder, message.uid);
|
||||
if (messagesDom) {
|
||||
let id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''),
|
||||
body = elementById(id);
|
||||
if (body) {
|
||||
message.body = body;
|
||||
message.isHtml(body.classList.contains('html'));
|
||||
message.hasImages(body.rlHasImages);
|
||||
} else {
|
||||
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
||||
+ (message.pgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (message.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
+ '">'
|
||||
+ '</div>');
|
||||
message.body = body;
|
||||
if (!SettingsUserStore.viewHTML() || !message.viewHtml()) {
|
||||
message.viewPlain();
|
||||
}
|
||||
|
||||
MessageUserStore.purgeMessageBodyCache();
|
||||
}
|
||||
|
||||
messagesDom.append(body);
|
||||
|
||||
if (!oMessage) {
|
||||
MessageUserStore.activeDom(message.body);
|
||||
MessageUserStore.hideMessageBodies();
|
||||
message.body.hidden = false;
|
||||
}
|
||||
oMessage && message.viewPopupMessage();
|
||||
}
|
||||
|
||||
MessageFlagsCache.initMessage(message);
|
||||
if (message.isUnseen()) {
|
||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||
() => MessagelistUserStore.setAction(message.folder, MessageSetAction.SetSeen, [message]),
|
||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||
);
|
||||
}
|
||||
|
||||
if (message && isNew) {
|
||||
let selectedMessage = MessagelistUserStore.selectedMessage();
|
||||
if (
|
||||
selectedMessage &&
|
||||
(message.folder !== selectedMessage.folder || message.uid != selectedMessage.uid)
|
||||
) {
|
||||
MessagelistUserStore.selectedMessage(null);
|
||||
if (1 === MessagelistUserStore.length) {
|
||||
MessagelistUserStore.focusedMessage(null);
|
||||
}
|
||||
} else if (!selectedMessage) {
|
||||
selectedMessage = MessagelistUserStore.find(
|
||||
subMessage =>
|
||||
subMessage &&
|
||||
subMessage.folder === message.folder &&
|
||||
subMessage.uid == message.uid
|
||||
);
|
||||
|
||||
if (selectedMessage) {
|
||||
MessagelistUserStore.selectedMessage(selectedMessage);
|
||||
MessagelistUserStore.focusedMessage(selectedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
preload || MessageUserStore.loading(false);
|
||||
}, oMessage.folder, oMessage.uid);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue