mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-08 22:18:28 +03:00
Merge branch 'the-djmaze:master' into master
This commit is contained in:
commit
00a507ff97
246 changed files with 4471 additions and 9935 deletions
|
|
@ -316,5 +316,5 @@ dev_email = ""
|
|||
dev_password = ""
|
||||
|
||||
[version]
|
||||
current = "2.26.1"
|
||||
current = "2.26.4"
|
||||
saved = "Sun, 18 Dec 2022 22:10:48 +0000"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
# Header set Strict-Transport-Security "max-age=31536000"
|
||||
Header set imagetoolbar "no"
|
||||
# Header set X-Content-Type-Options "nosniff"
|
||||
# Header set X-Frame-Options "DENY"
|
||||
# Header set X-XSS-Protection "1; mode=block"
|
||||
Header set Service-Worker-Allowed "/"
|
||||
|
||||
|
|
|
|||
101
CHANGELOG.md
101
CHANGELOG.md
|
|
@ -1,3 +1,104 @@
|
|||
## 2.26.4 – 2023-02-24
|
||||
|
||||
## Added
|
||||
- Add CSP frame-ancestors for
|
||||
[#537](https://github.com/the-djmaze/snappymail/pull/537)
|
||||
|
||||
## Changed
|
||||
- Reduce/simplify CSS footprint
|
||||
- Use the System/Browser font by default by @HeySora
|
||||
[#988](https://github.com/the-djmaze/snappymail/pull/988)
|
||||
- Make layout fully responsive using matchMedia('(max-width: 799px)')
|
||||
- Move brotli and gzip compress option to application.ini
|
||||
- After page refreshes in background whilst editing a draft, the space bar stops working
|
||||
[#860](https://github.com/the-djmaze/snappymail/pull/860)
|
||||
- Updated Portuguese by @ner00
|
||||
[#984](https://github.com/the-djmaze/snappymail/pull/984)
|
||||
- Updated French by @hguilbert
|
||||
[#985](https://github.com/the-djmaze/snappymail/pull/985)
|
||||
|
||||
## Fixed
|
||||
- unset border-box for message body
|
||||
[#990](https://github.com/the-djmaze/snappymail/pull/990)
|
||||
- Unread email count badge shows -1
|
||||
[#989](https://github.com/the-djmaze/snappymail/pull/989)
|
||||
- unicode mailto: addresses not decoded
|
||||
- Unicode email/url address matching
|
||||
[#955](https://github.com/the-djmaze/snappymail/pull/955)
|
||||
- Invalid Token if run in IFrame
|
||||
[#537](https://github.com/the-djmaze/snappymail/pull/537)
|
||||
|
||||
### Removed
|
||||
- rl.initData
|
||||
|
||||
|
||||
## 2.26.3 – 2023-02-21
|
||||
|
||||
## Added
|
||||
- Translate "Are you sure you want to exit?"
|
||||
- stderr as Logger
|
||||
|
||||
## Changed
|
||||
- Moved rl.fetch and rl.fetchJSON to boot.js so that AppData can be fetched as JSON
|
||||
- Many AppData properties to JavaScript camelCase
|
||||
- Cleanup Identity handling
|
||||
- Merge GnuPG and OpenPGP.js passphrases
|
||||
|
||||
## Fixed
|
||||
- Notification enum conflicts with window.Notification
|
||||
- language selector didn't show current language in green
|
||||
- Threads indicator got lost with new message cache handling
|
||||
- messagesBodiesDom never cached previous messages
|
||||
- Nextcloud File picker doesn't insert multiple links
|
||||
[#981](https://github.com/the-djmaze/snappymail/pull/981)
|
||||
- Call to protected method RainLoop\Enumerations\UploadError::getUserMessage()
|
||||
[#982](https://github.com/the-djmaze/snappymail/pull/982)
|
||||
- Issue with the top logo and text of a certain e-mail
|
||||
[#953](https://github.com/the-djmaze/snappymail/pull/953)
|
||||
- Facebook logo missing from emails sent out by it
|
||||
[#954](https://github.com/the-djmaze/snappymail/pull/954)
|
||||
|
||||
### Removed
|
||||
- Unused Squire code
|
||||
|
||||
|
||||
|
||||
## 2.26.2 – 2023-02-17
|
||||
|
||||
## Added
|
||||
- Remove CSS white-space from messages to prevent annoying side scrolling
|
||||
- Show error when trying to send empty message
|
||||
[#974](https://github.com/the-djmaze/snappymail/pull/974)
|
||||
- max_sys_loadavg as setting
|
||||
[#971](https://github.com/the-djmaze/snappymail/pull/971)
|
||||
|
||||
## Changed
|
||||
- Speedup and improved cleanHtml() and cleanCSS() handling
|
||||
- Better handling of upload .eml files to a mailbox
|
||||
- Rename Folder Hash to ETag as it is the more obvious name
|
||||
- Moved AllowDraftAutosave option from Settings -> Security to Settings -> General -> Compose
|
||||
- SmtpClient->Connect() use $oSettings->Ehlo
|
||||
- Improved handling of `<style>` elements in messages
|
||||
- French updated by @hguilbert
|
||||
|
||||
## Fixed
|
||||
- Remove SVG elements properly from messages
|
||||
[#972](https://github.com/the-djmaze/snappymail/pull/972)
|
||||
- Sending large HTML messages slow/fail
|
||||
[#962](https://github.com/the-djmaze/snappymail/pull/962)
|
||||
- Nextcloud login failed
|
||||
[#969](https://github.com/the-djmaze/snappymail/pull/969)
|
||||
- DoMessageList() hash check was incorrect
|
||||
- "Move to folder" button overlays folder sidebar on mobile
|
||||
[#961](https://github.com/the-djmaze/snappymail/pull/961)
|
||||
- Spanish translation SPAM and NOT SPAM strings are reversed
|
||||
[#964](https://github.com/the-djmaze/snappymail/pull/964)
|
||||
- Can't open Thunderbird PGP keys from decrypted message
|
||||
[#958](https://github.com/the-djmaze/snappymail/pull/958)
|
||||
- Can't close Sieve dialog
|
||||
[#960](https://github.com/the-djmaze/snappymail/pull/960)
|
||||
|
||||
|
||||
## 2.26.1 – 2023-02-14
|
||||
|
||||
## Added
|
||||
|
|
|
|||
40
README.md
40
README.md
|
|
@ -141,28 +141,28 @@ RainLoop 1.17 vs SnappyMail
|
|||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.170.153 | 81.159 |
|
||||
|app.js |4.207.787 | 414.949 |
|
||||
|boot.js | 868.735 | 2.711 |
|
||||
|libs.js | 658.812 | 192.392 |
|
||||
|sieve.js | 0 | 85.597 |
|
||||
|admin.js |2.170.153 | 78.971 |
|
||||
|app.js |4.207.787 | 409.340 |
|
||||
|boot.js | 868.735 | 4.142 |
|
||||
|libs.js | 658.812 | 185.881 |
|
||||
|sieve.js | 0 | 85.599 |
|
||||
|polyfills.js | 334.608 | 0 |
|
||||
|serviceworker.js | 0 | 285 |
|
||||
|TOTAL |8.240.095 | 777.093 |
|
||||
|TOTAL |8.240.095 | 764.218 |
|
||||
|
||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|admin.min.js | 256.831 | 40.198 | 73.606 | 13.281 | 60.877 | 11.916 |
|
||||
|app.min.js | 515.367 | 190.317 |139.456 | 63.879 |110.485 | 54.792 |
|
||||
|boot.min.js | 84.659 | 1.600 | 26.998 | 940 | 23.643 | 767 |
|
||||
|libs.min.js | 584.772 | 91.434 |180.901 | 33.893 |155.182 | 30.383 |
|
||||
|sieve.min.js | 0 | 41.566 | 0 | 10.427 | 0 | 9.413 |
|
||||
|admin.min.js | 256.831 | 38.936 | 73.606 | 13.036 | 60.877 | 11.694 |
|
||||
|app.min.js | 515.367 | 186.883 |139.456 | 63.102 |110.485 | 54.200 |
|
||||
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|
||||
|libs.min.js | 584.772 | 90.445 |180.901 | 33.592 |155.182 | 30.108 |
|
||||
|sieve.min.js | 0 | 41.525 | 0 | 10.435 | 0 | 9.401 |
|
||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||
|TOTAL user |1.217.635 | 283.351 |358.761 | 98.712 |299.485 | 85.942 |
|
||||
|TOTAL user+sieve |1.217.635 | 324.917 |358.761 |109.139 |299.485 | 95.355 |
|
||||
|TOTAL admin | 959.099 | 133.232 |292.911 | 48.114 |249.877 | 43.066 |
|
||||
|TOTAL user |1.217.635 | 279.412 |358.761 | 97.896 |299.485 | 85.311 |
|
||||
|TOTAL user+sieve |1.217.635 | 320.937 |358.761 |108.331 |299.485 | 94.712 |
|
||||
|TOTAL admin | 959.099 | 131.465 |292.911 | 47.830 |249.877 | 42.805 |
|
||||
|
||||
For a user its around 70% smaller and faster than traditional RainLoop.
|
||||
For a user it is around 72% smaller and faster than traditional RainLoop.
|
||||
|
||||
### CSS changes
|
||||
|
||||
|
|
@ -189,12 +189,12 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|
|||
|
||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.331 | 82.134 | 46.946 | 17.067 | 14.688 |
|
||||
|app.min.css | 274.947 | 66.337 | 39.647 | 15.095 | 13.285 |
|
||||
|app.css | 340.331 | 81.526 | 46.946 | 16.960 | 14.620 |
|
||||
|app.min.css | 274.947 | 65.398 | 39.647 | 15.032 | 13.119 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 30.554 | | 6.984 | 6.085 |
|
||||
|admin.min.css | | 24.752 | | 6.339 | 5.606 |
|
||||
|admin.css | | 30.296 | | 6.939 | 6.066 |
|
||||
|admin.min.css | | 24.420 | | 6.272 | 5.531 |
|
||||
|
||||
### PGP
|
||||
RainLoop uses the old OpenPGP.js v2
|
||||
|
|
@ -208,7 +208,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|
|||
|
||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
||||
|openpgp.min.js | 330.742 | 539.988 |102.388 | 167.371 | 84.241 | 137.413 |
|
||||
|openpgp.min.js | 330.742 | 541.176 |102.388 | 168.266 | 84.241 | 138.278 |
|
||||
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
||||
|
||||
|
||||
|
|
|
|||
10
_include.php
10
_include.php
|
|
@ -4,16 +4,6 @@
|
|||
|
||||
//header('Strict-Transport-Security: max-age=31536000');
|
||||
|
||||
/**
|
||||
* Uncomment to use gzip compressed output
|
||||
*/
|
||||
//define('USE_GZIP', 1);
|
||||
|
||||
/**
|
||||
* Uncomment to use brotli compressed output
|
||||
*/
|
||||
//define('USE_BROTLI', 1);
|
||||
|
||||
/**
|
||||
* Uncomment to enable multiple domain installation.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -160,11 +160,6 @@ $tar->addFromString('index.php', $index);
|
|||
$zip->addFile('README.md');
|
||||
$tar->addFile('README.md');
|
||||
|
||||
$data = file_get_contents('dev/serviceworker.js');
|
||||
//$data = file_get_contents('snappymail/v/0.0.0/static/js/min/serviceworker.min.js');
|
||||
$zip->addFromString('serviceworker.js', $data);
|
||||
$tar->addFromString('serviceworker.js', $data);
|
||||
|
||||
$zip->close();
|
||||
|
||||
$tar->compress(Phar::GZ);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export class AdminApp extends AbstractApp {
|
|||
rl.route.root();
|
||||
setTimeout(() => location.href = '/', 1);
|
||||
} else if (SettingsGet('Auth')) {
|
||||
this.weakPassword(SettingsGet('WeakPassword'));
|
||||
this.weakPassword(SettingsGet('weakPassword'));
|
||||
startScreens([SettingsAdminScreen]);
|
||||
} else {
|
||||
startScreens([LoginAdminScreen]);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'External/User/ko';
|
||||
|
||||
import { SMAudio } from 'Common/Audio';
|
||||
import { isArray, pString, pInt } from 'Common/Utils';
|
||||
import { isArray, pInt } from 'Common/Utils';
|
||||
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
|
||||
|
||||
import {
|
||||
|
|
@ -156,22 +156,11 @@ export class AppUser extends AbstractApp {
|
|||
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
|
||||
: []
|
||||
);
|
||||
AccountUserStore.unshift(new AccountModel(SettingsGet('MainEmail'), '', false));
|
||||
AccountUserStore.unshift(new AccountModel(SettingsGet('mainEmail'), '', false));
|
||||
|
||||
items = oData.Result.Identities;
|
||||
IdentityUserStore(isArray(items)
|
||||
? items.map(identityData => {
|
||||
const identity = new IdentityModel(
|
||||
pString(identityData.Id),
|
||||
pString(identityData.Email)
|
||||
);
|
||||
identity.name(pString(identityData.Name));
|
||||
identity.replyTo(pString(identityData.ReplyTo));
|
||||
identity.bcc(pString(identityData.Bcc));
|
||||
identity.signature(pString(identityData.Signature));
|
||||
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
|
||||
return identity;
|
||||
})
|
||||
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
|
||||
: []
|
||||
);
|
||||
}
|
||||
|
|
@ -193,18 +182,17 @@ export class AppUser extends AbstractApp {
|
|||
bootstart() {
|
||||
super.bootstart();
|
||||
|
||||
addEventListener('resize', () => leftPanelDisabled(ThemeStore.isMobile() || 1000 > innerWidth));
|
||||
addEventListener('beforeunload', event => {
|
||||
if (arePopupsVisible() || (ThemeStore.isMobile() && MessageUserStore.message())) {
|
||||
if (arePopupsVisible() || (!SettingsUserStore.layout() && MessageUserStore.message())) {
|
||||
event.preventDefault();
|
||||
return event.returnValue = "Are you sure you want to exit?";
|
||||
return event.returnValue = i18n('POPUPS_ASK/EXIT_ARE_YOU_SURE');
|
||||
}
|
||||
}, {capture: true});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
ThemeStore.populate();
|
||||
LanguageStore.language(SettingsGet('Language'));
|
||||
LanguageStore.language(SettingsGet('language'));
|
||||
changeTheme(SettingsGet('Theme'));
|
||||
this.start();
|
||||
}
|
||||
|
|
@ -263,18 +251,15 @@ export class AppUser extends AbstractApp {
|
|||
|
||||
PgpUserStore.init();
|
||||
|
||||
// When auto-login is active
|
||||
try {
|
||||
navigator.registerProtocolHandler?.(
|
||||
'mailto',
|
||||
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
|
||||
(SettingsGet('Title') || 'SnappyMail')
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
||||
|
||||
// When auto-login is active
|
||||
navigator.registerProtocolHandler?.(
|
||||
'mailto',
|
||||
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
|
||||
(SettingsGet('title') || 'SnappyMail')
|
||||
);
|
||||
|
||||
setTimeout(() => mailToHelper(SettingsGet('MailToEmail')), 500);
|
||||
} else {
|
||||
this.logout();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import { MessageSetAction } from 'Common/EnumsUser';
|
||||
import { isArray } from 'Common/Utils';
|
||||
|
||||
let FOLDERS_CACHE = {},
|
||||
FOLDERS_NAME_CACHE = {},
|
||||
MESSAGE_FLAGS_CACHE = {},
|
||||
let FOLDERS_CACHE = new Map,
|
||||
FOLDERS_HASH_MAP = new Map,
|
||||
inboxFolderName = 'INBOX';
|
||||
|
||||
export const
|
||||
|
|
@ -11,18 +7,10 @@ export const
|
|||
* @returns {void}
|
||||
*/
|
||||
clearCache = () => {
|
||||
FOLDERS_CACHE = {};
|
||||
FOLDERS_NAME_CACHE = {};
|
||||
MESSAGE_FLAGS_CACHE = {};
|
||||
FOLDERS_CACHE.clear();
|
||||
FOLDERS_HASH_MAP.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @returns {string}
|
||||
*/
|
||||
getMessageKey = (folderFullName, uid) => folderFullName + '#' + uid,
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
|
|
@ -34,143 +22,35 @@ export const
|
|||
setFolderInboxName = name => inboxFolderName = name,
|
||||
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @param {string} fullNameHash
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderFullName = folderHash => (folderHash && FOLDERS_NAME_CACHE[folderHash]) || '',
|
||||
getFolderFromHashMap = fullNameHash => getFolderFromCacheList(FOLDERS_HASH_MAP.get(fullNameHash)),
|
||||
|
||||
/**
|
||||
* @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;
|
||||
folder.etag = '';
|
||||
FOLDERS_CACHE.set(folder.fullName, folder);
|
||||
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} folderHash
|
||||
* @param {string} folderETag
|
||||
*/
|
||||
setFolderHash = (folderFullName, folderHash) =>
|
||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
|
||||
setFolderETag = (folderFullName, folderETag) =>
|
||||
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {?FolderModel}
|
||||
*/
|
||||
getFolderFromCacheList = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] || null,
|
||||
FOLDERS_CACHE.get(folderFullName),
|
||||
|
||||
/**
|
||||
* @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]?.[uid]?.includes(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @returns {?Array}
|
||||
*/
|
||||
static getFor(folderFullName, uid) {
|
||||
return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @param {Array} flagsCache
|
||||
*/
|
||||
static setFor(folderFullName, uid, flags) {
|
||||
if (isArray(flags)) {
|
||||
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||
}
|
||||
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
*/
|
||||
static clearFolder(folderFullName) {
|
||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(MessageModel|null)} message
|
||||
*/
|
||||
static initMessage(message) {
|
||||
if (message) {
|
||||
const uid = message.uid,
|
||||
flags = this.getFor(message.folder, uid),
|
||||
thread = message.threads();
|
||||
|
||||
isArray(flags) && message.flags(flags);
|
||||
|
||||
if (thread.length) {
|
||||
message.hasUnseenSubMessage(!!thread.find(iSubUid =>
|
||||
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
|
||||
));
|
||||
message.hasFlaggedSubMessage(!!thread.find(iSubUid =>
|
||||
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(MessageModel|null)} message
|
||||
*/
|
||||
static store(message) {
|
||||
message && this.setFor(message.folder, message.uid, message.flags());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
* @param {number} setAction
|
||||
*/
|
||||
static storeBySetAction(folder, uid, setAction) {
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ SaveSettingStatus = {
|
|||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
Notification = {
|
||||
Notifications = {
|
||||
RequestError: 1,
|
||||
RequestAborted: 2,
|
||||
RequestTimeout: 3,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const EditorDefaultType = {
|
|||
* @enum {number}
|
||||
*/
|
||||
export const Layout = {
|
||||
NoPreview: 0,
|
||||
// NoPreview: 0,
|
||||
SidePreview: 1,
|
||||
BottomPreview: 2
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
import {
|
||||
MessageFlagsCache,
|
||||
setFolderHash,
|
||||
setFolderETag,
|
||||
getFolderInboxName,
|
||||
getFolderFromCacheList
|
||||
} from 'Common/Cache';
|
||||
|
|
@ -9,8 +8,6 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
|||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import { Settings } from 'Common/Globals';
|
||||
import { serverRequest } from 'Common/Links';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
|
|
@ -114,8 +111,8 @@ folderInformation = (folder, list) => {
|
|||
|
||||
if (arrayLength(list)) {
|
||||
list.forEach(messageListItem => {
|
||||
MessageFlagsCache.getFor(folder, messageListItem.uid) || uids.push(messageListItem.uid);
|
||||
messageListItem.threads.forEach(uid => MessageFlagsCache.getFor(folder, uid) || uids.push(uid));
|
||||
uids.push(messageListItem.uid);
|
||||
messageListItem.threads.forEach(uid => uids.push(uid));
|
||||
});
|
||||
count = uids.length;
|
||||
}
|
||||
|
|
@ -124,31 +121,21 @@ folderInformation = (folder, list) => {
|
|||
Remote.request('FolderInformation', (iError, data) => {
|
||||
if (!iError && data.Result) {
|
||||
const result = data.Result,
|
||||
folderFromCache = getFolderFromCacheList(result.folder);
|
||||
folderFromCache = getFolderFromCacheList(result.name);
|
||||
if (folderFromCache) {
|
||||
const oldHash = folderFromCache.hash,
|
||||
const oldHash = folderFromCache.etag,
|
||||
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
|
||||
|
||||
// folderFromCache.revivePropertiesFromJson(result);
|
||||
folderFromCache.expires = Date.now();
|
||||
folderFromCache.uidNext = result.uidNext;
|
||||
folderFromCache.hash = result.hash;
|
||||
folderFromCache.etag = result.etag;
|
||||
folderFromCache.totalEmails(result.totalEmails);
|
||||
folderFromCache.unreadEmails(result.unreadEmails);
|
||||
|
||||
unreadCountChange && MessageFlagsCache.clearFolder(folderFromCache.fullName);
|
||||
|
||||
if (result.messagesFlags.length) {
|
||||
result.messagesFlags.forEach(message =>
|
||||
MessageFlagsCache.setFor(folderFromCache.fullName, message.uid.toString(), message.flags)
|
||||
);
|
||||
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
}
|
||||
|
||||
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.newMessages);
|
||||
|
||||
if (!oldHash || unreadCountChange || result.hash !== oldHash) {
|
||||
if (!oldHash || unreadCountChange || result.etag !== oldHash) {
|
||||
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
/*
|
||||
|
|
@ -165,8 +152,6 @@ folderInformation = (folder, list) => {
|
|||
flagsUids: uids,
|
||||
uidNext: getFolderFromCacheList(folder)?.uidNext || 0 // Used to check for new messages
|
||||
});
|
||||
} else if (SettingsUserStore.useThreads()) {
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -181,21 +166,18 @@ folderInformationMultiply = (boot = false) => {
|
|||
if (!iError && arrayLength(oData.Result)) {
|
||||
const utc = Date.now();
|
||||
oData.Result.forEach(item => {
|
||||
const folder = getFolderFromCacheList(item.folder);
|
||||
|
||||
const folder = getFolderFromCacheList(item.name);
|
||||
if (folder) {
|
||||
const oldHash = folder.hash,
|
||||
const oldHash = folder.etag,
|
||||
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
|
||||
|
||||
// folder.revivePropertiesFromJson(item);
|
||||
folder.expires = utc;
|
||||
folder.hash = item.hash;
|
||||
folder.etag = item.etag;
|
||||
folder.totalEmails(item.totalEmails);
|
||||
folder.unreadEmails(item.unreadEmails);
|
||||
|
||||
unreadCountChange && MessageFlagsCache.clearFolder(folder.fullName);
|
||||
|
||||
if (!oldHash || item.hash !== oldHash) {
|
||||
if (!oldHash || item.etag !== oldHash) {
|
||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
}
|
||||
|
|
@ -217,13 +199,13 @@ folderInformationMultiply = (boot = false) => {
|
|||
|
||||
moveOrDeleteResponseHelper = (iError, oData) => {
|
||||
if (iError) {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||
alert(getNotification(iError));
|
||||
} else if (FolderUserStore.currentFolder()) {
|
||||
if (2 === arrayLength(oData.Result)) {
|
||||
setFolderHash(oData.Result[0], oData.Result[1]);
|
||||
setFolderETag(oData.Result[0], oData.Result[1]);
|
||||
} else {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||
}
|
||||
MessagelistUserStore.reload(!MessagelistUserStore.length);
|
||||
}
|
||||
|
|
@ -286,28 +268,20 @@ moveMessagesToFolder = (sFromFolderFullName, oUids, sToFolderFullName, bCopy) =>
|
|||
},
|
||||
|
||||
dropFilesInFolder = (sFolderFullName, files) => {
|
||||
let count = 0,
|
||||
fn = () => 0 == --count
|
||||
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
||||
&& MessagelistUserStore.reload(true, true);
|
||||
let count = files.length;
|
||||
for (const file of files) {
|
||||
if ('message/rfc822' === file.type) {
|
||||
++count;
|
||||
let data = new FormData;
|
||||
data.append('folder', sFolderFullName);
|
||||
data.append('appendFile', file);
|
||||
data.XToken = Settings.app('token');
|
||||
fetch(serverRequest('Append'), {
|
||||
method: 'POST',
|
||||
mode: 'same-origin',
|
||||
cache: 'no-cache',
|
||||
redirect: 'error',
|
||||
referrerPolicy: 'no-referrer',
|
||||
credentials: 'same-origin',
|
||||
body: data
|
||||
})
|
||||
.then(fn)
|
||||
.catch(fn);
|
||||
Remote.request('FolderAppend', (iError, data)=>{
|
||||
iError && console.error(data.ErrorMessage);
|
||||
0 == --count
|
||||
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
||||
&& MessagelistUserStore.reload(true, true);
|
||||
}, data);
|
||||
} else {
|
||||
--count;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import { doc, elementById, $htmlCL } from 'Common/Globals';
|
||||
import { doc, appEl, $htmlCL } from 'Common/Globals';
|
||||
|
||||
// Fullscreen must be on app, else other popups fail
|
||||
export const
|
||||
app = elementById('rl-app'),
|
||||
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === app,
|
||||
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
|
||||
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
||||
isFullscreen = ko.observable(false),
|
||||
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : app.requestFullscreen();
|
||||
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
|
||||
|
||||
if (app) {
|
||||
if (appEl) {
|
||||
let event = 'fullscreenchange';
|
||||
if (!app.requestFullscreen && app.webkitRequestFullscreen) {
|
||||
app.requestFullscreen = app.webkitRequestFullscreen;
|
||||
if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
|
||||
appEl.requestFullscreen = appEl.webkitRequestFullscreen;
|
||||
event = 'webkit'+event;
|
||||
}
|
||||
if (app.requestFullscreen) {
|
||||
if (appEl.requestFullscreen) {
|
||||
doc.addEventListener(event, () => {
|
||||
isFullscreen(appFullscreen());
|
||||
$htmlCL.toggle('rl-fullscreen', appFullscreen());
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export const
|
|||
|
||||
elementById = id => doc.getElementById(id),
|
||||
|
||||
appEl = elementById('rl-app'),
|
||||
|
||||
Settings = rl.settings,
|
||||
SettingsGet = Settings.get,
|
||||
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
||||
|
|
@ -31,6 +33,11 @@ export const
|
|||
new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
|
||||
),
|
||||
|
||||
stopEvent = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
|
||||
|
||||
addShortcut = (...args) => shortcuts.add(...args),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { forEachObjectEntry, pInt } from 'Common/Utils';
|
|||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
const
|
||||
tpl = createElement('template'),
|
||||
tmpl = createElement('template'),
|
||||
htmlre = /[&<>"']/g,
|
||||
httpre = /^(https?:)?\/\//i,
|
||||
htmlmap = {
|
||||
|
|
@ -14,10 +14,18 @@ const
|
|||
"'": '''
|
||||
},
|
||||
|
||||
disallowedTags = [
|
||||
'svg','script','title','link','base','meta',
|
||||
'input','output','select','button','textarea',
|
||||
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
|
||||
// not supported by <template> element
|
||||
// ,'html','head','body'
|
||||
].join(','),
|
||||
|
||||
blockquoteSwitcher = () => {
|
||||
SettingsUserStore.collapseBlockquotes() &&
|
||||
// tpl.content.querySelectorAll('blockquote').forEach(node => {
|
||||
[...tpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
|
||||
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
|
||||
[...tmpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
|
||||
const el = createElement('details', {class:'sm-bq-switcher'});
|
||||
el.innerHTML = '<summary>•••</summary>';
|
||||
node.replaceWith(el);
|
||||
|
|
@ -27,11 +35,11 @@ const
|
|||
|
||||
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
|
||||
|
||||
url = /(^|\s|\n|\/?>)(https?:\/\/[-A-Z0-9+&#/%?=()~_|!:,.;]*[-A-Z0-9+&#/%=~()_|])/gi,
|
||||
urlRegExp = /https?:\/\/[^\p{C}\p{Z}]+[^\p{C}\p{Z}.]/gu,
|
||||
// eslint-disable-next-line max-len
|
||||
email = /(^|\s|\n|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
|
||||
email = /(^|\r|\n|\p{C}\p{Z})((?:[^"(),.:;<>@[\]\\\p{C}\p{Z}]+(?:\.[^"(),.:;<>@[\]\\\p{C}\p{Z}]+)*|"(?:\\?[^"\\\p{C}\p{Z}])*")@[^@\p{C}\p{Z}]+[^@\p{C}\p{Z}.])/gui,
|
||||
// rfc3966
|
||||
tel = /(^|\s|\n|\/?>)(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
|
||||
tel = /(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
|
||||
|
||||
// Strip tracking
|
||||
/** TODO: implement other url strippers like from
|
||||
|
|
@ -90,6 +98,13 @@ const
|
|||
return url;
|
||||
},
|
||||
|
||||
cleanCSS = source =>
|
||||
source.trim().replace(/-(ms|webkit)-[^;]+(;|$)/g, '')
|
||||
.replace(/white-space[^;]+(;|$)/g, '')
|
||||
// Drop Microsoft Office style properties
|
||||
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
||||
,
|
||||
|
||||
/*
|
||||
Parses given css string, and returns css object
|
||||
keys as selectors and values are css rules
|
||||
|
|
@ -161,7 +176,7 @@ const
|
|||
// we have standard css
|
||||
css.push({
|
||||
selector: selector,
|
||||
rules: arr[6]
|
||||
rules: cleanCSS(arr[6])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -239,13 +254,6 @@ export const
|
|||
// td
|
||||
'colspan', 'rowspan', 'headers'
|
||||
],
|
||||
disallowedTags = [
|
||||
'SVG','SCRIPT','TITLE','LINK','BASE','META',
|
||||
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
|
||||
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
|
||||
// Not supported by <template> element
|
||||
// ,'HTML','HEAD','BODY'
|
||||
],
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG'
|
||||
];
|
||||
|
|
@ -256,9 +264,9 @@ export const
|
|||
msgId = 0;
|
||||
}
|
||||
|
||||
tpl.innerHTML = html
|
||||
tmpl.innerHTML = html
|
||||
// Strip Microsoft comments
|
||||
.replace(/<!--\[if[\s\S]*?endif\]-->/gi, '')
|
||||
.replace(/<!--\[if[\s\S]*?-->/gi, '')
|
||||
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
|
||||
// Not supported by <template> element
|
||||
// .replace(/<!doctype[^>]*>/gi, '')
|
||||
|
|
@ -274,16 +282,17 @@ export const
|
|||
|
||||
// \MailSo\Base\HtmlUtils::ClearComments()
|
||||
// https://github.com/the-djmaze/snappymail/issues/187
|
||||
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
|
||||
const nodeIterator = document.createNodeIterator(tmpl.content, NodeFilter.SHOW_COMMENT);
|
||||
while (nodeIterator.nextNode()) {
|
||||
nodeIterator.referenceNode.remove();
|
||||
}
|
||||
|
||||
if (0 < bqLevel) {
|
||||
tpl.content.querySelectorAll(new Array(1 + bqLevel).fill('blockquote').join(' ')).forEach(node => node.remove());
|
||||
}
|
||||
tmpl.content.querySelectorAll(
|
||||
disallowedTags
|
||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||
).forEach(oElement => oElement.remove());
|
||||
|
||||
tpl.content.querySelectorAll('*').forEach(oElement => {
|
||||
[...tmpl.content.querySelectorAll('*')].forEach(oElement => {
|
||||
const name = oElement.tagName,
|
||||
oStyle = oElement.style;
|
||||
|
||||
|
|
@ -303,8 +312,7 @@ export const
|
|||
}
|
||||
|
||||
// \MailSo\Base\HtmlUtils::ClearTags()
|
||||
if (disallowedTags.includes(name)
|
||||
|| 'none' == oStyle.display
|
||||
if ('none' == oStyle.display
|
||||
|| 'hidden' == oStyle.visibility
|
||||
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
|
||||
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
||||
|
|
@ -320,15 +328,16 @@ export const
|
|||
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);
|
||||
delAttribute = name => {
|
||||
let value = getAttribute(name);
|
||||
oElement.removeAttribute(name);
|
||||
return value;
|
||||
};
|
||||
|
||||
if ('mail-body' === className) {
|
||||
forEachObjectEntry(tasks, (name, cb) => {
|
||||
if (hasAttribute(name)) {
|
||||
cb(getAttribute(name), oElement);
|
||||
delAttribute(name);
|
||||
}
|
||||
});
|
||||
forEachObjectEntry(tasks, (name, cb) =>
|
||||
hasAttribute(name) && cb(delAttribute(name), oElement)
|
||||
);
|
||||
} else if (msgId && className) {
|
||||
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
|
||||
}
|
||||
|
|
@ -350,21 +359,21 @@ export const
|
|||
if (!oStyle.backgroundImage) {
|
||||
if ('TD' !== name && 'TH' !== name) {
|
||||
// Make width responsive
|
||||
if (hasAttribute('width')) {
|
||||
value = getAttribute('width');
|
||||
if (hasAttribute('width') && !oStyle.width) {
|
||||
value = delAttribute('width');
|
||||
oStyle.width = value.includes('%') ? value : value + 'px';
|
||||
delAttribute('width');
|
||||
}
|
||||
value = oStyle.removeProperty('width');
|
||||
if (parseInt(value,10) && !oStyle.maxWidth) {
|
||||
value = oStyle.width;
|
||||
if (100 < parseInt(value,10) && !oStyle.maxWidth) {
|
||||
oStyle.maxWidth = value;
|
||||
oStyle.width = '100%';
|
||||
} else if (!value?.includes('%')) {
|
||||
oStyle.removeProperty('width');
|
||||
}
|
||||
// Make height responsive
|
||||
if (hasAttribute('height')) {
|
||||
value = getAttribute('height');
|
||||
value = delAttribute('height');
|
||||
oStyle.height = value.includes('%') ? value : value + 'px';
|
||||
delAttribute('height');
|
||||
}
|
||||
value = oStyle.removeProperty('height');
|
||||
if (value && !oStyle.maxHeight) {
|
||||
|
|
@ -402,8 +411,7 @@ export const
|
|||
|
||||
let skipStyle = false;
|
||||
if (hasAttribute('src')) {
|
||||
value = stripTracking(getAttribute('src'));
|
||||
delAttribute('src');
|
||||
value = stripTracking(delAttribute('src'));
|
||||
|
||||
if ('IMG' === name) {
|
||||
oElement.loading = 'lazy';
|
||||
|
|
@ -464,18 +472,15 @@ export const
|
|||
}
|
||||
|
||||
if (hasAttribute('background')) {
|
||||
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
|
||||
delAttribute('background');
|
||||
oStyle.backgroundImage = 'url("' + delAttribute('background') + '")';
|
||||
}
|
||||
|
||||
if (hasAttribute('bgcolor')) {
|
||||
oStyle.backgroundColor = getAttribute('bgcolor');
|
||||
delAttribute('bgcolor');
|
||||
oStyle.backgroundColor = delAttribute('bgcolor');
|
||||
}
|
||||
|
||||
if (hasAttribute('color')) {
|
||||
oStyle.color = getAttribute('color');
|
||||
delAttribute('color');
|
||||
oStyle.color = delAttribute('color');
|
||||
}
|
||||
|
||||
if (!skipStyle) {
|
||||
|
|
@ -540,8 +545,7 @@ export const
|
|||
oStyle.removeProperty('color');
|
||||
}
|
||||
|
||||
// Drop Microsoft Office style properties
|
||||
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
|
||||
oStyle.cssText = cleanCSS(oStyle.cssText);
|
||||
}
|
||||
|
||||
if (debug && aAttrsForRemove.length) {
|
||||
|
|
@ -551,8 +555,8 @@ export const
|
|||
|
||||
blockquoteSwitcher();
|
||||
|
||||
// return tpl.content.firstChild;
|
||||
result.html = tpl.innerHTML.trim();
|
||||
// return tmpl.content.firstChild;
|
||||
result.html = tmpl.innerHTML.trim();
|
||||
return result;
|
||||
},
|
||||
|
||||
|
|
@ -563,7 +567,7 @@ export const
|
|||
htmlToPlain = html => {
|
||||
const
|
||||
hr = '⎯'.repeat(64),
|
||||
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
|
||||
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
|
||||
blockquotes = node => {
|
||||
let bq;
|
||||
while ((bq = node.querySelector('blockquote'))) {
|
||||
|
|
@ -591,7 +595,7 @@ export const
|
|||
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
||||
}
|
||||
|
||||
tpl.innerHTML = html
|
||||
tmpl.innerHTML = html
|
||||
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
||||
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||
|
||||
|
|
@ -641,16 +645,16 @@ export const
|
|||
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
||||
|
||||
// Convert line-breaks
|
||||
tpl.innerHTML = tpl.innerHTML
|
||||
tmpl.innerHTML = tmpl.innerHTML
|
||||
.replace(/\n{3,}/gm, '\n\n')
|
||||
.replace(/\n<br[^>]*>/g, '\n')
|
||||
.replace(/<br[^>]*>\n/g, '\n');
|
||||
forEach('br', br => br.replaceWith('\n'));
|
||||
|
||||
// Blockquotes must be last
|
||||
blockquotes(tpl.content);
|
||||
blockquotes(tmpl.content);
|
||||
|
||||
return (tpl.content.textContent || '').trim();
|
||||
return (tmpl.content.textContent || '').trim();
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -704,22 +708,22 @@ export const
|
|||
aText = aNextText;
|
||||
} while (bDo);
|
||||
|
||||
tpl.innerHTML = aText.join('\n')
|
||||
tmpl.innerHTML = aText.join('\n')
|
||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(url, (...m) => {
|
||||
m[2] = stripTracking(m[2]);
|
||||
return `${m[1]}<a href="${m[2]}" target="_blank">${m[2]}</a>`;
|
||||
.replace(urlRegExp, (...m) => {
|
||||
m[0] = stripTracking(m[0]);
|
||||
return `<a href="${m[0]}" target="_blank">${m[0]}</a>`;
|
||||
})
|
||||
.replace(email, '$1<a href="mailto:$2">$2</a>')
|
||||
.replace(tel, '$1<a href="$2">$2</a>')
|
||||
.replace(tel, '<a href="$1">$1</a>')
|
||||
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
|
||||
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
|
||||
.replace(/\n/g, '<br>');
|
||||
blockquoteSwitcher();
|
||||
return tpl.innerHTML.trim();
|
||||
return tmpl.innerHTML.trim();
|
||||
},
|
||||
|
||||
WYSIWYGS = ko.observableArray();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const
|
|||
*/
|
||||
serverRequestRaw = (type, hash) =>
|
||||
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||
+ '0/' // Settings.get('AccountHash') ?
|
||||
+ '0/' // Settings.get('accountHash') ?
|
||||
+ (type
|
||||
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
||||
: ''),
|
||||
|
|
|
|||
|
|
@ -87,8 +87,7 @@ export class Selector {
|
|||
* Below code is used to keep checked/focused/selected states when array is refreshed.
|
||||
*/
|
||||
|
||||
let aCache = [],
|
||||
aCheckedCache = [],
|
||||
let aCheckedCache = [],
|
||||
mFocused = null,
|
||||
mSelected = null;
|
||||
|
||||
|
|
@ -99,7 +98,6 @@ export class Selector {
|
|||
items.forEach(item => {
|
||||
const uid = this.getItemUid(item);
|
||||
if (uid) {
|
||||
aCache.push(uid);
|
||||
item.checked() && aCheckedCache.push(uid);
|
||||
if (!mFocused && item.focused()) {
|
||||
mFocused = uid;
|
||||
|
|
@ -118,8 +116,7 @@ export class Selector {
|
|||
koList.subscribe(aItems => {
|
||||
selectedItemUseCallback = false;
|
||||
|
||||
koFocusedItem(null);
|
||||
koSelectedItem(null);
|
||||
this.unselect();
|
||||
|
||||
if (isArray(aItems)) {
|
||||
let temp,
|
||||
|
|
@ -182,7 +179,6 @@ export class Selector {
|
|||
}
|
||||
}
|
||||
|
||||
aCache = [];
|
||||
aCheckedCache = [];
|
||||
mFocused = null;
|
||||
mSelected = null;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import ko from 'ko';
|
||||
import { Notification, UploadErrorCode } from 'Common/Enums';
|
||||
import { Notifications, UploadErrorCode } from 'Common/Enums';
|
||||
import { langLink } from 'Common/Links';
|
||||
import { doc, createElement } from 'Common/Globals';
|
||||
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
||||
|
|
@ -20,7 +20,7 @@ const
|
|||
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
||||
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
let key = getKeyByValue(Notifications, code);
|
||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||
},
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ export const
|
|||
*/
|
||||
getNotification = (code, message = '', defCode = 0) => {
|
||||
code = pInt(code);
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
if (Notifications.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,15 @@ export const
|
|||
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)))),
|
||||
b64Encode = data => btoa(unescape(encodeURIComponent(data))),
|
||||
/* // Without deprecated 'unescape':
|
||||
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
|
||||
b64Encode = data => btoa(encodeURIComponent(data).replace(
|
||||
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
||||
)),
|
||||
*/
|
||||
|
||||
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
|
||||
|
||||
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
|
||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { MessageFlagsCache } from 'Common/Cache';
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||
import { plainToHtml } from 'Common/Html';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||
import { MessageModel } from 'Model/Message';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
|
@ -43,10 +41,11 @@ download = (link, name = "") => {
|
|||
}
|
||||
},
|
||||
|
||||
downloadZip = (hashes, onError, fTrigger, folder) => {
|
||||
downloadZip = (name, hashes, onError, fTrigger, folder) => {
|
||||
if (hashes.length) {
|
||||
let params = {
|
||||
target: 'zip',
|
||||
filename: name,
|
||||
hashes: hashes
|
||||
};
|
||||
if (!onError) {
|
||||
|
|
@ -156,7 +155,7 @@ mailToHelper = mailToUrl => {
|
|||
mailToUrl = mailToUrl.slice(7).split('?');
|
||||
|
||||
const
|
||||
email = mailToUrl[0],
|
||||
email = decodeURIComponent(mailToUrl[0]),
|
||||
params = new URLSearchParams(mailToUrl[1]),
|
||||
to = params.get('to'),
|
||||
toEmailModel = value => EmailCollectionModel.fromString(value);
|
||||
|
|
@ -246,75 +245,78 @@ setLayoutResizer = (source, sClientSideKeyName, mode) =>
|
|||
}
|
||||
},
|
||||
|
||||
viewMessage = (oMessage, popup) => {
|
||||
if (popup) {
|
||||
oMessage.viewPopupMessage();
|
||||
} else {
|
||||
MessageUserStore.error('');
|
||||
let id = 'rl-msg-' + oMessage.hash,
|
||||
body = oMessage.body || elementById(id);
|
||||
if (!body) {
|
||||
body = createElement('div',{
|
||||
id:id,
|
||||
hidden:'',
|
||||
class:'b-text-part'
|
||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
});
|
||||
MessageUserStore.purgeCache();
|
||||
}
|
||||
|
||||
body.message = oMessage;
|
||||
oMessage.body = body;
|
||||
|
||||
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
||||
oMessage.viewPlain();
|
||||
}
|
||||
|
||||
MessageUserStore.bodiesDom().append(body);
|
||||
|
||||
MessageUserStore.loading(false);
|
||||
oMessage.body.hidden = false;
|
||||
|
||||
if (oMessage.isUnseen()) {
|
||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
populateMessageBody = (oMessage, popup) => {
|
||||
if (oMessage) {
|
||||
popup || MessageUserStore.message(oMessage);
|
||||
popup || MessageUserStore.loading(true);
|
||||
Remote.message((iError, oData/*, bCached*/) => {
|
||||
if (iError) {
|
||||
if (Notification.RequestAborted !== iError && !popup) {
|
||||
MessageUserStore.message(null);
|
||||
MessageUserStore.error(getNotification(iError));
|
||||
}
|
||||
} else {
|
||||
let json = oData?.Result;
|
||||
if (json
|
||||
&& MessageModel.validJson(json)
|
||||
&& oMessage.hash === json.hash
|
||||
// && oMessage.folder === json.folder
|
||||
// && oMessage.uid == json.uid
|
||||
&& oMessage.revivePropertiesFromJson(json)
|
||||
) {
|
||||
if (oMessage.body) {
|
||||
viewMessage(oMessage, popup);
|
||||
} else {
|
||||
popup || MessageUserStore.loading(true);
|
||||
Remote.message((iError, oData/*, bCached*/) => {
|
||||
if (iError) {
|
||||
if (Notifications.RequestAborted !== iError && !popup) {
|
||||
MessageUserStore.message(null);
|
||||
MessageUserStore.error(getNotification(iError));
|
||||
}
|
||||
} else {
|
||||
let json = oData?.Result;
|
||||
if (json
|
||||
&& oMessage.hash === json.hash
|
||||
// && oMessage.folder === json.folder
|
||||
// && oMessage.uid == json.uid
|
||||
&& oMessage.revivePropertiesFromJson(json)
|
||||
) {
|
||||
/*
|
||||
if (bCached) {
|
||||
delete json.flags;
|
||||
}
|
||||
*/
|
||||
if (popup) {
|
||||
oMessage.viewPopupMessage();
|
||||
} else {
|
||||
MessageUserStore.error('');
|
||||
const messagesDom = MessageUserStore.bodiesDom();
|
||||
if (messagesDom) {
|
||||
let id = 'rl-msg-' + oMessage.hash,
|
||||
body = elementById(id);
|
||||
if (body) {
|
||||
oMessage.body = body;
|
||||
oMessage.isHtml(body.classList.contains('html'));
|
||||
oMessage.hasImages(body.rlHasImages);
|
||||
} else {
|
||||
body = createElement('div',{
|
||||
id:id,
|
||||
hidden:'',
|
||||
class:'b-text-part'
|
||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
});
|
||||
oMessage.body = body;
|
||||
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
||||
oMessage.viewPlain();
|
||||
}
|
||||
|
||||
MessageUserStore.purgeMessageBodyCache();
|
||||
}
|
||||
|
||||
messagesDom.append(body);
|
||||
|
||||
oMessage.body.hidden = false;
|
||||
if (bCached) {
|
||||
delete json.flags;
|
||||
}
|
||||
}
|
||||
|
||||
MessageFlagsCache.initMessage(oMessage);
|
||||
if (oMessage.isUnseen()) {
|
||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||
);
|
||||
oMessage.body.remove();
|
||||
*/
|
||||
viewMessage(oMessage, popup);
|
||||
}
|
||||
}
|
||||
}
|
||||
popup || MessageUserStore.loading(false);
|
||||
}, oMessage.folder, oMessage.uid);
|
||||
popup || MessageUserStore.loading(false);
|
||||
}, oMessage.folder, oMessage.uid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
13
dev/External/SquireUI.js
vendored
13
dev/External/SquireUI.js
vendored
|
|
@ -27,11 +27,6 @@ const
|
|||
|
||||
SquireDefaultConfig = {
|
||||
/*
|
||||
blockTag: 'P',
|
||||
undo: {
|
||||
documentSizeThreshold: -1, // -1 means no threshold
|
||||
undoLimit: -1 // -1 means no limit
|
||||
},
|
||||
addLinks: true // allow_smart_html_links
|
||||
*/
|
||||
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
||||
|
|
@ -216,7 +211,7 @@ class SquireUI
|
|||
cmd: () => {
|
||||
let node = squire.getSelectionClosest('IMG'),
|
||||
src = prompt("Image", node?.src || "https://");
|
||||
src.length ? squire.insertImage(src) : (node && squire.detach(node));
|
||||
src?.length ? squire.insertImage(src) : (node && squire.detach(node));
|
||||
},
|
||||
hint: 'Image URL'
|
||||
},
|
||||
|
|
@ -288,11 +283,6 @@ class SquireUI
|
|||
toolbar.className = 'squire-toolbar btn-toolbar';
|
||||
let group, action/*, touchTap*/;
|
||||
for (group in actions) {
|
||||
/*
|
||||
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
|
||||
continue;
|
||||
}
|
||||
*/
|
||||
let toolgroup = createElement('div');
|
||||
toolgroup.className = 'btn-group';
|
||||
toolgroup.id = 'squire-toolgroup-'+group;
|
||||
|
|
@ -379,7 +369,6 @@ class SquireUI
|
|||
squire.addEventListener('cursor', );
|
||||
squire.addEventListener('select', );
|
||||
squire.addEventListener('input', );
|
||||
squire.addEventListener('willPaste', );
|
||||
squire.addEventListener( 'keydown keyup', monitorShiftKey )
|
||||
squire.addEventListener( 'keydown', onKey )
|
||||
*/
|
||||
|
|
|
|||
47
dev/External/ko.js
vendored
47
dev/External/ko.js
vendored
|
|
@ -28,6 +28,18 @@ export const
|
|||
|
||||
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
|
||||
|
||||
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
|
||||
let fn = event => {
|
||||
if (key == event.key) {
|
||||
// stopEvent(event);
|
||||
// element.dispatchEvent(new Event('change'));
|
||||
fValueAccessor().call(model);
|
||||
}
|
||||
};
|
||||
element.addEventListener('keydown', fn);
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
||||
},
|
||||
|
||||
// With this we don't need delegateRunOnDestroy
|
||||
koArrayWithDestroy = data => {
|
||||
data = ko.observableArray(data);
|
||||
|
|
@ -56,41 +68,18 @@ Object.assign(ko.bindingHandlers, {
|
|||
},
|
||||
|
||||
onEnter: {
|
||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
||||
let fn = event => {
|
||||
if ('Enter' == event.key) {
|
||||
element.dispatchEvent(new Event('change'));
|
||||
fValueAccessor().call(viewModel);
|
||||
}
|
||||
};
|
||||
element.addEventListener('keydown', fn);
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
||||
}
|
||||
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||
onKey('Enter', element, fValueAccessor, fAllBindings, model)
|
||||
},
|
||||
|
||||
onEsc: {
|
||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
||||
let fn = event => {
|
||||
if ('Escape' == event.key) {
|
||||
element.dispatchEvent(new Event('change'));
|
||||
fValueAccessor().call(viewModel);
|
||||
}
|
||||
};
|
||||
element.addEventListener('keyup', fn);
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
||||
}
|
||||
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||
onKey('Escape', element, fValueAccessor, fAllBindings, model)
|
||||
},
|
||||
|
||||
onSpace: {
|
||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
||||
let fn = event => {
|
||||
if (' ' == event.key) {
|
||||
fValueAccessor().call(viewModel, event);
|
||||
}
|
||||
};
|
||||
element.addEventListener('keyup', fn);
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
||||
}
|
||||
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||
onKey(' ', element, fValueAccessor, fAllBindings, model)
|
||||
},
|
||||
|
||||
i18nUpdate: {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { doc, $htmlCL, elementById, createElement, fireEvent } from 'Common/Glob
|
|||
import { forEachObjectEntry } from 'Common/Utils';
|
||||
import { i18nToNodes } from 'Common/Translator';
|
||||
|
||||
import { leftPanelDisabled } from 'Common/Globals';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
let
|
||||
currentScreen = null,
|
||||
defaultScreenName = '';
|
||||
|
|
@ -151,6 +154,7 @@ const
|
|||
vm.onHide?.();
|
||||
destroy && vm.viewModelDom.remove();
|
||||
});
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//import { b64Encode } from 'Common/Utils';
|
||||
|
||||
const
|
||||
// RFC2045
|
||||
|
|
@ -68,6 +69,7 @@ export function ParseMime(text)
|
|||
body = QPDecode(body);
|
||||
}
|
||||
body = btoa(body);
|
||||
// body = b64Encode(body);
|
||||
}
|
||||
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,7 @@ export function MimeToMessage(data, message)
|
|||
attachment.url = part.dataUrl;
|
||||
attachment.estimatedSize = part.body.length;
|
||||
/*
|
||||
attachment.isThumbnail = false;
|
||||
attachment.contentLocation = '';
|
||||
attachment.download = '';
|
||||
attachment.folder = '';
|
||||
attachment.uid = '';
|
||||
attachment.mimeIndex = part.id;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { FileInfo, FileType } from 'Common/File';
|
||||
import { stopEvent, SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||
import { b64EncodeJSONSafe } from 'Common/Utils';
|
||||
import {
|
||||
attachmentDownload,
|
||||
serverRequestRaw
|
||||
|
|
@ -22,10 +24,8 @@ export class AttachmentModel extends AbstractModel {
|
|||
this.fileName = '';
|
||||
this.fileNameExt = '';
|
||||
this.fileType = FileType.Unknown;
|
||||
this.isThumbnail = false;
|
||||
this.cId = '';
|
||||
this.contentLocation = '';
|
||||
this.download = '';
|
||||
this.folder = '';
|
||||
this.uid = '';
|
||||
this.url = '';
|
||||
|
|
@ -53,7 +53,7 @@ export class AttachmentModel extends AbstractModel {
|
|||
}
|
||||
|
||||
toggleChecked(self, event) {
|
||||
event.stopPropagation();
|
||||
stopEvent(event);
|
||||
self.checked(!self.checked());
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +125,17 @@ export class AttachmentModel extends AbstractModel {
|
|||
);
|
||||
}
|
||||
|
||||
get download() {
|
||||
return b64EncodeJSONSafe({
|
||||
folder: this.folder,
|
||||
uid: this.uid,
|
||||
mimeIndex: this.mimeIndex,
|
||||
mimeType: this.mimeType,
|
||||
fileName: this.fileName,
|
||||
accountHash: SettingsGet('accountHash')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
|
|
@ -143,7 +154,7 @@ export class AttachmentModel extends AbstractModel {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
hasThumbnail() {
|
||||
return this.isThumbnail && !this.isLinked();
|
||||
return SettingsCapa('AttachmentThumbnails') && this.isImage() && !this.isLinked();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
|||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||
import { getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
|
||||
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName, removeFolderFromCacheList } from 'Common/Cache';
|
||||
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
|
||||
import * as Local from 'Storage/Client';
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
|||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import { sortFolders } from 'Common/Folders';
|
||||
import { i18n, translateTrigger } from 'Common/Translator';
|
||||
import { i18n, translateTrigger, getNotification } from 'Common/Translator';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
|
|
@ -91,6 +92,7 @@ export const
|
|||
Remote.abort('Folders')
|
||||
.post('Folders', FolderUserStore.foldersLoading)
|
||||
.then(data => {
|
||||
clearCache();
|
||||
FolderCollectionModel.reviveFromJson(data.Result)?.storeIt();
|
||||
fCallback?.(true);
|
||||
// Repeat every 15 minutes?
|
||||
|
|
@ -127,8 +129,8 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
let oCacheFolder = getFolderFromCacheList(oFolder.fullName);
|
||||
if (oCacheFolder) {
|
||||
// oCacheFolder.revivePropertiesFromJson(oFolder);
|
||||
if (oFolder.hash) {
|
||||
oCacheFolder.hash = oFolder.hash;
|
||||
if (oFolder.etag) {
|
||||
oCacheFolder.etag = oFolder.etag;
|
||||
}
|
||||
if (null != oFolder.totalEmails) {
|
||||
oCacheFolder.totalEmails(oFolder.totalEmails);
|
||||
|
|
@ -167,22 +169,22 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
break;
|
||||
}
|
||||
// Flags
|
||||
if (oFolder.flags.includes('\\sentmail')) {
|
||||
if (oFolder.attributes.includes('\\sentmail')) {
|
||||
role = 'sent';
|
||||
}
|
||||
if (oFolder.flags.includes('\\spam')) {
|
||||
if (oFolder.attributes.includes('\\spam')) {
|
||||
role = 'junk';
|
||||
}
|
||||
if (oFolder.flags.includes('\\bin')) {
|
||||
if (oFolder.attributes.includes('\\bin')) {
|
||||
role = 'trash';
|
||||
}
|
||||
if (oFolder.flags.includes('\\important')) {
|
||||
if (oFolder.attributes.includes('\\important')) {
|
||||
role = 'important';
|
||||
}
|
||||
if (oFolder.flags.includes('\\starred')) {
|
||||
if (oFolder.attributes.includes('\\starred')) {
|
||||
role = 'flagged';
|
||||
}
|
||||
if (oFolder.flags.includes('\\all') || oFolder.flags.includes('\\allmail')) {
|
||||
if (oFolder.attributes.includes('\\all') || oFolder.flags.includes('\\allmail')) {
|
||||
role = 'all';
|
||||
}
|
||||
}
|
||||
|
|
@ -226,9 +228,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
name: name,
|
||||
fullName: parentName,
|
||||
delimiter: delimiter,
|
||||
exists: false,
|
||||
isSubscribed: false,
|
||||
flags: ['\\nonexistent']
|
||||
attributes: ['\\nonexistent']
|
||||
});
|
||||
setFolder(pfolder);
|
||||
result.splice(i, 0, pfolder);
|
||||
|
|
@ -300,7 +300,7 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
this.exists = true;
|
||||
|
||||
this.hash = '';
|
||||
this.etag = '';
|
||||
this.id = 0;
|
||||
this.uidNext = 0;
|
||||
|
||||
|
|
@ -330,7 +330,8 @@ export class FolderModel extends AbstractModel {
|
|||
tagsAllowed: false
|
||||
});
|
||||
|
||||
this.flags = ko.observableArray();
|
||||
this.attributes = ko.observableArray();
|
||||
// For messages
|
||||
this.permanentFlags = ko.observableArray();
|
||||
|
||||
this.addSubscribables({
|
||||
|
|
@ -379,6 +380,8 @@ export class FolderModel extends AbstractModel {
|
|||
isFlagged: () => FolderUserStore.currentFolder() === this
|
||||
&& MessagelistUserStore.listSearch().includes('flagged'),
|
||||
|
||||
// isSubscribed: () => this.attributes().includes('\\subscribed'),
|
||||
|
||||
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
|
||||
|
||||
hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find(
|
||||
|
|
@ -465,6 +468,49 @@ export class FolderModel extends AbstractModel {
|
|||
});
|
||||
}
|
||||
|
||||
edit() {
|
||||
this.canBeEdited() && this.editing(true);
|
||||
}
|
||||
|
||||
unedit() {
|
||||
this.editing(false);
|
||||
}
|
||||
|
||||
rename() {
|
||||
const folder = this,
|
||||
nameToEdit = folder.nameForEdit().trim();
|
||||
if (nameToEdit && folder.name() !== nameToEdit) {
|
||||
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
||||
folder: folder.fullName,
|
||||
newFolderName: nameToEdit,
|
||||
subscribe: folder.isSubscribed() ? 1 : 0
|
||||
})
|
||||
.then(data => {
|
||||
folder.name(nameToEdit/*data.name*/);
|
||||
if (folder.subFolders.length) {
|
||||
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
||||
// clearTimeout(Remote.foldersTimeout);
|
||||
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
|
||||
setTimeout(loadFolders, 500);
|
||||
// TODO: rename all subfolders with folder.delimiter to prevent reload?
|
||||
} else {
|
||||
removeFolderFromCacheList(folder.fullName);
|
||||
folder.fullName = data.Result.fullName;
|
||||
setFolder(folder);
|
||||
const parent = getFolderFromCacheList(folder.parentName);
|
||||
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
FolderUserStore.folderListError(
|
||||
getNotification(error.code, '', Notifications.CantRenameFolder)
|
||||
+ '.\n' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
folder.editing(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* For url safe '/#/mailbox/...' path
|
||||
*/
|
||||
|
|
@ -491,6 +537,10 @@ export class FolderModel extends AbstractModel {
|
|||
path.pop();
|
||||
folder.parentName = path.join(folder.delimiter);
|
||||
|
||||
folder.isSubscribed(folder.attributes.includes('\\subscribed'));
|
||||
folder.exists = !folder.attributes.includes('\\nonexistent');
|
||||
folder.selectable(folder.exists && !folder.attributes.includes('\\noselect'));
|
||||
|
||||
type && 'mail' != type && folder.kolabType(type);
|
||||
}
|
||||
return folder;
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ export class IdentityModel extends AbstractModel {
|
|||
* @param {string} id
|
||||
* @param {string} email
|
||||
*/
|
||||
constructor(id, email) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
addObservablesTo(this, {
|
||||
id: id || '',
|
||||
email: email,
|
||||
id: '',
|
||||
email: '',
|
||||
name: '',
|
||||
|
||||
replyTo: '',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { i18n } from 'Common/Translator';
|
|||
|
||||
import { doc, SettingsGet } from 'Common/Globals';
|
||||
import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html';
|
||||
import { forEachObjectEntry } from 'Common/Utils';
|
||||
import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||
import { serverRequestRaw, proxy } from 'Common/Links';
|
||||
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||
|
||||
|
|
@ -21,7 +21,6 @@ import PreviewHTML from 'Html/PreviewMessage.html';
|
|||
|
||||
import { LanguageStore } from 'Stores/Language';
|
||||
|
||||
//import { MessageFlagsCache } from 'Common/Cache';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
const
|
||||
|
|
@ -34,7 +33,6 @@ const
|
|||
Remote.request('MessageSetKeyword', iError => {
|
||||
if (!iError) {
|
||||
isSet ? flags.remove(lower) : flags.push(lower);
|
||||
// MessageFlagsCache.setFor(message.folder, message.uid, flags());
|
||||
}
|
||||
}, {
|
||||
folder: message.folder,
|
||||
|
|
@ -61,7 +59,6 @@ export class MessageModel extends AbstractModel {
|
|||
this.folder = '';
|
||||
this.uid = 0;
|
||||
this.hash = '';
|
||||
this.requestHash = '';
|
||||
this.from = new EmailCollectionModel;
|
||||
this.to = new EmailCollectionModel;
|
||||
this.cc = new EmailCollectionModel;
|
||||
|
|
@ -88,7 +85,8 @@ export class MessageModel extends AbstractModel {
|
|||
spamResult: '',
|
||||
isSpam: false,
|
||||
hasVirus: null, // or boolean when scanned
|
||||
dateTimeStampInUTC: 0,
|
||||
dateTimestamp: 0,
|
||||
internalTimestamp: 0,
|
||||
priority: MessagePriority.Normal,
|
||||
|
||||
senderEmailsString: '',
|
||||
|
|
@ -180,6 +178,16 @@ export class MessageModel extends AbstractModel {
|
|||
});
|
||||
}
|
||||
|
||||
get requestHash() {
|
||||
return b64EncodeJSONSafe({
|
||||
folder: this.folder,
|
||||
uid: this.uid,
|
||||
mimeType: 'message/rfc822',
|
||||
fileName: (this.subject() || 'message-' + this.hash) + '.eml',
|
||||
accountHash: SettingsGet('accountHash')
|
||||
});
|
||||
}
|
||||
|
||||
toggleTag(keyword) {
|
||||
toggleTag(this, keyword);
|
||||
}
|
||||
|
|
@ -290,54 +298,51 @@ export class MessageModel extends AbstractModel {
|
|||
return [[...toResult.values()], [...ccResult.values()]];
|
||||
}
|
||||
|
||||
viewHtml() {
|
||||
viewBody(html) {
|
||||
const body = this.body;
|
||||
if (body && this.html()) {
|
||||
let result = msgHtml(this);
|
||||
this.hasExternals(result.hasExternals);
|
||||
this.hasImages(body.rlHasImages = !!result.hasExternals);
|
||||
|
||||
body.innerHTML = result.html;
|
||||
|
||||
body.classList.toggle('html', 1);
|
||||
body.classList.toggle('plain', 0);
|
||||
|
||||
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
|
||||
if ('always' === SettingsUserStore.viewImages()) {
|
||||
this.showExternalImages();
|
||||
}
|
||||
if ('match' === SettingsUserStore.viewImages()) {
|
||||
this.showExternalImages(1);
|
||||
if (body) {
|
||||
if (html) {
|
||||
let result = msgHtml(this);
|
||||
this.hasExternals(result.hasExternals);
|
||||
this.hasImages(!!result.hasExternals);
|
||||
body.innerHTML = result.html;
|
||||
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
|
||||
if ('always' === SettingsUserStore.viewImages()) {
|
||||
this.showExternalImages();
|
||||
}
|
||||
if ('match' === SettingsUserStore.viewImages()) {
|
||||
this.showExternalImages(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
body.innerHTML = plainToHtml(
|
||||
(this.plain()
|
||||
? this.plain()
|
||||
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[^\r\n]+)+|SIGNATURE-----[\s\S]*)/sg, '')
|
||||
.trim()
|
||||
: htmlToPlain(body.innerHTML)
|
||||
)
|
||||
);
|
||||
this.hasImages(false);
|
||||
}
|
||||
|
||||
this.isHtml(true);
|
||||
body.classList.toggle('html', html);
|
||||
body.classList.toggle('plain', !html);
|
||||
this.isHtml(html);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
viewHtml() {
|
||||
return this.html() && this.viewBody(true);
|
||||
}
|
||||
|
||||
viewPlain() {
|
||||
const body = this.body;
|
||||
if (body) {
|
||||
body.classList.toggle('html', 0);
|
||||
body.classList.toggle('plain', 1);
|
||||
body.innerHTML = plainToHtml(
|
||||
(this.plain()
|
||||
? this.plain()
|
||||
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
|
||||
.trim()
|
||||
: htmlToPlain(body.innerHTML)
|
||||
)
|
||||
);
|
||||
this.isHtml(false);
|
||||
this.hasImages(false);
|
||||
return true;
|
||||
}
|
||||
return this.viewBody(false);
|
||||
}
|
||||
|
||||
viewPopupMessage(print) {
|
||||
const
|
||||
timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
||||
timeStampInUTC = this.dateTimestamp() || 0,
|
||||
ccLine = this.cc.toString(),
|
||||
bccLine = this.bcc.toString(),
|
||||
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
||||
|
|
@ -430,7 +435,7 @@ export class MessageModel extends AbstractModel {
|
|||
hasImages = true;
|
||||
},
|
||||
attr = 'data-x-src',
|
||||
src, useProxy = !!SettingsGet('UseLocalProxyForExternalImages');
|
||||
src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
|
||||
body.querySelectorAll('img[' + attr + ']').forEach(node => {
|
||||
src = node.getAttribute(attr);
|
||||
if (isValid(src)) {
|
||||
|
|
@ -447,7 +452,6 @@ export class MessageModel extends AbstractModel {
|
|||
});
|
||||
|
||||
this.hasImages(hasImages);
|
||||
body.rlHasImages = hasImages;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
|||
import { MessageModel } from 'Model/Message';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
|
||||
import {
|
||||
MessageFlagsCache
|
||||
} from 'Common/Cache';
|
||||
|
||||
'use strict';
|
||||
|
||||
export class MessageCollectionModel extends AbstractCollectionModel
|
||||
|
|
@ -13,17 +9,16 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
/*
|
||||
constructor() {
|
||||
super();
|
||||
this.Filtered
|
||||
this.filtered
|
||||
this.folder
|
||||
this.folderHash
|
||||
this.folderInfo
|
||||
this.totalEmails
|
||||
this.unreadEmails
|
||||
this.totalThreads
|
||||
this.threadUid
|
||||
this.newMessages
|
||||
this.offset
|
||||
this.limit
|
||||
this.search
|
||||
this.limited
|
||||
}
|
||||
*/
|
||||
|
||||
|
|
@ -31,7 +26,7 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
* @param {?Object} json
|
||||
* @returns {MessageCollectionModel}
|
||||
*/
|
||||
static reviveFromJson(object, cached) {
|
||||
static reviveFromJson(object/*, cached*/) {
|
||||
let msg = MessageUserStore.message();
|
||||
return super.reviveFromJson(object, message => {
|
||||
// If message is currently viewed, use that.
|
||||
|
|
@ -39,7 +34,6 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message);
|
||||
if (message) {
|
||||
message.deleted(false);
|
||||
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(message);
|
||||
return message;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { isArray, pInt, pString } from 'Common/Utils';
|
||||
import { serverRequest } from 'Common/Links';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
|
@ -9,18 +9,18 @@ const getURL = (add = '') => serverRequest('Json') + pString(add),
|
|||
|
||||
checkResponseError = data => {
|
||||
const err = data ? data.ErrorCode : null;
|
||||
if (Notification.InvalidToken === err) {
|
||||
if (Notifications.InvalidToken === err) {
|
||||
console.error(getNotification(err));
|
||||
// alert(getNotification(err));
|
||||
rl.logoutReload();
|
||||
} else if ([
|
||||
Notification.AuthError,
|
||||
Notification.ConnectionError,
|
||||
Notification.DomainNotAllowed,
|
||||
Notification.AccountNotAllowed,
|
||||
Notification.MailServerError,
|
||||
Notification.UnknownNotification,
|
||||
Notification.UnknownError
|
||||
Notifications.AuthError,
|
||||
Notifications.ConnectionError,
|
||||
Notifications.DomainNotAllowed,
|
||||
Notifications.AccountNotAllowed,
|
||||
Notifications.MailServerError,
|
||||
Notifications.UnknownNotification,
|
||||
Notifications.UnknownError
|
||||
].includes(err)
|
||||
) {
|
||||
if (7 < ++iJsonErrorCount) {
|
||||
|
|
@ -65,7 +65,7 @@ class FetchError extends Error
|
|||
{
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.code = code || Notification.JsonFalse;
|
||||
this.code = code || Notifications.JsonFalse;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ export class AbstractFetchRemote
|
|||
iJsonErrorCount = 0;
|
||||
} else {
|
||||
checkResponseError(data);
|
||||
iError = data.ErrorCode || Notification.UnknownError
|
||||
iError = data.ErrorCode || Notifications.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ export class AbstractFetchRemote
|
|||
abort(action, 0, 1);
|
||||
|
||||
if (!data) {
|
||||
return Promise.reject(new FetchError(Notification.JsonParse));
|
||||
return Promise.reject(new FetchError(Notifications.JsonParse));
|
||||
}
|
||||
/*
|
||||
let isCached = false, type = '';
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
const
|
||||
// folder = getFolderFromCacheList(params.folder.fullName),
|
||||
folder = getFolderFromCacheList(params.folder),
|
||||
folderHash = folder?.hash || '';
|
||||
folderETag = folder?.etag || '';
|
||||
|
||||
params = Object.assign({
|
||||
offset: 0,
|
||||
|
|
@ -40,8 +40,8 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
}
|
||||
|
||||
let sGetAdd = '';
|
||||
if (folderHash) {
|
||||
params.hash = folderHash + '-' + SettingsGet('AccountHash');
|
||||
if (folderETag) {
|
||||
params.hash = folderETag + '-' + SettingsGet('accountHash');
|
||||
sGetAdd = 'MessageList/' + SUB_QUERY_PREFIX + '/' + b64EncodeJSONSafe(params);
|
||||
params = {};
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
sFolderFullName,
|
||||
iUid,
|
||||
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
|
||||
SettingsGet('AccountHash')
|
||||
SettingsGet('accountHash')
|
||||
])
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import ko from 'ko';
|
|||
|
||||
import { pString } from 'Common/Utils';
|
||||
import { settings } from 'Common/Links';
|
||||
import { createElement, elementById } from 'Common/Globals';
|
||||
import { createElement/*, elementById*/ } from 'Common/Globals';
|
||||
|
||||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||
import { i18nToNodes } from 'Common/Translator';
|
||||
|
|
@ -29,7 +29,8 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
);
|
||||
|
||||
if (RoutedSettingsViewModel) {
|
||||
const vmPlace = elementById('rl-settings-subscreen');
|
||||
// const vmPlace = elementById('V-SettingsPane') || elementById('V-AdminPane);
|
||||
const vmPlace = this.viewModels[1].__dom;
|
||||
if (RoutedSettingsViewModel.__vm) {
|
||||
settingsScreen = RoutedSettingsViewModel.__vm;
|
||||
} else if (vmPlace) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import { Scope } from 'Common/Enums';
|
||||
import { doc, createElement, leftPanelDisabled, Settings } from 'Common/Globals';
|
||||
import { doc, createElement, Settings } from 'Common/Globals';
|
||||
import { pString, pInt } from 'Common/Utils';
|
||||
import { moveAction } from 'Common/UtilsUser';
|
||||
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
|
||||
import { getFolderFromHashMap, getFolderInboxName } from 'Common/Cache';
|
||||
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
|
||||
import { MailFolderList } from 'View/User/MailBox/FolderList';
|
||||
|
|
@ -57,11 +56,8 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
*/
|
||||
onShow() {
|
||||
this.updateWindowTitle();
|
||||
|
||||
AppUserStore.focusedState('none');
|
||||
AppUserStore.focusedState(Scope.MessageList);
|
||||
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -71,7 +67,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
* @returns {void}
|
||||
*/
|
||||
onRoute(folderHash, page, search, messageUid) {
|
||||
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
|
||||
const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
|
||||
if (folder) {
|
||||
FolderUserStore.currentFolder(folder);
|
||||
MessagelistUserStore.page(1 > page ? 1 : page);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Scope } from 'Common/Enums';
|
||||
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
|
||||
import { keyScope, SettingsCapa } from 'Common/Globals';
|
||||
import { runSettingsViewModelHooks } from 'Common/Plugins';
|
||||
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ import { SettingsPaneUserView } from 'View/User/Settings/Pane';
|
|||
|
||||
export class SettingsUserScreen extends AbstractSettingsScreen {
|
||||
constructor() {
|
||||
super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
|
||||
super([SettingsMenuUserView, SettingsPaneUserView, SystemDropDownUserView]);
|
||||
|
||||
const views = [
|
||||
UserSettingsGeneral
|
||||
|
|
@ -67,7 +66,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
|
|||
onShow() {
|
||||
this.setSettingsTitle();
|
||||
keyScope(Scope.Settings);
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
}
|
||||
|
||||
setSettingsTitle() {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
|||
coreWarning: false,
|
||||
coreVersion: '',
|
||||
coreVersionCompare: -2,
|
||||
load1: 0,
|
||||
load5: 0,
|
||||
load15: 0,
|
||||
errorDesc: ''
|
||||
});
|
||||
this.coreChecking = ko.observable(false).extend({ throttle: 100 });
|
||||
|
|
@ -51,18 +54,21 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
|||
}
|
||||
|
||||
onBuild() {
|
||||
Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
|
||||
|
||||
// beforeShow() {
|
||||
this.coreChecking(true);
|
||||
Remote.request('AdminUpdateInfo', (iError, data) => {
|
||||
Remote.request('AdminInfo', (iError, data) => {
|
||||
this.coreChecking(false);
|
||||
if (!iError && data?.Result) {
|
||||
data = data?.Result;
|
||||
if (!iError && data) {
|
||||
this.load1(data.system.load?.[0]);
|
||||
this.load5(data.system.load?.[1]);
|
||||
this.load15(data.system.load?.[2]);
|
||||
this.phpextensions(data.php);
|
||||
this.coreReal(true);
|
||||
this.coreUpdatable(!!data.Result.updatable);
|
||||
this.coreWarning(!!data.Result.warning);
|
||||
this.coreVersion(data.Result.version || '');
|
||||
this.coreVersionCompare(data.Result.versionCompare);
|
||||
this.coreUpdatable(!!data.core.updatable);
|
||||
this.coreWarning(!!data.core.warning);
|
||||
this.coreVersion(data.core.version || '');
|
||||
this.coreVersionCompare(data.core.versionCompare);
|
||||
} else {
|
||||
this.coreReal(false);
|
||||
this.coreWarning(false);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
|||
export class AdminSettingsBranding extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
this.addSetting('Title');
|
||||
this.addSetting('LoadingDescription');
|
||||
this.addSetting('FaviconUrl');
|
||||
this.addSetting('title');
|
||||
this.addSetting('loadingDescription');
|
||||
this.addSetting('faviconUrl');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,16 +13,16 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
|||
super();
|
||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||
|
||||
this.addSetting('ContactsPdoDsn');
|
||||
this.addSetting('ContactsPdoUser');
|
||||
this.addSetting('ContactsPdoPassword');
|
||||
this.addSetting('ContactsPdoType', () => {
|
||||
this.addSetting('contactsPdoDsn');
|
||||
this.addSetting('contactsPdoUser');
|
||||
this.addSetting('contactsPdoPassword');
|
||||
this.addSetting('contactsPdoType', () => {
|
||||
this.testContactsSuccess(false);
|
||||
this.testContactsError(false);
|
||||
this.testContactsErrorMessage('');
|
||||
});
|
||||
|
||||
this.addSettings(['ContactsEnable','ContactsSync']);
|
||||
this.addSettings(['contactsEnable','contactsSync']);
|
||||
|
||||
addObservablesTo(this, {
|
||||
testing: false,
|
||||
|
|
@ -31,7 +31,7 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
|||
testContactsErrorMessage: ''
|
||||
});
|
||||
|
||||
this.addSetting('ContactsSuggestionsLimit');
|
||||
this.addSetting('contactsSuggestionsLimit');
|
||||
|
||||
const supportedTypes = SettingsGet('supportedPdoDrivers') || [],
|
||||
types = [{
|
||||
|
|
@ -92,10 +92,10 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
|||
|
||||
this.testing(false);
|
||||
}, {
|
||||
ContactsPdoType: this.contactsPdoType(),
|
||||
ContactsPdoDsn: this.contactsPdoDsn(),
|
||||
ContactsPdoUser: this.contactsPdoUser(),
|
||||
ContactsPdoPassword: this.contactsPdoPassword()
|
||||
contactsPdoType: this.contactsPdoType(),
|
||||
contactsPdoDsn: this.contactsPdoDsn(),
|
||||
contactsPdoUser: this.contactsPdoUser(),
|
||||
contactsPdoPassword: this.contactsPdoPassword()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
|||
|
||||
const aLanguagesAdmin = Settings.app('languagesAdmin');
|
||||
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
|
||||
this.languageAdmin = ko.observable(SettingsGet('LanguageAdmin'));
|
||||
this.languageAdmin = ko.observable(SettingsGet('languageAdmin'));
|
||||
|
||||
this.theme = ThemeStore.theme;
|
||||
this.themes = ThemeStore.themes;
|
||||
|
||||
this.addSettings(['AllowLanguagesOnSettings']);
|
||||
this.addSettings(['allowLanguagesOnSettings']);
|
||||
|
||||
addObservablesTo(this, {
|
||||
capaThemes: SettingsCapa('Themes'),
|
||||
|
|
@ -53,14 +53,14 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
|||
*/
|
||||
|
||||
this.attachmentLimit = ko
|
||||
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
|
||||
.observable(SettingsGet('attachmentLimit') / (1024 * 1024))
|
||||
.extend({ debounce: 500 });
|
||||
|
||||
this.addSetting('Language');
|
||||
this.addSetting('AttachmentLimit');
|
||||
this.addSetting('language');
|
||||
this.addSetting('attachmentLimit');
|
||||
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
|
||||
|
||||
this.uploadData = SettingsGet('PhpUploadSizes');
|
||||
this.uploadData = SettingsGet('phpUploadSizes');
|
||||
this.uploadDataDesc =
|
||||
(this.uploadData?.upload_max_filesize || this.uploadData?.post_max_size)
|
||||
? [
|
||||
|
|
@ -91,7 +91,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
|||
this.languageAdminTrigger(SaveSettingStatus.Saving);
|
||||
translatorReload(value, 1)
|
||||
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
|
||||
.then(() => Remote.saveSetting('LanguageAdmin', value));
|
||||
.then(() => Remote.saveSetting('languageAdmin', value));
|
||||
},
|
||||
|
||||
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
|
||||
|
|
@ -114,7 +114,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
|||
showScreenPopup(LanguagesPopupView, [
|
||||
this.languageAdmin,
|
||||
this.languagesAdmin(),
|
||||
SettingsGet('UserLanguageAdmin')
|
||||
SettingsGet('languageUsers')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
|||
export class AdminSettingsLogin extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
this.addSetting('LoginDefaultDomain');
|
||||
this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin']);
|
||||
this.addSetting('loginDefaultDomain');
|
||||
this.addSettings(['determineUserLanguage','determineUserDomain','allowLanguagesOnLogin']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import { PackageAdminStore } from 'Stores/Admin/Package';
|
||||
|
|
@ -15,7 +15,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.addSettings(['EnabledPlugins']);
|
||||
this.addSettings(['pluginsEnable']);
|
||||
|
||||
addObservablesTo(this, {
|
||||
packagesError: ''
|
||||
|
|
@ -67,7 +67,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
|
||||
if (iError) {
|
||||
this.packagesError(
|
||||
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
|
||||
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
|
||||
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
|
||||
);
|
||||
} else if (data.Result.Reload) {
|
||||
|
|
@ -113,7 +113,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
if (iError) {
|
||||
plugin.enabled(disable);
|
||||
this.packagesError(
|
||||
(Notification.UnsupportedPluginPackage === iError && data?.ErrorMessage)
|
||||
(Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage)
|
||||
? data.ErrorMessage
|
||||
: getNotification(iError)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.addSettings(['UseLocalProxyForExternalImages']);
|
||||
this.addSettings(['useLocalProxyForExternalImages']);
|
||||
|
||||
this.weakPassword = rl.app.weakPassword;
|
||||
|
||||
addObservablesTo(this, {
|
||||
adminLogin: SettingsGet('AdminLogin'),
|
||||
adminLogin: SettingsGet('adminLogin'),
|
||||
adminLoginError: false,
|
||||
adminPassword: '',
|
||||
adminPasswordNew: '',
|
||||
|
|
@ -68,7 +68,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
|||
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
||||
});
|
||||
|
||||
this.adminTOTP(SettingsGet('AdminTOTP'));
|
||||
this.adminTOTP(SettingsGet('adminTOTP'));
|
||||
|
||||
decorateKoCommands(this, {
|
||||
saveAdminUserCommand: self => self.adminLogin().trim() && self.adminPassword()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
|||
this.accounts = AccountUserStore;
|
||||
this.loading = AccountUserStore.loading;
|
||||
this.identities = IdentityUserStore;
|
||||
this.mainEmail = SettingsGet('MainEmail');
|
||||
this.mainEmail = SettingsGet('mainEmail');
|
||||
|
||||
this.accountForDeletion = ko.observable(null).askDeleteHelper();
|
||||
this.identityForDeletion = ko.observable(null).askDeleteHelper();
|
||||
|
|
@ -88,7 +88,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
|||
|
||||
accountsAndIdentitiesAfterMove() {
|
||||
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
||||
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('MainEmail')),
|
||||
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('mainEmail')),
|
||||
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { FolderMetadataKeys } from 'Common/EnumsUser';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
|
||||
import { getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
|
||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||
import { sortFolders } from 'Common/Folders';
|
||||
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
|
|
@ -18,7 +17,6 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
|||
|
||||
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||
import { loadFolders } from 'Model/FolderCollection';
|
||||
|
||||
const folderForDeletion = ko.observable(null).askDeleteHelper();
|
||||
|
||||
|
|
@ -52,52 +50,10 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
|
||||
this.folderForDeletion = folderForDeletion;
|
||||
|
||||
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'editing'] });
|
||||
|
||||
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
|
||||
SettingsUserStore.unhideKolabFolders.subscribe(value => Remote.saveSetting('UnhideKolabFolders', value));
|
||||
}
|
||||
|
||||
folderEditOnEnter(folder) {
|
||||
const nameToEdit = folder?.nameForEdit().trim();
|
||||
if (nameToEdit && folder.name() !== nameToEdit) {
|
||||
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
||||
folder: folder.fullName,
|
||||
newFolderName: nameToEdit,
|
||||
subscribe: folder.isSubscribed() ? 1 : 0
|
||||
})
|
||||
.then(data => {
|
||||
folder.name(nameToEdit/*data.name*/);
|
||||
if (folder.subFolders.length) {
|
||||
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
||||
// clearTimeout(Remote.foldersTimeout);
|
||||
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
|
||||
setTimeout(loadFolders, 500);
|
||||
// TODO: rename all subfolders with folder.delimiter to prevent reload?
|
||||
} else {
|
||||
removeFolderFromCacheList(folder.fullName);
|
||||
folder.fullName = data.Result.fullName;
|
||||
setFolder(folder);
|
||||
const parent = getFolderFromCacheList(folder.parentName);
|
||||
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
FolderUserStore.folderListError(
|
||||
getNotification(error.code, '', Notification.CantRenameFolder)
|
||||
+ '.\n' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// this.folderForEdit(null);
|
||||
folder.editing(false);
|
||||
}
|
||||
|
||||
folderEditOnEsc(folder) {
|
||||
// this.folderForEdit(null);
|
||||
folder?.editing(false);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
FolderUserStore.folderListError('');
|
||||
}
|
||||
|
|
@ -119,8 +75,8 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
&& folderToRemove.askDelete()
|
||||
) {
|
||||
if (0 < folderToRemove.totalEmails()) {
|
||||
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
// FolderUserStore.folderListError(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
||||
folderToRemove.errorMsg(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
||||
} else {
|
||||
folderForDeletion(null);
|
||||
|
||||
|
|
@ -129,7 +85,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
folder: folderToRemove.fullName
|
||||
}).then(
|
||||
() => {
|
||||
// folderToRemove.flags.push('\\nonexistent');
|
||||
// folderToRemove.attributes.push('\\nonexistent');
|
||||
folderToRemove.selectable(false);
|
||||
// folderToRemove.isSubscribed(false);
|
||||
// folderToRemove.checkable(false);
|
||||
|
|
@ -141,7 +97,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
},
|
||||
error => {
|
||||
FolderUserStore.folderListError(
|
||||
getNotification(error.code, '', Notification.CantDeleteFolder)
|
||||
getNotification(error.code, '', Notifications.CantDeleteFolder)
|
||||
+ '.\n' + error.message
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { LanguageStore } from 'Stores/Language';
|
|||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
|
@ -35,7 +34,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
|
||||
this.soundNotification = SMAudio.notifications;
|
||||
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
||||
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
|
||||
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
|
||||
|
||||
this.desktopNotification = NotificationUserStore.enabled;
|
||||
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
||||
|
|
@ -44,12 +43,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
|
||||
['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
||||
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles',
|
||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
||||
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel',
|
||||
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck'
|
||||
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck',
|
||||
'showNextMessage'
|
||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||
|
||||
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
||||
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
|
||||
|
||||
this.languageTrigger = ko.observable(SaveSettingStatus.Idle);
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
layoutTypes: () => {
|
||||
translateTrigger();
|
||||
return [
|
||||
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_NO_SPLIT') },
|
||||
{ id: 0, name: i18n('SETTINGS_GENERAL/LAYOUT_NO_SPLIT') },
|
||||
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LAYOUT_VERTICAL_SPLIT') },
|
||||
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_HORIZONTAL_SPLIT') }
|
||||
];
|
||||
|
|
@ -102,10 +102,10 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.addSetting('Layout');
|
||||
this.addSetting('MaxBlockquotesLevel');
|
||||
|
||||
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'AllowStyles',
|
||||
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'RemoveColors', 'AllowStyles',
|
||||
'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder',
|
||||
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
|
||||
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes']);
|
||||
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes', 'AllowDraftAutosave', 'showNextMessage']);
|
||||
|
||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||
this.languageTrigger(saveSettingsStep);
|
||||
|
|
@ -117,20 +117,12 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.languageTrigger(SaveSettingStatus.Saving);
|
||||
translatorReload(value)
|
||||
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
|
||||
.then(() => Remote.saveSetting('Language', value));
|
||||
.then(() => Remote.saveSetting('language', value));
|
||||
},
|
||||
|
||||
hourCycle: value =>
|
||||
Remote.saveSetting('hourCycle', value),
|
||||
|
||||
removeColors: value => {
|
||||
let dom = MessageUserStore.bodiesDom();
|
||||
if (dom) {
|
||||
dom.innerHTML = '';
|
||||
}
|
||||
Remote.saveSetting('RemoveColors', value);
|
||||
},
|
||||
|
||||
notificationSound: value => {
|
||||
Remote.saveSetting('NotificationSound', value);
|
||||
Settings.set('NotificationSound', value);
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
|||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
|
||||
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
||||
|
||||
//import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export class UserSettingsSecurity extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -46,10 +46,6 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
|||
this.canOpenPGP = SettingsCapa('OpenPGP');
|
||||
this.canGnuPG = GnuPGUserStore.isSupported();
|
||||
this.canMailvelope = !!window.mailvelope;
|
||||
|
||||
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
|
||||
|
||||
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value))
|
||||
}
|
||||
|
||||
addOpenPgpKey() {
|
||||
|
|
@ -66,5 +62,11 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
|||
* The iframe will be injected into the container identified by selector.
|
||||
*/
|
||||
window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/);
|
||||
/**
|
||||
* https://github.com/the-djmaze/snappymail/issues/973
|
||||
Remote.request('GetStoredPGPKeys', (iError, data) => {
|
||||
console.dir([iError, data]);
|
||||
});
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { i18n } from 'Common/Translator';
|
|||
import { SettingsCapa } from 'Common/Globals';
|
||||
|
||||
import { ThemeStore, convertThemeName, changeTheme } from 'Stores/Theme';
|
||||
import { addSubscribablesTo } from 'External/ko';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
|
|
@ -24,20 +25,22 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
|||
this.fontSansSerif = ThemeStore.fontSansSerif;
|
||||
this.fontSerif = ThemeStore.fontSerif;
|
||||
this.fontMono = ThemeStore.fontMono;
|
||||
ThemeStore.fontSansSerif.subscribe(value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontSansSerif: value
|
||||
});
|
||||
});
|
||||
ThemeStore.fontSerif.subscribe(value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontSerif: value
|
||||
});
|
||||
});
|
||||
ThemeStore.fontMono.subscribe(value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontMono: value
|
||||
});
|
||||
addSubscribablesTo(ThemeStore, {
|
||||
fontSansSerif: value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontSansSerif: value
|
||||
});
|
||||
},
|
||||
fontSerif: value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontSerif: value
|
||||
});
|
||||
},
|
||||
fontMono: value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontMono: value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.theme = ThemeStore.theme;
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ export const
|
|||
arr.map(item => item.toString?.() || item).join(separator),
|
||||
/*
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
let key = getKeyByValue(Notifications, code);
|
||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||
rl.i18n('NOTIFICATIONS/')
|
||||
},
|
||||
getNotification = (code, message = '', defCode = 0) => {
|
||||
code = parseInt(code, 10) || 0;
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
if (Notifications.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export class FilterPopupView extends rl.pluginPopupView {
|
|||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||
this.folderSelectList = koComputable(() =>
|
||||
folderListOptionsBuilder(
|
||||
[rl.settings.get('SieveAllowFileintoInbox') ? '' : 'INBOX'],
|
||||
[rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX'],
|
||||
[['', '']],
|
||||
item => item?.localName() || ''
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ try {
|
|||
setItem: (key, value) => {
|
||||
data[key] = ''+value; // forces the value to a string
|
||||
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
||||
+"; expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString())
|
||||
+"; path=/; samesite=strict";
|
||||
+";expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString())
|
||||
+";path=/;samesite=strict";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
1
dev/Storage/Passphrases.js
Normal file
1
dev/Storage/Passphrases.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const Passphrases = new Map();
|
||||
|
|
@ -11,8 +11,8 @@ export const LanguageStore = {
|
|||
populate: function() {
|
||||
const aLanguages = Settings.app('languages');
|
||||
this.languages(isArray(aLanguages) ? aLanguages : []);
|
||||
this.language(SettingsGet('Language'));
|
||||
this.userLanguage(SettingsGet('UserLanguage'));
|
||||
this.language(SettingsGet('language'));
|
||||
this.userLanguage(SettingsGet('userLanguage'));
|
||||
this.hourCycle(SettingsGet('hourCycle'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import ko from 'ko';
|
||||
import { $htmlCL, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
||||
import { $htmlCL, appEl, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
||||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
import { cssLink, serverRequestRaw } from 'Common/Links';
|
||||
import { SaveSettingStatus } from 'Common/Enums';
|
||||
import { addSubscribablesTo } from 'External/ko';
|
||||
|
||||
let __themeTimer = 0;
|
||||
|
||||
export const
|
||||
appEl = () => elementById('rl-app'),
|
||||
// Also see Styles/_Values.less @maxMobileWidth
|
||||
isMobile = matchMedia('(max-width: 799px)'),
|
||||
|
||||
ThemeStore = {
|
||||
theme: ko.observable(''),
|
||||
|
|
@ -17,7 +19,7 @@ export const
|
|||
fontSansSerif: ko.observable(''),
|
||||
fontSerif: ko.observable(''),
|
||||
fontMono: ko.observable(''),
|
||||
isMobile: ko.observable($htmlCL.contains('rl-mobile')),
|
||||
isMobile: ko.observable(false),
|
||||
|
||||
populate: () => {
|
||||
const themes = Settings.app('themes');
|
||||
|
|
@ -25,8 +27,8 @@ export const
|
|||
ThemeStore.themes(isArray(themes) ? themes : []);
|
||||
ThemeStore.theme(SettingsGet('Theme'));
|
||||
if (!ThemeStore.isMobile()) {
|
||||
ThemeStore.userBackgroundName(SettingsGet('UserBackgroundName'));
|
||||
ThemeStore.userBackgroundHash(SettingsGet('UserBackgroundHash'));
|
||||
ThemeStore.userBackgroundName(SettingsGet('userBackgroundName'));
|
||||
ThemeStore.userBackgroundHash(SettingsGet('userBackgroundHash'));
|
||||
}
|
||||
ThemeStore.fontSansSerif(SettingsGet('fontSansSerif'));
|
||||
ThemeStore.fontSerif(SettingsGet('fontSerif'));
|
||||
|
|
@ -62,35 +64,44 @@ export const
|
|||
|
||||
convertThemeName = theme => theme.replace(/@[a-z]+$/, '').replace(/([A-Z])/g, ' $1').trim();
|
||||
|
||||
ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value));
|
||||
addSubscribablesTo(ThemeStore, {
|
||||
fontSansSerif: value => {
|
||||
if (null != value) {
|
||||
let cl = appEl.classList;
|
||||
cl.forEach(name => {
|
||||
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
||||
cl.remove(name);
|
||||
}
|
||||
});
|
||||
value && cl.add('font'+value);
|
||||
}
|
||||
},
|
||||
|
||||
ThemeStore.fontSansSerif.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = appEl().classList;
|
||||
cl.forEach(name => {
|
||||
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
||||
cl.remove(name);
|
||||
}
|
||||
});
|
||||
value && cl.add('font'+value);
|
||||
}
|
||||
});
|
||||
ThemeStore.fontSerif.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = appEl().classList;
|
||||
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
||||
value && cl.add('fontSerif'+value);
|
||||
}
|
||||
});
|
||||
ThemeStore.fontMono.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = appEl().classList;
|
||||
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
||||
value && cl.add('fontMono'+value);
|
||||
fontSerif: value => {
|
||||
if (null != value) {
|
||||
let cl = appEl.classList;
|
||||
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
||||
value && cl.add('fontSerif'+value);
|
||||
}
|
||||
},
|
||||
|
||||
fontMono: value => {
|
||||
if (null != value) {
|
||||
let cl = appEl.classList;
|
||||
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
||||
value && cl.add('fontMono'+value);
|
||||
}
|
||||
},
|
||||
|
||||
userBackgroundHash: value => {
|
||||
appEl.classList.toggle('UserBackground', !!value);
|
||||
appEl.style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
|
||||
}
|
||||
});
|
||||
|
||||
ThemeStore.userBackgroundHash.subscribe(value => {
|
||||
appEl().classList.toggle('UserBackground', !!value);
|
||||
appEl().style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
|
||||
});
|
||||
isMobile.onchange = e => {
|
||||
ThemeStore.isMobile(e.matches);
|
||||
$htmlCL.toggle('rl-mobile', e.matches);
|
||||
leftPanelDisabled(e.matches);
|
||||
};
|
||||
isMobile.onchange(isMobile);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { keyScope, leftPanelDisabled, SettingsGet, elementById } from 'Common/Globals';
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { arePopupsVisible } from 'Knoin/Knoin';
|
||||
|
||||
export const AppUserStore = {
|
||||
allowContacts: () => !!SettingsGet('ContactsIsAllowed')
|
||||
allowContacts: () => !!SettingsGet('contactsAllowed')
|
||||
};
|
||||
|
||||
addObservablesTo(AppUserStore, {
|
||||
|
|
@ -15,10 +16,9 @@ addObservablesTo(AppUserStore, {
|
|||
AppUserStore.focusedState.subscribe(value => {
|
||||
['FolderList','MessageList','MessageView'].forEach(name => {
|
||||
if (name === value) {
|
||||
keyScope(value);
|
||||
arePopupsVisible() || keyScope(value);
|
||||
ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
|
||||
}
|
||||
let dom = elementById('V-Mail'+name);
|
||||
dom?.classList.toggle('focused', name === value);
|
||||
elementById('V-Mail'+name).classList.toggle('focused', name === value);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import ko from 'ko';
|
|||
import { SettingsGet } from 'Common/Globals';
|
||||
import { koComputable, addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
|
||||
export const ContactUserStore = koArrayWithDestroy();
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ ContactUserStore.sync = fResultFunc => {
|
|||
} catch (e) {
|
||||
ContactUserStore.syncing(false);
|
||||
console.error(e);
|
||||
fResultFunc?.(Notification.UnknownError);
|
||||
fResultFunc?.(Notifications.UnknownError);
|
||||
}
|
||||
}, 'ContactsSync');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,11 +199,11 @@ FolderUserStore = new class {
|
|||
|
||||
saveSystemFolders(folders) {
|
||||
folders = folders || {
|
||||
Sent: FolderUserStore.sentFolder(),
|
||||
Drafts: FolderUserStore.draftsFolder(),
|
||||
Spam: FolderUserStore.spamFolder(),
|
||||
Trash: FolderUserStore.trashFolder(),
|
||||
Archive: FolderUserStore.archiveFolder()
|
||||
sent: FolderUserStore.sentFolder(),
|
||||
drafts: FolderUserStore.draftsFolder(),
|
||||
junk: FolderUserStore.spamFolder(),
|
||||
trash: FolderUserStore.trashFolder(),
|
||||
archive: FolderUserStore.archiveFolder()
|
||||
};
|
||||
forEachObjectEntry(folders, (k,v)=>Settings.set(k+'Folder',v));
|
||||
rl.app.Remote.request('SystemFoldersUpdate', null, folders);
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
|||
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
||||
import { AskPopupView } from 'View/Popup/Ask';
|
||||
|
||||
import { Passphrases } from 'Storage/Passphrases';
|
||||
|
||||
const
|
||||
passphrases = new Map(),
|
||||
askPassphrase = async (privateKey, btnTxt = 'LABEL_SIGN') => {
|
||||
const key = privateKey.id,
|
||||
pass = passphrases.has(key)
|
||||
? {password:passphrases.get(key), remember:false}
|
||||
pass = Passphrases.has(key)
|
||||
? {password:Passphrases.get(key), remember:false}
|
||||
: await AskPopupView.password('GnuPG key<br>' + key + ' ' + privateKey.emails[0], 'OPENPGP/'+btnTxt);
|
||||
pass && pass.remember && passphrases.set(key, pass.password);
|
||||
pass && pass.remember && Passphrases.set(key, pass.password);
|
||||
return pass.password;
|
||||
},
|
||||
|
||||
|
|
@ -83,7 +84,7 @@ export const GnuPGUserStore = new class {
|
|||
key.armor = oData.Result;
|
||||
showScreenPopup(OpenPgpKeyPopupView, [key]);
|
||||
} else {
|
||||
passphrases.delete(key.id);
|
||||
Passphrases.delete(key.id);
|
||||
}
|
||||
}, {
|
||||
keyId: key.id,
|
||||
|
|
@ -198,7 +199,7 @@ export const GnuPGUserStore = new class {
|
|||
if (result?.Result && false !== result.Result.data) {
|
||||
return result.Result;
|
||||
}
|
||||
passphrases.delete(key.id);
|
||||
Passphrases.delete(key.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,25 +30,21 @@ export const MessageUserStore = new class {
|
|||
AppUserStore.focusedState(Scope.MessageList);
|
||||
exitFullscreen();
|
||||
}
|
||||
this.hideMessageBodies();
|
||||
[...(this.bodiesDom()?.children || [])].forEach(el => el.hidden = true);
|
||||
},
|
||||
});
|
||||
|
||||
this.purgeMessageBodyCache = this.purgeMessageBodyCache.throttle(30000);
|
||||
this.purgeCache = this.purgeCache.throttle(30000);
|
||||
}
|
||||
|
||||
purgeMessageBodyCache() {
|
||||
const messagesDom = this.bodiesDom(),
|
||||
children = messagesDom?.children;
|
||||
if (children) {
|
||||
while (15 < children.length) {
|
||||
children[0].remove();
|
||||
purgeCache(all) {
|
||||
const children = this.bodiesDom()?.children || [];
|
||||
let i = Math.max(0, children.length - (all ? 0 : 15));
|
||||
while (i--) {
|
||||
children[i].remove();
|
||||
if (children[i].message) {
|
||||
children[i].message.body = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideMessageBodies() {
|
||||
const messagesDom = this.bodiesDom();
|
||||
messagesDom && Array.from(messagesDom.children).forEach(el => el.hidden = true);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { koComputable, addObservablesTo, addComputablesTo } from 'External/ko';
|
||||
|
||||
import { SMAudio } from 'Common/Audio';
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { MessageSetAction } from 'Common/EnumsUser';
|
||||
import { $htmlCL } from 'Common/Globals';
|
||||
import { arrayLength, pInt, pString } from 'Common/Utils';
|
||||
import { $htmlCL, fireEvent } from 'Common/Globals';
|
||||
import { arrayLength, pString } from 'Common/Utils';
|
||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||
|
||||
import {
|
||||
getFolderInboxName,
|
||||
getFolderFromCacheList,
|
||||
setFolderHash,
|
||||
MessageFlagsCache
|
||||
setFolderETag
|
||||
} from 'Common/Cache';
|
||||
|
||||
import { mailBox } from 'Common/Links';
|
||||
|
|
@ -174,7 +173,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
|||
let iOffset = (MessagelistUserStore.page() - 1) * SettingsUserStore.messagesPerPage();
|
||||
|
||||
if (bDropCurrentFolderCache) {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||
}
|
||||
|
||||
if (bDropPagePosition) {
|
||||
|
|
@ -198,7 +197,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
|||
let error = '';
|
||||
if (iError) {
|
||||
error = getNotification(iError);
|
||||
if (Notification.RequestAborted !== iError) {
|
||||
if (Notifications.RequestAborted !== iError) {
|
||||
MessagelistUserStore([]);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -207,27 +206,24 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
|||
error = '';
|
||||
|
||||
const
|
||||
folder = getFolderFromCacheList(collection.folder),
|
||||
folderInfo = collection.folderInfo;
|
||||
folderInfo = collection.folder,
|
||||
folder = getFolderFromCacheList(folderInfo.name);
|
||||
collection.folder = folderInfo.name;
|
||||
if (folder && !bCached) {
|
||||
// folder.revivePropertiesFromJson(result);
|
||||
folder.expires = Date.now();
|
||||
folder.uidNext = folderInfo.uidNext;
|
||||
folder.hash = collection.folderHash;
|
||||
folder.etag = folderInfo.etag;
|
||||
|
||||
if (null != folderInfo.totalEmails) {
|
||||
folder.totalEmails(folderInfo.totalEmails);
|
||||
}
|
||||
|
||||
if (null != folderInfo.unreadEmails) {
|
||||
if (pInt(folder.unreadEmails()) !== pInt(folderInfo.unreadEmails)) {
|
||||
MessageFlagsCache.clearFolder(folder.fullName);
|
||||
}
|
||||
folder.unreadEmails(folderInfo.unreadEmails);
|
||||
}
|
||||
|
||||
folder.flags(folderInfo.flags);
|
||||
let flags = folderInfo.permanentFlags;
|
||||
let flags = folderInfo.permanentFlags || [];
|
||||
if (flags.includes('\\*')) {
|
||||
let i = 6;
|
||||
while (--i) {
|
||||
|
|
@ -251,14 +247,14 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
|||
MessagelistUserStore.threadUid(collection.threadUid);
|
||||
|
||||
MessagelistUserStore.endHash(
|
||||
collection.folder +
|
||||
folderInfo.name +
|
||||
'|' + collection.search +
|
||||
'|' + MessagelistUserStore.threadUid() +
|
||||
'|' + MessagelistUserStore.page()
|
||||
);
|
||||
MessagelistUserStore.endThreadUid(collection.threadUid);
|
||||
const message = MessageUserStore.message();
|
||||
if (message && collection.folder !== message.folder) {
|
||||
if (message && folderInfo.name !== message.folder) {
|
||||
MessageUserStore.message(null);
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +276,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
|||
} else {
|
||||
MessagelistUserStore.count(0);
|
||||
MessagelistUserStore([]);
|
||||
error = getNotification(Notification.CantGetMessageList);
|
||||
error = getNotification(Notifications.CantGetMessageList);
|
||||
}
|
||||
MessagelistUserStore.error(error);
|
||||
}
|
||||
|
|
@ -306,22 +302,38 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
|
|||
messages = messages || MessagelistUserStore.listChecked();
|
||||
|
||||
let folder,
|
||||
alreadyUnread = 0,
|
||||
rootUids = messages.map(oMessage => oMessage?.uid).validUnique(),
|
||||
length = rootUids.length;
|
||||
rootUids = [],
|
||||
length;
|
||||
|
||||
if (iSetAction == MessageSetAction.SetSeen) {
|
||||
messages.forEach(oMessage =>
|
||||
oMessage.isUnseen() && rootUids.push(oMessage.uid) && oMessage.flags.push('\\seen')
|
||||
);
|
||||
} else if (iSetAction == MessageSetAction.UnsetSeen) {
|
||||
messages.forEach(oMessage =>
|
||||
!oMessage.isUnseen() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\seen')
|
||||
);
|
||||
} else if (iSetAction == MessageSetAction.SetFlag) {
|
||||
messages.forEach(oMessage =>
|
||||
!oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.push('\\flagged')
|
||||
);
|
||||
} else if (iSetAction == MessageSetAction.UnsetFlag) {
|
||||
messages.forEach(oMessage =>
|
||||
oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\flagged')
|
||||
);
|
||||
}
|
||||
rootUids = rootUids.validUnique();
|
||||
length = rootUids.length;
|
||||
|
||||
if (sFolderFullName && length) {
|
||||
rootUids.forEach(sSubUid =>
|
||||
alreadyUnread += MessageFlagsCache.storeBySetAction(sFolderFullName, sSubUid, iSetAction)
|
||||
);
|
||||
switch (iSetAction) {
|
||||
case MessageSetAction.SetSeen:
|
||||
length = 0;
|
||||
length = -length;
|
||||
// fallthrough is intentionally
|
||||
case MessageSetAction.UnsetSeen:
|
||||
folder = getFolderFromCacheList(sFolderFullName);
|
||||
if (folder) {
|
||||
folder.unreadEmails(folder.unreadEmails() - alreadyUnread + length);
|
||||
folder.unreadEmails(Math.max(0, folder.unreadEmails() + length));
|
||||
}
|
||||
Remote.request('MessageSetSeen', null, {
|
||||
folder: sFolderFullName,
|
||||
|
|
@ -340,8 +352,6 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
|
|||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -370,22 +380,20 @@ MessagelistUserStore.removeMessagesFromList = (
|
|||
messages.forEach(item => item?.isUnseen() && ++unseenCount);
|
||||
|
||||
if (fromFolder) {
|
||||
fromFolder.hash = '';
|
||||
fromFolder.etag = '';
|
||||
if (!copy) {
|
||||
fromFolder.totalEmails(
|
||||
0 <= fromFolder.totalEmails() - oUids.size ? fromFolder.totalEmails() - oUids.size : 0
|
||||
);
|
||||
|
||||
if (0 < unseenCount) {
|
||||
fromFolder.unreadEmails(
|
||||
0 <= fromFolder.unreadEmails() - unseenCount ? fromFolder.unreadEmails() - unseenCount : 0
|
||||
);
|
||||
fromFolder.unreadEmails(Math.max(0, fromFolder.unreadEmails() - unseenCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toFolder) {
|
||||
toFolder.hash = '';
|
||||
toFolder.etag = '';
|
||||
|
||||
if (trashFolder === toFolder.fullName || spamFolder === toFolder.fullName) {
|
||||
unseenCount = 0;
|
||||
|
|
@ -405,6 +413,18 @@ MessagelistUserStore.removeMessagesFromList = (
|
|||
} else {
|
||||
MessagelistUserStore.isIncomplete(true);
|
||||
|
||||
// Select next email https://github.com/the-djmaze/snappymail/issues/968
|
||||
if (currentMessage && 1 == messages.length && SettingsUserStore.showNextMessage()) {
|
||||
let next = MessagelistUserStore.indexOf(currentMessage) + 1;
|
||||
if (0 < next && (next = MessagelistUserStore()[next])) {
|
||||
currentMessage = null;
|
||||
fireEvent('mailbox.message.show', {
|
||||
folder: next.folder,
|
||||
uid: next.uid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
messages.forEach(item => {
|
||||
if (currentMessage && currentMessage.hash === item.hash) {
|
||||
currentMessage = null;
|
||||
|
|
@ -455,9 +475,4 @@ MessagelistUserStore.removeMessagesFromList = (
|
|||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage = () => {
|
||||
MessagelistUserStore.forEach(message => MessageFlagsCache.initMessage(message));
|
||||
MessageFlagsCache.initMessage(MessageUserStore.message());
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import * as Links from 'Common/Links';
|
||||
import { staticLink } from 'Common/Links';
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
import { fireEvent } from 'Common/Globals';
|
||||
|
||||
|
|
@ -22,15 +22,19 @@ let DesktopNotifications = false,
|
|||
WorkerNotifications = navigator.serviceWorker;
|
||||
|
||||
// Are Notifications supported in the service worker?
|
||||
if (WorkerNotifications && ServiceWorkerRegistration && ServiceWorkerRegistration.prototype.showNotification) {
|
||||
/* Listen for close requests from the ServiceWorker */
|
||||
WorkerNotifications.addEventListener('message', event => {
|
||||
const obj = JSON.parse(event.data);
|
||||
'notificationclick' === obj?.action && dispatchMessage(obj.data);
|
||||
});
|
||||
if (WorkerNotifications) {
|
||||
if (ServiceWorkerRegistration && ServiceWorkerRegistration.prototype.showNotification) {
|
||||
/* Listen for close requests from the ServiceWorker */
|
||||
WorkerNotifications.addEventListener('message', event => {
|
||||
const obj = JSON.parse(event.data);
|
||||
'notificationclick' === obj?.action && dispatchMessage(obj.data);
|
||||
});
|
||||
} else {
|
||||
console.log('ServiceWorkerRegistration.showNotification undefined');
|
||||
WorkerNotifications = null;
|
||||
}
|
||||
} else {
|
||||
WorkerNotifications = null;
|
||||
console.log('ServiceWorker Notifications not supported');
|
||||
console.log('ServiceWorker undefined');
|
||||
}
|
||||
|
||||
export const NotificationUserStore = new class {
|
||||
|
|
@ -57,7 +61,7 @@ export const NotificationUserStore = new class {
|
|||
if (DesktopNotifications && NotificationsGranted()) {
|
||||
const options = {
|
||||
body: text,
|
||||
icon: imageSrc || Links.staticLink('images/icon-message-notification.png'),
|
||||
icon: imageSrc || staticLink('images/icon-message-notification.png'),
|
||||
data: messageData
|
||||
};
|
||||
if (messageData?.uid) {
|
||||
|
|
@ -65,8 +69,7 @@ export const NotificationUserStore = new class {
|
|||
}
|
||||
if (WorkerNotifications) {
|
||||
// Service-Worker-Allowed HTTP header to allow the scope.
|
||||
WorkerNotifications.register('/serviceworker.js')
|
||||
// WorkerNotifications.register(Links.staticLink('js/serviceworker.js'), {scope:'/'})
|
||||
WorkerNotifications.register(staticLink('js/serviceworker.js'), {scope:'/'})
|
||||
.then(() =>
|
||||
WorkerNotifications.ready.then(registration =>
|
||||
/* Show the notification */
|
||||
|
|
@ -80,7 +83,10 @@ export const NotificationUserStore = new class {
|
|||
)
|
||||
)
|
||||
)
|
||||
.catch(e => console.error(e));
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
WorkerNotifications = null;
|
||||
});
|
||||
} else {
|
||||
const notification = new HTML5Notification(title, options);
|
||||
notification.show?.();
|
||||
|
|
|
|||
|
|
@ -12,22 +12,23 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
|||
import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
||||
import { AskPopupView } from 'View/Popup/Ask';
|
||||
|
||||
import { Passphrases } from 'Storage/Passphrases';
|
||||
|
||||
const
|
||||
findOpenPGPKey = (keys, query/*, sign*/) =>
|
||||
keys.find(key =>
|
||||
key.emails.includes(query) || query == key.id || query == key.fingerprint
|
||||
),
|
||||
|
||||
passphrases = new Map(),
|
||||
decryptKey = async (privateKey, btnTxt = 'LABEL_SIGN') => {
|
||||
if (privateKey.key.isDecrypted()) {
|
||||
return privateKey.key;
|
||||
}
|
||||
const key = privateKey.id,
|
||||
pass = passphrases.has(key)
|
||||
? {password:passphrases.get(key), remember:false}
|
||||
pass = Passphrases.has(key)
|
||||
? {password:Passphrases.get(key), remember:false}
|
||||
: await AskPopupView.password(
|
||||
'OpenPGP.js key<br>' + privateKey.id + ' ' + privateKey.emails[0],
|
||||
'OpenPGP.js key<br>' + key + ' ' + privateKey.emails[0],
|
||||
'OPENPGP/'+btnTxt
|
||||
);
|
||||
if (pass) {
|
||||
|
|
@ -36,7 +37,7 @@ const
|
|||
privateKey: privateKey.key,
|
||||
passphrase
|
||||
});
|
||||
result && pass.remember && passphrases.set(key, passphrase);
|
||||
result && pass.remember && Passphrases.set(key, passphrase);
|
||||
return result;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,17 +9,15 @@ import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
|||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||
|
||||
// https://mailvelope.github.io/mailvelope/Keyring.html
|
||||
let mailvelopeKeyring = null;
|
||||
|
||||
export const
|
||||
BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----',
|
||||
// BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----',
|
||||
// BEGIN_PGP_SIGNED = '-----BEGIN PGP SIGNED MESSAGE-----',
|
||||
|
||||
PgpUserStore = new class {
|
||||
constructor() {
|
||||
// https://mailvelope.github.io/mailvelope/Keyring.html
|
||||
this.mailvelopeKeyring = null;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (SettingsCapa('OpenPGP') && window.crypto && crypto.getRandomValues) {
|
||||
rl.loadScript(SettingsGet('StaticLibsJs').replace('/libs.', '/openpgp.'))
|
||||
|
|
@ -38,7 +36,7 @@ export const
|
|||
identifier = identifier || SettingsGet('Email');
|
||||
if (window.mailvelope) {
|
||||
const fn = keyring => {
|
||||
this.mailvelopeKeyring = keyring;
|
||||
mailvelopeKeyring = keyring;
|
||||
console.log('mailvelope ready');
|
||||
};
|
||||
mailvelope.getKeyring().then(fn, err => {
|
||||
|
|
@ -79,8 +77,7 @@ export const
|
|||
|
||||
async mailvelopeHasPublicKeyForEmails(recipients) {
|
||||
const
|
||||
keyring = this.mailvelopeKeyring,
|
||||
mailvelope = keyring && await keyring.validKeyForAddress(recipients)
|
||||
mailvelope = mailvelopeKeyring && await mailvelopeKeyring.validKeyForAddress(recipients)
|
||||
/*.then(LookupResult => Object.entries(LookupResult))*/,
|
||||
entries = mailvelope && Object.entries(mailvelope);
|
||||
return entries && entries.filter(value => value[1]).length === recipients.length;
|
||||
|
|
@ -104,8 +101,7 @@ export const
|
|||
}
|
||||
|
||||
async getMailvelopePrivateKeyFor(email/*, sign*/) {
|
||||
let keyring = this.mailvelopeKeyring;
|
||||
if (keyring && await keyring.hasPrivateKey({email:email})) {
|
||||
if (mailvelopeKeyring && await mailvelopeKeyring.hasPrivateKey({email:email})) {
|
||||
return ['mailvelope', email];
|
||||
}
|
||||
return false;
|
||||
|
|
@ -145,46 +141,48 @@ export const
|
|||
}
|
||||
|
||||
// Try Mailvelope (does not support inline images)
|
||||
try {
|
||||
let emails = [...message.from,...message.to,...message.cc].validUnique(),
|
||||
i = emails.length;
|
||||
while (i--) {
|
||||
if (await this.getMailvelopePrivateKeyFor(emails[i].email)) {
|
||||
/**
|
||||
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
|
||||
* Creates an iframe to display an encrypted form
|
||||
*/
|
||||
// mailvelope.createEncryptedFormContainer('#mailvelope-form');
|
||||
/**
|
||||
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
|
||||
* Creates an iframe to display the decrypted content of the encrypted mail.
|
||||
*/
|
||||
const body = message.body;
|
||||
body.textContent = '';
|
||||
let result = await mailvelope.createDisplayContainer(
|
||||
'#'+body.id,
|
||||
armoredText,
|
||||
this.mailvelopeKeyring,
|
||||
{
|
||||
senderAddress: sender
|
||||
// emails[i].email
|
||||
}
|
||||
);
|
||||
if (result) {
|
||||
if (result.error?.message) {
|
||||
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
|
||||
alert(result.error.code + ': ' + result.error.message);
|
||||
if (mailvelopeKeyring) {
|
||||
try {
|
||||
let emails = [...message.from,...message.to,...message.cc].validUnique(),
|
||||
i = emails.length;
|
||||
while (i--) {
|
||||
if (await this.getMailvelopePrivateKeyFor(emails[i].email)) {
|
||||
/**
|
||||
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
|
||||
* Creates an iframe to display an encrypted form
|
||||
*/
|
||||
// mailvelope.createEncryptedFormContainer('#mailvelope-form');
|
||||
/**
|
||||
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
|
||||
* Creates an iframe to display the decrypted content of the encrypted mail.
|
||||
*/
|
||||
const body = message.body;
|
||||
body.textContent = '';
|
||||
let result = await mailvelope.createDisplayContainer(
|
||||
'#'+body.id,
|
||||
armoredText,
|
||||
mailvelopeKeyring,
|
||||
{
|
||||
senderAddress: sender
|
||||
// emails[i].email
|
||||
}
|
||||
);
|
||||
if (result) {
|
||||
if (result.error?.message) {
|
||||
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
|
||||
alert(result.error.code + ': ' + result.error.message);
|
||||
}
|
||||
} else {
|
||||
body.classList.add('mailvelope');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
body.classList.add('mailvelope');
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// Now try GnuPG
|
||||
|
|
@ -217,19 +215,19 @@ export const
|
|||
* So far this is only the autocrypt header.
|
||||
*/
|
||||
/*
|
||||
this.mailvelopeKeyring.additionalHeadersForOutgoingEmail(headers)
|
||||
this.mailvelopeKeyring.addSyncHandler(syncHandlerObj)
|
||||
this.mailvelopeKeyring.createKeyBackupContainer(selector, options)
|
||||
this.mailvelopeKeyring.createKeyGenContainer(selector, {
|
||||
mailvelopeKeyring.additionalHeadersForOutgoingEmail(headers)
|
||||
mailvelopeKeyring.addSyncHandler(syncHandlerObj)
|
||||
mailvelopeKeyring.createKeyBackupContainer(selector, options)
|
||||
mailvelopeKeyring.createKeyGenContainer(selector, {
|
||||
// userIds: [],
|
||||
keySize: 4096
|
||||
})
|
||||
|
||||
this.mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
|
||||
this.mailvelopeKeyring.importPublicKey(armored)
|
||||
mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
|
||||
mailvelopeKeyring.importPublicKey(armored)
|
||||
|
||||
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
|
||||
this.mailvelopeKeyring.addSyncHandler({
|
||||
mailvelopeKeyring.addSyncHandler({
|
||||
uploadSync
|
||||
downloadSync
|
||||
backup
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const SettingsUserStore = new class {
|
|||
simpleAttachmentsList: 0,
|
||||
useCheckboxesInList: 1,
|
||||
listGrouped: 0,
|
||||
showNextMessage: 0,
|
||||
allowDraftAutosave: 1,
|
||||
useThreads: 0,
|
||||
replySameFolder: 0,
|
||||
|
|
@ -49,13 +50,12 @@ export const SettingsUserStore = new class {
|
|||
|
||||
self.init();
|
||||
|
||||
self.usePreviewPane = koComputable(() => Layout.NoPreview !== self.layout() && !ThemeStore.isMobile());
|
||||
self.usePreviewPane = koComputable(() => self.layout() && !ThemeStore.isMobile());
|
||||
|
||||
const toggleLayout = () => {
|
||||
const value = ThemeStore.isMobile() ? Layout.NoPreview : self.layout();
|
||||
$htmlCL.toggle('rl-no-preview-pane', Layout.NoPreview === value);
|
||||
$htmlCL.toggle('rl-side-preview-pane', Layout.SidePreview === value);
|
||||
$htmlCL.toggle('rl-bottom-preview-pane', Layout.BottomPreview === value);
|
||||
const value = ThemeStore.isMobile() ? 0 : self.layout();
|
||||
$htmlCL.toggle('sm-msgView-side', Layout.SidePreview === value);
|
||||
$htmlCL.toggle('sm-msgView-bottom', Layout.BottomPreview === value);
|
||||
fireEvent('rl-layout', value);
|
||||
};
|
||||
self.layout.subscribe(toggleLayout);
|
||||
|
|
@ -65,7 +65,7 @@ export const SettingsUserStore = new class {
|
|||
let iAutoLogoutTimer;
|
||||
self.delayLogout = (() => {
|
||||
clearTimeout(iAutoLogoutTimer);
|
||||
if (0 < self.autoLogout() && !SettingsGet('AccountSignMe')) {
|
||||
if (0 < self.autoLogout() && !SettingsGet('accountSignMe')) {
|
||||
iAutoLogoutTimer = setTimeout(
|
||||
rl.app.logout,
|
||||
self.autoLogout() * 60000
|
||||
|
|
@ -96,6 +96,7 @@ export const SettingsUserStore = new class {
|
|||
self.simpleAttachmentsList(SettingsGet('simpleAttachmentsList'));
|
||||
self.useCheckboxesInList(SettingsGet('UseCheckboxesInList'));
|
||||
self.listGrouped(SettingsGet('listGrouped'));
|
||||
self.showNextMessage(SettingsGet('showNextMessage'));
|
||||
self.allowDraftAutosave(SettingsGet('AllowDraftAutosave'));
|
||||
self.useThreads(SettingsGet('UseThreads'));
|
||||
self.replySameFolder(SettingsGet('ReplySameFolder'));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
:root {
|
||||
--fontSans: Verdana, Geneva, "Bitstream Vera Sans", "DejaVu LGC Sans", Arial, sans-serif;
|
||||
--fontSerif: "Nimbus Roman No9 L", "Times New Roman", Times, FreeSerif, serif;
|
||||
--fontMono: "Liberation Mono", Monaco, Menlo, Consolas, "Courier New", FreeMono, Courier, monospace;
|
||||
--fontSans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--fontSerif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--fontMono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
#rl-app.fontArial {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@
|
|||
height: calc(100% - 50px - @rlLowMargin);
|
||||
margin: 50px @rlLowMargin 0 0;
|
||||
|
||||
background-color: var(--panel-bg-clr, #fff);
|
||||
border: 1px solid var(--border-color, #aaa);
|
||||
border-radius: var(--border-radius, 5px);
|
||||
box-shadow: var(--smMainShadow);
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
> .b-toolbar {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
|
|
@ -56,28 +64,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.rl-mobile #V-AdminPane {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.btn.btn-logout {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
#rl-settings-subscreen {
|
||||
background-color: var(--settings-bg-clr, #fff);
|
||||
border: 1px solid var(--border-color, #aaa);
|
||||
border-radius: var(--border-radius, 5px);
|
||||
box-shadow: var(--smMainShadow);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* desktop */
|
||||
@media screen and (min-width: 1000px) {
|
||||
@media screen and (min-width: @maxMobileWidth + 1px) {
|
||||
.toggleLeft {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -90,16 +83,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* mobile and tablet */
|
||||
@media screen and (max-width: 999px) {
|
||||
#rl-settings-subscreen {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
#rl-right {
|
||||
min-width: calc(100% - @rlLowMargin);
|
||||
}
|
||||
|
||||
#V-AdminPane {
|
||||
margin-right: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
html.rl-left-panel-disabled {
|
||||
#rl-left {
|
||||
width: 0;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 733px) {
|
||||
@media screen and (max-width: 480px) {
|
||||
#V-PopupsLanguages label {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@
|
|||
z-index: 5;
|
||||
}
|
||||
|
||||
@glass-color: #fff;
|
||||
@glass-error-color: #f76260;
|
||||
@glass-m-color: rgba(255, 255, 255, .8);
|
||||
|
||||
.LoginView {
|
||||
|
||||
.descWrapper {
|
||||
|
|
@ -29,18 +25,18 @@
|
|||
border: var(--login-border, 1px solid rgba(255, 255, 255, .2));
|
||||
border-radius: var(--login-border-radius, 7px);
|
||||
box-shadow: var(--login-box-shadow);
|
||||
color: var(--login-color, @glass-m-color);
|
||||
color: var(--login-color, rgba(255, 255, 255, .8));
|
||||
margin: 0;
|
||||
padding: 40px 40px 20px 40px;
|
||||
position: relative;
|
||||
|
||||
* {
|
||||
color: var(--login-color, @glass-m-color);
|
||||
color: var(--login-color, rgba(255, 255, 255, .8));
|
||||
}
|
||||
|
||||
.controls.error * {
|
||||
color: @glass-error-color;
|
||||
border-color: @glass-error-color;
|
||||
color: #f76260;
|
||||
border-color: #f76260;
|
||||
}
|
||||
|
||||
&.submitting > * {
|
||||
|
|
@ -77,17 +73,17 @@
|
|||
padding-right: 30px;
|
||||
|
||||
&:placeholder {
|
||||
color: @glass-color;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:focus, &:hover {
|
||||
border-color: @glass-color;
|
||||
border-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
input, .btn {
|
||||
background: none;
|
||||
border: 1px solid @glass-m-color;
|
||||
border: 1px solid rgba(255, 255, 255, .8);
|
||||
}
|
||||
|
||||
.fontastic + input {
|
||||
|
|
@ -107,7 +103,7 @@
|
|||
font-size: 13px;
|
||||
|
||||
&:hover, &:active {
|
||||
border-color: @glass-color;
|
||||
border-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ html.list-loading body {
|
|||
cursor: progress;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
@media screen and (min-width: @maxMobileWidth + 1px) {
|
||||
#rl-app {
|
||||
background-image: var(--main-bg-image);
|
||||
background-size: var(--main-bg-size);
|
||||
|
|
@ -73,13 +73,14 @@ dialog:not(.animate) {
|
|||
|
||||
/**
|
||||
* https://github.com/the-djmaze/snappymail/issues/686
|
||||
@media screen and (max-width: 999px) {
|
||||
*/
|
||||
.rl-mobile dialog {
|
||||
margin: 0 auto;
|
||||
/* max-height: calc(100vh - 86px);*/
|
||||
/* max-height: 100dvh;*/
|
||||
width: 100%;
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
dialog {
|
||||
margin: 0 auto;
|
||||
/* max-height: calc(100vh - 86px);*/
|
||||
/* max-height: 100dvh;*/
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
dialog > header {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
#rl-settings-subscreen,
|
||||
.g-ui-user-select-none {
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
|
|
@ -159,20 +158,7 @@ summary.legend {
|
|||
padding: 0 9px;
|
||||
}
|
||||
|
||||
.alert a:not(.close) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.alert.alert-info a:not(.close) {
|
||||
color: var(--info-clr, #3a87ad);
|
||||
}
|
||||
|
||||
.alert.alert-error a:not(.close) {
|
||||
color: var(--error-clr, #b94a48);
|
||||
}
|
||||
|
||||
/* mobile and tablet */
|
||||
@media screen and (max-width: 999px) {
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
.form-horizontal {
|
||||
.control-group {
|
||||
> label {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
margin-right:1em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 410px) {
|
||||
@media screen and (max-width: 480px) {
|
||||
#advancedsearchform label {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
100% {background-position: 60px 0;}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
@media screen and (min-width: @maxMobileWidth + 1px) {
|
||||
|
||||
.b-folders li .anim-action-class {
|
||||
animation: highlight-folder-row 0.5s linear;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
padding-top: 50px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
opacity: 0.5;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
|
|
@ -131,18 +131,20 @@
|
|||
top: 5px;
|
||||
bottom: 5px;
|
||||
|
||||
border: 2px #777 dashed;
|
||||
border: 2px var(--border-color, #ddd) dashed;
|
||||
z-index: 300;
|
||||
|
||||
line-height: 119px;
|
||||
text-align: center;
|
||||
background-color: #efefef;
|
||||
background-color: var(--dialog-bg-clr, #fff);
|
||||
font-size: 24px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
opacity: 0.9;
|
||||
&.dragAndDropOver {
|
||||
background-color: #fff;
|
||||
border-color: var(--main-color, #fff);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
html:not(rl-mobile) {
|
||||
@media screen and (min-width: @maxMobileWidth + 1px) {
|
||||
.hideContactListCheckbox {
|
||||
.checkboxItem {
|
||||
visibility: hidden;
|
||||
|
|
@ -203,7 +203,7 @@ html:not(rl-mobile) {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
#V-PopupsContacts {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.focused .b-folders a.focused {
|
||||
background-color: var(--folders-focused-bg-color, #333);
|
||||
border-left-color: #fff;
|
||||
color: var(--folders-focused-color, #eee);
|
||||
}
|
||||
|
||||
.b-folders {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -38,7 +44,6 @@
|
|||
}
|
||||
|
||||
.b-toolbar {
|
||||
padding: 10px 0 10px @rlLowMargin;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +61,6 @@
|
|||
|
||||
hr {
|
||||
margin: 10px;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
li {
|
||||
|
|
@ -101,22 +105,12 @@
|
|||
background-color: var(--folders-selected-bg-color, #333);
|
||||
color: var(--folders-selected-color, #eee);
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background-color: var(--folders-focused-bg-color, #333);
|
||||
color: var(--folders-focused-color, #eee);
|
||||
}
|
||||
}
|
||||
&:not(.selectable) {
|
||||
cursor: default;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background-color: #888;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
|
||||
&.unread-sub {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -213,24 +207,19 @@
|
|||
/**/
|
||||
}
|
||||
|
||||
.btn {
|
||||
&.buttonCompose {
|
||||
max-height: 2em;
|
||||
max-width: fit-content;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
#rl-left .buttonCompose {
|
||||
max-height: 2em;
|
||||
max-width: fit-content;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
|
||||
html.rl-left-panel-disabled {
|
||||
.buttonComposeText{
|
||||
.buttonComposeText,
|
||||
.buttonContacts {
|
||||
display: none;
|
||||
}
|
||||
.btn.buttonCompose {
|
||||
#rl-left .buttonCompose {
|
||||
width: auto;
|
||||
}
|
||||
.btn.buttonContacts {
|
||||
margin-top: 10px !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@
|
|||
/* transition: width 0.3s ease-out;*/
|
||||
width: @rlLeftWidth;
|
||||
z-index: 0;
|
||||
|
||||
.b-toolbar {
|
||||
padding: 10px 0 10px @rlLowMargin;
|
||||
}
|
||||
}
|
||||
|
||||
#rl-right:not([hidden]) {
|
||||
|
|
@ -19,9 +23,6 @@
|
|||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
.rl-bottom-preview-pane #rl-right {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/*
|
||||
.resizable::after {
|
||||
|
|
@ -41,47 +42,12 @@
|
|||
}
|
||||
*/
|
||||
|
||||
.resizer {
|
||||
background: #aaa;
|
||||
background: rgba(255,255,255,0.5);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
z-index: 102;
|
||||
}
|
||||
.resizer:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
html:not(.rl-left-panel-disabled) #rl-left {
|
||||
resize: horizontal; overflow: auto;
|
||||
min-width: 155px;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
#rl-left > .resizer,
|
||||
.rl-side-preview-pane #V-MailMessageList .resizer {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
}
|
||||
.rl-bottom-preview-pane #V-MailMessageList .resizer {
|
||||
cursor: ns-resize;
|
||||
cursor: row-resize;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
html:not(.rl-left-panel-disabled) #rl-left > .resizer,
|
||||
#V-MailMessageList .resizer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#top-system-dropdown-id::after,
|
||||
#button-add-prop-dropdown-id::after {
|
||||
content: '▼';
|
||||
|
|
@ -140,32 +106,6 @@ html:not(.rl-left-panel-disabled) #rl-left > .resizer,
|
|||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* desktop */
|
||||
@media screen and (min-width: 1000px) {
|
||||
}
|
||||
|
||||
/* desktop-large */
|
||||
@media screen and (min-width: 1401px) {
|
||||
#rl-left {
|
||||
width: @rlLeftWidth + 20;
|
||||
}
|
||||
}
|
||||
|
||||
/* mobile and tablet */
|
||||
@media screen and (max-width: 999px) {
|
||||
#rl-left {
|
||||
width: 155px;
|
||||
}
|
||||
|
||||
#rl-settings-subscreen {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.dropdown-menu a {
|
||||
line-height: 2.5;
|
||||
}
|
||||
}
|
||||
|
||||
html:not(.rl-ctrl-key-pressed) .visible-on-ctrl-btn,
|
||||
html.rl-ctrl-key-pressed .hidden-on-ctrl-btn,
|
||||
.show-on-panel-disabled {
|
||||
|
|
@ -191,32 +131,6 @@ html.rl-left-panel-disabled {
|
|||
}
|
||||
}
|
||||
|
||||
html.rl-mobile #rl-left > .resizer,
|
||||
html.rl-no-preview-pane #rl-right .resizer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html.rl-mobile {
|
||||
#rl-left {
|
||||
background: var(--main-bg-color, #aaa);
|
||||
border-right: 1px solid var(--border-color, #ddd);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 50vw;
|
||||
z-index: 3;
|
||||
}
|
||||
&.rl-left-panel-disabled #rl-left {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
html.rl-mobile .hide-mobile,
|
||||
html:not(.rl-mobile) .show-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.e-paginator {
|
||||
|
||||
a {
|
||||
|
|
@ -237,3 +151,85 @@ html:not(.rl-mobile) .show-mobile {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
#rl-app {
|
||||
#V-SettingsPane {
|
||||
margin-right: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#rl-left {
|
||||
background: var(--main-bg-color, #aaa);
|
||||
border-right: 1px solid var(--border-color, #ddd);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 50vw;
|
||||
z-index: 3;
|
||||
}
|
||||
.rl-left-panel-disabled #rl-left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-menu a {
|
||||
line-height: 2.5;
|
||||
}
|
||||
|
||||
.hide-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* desktop */
|
||||
@media screen and (min-width: @maxMobileWidth + 1px) {
|
||||
.toggleLeft,
|
||||
#V-MailMessageList .buttonCompose {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sm-msgView-bottom #rl-right {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
background: #aaa;
|
||||
background: rgba(255,255,255,0.5);
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 102;
|
||||
}
|
||||
.resizer:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
html:not(.rl-left-panel-disabled) #rl-left > .resizer,
|
||||
.sm-msgView-side #V-MailMessageList .resizer {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
display: block;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
}
|
||||
.sm-msgView-bottom #V-MailMessageList .resizer {
|
||||
cursor: ns-resize;
|
||||
cursor: row-resize;
|
||||
display: block;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* desktop-large */
|
||||
@media screen and (min-width: 1401px) {
|
||||
#rl-left {
|
||||
width: @rlLeftWidth + 20;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,9 @@
|
|||
|
||||
html.rl-mobile, html.rl-no-preview-pane {
|
||||
.message-selected #V-MailMessageList {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#V-MailMessageList {
|
||||
position: relative;
|
||||
}
|
||||
.rl-side-preview-pane #V-MailMessageList {
|
||||
height: 100%;
|
||||
}
|
||||
.rl-no-preview-pane #V-MailMessageList {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.rl-side-preview-pane #V-MailMessageList .messageList {
|
||||
max-width: 50vw;
|
||||
min-width: 320px;
|
||||
overflow: auto;
|
||||
resize: horizontal;
|
||||
width: 35vw;
|
||||
}
|
||||
.rl-bottom-preview-pane #V-MailMessageList .messageList {
|
||||
height: 35vh;
|
||||
max-height: 50vh;
|
||||
min-height: 200px;
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#V-MailMessageList.focused .messageList {
|
||||
border-color: #9d9d9d;
|
||||
|
|
@ -47,7 +22,7 @@ html.rl-mobile, html.rl-no-preview-pane {
|
|||
|
||||
.messageList {
|
||||
height: calc(100% - 60px);
|
||||
background-color: #fff;
|
||||
background-color: var(--panel-bg-clr, #fff);
|
||||
border: 1px solid var(--border-color, #aaa);
|
||||
border-radius: var(--border-radius, 5px);
|
||||
display: flex;
|
||||
|
|
@ -71,7 +46,7 @@ html.rl-mobile, html.rl-no-preview-pane {
|
|||
}
|
||||
|
||||
.b-footer {
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 7px;
|
||||
|
||||
|
|
@ -196,18 +171,6 @@ html.rl-mobile, html.rl-no-preview-pane {
|
|||
}
|
||||
}
|
||||
|
||||
html:not(rl-mobile) {
|
||||
.hideMessageListCheckbox {
|
||||
.checkboxCheckAll {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.messageCheckbox {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageListItem > div + div {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
|
@ -396,20 +359,55 @@ html:not(rl-mobile) {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
html:not(.sm-msgView-bottom):not(.sm-msgView-side) .message-selected #V-MailMessageList {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
#V-MailMessageList .btn-toolbar:not(.hasChecked) .onCheckedShow,
|
||||
#V-MailMessageList .hasChecked .onCheckedHide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: @maxMobileWidth + 1px) {
|
||||
.messageList {
|
||||
.listDragOver {
|
||||
transition: all 400ms ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* desktop-large */
|
||||
@media screen and (min-width: 1401px) {
|
||||
.messageListItem {
|
||||
font-size: 13px;
|
||||
time {
|
||||
font-size: 13px;
|
||||
.hideMessageListCheckbox {
|
||||
.checkboxCheckAll {
|
||||
visibility: hidden;
|
||||
}
|
||||
.checkboxMessage {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sm-msgView-side #V-MailMessageList {
|
||||
width: unset;
|
||||
|
||||
.messageList {
|
||||
max-width: 50vw;
|
||||
min-width: 320px;
|
||||
overflow: auto;
|
||||
resize: horizontal;
|
||||
width: 35vw;
|
||||
}
|
||||
}
|
||||
|
||||
.sm-msgView-bottom #V-MailMessageList {
|
||||
height: unset;
|
||||
width: unset;
|
||||
|
||||
.messageList {
|
||||
height: 35vh;
|
||||
max-height: 50vh;
|
||||
min-height: 200px;
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -425,7 +423,7 @@ html:not(rl-mobile) {
|
|||
top: 0;
|
||||
}
|
||||
|
||||
.rl-side-preview-pane, .rl-mobile {
|
||||
.sm-msgView-side, .rl-mobile {
|
||||
|
||||
.messageListItem > div + div {
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -30,28 +30,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
html.rl-no-preview-pane {
|
||||
#rl-right:not(.message-selected) #V-MailMessageView {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#V-MailMessageView .top-toolbar {
|
||||
color: #fff;
|
||||
padding: 10px 0;
|
||||
}
|
||||
html.rl-no-preview-pane #V-MailMessageView .top-toolbar {
|
||||
padding-left: 1px;
|
||||
}
|
||||
html:not(.rl-mobile):not(.rl-no-preview-pane) #V-MailMessageView .top-toolbar {
|
||||
visibility: hidden;
|
||||
}
|
||||
html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
||||
display: none;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.messageView {
|
||||
background-color: #fff;
|
||||
background-color: var(--panel-bg-clr, #fff);
|
||||
border: 1px solid var(--border-color, #aaa);
|
||||
border-radius: var(--border-radius, 5px);
|
||||
height: calc(100% - 50px);
|
||||
|
|
@ -140,7 +126,7 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
|
||||
.messageItemHeader {
|
||||
|
||||
background-color: #f8f8f8;
|
||||
background-color: var(--message-header-bg-clr, #eee);
|
||||
border-bottom: 1px solid var(--border-color, #bbb);
|
||||
border-radius: var(--border-radius, 5px) var(--border-radius, 5px) 0 0;
|
||||
padding: 10px;
|
||||
|
|
@ -215,7 +201,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
}
|
||||
|
||||
.openpgp-control {
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
span {
|
||||
|
|
@ -248,9 +233,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
.b-text-part > iframe {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.thread-controls {
|
||||
.dropdown-toggle {
|
||||
|
|
@ -271,7 +253,6 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
|
||||
#messageItem {
|
||||
|
||||
color: #000;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
|
|
@ -283,11 +264,13 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
padding: 50px 0;
|
||||
}
|
||||
|
||||
.bodySubHeader {
|
||||
background-color: var(--message-header-bg-clr, #eee);
|
||||
}
|
||||
.bodySubHeader > * {
|
||||
background-color: #eee;
|
||||
border-bottom: 1px solid var(--border-color, #bbb);
|
||||
cursor: pointer;
|
||||
padding: 10px 15px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.pgpInfo {
|
||||
|
|
@ -300,14 +283,8 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
}
|
||||
}
|
||||
|
||||
.readReceipt {
|
||||
background-color: #ffffd9;
|
||||
}
|
||||
|
||||
.attachmentsPlace {
|
||||
|
||||
padding: 10px 10px 6px 10px;
|
||||
background: #eee;
|
||||
border-bottom: 1px solid var(--border-color, #bbb);
|
||||
position: relative;
|
||||
|
||||
|
|
@ -336,9 +313,8 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
}
|
||||
|
||||
.attachmentsControls {
|
||||
padding: 7px 5px 7px 14px;
|
||||
background: #e8e8e8;
|
||||
border-bottom: 1px solid var(--border-color, #bbb);
|
||||
padding-top: 7px;
|
||||
border-top: 1px solid var(--border-color, #bbb);
|
||||
}
|
||||
.attachmentsControls > * {
|
||||
display: inline-block;
|
||||
|
|
@ -365,8 +341,12 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
height: 100%;
|
||||
padding: 10px;
|
||||
|
||||
* {
|
||||
box-sizing: unset;
|
||||
}
|
||||
|
||||
a {
|
||||
color: blue;
|
||||
color: var(--link-color, blue);
|
||||
text-decoration: underline;
|
||||
|
||||
&:visited {
|
||||
|
|
@ -382,6 +362,10 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
opacity: 0.8;
|
||||
padding: 0 10px;
|
||||
margin: 0;
|
||||
|
||||
blockquote {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.html {
|
||||
|
|
@ -471,15 +455,34 @@ html.rl-bottom-preview-pane #V-MailMessageView .top-toolbar {
|
|||
}
|
||||
}
|
||||
|
||||
html.rl-no-preview-pane .messageView {
|
||||
border: 1px solid var(--border-color, #aaa);
|
||||
box-shadow: var(--smMainShadow);
|
||||
.mailvelope iframe {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
html.rl-bottom-preview-pane .messageView {
|
||||
height: 100%;
|
||||
html:not(.sm-msgView-bottom):not(.sm-msgView-side) {
|
||||
#rl-right:not(.message-selected) #V-MailMessageView {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1400px) {
|
||||
html.sm-msgView-side #V-MailMessageView .top-toolbar {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
html.sm-msgView-bottom {
|
||||
#V-MailMessageView .top-toolbar {
|
||||
display: none;
|
||||
}
|
||||
.messageView {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@media all and (display-mode: fullscreen) {
|
||||
*/
|
||||
html.rl-fullscreen {
|
||||
|
||||
#rl-left,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
}
|
||||
|
||||
#V-PopupsOpenPgpKey {
|
||||
.key-viewer {
|
||||
pre {
|
||||
margin: 0;
|
||||
max-height: 500px;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,9 +14,8 @@
|
|||
#V-PopupsOpenPgpImport {
|
||||
max-width: 645px;
|
||||
|
||||
.inputKey {
|
||||
textarea {
|
||||
font-family: var(--fontMono);
|
||||
width: 600px;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,5 @@
|
|||
|
||||
#V-SettingsMenu {
|
||||
|
||||
.b-footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding: 0 10px 0 5px;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.b-content {
|
||||
position: absolute;
|
||||
top: 50px + @rlLowMargin + 10px;
|
||||
bottom: @rlLowMargin;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
nav {
|
||||
|
||||
a {
|
||||
|
|
@ -51,15 +32,20 @@
|
|||
#V-SettingsPane {
|
||||
height: calc(100% - 50px - @rlLowMargin);
|
||||
margin: 50px @rlLowMargin 0 0;
|
||||
padding:20px;
|
||||
width: 100%;
|
||||
|
||||
background-color: var(--panel-bg-clr, #fff);
|
||||
border: 1px solid var(--border-color, #aaa);
|
||||
border-radius: var(--border-radius, 5px);
|
||||
box-shadow: var(--smMainShadow);
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
.btn-toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding: 8px 0;
|
||||
color: #fff;
|
||||
top: 8px;
|
||||
left: @rlLowMargin;
|
||||
}
|
||||
|
||||
td {
|
||||
|
|
@ -68,22 +54,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.rl-mobile #V-SettingsPane {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#rl-settings-subscreen {
|
||||
background-color: var(--settings-bg-clr, #fff);
|
||||
border: 1px solid var(--border-color, #aaa);
|
||||
border-radius: var(--border-radius, 5px);
|
||||
box-shadow: var(--smMainShadow);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding:20px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.firefox-drag #rl-settings-subscreen,
|
||||
.firefox-drag #V-SettingsPane,
|
||||
.firefox-drag .modal-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
|
||||
#top-system-dropdown-id {
|
||||
overflow: hidden;
|
||||
padding-left: 8px;
|
||||
padding-right: 6px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.rl-left-panel-disabled #more-list-dropdown-id + .dropdown-menu {
|
||||
|
|
@ -50,16 +48,15 @@
|
|||
}
|
||||
|
||||
.accountPlace {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
border-right: 1px solid var(--btn-border-clr, rgba(0, 0, 0, 0.2));
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 29px;
|
||||
line-height: 22px;
|
||||
margin: 0 6px -3px 0;
|
||||
max-width: 25vw;
|
||||
overflow: hidden;
|
||||
padding: 1px 8px;
|
||||
overflow-x: hidden;
|
||||
padding-right: 6px;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 0 #000;
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
white-space: nowrap;
|
||||
z-index: 2001;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
[data-rainloopErrorTip]::before {
|
||||
min-width: 60vw;
|
||||
white-space: normal;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@
|
|||
@rlLeftWidth: 200px;
|
||||
@rlLowMargin: 8px;
|
||||
|
||||
/* mobile and tablet */
|
||||
@maxMobileWidth: 799px;
|
||||
|
||||
:root {
|
||||
--smDialogShrink: 20px;
|
||||
--smMainShadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.rl-mobile {
|
||||
/* For Firefox */
|
||||
--smDialogShrink: 25px;
|
||||
/* For Firefox */
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
:root {
|
||||
--smDialogShrink: 25px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import {
|
||||
Notification,
|
||||
Notifications,
|
||||
UploadErrorCode
|
||||
} from 'Common/Enums';
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
FolderType
|
||||
} from 'Common/EnumsUser';
|
||||
|
||||
import { pInt, isArray, arrayLength } from 'Common/Utils';
|
||||
import { pInt, isArray, arrayLength, b64Encode } from 'Common/Utils';
|
||||
import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
|
||||
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||
|
||||
|
|
@ -19,8 +19,8 @@ import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
|||
import { folderInformation, messagesDeleteHelper } from 'Common/Folders';
|
||||
import { serverRequest } from 'Common/Links';
|
||||
import { i18n, getNotification, getUploadErrorDescByCode, timestampToString } from 'Common/Translator';
|
||||
import { MessageFlagsCache, setFolderHash } from 'Common/Cache';
|
||||
import { Settings, SettingsCapa, SettingsGet, elementById, addShortcut, createElement } from 'Common/Globals';
|
||||
import { setFolderETag } from 'Common/Cache';
|
||||
import { SettingsCapa, SettingsGet, elementById, addShortcut, createElement } from 'Common/Globals';
|
||||
//import { exitFullscreen, isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
|
|
@ -57,7 +57,7 @@ const
|
|||
|
||||
tpl = createElement('template'),
|
||||
|
||||
base64_encode = text => btoa(unescape(encodeURIComponent(text))).match(/.{1,76}/g).join('\r\n'),
|
||||
base64_encode = text => b64Encode(text).match(/.{1,76}/g).join('\r\n'),
|
||||
|
||||
getEmail = value => addressparser(value)[0]?.email || false,
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ const
|
|||
reloadDraftFolder = () => {
|
||||
const draftsFolder = FolderUserStore.draftsFolder();
|
||||
if (draftsFolder && UNUSED_OPTION_VALUE !== draftsFolder) {
|
||||
setFolderHash(draftsFolder, '');
|
||||
setFolderETag(draftsFolder, '');
|
||||
if (FolderUserStore.currentFolderFullName() === draftsFolder) {
|
||||
MessagelistUserStore.reload(true);
|
||||
} else {
|
||||
|
|
@ -441,16 +441,6 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
this.sendError(false);
|
||||
this.sending(true);
|
||||
|
||||
if (3 === arrayLength(this.aDraftInfo)) {
|
||||
const flagsCache = MessageFlagsCache.getFor(this.aDraftInfo[2], this.aDraftInfo[1]);
|
||||
if (isArray(flagsCache)) {
|
||||
flagsCache.push(('forward' === this.aDraftInfo[0]) ? '$forwarded' : '\\answered');
|
||||
MessageFlagsCache.setFor(this.aDraftInfo[2], this.aDraftInfo[1], flagsCache);
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
setFolderHash(this.aDraftInfo[2], '');
|
||||
}
|
||||
}
|
||||
|
||||
sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
|
||||
|
||||
this.getMessageRequestParams(sSentFolder).then(params => {
|
||||
|
|
@ -458,19 +448,23 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
(iError, data) => {
|
||||
this.sending(false);
|
||||
if (iError) {
|
||||
if (Notification.CantSaveMessage === iError) {
|
||||
if (Notifications.CantSaveMessage === iError) {
|
||||
this.sendSuccessButSaveError(true);
|
||||
this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim());
|
||||
} else {
|
||||
this.sendError(true);
|
||||
this.sendErrorDesc(getNotification(iError, data?.ErrorMessage)
|
||||
|| getNotification(Notification.CantSendMessage));
|
||||
|| getNotification(Notifications.CantSendMessage));
|
||||
}
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
setFolderHash(this.draftsFolder(), '');
|
||||
setFolderHash(sSentFolder, '');
|
||||
setFolderETag(this.draftsFolder(), '');
|
||||
setFolderETag(sSentFolder, '');
|
||||
if (3 === arrayLength(this.aDraftInfo)) {
|
||||
const folder = this.aDraftInfo[2];
|
||||
setFolderETag(folder, '');
|
||||
}
|
||||
reloadDraftFolder();
|
||||
},
|
||||
params,
|
||||
|
|
@ -492,57 +486,59 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
}
|
||||
|
||||
saveCommand() {
|
||||
if (FolderUserStore.draftsFolderNotEnabled()) {
|
||||
showScreenPopup(FolderSystemPopupView, [FolderType.Drafts]);
|
||||
} else {
|
||||
this.savedError(false);
|
||||
this.saving(true);
|
||||
this.autosaveStart();
|
||||
this.getMessageRequestParams(FolderUserStore.draftsFolder(), 1).then(params => {
|
||||
Remote.request('SaveMessage',
|
||||
(iError, oData) => {
|
||||
let result = false;
|
||||
if (!this.saving() && !this.sending()) {
|
||||
if (FolderUserStore.draftsFolderNotEnabled()) {
|
||||
showScreenPopup(FolderSystemPopupView, [FolderType.Drafts]);
|
||||
} else {
|
||||
this.savedError(false);
|
||||
this.saving(true);
|
||||
this.autosaveStart();
|
||||
this.getMessageRequestParams(FolderUserStore.draftsFolder(), 1).then(params => {
|
||||
Remote.request('SaveMessage',
|
||||
(iError, oData) => {
|
||||
let result = false;
|
||||
|
||||
this.saving(false);
|
||||
this.saving(false);
|
||||
|
||||
if (!iError) {
|
||||
if (oData.Result.folder && oData.Result.uid) {
|
||||
result = true;
|
||||
if (!iError) {
|
||||
if (oData.Result.folder && oData.Result.uid) {
|
||||
result = true;
|
||||
|
||||
if (this.bFromDraft) {
|
||||
const message = MessageUserStore.message();
|
||||
if (message && this.draftsFolder() === message.folder && this.draftUid() == message.uid) {
|
||||
MessageUserStore.message(null);
|
||||
if (this.bFromDraft) {
|
||||
const message = MessageUserStore.message();
|
||||
if (message && this.draftsFolder() === message.folder && this.draftUid() == message.uid) {
|
||||
MessageUserStore.message(null);
|
||||
}
|
||||
}
|
||||
|
||||
this.draftsFolder(oData.Result.folder);
|
||||
this.draftUid(oData.Result.uid);
|
||||
|
||||
this.savedTime(new Date);
|
||||
|
||||
if (this.bFromDraft) {
|
||||
setFolderETag(this.draftsFolder(), '');
|
||||
}
|
||||
setFolderETag(FolderUserStore.draftsFolder(), '');
|
||||
}
|
||||
|
||||
this.draftsFolder(oData.Result.folder);
|
||||
this.draftUid(oData.Result.uid);
|
||||
|
||||
this.savedTime(new Date);
|
||||
|
||||
if (this.bFromDraft) {
|
||||
setFolderHash(this.draftsFolder(), '');
|
||||
}
|
||||
setFolderHash(FolderUserStore.draftsFolder(), '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
this.savedError(true);
|
||||
this.savedErrorDesc(getNotification(Notification.CantSaveMessage));
|
||||
}
|
||||
if (!result) {
|
||||
this.savedError(true);
|
||||
this.savedErrorDesc(getNotification(Notifications.CantSaveMessage));
|
||||
}
|
||||
|
||||
reloadDraftFolder();
|
||||
},
|
||||
params,
|
||||
200000
|
||||
);
|
||||
}).catch(e => {
|
||||
this.saving(false);
|
||||
this.savedError(true);
|
||||
this.savedErrorDesc(getNotification(Notification.CantSaveMessage) + ': ' + e);
|
||||
});
|
||||
reloadDraftFolder();
|
||||
},
|
||||
params,
|
||||
200000
|
||||
);
|
||||
}).catch(e => {
|
||||
this.saving(false);
|
||||
this.savedError(true);
|
||||
this.savedErrorDesc(getNotification(Notifications.CantSaveMessage) + ': ' + e);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -569,12 +565,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
skipCommand() {
|
||||
ComposePopupView.inEdit(true);
|
||||
|
||||
if (
|
||||
!this.saving() &&
|
||||
!this.sending() &&
|
||||
!FolderUserStore.draftsFolderNotEnabled() &&
|
||||
SettingsUserStore.allowDraftAutosave()
|
||||
) {
|
||||
if (!FolderUserStore.draftsFolderNotEnabled() && SettingsUserStore.allowDraftAutosave()) {
|
||||
this.saveCommand();
|
||||
}
|
||||
|
||||
|
|
@ -597,8 +588,6 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
&& !FolderUserStore.draftsFolderNotEnabled()
|
||||
&& SettingsUserStore.allowDraftAutosave()
|
||||
&& !this.isEmptyForm(false)
|
||||
&& !this.saving()
|
||||
&& !this.sending()
|
||||
&& !this.savedError()
|
||||
) {
|
||||
this.saveCommand();
|
||||
|
|
@ -617,7 +606,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
data.Result.map(item => (item?.[0] ? (new EmailModel(item[0], item[1])).toLine() : null))
|
||||
.filter(v => v)
|
||||
);
|
||||
} else if (Notification.RequestAborted !== iError) {
|
||||
} else if (Notifications.RequestAborted !== iError) {
|
||||
fResponse([]);
|
||||
}
|
||||
},
|
||||
|
|
@ -870,9 +859,9 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
|
||||
// https://github.com/the-djmaze/snappymail/issues/491
|
||||
tpl.innerHTML = message.bodyAsHTML();
|
||||
tpl.content.querySelectorAll('img').forEach(img =>
|
||||
img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
|
||||
);
|
||||
tpl.content.querySelectorAll('img').forEach(img => {
|
||||
img.src || img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
|
||||
});
|
||||
sText = tpl.innerHTML.trim();
|
||||
|
||||
switch (msgComposeType) {
|
||||
|
|
@ -1010,7 +999,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
clickElement: dom.querySelector('#composeUploadButton'),
|
||||
dragAndDropElement: dom.querySelector('.b-attachment-place')
|
||||
}),
|
||||
attachmentSizeLimit = pInt(SettingsGet('AttachmentLimit'));
|
||||
attachmentSizeLimit = pInt(SettingsGet('attachmentLimit'));
|
||||
|
||||
oJua
|
||||
.on('onDragEnter', () => {
|
||||
|
|
@ -1124,12 +1113,12 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
return false;
|
||||
});
|
||||
|
||||
if (Settings.app('allowCtrlEnterOnCompose')) {
|
||||
addShortcut('enter', 'meta', ScopeCompose, () => {
|
||||
addShortcut('enter', 'meta', ScopeCompose, () => {
|
||||
// if (SettingsUserStore.allowCtrlEnterOnCompose()) {
|
||||
this.sendCommand();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
// }
|
||||
});
|
||||
addShortcut('mailsend', '', ScopeCompose, () => {
|
||||
this.sendCommand();
|
||||
return false;
|
||||
|
|
@ -1297,7 +1286,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
*/
|
||||
let text = this.oEditor.getData(),
|
||||
encrypted = PgpUserStore.isEncrypted(text),
|
||||
size = SettingsGet('PhpUploadSizes')['post_max_size'],
|
||||
size = SettingsGet('phpUploadSizes')['post_max_size'],
|
||||
quota = pInt(size);
|
||||
switch (size.slice(-1)) {
|
||||
case 'G': quota *= 1024; // fallthrough
|
||||
|
|
@ -1409,7 +1398,10 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
encrypt = this.pgpEncrypt() && this.canPgpEncrypt(),
|
||||
isHtml = this.oEditor.isHtml();
|
||||
|
||||
let Text = this.oEditor.getData();
|
||||
let Text = this.oEditor.getData().trim();
|
||||
if (!Text.length) {
|
||||
throw 'Message body is empty';
|
||||
}
|
||||
if (isHtml) {
|
||||
let l;
|
||||
do {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { koComputable, addObservablesTo } from 'External/ko';
|
||||
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
|
@ -65,7 +65,7 @@ export class FolderCreatePopupView extends AbstractViewPopup {
|
|||
},
|
||||
error => {
|
||||
FolderUserStore.folderListError(
|
||||
getNotification(error.code, '', Notification.CantCreateFolder)
|
||||
getNotification(error.code, '', Notifications.CantCreateFolder)
|
||||
+ '.\n' + error.message);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { addObservablesTo, addSubscribablesTo } from 'External/ko';
|
||||
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
|
|
@ -6,49 +7,34 @@ import Remote from 'Remote/User/Fetch';
|
|||
|
||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||
|
||||
import { IdentityModel } from 'Model/Identity';
|
||||
|
||||
export class IdentityPopupView extends AbstractViewPopup {
|
||||
constructor() {
|
||||
super('Identity');
|
||||
|
||||
this.id = '';
|
||||
addObservablesTo(this, {
|
||||
id: '',
|
||||
edit: false,
|
||||
owner: false,
|
||||
|
||||
email: '',
|
||||
emailFocused: false,
|
||||
|
||||
name: '',
|
||||
nameFocused: false,
|
||||
|
||||
replyTo: '',
|
||||
replyToFocused: false,
|
||||
showReplyTo: false,
|
||||
|
||||
bcc: '',
|
||||
bccFocused: false,
|
||||
bccHasError: false,
|
||||
showBcc: false,
|
||||
|
||||
signature: '',
|
||||
signatureInsertBefore: false,
|
||||
|
||||
showBcc: false,
|
||||
showReplyTo: false,
|
||||
|
||||
submitRequest: false,
|
||||
submitError: ''
|
||||
});
|
||||
|
||||
addSubscribablesTo(this, {
|
||||
replyTo: value => {
|
||||
if (false === this.showReplyTo() && value.length) {
|
||||
this.showReplyTo(true);
|
||||
}
|
||||
},
|
||||
bcc: value => {
|
||||
if (false === this.showBcc() && value.length) {
|
||||
this.showBcc(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
/*
|
||||
this.email.valueHasMutated();
|
||||
this.replyTo.valueHasMutated();
|
||||
|
|
@ -61,9 +47,8 @@ export class IdentityPopupView extends AbstractViewPopup {
|
|||
this.signature?.__fetchEditorValue?.();
|
||||
this.submitRequest(true);
|
||||
const data = new FormData(form);
|
||||
data.set('Id', this.id);
|
||||
data.set('Id', this.id());
|
||||
data.set('Signature', this.signature());
|
||||
data.set('SignatureInsertBefore', this.signatureInsertBefore() ? 1 : 0);
|
||||
Remote.request('IdentityUpdate', iError => {
|
||||
this.submitRequest(false);
|
||||
if (iError) {
|
||||
|
|
@ -89,32 +74,23 @@ export class IdentityPopupView extends AbstractViewPopup {
|
|||
|
||||
if (identity) {
|
||||
this.edit(true);
|
||||
|
||||
this.id = identity.id() || '';
|
||||
this.name(identity.name());
|
||||
this.email(identity.email());
|
||||
this.replyTo(identity.replyTo());
|
||||
this.bcc(identity.bcc());
|
||||
this.signature(identity.signature());
|
||||
this.signatureInsertBefore(identity.signatureInsertBefore());
|
||||
|
||||
this.owner(!this.id);
|
||||
} else {
|
||||
this.edit(false);
|
||||
|
||||
this.id = Jua.randomId();
|
||||
this.name('');
|
||||
this.email('');
|
||||
this.replyTo('');
|
||||
this.bcc('');
|
||||
this.signature('');
|
||||
this.signatureInsertBefore(false);
|
||||
|
||||
this.owner(false);
|
||||
identity = new IdentityModel;
|
||||
identity.id(Jua.randomId());
|
||||
}
|
||||
this.id(identity.id() || '');
|
||||
this.name(identity.name());
|
||||
this.email(identity.email());
|
||||
this.replyTo(identity.replyTo());
|
||||
this.showReplyTo(0 < identity.replyTo().length);
|
||||
this.bcc(identity.bcc());
|
||||
this.showBcc(0 < identity.bcc().length);
|
||||
this.signature(identity.signature());
|
||||
this.signatureInsertBefore(identity.signatureInsertBefore());
|
||||
}
|
||||
|
||||
afterShow() {
|
||||
this.owner() || this.emailFocused(true);
|
||||
this.id() ? this.emailFocused(true) : this.nameFocused(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import ko from 'ko';
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
import { convertLangName } from 'Common/Translator';
|
||||
|
||||
|
|
@ -8,46 +7,19 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
|||
export class LanguagesPopupView extends AbstractViewPopup {
|
||||
constructor() {
|
||||
super('Languages');
|
||||
|
||||
this.fLang = null;
|
||||
this.userLanguage = ko.observable('');
|
||||
|
||||
this.langs = ko.observableArray();
|
||||
|
||||
this.languages = koComputable(() => {
|
||||
const userLanguage = this.userLanguage();
|
||||
return this.langs.map(language => ({
|
||||
key: language,
|
||||
user: language === userLanguage,
|
||||
selected: ko.observable(false),
|
||||
fullName: convertLangName(language)
|
||||
}));
|
||||
});
|
||||
|
||||
this.langs.subscribe(() => this.setLanguageSelection());
|
||||
}
|
||||
|
||||
languageTooltipName(language) {
|
||||
return convertLangName(language, true);
|
||||
}
|
||||
|
||||
setLanguageSelection() {
|
||||
const currentLang = this.fLang ? ko.unwrap(this.fLang) : '';
|
||||
this.languages().forEach(item => item.selected(item.key === currentLang));
|
||||
}
|
||||
|
||||
beforeShow() {
|
||||
this.fLang = null;
|
||||
this.userLanguage('');
|
||||
|
||||
this.langs([]);
|
||||
this.languages = ko.observableArray();
|
||||
}
|
||||
|
||||
onShow(fLanguage, langs, userLanguage) {
|
||||
this.fLang = fLanguage;
|
||||
this.userLanguage(userLanguage || '');
|
||||
|
||||
this.langs(langs);
|
||||
this.languages(langs.map(language => ({
|
||||
key: language,
|
||||
user: userLanguage === language,
|
||||
selected: fLanguage?.() === language,
|
||||
fullName: convertLangName(language),
|
||||
title: convertLangName(language, true)
|
||||
})));
|
||||
}
|
||||
|
||||
changeLanguage(lang) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { ClientSideKeyNameLastSignMe } from 'Common/EnumsUser';
|
||||
import { SettingsGet, fireEvent } from 'Common/Globals';
|
||||
import { getNotification, translatorReload, convertLangName } from 'Common/Translator';
|
||||
|
|
@ -26,7 +26,7 @@ export class LoginUserView extends AbstractViewLogin {
|
|||
super();
|
||||
|
||||
addObservablesTo(this, {
|
||||
loadingDesc: SettingsGet('LoadingDescription'),
|
||||
loadingDesc: SettingsGet('loadingDescription'),
|
||||
|
||||
email: SettingsGet('DevEmail'),
|
||||
password: SettingsGet('DevPassword'),
|
||||
|
|
@ -44,7 +44,7 @@ export class LoginUserView extends AbstractViewLogin {
|
|||
signMeType: SignMeUnused
|
||||
});
|
||||
|
||||
this.allowLanguagesOnLogin = !!SettingsGet('AllowLanguagesOnLogin');
|
||||
this.allowLanguagesOnLogin = !!SettingsGet('allowLanguagesOnLogin');
|
||||
|
||||
this.language = LanguageStore.language;
|
||||
this.languages = LanguageStore.languages;
|
||||
|
|
@ -110,8 +110,8 @@ export class LoginUserView extends AbstractViewLogin {
|
|||
|
||||
if (valid) {
|
||||
this.submitRequest(true);
|
||||
data.set('Language', this.bSendLanguage ? this.language() : '');
|
||||
data.set('SignMe', this.signMe() ? 1 : 0);
|
||||
data.set('language', this.bSendLanguage ? this.language() : '');
|
||||
data.set('signMe', this.signMe() ? 1 : 0);
|
||||
Remote.request('Login',
|
||||
(iError, oData) => {
|
||||
fireEvent('sm-user-login-response', {
|
||||
|
|
@ -120,11 +120,11 @@ export class LoginUserView extends AbstractViewLogin {
|
|||
});
|
||||
if (iError) {
|
||||
this.submitRequest(false);
|
||||
if (Notification.InvalidInputArgument == iError) {
|
||||
iError = Notification.AuthError;
|
||||
if (Notifications.InvalidInputArgument == iError) {
|
||||
iError = Notifications.AuthError;
|
||||
}
|
||||
this.submitError(getNotification(iError, oData?.ErrorMessage,
|
||||
Notification.UnknownNotification));
|
||||
Notifications.UnknownNotification));
|
||||
this.submitErrorAdditional(oData?.ErrorMessageAdditional);
|
||||
} else {
|
||||
rl.setData(oData.Result);
|
||||
|
|
@ -142,7 +142,7 @@ export class LoginUserView extends AbstractViewLogin {
|
|||
onBuild(dom) {
|
||||
super.onBuild(dom);
|
||||
|
||||
const signMe = (SettingsGet('SignMe') || '').toLowerCase();
|
||||
const signMe = (SettingsGet('signMe') || '').toLowerCase();
|
||||
|
||||
switch (signMe) {
|
||||
case 'defaultoff':
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Scope } from 'Common/Enums';
|
||||
import { addShortcut } from 'Common/Globals';
|
||||
import { addShortcut, stopEvent } from 'Common/Globals';
|
||||
import { mailBox, settings } from 'Common/Links';
|
||||
//import { setFolderHash } from 'Common/Cache';
|
||||
//import { setFolderETag } from 'Common/Cache';
|
||||
import { addComputablesTo } from 'External/ko';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
|
|
@ -80,8 +80,7 @@ export class MailFolderList extends AbstractViewLeft {
|
|||
setExpandedFolder(folder.fullName, collapsed);
|
||||
|
||||
folder.collapsed(!collapsed);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
stopEvent(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +104,7 @@ export class MailFolderList extends AbstractViewLeft {
|
|||
}
|
||||
/*
|
||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
setFolderHash(folder.fullName, '');
|
||||
setFolderETag(folder.fullName, '');
|
||||
}
|
||||
*/
|
||||
let search = '';
|
||||
|
|
@ -174,15 +173,6 @@ export class MailFolderList extends AbstractViewLeft {
|
|||
moveAction(false);
|
||||
return false;
|
||||
});
|
||||
|
||||
AppUserStore.focusedState.subscribe(value => {
|
||||
let el = qs('li a.focused');
|
||||
el?.classList.remove('focused');
|
||||
if (Scope.FolderList === value) {
|
||||
el = qs('li a.selected');
|
||||
el?.classList.add('focused');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollToFocused() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { ComposeType, FolderType, MessageSetAction } from 'Common/EnumsUser';
|
|||
import { doc,
|
||||
leftPanelDisabled, toggleLeftPanel,
|
||||
Settings, SettingsCapa,
|
||||
addEventsListeners,
|
||||
addEventsListeners, stopEvent,
|
||||
addShortcut, registerShortcut, formFieldFocused
|
||||
} from 'Common/Globals';
|
||||
|
||||
|
|
@ -24,10 +24,7 @@ import { i18n } from 'Common/Translator';
|
|||
|
||||
import { dropFilesInFolder } from 'Common/Folders';
|
||||
|
||||
import {
|
||||
getFolderFromCacheList,
|
||||
MessageFlagsCache
|
||||
} from 'Common/Cache';
|
||||
import { getFolderFromCacheList } from 'Common/Cache';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
|
@ -90,7 +87,7 @@ export class MailMessageList extends AbstractViewRight {
|
|||
|
||||
this.composeInEdit = ComposePopupView.inEdit;
|
||||
|
||||
this.isMobile = ThemeStore.isMobile;
|
||||
this.isMobile = ThemeStore.isMobile; // Obsolete
|
||||
this.leftPanelDisabled = leftPanelDisabled;
|
||||
this.toggleLeftPanel = toggleLeftPanel;
|
||||
|
||||
|
|
@ -146,10 +143,6 @@ export class MailMessageList extends AbstractViewRight {
|
|||
return c && MessagelistUserStore().length > c;
|
||||
},
|
||||
|
||||
mobileCheckedStateShow: () => ThemeStore.isMobile() ? MessagelistUserStore.hasChecked() : 1,
|
||||
|
||||
mobileCheckedStateHide: () => ThemeStore.isMobile() ? !MessagelistUserStore.hasChecked() : 1,
|
||||
|
||||
listGrouped: () => {
|
||||
let uid = MessagelistUserStore.threadUid(),
|
||||
sort = FolderUserStore.sortMode() || 'DATE';
|
||||
|
|
@ -179,7 +172,7 @@ export class MailMessageList extends AbstractViewRight {
|
|||
rtf = Intl.RelativeTimeFormat
|
||||
? new Intl.RelativeTimeFormat(doc.documentElement.lang, { numeric: "auto" }) : 0;
|
||||
MessagelistUserStore.forEach(msg => {
|
||||
let dt = (new Date(msg.dateTimeStampInUTC() * 1000)),
|
||||
let dt = (new Date(msg.dateTimestamp() * 1000)),
|
||||
date,
|
||||
ymd = Ymd(dt);
|
||||
if (!current || ymd != current.id) {
|
||||
|
|
@ -367,13 +360,19 @@ export class MailMessageList extends AbstractViewRight {
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected messages
|
||||
*/
|
||||
downloadZipCommand() {
|
||||
let hashes = []/*, uids = []*/;
|
||||
// MessagelistUserStore.forEach(message => message.checked() && uids.push(message.uid));
|
||||
MessagelistUserStore.forEach(message => message.checked() && hashes.push(message.requestHash));
|
||||
downloadZip(hashes, null, null, MessagelistUserStore().folder);
|
||||
downloadZip(null, hashes, null, null, MessagelistUserStore().folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download attachments of selected messages
|
||||
*/
|
||||
downloadAttachCommand() {
|
||||
let hashes = [];
|
||||
MessagelistUserStore.forEach(message => {
|
||||
|
|
@ -385,7 +384,7 @@ export class MailMessageList extends AbstractViewRight {
|
|||
});
|
||||
}
|
||||
});
|
||||
downloadZip(hashes);
|
||||
downloadZip(null, hashes);
|
||||
}
|
||||
|
||||
deleteWithoutMoveCommand() {
|
||||
|
|
@ -410,10 +409,9 @@ export class MailMessageList extends AbstractViewRight {
|
|||
}
|
||||
|
||||
moveCommand(vm, event) {
|
||||
if (this.mobileCheckedStateShow()) {
|
||||
if (MessagelistUserStore.hasChecked()) {
|
||||
if (vm && event?.preventDefault) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
stopEvent(event);
|
||||
}
|
||||
|
||||
let b = moveAction();
|
||||
|
|
@ -475,15 +473,11 @@ export class MailMessageList extends AbstractViewRight {
|
|||
folder.unreadEmails(0);
|
||||
}
|
||||
|
||||
MessageFlagsCache.clearFolder(sFolderFullName);
|
||||
|
||||
Remote.request('MessageSetSeenToAll', null, {
|
||||
folder: sFolderFullName,
|
||||
setAction: 1,
|
||||
threadUids: uids.join(',')
|
||||
});
|
||||
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -574,7 +568,7 @@ export class MailMessageList extends AbstractViewRight {
|
|||
fToggle = () => {
|
||||
let layout = SettingsUserStore.layout();
|
||||
setLayoutResizer(top, ClientSideKeyNameMessageListSize,
|
||||
(ThemeStore.isMobile() || Layout.NoPreview === layout)
|
||||
(ThemeStore.isMobile() || !layout)
|
||||
? 0
|
||||
: (Layout.SidePreview === layout ? 'Width' : 'Height')
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ import {
|
|||
|
||||
import {
|
||||
elementById,
|
||||
leftPanelDisabled,
|
||||
keyScopeReal,
|
||||
Settings,
|
||||
SettingsCapa,
|
||||
fireEvent,
|
||||
stopEvent,
|
||||
addShortcut,
|
||||
registerShortcut
|
||||
} from 'Common/Globals';
|
||||
|
|
@ -30,15 +30,12 @@ import { SMAudio } from 'Common/Audio';
|
|||
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
||||
import { MessageFlagsCache } from 'Common/Cache';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
import * as Local from 'Storage/Client';
|
||||
|
||||
|
|
@ -128,7 +125,6 @@ export class MailMessageView extends AbstractViewRight {
|
|||
|
||||
this.message = currentMessage;
|
||||
this.messageLoadingThrottle = MessageUserStore.loading;
|
||||
this.messagesBodiesDom = MessageUserStore.bodiesDom;
|
||||
this.messageError = MessageUserStore.error;
|
||||
|
||||
this.fullScreenMode = isFullscreen;
|
||||
|
|
@ -166,7 +162,7 @@ export class MailMessageView extends AbstractViewRight {
|
|||
|
||||
listAttachments: () => currentMessage()?.attachments()
|
||||
.filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()),
|
||||
hasAttachments: () => this.listAttachments().length,
|
||||
hasAttachments: () => this.listAttachments()?.length,
|
||||
|
||||
canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisibility(),
|
||||
|
||||
|
|
@ -280,23 +276,19 @@ export class MailMessageView extends AbstractViewRight {
|
|||
onBuild(dom) {
|
||||
const eqs = (ev, s) => ev.target.closestWithin(s, dom);
|
||||
dom.addEventListener('click', event => {
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
|
||||
let el = eqs(event, 'a');
|
||||
if (el && 0 === event.button && mailToHelper(el.href)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
stopEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (eqs(event, '.attachmentsPlace .showPreview')) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
el = eqs(event, '.attachmentsPlace .showPreplay');
|
||||
if (el) {
|
||||
event.stopPropagation();
|
||||
stopEvent(event);
|
||||
const attachment = ko.dataFor(el);
|
||||
if (attachment && SMAudio.supported) {
|
||||
switch (true) {
|
||||
|
|
@ -343,13 +335,6 @@ export class MailMessageView extends AbstractViewRight {
|
|||
}
|
||||
});
|
||||
|
||||
AppUserStore.focusedState.subscribe(value => {
|
||||
if (Scope.MessageView !== value) {
|
||||
this.scrollMessageToTop();
|
||||
this.scrollMessageToLeft();
|
||||
}
|
||||
});
|
||||
|
||||
keyScopeReal.subscribe(value => this.messageDomFocused(Scope.MessageView === value));
|
||||
|
||||
// initShortcuts
|
||||
|
|
@ -459,10 +444,12 @@ export class MailMessageView extends AbstractViewRight {
|
|||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
MessageUserStore.bodiesDom(dom.querySelector('.bodyText'));
|
||||
}
|
||||
|
||||
scrollMessageToTop() {
|
||||
oMessageScrollerDom().scrollTop = (50 < oMessageScrollerDom().scrollTop) ? 50 : 0;
|
||||
oMessageScrollerDom().scrollTop = 0;
|
||||
}
|
||||
|
||||
scrollMessageToLeft() {
|
||||
|
|
@ -479,7 +466,9 @@ export class MailMessageView extends AbstractViewRight {
|
|||
const hashes = (currentMessage()?.attachments || [])
|
||||
.map(item => item?.checked() /*&& !item?.isLinked()*/ ? item.download : '')
|
||||
.filter(v => v);
|
||||
downloadZip(hashes,
|
||||
downloadZip(
|
||||
currentMessage().subject(),
|
||||
hashes,
|
||||
() => this.downloadAsZipError(true),
|
||||
this.downloadAsZipLoading
|
||||
);
|
||||
|
|
@ -524,8 +513,6 @@ export class MailMessageView extends AbstractViewRight {
|
|||
if (!iError) {
|
||||
oMessage.flags.push('$mdnsent');
|
||||
// oMessage.flags.valueHasMutated();
|
||||
MessageFlagsCache.store(oMessage);
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
}
|
||||
}, {
|
||||
messageFolder: oMessage.folder,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { settings } from 'Common/Links';
|
||||
|
||||
import { mailbox } from 'Common/Links';
|
||||
import { getFolderInboxName } from 'Common/Cache';
|
||||
import { AbstractViewLeft } from 'Knoin/AbstractViews';
|
||||
|
||||
export class SettingsMenuUserView extends AbstractViewLeft {
|
||||
|
|
@ -15,4 +16,8 @@ export class SettingsMenuUserView extends AbstractViewLeft {
|
|||
link(route) {
|
||||
return settings(route);
|
||||
}
|
||||
|
||||
backToInbox() {
|
||||
hasher.setHash(mailbox(getFolderInboxName()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { mailbox } from 'Common/Links';
|
||||
import { getFolderInboxName } from 'Common/Cache';
|
||||
import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals';
|
||||
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
|
|
@ -11,7 +9,6 @@ export class SettingsPaneUserView extends AbstractViewRight {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.isMobile = ThemeStore.isMobile;
|
||||
this.leftPanelDisabled = leftPanelDisabled;
|
||||
this.toggleLeftPanel = toggleLeftPanel;
|
||||
}
|
||||
|
|
@ -25,8 +22,4 @@ export class SettingsPaneUserView extends AbstractViewRight {
|
|||
ThemeStore.isMobile() && !event.target.closestWithin('.toggleLeft', dom) && leftPanelDisabled(true)
|
||||
);
|
||||
}
|
||||
|
||||
backToInbox() {
|
||||
hasher.setHash(mailbox(getFolderInboxName()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,10 @@ import { KeyboardShortcutsHelpPopupView } from 'View/Popup/KeyboardShortcutsHelp
|
|||
import { AccountPopupView } from 'View/Popup/Account';
|
||||
import { ContactsPopupView } from 'View/Popup/Contacts';
|
||||
|
||||
import { doc, leftPanelDisabled, fireEvent, SettingsCapa, registerShortcut } from 'Common/Globals';
|
||||
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { fireEvent, stopEvent, SettingsCapa, registerShortcut } from 'Common/Globals';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
//import { clearCache } from 'Common/Cache';
|
||||
//import { koComputable } from 'External/ko';
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
|
||||
|
|
@ -56,8 +53,7 @@ export class SystemDropDownUserView extends AbstractViewRight {
|
|||
let email = account?.email;
|
||||
if (email && 0 === event.button && AccountUserStore.email() != email) {
|
||||
AccountUserStore.loading(true);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
stopEvent(event);
|
||||
Remote.request('AccountSwitch',
|
||||
(iError/*, oData*/) => {
|
||||
if (iError) {
|
||||
|
|
@ -69,10 +65,8 @@ export class SystemDropDownUserView extends AbstractViewRight {
|
|||
} else {
|
||||
/* // Not working yet
|
||||
forEachObjectEntry(oData.Result, (key, value) => rl.settings.set(key, value));
|
||||
clearCache();
|
||||
// MessageUserStore.setMessage();
|
||||
// MessageUserStore.purgeMessageBodyCache();
|
||||
// MessageUserStore.hideMessageBodies();
|
||||
// MessageUserStore.message();
|
||||
// MessageUserStore.purgeCache();
|
||||
MessagelistUserStore([]);
|
||||
// FolderUserStore.folderList([]);
|
||||
loadFolders(value => {
|
||||
|
|
@ -113,14 +107,6 @@ export class SystemDropDownUserView extends AbstractViewRight {
|
|||
this.allowContacts && showScreenPopup(ContactsPopupView);
|
||||
}
|
||||
|
||||
toggleLayout()
|
||||
{
|
||||
const mobile = !ThemeStore.isMobile();
|
||||
doc.cookie = 'rllayout=' + (mobile ? 'mobile' : 'desktop') + '; samesite=strict';
|
||||
ThemeStore.isMobile(mobile);
|
||||
leftPanelDisabled(mobile);
|
||||
}
|
||||
|
||||
logoutClick() {
|
||||
rl.app.logout();
|
||||
}
|
||||
|
|
|
|||
138
dev/boot.js
138
dev/boot.js
|
|
@ -3,30 +3,26 @@
|
|||
const
|
||||
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
|
||||
eId = id => doc.getElementById('rl-'+id),
|
||||
app = eId('app'),
|
||||
admin = app && '1' == app.dataset.admin,
|
||||
layout = doc.cookie.match(/(^|;) ?rllayout=([^;]+)/) || '',
|
||||
admin = '1' == eId('app')?.dataset?.admin,
|
||||
|
||||
showError = msg => {
|
||||
let div = eId('loading-error');
|
||||
div.append(msg);
|
||||
toggle = div => {
|
||||
eId('loading').hidden = true;
|
||||
div.hidden = false;
|
||||
},
|
||||
showError = msg => {
|
||||
let div = eId('loading-error');
|
||||
div.append(msg);
|
||||
toggle(div);
|
||||
},
|
||||
|
||||
loadScript = src => {
|
||||
if (!src) {
|
||||
throw new Error('src should not be empty.');
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
loadScript = src => src ? new Promise((resolve, reject) => {
|
||||
const script = doc.createElement('script');
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject('Failed loading ' + src);
|
||||
script.src = src;
|
||||
// script.async = true;
|
||||
doc.head.append(script);
|
||||
});
|
||||
};
|
||||
}) : Promise.reject('src is empty');
|
||||
|
||||
try {
|
||||
let smctoken = doc.cookie.match(/(^|;) ?smctoken=([^;]+)/);
|
||||
|
|
@ -37,8 +33,6 @@ try {
|
|||
smctoken = encodeURIComponent(btoa(String.fromCharCode(...data)));
|
||||
}
|
||||
localStorage.setItem('smctoken', smctoken);
|
||||
// doc.cookie = `smctoken=${smctoken};path=${doc.location.pathname};samesite=strict;secure";
|
||||
// doc.cookie = 'smctoken='+smctoken+";path=/;samesite=lax";
|
||||
doc.cookie = 'smctoken='+smctoken+";path=/;samesite=strict";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -46,8 +40,6 @@ try {
|
|||
|
||||
let RL_APP_DATA = {};
|
||||
|
||||
doc.documentElement.classList.toggle('rl-mobile', 'mobile' === layout[2] || (!layout && 1000 > innerWidth));
|
||||
|
||||
window.rl = {
|
||||
adminArea: () => admin,
|
||||
|
||||
|
|
@ -58,9 +50,94 @@ window.rl = {
|
|||
},
|
||||
|
||||
setTitle: title =>
|
||||
doc.title = (title || '') + (RL_APP_DATA.Title ? (title ? ' - ' : '') + RL_APP_DATA.Title : ''),
|
||||
doc.title = (title || '') + (RL_APP_DATA.title ? (title ? ' - ' : '') + RL_APP_DATA.title : ''),
|
||||
|
||||
initData: appData => {
|
||||
setData: appData => {
|
||||
RL_APP_DATA = appData;
|
||||
rl.app.refresh();
|
||||
},
|
||||
|
||||
loadScript: loadScript,
|
||||
|
||||
fetch: (resource, init, postData) => {
|
||||
init = Object.assign({
|
||||
mode: 'same-origin',
|
||||
cache: 'no-cache',
|
||||
redirect: 'error',
|
||||
referrerPolicy: 'no-referrer',
|
||||
credentials: 'same-origin',
|
||||
headers: {}
|
||||
}, init);
|
||||
let asJSON = 1,
|
||||
XToken = RL_APP_DATA.System?.token,
|
||||
object = {};
|
||||
if (postData) {
|
||||
init.method = 'POST';
|
||||
if (postData instanceof FormData) {
|
||||
postData.forEach((value, key) => {
|
||||
if (value instanceof File) {
|
||||
asJSON = 0;
|
||||
} else if (!Reflect.has(object, key)) {
|
||||
object[key] = value;
|
||||
} else {
|
||||
Array.isArray(object[key]) || (object[key] = [object[key]]);
|
||||
object[key].push(value);
|
||||
}
|
||||
});
|
||||
if (asJSON) {
|
||||
postData = object;
|
||||
// postData.XToken = XToken;
|
||||
} else {
|
||||
XToken && postData.set('XToken', XToken);
|
||||
}
|
||||
}
|
||||
if (asJSON) {
|
||||
init.headers['Content-Type'] = 'application/json';
|
||||
postData = JSON.stringify(postData);
|
||||
}
|
||||
init.body = postData;
|
||||
}
|
||||
XToken && (init.headers['X-SM-Token'] = XToken);
|
||||
// init.headers = new Headers(init.headers);
|
||||
return fetch(resource, init);
|
||||
},
|
||||
|
||||
fetchJSON: (resource, init, postData) => {
|
||||
init = Object.assign({ headers: {} }, init);
|
||||
init.headers.Accept = 'application/json';
|
||||
return rl.fetch(resource, init, postData).then(response => {
|
||||
if (response.ok) {
|
||||
/* TODO: use this for non-developers?
|
||||
response.clone()
|
||||
let data = response.text();
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// console.log(data);
|
||||
return Promise.reject(Notifications.JsonParse);
|
||||
return {
|
||||
Result: false,
|
||||
ErrorCode: 952, // Notifications.JsonParse
|
||||
ErrorMessage: e.message,
|
||||
ErrorMessageAdditional: data
|
||||
}
|
||||
}
|
||||
*/
|
||||
return response.json();
|
||||
}
|
||||
return Promise.reject('Network response error: ' + response.status);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!navigator.cookieEnabled) {
|
||||
toggle(eId('NoCookie'));
|
||||
} else if (![].flat) {
|
||||
toggle(eId('BadBrowser'));
|
||||
} else {
|
||||
rl.fetchJSON(qUri(`${admin ? 'Admin' : ''}AppData/0/${Math.random().toString().slice(2)}/`))
|
||||
.then(appData => {
|
||||
RL_APP_DATA = appData;
|
||||
const url = appData.StaticLibsJs,
|
||||
cb = () => rl.app.bootstart();
|
||||
|
|
@ -68,31 +145,14 @@ window.rl = {
|
|||
.then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`)))
|
||||
.then(() => appData.PluginsLink ? loadScript(qUri(appData.PluginsLink)) : Promise.resolve())
|
||||
.then(() => rl.app
|
||||
? cb()
|
||||
: doc.addEventListener('readystatechange', () => 'complete' == doc.readyState && cb())
|
||||
? cb()
|
||||
: doc.addEventListener('readystatechange', () => 'complete' == doc.readyState && cb())
|
||||
)
|
||||
.catch(e => {
|
||||
showError(e);
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
setData: appData => {
|
||||
RL_APP_DATA = appData;
|
||||
rl.app.refresh();
|
||||
},
|
||||
|
||||
loadScript: loadScript
|
||||
};
|
||||
|
||||
if (!navigator.cookieEnabled) {
|
||||
eId('loading').hidden = true;
|
||||
eId('NoCookie').hidden = false;
|
||||
} else if (![].flat) {
|
||||
eId('loading').hidden = true;
|
||||
eId('BadBrowser').hidden = false;
|
||||
} else {
|
||||
loadScript(qUri(`${admin ? 'Admin' : ''}AppData/0/${Math.random().toString().slice(2)}/`))
|
||||
})
|
||||
.catch(e => showError(e));
|
||||
}
|
||||
|
||||
|
|
|
|||
64
dev/bootstrap.js
vendored
64
dev/bootstrap.js
vendored
|
|
@ -1,10 +1,6 @@
|
|||
import { Settings } from 'Common/Globals';
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
||||
import { root } from 'Common/Links';
|
||||
|
||||
import { isArray } from 'Common/Utils';
|
||||
|
||||
export default App => {
|
||||
|
||||
rl.app = App;
|
||||
|
|
@ -33,64 +29,4 @@ export default App => {
|
|||
on: () => hasher.active = true
|
||||
};
|
||||
|
||||
rl.fetch = (resource, init, postData) => {
|
||||
init = Object.assign({
|
||||
mode: 'same-origin',
|
||||
cache: 'no-cache',
|
||||
redirect: 'error',
|
||||
referrerPolicy: 'no-referrer',
|
||||
credentials: 'same-origin',
|
||||
headers: {}
|
||||
}, init);
|
||||
if (postData) {
|
||||
init.method = 'POST';
|
||||
init.headers['Content-Type'] = 'application/json';
|
||||
if (postData instanceof FormData) {
|
||||
const object = {};
|
||||
postData.forEach((value, key) => {
|
||||
if (!Reflect.has(object, key)){
|
||||
object[key] = value;
|
||||
} else {
|
||||
isArray(object[key]) || (object[key] = [object[key]]);
|
||||
object[key].push(value);
|
||||
}
|
||||
});
|
||||
postData = object;
|
||||
}
|
||||
postData.XToken = Settings.app('token');
|
||||
init.body = JSON.stringify(postData);
|
||||
}
|
||||
init.headers['X-SM-Token'] = Settings.app('token');
|
||||
// init.headers = new Headers(init.headers);
|
||||
return fetch(resource, init);
|
||||
};
|
||||
|
||||
rl.fetchJSON = (resource, init, postData) => {
|
||||
init = Object.assign({ headers: {} }, init);
|
||||
init.headers.Accept = 'application/json';
|
||||
return rl.fetch(resource, init, postData).then(response => {
|
||||
if (!response.ok) {
|
||||
return Promise.reject('Network response error: ' + response.status);
|
||||
}
|
||||
/* TODO: use this for non-developers?
|
||||
response.clone()
|
||||
let data = response.text();
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// console.log(data);
|
||||
return Promise.reject(Notification.JsonParse);
|
||||
return {
|
||||
Result: false,
|
||||
ErrorCode: 952, // Notification.JsonParse
|
||||
ErrorMessage: e.message,
|
||||
ErrorMessageAdditional: data
|
||||
}
|
||||
}
|
||||
*/
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
};
|
||||
|
|
|
|||
8
dev/prototype.js
vendored
8
dev/prototype.js
vendored
|
|
@ -53,7 +53,7 @@
|
|||
return function(...args) {
|
||||
timer && clearTimeout(timer);
|
||||
timer = setTimeout(()=>{
|
||||
func.apply(this, args)
|
||||
func.apply(this, args);
|
||||
timer = 0;
|
||||
}, ms);
|
||||
};
|
||||
|
|
@ -68,12 +68,10 @@
|
|||
Function.prototype.throttle = function(ms) {
|
||||
let func = this, timer;
|
||||
return function(...args) {
|
||||
if (!timer) {
|
||||
timer = setTimeout(()=>{
|
||||
func.apply(this, args)
|
||||
timer = timer || setTimeout(()=>{
|
||||
func.apply(this, args);
|
||||
timer = 0;
|
||||
}, ms);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
This app packages SnappyMail <upstream>2.26.1</upstream>.
|
||||
This app packages SnappyMail <upstream>2.26.4</upstream>.
|
||||
|
||||
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RUN mkdir -p /app/code
|
|||
WORKDIR /app/code
|
||||
|
||||
# If you change the extraction below, be sure to test on scaleway
|
||||
VERSION=2.26.1
|
||||
VERSION=2.26.4
|
||||
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
|
||||
unzip /tmp/snappymail.zip -d /app/code && \
|
||||
rm /tmp/snappymail.zip && \
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<id>snappymail</id>
|
||||
<name>SnappyMail</name>
|
||||
<summary>SnappyMail Webmail</summary>
|
||||
<version>2.26.1</version>
|
||||
<version>2.26.4</version>
|
||||
<licence>agpl</licence>
|
||||
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**
|
||||
|
|
@ -39,7 +39,7 @@ There, click on the link to go to the SnappyMail admin panel.
|
|||
<repository>https://github.com/the-djmaze/snappymail/tree/master/integrations/nextcloud</repository>
|
||||
<bugs>https://github.com/the-djmaze/snappymail/issues</bugs>
|
||||
<dependencies>
|
||||
<php min-version="7.4" min-int-size="64" />
|
||||
<php min-version="7.4" min-int-size="32" />
|
||||
<lib>mbstring</lib>
|
||||
<lib>zlib</lib>
|
||||
<!--
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
|
|||
function __construct() {
|
||||
$CSP = \RainLoop\Api::getCSP();
|
||||
|
||||
$this->allowedScriptDomains = \array_unique(\array_merge($this->allowedScriptDomains, $CSP->script));
|
||||
$this->allowedScriptDomains = \array_unique(\array_merge($this->allowedScriptDomains, $CSP->get('script-src')));
|
||||
$this->allowedScriptDomains = \array_diff($this->allowedScriptDomains, ["'unsafe-inline'", "'unsafe-eval'"]);
|
||||
|
||||
// Nextcloud only sets 'strict-dynamic' when browserSupportsCspV3() ?
|
||||
|
|
@ -24,12 +24,12 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
|
|||
? $this->useStrictDynamic(true) // NC24+
|
||||
: $this->addAllowedScriptDomain("'strict-dynamic'");
|
||||
|
||||
$this->allowedImageDomains = \array_unique(\array_merge($this->allowedImageDomains, $CSP->img));
|
||||
$this->allowedImageDomains = \array_unique(\array_merge($this->allowedImageDomains, $CSP->get('img-src')));
|
||||
|
||||
$this->allowedStyleDomains = \array_unique(\array_merge($this->allowedStyleDomains, $CSP->style));
|
||||
$this->allowedStyleDomains = \array_unique(\array_merge($this->allowedStyleDomains, $CSP->get('style-src')));
|
||||
$this->allowedStyleDomains = \array_diff($this->allowedStyleDomains, ["'unsafe-inline'"]);
|
||||
|
||||
$this->allowedFrameDomains = \array_unique(\array_merge($this->allowedFrameDomains, $CSP->frame));
|
||||
$this->allowedFrameDomains = \array_unique(\array_merge($this->allowedFrameDomains, $CSP->get('frame-src')));
|
||||
|
||||
$this->reportTo = \array_unique(\array_merge($this->reportTo, $CSP->report_to));
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue