mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03:00
Merge branch 'master' into UserMailTemplates
# Conflicts: # dev/App/User.js # dev/Common/Enums.js # dev/Screen/User/Settings.js # dev/Settings/Admin/General.js # snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php # snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php # snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php
This commit is contained in:
commit
f7f56b3789
794 changed files with 96444 additions and 74188 deletions
|
|
@ -15,7 +15,7 @@ RUN pecl install xxtea-1.0.11 && \
|
||||||
|
|
||||||
RUN docker-php-ext-configure intl && \
|
RUN docker-php-ext-configure intl && \
|
||||||
docker-php-ext-configure ldap && \
|
docker-php-ext-configure ldap && \
|
||||||
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
|
docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ && \
|
||||||
docker-php-ext-install opcache pdo_mysql zip intl gd ldap
|
docker-php-ext-install opcache pdo_mysql zip intl gd ldap
|
||||||
|
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ RUN mkdir -p /usr/share/man/man1/ /usr/share/man/man3/ /usr/share/man/man7/ && \
|
||||||
RUN php -m && \
|
RUN php -m && \
|
||||||
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
||||||
docker-php-ext-configure intl && \
|
docker-php-ext-configure intl && \
|
||||||
docker-php-ext-configure gd --with-freetype-dir=/usr/include --with-jpeg-dir=/usr/include/ && \
|
docker-php-ext-configure gd --with-freetype --with-jpeg && \
|
||||||
docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \
|
docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \
|
||||||
php -m
|
php -m
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ allow_additional_identities = On
|
||||||
; Number of messages displayed on page by default
|
; Number of messages displayed on page by default
|
||||||
messages_per_page = 20
|
messages_per_page = 20
|
||||||
|
|
||||||
|
; Mark message read after N seconds
|
||||||
|
message_read_delay = 5
|
||||||
|
|
||||||
; File size limit (MB) for file upload on compose screen
|
; File size limit (MB) for file upload on compose screen
|
||||||
; 0 for unlimited.
|
; 0 for unlimited.
|
||||||
attachment_size_limit = 2
|
attachment_size_limit = 2
|
||||||
|
|
@ -68,6 +71,8 @@ hide_x_mailer_header = On
|
||||||
admin_panel_host = ""
|
admin_panel_host = ""
|
||||||
admin_panel_key = "admin"
|
admin_panel_key = "admin"
|
||||||
content_security_policy = ""
|
content_security_policy = ""
|
||||||
|
csp_report = Off
|
||||||
|
encrypt_cipher = "aes-256-cbc-hmac-sha1"
|
||||||
|
|
||||||
[ssl]
|
[ssl]
|
||||||
; Require verification of SSL certificate used.
|
; Require verification of SSL certificate used.
|
||||||
|
|
@ -86,18 +91,12 @@ capath = ""
|
||||||
client_cert = ""
|
client_cert = ""
|
||||||
|
|
||||||
[capa]
|
[capa]
|
||||||
composer = On
|
|
||||||
contacts = On
|
contacts = On
|
||||||
settings = On
|
|
||||||
quota = On
|
quota = On
|
||||||
help = On
|
|
||||||
reload = On
|
|
||||||
search = On
|
search = On
|
||||||
search_adv = On
|
search_adv = On
|
||||||
x-templates = Off
|
|
||||||
dangerous_actions = On
|
dangerous_actions = On
|
||||||
message_actions = On
|
message_actions = On
|
||||||
messagelist_actions = On
|
|
||||||
attachments_actions = On
|
attachments_actions = On
|
||||||
|
|
||||||
[login]
|
[login]
|
||||||
|
|
@ -134,6 +133,7 @@ view_editor_type = "Html"
|
||||||
view_layout = 1
|
view_layout = 1
|
||||||
view_use_checkboxes = On
|
view_use_checkboxes = On
|
||||||
autologout = 30
|
autologout = 30
|
||||||
|
view_html = On
|
||||||
show_images = Off
|
show_images = Off
|
||||||
contacts_autosave = On
|
contacts_autosave = On
|
||||||
mail_use_threads = Off
|
mail_use_threads = Off
|
||||||
|
|
@ -144,6 +144,17 @@ mail_reply_same_folder = Off
|
||||||
; Enable logging
|
; Enable logging
|
||||||
enable = Off
|
enable = Off
|
||||||
|
|
||||||
|
; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest).
|
||||||
|
; 0 = Emergency
|
||||||
|
; 1 = Alert
|
||||||
|
; 2 = Critical
|
||||||
|
; 3 = Error
|
||||||
|
; 4 = Warning
|
||||||
|
; 5 = Notice
|
||||||
|
; 6 = Informational
|
||||||
|
; 7 = Debug
|
||||||
|
level = 4
|
||||||
|
|
||||||
; Logs entire request only if error occured (php requred)
|
; Logs entire request only if error occured (php requred)
|
||||||
write_on_error_only = Off
|
write_on_error_only = Off
|
||||||
|
|
||||||
|
|
@ -193,6 +204,9 @@ auth_logging = Off
|
||||||
auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt"
|
auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt"
|
||||||
auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"
|
auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"
|
||||||
|
|
||||||
|
; Enable auth logging to syslog for fail2ban
|
||||||
|
auth_syslog = On
|
||||||
|
|
||||||
[debug]
|
[debug]
|
||||||
; Special option required for development purposes
|
; Special option required for development purposes
|
||||||
enable = Off
|
enable = Off
|
||||||
|
|
@ -206,7 +220,7 @@ enable = On
|
||||||
; Additional caching key. If changed, cache is purged
|
; Additional caching key. If changed, cache is purged
|
||||||
index = "v1"
|
index = "v1"
|
||||||
|
|
||||||
; Can be: files, APC, memcache, redis (beta)
|
; Can be: files, APCU, memcache, redis (beta)
|
||||||
fast_cache_driver = "files"
|
fast_cache_driver = "files"
|
||||||
|
|
||||||
; Additional caching key. If changed, fast cache is purged
|
; Additional caching key. If changed, fast cache is purged
|
||||||
|
|
@ -222,12 +236,10 @@ http_expires = 3600
|
||||||
server_uids = On
|
server_uids = On
|
||||||
|
|
||||||
[labs]
|
[labs]
|
||||||
update_channel = "stable"
|
allow_prefetch = Off
|
||||||
allow_prefetch = On
|
|
||||||
allow_smart_html_links = On
|
|
||||||
cache_system_data = On
|
cache_system_data = On
|
||||||
date_from_headers = On
|
date_from_headers = On
|
||||||
autocreate_system_folders = On
|
autocreate_system_folders = Off
|
||||||
allow_message_append = Off
|
allow_message_append = Off
|
||||||
login_fault_delay = 1
|
login_fault_delay = 1
|
||||||
log_ajax_response_write_limit = 300
|
log_ajax_response_write_limit = 300
|
||||||
|
|
@ -239,7 +251,6 @@ use_mobile_version_for_tablets = Off
|
||||||
use_app_debug_css = Off
|
use_app_debug_css = Off
|
||||||
use_imap_sort = On
|
use_imap_sort = On
|
||||||
use_imap_force_selection = Off
|
use_imap_force_selection = Off
|
||||||
use_imap_list_subscribe = On
|
|
||||||
use_imap_thread = On
|
use_imap_thread = On
|
||||||
use_imap_move = Off
|
use_imap_move = Off
|
||||||
use_imap_expunge_all_on_delete = Off
|
use_imap_expunge_all_on_delete = Off
|
||||||
|
|
@ -254,18 +265,16 @@ imap_message_all_headers = Off
|
||||||
imap_large_thread_limit = 50
|
imap_large_thread_limit = 50
|
||||||
imap_folder_list_limit = 200
|
imap_folder_list_limit = 200
|
||||||
imap_show_login_alert = On
|
imap_show_login_alert = On
|
||||||
imap_use_auth_plain = On
|
imap_use_list_status = On
|
||||||
imap_use_auth_cram_md5 = Off
|
imap_timeout = 300
|
||||||
smtp_show_server_errors = Off
|
smtp_show_server_errors = Off
|
||||||
smtp_use_auth_plain = On
|
smtp_timeout = 60
|
||||||
smtp_use_auth_cram_md5 = Off
|
|
||||||
sieve_utf8_folder_name = On
|
|
||||||
sieve_auth_plain_initial = On
|
sieve_auth_plain_initial = On
|
||||||
sieve_allow_fileinto_inbox = Off
|
sieve_allow_fileinto_inbox = Off
|
||||||
imap_timeout = 300
|
|
||||||
smtp_timeout = 60
|
|
||||||
sieve_timeout = 10
|
sieve_timeout = 10
|
||||||
domain_list_limit = 99
|
sasl_allow_plain = On
|
||||||
|
sasl_allow_scram_sha = Off
|
||||||
|
sasl_allow_cram_md5 = Off
|
||||||
mail_func_clear_headers = On
|
mail_func_clear_headers = On
|
||||||
mail_func_additional_parameters = Off
|
mail_func_additional_parameters = Off
|
||||||
favicon_status = On
|
favicon_status = On
|
||||||
|
|
@ -285,12 +294,12 @@ cookie_default_path = ""
|
||||||
cookie_default_secure = Off
|
cookie_default_secure = Off
|
||||||
check_new_messages = On
|
check_new_messages = On
|
||||||
replace_env_in_configuration = ""
|
replace_env_in_configuration = ""
|
||||||
startup_url = ""
|
|
||||||
strict_html_parser = Off
|
strict_html_parser = Off
|
||||||
boundary_prefix = ""
|
boundary_prefix = ""
|
||||||
|
kolab_enabled = Off
|
||||||
dev_email = ""
|
dev_email = ""
|
||||||
dev_password = ""
|
dev_password = ""
|
||||||
|
|
||||||
[version]
|
[version]
|
||||||
current = "2.7.1"
|
current = "2.13.4"
|
||||||
saved = "Mon, 23 Aug 2021 07:55:13 +0000"
|
saved = "Fri, 04 Mar 2022 08:55:26 +0000"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
es6: true
|
es2020: true
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
// SnappyMail
|
// SnappyMail
|
||||||
|
|
@ -35,7 +35,9 @@ module.exports = {
|
||||||
// vendors/jua
|
// vendors/jua
|
||||||
'Jua': "readonly",
|
'Jua': "readonly",
|
||||||
// vendors/bootstrap/bootstrap.native.js
|
// vendors/bootstrap/bootstrap.native.js
|
||||||
'BSN': "readonly"
|
'BSN': "readonly",
|
||||||
|
// Mailvelope
|
||||||
|
'mailvelope': "readonly"
|
||||||
},
|
},
|
||||||
// http://eslint.org/docs/rules/
|
// http://eslint.org/docs/rules/
|
||||||
rules: {
|
rules: {
|
||||||
|
|
|
||||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1,3 +1,2 @@
|
||||||
github: the-djmaze
|
|
||||||
community_bridge: SnappyMail
|
community_bridge: SnappyMail
|
||||||
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
||||||
|
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -18,3 +18,11 @@
|
||||||
/include.php
|
/include.php
|
||||||
.idea/
|
.idea/
|
||||||
.env
|
.env
|
||||||
|
/test
|
||||||
|
/public_html
|
||||||
|
/vendors/knockout/spec
|
||||||
|
/vendors/openpgp-5
|
||||||
|
!/vendors/openpgp-5/dist
|
||||||
|
/vendors/vanillaqr.js/
|
||||||
|
/integrations/nextcloud/rainloop
|
||||||
|
/integrations/owncloud/rainloop
|
||||||
|
|
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "vendors/openpgp-5"]
|
||||||
|
path = vendors/openpgp-5
|
||||||
|
url = git@github.com:the-djmaze/openpgpjs.git
|
||||||
86
README.md
86
README.md
|
|
@ -34,7 +34,7 @@ And don't forget to read the [RainLoop documentation](https://www.rainloop.net/d
|
||||||
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
|
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
|
||||||
http://www.gnu.org/licenses/agpl-3.0.html
|
http://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
Copyright (c) 2020 - 2021 SnappyMail
|
Copyright (c) 2020 - 2022 SnappyMail
|
||||||
Copyright (c) 2013 - 2021 RainLoop
|
Copyright (c) 2013 - 2021 RainLoop
|
||||||
|
|
||||||
## Modifications
|
## Modifications
|
||||||
|
|
@ -45,24 +45,25 @@ This fork of RainLoop has the following changes:
|
||||||
* Admin uses password_hash/password_verify
|
* Admin uses password_hash/password_verify
|
||||||
* Auth failed attempts written to syslog
|
* Auth failed attempts written to syslog
|
||||||
* Added Fail2ban instructions
|
* Added Fail2ban instructions
|
||||||
* ES2015
|
* ES2018
|
||||||
* PHP 7.3+ required
|
* PHP 7.3+ required
|
||||||
* PHP mbstring extension required
|
* PHP mbstring extension required
|
||||||
* PHP replaced pclZip with PharData and ZipArchive
|
* PHP replaced pclZip with PharData and ZipArchive
|
||||||
* PHP yaml extension else use the old Spyc
|
* Dark mode
|
||||||
* Added option to remove background/font colors from messages for real "dark mode"
|
* Added option to remove background/font colors from messages for real "dark mode"
|
||||||
* Removed BackwardCapability (class \RainLoop\Account)
|
* Removed BackwardCapability (class \RainLoop\Account)
|
||||||
* Removed ChangePassword (re-implemented as plugin)
|
* Removed ChangePassword (re-implemented as plugin)
|
||||||
* Removed OAuth support
|
|
||||||
* Removed POP3 support
|
* Removed POP3 support
|
||||||
* Removed background video support
|
* Removed background video support
|
||||||
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
||||||
|
* Removed Spyc yaml
|
||||||
* Replaced gulp-uglify with gulp-terser
|
* Replaced gulp-uglify with gulp-terser
|
||||||
* CRLF => LF line endings
|
* CRLF => LF line endings
|
||||||
* Embed boot.js and boot.css into index.html
|
* Embed boot.js and boot.css into index.html
|
||||||
* Ongoing removal of old JavaScript code (things are native these days)
|
* Ongoing removal of old JavaScript code (things are native these days)
|
||||||
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
||||||
* Split Admin specific JavaScript code from User code
|
* Split Admin specific JavaScript code from User code
|
||||||
|
* Split Sieve specific JavaScript code from User code
|
||||||
* JSON reviver
|
* JSON reviver
|
||||||
* Better memory garbage collection management
|
* Better memory garbage collection management
|
||||||
* Added serviceworker for Notifications
|
* Added serviceworker for Notifications
|
||||||
|
|
@ -76,6 +77,16 @@ This fork of RainLoop has the following changes:
|
||||||
* Prevent Google FLoC
|
* Prevent Google FLoC
|
||||||
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
||||||
* Reduced excessive DOM size
|
* Reduced excessive DOM size
|
||||||
|
* Support [Kolab groupware](https://kolab.org/)
|
||||||
|
* Support IMAP RFC 2971 ID extension
|
||||||
|
* Support IMAP RFC 5258 LIST-EXTENDED
|
||||||
|
* Support IMAP RFC 5464 METADATA
|
||||||
|
* Support IMAP RFC 5819 LIST-STATUS
|
||||||
|
* Support IMAP RFC 7628 SASL OAUTHBEARER aka XOAUTH2
|
||||||
|
* Support IMAP4rev2 RFC 9051
|
||||||
|
* Support Sodium and OpenSSL for encryption
|
||||||
|
* Much better PGP support
|
||||||
|
|
||||||
|
|
||||||
### Supported browsers
|
### Supported browsers
|
||||||
|
|
||||||
|
|
@ -96,10 +107,9 @@ The result is faster and smaller download code (good for mobile networks).
|
||||||
* Added dev/prototype.js for some additional features
|
* Added dev/prototype.js for some additional features
|
||||||
* boot.js without webpack overhead
|
* boot.js without webpack overhead
|
||||||
* Modified Jua.js to be without jQuery
|
* Modified Jua.js to be without jQuery
|
||||||
* Replaced ProgressJS with simple native dropin
|
|
||||||
* Replaced Autolinker with simple https/email detection
|
* Replaced Autolinker with simple https/email detection
|
||||||
* Replaced ifvisible.js with simple drop-in replacement
|
* Replaced ifvisible.js with simple drop-in replacement
|
||||||
* Replaced momentToNode with proper HTML5 <time>
|
* Replaced momentToNode with proper HTML5 `<time>`
|
||||||
* Replaced resize listeners with ResizeObserver
|
* Replaced resize listeners with ResizeObserver
|
||||||
* Replaced bootstrap.js with native drop-in replacement
|
* Replaced bootstrap.js with native drop-in replacement
|
||||||
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
|
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
|
||||||
|
|
@ -123,36 +133,35 @@ The result is faster and smaller download code (good for mobile networks).
|
||||||
* Removed momentjs (use Intl)
|
* Removed momentjs (use Intl)
|
||||||
* Removed opentip (use CSS)
|
* Removed opentip (use CSS)
|
||||||
* Removed non-community (aka Prem/Premium/License) code
|
* Removed non-community (aka Prem/Premium/License) code
|
||||||
|
* Removed ProgressJS
|
||||||
|
|
||||||
|
|
||||||
RainLoop 1.15 vs SnappyMail
|
RainLoop 1.15 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.158.025 | 88.633 |
|
|admin.js |2.158.025 | 79.018 |
|
||||||
|app.js |4.215.733 | 446.059 |
|
|app.js |4.215.733 | 407.697 |
|
||||||
|boot.js | 672.433 | 2.856 |
|
|boot.js | 672.433 | 2.025 |
|
||||||
|libs.js | 647.679 | 213.208 |
|
|libs.js | 647.679 | 200.131 |
|
||||||
|
|sieve.js | 0 | 75.642 |
|
||||||
|polyfills.js | 325.908 | 0 |
|
|polyfills.js | 325.908 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.019.778 | 751.041 |
|
|TOTAL |8.019.778 | 764.798 |
|
||||||
|
|
||||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||||
|admin.min.js | 255.514 | 45.597 | 73.899 | 13.933 | 60.674 | 12.463 |
|
|admin.min.js | 255.514 | 39.256 | 73.899 | 13.076 | 60.674 | 11.702 |
|
||||||
|app.min.js | 516.000 | 228.862 |140.430 | 67.769 |110.657 | 57.420 |
|
|app.min.js | 516.000 | 194.148 |140.430 | 62.297 |110.657 | 53.432 |
|
||||||
|boot.min.js | 66.456 | 1.648 | 22.553 | 986 | 20.043 | 822 |
|
|boot.min.js | 66.456 | 1.252 | 22.553 | 782 | 20.043 | 631 |
|
||||||
|libs.min.js | 574.626 | 102.959 |177.280 | 37.514 |151.855 | 33.617 |
|
|libs.min.js | 574.626 | 96.201 |177.280 | 35.522 |151.855 | 31.746 |
|
||||||
|
|sieve.min.js | 0 | 36.632 | 0 | 9.689 | 0 | 8.770 |
|
||||||
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|
||||||
|TOTAL |1.445.204 | 379.066 |425.477 |120.202 |353.301 |104.322 |
|
|TOTAL user |1.189.690 | 291.601 |351.061 | 98.601 |292.627 | 85.809 |
|
||||||
|TOTAL (no admin) |1.189.690 | 333.469 |351.061 |106.269 |292.627 | 91.859 |
|
|TOTAL user sieve |1.189.690 | 328.233 |351.061 |108.290 |292.627 | 94.579 |
|
||||||
|
|TOTAL admin |1.189.690 | 136.709 |351.061 | 49.380 |292.627 | 44.079 |
|
||||||
|
|
||||||
For a user its around 68% smaller and faster than traditional RainLoop.
|
For a user its around 70% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|
||||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
|
||||||
|openpgp.min.js | 330.742 | 293.972 |102.388 | 93.030 | 84.241 | 77.142 |
|
|
||||||
|openpgp.worker | 1.499 | 1.125 | 824 | 567 | 695 | 467 |
|
|
||||||
|
|
||||||
### CSS changes
|
### CSS changes
|
||||||
|
|
||||||
|
|
@ -169,13 +178,7 @@ For a user its around 68% smaller and faster than traditional RainLoop.
|
||||||
* Removed Internet Explorer from normalize.css
|
* Removed Internet Explorer from normalize.css
|
||||||
* Removed node_modules/opentip/css/opentip.css
|
* Removed node_modules/opentip/css/opentip.css
|
||||||
* Removed node_modules/pikaday/css/pikaday.css
|
* Removed node_modules/pikaday/css/pikaday.css
|
||||||
* Removed vendors/bootstrap/less/breadcrumbs.less
|
* Removed unused vendors/bootstrap/less/*
|
||||||
* Removed vendors/bootstrap/less/navbar.less
|
|
||||||
* Removed vendors/bootstrap/less/popovers.less
|
|
||||||
* Removed vendors/bootstrap/less/progress-bars.less
|
|
||||||
* Removed vendors/bootstrap/less/scaffolding.less
|
|
||||||
* Removed vendors/bootstrap/less/sprites.less
|
|
||||||
* Removed vendors/bootstrap/less/tooltip.less
|
|
||||||
* Removed vendors/jquery-nanoscroller/nanoscroller.css
|
* Removed vendors/jquery-nanoscroller/nanoscroller.css
|
||||||
* Removed vendors/jquery-letterfx/jquery-letterfx.min.css
|
* Removed vendors/jquery-letterfx/jquery-letterfx.min.css
|
||||||
* Removed vendors/Progress.js/minified/progressjs.min.css
|
* Removed vendors/Progress.js/minified/progressjs.min.css
|
||||||
|
|
@ -184,12 +187,27 @@ For a user its around 68% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||||
|------------ |-------: |------: |------: |------: |--------: |
|
|------------ |-------: |------: |------: |------: |--------: |
|
||||||
|app.css | 340.334 | 93.382 | 46.959 | 17.277 | 14.966 |
|
|app.css | 340.334 | 80.865 | 46.959 | 16.751 | 14.420 |
|
||||||
|app.min.css | 274.791 | 75.885 | 39.618 | 15.430 | 13.632 |
|
|app.min.css | 274.791 | 65.086 | 39.618 | 14.855 | 13.088 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 1.326 | | 664 | 545 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||||
|admin.css | | 40.355 | | 8.507 | 7.437 |
|
|admin.css | | 29.977 | | 6.795 | 5.900 |
|
||||||
|admin.min.css | | 31.835 | | 7.465 | 6.628 |
|
|admin.min.css | | 24.101 | | 6.167 | 5.421 |
|
||||||
|
|
||||||
|
### PGP
|
||||||
|
RainLoop uses the old OpenPGP.js v2
|
||||||
|
SnappyMail v2.12 uses OpenPGP.js v5, GnuPG and Mailvelope.
|
||||||
|
SnappyMail is able to use and generate ECDSA and EDDSA keys, where RainLoop does not.
|
||||||
|
|
||||||
|
Since SnappyMail tries to achieve the best mobile experience, it forked OpenPGP.js to strip it down.
|
||||||
|
* remove all unused Node.js
|
||||||
|
* remove all old browsers support
|
||||||
|
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.642 |102.388 | 167.112 | 84.241 | 137.447 |
|
||||||
|
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
||||||
|
|
||||||
|
|
||||||
### Squire vs CKEditor
|
### Squire vs CKEditor
|
||||||
|
|
|
||||||
40
SECURITY.md
Normal file
40
SECURITY.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Currently due to the fast development only the latest version receives security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| -------- | --------- |
|
||||||
|
| 2.13.x | ✔ |
|
||||||
|
| < 2.13.0 | ❌ |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security issues or vulnerabilities as an encrypted email to [security@snappymail.eu](mailto:security@snappymail.eu).
|
||||||
|
Your report should be detailed enough with clear steps to reproduce and classify the found vulnerability.
|
||||||
|
|
||||||
|
You can find the PGP public key below and on the major public keyservers like [pgp.key-server.io](https://pgp.key-server.io).
|
||||||
|
```
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
Comment: Type: 255-bit EdDSA
|
||||||
|
Comment: Fingerprint: 445D265124E6072671E64D0733F868A7E35E8277
|
||||||
|
|
||||||
|
mDMEYiE2QRYJKwYBBAHaRw8BAQdAqMrQUm6DddWcQNo0VEjNIu3Q6CfP3nokVv2Y
|
||||||
|
rNQ1avq0LFNuYXBweU1haWwgU2VjdXJpdHkgPHNlY3VyaXR5QHNuYXBweW1haWwu
|
||||||
|
ZXU+iJQEExYKADwWIQREXSZRJOYHJnHmTQcz+Gin416CdwUCYiE2QQIbAwULCQgH
|
||||||
|
AgMiAgEGFQoJCAsCBBYCAwECHgcCF4AACgkQM/hop+NegnfSGgD9GEHpOrvWpBGY
|
||||||
|
dYfvVd/+Lv5d+dFBcPyki9zu9zHfhwkBAL343EF6ZR0XwMlOQu9wu0hT9KBz4g55
|
||||||
|
6D41i0PrEaoBuDgEYiE2QRIKKwYBBAGXVQEFAQEHQMMr9gcVcJ3aiup/tpl8ZXxy
|
||||||
|
aJiJRGkPyNwGI5vxHMpZAwEIB4h4BBgWCgAgFiEERF0mUSTmByZx5k0HM/hop+Ne
|
||||||
|
gncFAmIhNkECGwwACgkQM/hop+NegndVhgD/SVGSKbF4G2W024VpW2tm3zCT+ue+
|
||||||
|
YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
|
||||||
|
=nSOi
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing and Credits
|
||||||
|
|
||||||
|
I will analyze and fix the reported issue as fast as possible.
|
||||||
|
Together with the reporter I plan the disclosure of the found and fixed vulnerability.
|
||||||
|
Credits to the reporter are granted and can be included in all public communication if desired.
|
||||||
33
_include.php
33
_include.php
|
|
@ -4,32 +4,33 @@
|
||||||
|
|
||||||
//header('Strict-Transport-Security: max-age=31536000');
|
//header('Strict-Transport-Security: max-age=31536000');
|
||||||
|
|
||||||
// Uncomment to use gzip encoded output
|
/**
|
||||||
|
* Uncomment to use gzip compressed output
|
||||||
|
*/
|
||||||
//define('USE_GZIP', 1);
|
//define('USE_GZIP', 1);
|
||||||
|
|
||||||
// Uncomment to enable multiple domain installation.
|
/**
|
||||||
|
* Uncomment to use brotli compressed output
|
||||||
|
*/
|
||||||
|
//define('USE_BROTLI', 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uncomment to enable multiple domain installation.
|
||||||
|
*/
|
||||||
//define('MULTIDOMAIN', 1);
|
//define('MULTIDOMAIN', 1);
|
||||||
|
|
||||||
// Uncomment to disable APCU.
|
/**
|
||||||
|
* Uncomment to disable APCU.
|
||||||
|
*/
|
||||||
//define('APP_USE_APCU_CACHE', false);
|
//define('APP_USE_APCU_CACHE', false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom 'data' folder path
|
* Custom 'data' folder path
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
function __get_custom_data_full_path()
|
//define('APP_DATA_FOLDER_PATH', dirname(__DIR__) . '/snappymail-data/');
|
||||||
{
|
//define('APP_DATA_FOLDER_PATH', '/var/external-snappymail-data-folder/');
|
||||||
return '';
|
|
||||||
return dirname(__DIR__) . '/snappymail-data';
|
|
||||||
return '/var/external-snappymail-data-folder';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional configuration file name
|
* Additional configuration file name
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
function __get_additional_configuration_name()
|
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
|
||||||
{
|
|
||||||
return '';
|
|
||||||
return defined('APP_SITE') && 0 < strlen(APP_SITE) ? APP_SITE.'.ini' : '';
|
|
||||||
}
|
|
||||||
|
|
|
||||||
9
build/SnappyMail.asc
Normal file
9
build/SnappyMail.asc
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mDMEYg0atBYJKwYBBAHaRw8BAQdA2S2tvGavChACjtBastsKRThD3rsBW1LUZLmN
|
||||||
|
Zbs4uaG0I1NuYXBweU1haWwgPHJlbGVhc2VzQHNuYXBweW1haWwuZXU+iJQEExYK
|
||||||
|
ADwWIQQQFuRweRRVQvi6EzVIIIuhMpDz6wUCYg0atAIbAwULCQgHAgMiAgEGFQoJ
|
||||||
|
CAsCBBYCAwECHgcCF4AACgkQSCCLoTKQ8+u9SAD/Q/IoAwjUkKDJBPq0RGwCFnl6
|
||||||
|
FG/VHB97CvBSpGOxtIsBAMCwMhWlsaBHAEqbzxiN+cdlMYwV23+SWLUJ/XMFgukE
|
||||||
|
=5ZwF
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
57
build/arch/PKGBUILD
Normal file
57
build/arch/PKGBUILD
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Maintainer: George Rawlinson <george@rawlinson.net.nz>
|
||||||
|
|
||||||
|
pkgname=snappymail
|
||||||
|
pkgver=2.7.2
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="modern PHP webmail client"
|
||||||
|
arch=('any')
|
||||||
|
license=('AGPL3')
|
||||||
|
url="https://github.com/the-djmaze/snappymail"
|
||||||
|
depends=('php-fpm')
|
||||||
|
makedepends=('php', 'nodejs' 'yarn' 'gulp', 'rollup')
|
||||||
|
optdepends=('mariadb: storage backend for contacts'
|
||||||
|
'php-pgsql: storage backend for contacts'
|
||||||
|
'php-sqlite: storage backend for contacts')
|
||||||
|
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz"
|
||||||
|
"$pkgname.sysusers"
|
||||||
|
"$pkgname.tmpfiles")
|
||||||
|
b2sums=('a877041eaa74ca9824323b0ba80b3515205628825ed76fccff1511b2547c27419bae4733d24f8f36e6860c671cb609c4c5f31fa19529a92c628cb714570ff8ae'
|
||||||
|
'e020b2d4bc694ca056f5c15b148c69553ab610b5e1789f52543aa65e098f8097a41709b5b0fc22a6a01088a9d3f14d623b1b6e9ae2570acd4f380f429301c003'
|
||||||
|
'2536e11622895322cc752c6b651811b2122d3ae60099fe609609d7b45ba1ed00ea729c23f344405078698d161dbf9bcaffabf8eff14b740acdce3c681c513318')
|
||||||
|
|
||||||
|
prepare() {
|
||||||
|
sed -i "s/\$sCustomDataPath = '';/\$sCustomDataPath = '\/var\/lib\/$pkgname';/" "$pkgname-$pkgver/$pkgname/v/0.0.0/include.php"
|
||||||
|
|
||||||
|
# create folder for build output
|
||||||
|
mkdir -p build
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$pkgname-$pkgver"
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# build snappymail
|
||||||
|
php release.php --aur
|
||||||
|
bsdtar -x \
|
||||||
|
-C "$srcdir/build" \
|
||||||
|
-f "build/dist/releases/webmail/$pkgver/$pkgname-$pkgver.zip"
|
||||||
|
find . -type d -exec chmod 755 {} \;
|
||||||
|
find . -type f -exec chmod 644 {} \;
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
# directories
|
||||||
|
install -dm755 "$pkgdir/usr/share/snappymail" \
|
||||||
|
"$pkgdir/var/lib/snappymail"
|
||||||
|
|
||||||
|
# application files
|
||||||
|
cp -r "$srcdir/build/snappymail" "$pkgdir/usr/share/snappymail"
|
||||||
|
install -Dm644 -t "$pkgdir/usr/share/snappymail" "$srcdir/build/index.php"
|
||||||
|
|
||||||
|
# data files
|
||||||
|
cp -r "$srcdir/build/data" "$pkgdir/var/lib/snappymail"
|
||||||
|
|
||||||
|
# sysusers
|
||||||
|
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
|
||||||
|
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
|
||||||
|
}
|
||||||
1
build/arch/snappymail.sysusers
Normal file
1
build/arch/snappymail.sysusers
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
u snappymail - "Snappymail User" /var/lib/snappymail
|
||||||
1
build/arch/snappymail.tmpfiles
Normal file
1
build/arch/snappymail.tmpfiles
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Z /var/lib/snappymail - snappymail snappymail
|
||||||
83
build/deb.php
Executable file
83
build/deb.php
Executable file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
echo "\x1b[33;1m === Debian === \x1b[0m\n";
|
||||||
|
|
||||||
|
// Debian Repository
|
||||||
|
define('DEB_SOURCE_DIR', __DIR__ . '/deb');
|
||||||
|
define('DEB_DEST_DIR', DEB_SOURCE_DIR . "/snappymail_{$package->version}-1_all");
|
||||||
|
is_dir(DEB_DEST_DIR) && passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
|
||||||
|
|
||||||
|
$dir = DEB_DEST_DIR . '/DEBIAN';
|
||||||
|
$data = file_get_contents(DEB_SOURCE_DIR . '/DEBIAN/control');
|
||||||
|
$data = str_replace('0.0.0', $package->version, $data);
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
file_put_contents("{$dir}/control", $data);
|
||||||
|
copy(DEB_SOURCE_DIR . '/DEBIAN/postinst', $dir . '/postinst');
|
||||||
|
chmod($dir . '/postinst', 0755);
|
||||||
|
|
||||||
|
$dir = DEB_DEST_DIR . '/var/lib/snappymail';
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
file_put_contents($dir . '/VERSION', $package->version);
|
||||||
|
copy('data/README.md', "{$dir}/README.md");
|
||||||
|
|
||||||
|
$dir = DEB_DEST_DIR . '/usr/share/doc/snappymail';
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
|
||||||
|
copy('CONTRIBUTING.md', "{$dir}/CONTRIBUTING.md");
|
||||||
|
copy('README.md', "{$dir}/README.md");
|
||||||
|
copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
|
||||||
|
//usr/share/doc/snappymail/README.Debian
|
||||||
|
//usr/share/doc/snappymail/changelog.Debian.gz
|
||||||
|
//usr/share/doc/snappymail/copyright
|
||||||
|
|
||||||
|
// Move files into package directory
|
||||||
|
$dir = DEB_DEST_DIR . '/usr/share/snappymail';
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
passthru('cp -r "' . dirname(__DIR__) . '/snappymail" "' . $dir . '"');
|
||||||
|
|
||||||
|
rename("{$dir}/snappymail/v/0.0.0", "{$dir}/snappymail/v/{$package->version}");
|
||||||
|
|
||||||
|
$data = file_get_contents('index.php');
|
||||||
|
file_put_contents("{$dir}/index.php", str_replace('0.0.0', $package->version, $data));
|
||||||
|
|
||||||
|
$data = file_get_contents('_include.php');
|
||||||
|
file_put_contents("{$dir}/include.php", preg_replace('@(external-snappymail-data-folder/\'\);)@', "\$1\ndefine('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');", $data));
|
||||||
|
|
||||||
|
passthru('dpkg --build ' . escapeshellarg(DEB_DEST_DIR));
|
||||||
|
|
||||||
|
$TARGET_DIR = __DIR__ . "/dist/releases/webmail/{$package->version}/";
|
||||||
|
|
||||||
|
passthru('mv '
|
||||||
|
. escapeshellarg(DEB_DEST_DIR.'.deb') . ' '
|
||||||
|
. escapeshellarg($TARGET_DIR . basename(DEB_DEST_DIR.'.deb'))
|
||||||
|
);
|
||||||
|
|
||||||
|
passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
|
||||||
|
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
|
||||||
|
$cwd = getcwd();
|
||||||
|
chdir($TARGET_DIR);
|
||||||
|
passthru('dpkg-scanpackages . /dev/null > '.escapeshellarg($TARGET_DIR . 'Packages'));
|
||||||
|
passthru('dpkg-scanpackages . /dev/null | gzip -9c > '.escapeshellarg($TARGET_DIR . 'Packages.gz'));
|
||||||
|
$size = filesize($TARGET_DIR . 'Packages');
|
||||||
|
$gz_size = filesize($TARGET_DIR . 'Packages.gz');
|
||||||
|
$Release = 'Origin: SnappyMail Repository
|
||||||
|
Label: SnappyMail
|
||||||
|
Suite: stable
|
||||||
|
Codename: stable
|
||||||
|
Version: 1.0
|
||||||
|
Architectures: all
|
||||||
|
Components: main
|
||||||
|
Description: SnappyMail repository
|
||||||
|
Date: ' . gmdate('r') . '
|
||||||
|
MD5Sum:
|
||||||
|
' . hash_file('md5', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
||||||
|
' . hash_file('md5', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
||||||
|
SHA1:
|
||||||
|
' . hash_file('sha1', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
||||||
|
' . hash_file('sha1', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
||||||
|
SHA256:
|
||||||
|
' . hash_file('sha256', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
||||||
|
' . hash_file('sha256', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
||||||
|
';
|
||||||
|
file_put_contents($TARGET_DIR . 'Release', $Release);
|
||||||
|
chdir($cwd);
|
||||||
14
build/deb/DEBIAN/control
Normal file
14
build/deb/DEBIAN/control
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
Package: snappymail
|
||||||
|
Version: 0.0.0
|
||||||
|
Maintainer: SnappyMail <debian@snappymail.eu>
|
||||||
|
Depends: nginx | apache2 | httpd, php-fpm | libapache2-mod-php, php-json, php-mbstring
|
||||||
|
Recommends: php-intl, php-sodium, php-uuid
|
||||||
|
Suggests: php-sqlite3 | php-mysql | php-pgsql, php-curl, php-exif, php-gnupg, php-gd | php-gmagick | php-imagick, php-openssl, php-zip
|
||||||
|
Architecture: all
|
||||||
|
Homepage: https://snappymail.eu
|
||||||
|
Vcs-Browser: https://github.com/the-djmaze/snappymail
|
||||||
|
Vcs-Git: https://github.com/the-djmaze/snappymail.git
|
||||||
|
Description: SnappyMail is a PHP-based simple, modern, lightweight & fast web-based email client with no database requirements.
|
||||||
|
It supports IMAP, SMTP and Sieve protocols, multiple accounts and identities, an admin panel for configuration.
|
||||||
|
Plugins can be installed to further extend functionality.
|
||||||
|
Emails are not stored locally, but are accessed through IMAP.
|
||||||
2
build/deb/DEBIAN/postinst
Executable file
2
build/deb/DEBIAN/postinst
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
chown -R www-data:www-data /var/lib/snappymail
|
||||||
77
build/plugins.php
Executable file
77
build/plugins.php
Executable file
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
define('ROOT_DIR', dirname(__DIR__));
|
||||||
|
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
|
||||||
|
|
||||||
|
$destPath = __DIR__ . 'build/dist/releases/plugins/';
|
||||||
|
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
|
||||||
|
$manifest = [];
|
||||||
|
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
|
||||||
|
$keys = [
|
||||||
|
'author',
|
||||||
|
'category',
|
||||||
|
'description',
|
||||||
|
'file',
|
||||||
|
'id',
|
||||||
|
'license',
|
||||||
|
'name',
|
||||||
|
'release',
|
||||||
|
'required',
|
||||||
|
'type',
|
||||||
|
'url',
|
||||||
|
'version'
|
||||||
|
];
|
||||||
|
foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
||||||
|
if (is_file("{$dir}/index.php")) {
|
||||||
|
require "{$dir}/index.php";
|
||||||
|
$name = basename($dir);
|
||||||
|
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
|
||||||
|
$manifest_item = [];
|
||||||
|
foreach ($class->getConstants() as $key => $value) {
|
||||||
|
$key = \strtolower($key);
|
||||||
|
if (in_array($key, $keys)) {
|
||||||
|
$manifest_item[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$version = $manifest_item['version'];
|
||||||
|
if (0 < floatval($version)) {
|
||||||
|
echo "+ {$name} {$version}\n";
|
||||||
|
$manifest_item['type'] = 'plugin';
|
||||||
|
$manifest_item['id'] = $name;
|
||||||
|
$manifest_item['file'] = "{$dir}-{$version}.tgz";
|
||||||
|
ksort($manifest_item);
|
||||||
|
$manifest[$name] = $manifest_item;
|
||||||
|
$tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar";
|
||||||
|
$tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz";
|
||||||
|
@unlink($tgz_destination);
|
||||||
|
@unlink("{$tar_destination}.gz");
|
||||||
|
$tar = new PharData($tar_destination);
|
||||||
|
$tar->buildFromDirectory('./plugins/', '/' . \preg_quote("./plugins/{$name}", '/') . '/');
|
||||||
|
$tar->compress(Phar::GZ);
|
||||||
|
unlink($tar_destination);
|
||||||
|
rename("{$tar_destination}.gz", $tgz_destination);
|
||||||
|
if (Phar::canWrite()) {
|
||||||
|
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
|
||||||
|
@unlink($phar_destination);
|
||||||
|
$tar = new Phar($phar_destination);
|
||||||
|
$tar->buildFromDirectory("./plugins/{$name}/");
|
||||||
|
$tar->compress(Phar::GZ);
|
||||||
|
unlink($phar_destination);
|
||||||
|
rename("{$phar_destination}.gz", $phar_destination);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "- {$name} {$version}\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "- {$name}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($manifest);
|
||||||
|
$manifest = json_encode(array_values($manifest));
|
||||||
|
$manifest = str_replace('{"', "\n\t{\n\t\t\"", $manifest);
|
||||||
|
$manifest = str_replace('"}', "\"\n\t}", $manifest);
|
||||||
|
$manifest = str_replace('}]', "}\n]", $manifest);
|
||||||
|
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
|
||||||
|
$manifest = str_replace('\/', '/', $manifest);
|
||||||
|
file_put_contents(PLUGINS_DEST_DIR . "/packages.json", $manifest);
|
||||||
|
exit;
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import {
|
import { Settings, SettingsGet } from 'Common/Globals';
|
||||||
elementById,
|
import { changeTheme } from 'Common/Utils';
|
||||||
Settings
|
|
||||||
} from 'Common/Globals';
|
|
||||||
|
|
||||||
import { logoutLink } from 'Common/Links';
|
import { logoutLink } from 'Common/Links';
|
||||||
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
||||||
|
|
@ -25,11 +23,9 @@ export class AbstractApp {
|
||||||
this.Remote = Remote;
|
this.Remote = Remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutReload(close = false) {
|
logoutReload() {
|
||||||
const url = logoutLink();
|
const url = logoutLink();
|
||||||
|
|
||||||
close && window.close && window.close();
|
|
||||||
|
|
||||||
if (location.href !== url) {
|
if (location.href !== url) {
|
||||||
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 100);
|
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 100);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -37,22 +33,27 @@ export class AbstractApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
// rl.adminArea() || !translatorReload(false, );
|
||||||
|
rl.adminArea() || (
|
||||||
|
LanguageStore.language(SettingsGet('Language'))
|
||||||
|
& ThemeStore.populate()
|
||||||
|
& changeTheme(SettingsGet('Theme'))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
bootstart() {
|
bootstart() {
|
||||||
const register = (key, ClassObject, templateID) => ko.components.register(key, {
|
const register = (key, ClassObject, templateID) => ko.components.register(key, {
|
||||||
template: { element: templateID || (key + 'Component') },
|
template: { element: templateID || (key + 'Component') },
|
||||||
viewModel: {
|
viewModel: {
|
||||||
createViewModel: (params, componentInfo) => {
|
createViewModel: (params, componentInfo) => {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
|
i18nToNodes(componentInfo.element);
|
||||||
if (componentInfo && componentInfo.element) {
|
if (params.inline) {
|
||||||
|
componentInfo.element.style.display = 'inline-block';
|
||||||
i18nToNodes(componentInfo.element);
|
|
||||||
|
|
||||||
if (params.inline) {
|
|
||||||
componentInfo.element.style.display = 'inline-block';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ClassObject(params);
|
return new ClassObject(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -68,14 +69,7 @@ export class AbstractApp {
|
||||||
|
|
||||||
LanguageStore.populate();
|
LanguageStore.populate();
|
||||||
ThemeStore.populate();
|
ThemeStore.populate();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.start();
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
hideLoading() {
|
|
||||||
elementById('rl-content').hidden = false;
|
|
||||||
elementById('rl-loading').remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'External/Admin/ko';
|
import 'External/ko';
|
||||||
|
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { Settings, SettingsGet } from 'Common/Globals';
|
||||||
|
|
||||||
|
|
@ -16,9 +16,8 @@ class AdminApp extends AbstractApp {
|
||||||
this.weakPassword = ko.observable(false);
|
this.weakPassword = ko.observable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstart() {
|
start() {
|
||||||
super.bootstart();
|
if (!Settings.app('adminAllowed')) {
|
||||||
if (!Settings.app('allowAdminPanel')) {
|
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.href = '/', 1);
|
setTimeout(() => location.href = '/', 1);
|
||||||
} else if (SettingsGet('Auth')) {
|
} else if (SettingsGet('Auth')) {
|
||||||
|
|
@ -27,7 +26,6 @@ class AdminApp extends AbstractApp {
|
||||||
} else {
|
} else {
|
||||||
startScreens([LoginAdminScreen]);
|
startScreens([LoginAdminScreen]);
|
||||||
}
|
}
|
||||||
this.hideLoading();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
848
dev/App/User.js
848
dev/App/User.js
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
||||||
import * as Links from 'Common/Links';
|
import * as Links from 'Common/Links';
|
||||||
import { doc, SettingsGet } from 'Common/Globals';
|
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
|
||||||
|
|
||||||
let notificator = null,
|
let notificator = null,
|
||||||
player = null,
|
player = null,
|
||||||
|
|
@ -12,7 +12,7 @@ let notificator = null,
|
||||||
player.src = url;
|
player.src = url;
|
||||||
player.play();
|
player.play();
|
||||||
name = name.trim();
|
name = name.trim();
|
||||||
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'}));
|
fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -73,8 +73,7 @@ export const SMAudio = new class {
|
||||||
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
|
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
|
||||||
if (player) {
|
if (player) {
|
||||||
const stopFn = () => this.pause();
|
const stopFn = () => this.pause();
|
||||||
player.addEventListener('ended', stopFn);
|
addEventsListener(player, ['ended','error'], stopFn);
|
||||||
player.addEventListener('error', stopFn);
|
|
||||||
addEventListener('audio.api.stop', stopFn);
|
addEventListener('audio.api.stop', stopFn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +88,7 @@ export const SMAudio = new class {
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
player && player.pause();
|
player && player.pause();
|
||||||
dispatchEvent(new CustomEvent('audio.stop'));
|
fireEvent('audio.stop');
|
||||||
}
|
}
|
||||||
|
|
||||||
playMp3(url, name) {
|
playMp3(url, name) {
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,31 @@
|
||||||
import { MessageSetAction } from 'Common/EnumsUser';
|
import { MessageSetAction } from 'Common/EnumsUser';
|
||||||
import { arrayLength, pInt } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
|
|
||||||
let FOLDERS_CACHE = {},
|
let FOLDERS_CACHE = {},
|
||||||
FOLDERS_NAME_CACHE = {},
|
FOLDERS_NAME_CACHE = {},
|
||||||
FOLDERS_HASH_CACHE = {},
|
|
||||||
FOLDERS_UID_NEXT_CACHE = {},
|
|
||||||
MESSAGE_FLAGS_CACHE = {},
|
MESSAGE_FLAGS_CACHE = {},
|
||||||
NEW_MESSAGE_CACHE = {},
|
NEW_MESSAGE_CACHE = {},
|
||||||
|
REQUESTED_MESSAGE_CACHE = {},
|
||||||
inboxFolderName = 'INBOX';
|
inboxFolderName = 'INBOX';
|
||||||
|
|
||||||
const REQUESTED_MESSAGE_CACHE = {};
|
|
||||||
|
|
||||||
export const
|
export const
|
||||||
/**
|
/**
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
clear = () => {
|
clearCache = () => {
|
||||||
FOLDERS_CACHE = {};
|
FOLDERS_CACHE = {};
|
||||||
FOLDERS_NAME_CACHE = {};
|
FOLDERS_NAME_CACHE = {};
|
||||||
FOLDERS_HASH_CACHE = {};
|
|
||||||
FOLDERS_UID_NEXT_CACHE = {};
|
|
||||||
MESSAGE_FLAGS_CACHE = {};
|
MESSAGE_FLAGS_CACHE = {};
|
||||||
|
NEW_MESSAGE_CACHE = {};
|
||||||
|
REQUESTED_MESSAGE_CACHE = {};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @param {string} uid
|
* @param {string} uid
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getMessageKey = (folderFullNameRaw, uid) => `${folderFullNameRaw}#${uid}`,
|
getMessageKey = (folderFullName, uid) => `${folderFullName}#${uid}`,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folder
|
* @param {string} folder
|
||||||
|
|
@ -44,18 +41,18 @@ export const
|
||||||
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
|
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @param {string} uid
|
* @param {string} uid
|
||||||
*/
|
*/
|
||||||
addNewMessageCache = (folderFullNameRaw, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true,
|
addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @param {string} uid
|
* @param {string} uid
|
||||||
*/
|
*/
|
||||||
hasNewMessageAndRemoveFromCache = (folderFullNameRaw, uid) => {
|
hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
|
||||||
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
|
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
|
||||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
|
NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -80,72 +77,81 @@ export const
|
||||||
* @param {string} folderHash
|
* @param {string} folderHash
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getFolderFullNameRaw = folderHash =>
|
getFolderFullName = folderHash =>
|
||||||
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
|
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderHash
|
* @param {string} folderHash
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @param {?FolderModel} folder
|
* @param {?FolderModel} folder
|
||||||
*/
|
*/
|
||||||
setFolder = (folderHash, folderFullNameRaw, folder) => {
|
setFolder = folder => {
|
||||||
FOLDERS_CACHE[folderFullNameRaw] = folder;
|
folder.hash = '';
|
||||||
FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
|
FOLDERS_CACHE[folder.fullName] = folder;
|
||||||
|
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getFolderHash = folderFullNameRaw =>
|
getFolderHash = folderFullName =>
|
||||||
folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '',
|
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @param {string} folderHash
|
* @param {string} folderHash
|
||||||
*/
|
*/
|
||||||
setFolderHash = (folderFullNameRaw, folderHash) =>
|
setFolderHash = (folderFullName, folderHash) =>
|
||||||
folderFullNameRaw && (FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash),
|
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getFolderUidNext = folderFullNameRaw =>
|
getFolderUidNext = folderFullName =>
|
||||||
folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
|
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
|
||||||
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
|
|
||||||
: '',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @param {string} uidNext
|
* @param {string} uidNext
|
||||||
*/
|
*/
|
||||||
setFolderUidNext = (folderFullNameRaw, uidNext) =>
|
setFolderUidNext = (folderFullName, uidNext) =>
|
||||||
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext,
|
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
* @returns {?FolderModel}
|
* @returns {?FolderModel}
|
||||||
*/
|
*/
|
||||||
getFolderFromCacheList = folderFullNameRaw =>
|
getFolderFromCacheList = folderFullName =>
|
||||||
folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null,
|
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullNameRaw
|
* @param {string} folderFullName
|
||||||
*/
|
*/
|
||||||
removeFolderFromCacheList = folderFullNameRaw => delete FOLDERS_CACHE[folderFullNameRaw];
|
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
|
||||||
|
|
||||||
export class MessageFlagsCache
|
export class MessageFlagsCache
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullName
|
||||||
|
* @param {string} uid
|
||||||
|
* @param {string} flag
|
||||||
|
* @returns {bool}
|
||||||
|
*/
|
||||||
|
static hasFlag(folderFullName, uid, flag) {
|
||||||
|
return MESSAGE_FLAGS_CACHE[folderFullName]
|
||||||
|
&& MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||||
|
&& MESSAGE_FLAGS_CACHE[folderFullName][uid].includes(flag);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
* @param {string} uid
|
* @param {string} uid
|
||||||
* @returns {?Array}
|
* @returns {?Array}
|
||||||
*/
|
*/
|
||||||
static getFor(folderFullName, uid) {
|
static getFor(folderFullName, uid) {
|
||||||
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid];
|
||||||
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -153,11 +159,13 @@ export class MessageFlagsCache
|
||||||
* @param {string} uid
|
* @param {string} uid
|
||||||
* @param {Array} flagsCache
|
* @param {Array} flagsCache
|
||||||
*/
|
*/
|
||||||
static setFor(folderFullName, uid, flagsCache) {
|
static setFor(folderFullName, uid, flags) {
|
||||||
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
if (isArray(flags)) {
|
||||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
||||||
|
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||||
|
}
|
||||||
|
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
|
||||||
}
|
}
|
||||||
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -173,39 +181,24 @@ export class MessageFlagsCache
|
||||||
static initMessage(message) {
|
static initMessage(message) {
|
||||||
if (message) {
|
if (message) {
|
||||||
const uid = message.uid,
|
const uid = message.uid,
|
||||||
flags = this.getFor(message.folder, uid);
|
flags = this.getFor(message.folder, uid),
|
||||||
|
thread = message.threads;
|
||||||
|
|
||||||
if (flags && flags.length) {
|
if (isArray(flags)) {
|
||||||
message.isFlagged(!!flags[1]);
|
message.flags(flags);
|
||||||
|
|
||||||
if (!message.isSimpleMessage) {
|
|
||||||
message.isUnseen(!!flags[0]);
|
|
||||||
message.isAnswered(!!flags[2]);
|
|
||||||
message.isForwarded(!!flags[3]);
|
|
||||||
message.isReadReceipt(!!flags[4]);
|
|
||||||
message.isDeleted(!!flags[5]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.threads.length) {
|
if (thread.length) {
|
||||||
const unseenSubUid = message.threads.find(sSubUid => {
|
const unseenSubUid = thread.find(iSubUid =>
|
||||||
if (uid !== sSubUid) {
|
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
|
||||||
const subFlags = this.getFor(message.folder, sSubUid);
|
);
|
||||||
return subFlags && subFlags.length && !!subFlags[0];
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const flaggedSubUid = message.threads.find(sSubUid => {
|
const flaggedSubUid = thread.find(iSubUid =>
|
||||||
if (uid !== sSubUid) {
|
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
|
||||||
const subFlags = this.getFor(message.folder, sSubUid);
|
);
|
||||||
return subFlags && subFlags.length && !!subFlags[1];
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
message.hasUnseenSubMessage(unseenSubUid && 0 < pInt(unseenSubUid));
|
message.hasUnseenSubMessage(!!unseenSubUid);
|
||||||
message.hasFlaggedSubMessage(flaggedSubUid && 0 < pInt(flaggedSubUid));
|
message.hasFlaggedSubMessage(!!flaggedSubUid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,25 +208,7 @@ export class MessageFlagsCache
|
||||||
*/
|
*/
|
||||||
static store(message) {
|
static store(message) {
|
||||||
if (message) {
|
if (message) {
|
||||||
this.setFor(message.folder, message.uid, [
|
this.setFor(message.folder, message.uid, message.flags());
|
||||||
message.isUnseen(),
|
|
||||||
message.isFlagged(),
|
|
||||||
message.isAnswered(),
|
|
||||||
message.isForwarded(),
|
|
||||||
message.isReadReceipt(),
|
|
||||||
message.isDeleted()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folder
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {Array} flags
|
|
||||||
*/
|
|
||||||
static storeByFolderAndUid(folder, uid, flags) {
|
|
||||||
if (arrayLength(flags)) {
|
|
||||||
this.setFor(folder, uid, flags);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,33 +218,30 @@ export class MessageFlagsCache
|
||||||
* @param {number} setAction
|
* @param {number} setAction
|
||||||
*/
|
*/
|
||||||
static storeBySetAction(folder, uid, setAction) {
|
static storeBySetAction(folder, uid, setAction) {
|
||||||
let unread = 0;
|
let flags = this.getFor(folder, uid) || [];
|
||||||
const flags = this.getFor(folder, uid);
|
const
|
||||||
|
unread = flags.includes('\\seen') ? 0 : 1,
|
||||||
|
add = item => flags.includes(item) || flags.push(item),
|
||||||
|
remove = item => flags = flags.filter(flag => flag != item);
|
||||||
|
|
||||||
if (arrayLength(flags)) {
|
switch (setAction) {
|
||||||
if (flags[0]) {
|
case MessageSetAction.SetSeen:
|
||||||
unread = 1;
|
add('\\seen');
|
||||||
}
|
break;
|
||||||
|
case MessageSetAction.UnsetSeen:
|
||||||
switch (setAction) {
|
remove('\\seen');
|
||||||
case MessageSetAction.SetSeen:
|
break;
|
||||||
flags[0] = false;
|
case MessageSetAction.SetFlag:
|
||||||
break;
|
add('\\flagged');
|
||||||
case MessageSetAction.UnsetSeen:
|
break;
|
||||||
flags[0] = true;
|
case MessageSetAction.UnsetFlag:
|
||||||
break;
|
remove('\\flagged');
|
||||||
case MessageSetAction.SetFlag:
|
break;
|
||||||
flags[1] = true;
|
// no default
|
||||||
break;
|
|
||||||
case MessageSetAction.UnsetFlag:
|
|
||||||
flags[1] = false;
|
|
||||||
break;
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setFor(folder, uid, flags);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setFor(folder, uid, flags);
|
||||||
|
|
||||||
return unread;
|
return unread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,53 @@
|
||||||
/* eslint quote-props: 0 */
|
/* eslint quote-props: 0 */
|
||||||
|
|
||||||
/**
|
export const
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
export const Capa = {
|
|
||||||
OpenPGP: 'OPEN_PGP',
|
|
||||||
Prefetch: 'PREFETCH',
|
|
||||||
Composer: 'COMPOSER',
|
|
||||||
Contacts: 'CONTACTS',
|
|
||||||
Reload: 'RELOAD',
|
|
||||||
Search: 'SEARCH',
|
|
||||||
SearchAdv: 'SEARCH_ADV',
|
|
||||||
MessageActions: 'MESSAGE_ACTIONS',
|
|
||||||
MessageListActions: 'MESSAGELIST_ACTIONS',
|
|
||||||
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
|
|
||||||
DangerousActions: 'DANGEROUS_ACTIONS',
|
|
||||||
Settings: 'SETTINGS',
|
|
||||||
Help: 'HELP',
|
|
||||||
Themes: 'THEMES',
|
|
||||||
UserBackground: 'USER_BACKGROUND',
|
|
||||||
Sieve: 'SIEVE',
|
|
||||||
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
|
|
||||||
Templates: 'TEMPLATES',
|
|
||||||
AutoLogout: 'AUTOLOGOUT',
|
|
||||||
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
|
|
||||||
Identities: 'IDENTITIES'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const Scope = {
|
Scope = {
|
||||||
All: 'all',
|
|
||||||
None: 'none',
|
|
||||||
Contacts: 'Contacts',
|
|
||||||
MessageList: 'MessageList',
|
MessageList: 'MessageList',
|
||||||
FolderList: 'FolderList',
|
FolderList: 'FolderList',
|
||||||
MessageView: 'MessageView',
|
MessageView: 'MessageView',
|
||||||
Compose: 'Compose',
|
Settings: 'Settings'
|
||||||
Settings: 'Settings',
|
},
|
||||||
Menu: 'Menu',
|
|
||||||
ComposeOpenPgp: 'ComposeOpenPgp',
|
|
||||||
MessageOpenPgp: 'MessageOpenPgp',
|
|
||||||
ViewOpenPgpKey: 'ViewOpenPgpKey',
|
|
||||||
KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
|
|
||||||
Ask: 'Ask'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const UploadErrorCode = {
|
UploadErrorCode = {
|
||||||
Normal: 0,
|
Normal: 0,
|
||||||
FileIsTooBig: 1,
|
FileIsTooBig: 1,
|
||||||
FilePartiallyUploaded: 2,
|
FilePartiallyUploaded: 3,
|
||||||
NoFileUploaded: 3,
|
NoFileUploaded: 4,
|
||||||
MissingTempFolder: 4,
|
MissingTempFolder: 6,
|
||||||
OnSavingFile: 5,
|
OnSavingFile: 7,
|
||||||
FileType: 98,
|
FileType: 98,
|
||||||
Unknown: 99
|
Unknown: 99
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const SaveSettingsStep = {
|
SaveSettingsStep = {
|
||||||
Animate: -2,
|
Animate: -2,
|
||||||
Idle: -1,
|
Idle: -1,
|
||||||
TrueResult: 1,
|
TrueResult: 1,
|
||||||
FalseResult: 0
|
FalseResult: 0
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const Notification = {
|
Notification = {
|
||||||
RequestError: 1,
|
RequestError: 1,
|
||||||
RequestAborted: 2,
|
RequestAborted: 2,
|
||||||
|
|
||||||
|
// Global
|
||||||
InvalidToken: 101,
|
InvalidToken: 101,
|
||||||
AuthError: 102,
|
AuthError: 102,
|
||||||
|
|
||||||
|
// User
|
||||||
ConnectionError: 104,
|
ConnectionError: 104,
|
||||||
DomainNotAllowed: 109,
|
DomainNotAllowed: 109,
|
||||||
AccountNotAllowed: 110,
|
AccountNotAllowed: 110,
|
||||||
|
|
@ -110,20 +78,15 @@ export const Notification = {
|
||||||
CantDeleteNonEmptyFolder: 405,
|
CantDeleteNonEmptyFolder: 405,
|
||||||
|
|
||||||
// CantSaveSettings: 501,
|
// CantSaveSettings: 501,
|
||||||
CantSavePluginSettings: 502,
|
|
||||||
|
|
||||||
DomainAlreadyExists: 601,
|
DomainAlreadyExists: 601,
|
||||||
|
|
||||||
CantInstallPackage: 701,
|
|
||||||
CantDeletePackage: 702,
|
|
||||||
InvalidPluginPackage: 703,
|
|
||||||
UnsupportedPluginPackage: 704,
|
|
||||||
|
|
||||||
DemoSendMessageError: 750,
|
DemoSendMessageError: 750,
|
||||||
DemoAccountError: 751,
|
DemoAccountError: 751,
|
||||||
|
|
||||||
AccountAlreadyExists: 801,
|
AccountAlreadyExists: 801,
|
||||||
AccountDoesNotExist: 802,
|
AccountDoesNotExist: 802,
|
||||||
|
AccountSwitchFailed: 803,
|
||||||
|
|
||||||
MailServerError: 901,
|
MailServerError: 901,
|
||||||
ClientViewError: 902,
|
ClientViewError: 902,
|
||||||
|
|
@ -134,5 +97,12 @@ export const Notification = {
|
||||||
// JsonTimeout: 953,
|
// JsonTimeout: 953,
|
||||||
|
|
||||||
UnknownNotification: 998,
|
UnknownNotification: 998,
|
||||||
UnknownError: 999
|
UnknownError: 999,
|
||||||
|
|
||||||
|
// Admin
|
||||||
|
CantInstallPackage: 701,
|
||||||
|
CantDeletePackage: 702,
|
||||||
|
InvalidPluginPackage: 703,
|
||||||
|
UnsupportedPluginPackage: 704,
|
||||||
|
CantSavePluginSettings: 705
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,20 @@ export const FolderType = {
|
||||||
NotSpam: 80
|
NotSpam: 80
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const FolderMetadataKeys = {
|
||||||
|
// RFC 5464
|
||||||
|
Comment: '/private/comment',
|
||||||
|
CommentShared: '/shared/comment',
|
||||||
|
// RFC 6154
|
||||||
|
SpecialUse: '/private/specialuse',
|
||||||
|
// Kolab
|
||||||
|
KolabFolderType: '/private/vendor/kolab/folder-type',
|
||||||
|
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -34,13 +48,13 @@ export const FolderSortMode = {
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const ComposeType = {
|
export const ComposeType = {
|
||||||
Empty: 'empty',
|
Empty: 0,
|
||||||
Reply: 'reply',
|
Reply: 1,
|
||||||
ReplyAll: 'replyall',
|
ReplyAll: 2,
|
||||||
Forward: 'forward',
|
Forward: 3,
|
||||||
ForwardAsAttachment: 'forward-as-attachment',
|
ForwardAsAttachment: 4,
|
||||||
Draft: 'draft',
|
Draft: 5,
|
||||||
EditAsNew: 'editasnew'
|
EditAsNew: 6
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -58,19 +72,14 @@ export const SetSystemFoldersNotification = {
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const ClientSideKeyName = {
|
export const
|
||||||
FoldersLashHash: 0,
|
ClientSideKeyNameExpandedFolders = 3,
|
||||||
MessagesInboxLastHash: 1,
|
ClientSideKeyNameFolderListSize = 4,
|
||||||
MailBoxListSize: 2,
|
ClientSideKeyNameMessageListSize = 5,
|
||||||
ExpandedFolders: 3,
|
ClientSideKeyNameLastReplyAction = 6,
|
||||||
FolderListSize: 4,
|
ClientSideKeyNameLastSignMe = 7,
|
||||||
MessageListSize: 5,
|
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
||||||
LastReplyAction: 6,
|
ClientSideKeyNameMessageAttachmentControls = 10;
|
||||||
LastSignMe: 7,
|
|
||||||
ComposeLastIdentityID: 8,
|
|
||||||
MessageHeaderFullInfo: 9,
|
|
||||||
MessageAttachmentControls: 10
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const
|
||||||
msOffice = app+'vnd.openxmlformats-officedocument.',
|
msOffice = app+'vnd.openxmlformats-officedocument.',
|
||||||
openDoc = app+'vnd.oasis.opendocument.',
|
openDoc = app+'vnd.oasis.opendocument.',
|
||||||
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
|
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
|
||||||
|
lowerCase = text => text.toLowerCase().trim(),
|
||||||
|
|
||||||
exts = {
|
exts = {
|
||||||
eml: 'message/rfc822',
|
eml: 'message/rfc822',
|
||||||
|
|
@ -123,13 +124,13 @@ export const FileInfo = {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getExtension: fileName => {
|
getExtension: fileName => {
|
||||||
fileName = fileName.toLowerCase().trim();
|
fileName = lowerCase(fileName);
|
||||||
const result = fileName.split('.').pop();
|
const result = fileName.split('.').pop();
|
||||||
return result === fileName ? '' : result;
|
return result === fileName ? '' : result;
|
||||||
},
|
},
|
||||||
|
|
||||||
getContentType: fileName => {
|
getContentType: fileName => {
|
||||||
fileName = fileName.toLowerCase().trim();
|
fileName = lowerCase(fileName);
|
||||||
if ('winmail.dat' === fileName) {
|
if ('winmail.dat' === fileName) {
|
||||||
return app + 'ms-tnef';
|
return app + 'ms-tnef';
|
||||||
}
|
}
|
||||||
|
|
@ -158,8 +159,8 @@ export const FileInfo = {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getType: (ext, mimeType) => {
|
getType: (ext, mimeType) => {
|
||||||
ext = ext.toLowerCase().trim();
|
ext = lowerCase(ext);
|
||||||
mimeType = mimeType.toLowerCase().trim().replace('csv/plain', 'text/csv');
|
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
|
||||||
|
|
||||||
let key = ext + mimeType;
|
let key = ext + mimeType;
|
||||||
if (cache[key]) {
|
if (cache[key]) {
|
||||||
|
|
@ -249,10 +250,10 @@ export const FileInfo = {
|
||||||
* @param {string} sFileType
|
* @param {string} sFileType
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getCombinedIconClass: data => {
|
getAttachmentsIconClass: data => {
|
||||||
if (arrayLength(data)) {
|
if (arrayLength(data)) {
|
||||||
let icons = data
|
let icons = data
|
||||||
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '')
|
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
|
||||||
.validUnique();
|
.validUnique();
|
||||||
|
|
||||||
return (icons && 1 === icons.length && 'icon-file' !== icons[0])
|
return (icons && 1 === icons.length && 'icon-file' !== icons[0])
|
||||||
|
|
|
||||||
259
dev/Common/Folders.js
Normal file
259
dev/Common/Folders.js
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
import { isArray, arrayLength } from 'Common/Utils';
|
||||||
|
import {
|
||||||
|
MessageFlagsCache,
|
||||||
|
setFolderHash,
|
||||||
|
getFolderHash,
|
||||||
|
getFolderInboxName,
|
||||||
|
getFolderFromCacheList,
|
||||||
|
getFolderUidNext
|
||||||
|
} from 'Common/Cache';
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
import { getNotification } from 'Common/Translator';
|
||||||
|
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
export const
|
||||||
|
|
||||||
|
sortFolders = folders => {
|
||||||
|
try {
|
||||||
|
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||||
|
folders.sort((a, b) =>
|
||||||
|
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?Function} fCallback
|
||||||
|
* @param {string} folder
|
||||||
|
* @param {Array=} list = []
|
||||||
|
*/
|
||||||
|
fetchFolderInformation = (fCallback, folder, list = []) => {
|
||||||
|
let fetch = !arrayLength(list);
|
||||||
|
const uids = [];
|
||||||
|
|
||||||
|
if (!fetch) {
|
||||||
|
list.forEach(messageListItem => {
|
||||||
|
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
|
||||||
|
uids.push(messageListItem.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageListItem.threads.length) {
|
||||||
|
messageListItem.threads.forEach(uid => {
|
||||||
|
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
|
||||||
|
uids.push(uid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fetch = uids.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetch) {
|
||||||
|
Remote.request('FolderInformation', fCallback, {
|
||||||
|
Folder: folder,
|
||||||
|
FlagsUids: uids,
|
||||||
|
UidNext: getFolderUidNext(folder) // Used to check for new messages
|
||||||
|
});
|
||||||
|
} else if (SettingsUserStore.useThreads()) {
|
||||||
|
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array=} aDisabled
|
||||||
|
* @param {Array=} aHeaderLines
|
||||||
|
* @param {Function=} fRenameCallback
|
||||||
|
* @param {Function=} fDisableCallback
|
||||||
|
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
folderListOptionsBuilder = (
|
||||||
|
aDisabled,
|
||||||
|
aHeaderLines,
|
||||||
|
fRenameCallback,
|
||||||
|
fDisableCallback,
|
||||||
|
bNoSelectSelectable,
|
||||||
|
aList = FolderUserStore.folderList()
|
||||||
|
) => {
|
||||||
|
const
|
||||||
|
aResult = [],
|
||||||
|
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||||
|
// FolderSystemPopupView should always be true
|
||||||
|
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
||||||
|
|
||||||
|
foldersWalk = folders => {
|
||||||
|
folders.forEach(oItem => {
|
||||||
|
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||||
|
aResult.push({
|
||||||
|
id: oItem.fullName,
|
||||||
|
name:
|
||||||
|
sDeepPrefix.repeat(oItem.deep) +
|
||||||
|
fRenameCallback(oItem),
|
||||||
|
system: false,
|
||||||
|
disabled: !bNoSelectSelectable && (
|
||||||
|
!oItem.selectable() ||
|
||||||
|
aDisabled.includes(oItem.fullName) ||
|
||||||
|
fDisableCallback(oItem))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oItem.subFolders.length) {
|
||||||
|
foldersWalk(oItem.subFolders());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
fDisableCallback = fDisableCallback || (() => false);
|
||||||
|
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
||||||
|
isArray(aDisabled) || (aDisabled = []);
|
||||||
|
|
||||||
|
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
||||||
|
aResult.push({
|
||||||
|
id: line[0],
|
||||||
|
name: line[1],
|
||||||
|
system: false,
|
||||||
|
disabled: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
foldersWalk(aList);
|
||||||
|
|
||||||
|
return aResult;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Every 5 minutes
|
||||||
|
refreshFoldersInterval = 300000,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean=} boot = false
|
||||||
|
*/
|
||||||
|
folderInformationMultiply = (boot = false) => {
|
||||||
|
const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
|
||||||
|
if (arrayLength(folders)) {
|
||||||
|
Remote.request('FolderInformationMultiply', (iError, oData) => {
|
||||||
|
if (!iError && arrayLength(oData.Result)) {
|
||||||
|
const utc = Date.now();
|
||||||
|
oData.Result.forEach(item => {
|
||||||
|
const hash = getFolderHash(item.Folder),
|
||||||
|
folder = getFolderFromCacheList(item.Folder);
|
||||||
|
|
||||||
|
if (folder) {
|
||||||
|
folder.expires = utc;
|
||||||
|
|
||||||
|
setFolderHash(item.Folder, item.Hash);
|
||||||
|
|
||||||
|
folder.messageCountAll(item.MessageCount);
|
||||||
|
|
||||||
|
let unreadCountChange = folder.messageCountUnread() !== item.MessageUnseenCount;
|
||||||
|
|
||||||
|
folder.messageCountUnread(item.MessageUnseenCount);
|
||||||
|
|
||||||
|
if (unreadCountChange) {
|
||||||
|
MessageFlagsCache.clearFolder(folder.fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hash || item.Hash !== hash) {
|
||||||
|
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||||
|
MessagelistUserStore.reload();
|
||||||
|
}
|
||||||
|
} else if (unreadCountChange
|
||||||
|
&& folder.fullName === FolderUserStore.currentFolderFullName()
|
||||||
|
&& MessagelistUserStore.length) {
|
||||||
|
rl.app.folderInformation(folder.fullName, MessagelistUserStore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (boot) {
|
||||||
|
setTimeout(() => folderInformationMultiply(true), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
Folders: folders
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
moveOrDeleteResponseHelper = (iError, oData) => {
|
||||||
|
if (iError) {
|
||||||
|
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||||
|
alert(getNotification(iError));
|
||||||
|
} else if (FolderUserStore.currentFolder()) {
|
||||||
|
if (2 === arrayLength(oData.Result)) {
|
||||||
|
setFolderHash(oData.Result[0], oData.Result[1]);
|
||||||
|
} else {
|
||||||
|
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||||
|
}
|
||||||
|
MessagelistUserStore.reload(!MessagelistUserStore.length);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
|
||||||
|
const
|
||||||
|
sSpamFolder = FolderUserStore.spamFolder(),
|
||||||
|
isSpam = sSpamFolder === toFolderFullName,
|
||||||
|
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
|
||||||
|
|
||||||
|
Remote.request('MessageMove',
|
||||||
|
moveOrDeleteResponseHelper,
|
||||||
|
{
|
||||||
|
FromFolder: fromFolderFullName,
|
||||||
|
ToFolder: toFolderFullName,
|
||||||
|
Uids: uidsForMove.join(','),
|
||||||
|
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
|
||||||
|
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
['MessageList']
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
|
||||||
|
Remote.request('MessageDelete',
|
||||||
|
moveOrDeleteResponseHelper,
|
||||||
|
{
|
||||||
|
Folder: sFromFolderFullName,
|
||||||
|
Uids: aUidForRemove.join(',')
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
['MessageList']
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} sFromFolderFullName
|
||||||
|
* @param {Array} aUidForMove
|
||||||
|
* @param {string} sToFolderFullName
|
||||||
|
* @param {boolean=} bCopy = false
|
||||||
|
*/
|
||||||
|
moveMessagesToFolder = (sFromFolderFullName, aUidForMove, sToFolderFullName, bCopy) => {
|
||||||
|
if (sFromFolderFullName !== sToFolderFullName && arrayLength(aUidForMove)) {
|
||||||
|
const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
|
||||||
|
oToFolder = getFolderFromCacheList(sToFolderFullName);
|
||||||
|
|
||||||
|
if (oFromFolder && oToFolder) {
|
||||||
|
if (bCopy) {
|
||||||
|
Remote.request('MessageCopy', null, {
|
||||||
|
FromFolder: oFromFolder.fullName,
|
||||||
|
ToFolder: oToFolder.fullName,
|
||||||
|
Uids: aUidForMove.join(',')
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, aUidForMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessagelistUserStore.removeMessagesFromList(oFromFolder.fullName, aUidForMove, oToFolder.fullName, bCopy);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { Scope } from 'Common/Enums';
|
|
||||||
|
|
||||||
let keyScopeFake = Scope.All;
|
let keyScopeFake = 'all';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
ScopeMenu = 'Menu',
|
||||||
|
|
||||||
doc = document,
|
doc = document,
|
||||||
|
|
||||||
|
|
@ -11,12 +11,14 @@ export const
|
||||||
|
|
||||||
elementById = id => doc.getElementById(id),
|
elementById = id => doc.getElementById(id),
|
||||||
|
|
||||||
exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen)(),
|
exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
||||||
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
|
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
|
||||||
|
|
||||||
Settings = rl.settings,
|
Settings = rl.settings,
|
||||||
SettingsGet = Settings.get,
|
SettingsGet = Settings.get,
|
||||||
|
SettingsCapa = Settings.capa,
|
||||||
|
|
||||||
|
dropdowns = [],
|
||||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
||||||
|
|
||||||
moveAction = ko.observable(false),
|
moveAction = ko.observable(false),
|
||||||
|
|
@ -28,31 +30,46 @@ export const
|
||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
// keys
|
fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})),
|
||||||
keyScopeReal = ko.observable(Scope.All),
|
|
||||||
|
formFieldFocused = () => doc.activeElement && doc.activeElement.matches('input,textarea'),
|
||||||
|
|
||||||
|
addShortcut = (...args) => shortcuts.add(...args),
|
||||||
|
|
||||||
|
registerShortcut = (keys, modifiers, scopes, method) =>
|
||||||
|
addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
|
||||||
|
|
||||||
|
addEventsListener = (element, events, fn, options) =>
|
||||||
|
events.forEach(event => element.addEventListener(event, fn, options)),
|
||||||
|
|
||||||
|
addEventsListeners = (element, events) =>
|
||||||
|
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
|
||||||
|
|
||||||
|
// keys / shortcuts
|
||||||
|
keyScopeReal = ko.observable('all'),
|
||||||
keyScope = value => {
|
keyScope = value => {
|
||||||
if (value) {
|
if (!value) {
|
||||||
if (Scope.Menu !== value) {
|
|
||||||
keyScopeFake = value;
|
|
||||||
if (dropdownVisibility()) {
|
|
||||||
value = Scope.Menu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keyScopeReal(value);
|
|
||||||
shortcuts.setScope(value);
|
|
||||||
} else {
|
|
||||||
return keyScopeFake;
|
return keyScopeFake;
|
||||||
}
|
}
|
||||||
|
if (ScopeMenu !== value) {
|
||||||
|
keyScopeFake = value;
|
||||||
|
if (dropdownVisibility()) {
|
||||||
|
value = ScopeMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyScopeReal(value);
|
||||||
|
shortcuts.setScope(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
dropdownVisibility.subscribe(value => {
|
dropdownVisibility.subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
keyScope(Scope.Menu);
|
keyScope(ScopeMenu);
|
||||||
} else if (Scope.Menu === shortcuts.getScope()) {
|
} else if (ScopeMenu === shortcuts.getScope()) {
|
||||||
keyScope(keyScopeFake);
|
keyScope(keyScopeFake);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
|
||||||
leftPanelDisabled.subscribe(value => {
|
leftPanelDisabled.subscribe(value => {
|
||||||
value && moveAction() && moveAction(false);
|
value && moveAction() && moveAction(false);
|
||||||
$htmlCL.toggle('rl-left-panel-disabled', value);
|
$htmlCL.toggle('rl-left-panel-disabled', value);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
|
import { createElement, SettingsGet } from 'Common/Globals';
|
||||||
|
import { forEachObjectEntry, pInt } from 'Common/Utils';
|
||||||
|
import { proxy } from 'Common/Links';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
tpl = createElement('template'),
|
||||||
htmlre = /[&<>"']/g,
|
htmlre = /[&<>"']/g,
|
||||||
htmlmap = {
|
htmlmap = {
|
||||||
'&': '&',
|
'&': '&',
|
||||||
|
|
@ -6,15 +11,487 @@ const
|
||||||
'>': '>',
|
'>': '>',
|
||||||
'"': '"',
|
'"': '"',
|
||||||
"'": '''
|
"'": '''
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
|
||||||
* @param {string} text
|
|
||||||
* @returns {string}
|
// Strip utm_* tracking
|
||||||
*/
|
stripTracking = text => text.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1').replace(/&&+/, '');
|
||||||
export function encodeHtml(text) {
|
|
||||||
return (text && text.toString ? text.toString() : ''+text).replace(htmlre, m => htmlmap[m]);
|
export const
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the Message Html for viewing
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
cleanHtml = (html, contentLocationUrls, removeColors) => {
|
||||||
|
const
|
||||||
|
debug = false, // Config()->Get('debug', 'enable', false);
|
||||||
|
useProxy = !!SettingsGet('UseLocalProxyForExternalImages'),
|
||||||
|
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
|
||||||
|
|
||||||
|
result = {
|
||||||
|
hasExternals: false,
|
||||||
|
foundCIDs: [],
|
||||||
|
foundContentLocationUrls: []
|
||||||
|
},
|
||||||
|
|
||||||
|
// convert body attributes to CSS
|
||||||
|
tasks = {
|
||||||
|
link: value => {
|
||||||
|
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
|
||||||
|
tpl.content.querySelectorAll('a').forEach(node => node.style.color = value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: (value, node) => node.style.color = value,
|
||||||
|
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
|
||||||
|
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
|
||||||
|
bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
|
||||||
|
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
|
||||||
|
},
|
||||||
|
allowedAttributes = [
|
||||||
|
// defaults
|
||||||
|
'name',
|
||||||
|
'dir', 'lang', 'style', 'title',
|
||||||
|
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
|
||||||
|
'border', 'bordercolor', 'charset', 'direction',
|
||||||
|
// a
|
||||||
|
'download', 'hreflang',
|
||||||
|
// body
|
||||||
|
'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
|
||||||
|
// col
|
||||||
|
'align', 'valign',
|
||||||
|
// font
|
||||||
|
'color', 'face', 'size',
|
||||||
|
// hr
|
||||||
|
'noshade',
|
||||||
|
// img
|
||||||
|
'hspace', 'sizes', 'srcset', 'vspace',
|
||||||
|
// meter
|
||||||
|
'low', 'high', 'optimum', 'value',
|
||||||
|
// ol
|
||||||
|
'reversed', 'start',
|
||||||
|
// table
|
||||||
|
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
|
||||||
|
// th
|
||||||
|
'abbr', 'scope',
|
||||||
|
// td
|
||||||
|
'colspan', 'rowspan', 'headers'
|
||||||
|
],
|
||||||
|
disallowedTags = [
|
||||||
|
'HEAD','STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
|
||||||
|
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
|
||||||
|
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
|
||||||
|
],
|
||||||
|
nonEmptyTags = [
|
||||||
|
'A','B','EM','I','SPAN','STRONG','O:P','TABLE'
|
||||||
|
];
|
||||||
|
|
||||||
|
tpl.innerHTML = html
|
||||||
|
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
|
||||||
|
.replace(/<!doctype[^>]*>/gi, '')
|
||||||
|
.replace(/<\?xml[^>]*\?>/gi, '')
|
||||||
|
// Not supported by <template> element
|
||||||
|
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
||||||
|
.replace(/<\/?(html|head)[^>]*>/gi, '')
|
||||||
|
.trim();
|
||||||
|
html = '';
|
||||||
|
|
||||||
|
// \MailSo\Base\HtmlUtils::ClearComments()
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/187
|
||||||
|
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
|
||||||
|
while (nodeIterator.nextNode()) {
|
||||||
|
nodeIterator.referenceNode.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl.content.querySelectorAll('*').forEach(oElement => {
|
||||||
|
const name = oElement.tagName,
|
||||||
|
oStyle = oElement.style;
|
||||||
|
|
||||||
|
// \MailSo\Base\HtmlUtils::ClearTags()
|
||||||
|
if (disallowedTags.includes(name)
|
||||||
|
|| 'none' == oStyle.display
|
||||||
|
|| 'hidden' == oStyle.visibility
|
||||||
|
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
|
||||||
|
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
||||||
|
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
|
||||||
|
// || ('0' === oStyle.opacity
|
||||||
|
|| (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim() && !oElement.querySelector('img')))
|
||||||
|
) {
|
||||||
|
oElement.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if (['CENTER','FORM'].includes(name)) {
|
||||||
|
if ('FORM' === name || 'O:P' === name) {
|
||||||
|
replaceWithChildren(oElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
// Idea to allow CSS
|
||||||
|
if ('STYLE' === name) {
|
||||||
|
msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
|
||||||
|
oElement.textContent = oElement.textContent
|
||||||
|
.replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
|
||||||
|
.replace(/(background-)color:[^};]+/g, '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const aAttrsForRemove = [],
|
||||||
|
hasAttribute = name => oElement.hasAttribute(name),
|
||||||
|
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
|
||||||
|
setAttribute = (name, value) => oElement.setAttribute(name, value),
|
||||||
|
delAttribute = name => oElement.removeAttribute(name);
|
||||||
|
|
||||||
|
if ('mail-body' === oElement.className) {
|
||||||
|
forEachObjectEntry(tasks, (name, cb) => {
|
||||||
|
if (hasAttribute(name)) {
|
||||||
|
cb(getAttribute(name), oElement);
|
||||||
|
delAttribute(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oElement.hasAttributes()) {
|
||||||
|
let i = oElement.attributes.length;
|
||||||
|
while (i--) {
|
||||||
|
let sAttrName = oElement.attributes[i].name.toLowerCase();
|
||||||
|
if (!allowedAttributes.includes(sAttrName)) {
|
||||||
|
delAttribute(sAttrName);
|
||||||
|
aAttrsForRemove.push(sAttrName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let value;
|
||||||
|
|
||||||
|
if ('TABLE' === name) {
|
||||||
|
if (hasAttribute('width')) {
|
||||||
|
oStyle.width = getAttribute('width');
|
||||||
|
delAttribute('width');
|
||||||
|
}
|
||||||
|
value = oStyle.width;
|
||||||
|
if (value && !value.includes('%')) {
|
||||||
|
oStyle.maxWidth = value;
|
||||||
|
oStyle.width = '100%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if ('A' === name) {
|
||||||
|
value = oElement.href;
|
||||||
|
if (!/^([a-z]+):/i.test(value)) {
|
||||||
|
setAttribute('data-x-broken-href', value);
|
||||||
|
delAttribute('href');
|
||||||
|
} else {
|
||||||
|
oElement.href = stripTracking(value);
|
||||||
|
setAttribute('target', '_blank');
|
||||||
|
setAttribute('rel', 'external nofollow noopener noreferrer');
|
||||||
|
}
|
||||||
|
setAttribute('tabindex', '-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVG xlink:href
|
||||||
|
/*
|
||||||
|
if (hasAttribute('xlink:href')) {
|
||||||
|
delAttribute('xlink:href');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
let skipStyle = false;
|
||||||
|
if (hasAttribute('src')) {
|
||||||
|
value = getAttribute('src');
|
||||||
|
delAttribute('src');
|
||||||
|
|
||||||
|
if (detectHiddenImages
|
||||||
|
&& 'IMG' === name
|
||||||
|
&& (('' != getAttribute('height') && 3 > pInt(getAttribute('height')))
|
||||||
|
|| ('' != getAttribute('width') && 3 > pInt(getAttribute('width')))
|
||||||
|
|| [
|
||||||
|
'email.microsoftemail.com/open',
|
||||||
|
'github.com/notifications/beacon/',
|
||||||
|
'mandrillapp.com/track/open',
|
||||||
|
'list-manage.com/track/open'
|
||||||
|
].filter(uri => value.toLowerCase().includes(uri)).length
|
||||||
|
)) {
|
||||||
|
skipStyle = true;
|
||||||
|
setAttribute('style', 'display:none');
|
||||||
|
setAttribute('data-x-hidden-src', value);
|
||||||
|
}
|
||||||
|
else if (contentLocationUrls[value])
|
||||||
|
{
|
||||||
|
setAttribute('data-x-src-location', value);
|
||||||
|
result.foundContentLocationUrls.push(value);
|
||||||
|
}
|
||||||
|
else if ('cid:' === value.slice(0, 4))
|
||||||
|
{
|
||||||
|
setAttribute('data-x-src-cid', value.slice(4));
|
||||||
|
result.foundCIDs.push(value.slice(4));
|
||||||
|
}
|
||||||
|
else if (/^https?:\/\//i.test(value) || '//' === value.slice(0, 2))
|
||||||
|
{
|
||||||
|
setAttribute('data-x-src', useProxy ? proxy(value) : value);
|
||||||
|
result.hasExternals = true;
|
||||||
|
}
|
||||||
|
else if ('data:image/' === value.slice(0, 11))
|
||||||
|
{
|
||||||
|
setAttribute('src', value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setAttribute('data-x-broken-src', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAttribute('background')) {
|
||||||
|
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
|
||||||
|
delAttribute('background');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAttribute('bgcolor')) {
|
||||||
|
oStyle.backgroundColor = getAttribute('bgcolor');
|
||||||
|
delAttribute('bgcolor');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAttribute('color')) {
|
||||||
|
oStyle.color = getAttribute('color');
|
||||||
|
delAttribute('color');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipStyle) {
|
||||||
|
/*
|
||||||
|
if ('fixed' === oStyle.position) {
|
||||||
|
oStyle.position = 'absolute';
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
oStyle.removeProperty('behavior');
|
||||||
|
oStyle.removeProperty('cursor');
|
||||||
|
oStyle.removeProperty('min-width');
|
||||||
|
|
||||||
|
const urls = {
|
||||||
|
cid: [], // 'data-x-style-cid'
|
||||||
|
remote: [], // 'data-x-style-url'
|
||||||
|
broken: [] // 'data-x-broken-style-src'
|
||||||
|
};
|
||||||
|
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
|
||||||
|
if (oStyle[property]) {
|
||||||
|
let value = oStyle[property],
|
||||||
|
found = value.match(/url\s*\(([^)]+)\)/gi);
|
||||||
|
if (found) {
|
||||||
|
oStyle[property] = null;
|
||||||
|
found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
|
||||||
|
let lowerUrl = found.toLowerCase();
|
||||||
|
if ('cid:' === lowerUrl.slice(0, 4)) {
|
||||||
|
found = found.slice(4);
|
||||||
|
urls.cid[property] = found
|
||||||
|
result.foundCIDs.push(found);
|
||||||
|
} else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
|
||||||
|
result.hasExternals = true;
|
||||||
|
urls.remote[property] = useProxy ? proxy(found) : found;
|
||||||
|
} else if ('data:image/' === lowerUrl.slice(0, 11)) {
|
||||||
|
oStyle[property] = value;
|
||||||
|
} else {
|
||||||
|
urls.broken[property] = found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// oStyle.removeProperty('background-image');
|
||||||
|
// oStyle.removeProperty('list-style-image');
|
||||||
|
|
||||||
|
if (urls.cid.length) {
|
||||||
|
setAttribute('data-x-style-cid', JSON.stringify(urls.cid));
|
||||||
|
}
|
||||||
|
if (urls.remote.length) {
|
||||||
|
setAttribute('data-x-style-url', JSON.stringify(urls.remote));
|
||||||
|
}
|
||||||
|
if (urls.broken.length) {
|
||||||
|
setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (11 > pInt(oStyle.fontSize)) {
|
||||||
|
oStyle.removeProperty('font-size');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes background and color
|
||||||
|
// Many e-mails incorrectly only define one, not both
|
||||||
|
// And in dark theme mode this kills the readability
|
||||||
|
if (removeColors) {
|
||||||
|
oStyle.removeProperty('background-color');
|
||||||
|
oStyle.removeProperty('background-image');
|
||||||
|
oStyle.removeProperty('color');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop Microsoft Office style properties
|
||||||
|
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug && aAttrsForRemove.length) {
|
||||||
|
setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// return tpl.content.firstChild;
|
||||||
|
result.html = tpl.innerHTML.trim();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} html
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
htmlToPlain = html => {
|
||||||
|
const
|
||||||
|
hr = '⎯'.repeat(64),
|
||||||
|
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
|
||||||
|
blockquotes = node => {
|
||||||
|
let bq;
|
||||||
|
while ((bq = node.querySelector('blockquote'))) {
|
||||||
|
// Convert child blockquote first
|
||||||
|
blockquotes(bq);
|
||||||
|
// Convert blockquote
|
||||||
|
// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '> ');
|
||||||
|
// replaceWithChildren(bq);
|
||||||
|
bq.replaceWith(
|
||||||
|
'\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
html = html
|
||||||
|
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
|
||||||
|
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
|
||||||
|
.replace(/\r?\n/g, '')
|
||||||
|
.replace(/\s+/gm, ' ');
|
||||||
|
|
||||||
|
while (/<(div|tr)[\s>]/i.test(html)) {
|
||||||
|
html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
||||||
|
}
|
||||||
|
while (/<\/(div|tr)[\s>]/i.test(html)) {
|
||||||
|
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl.innerHTML = html
|
||||||
|
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
||||||
|
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||||
|
|
||||||
|
// Convert line-breaks
|
||||||
|
forEach('br', br => br.replaceWith('\n'));
|
||||||
|
|
||||||
|
// lines
|
||||||
|
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
|
||||||
|
|
||||||
|
// headings
|
||||||
|
forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
|
||||||
|
|
||||||
|
// paragraphs
|
||||||
|
forEach('p', node => {
|
||||||
|
node.prepend('\n\n');
|
||||||
|
if ('' == node.textContent.trim()) {
|
||||||
|
node.remove();
|
||||||
|
} else {
|
||||||
|
node.after('\n\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// proper indenting and numbering of (un)ordered lists
|
||||||
|
forEach('ol,ul', node => {
|
||||||
|
let prefix = '',
|
||||||
|
parent = node,
|
||||||
|
ordered = 'OL' == node.tagName,
|
||||||
|
i = 0;
|
||||||
|
while (parent && parent.parentNode && parent.parentNode.closest) {
|
||||||
|
parent = parent.parentNode.closest('ol,ul');
|
||||||
|
parent && (prefix = ' ' + prefix);
|
||||||
|
}
|
||||||
|
node.querySelectorAll(':scope > li').forEach(li => {
|
||||||
|
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
|
||||||
|
});
|
||||||
|
node.prepend('\n\n');
|
||||||
|
node.after('\n\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert anchors
|
||||||
|
forEach('a', a => {
|
||||||
|
let txt = a.textContent, href = a.href;
|
||||||
|
return a.replaceWith((txt.trim() == href ? txt : txt + ' ' + href + ' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
|
||||||
|
// Italic
|
||||||
|
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
||||||
|
|
||||||
|
// Blockquotes must be last
|
||||||
|
blockquotes(tpl.content);
|
||||||
|
|
||||||
|
return (tpl.content.textContent || '').replace(/\n{3,}/gm, '\n\n').trim();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} plain
|
||||||
|
* @param {boolean} findEmailAndLinksInText = false
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
plainToHtml = plain => {
|
||||||
|
plain = stripTracking(plain)
|
||||||
|
.toString()
|
||||||
|
.replace(/\r/g, '')
|
||||||
|
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
|
||||||
|
|
||||||
|
let bIn = false,
|
||||||
|
bDo = true,
|
||||||
|
bStart = true,
|
||||||
|
aNextText = [],
|
||||||
|
aText = plain.split('\n');
|
||||||
|
|
||||||
|
do {
|
||||||
|
bDo = false;
|
||||||
|
aNextText = [];
|
||||||
|
aText.forEach(sLine => {
|
||||||
|
bStart = '>' === sLine.slice(0, 1);
|
||||||
|
if (bStart && !bIn) {
|
||||||
|
bDo = true;
|
||||||
|
bIn = true;
|
||||||
|
aNextText.push('~~~blockquote~~~');
|
||||||
|
aNextText.push(sLine.slice(1));
|
||||||
|
} else if (!bStart && bIn) {
|
||||||
|
if (sLine) {
|
||||||
|
bIn = false;
|
||||||
|
aNextText.push('~~~/blockquote~~~');
|
||||||
|
aNextText.push(sLine);
|
||||||
|
} else {
|
||||||
|
aNextText.push(sLine);
|
||||||
|
}
|
||||||
|
} else if (bStart && bIn) {
|
||||||
|
aNextText.push(sLine.slice(1));
|
||||||
|
} else {
|
||||||
|
aNextText.push(sLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bIn) {
|
||||||
|
bIn = false;
|
||||||
|
aNextText.push('~~~/blockquote~~~');
|
||||||
|
}
|
||||||
|
|
||||||
|
aText = aNextText;
|
||||||
|
} while (bDo);
|
||||||
|
|
||||||
|
return aText.join('\n')
|
||||||
|
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
|
||||||
|
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
};
|
||||||
|
|
||||||
export class HtmlEditor {
|
export class HtmlEditor {
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,12 +506,6 @@ export class HtmlEditor {
|
||||||
this.onBlur = onBlur;
|
this.onBlur = onBlur;
|
||||||
this.onModeChange = onModeChange;
|
this.onModeChange = onModeChange;
|
||||||
|
|
||||||
this.resize = (() => {
|
|
||||||
try {
|
|
||||||
this.editor && this.editor.resize(element.clientWidth, element.clientHeight);
|
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
|
||||||
}).throttle(100);
|
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
let editor;
|
let editor;
|
||||||
|
|
||||||
|
|
@ -42,7 +513,6 @@ export class HtmlEditor {
|
||||||
this.onReady = fn => onReady.push(fn);
|
this.onReady = fn => onReady.push(fn);
|
||||||
const readyCallback = () => {
|
const readyCallback = () => {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.resize();
|
|
||||||
this.onReady = fn => fn();
|
this.onReady = fn => fn();
|
||||||
onReady.forEach(fn => fn());
|
onReady.forEach(fn => fn());
|
||||||
};
|
};
|
||||||
|
|
@ -112,31 +582,25 @@ export class HtmlEditor {
|
||||||
* @param {boolean=} wrapIsHtml = false
|
* @param {boolean=} wrapIsHtml = false
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getData(wrapIsHtml = false) {
|
getData() {
|
||||||
let result = '';
|
let result = '';
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
try {
|
try {
|
||||||
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
|
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
|
||||||
result = this.editor.__plain.getRawData();
|
result = this.editor.__plain.getRawData();
|
||||||
} else {
|
} else {
|
||||||
result = wrapIsHtml
|
result = this.editor.getData();
|
||||||
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
|
|
||||||
this.editor.getData() +
|
|
||||||
'</div>'
|
|
||||||
: this.editor.getData();
|
|
||||||
}
|
}
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} wrapIsHtml = false
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getDataWithHtmlMark(wrapIsHtml = false) {
|
getDataWithHtmlMark() {
|
||||||
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml);
|
return (this.isHtml() ? ':HTML:' : '') + this.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
modeWysiwyg() {
|
modeWysiwyg() {
|
||||||
|
|
@ -147,8 +611,8 @@ export class HtmlEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
setHtmlOrPlain(text) {
|
setHtmlOrPlain(text) {
|
||||||
if (':HTML:' === text.substr(0, 6)) {
|
if (':HTML:' === text.slice(0, 6)) {
|
||||||
this.setHtml(text.substr(6));
|
this.setHtml(text.slice(6));
|
||||||
} else {
|
} else {
|
||||||
this.setPlain(text);
|
this.setPlain(text);
|
||||||
}
|
}
|
||||||
|
|
@ -197,3 +661,8 @@ export class HtmlEditor {
|
||||||
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
|
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rl.Utils = {
|
||||||
|
htmlToPlain: htmlToPlain,
|
||||||
|
plainToHtml: plainToHtml
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@ import { pString, pInt } from 'Common/Utils';
|
||||||
import { Settings } from 'Common/Globals';
|
import { Settings } from 'Common/Globals';
|
||||||
|
|
||||||
const
|
const
|
||||||
ROOT = './',
|
|
||||||
HASH_PREFIX = '#/',
|
HASH_PREFIX = '#/',
|
||||||
SERVER_PREFIX = './?',
|
SERVER_PREFIX = './?',
|
||||||
VERSION = Settings.app('version'),
|
VERSION = Settings.app('version'),
|
||||||
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/';
|
VERSION_PREFIX = () => Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
|
||||||
|
|
||||||
|
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
|
||||||
|
|
||||||
|
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
|
||||||
|
|
||||||
export const
|
export const
|
||||||
SUB_QUERY_PREFIX = '&q[]=',
|
SUB_QUERY_PREFIX = '&q[]=',
|
||||||
|
|
@ -15,14 +18,12 @@ export const
|
||||||
* @param {string=} startupUrl
|
* @param {string=} startupUrl
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
root = (startupUrl = '') => HASH_PREFIX + pString(startupUrl),
|
root = () => HASH_PREFIX,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
logoutLink = () => (rl.adminArea() && !Settings.app('adminHostUse'))
|
logoutLink = () => adminPath() ? prefix() : './',
|
||||||
? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
|
|
||||||
: ROOT,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
|
|
@ -45,17 +46,21 @@ export const
|
||||||
attachmentDownload = (download, customSpecSuffix) =>
|
attachmentDownload = (download, customSpecSuffix) =>
|
||||||
serverRequestRaw('Download', download, customSpecSuffix),
|
serverRequestRaw('Download', download, customSpecSuffix),
|
||||||
|
|
||||||
|
proxy = url =>
|
||||||
|
SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
|
/*
|
||||||
|
return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
|
||||||
|
'Rnd' => \md5(\microtime(true)),
|
||||||
|
'Token' => Utils::GetConnectionToken(),
|
||||||
|
'Url' => $sUrl
|
||||||
|
)).'/';
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
serverRequest = type => SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} email
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
change = email => serverRequest('Change') + encodeURIComponent(email) + '/',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} lang
|
* @param {string} lang
|
||||||
|
|
@ -69,26 +74,16 @@ export const
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
staticLink = path => VERSION_PREFIX + 'static/' + path,
|
staticLink = path => VERSION_PREFIX() + 'static/' + path,
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
openPgpJs = () => staticLink('js/min/openpgp.min.js'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
openPgpWorkerJs = () => staticLink('js/min/openpgp.worker.min.js'),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} theme
|
* @param {string} theme
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
themePreviewLink = theme => {
|
themePreviewLink = theme => {
|
||||||
let prefix = VERSION_PREFIX;
|
let prefix = VERSION_PREFIX();
|
||||||
if ('@custom' === theme.substr(-7)) {
|
if ('@custom' === theme.slice(-7)) {
|
||||||
theme = theme.substr(0, theme.length - 7).trim();
|
theme = theme.slice(0, theme.length - 7).trim();
|
||||||
prefix = Settings.app('webPath') || '';
|
prefix = Settings.app('webPath') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,23 +119,22 @@ export const
|
||||||
* @param {number=} threadUid = 0
|
* @param {number=} threadUid = 0
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
mailBox = (folder, page = 1, search = '', threadUid = 0) => {
|
mailBox = (folder, page, search, threadUid) => {
|
||||||
page = pInt(page, 1);
|
let result = [HASH_PREFIX + 'mailbox'];
|
||||||
search = pString(search);
|
|
||||||
|
|
||||||
let result = HASH_PREFIX + 'mailbox/';
|
|
||||||
|
|
||||||
if (folder) {
|
if (folder) {
|
||||||
result += encodeURI(folder) + (threadUid ? '~' + threadUid : '');
|
result.push(folder + (threadUid ? '~' + threadUid : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page = pInt(page, 1);
|
||||||
if (1 < page) {
|
if (1 < page) {
|
||||||
result = result.replace(/\/+$/, '') + '/p' + page;
|
result.push('p' + page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search = pString(search);
|
||||||
if (search) {
|
if (search) {
|
||||||
result = result.replace(/\/+$/, '') + '/' + encodeURI(search);
|
result.push(encodeURI(search));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.join('/');
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { i18n } from 'Common/Translator';
|
|
||||||
|
|
||||||
export function timestampToString(timeStampInUTC, formatStr) {
|
|
||||||
const now = Date.now(),
|
|
||||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
|
||||||
|
|
||||||
if (31536000000 < time) {
|
|
||||||
const m = new Date(time);
|
|
||||||
switch (formatStr) {
|
|
||||||
case 'FROMNOW':
|
|
||||||
return m.fromNow();
|
|
||||||
case 'SHORT': {
|
|
||||||
if (4 >= (now - time) / 3600000)
|
|
||||||
return m.fromNow();
|
|
||||||
const mt = m.getTime(), date = new Date,
|
|
||||||
dt = date.setHours(0,0,0,0);
|
|
||||||
if (mt > dt)
|
|
||||||
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
|
||||||
if (mt > dt - 86400000)
|
|
||||||
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
|
||||||
if (date.getFullYear() === m.getFullYear())
|
|
||||||
return m.format('d M');
|
|
||||||
return m.format('LL');
|
|
||||||
}
|
|
||||||
case 'FULL':
|
|
||||||
return m.format('LLL');
|
|
||||||
default:
|
|
||||||
return m.format(formatStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function timeToNode(element, time) {
|
|
||||||
try {
|
|
||||||
time = time || (Date.parse(element.dateTime) / 1000);
|
|
||||||
if (time) {
|
|
||||||
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
|
||||||
|
|
||||||
let key = element.dataset.momentFormat;
|
|
||||||
if (key) {
|
|
||||||
element.textContent = timestampToString(time, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((key = element.dataset.momentFormatTitle)) {
|
|
||||||
element.title = timestampToString(time, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// prevent knockout crashes
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { settingsAddViewModel } from 'Screen/AbstractSettings';
|
import { settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||||
import { SettingsGet } from 'Common/Globals';
|
import { SettingsGet } from 'Common/Globals';
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
|
||||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
const USER_VIEW_MODELS_HOOKS = [],
|
const USER_VIEW_MODELS_HOOKS = [],
|
||||||
|
|
@ -13,7 +12,7 @@ const USER_VIEW_MODELS_HOOKS = [],
|
||||||
* @param {?number=} timeout
|
* @param {?number=} timeout
|
||||||
*/
|
*/
|
||||||
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
|
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
|
||||||
rl.app && rl.app.Remote.defaultRequest(callback, 'Plugin' + action, parameters, timeout);
|
rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,5 +55,4 @@ rl.pluginSettingsGet = (pluginSection, name) => {
|
||||||
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
|
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
rl.showPluginPopup = showScreenPopup;
|
|
||||||
rl.pluginPopupView = AbstractViewPopup;
|
rl.pluginPopupView = AbstractViewPopup;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
|
import { koComputable } from 'External/ko';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
oCallbacks:
|
oCallbacks:
|
||||||
|
|
@ -28,8 +30,8 @@ export class Selector {
|
||||||
sItemFocusedSelector
|
sItemFocusedSelector
|
||||||
) {
|
) {
|
||||||
this.list = koList;
|
this.list = koList;
|
||||||
this.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
|
this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
|
||||||
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
|
this.isListChecked = koComputable(() => 0 < this.listChecked().length);
|
||||||
|
|
||||||
this.focusedItem = koFocusedItem || ko.observable(null);
|
this.focusedItem = koFocusedItem || ko.observable(null);
|
||||||
this.selectedItem = koSelectedItem || ko.observable(null);
|
this.selectedItem = koSelectedItem || ko.observable(null);
|
||||||
|
|
@ -238,32 +240,33 @@ export class Selector {
|
||||||
return el ? ko.dataFor(el) : null;
|
return el ? ko.dataFor(el) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
contentScrollable.addEventListener('click', event => {
|
addEventsListeners(contentScrollable, {
|
||||||
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
click: event => {
|
||||||
el && this.actionClick(ko.dataFor(el), event);
|
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||||
|
el && this.actionClick(ko.dataFor(el), event);
|
||||||
|
|
||||||
const item = getItem(this.sItemCheckedSelector);
|
const item = getItem(this.sItemCheckedSelector);
|
||||||
if (item) {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
this.actionClick(item, event);
|
|
||||||
} else {
|
|
||||||
this.focusedItem(item);
|
|
||||||
item.checked(!item.checked());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
contentScrollable.addEventListener('auxclick', event => {
|
|
||||||
if (1 == event.button) {
|
|
||||||
const item = getItem(this.sItemSelector);
|
|
||||||
if (item) {
|
if (item) {
|
||||||
this.focusedItem(item);
|
if (event.shiftKey) {
|
||||||
(this.oCallbacks.MiddleClick || (()=>0))(item);
|
this.actionClick(item, event);
|
||||||
|
} else {
|
||||||
|
this.focusedItem(item);
|
||||||
|
item.checked(!item.checked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auxclick: event => {
|
||||||
|
if (1 == event.button) {
|
||||||
|
const item = getItem(this.sItemSelector);
|
||||||
|
if (item) {
|
||||||
|
this.focusedItem(item);
|
||||||
|
(this.oCallbacks.MiddleClick || (()=>0))(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
shortcuts.add('enter,open', '', keyScope, () => {
|
registerShortcut('enter,open', '', keyScope, () => {
|
||||||
const focused = this.focusedItem();
|
const focused = this.focusedItem();
|
||||||
if (focused && !focused.selected()) {
|
if (focused && !focused.selected()) {
|
||||||
this.actionClick(focused);
|
this.actionClick(focused);
|
||||||
|
|
@ -271,13 +274,13 @@ export class Selector {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
shortcuts.add('arrowup,arrowdown', 'meta', keyScope, () => false);
|
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
|
||||||
|
|
||||||
shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => {
|
addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
|
||||||
this.newSelectPosition(event.key, true);
|
this.newSelectPosition(event.key, true);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
||||||
this.newSelectPosition(event.key, false);
|
this.newSelectPosition(event.key, false);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
@ -288,7 +291,7 @@ export class Selector {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
autoSelect() {
|
autoSelect() {
|
||||||
return !!(this.oCallbacks.AutoSelect || (()=>true))();
|
return !!(this.oCallbacks.AutoSelect || (()=>1))();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -329,7 +332,7 @@ export class Selector {
|
||||||
} else if (++i < listLen) {
|
} else if (++i < listLen) {
|
||||||
result = list[i];
|
result = list[i];
|
||||||
}
|
}
|
||||||
result || (this.oCallbacks.UpOrDown || (()=>true))('ArrowUp' === sEventKey);
|
result || (this.oCallbacks.UpOrDown || (()=>0))('ArrowUp' === sEventKey);
|
||||||
} else if ('Home' === sEventKey) {
|
} else if ('Home' === sEventKey) {
|
||||||
result = list[0];
|
result = list[0];
|
||||||
} else if ('End' === sEventKey) {
|
} else if ('End' === sEventKey) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import ko from 'ko';
|
||||||
import { Notification, UploadErrorCode } from 'Common/Enums';
|
import { Notification, UploadErrorCode } from 'Common/Enums';
|
||||||
import { langLink } from 'Common/Links';
|
import { langLink } from 'Common/Links';
|
||||||
import { doc, createElement } from 'Common/Globals';
|
import { doc, createElement } from 'Common/Globals';
|
||||||
|
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
||||||
|
|
||||||
let I18N_DATA = {};
|
let I18N_DATA = {};
|
||||||
|
|
||||||
|
|
@ -9,16 +10,16 @@ const
|
||||||
i18nToNode = element => {
|
i18nToNode = element => {
|
||||||
const key = element.dataset.i18n;
|
const key = element.dataset.i18n;
|
||||||
if (key) {
|
if (key) {
|
||||||
if ('[' === key.substr(0, 1)) {
|
if ('[' === key.slice(0, 1)) {
|
||||||
switch (key.substr(0, 6)) {
|
switch (key.slice(0, 6)) {
|
||||||
case '[html]':
|
case '[html]':
|
||||||
element.innerHTML = i18n(key.substr(6));
|
element.innerHTML = i18n(key.slice(6));
|
||||||
break;
|
break;
|
||||||
case '[place':
|
case '[place':
|
||||||
element.placeholder = i18n(key.substr(13));
|
element.placeholder = i18n(key.slice(13));
|
||||||
break;
|
break;
|
||||||
case '[title':
|
case '[title':
|
||||||
element.title = i18n(key.substr(7));
|
element.title = i18n(key.slice(7));
|
||||||
break;
|
break;
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
|
|
@ -39,14 +40,9 @@ const
|
||||||
|
|
||||||
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
||||||
|
|
||||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v),
|
|
||||||
|
|
||||||
getNotificationMessage = code => {
|
getNotificationMessage = code => {
|
||||||
let key = getKeyByValue(Notification, code);
|
let key = getKeyByValue(Notification, code);
|
||||||
if (key) {
|
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||||
key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
|
|
||||||
return I18N_DATA.NOTIFICATIONS[key];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
@ -67,7 +63,7 @@ export const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (valueList) {
|
if (valueList) {
|
||||||
Object.entries(valueList).forEach(([key, value]) => {
|
forEachObjectEntry(valueList, (key, value) => {
|
||||||
result = result.replace('%' + key + '%', value);
|
result = result.replace('%' + key + '%', value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +79,64 @@ export const
|
||||||
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
|
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
|
||||||
, 1),
|
, 1),
|
||||||
|
|
||||||
|
timestampToString = (timeStampInUTC, formatStr) => {
|
||||||
|
const now = Date.now(),
|
||||||
|
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||||
|
|
||||||
|
if (31536000000 < time) {
|
||||||
|
const m = new Date(time);
|
||||||
|
switch (formatStr) {
|
||||||
|
case 'FROMNOW':
|
||||||
|
return m.fromNow();
|
||||||
|
case 'SHORT': {
|
||||||
|
if (4 >= (now - time) / 3600000)
|
||||||
|
return m.fromNow();
|
||||||
|
const mt = m.getTime(), date = new Date,
|
||||||
|
dt = date.setHours(0,0,0,0);
|
||||||
|
if (mt > dt)
|
||||||
|
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
||||||
|
if (mt > dt - 86400000)
|
||||||
|
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
||||||
|
if (date.getFullYear() === m.getFullYear())
|
||||||
|
return m.format('d M');
|
||||||
|
return m.format('LL');
|
||||||
|
}
|
||||||
|
case 'FULL':
|
||||||
|
return m.format('LLL');
|
||||||
|
default:
|
||||||
|
return m.format(formatStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
timeToNode = (element, time) => {
|
||||||
|
try {
|
||||||
|
if (time) {
|
||||||
|
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
||||||
|
} else {
|
||||||
|
time = Date.parse(element.dateTime) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = element.dataset.momentFormat;
|
||||||
|
if (key) {
|
||||||
|
element.textContent = timestampToString(time, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((key = element.dataset.momentFormatTitle)) {
|
||||||
|
element.title = timestampToString(time, key);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// prevent knockout crashes
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reloadTime = () => setTimeout(() =>
|
||||||
|
doc.querySelectorAll('time').forEach(element => timeToNode(element))
|
||||||
|
, 1),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} startCallback
|
* @param {Function} startCallback
|
||||||
* @param {Function=} langCallback = null
|
* @param {Function=} langCallback = null
|
||||||
|
|
@ -115,33 +169,22 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getUploadErrorDescByCode = code => {
|
getUploadErrorDescByCode = code => {
|
||||||
let result = 'UNKNOWN';
|
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
|
||||||
code = parseInt(code, 10) || 0;
|
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
|
||||||
switch (code) {
|
|
||||||
case UploadErrorCode.FileIsTooBig:
|
|
||||||
case UploadErrorCode.FilePartiallyUploaded:
|
|
||||||
case UploadErrorCode.NoFileUploaded:
|
|
||||||
case UploadErrorCode.MissingTempFolder:
|
|
||||||
case UploadErrorCode.OnSavingFile:
|
|
||||||
case UploadErrorCode.FileType:
|
|
||||||
result = i18nKey(getKeyByValue(UploadErrorCode, code));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return i18n('UPLOAD/ERROR_' + result);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} admin
|
* @param {boolean} admin
|
||||||
* @param {string} language
|
* @param {string} language
|
||||||
*/
|
*/
|
||||||
reload = (admin, language) =>
|
translatorReload = (admin, language) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const script = createElement('script');
|
const script = createElement('script');
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
// reload the data
|
// reload the data
|
||||||
if (init()) {
|
if (init()) {
|
||||||
i18nToNodes(doc);
|
i18nToNodes(doc);
|
||||||
admin || rl.app.reloadTime();
|
admin || reloadTime();
|
||||||
trigger(!trigger());
|
trigger(!trigger());
|
||||||
}
|
}
|
||||||
script.remove();
|
script.remove();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
import { doc, elementById } from 'Common/Globals';
|
import { elementById } from 'Common/Globals';
|
||||||
|
|
||||||
let __themeTimer = 0,
|
let __themeTimer = 0,
|
||||||
__themeJson = null;
|
__themeJson = null;
|
||||||
|
|
@ -10,6 +10,10 @@ export const
|
||||||
isFunction = v => typeof v === 'function',
|
isFunction = v => typeof v === 'function',
|
||||||
pString = value => null != value ? '' + value : '',
|
pString = value => null != value ? '' + value : '',
|
||||||
|
|
||||||
|
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||||
|
|
||||||
|
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
|
||||||
|
|
||||||
pInt = (value, defaultValue = 0) => {
|
pInt = (value, defaultValue = 0) => {
|
||||||
value = parseInt(value, 10);
|
value = parseInt(value, 10);
|
||||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
||||||
|
|
@ -25,72 +29,43 @@ export const
|
||||||
domItem && item && undefined !== item.disabled
|
domItem && item && undefined !== item.disabled
|
||||||
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
|
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
|
||||||
|
|
||||||
addObservablesTo = (target, observables) =>
|
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
||||||
Object.entries(observables).forEach(([key, value]) =>
|
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
|
||||||
target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) ),
|
/* // Without deprecated 'unescape':
|
||||||
|
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
|
||||||
addComputablesTo = (target, computables) =>
|
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
||||||
Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn)),
|
)),
|
||||||
|
*/
|
||||||
addSubscribablesTo = (target, subscribables) =>
|
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn)),
|
|
||||||
|
|
||||||
inFocus = () => {
|
|
||||||
try {
|
|
||||||
return doc.activeElement && doc.activeElement.matches(
|
|
||||||
'input,textarea,[contenteditable]'
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
settingsSaveHelperSimpleFunction = (koTrigger, context) =>
|
|
||||||
iError => {
|
|
||||||
koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
|
|
||||||
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
changeTheme = (value, themeTrigger = ()=>0) => {
|
changeTheme = (value, themeTrigger = ()=>0) => {
|
||||||
const themeStyle = elementById('app-theme-style'),
|
const themeStyle = elementById('app-theme-style'),
|
||||||
clearTimer = () => {
|
clearTimer = () => {
|
||||||
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
|
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
|
||||||
__themeJson = null;
|
__themeJson = null;
|
||||||
};
|
},
|
||||||
|
url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
|
||||||
|
|
||||||
let url = themeStyle.dataset.href;
|
clearTimeout(__themeTimer);
|
||||||
|
|
||||||
if (url) {
|
themeTrigger(SaveSettingsStep.Animate);
|
||||||
url = url.toString()
|
|
||||||
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
|
|
||||||
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
|
|
||||||
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
|
|
||||||
|
|
||||||
if ('Json/' !== url.substr(-5)) {
|
if (__themeJson) {
|
||||||
url += 'Json/';
|
__themeJson.abort();
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(__themeTimer);
|
|
||||||
|
|
||||||
themeTrigger(SaveSettingsStep.Animate);
|
|
||||||
|
|
||||||
if (__themeJson) {
|
|
||||||
__themeJson.abort();
|
|
||||||
}
|
|
||||||
let init = {};
|
|
||||||
if (window.AbortController) {
|
|
||||||
__themeJson = new AbortController();
|
|
||||||
init.signal = __themeJson.signal;
|
|
||||||
}
|
|
||||||
rl.fetchJSON(url, init)
|
|
||||||
.then(data => {
|
|
||||||
if (2 === arrayLength(data)) {
|
|
||||||
themeStyle.textContent = data[1];
|
|
||||||
themeStyle.dataset.href = url;
|
|
||||||
themeStyle.dataset.theme = data[0];
|
|
||||||
themeTrigger(SaveSettingsStep.TrueResult);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(clearTimer, clearTimer);
|
|
||||||
}
|
}
|
||||||
};
|
let init = {};
|
||||||
|
if (window.AbortController) {
|
||||||
|
__themeJson = new AbortController();
|
||||||
|
init.signal = __themeJson.signal;
|
||||||
|
}
|
||||||
|
rl.fetchJSON(url, init)
|
||||||
|
.then(data => {
|
||||||
|
if (2 === arrayLength(data)) {
|
||||||
|
themeStyle.textContent = data[1];
|
||||||
|
themeTrigger(SaveSettingsStep.TrueResult);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(clearTimer, clearTimer);
|
||||||
|
},
|
||||||
|
|
||||||
|
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
||||||
|
|
|
||||||
|
|
@ -1,260 +1,46 @@
|
||||||
import { ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
import { MessageFlagsCache, addRequestedMessage } from 'Common/Cache';
|
||||||
|
import { Notification } from 'Common/Enums';
|
||||||
|
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||||
|
import { doc, createElement, elementById, dropdowns, dropdownVisibility } from 'Common/Globals';
|
||||||
|
import { plainToHtml } from 'Common/Html';
|
||||||
|
import { getNotification } from 'Common/Translator';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
import { encodeHtml } from 'Common/Html';
|
import { MessageModel } from 'Model/Message';
|
||||||
import { isArray } from 'Common/Utils';
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
import { createElement } from 'Common/Globals';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
import * as Local from 'Storage/Client';
|
||||||
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
export const
|
||||||
|
|
||||||
|
dropdownsDetectVisibility = (() =>
|
||||||
|
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||||
|
).debounce(50),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {(string|number)} value
|
* @param {string} link
|
||||||
* @param {boolean=} includeZero = true
|
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isPosNumeric(value) {
|
download = (link, name = "") => {
|
||||||
return null != value && /^[0-9]*$/.test(value.toString());
|
if (ThemeStore.isMobile()) {
|
||||||
}
|
open(link, '_self');
|
||||||
|
focus();
|
||||||
/**
|
} else {
|
||||||
* @param {string} html
|
const oLink = createElement('a');
|
||||||
* @returns {string}
|
oLink.href = link;
|
||||||
*/
|
oLink.target = '_blank';
|
||||||
export function htmlToPlain(html) {
|
oLink.download = name;
|
||||||
let pos = 0,
|
doc.body.appendChild(oLink).click();
|
||||||
limit = 800,
|
oLink.remove();
|
||||||
iP1 = 0,
|
|
||||||
iP2 = 0,
|
|
||||||
iP3 = 0,
|
|
||||||
text = '';
|
|
||||||
|
|
||||||
const
|
|
||||||
tpl = createElement('template'),
|
|
||||||
|
|
||||||
convertBlockquote = (blockquoteText) => {
|
|
||||||
blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
|
|
||||||
return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
|
|
||||||
args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
convertDivs = (...args) => {
|
|
||||||
let divText = 1 < args.length ? args[1].trim() : '';
|
|
||||||
if (divText.length) {
|
|
||||||
divText = '\n' + divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs).trim() + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
return divText;
|
|
||||||
},
|
|
||||||
|
|
||||||
convertPre = (...args) =>
|
|
||||||
1 < args.length
|
|
||||||
? args[1]
|
|
||||||
.toString()
|
|
||||||
.replace(/[\n]/gm, '<br/>')
|
|
||||||
.replace(/[\r]/gm, '')
|
|
||||||
: '',
|
|
||||||
fixAttibuteValue = (...args) => (1 < args.length ? args[1] + encodeHtml(args[2]) : ''),
|
|
||||||
|
|
||||||
convertLinks = (...args) => (1 < args.length ? args[1].trim() : '');
|
|
||||||
|
|
||||||
tpl.innerHTML = html
|
|
||||||
.replace(/<p[^>]*><\/p>/gi, '')
|
|
||||||
.replace(/<pre[^>]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre)
|
|
||||||
.replace(/[\s]+/gm, ' ')
|
|
||||||
.replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue)
|
|
||||||
.replace(/<br[^>]*>/gim, '\n')
|
|
||||||
.replace(/<\/h[\d]>/gi, '\n')
|
|
||||||
.replace(/<\/p>/gi, '\n\n')
|
|
||||||
.replace(/<ul[^>]*>/gim, '\n')
|
|
||||||
.replace(/<\/ul>/gi, '\n')
|
|
||||||
.replace(/<li[^>]*>/gim, ' * ')
|
|
||||||
.replace(/<\/li>/gi, '\n')
|
|
||||||
.replace(/<\/td>/gi, '\n')
|
|
||||||
.replace(/<\/tr>/gi, '\n')
|
|
||||||
.replace(/<hr[^>]*>/gim, '\n_______________________________\n\n')
|
|
||||||
.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs)
|
|
||||||
.replace(/<blockquote[^>]*>/gim, '\n__bq__start__\n')
|
|
||||||
.replace(/<\/blockquote>/gim, '\n__bq__end__\n')
|
|
||||||
.replace(/<a [^>]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks)
|
|
||||||
.replace(/<\/div>/gi, '\n')
|
|
||||||
.replace(/ /gi, ' ')
|
|
||||||
.replace(/"/gi, '"')
|
|
||||||
.replace(/<[^>]*>/gm, '');
|
|
||||||
|
|
||||||
text = tpl.content.textContent;
|
|
||||||
if (text) {
|
|
||||||
text = text
|
|
||||||
.replace(/\n[ \t]+/gm, '\n')
|
|
||||||
.replace(/[\n]{3,}/gm, '\n\n')
|
|
||||||
.replace(/>/gi, '>')
|
|
||||||
.replace(/</gi, '<')
|
|
||||||
.replace(/&/gi, '&')
|
|
||||||
// wordwrap max line length 100
|
|
||||||
.match(/.{1,100}(\s|$)|\S+?(\s|$)/g).join('\n');
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
while (0 < --limit) {
|
|
||||||
iP1 = text.indexOf('__bq__start__', pos);
|
|
||||||
if (0 > iP1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
iP2 = text.indexOf('__bq__start__', iP1 + 5);
|
|
||||||
iP3 = text.indexOf('__bq__end__', iP1 + 5);
|
|
||||||
|
|
||||||
if ((-1 === iP2 || iP3 < iP2) && iP1 < iP3) {
|
|
||||||
text = text.substr(0, iP1) + convertBlockquote(text.substring(iP1 + 13, iP3)) + text.substr(iP3 + 11);
|
|
||||||
pos = 0;
|
|
||||||
} else if (-1 < iP2 && iP2 < iP3) {
|
|
||||||
pos = iP2 - 1;
|
|
||||||
} else {
|
|
||||||
pos = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.replace(/__bq__start__|__bq__end__/gm, '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} plain
|
|
||||||
* @param {boolean} findEmailAndLinksInText = false
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function plainToHtml(plain) {
|
|
||||||
plain = plain.toString().replace(/\r/g, '');
|
|
||||||
plain = plain.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
|
|
||||||
|
|
||||||
let bIn = false,
|
|
||||||
bDo = true,
|
|
||||||
bStart = true,
|
|
||||||
aNextText = [],
|
|
||||||
aText = plain.split('\n');
|
|
||||||
|
|
||||||
do {
|
|
||||||
bDo = false;
|
|
||||||
aNextText = [];
|
|
||||||
aText.forEach(sLine => {
|
|
||||||
bStart = '>' === sLine.substr(0, 1);
|
|
||||||
if (bStart && !bIn) {
|
|
||||||
bDo = true;
|
|
||||||
bIn = true;
|
|
||||||
aNextText.push('~~~blockquote~~~');
|
|
||||||
aNextText.push(sLine.substr(1));
|
|
||||||
} else if (!bStart && bIn) {
|
|
||||||
if (sLine) {
|
|
||||||
bIn = false;
|
|
||||||
aNextText.push('~~~/blockquote~~~');
|
|
||||||
aNextText.push(sLine);
|
|
||||||
} else {
|
|
||||||
aNextText.push(sLine);
|
|
||||||
}
|
|
||||||
} else if (bStart && bIn) {
|
|
||||||
aNextText.push(sLine.substr(1));
|
|
||||||
} else {
|
|
||||||
aNextText.push(sLine);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bIn) {
|
|
||||||
bIn = false;
|
|
||||||
aNextText.push('~~~/blockquote~~~');
|
|
||||||
}
|
|
||||||
|
|
||||||
aText = aNextText;
|
|
||||||
} while (bDo);
|
|
||||||
|
|
||||||
return aText.join('\n')
|
|
||||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/~~~blockquote~~~[\s]*/g, '<blockquote>')
|
|
||||||
.replace(/[\s]*~~~\/blockquote~~~/g, '</blockquote>')
|
|
||||||
.replace(/\n/g, '<br/>');
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.Utils = {
|
|
||||||
htmlToPlain: htmlToPlain,
|
|
||||||
plainToHtml: plainToHtml
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array=} aDisabled
|
|
||||||
* @param {Array=} aHeaderLines
|
|
||||||
* @param {Function=} fDisableCallback
|
|
||||||
* @param {Function=} fRenameCallback
|
|
||||||
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
export function folderListOptionsBuilder(
|
|
||||||
aDisabled,
|
|
||||||
aHeaderLines,
|
|
||||||
fRenameCallback,
|
|
||||||
fDisableCallback,
|
|
||||||
bNoSelectSelectable,
|
|
||||||
aList = FolderUserStore.folderList()
|
|
||||||
) {
|
|
||||||
const
|
|
||||||
aResult = [],
|
|
||||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
|
||||||
// FolderSystemPopupView should always be true
|
|
||||||
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
|
||||||
|
|
||||||
foldersWalk = folders => {
|
|
||||||
folders.forEach(oItem => {
|
|
||||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
|
||||||
aResult.push({
|
|
||||||
id: oItem.fullNameRaw,
|
|
||||||
name:
|
|
||||||
sDeepPrefix.repeat(oItem.deep) +
|
|
||||||
fRenameCallback(oItem),
|
|
||||||
system: false,
|
|
||||||
disabled: !bNoSelectSelectable && (
|
|
||||||
!oItem.selectable() ||
|
|
||||||
aDisabled.includes(oItem.fullNameRaw) ||
|
|
||||||
fDisableCallback(oItem))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oItem.subFolders.length) {
|
|
||||||
foldersWalk(oItem.subFolders());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
fDisableCallback = fDisableCallback || (() => false);
|
|
||||||
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
|
||||||
isArray(aDisabled) || (aDisabled = []);
|
|
||||||
|
|
||||||
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
|
||||||
aResult.push({
|
|
||||||
id: line[0],
|
|
||||||
name: line[1],
|
|
||||||
system: false,
|
|
||||||
disabled: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
foldersWalk(aList);
|
|
||||||
|
|
||||||
return aResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
|
|
||||||
* @param {Object|Array} objectOrObjects
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function delegateRunOnDestroy(objectOrObjects) {
|
|
||||||
objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
|
|
||||||
obj => obj.onDestroy && obj.onDestroy()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {function}
|
* @returns {function}
|
||||||
*/
|
*/
|
||||||
export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||||
return () => {
|
return () => {
|
||||||
const currentPage = koCurrentPage(),
|
const currentPage = koCurrentPage(),
|
||||||
pageCount = koPageCount(),
|
pageCount = koPageCount(),
|
||||||
|
|
@ -335,71 +121,253 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} mailToUrl
|
* @param {string} mailToUrl
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function mailToHelper(mailToUrl) {
|
mailToHelper = mailToUrl => {
|
||||||
if (
|
if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) {
|
||||||
mailToUrl &&
|
mailToUrl = mailToUrl.slice(7).split('?');
|
||||||
'mailto:' ===
|
|
||||||
mailToUrl
|
|
||||||
.toString()
|
|
||||||
.substr(0, 7)
|
|
||||||
.toLowerCase()
|
|
||||||
) {
|
|
||||||
mailToUrl = mailToUrl.toString().substr(7);
|
|
||||||
|
|
||||||
let to = [],
|
const
|
||||||
params = {};
|
email = mailToUrl[0],
|
||||||
|
params = new URLSearchParams(mailToUrl[1]),
|
||||||
const email = mailToUrl.replace(/\?.+$/, ''),
|
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
|
||||||
query = mailToUrl.replace(/^[^?]*\?/, ''),
|
|
||||||
toEmailModel = value => null != value ? EmailModel.parseEmailLine(decodeURIComponent(value)) : null;
|
|
||||||
|
|
||||||
query.split('&').forEach(temp => {
|
|
||||||
temp = temp.split('=');
|
|
||||||
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (null != params.to) {
|
|
||||||
to = Object.values(
|
|
||||||
toEmailModel(email + ',' + params.to).reduce((result, value) => {
|
|
||||||
if (value) {
|
|
||||||
if (result[value.email]) {
|
|
||||||
if (!result[value.email].name) {
|
|
||||||
result[value.email] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result[value.email] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, {})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
to = EmailModel.parseEmailLine(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessageComposer([
|
showMessageComposer([
|
||||||
ComposeType.Empty,
|
ComposeType.Empty,
|
||||||
null,
|
null,
|
||||||
to,
|
params.get('to')
|
||||||
toEmailModel(params.cc),
|
? Object.values(
|
||||||
toEmailModel(params.bcc),
|
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
|
||||||
null == params.subject ? null : decodeURIComponent(params.subject),
|
if (value) {
|
||||||
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
|
if (result[value.email]) {
|
||||||
|
if (!result[value.email].name) {
|
||||||
|
result[value.email] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[value.email] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
: EmailModel.parseEmailLine(email),
|
||||||
|
toEmailModel(params.get('cc')),
|
||||||
|
toEmailModel(params.get('bcc')),
|
||||||
|
params.get('subject'),
|
||||||
|
plainToHtml(params.get('body') || '')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
},
|
||||||
|
|
||||||
export function showMessageComposer(params = [])
|
showMessageComposer = (params = []) =>
|
||||||
{
|
{
|
||||||
rl.app.showMessageComposer(params);
|
rl.app.showMessageComposer(params);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
initFullscreen = (el, fn) =>
|
||||||
|
{
|
||||||
|
let event = 'fullscreenchange';
|
||||||
|
if (!el.requestFullscreen && el.webkitRequestFullscreen) {
|
||||||
|
el.requestFullscreen = el.webkitRequestFullscreen;
|
||||||
|
event = 'webkit'+event;
|
||||||
|
}
|
||||||
|
if (el.requestFullscreen) {
|
||||||
|
el.addEventListener(event, fn);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
||||||
|
{
|
||||||
|
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
||||||
|
target.removeAttribute('style');
|
||||||
|
source.removeAttribute('style');
|
||||||
|
}
|
||||||
|
// source.classList.toggle('resizable', mode);
|
||||||
|
if (mode) {
|
||||||
|
const length = Local.get(sClientSideKeyName+mode);
|
||||||
|
if (!source.layoutResizer) {
|
||||||
|
const resizer = createElement('div', {'class':'resizer'}),
|
||||||
|
size = {},
|
||||||
|
store = () => {
|
||||||
|
if ('Width' == resizer.mode) {
|
||||||
|
target.style.left = source.offsetWidth + 'px';
|
||||||
|
Local.set(resizer.key+resizer.mode, source.offsetWidth);
|
||||||
|
} else {
|
||||||
|
target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
|
||||||
|
Local.set(resizer.key+resizer.mode, source.offsetHeight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cssint = s => {
|
||||||
|
let value = getComputedStyle(source, null)[s].replace('px', '');
|
||||||
|
if (value.includes('%')) {
|
||||||
|
value = source.parentElement['offset'+resizer.mode]
|
||||||
|
* value.replace('%', '') / 100;
|
||||||
|
}
|
||||||
|
return parseFloat(value);
|
||||||
|
};
|
||||||
|
source.layoutResizer = resizer;
|
||||||
|
source.append(resizer);
|
||||||
|
resizer.addEventListener('mousedown', {
|
||||||
|
handleEvent: function(e) {
|
||||||
|
if ('mousedown' == e.type) {
|
||||||
|
const lmode = resizer.mode.toLowerCase();
|
||||||
|
e.preventDefault();
|
||||||
|
size.pos = ('width' == lmode) ? e.pageX : e.pageY;
|
||||||
|
size.min = cssint('min-'+lmode);
|
||||||
|
size.max = cssint('max-'+lmode);
|
||||||
|
size.org = cssint(lmode);
|
||||||
|
addEventListener('mousemove', this);
|
||||||
|
addEventListener('mouseup', this);
|
||||||
|
} else if ('mousemove' == e.type) {
|
||||||
|
const lmode = resizer.mode.toLowerCase(),
|
||||||
|
length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
|
||||||
|
if (length >= size.min && length <= size.max ) {
|
||||||
|
source.style[lmode] = length + 'px';
|
||||||
|
source.observer || store();
|
||||||
|
}
|
||||||
|
} else if ('mouseup' == e.type) {
|
||||||
|
removeEventListener('mousemove', this);
|
||||||
|
removeEventListener('mouseup', this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
|
||||||
|
}
|
||||||
|
source.layoutResizer.mode = mode;
|
||||||
|
source.layoutResizer.key = sClientSideKeyName;
|
||||||
|
source.observer && source.observer.observe(source, { box: 'border-box' });
|
||||||
|
if (length) {
|
||||||
|
source.style[mode] = length + 'px';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source.observer && source.observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
populateMessageBody = (oMessage, preload) => {
|
||||||
|
if (oMessage) {
|
||||||
|
preload || MessageUserStore.hideMessageBodies();
|
||||||
|
preload || MessageUserStore.loading(true);
|
||||||
|
Remote.message((iError, oData/*, bCached*/) => {
|
||||||
|
if (iError) {
|
||||||
|
if (Notification.RequestAborted !== iError && !preload) {
|
||||||
|
MessageUserStore.message(null);
|
||||||
|
MessageUserStore.error(getNotification(iError));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oMessage = preload ? oMessage : null;
|
||||||
|
let
|
||||||
|
isNew = false,
|
||||||
|
json = oData && oData.Result,
|
||||||
|
message = oMessage || MessageUserStore.message();
|
||||||
|
|
||||||
|
if (
|
||||||
|
json &&
|
||||||
|
MessageModel.validJson(json) &&
|
||||||
|
message &&
|
||||||
|
message.folder === json.Folder
|
||||||
|
) {
|
||||||
|
const threads = message.threads(),
|
||||||
|
messagesDom = MessageUserStore.bodiesDom();
|
||||||
|
if (!oMessage && message.uid != json.Uid && threads.includes(json.Uid)) {
|
||||||
|
message = MessageModel.reviveFromJson(json);
|
||||||
|
if (message) {
|
||||||
|
message.threads(threads);
|
||||||
|
MessageFlagsCache.initMessage(message);
|
||||||
|
|
||||||
|
// Set clone
|
||||||
|
MessageUserStore.message(MessageModel.fromMessageListItem(message));
|
||||||
|
message = MessageUserStore.message();
|
||||||
|
|
||||||
|
isNew = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message && message.uid == json.Uid) {
|
||||||
|
oMessage || MessageUserStore.error('');
|
||||||
|
/*
|
||||||
|
if (bCached) {
|
||||||
|
delete json.Flags;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
isNew || message.revivePropertiesFromJson(json);
|
||||||
|
addRequestedMessage(message.folder, message.uid);
|
||||||
|
if (messagesDom) {
|
||||||
|
let id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''),
|
||||||
|
body = elementById(id);
|
||||||
|
if (body) {
|
||||||
|
message.body = body;
|
||||||
|
message.isHtml(body.classList.contains('html'));
|
||||||
|
message.hasImages(body.rlHasImages);
|
||||||
|
} else {
|
||||||
|
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
||||||
|
+ (message.pgpSigned() ? ' openpgp-signed' : '')
|
||||||
|
+ (message.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||||
|
+ '">'
|
||||||
|
+ '</div>');
|
||||||
|
message.body = body;
|
||||||
|
if (!SettingsUserStore.viewHTML() || !message.viewHtml()) {
|
||||||
|
message.viewPlain();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageUserStore.purgeMessageBodyCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesDom.append(body);
|
||||||
|
|
||||||
|
if (!oMessage) {
|
||||||
|
MessageUserStore.activeDom(message.body);
|
||||||
|
MessageUserStore.hideMessageBodies();
|
||||||
|
message.body.hidden = false;
|
||||||
|
}
|
||||||
|
oMessage && message.viewPopupMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageFlagsCache.initMessage(message);
|
||||||
|
if (message.isUnseen()) {
|
||||||
|
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||||
|
() => MessagelistUserStore.setAction(message.folder, MessageSetAction.SetSeen, [message]),
|
||||||
|
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message && isNew) {
|
||||||
|
let selectedMessage = MessagelistUserStore.selectedMessage();
|
||||||
|
if (
|
||||||
|
selectedMessage &&
|
||||||
|
(message.folder !== selectedMessage.folder || message.uid != selectedMessage.uid)
|
||||||
|
) {
|
||||||
|
MessagelistUserStore.selectedMessage(null);
|
||||||
|
if (1 === MessagelistUserStore.length) {
|
||||||
|
MessagelistUserStore.focusedMessage(null);
|
||||||
|
}
|
||||||
|
} else if (!selectedMessage) {
|
||||||
|
selectedMessage = MessagelistUserStore.find(
|
||||||
|
subMessage =>
|
||||||
|
subMessage &&
|
||||||
|
subMessage.folder === message.folder &&
|
||||||
|
subMessage.uid == message.uid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedMessage) {
|
||||||
|
MessagelistUserStore.selectedMessage(selectedMessage);
|
||||||
|
MessagelistUserStore.focusedMessage(selectedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preload || MessageUserStore.loading(false);
|
||||||
|
}, oMessage.folder, oMessage.uid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
|
|
||||||
export class AbstractComponent {
|
|
||||||
constructor() {
|
|
||||||
this.disposable = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.disposable.forEach((funcToDispose) => {
|
|
||||||
if (funcToDispose && funcToDispose.dispose) {
|
|
||||||
funcToDispose.dispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { AbstractComponent } from 'Component/Abstract';
|
|
||||||
|
|
||||||
class AbstractCheckbox extends AbstractComponent {
|
export class AbstractCheckbox {
|
||||||
/**
|
/**
|
||||||
* @param {Object} params = {}
|
* @param {Object} params = {}
|
||||||
*/
|
*/
|
||||||
constructor(params = {}) {
|
constructor(params = {}) {
|
||||||
super();
|
|
||||||
|
|
||||||
this.value = ko.isObservable(params.value) ? params.value
|
this.value = ko.isObservable(params.value) ? params.value
|
||||||
: ko.observable(!!params.value);
|
: ko.observable(!!params.value);
|
||||||
|
|
||||||
|
|
@ -24,5 +21,3 @@ class AbstractCheckbox extends AbstractComponent {
|
||||||
this.enable() && this.value(!this.value());
|
this.enable() && this.value(!this.value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AbstractCheckbox };
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { pInt } from 'Common/Utils';
|
import { pInt } from 'Common/Utils';
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
import { AbstractComponent } from 'Component/Abstract';
|
import { koComputable } from 'External/ko';
|
||||||
|
import { dispose } from 'External/ko';
|
||||||
|
|
||||||
class AbstractInput extends AbstractComponent {
|
export class AbstractInput {
|
||||||
/**
|
/**
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
*/
|
*/
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
super();
|
|
||||||
|
|
||||||
this.value = params.value || '';
|
this.value = params.value || '';
|
||||||
this.label = params.label || '';
|
this.label = params.label || '';
|
||||||
this.enable = null == params.enable ? true : params.enable;
|
this.enable = null == params.enable ? true : params.enable;
|
||||||
|
|
@ -18,7 +17,7 @@ class AbstractInput extends AbstractComponent {
|
||||||
|
|
||||||
this.labeled = null != params.label;
|
this.labeled = null != params.label;
|
||||||
|
|
||||||
let size = params.size || 0;
|
let size = 0 < params.size ? 'span' + params.size : '';
|
||||||
if (this.trigger) {
|
if (this.trigger) {
|
||||||
const
|
const
|
||||||
classForTrigger = ko.observable(''),
|
classForTrigger = ko.observable(''),
|
||||||
|
|
@ -38,17 +37,21 @@ class AbstractInput extends AbstractComponent {
|
||||||
|
|
||||||
setTriggerState(this.trigger());
|
setTriggerState(this.trigger());
|
||||||
|
|
||||||
this.className = ko.computed(() =>
|
this.className = koComputable(() =>
|
||||||
((0 < size ? 'span' + size : '') + ' settings-saved-trigger-input ' + classForTrigger()).trim()
|
(size + ' settings-saved-trigger-input ' + classForTrigger()).trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
this.disposable.push(this.trigger.subscribe(setTriggerState, this));
|
this.disposables = [
|
||||||
|
this.trigger.subscribe(setTriggerState, this),
|
||||||
|
this.className
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
this.className = ko.computed(() => 0 < size ? 'span' + size : '');
|
this.className = size;
|
||||||
|
this.disposables = [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.disposable.push(this.className);
|
dispose() {
|
||||||
|
this.disposables.forEach(dispose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AbstractInput };
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { doc, createElement } from 'Common/Globals';
|
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||||
import { isArray } from 'Common/Utils';
|
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
|
|
||||||
const contentType = 'snappymail/emailaddress',
|
const contentType = 'snappymail/emailaddress',
|
||||||
|
|
@ -17,7 +16,7 @@ export class EmailAddressesComponent {
|
||||||
doc.body.append(datalist);
|
doc.body.append(datalist);
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this,
|
const self = this,
|
||||||
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
|
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
|
||||||
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
|
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
|
||||||
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
|
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
|
||||||
|
|
@ -42,49 +41,55 @@ export class EmailAddressesComponent {
|
||||||
// Create the elements
|
// Create the elements
|
||||||
self.ul = createElement('ul',{class:"emailaddresses"});
|
self.ul = createElement('ul',{class:"emailaddresses"});
|
||||||
|
|
||||||
self.ul.addEventListener('click', e => self._focus(e));
|
addEventsListeners(self.ul, {
|
||||||
self.ul.addEventListener('dblclick', e => self._editTag(e));
|
click: e => self._focus(e),
|
||||||
self.ul.addEventListener("dragenter", fnDrag);
|
dblclick: e => self._editTag(e),
|
||||||
self.ul.addEventListener("dragover", fnDrag);
|
dragenter: fnDrag,
|
||||||
self.ul.addEventListener("drop", e => {
|
dragover: fnDrag,
|
||||||
if (validDropzone() && dragAddress.value) {
|
drop: e => {
|
||||||
e.preventDefault();
|
if (validDropzone() && dragAddress.value) {
|
||||||
dragAddress.source._removeDraggedTag(dragAddress.li);
|
e.preventDefault();
|
||||||
self._parseValue(dragAddress.value);
|
dragAddress.source._removeDraggedTag(dragAddress.li);
|
||||||
|
self._parseValue(dragAddress.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.input = createElement('input',{type:"text", list:datalist.id,
|
self.input = createElement('input',{type:"text", list:datalist.id,
|
||||||
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
|
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
|
||||||
|
|
||||||
self.input.addEventListener('focus', () => self._focusTrigger(true));
|
addEventsListeners(self.input, {
|
||||||
self.input.addEventListener('blur', () => {
|
focus: () => {
|
||||||
// prevent autoComplete menu click from causing a false 'blur'
|
self._focusTrigger(true);
|
||||||
self._parseInput(true);
|
self.input.value || self._resetDatalist();
|
||||||
self._focusTrigger(false);
|
},
|
||||||
});
|
blur: () => {
|
||||||
self.input.addEventListener('keydown', e => {
|
// prevent autoComplete menu click from causing a false 'blur'
|
||||||
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
|
|
||||||
// if our input contains no value and backspace has been pressed, select the last tag
|
|
||||||
var lastTag = self.inputCont.previousElementSibling,
|
|
||||||
input = self.input;
|
|
||||||
if (lastTag && (!input.value
|
|
||||||
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
lastTag.querySelector('a').focus();
|
|
||||||
}
|
|
||||||
self._updateDatalist();
|
|
||||||
} else if (e.key == 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
self._parseInput(true);
|
self._parseInput(true);
|
||||||
|
self._focusTrigger(false);
|
||||||
|
},
|
||||||
|
keydown: e => {
|
||||||
|
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
|
||||||
|
// if our input contains no value and backspace has been pressed, select the last tag
|
||||||
|
var lastTag = self.inputCont.previousElementSibling,
|
||||||
|
input = self.input;
|
||||||
|
if (lastTag && (!input.value
|
||||||
|
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
lastTag.querySelector('a').focus();
|
||||||
|
}
|
||||||
|
self._updateDatalist();
|
||||||
|
} else if (e.key == 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
self._parseInput(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: () => {
|
||||||
|
self._parseInput();
|
||||||
|
self._updateDatalist();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.input.addEventListener('input', () => {
|
|
||||||
self._parseInput();
|
|
||||||
self._updateDatalist();
|
|
||||||
});
|
|
||||||
self.input.addEventListener('focus', () => self.input.value || self._resetDatalist());
|
|
||||||
|
|
||||||
// define starting placeholder
|
// define starting placeholder
|
||||||
if (element.placeholder) {
|
if (element.placeholder) {
|
||||||
|
|
@ -138,20 +143,56 @@ export class EmailAddressesComponent {
|
||||||
|
|
||||||
_parseValue(val) {
|
_parseValue(val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
var self = this,
|
const self = this,
|
||||||
values = [];
|
v = val.trim(),
|
||||||
|
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
|
||||||
const v = val.trim(),
|
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
||||||
hook = (v && [',', ';', '\n'].includes(v.substr(-1)))
|
.flat(Infinity)
|
||||||
? EmailModel.splitEmailLine(val)
|
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
|
||||||
: null;
|
|
||||||
|
|
||||||
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
|
||||||
.flat(Infinity)
|
|
||||||
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
|
|
||||||
|
|
||||||
if (values.length) {
|
if (values.length) {
|
||||||
self._setChosen(values);
|
values.forEach(a => {
|
||||||
|
var v = a[0].trim(),
|
||||||
|
exists = false,
|
||||||
|
lastIndex = -1,
|
||||||
|
obj = {
|
||||||
|
key : '',
|
||||||
|
obj : null,
|
||||||
|
value : ''
|
||||||
|
};
|
||||||
|
|
||||||
|
self._chosenValues.forEach((vv, kk) => {
|
||||||
|
if (vv.value === self._lastEdit) {
|
||||||
|
lastIndex = kk;
|
||||||
|
}
|
||||||
|
|
||||||
|
vv.value === v && (exists = true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (v !== '' && a[1] && !exists) {
|
||||||
|
|
||||||
|
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
|
||||||
|
obj.value = v;
|
||||||
|
obj.obj = a[1];
|
||||||
|
|
||||||
|
if (-1 < lastIndex) {
|
||||||
|
self._chosenValues.splice(lastIndex, 0, obj);
|
||||||
|
} else {
|
||||||
|
self._chosenValues.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
self._lastEdit = '';
|
||||||
|
self._renderTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (values.length === 1 && values[0] === '' && self._lastEdit !== '') {
|
||||||
|
self._lastEdit = '';
|
||||||
|
self._renderTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
self._setValue(self._buildValue());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,56 +243,6 @@ export class EmailAddressesComponent {
|
||||||
self._resizeInput(ev);
|
self._resizeInput(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setChosen(valArr) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (!isArray(valArr)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
valArr.forEach(a => {
|
|
||||||
var v = a[0].trim(),
|
|
||||||
exists = false,
|
|
||||||
lastIndex = -1,
|
|
||||||
obj = {
|
|
||||||
key : '',
|
|
||||||
obj : null,
|
|
||||||
value : ''
|
|
||||||
};
|
|
||||||
|
|
||||||
self._chosenValues.forEach((vv, kk) => {
|
|
||||||
if (vv.value === self._lastEdit) {
|
|
||||||
lastIndex = kk;
|
|
||||||
}
|
|
||||||
|
|
||||||
vv.value === v && (exists = true);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (v !== '' && a && a[1] && !exists) {
|
|
||||||
|
|
||||||
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
|
|
||||||
obj.value = v;
|
|
||||||
obj.obj = a[1];
|
|
||||||
|
|
||||||
if (-1 < lastIndex) {
|
|
||||||
self._chosenValues.splice(lastIndex, 0, obj);
|
|
||||||
} else {
|
|
||||||
self._chosenValues.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
self._lastEdit = '';
|
|
||||||
self._renderTags();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (valArr.length === 1 && valArr[0] === '' && self._lastEdit !== '') {
|
|
||||||
self._lastEdit = '';
|
|
||||||
self._renderTags();
|
|
||||||
}
|
|
||||||
|
|
||||||
self._setValue(self._buildValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildValue() {
|
_buildValue() {
|
||||||
return this._chosenValues.map(v => v.value).join(',');
|
return this._chosenValues.map(v => v.value).join(',');
|
||||||
}
|
}
|
||||||
|
|
@ -276,66 +267,70 @@ export class EmailAddressesComponent {
|
||||||
|
|
||||||
el = createElement('a',{href:'#', class:'ficon'});
|
el = createElement('a',{href:'#', class:'ficon'});
|
||||||
el.append('✖');
|
el.append('✖');
|
||||||
el.addEventListener('click', e => self._removeTag(e, li));
|
addEventsListeners(el, {
|
||||||
el.addEventListener('focus', () => li.className = 'emailaddresses-selected');
|
click: e => self._removeTag(e, li),
|
||||||
el.addEventListener('blur', () => li.className = null);
|
focus: () => li.className = 'emailaddresses-selected',
|
||||||
el.addEventListener('keydown', e => {
|
blur: () => li.className = null,
|
||||||
switch (e.key) {
|
keydown: e => {
|
||||||
case 'Delete':
|
switch (e.key) {
|
||||||
case 'Backspace':
|
case 'Delete':
|
||||||
self._removeTag(e, li);
|
case 'Backspace':
|
||||||
break;
|
self._removeTag(e, li);
|
||||||
|
break;
|
||||||
|
|
||||||
// 'e' - edit tag (removes tag and places value into visible input
|
// 'e' - edit tag (removes tag and places value into visible input
|
||||||
case 'e':
|
case 'e':
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
self._editTag(e);
|
self._editTag(e);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
// select the previous tag or input if no more tags exist
|
// select the previous tag or input if no more tags exist
|
||||||
var previous = el.closest('li').previousElementSibling;
|
var previous = el.closest('li').previousElementSibling;
|
||||||
if (previous.matches('li')) {
|
if (previous.matches('li')) {
|
||||||
previous.querySelector('a').focus();
|
previous.querySelector('a').focus();
|
||||||
} else {
|
} else {
|
||||||
self.focus();
|
self.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
// select the next tag or input if no more tags exist
|
// select the next tag or input if no more tags exist
|
||||||
var next = el.closest('li').nextElementSibling;
|
var next = el.closest('li').nextElementSibling;
|
||||||
if (next !== this.inputCont) {
|
if (next !== this.inputCont) {
|
||||||
next.querySelector('a').focus();
|
next.querySelector('a').focus();
|
||||||
} else {
|
} else {
|
||||||
this.focus();
|
this.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
self._focus(e);
|
self._focus(e);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
li.append(el);
|
li.append(el);
|
||||||
|
|
||||||
li.emailaddress = v;
|
li.emailaddress = v;
|
||||||
|
|
||||||
li.addEventListener("dragstart", e => {
|
addEventsListeners(li, {
|
||||||
dragAddress = {
|
dragstart: e => {
|
||||||
source: self,
|
dragAddress = {
|
||||||
li: li,
|
source: self,
|
||||||
value: li.emailaddress.obj.toLine()
|
li: li,
|
||||||
};
|
value: li.emailaddress.obj.toLine()
|
||||||
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
|
};
|
||||||
e.dataTransfer.setData('text/plain', contentType);
|
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
|
||||||
// e.dataTransfer.setDragImage(li, 0, 0);
|
e.dataTransfer.setData('text/plain', contentType);
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
// e.dataTransfer.setDragImage(li, 0, 0);
|
||||||
li.style.opacity = 0.25;
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
});
|
li.style.opacity = 0.25;
|
||||||
li.addEventListener("dragend", () => {
|
},
|
||||||
dragAddress = null;
|
dragend: () => {
|
||||||
li.style.cssText = '';
|
dragAddress = null;
|
||||||
|
li.style.cssText = '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.inputCont.before(li);
|
self.inputCont.before(li);
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,3 @@
|
||||||
import ko from 'ko';
|
|
||||||
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
|
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
|
||||||
|
|
||||||
export class CheckboxMaterialDesignComponent extends AbstractCheckbox {
|
export class CheckboxMaterialDesignComponent extends AbstractCheckbox {}
|
||||||
/**
|
|
||||||
* @param {Object} params
|
|
||||||
*/
|
|
||||||
constructor(params) {
|
|
||||||
super(params);
|
|
||||||
|
|
||||||
this.animationBox = ko.observable(false).extend({ falseTimeout: 200 });
|
|
||||||
this.animationCheckmark = ko.observable(false).extend({ falseTimeout: 200 });
|
|
||||||
|
|
||||||
this.disposable.push(
|
|
||||||
this.value.subscribe(value => this.triggerAnimation(value), this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerAnimation(box) {
|
|
||||||
if (box) {
|
|
||||||
this.animationBox(true);
|
|
||||||
setTimeout(()=>this.animationCheckmark(true), 200);
|
|
||||||
} else {
|
|
||||||
this.animationCheckmark(true);
|
|
||||||
setTimeout(()=>this.animationBox(true), 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
10
dev/External/Admin/ko.js
vendored
10
dev/External/Admin/ko.js
vendored
|
|
@ -1,10 +0,0 @@
|
||||||
import 'External/ko';
|
|
||||||
import ko from 'ko';
|
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
|
||||||
|
|
||||||
// functions
|
|
||||||
|
|
||||||
ko.observable.fn.idleTrigger = function() {
|
|
||||||
this.trigger = ko.observable(SaveSettingsStep.Idle);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
155
dev/External/SquireUI.js
vendored
155
dev/External/SquireUI.js
vendored
|
|
@ -11,11 +11,14 @@ const
|
||||||
|
|
||||||
ctrlKey = shortcuts.getMetaKey() + ' + ',
|
ctrlKey = shortcuts.getMetaKey() + ' + ',
|
||||||
|
|
||||||
tpl = doc.createElement('template'),
|
createElement = name => doc.createElement(name),
|
||||||
clr = doc.createElement('input'),
|
|
||||||
|
tpl = createElement('template'),
|
||||||
|
clr = createElement('input'),
|
||||||
|
|
||||||
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
|
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
|
||||||
clearHtmlLine = html => rl.Utils.htmlToPlain(html).trim(),
|
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
|
||||||
|
plainToHtml = text => rl.Utils.plainToHtml(text),
|
||||||
|
|
||||||
getFragmentOfChildren = parent => {
|
getFragmentOfChildren = parent => {
|
||||||
let frag = doc.createDocumentFragment();
|
let frag = doc.createDocumentFragment();
|
||||||
|
|
@ -33,13 +36,12 @@ const
|
||||||
addLinks: true // allow_smart_html_links
|
addLinks: true // allow_smart_html_links
|
||||||
*/
|
*/
|
||||||
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
||||||
tpl.innerHTML = html
|
tpl.innerHTML = (html||'')
|
||||||
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
|
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
|
||||||
.replace(/<!--[^>]+-->/g,'')
|
.replace(/<!--[^>]+-->/g,'')
|
||||||
.replace(/<span[^>]*>\s*<\/span>/gi,'')
|
.replace(/<span[^>]*>\s*<\/span>/gi,'')
|
||||||
.trim();
|
.trim();
|
||||||
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
||||||
tpl.querySelectorAll('[data-x-div-type]').forEach(el => el.replaceWith(getFragmentOfChildren(el)));
|
|
||||||
if (isPaste) {
|
if (isPaste) {
|
||||||
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
|
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
|
||||||
tpl.querySelectorAll('*').forEach(el => {
|
tpl.querySelectorAll('*').forEach(el => {
|
||||||
|
|
@ -57,66 +59,6 @@ const
|
||||||
}
|
}
|
||||||
return tpl.content;
|
return tpl.content;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
rl_signature_replacer = (editor, text, signature, isHtml, insertBefore) => {
|
|
||||||
let
|
|
||||||
prevSignature = editor.__previous_signature,
|
|
||||||
skipInsert = false,
|
|
||||||
isEmptyText = false;
|
|
||||||
|
|
||||||
isEmptyText = !text.trim();
|
|
||||||
if (!isEmptyText && isHtml) {
|
|
||||||
isEmptyText = !clearHtmlLine(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevSignature && !isEmptyText) {
|
|
||||||
if (isHtml && !prevSignature.isHtml) {
|
|
||||||
prevSignature = {
|
|
||||||
body: rl.Utils.plainToHtml(prevSignature.body),
|
|
||||||
isHtml: true
|
|
||||||
};
|
|
||||||
} else if (!isHtml && prevSignature.isHtml) {
|
|
||||||
prevSignature = {
|
|
||||||
body: rl.Utils.htmlToPlain(prevSignature.body),
|
|
||||||
isHtml: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHtml) {
|
|
||||||
var clearSig = clearHtmlLine(prevSignature.body);
|
|
||||||
text = text.replace(/<signature>([\s\S]*)<\/signature>/igm, all => {
|
|
||||||
var c = clearSig === clearHtmlLine(all);
|
|
||||||
if (!c) {
|
|
||||||
skipInsert = true;
|
|
||||||
}
|
|
||||||
return c ? '' : all;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var textLen = text.length;
|
|
||||||
text = text
|
|
||||||
.replace(prevSignature.body, '')
|
|
||||||
.replace(prevSignature.body, '');
|
|
||||||
skipInsert = textLen === text.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipInsert) {
|
|
||||||
signature = (isHtml ? '<br/><br/><signature>' : "\n\n") + signature + (isHtml ? '</signature>' : '');
|
|
||||||
|
|
||||||
text = insertBefore ? signature + text : text + signature;
|
|
||||||
|
|
||||||
if (10 < signature.length) {
|
|
||||||
prevSignature = {
|
|
||||||
body: signature,
|
|
||||||
isHtml: isHtml
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.__previous_signature = prevSignature;
|
|
||||||
|
|
||||||
return text;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
clr.type = "color";
|
clr.type = "color";
|
||||||
|
|
@ -127,9 +69,9 @@ class SquireUI
|
||||||
{
|
{
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
const
|
const
|
||||||
doClr = fn => () => {
|
doClr = name => () => {
|
||||||
clr.value = '';
|
clr.value = '';
|
||||||
clr.onchange = () => squire[fn](clr.value);
|
clr.onchange = () => squire.setStyle({[name]:clr.value});
|
||||||
clr.click();
|
clr.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -171,29 +113,15 @@ class SquireUI
|
||||||
colors: {
|
colors: {
|
||||||
textColor: {
|
textColor: {
|
||||||
html: 'A<sub>▾</sub>',
|
html: 'A<sub>▾</sub>',
|
||||||
cmd: doClr('setTextColor'),
|
cmd: doClr('color'),
|
||||||
hint: 'Text color'
|
hint: 'Text color'
|
||||||
},
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
html: '🎨', /* ▧ */
|
html: '🎨', /* ▧ */
|
||||||
cmd: doClr('setBackgroundColor'),
|
cmd: doClr('backgroundColor'),
|
||||||
hint: 'Background color'
|
hint: 'Background color'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/*
|
|
||||||
bidi: {
|
|
||||||
bdoLtr: {
|
|
||||||
html: '‎𝐁',
|
|
||||||
cmd: () => this.doAction('bold','B'),
|
|
||||||
hint: 'Bold'
|
|
||||||
},
|
|
||||||
bdoRtl: {
|
|
||||||
html: '‏𝐁',
|
|
||||||
cmd: () => this.doAction('bold','B'),
|
|
||||||
hint: 'Bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
inline: {
|
inline: {
|
||||||
bold: {
|
bold: {
|
||||||
html: 'B',
|
html: 'B',
|
||||||
|
|
@ -318,10 +246,10 @@ class SquireUI
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
plain = doc.createElement('textarea'),
|
plain = createElement('textarea'),
|
||||||
wysiwyg = doc.createElement('div'),
|
wysiwyg = createElement('div'),
|
||||||
toolbar = doc.createElement('div'),
|
toolbar = createElement('div'),
|
||||||
browseImage = doc.createElement('input'),
|
browseImage = createElement('input'),
|
||||||
squire = new Squire(wysiwyg, SquireDefaultConfig);
|
squire = new Squire(wysiwyg, SquireDefaultConfig);
|
||||||
|
|
||||||
browseImage.type = 'file';
|
browseImage.type = 'file';
|
||||||
|
|
@ -356,17 +284,17 @@ class SquireUI
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let toolgroup = doc.createElement('div');
|
let toolgroup = createElement('div');
|
||||||
toolgroup.className = 'btn-group';
|
toolgroup.className = 'btn-group';
|
||||||
toolgroup.id = 'squire-toolgroup-'+group;
|
toolgroup.id = 'squire-toolgroup-'+group;
|
||||||
for (action in actions[group]) {
|
for (action in actions[group]) {
|
||||||
let cfg = actions[group][action], input, ev = 'click';
|
let cfg = actions[group][action], input, ev = 'click';
|
||||||
if (cfg.input) {
|
if (cfg.input) {
|
||||||
input = doc.createElement('input');
|
input = createElement('input');
|
||||||
input.type = cfg.input;
|
input.type = cfg.input;
|
||||||
ev = 'change';
|
ev = 'change';
|
||||||
} else if (cfg.select) {
|
} else if (cfg.select) {
|
||||||
input = doc.createElement('select');
|
input = createElement('select');
|
||||||
input.className = 'btn';
|
input.className = 'btn';
|
||||||
if (Array.isArray(cfg.select)) {
|
if (Array.isArray(cfg.select)) {
|
||||||
cfg.select.forEach(value => {
|
cfg.select.forEach(value => {
|
||||||
|
|
@ -376,7 +304,7 @@ class SquireUI
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Object.entries(cfg.select).forEach(([label, options]) => {
|
Object.entries(cfg.select).forEach(([label, options]) => {
|
||||||
let group = doc.createElement('optgroup');
|
let group = createElement('optgroup');
|
||||||
group.label = label;
|
group.label = label;
|
||||||
Object.entries(options).forEach(([text, value]) => {
|
Object.entries(options).forEach(([text, value]) => {
|
||||||
var option = new Option(text, value);
|
var option = new Option(text, value);
|
||||||
|
|
@ -388,7 +316,7 @@ class SquireUI
|
||||||
}
|
}
|
||||||
ev = 'input';
|
ev = 'input';
|
||||||
} else {
|
} else {
|
||||||
input = doc.createElement('button');
|
input = createElement('button');
|
||||||
input.type = 'button';
|
input.type = 'button';
|
||||||
input.className = 'btn';
|
input.className = 'btn';
|
||||||
input.innerHTML = cfg.html;
|
input.innerHTML = cfg.html;
|
||||||
|
|
@ -425,8 +353,8 @@ class SquireUI
|
||||||
changes.redo.input.disabled = !state.canRedo;
|
changes.redo.input.disabled = !state.canRedo;
|
||||||
});
|
});
|
||||||
|
|
||||||
squire.addEventListener('focus', () => shortcuts.off());
|
// squire.addEventListener('focus', () => shortcuts.off());
|
||||||
squire.addEventListener('blur', () => shortcuts.on());
|
// squire.addEventListener('blur', () => shortcuts.on());
|
||||||
|
|
||||||
container.append(toolbar, wysiwyg, plain);
|
container.append(toolbar, wysiwyg, plain);
|
||||||
|
|
||||||
|
|
@ -477,9 +405,9 @@ class SquireUI
|
||||||
let cl = this.container.classList;
|
let cl = this.container.classList;
|
||||||
cl.remove('squire-mode-'+this.mode);
|
cl.remove('squire-mode-'+this.mode);
|
||||||
if ('plain' == mode) {
|
if ('plain' == mode) {
|
||||||
this.plain.value = rl.Utils.htmlToPlain(this.squire.getHTML(), true).trim();
|
this.plain.value = htmlToPlain(this.squire.getHTML(), true);
|
||||||
} else {
|
} else {
|
||||||
this.setData(rl.Utils.plainToHtml(this.plain.value, true));
|
this.setData(plainToHtml(this.plain.value, true));
|
||||||
mode = 'wysiwyg';
|
mode = 'wysiwyg';
|
||||||
}
|
}
|
||||||
this.mode = mode; // 'wysiwyg' or 'plain'
|
this.mode = mode; // 'wysiwyg' or 'plain'
|
||||||
|
|
@ -509,19 +437,32 @@ class SquireUI
|
||||||
}, cfg);
|
}, cfg);
|
||||||
|
|
||||||
if (cfg.clearCache) {
|
if (cfg.clearCache) {
|
||||||
this.__previous_signature = null;
|
this._prev_txt_sig = null;
|
||||||
} else try {
|
} else try {
|
||||||
|
const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature;
|
||||||
if ('plain' === this.mode) {
|
if ('plain' === this.mode) {
|
||||||
if (cfg.isHtml) {
|
let
|
||||||
cfg.signature = rl.Utils.htmlToPlain(cfg.signature);
|
text = this.plain.value,
|
||||||
|
prevSignature = this._prev_txt_sig;
|
||||||
|
if (prevSignature) {
|
||||||
|
text = text.replace(prevSignature, '').trim();
|
||||||
}
|
}
|
||||||
this.plain.value = rl_signature_replacer(this, this.plain.value, cfg.signature, false, cfg.insertBefore);
|
this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature;
|
||||||
} else {
|
} else {
|
||||||
if (!cfg.isHtml) {
|
const squire = this.squire,
|
||||||
cfg.signature = rl.Utils.plainToHtml(cfg.signature);
|
root = squire.getRoot(),
|
||||||
}
|
range = squire.getSelection(),
|
||||||
this.setData(rl_signature_replacer(this, this.getData(), cfg.signature, true, cfg.insertBefore));
|
div = createElement('div');
|
||||||
|
div.className = 'rl-signature';
|
||||||
|
div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature);
|
||||||
|
root.querySelectorAll('div.rl-signature').forEach(node => node.remove());
|
||||||
|
cfg.insertBefore ? root.prepend(div) : root.append(div);
|
||||||
|
// Move cursor above signature
|
||||||
|
range.setStart(div, 0);
|
||||||
|
range.setEnd(div, 0);
|
||||||
|
squire.setSelection( range );
|
||||||
}
|
}
|
||||||
|
this._prev_txt_sig = signature;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
@ -540,12 +481,6 @@ class SquireUI
|
||||||
focus() {
|
focus() {
|
||||||
('plain' == this.mode ? this.plain : this.squire).focus();
|
('plain' == this.mode ? this.plain : this.squire).focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(width, height) {
|
|
||||||
height = Math.max(200, (height - this.wysiwyg.offsetTop)) + 'px';
|
|
||||||
this.wysiwyg.style.height = height;
|
|
||||||
this.plain.style.height = height;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.SquireUI = SquireUI;
|
this.SquireUI = SquireUI;
|
||||||
|
|
|
||||||
384
dev/External/User/ko.js
vendored
384
dev/External/User/ko.js
vendored
|
|
@ -1,11 +1,14 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { HtmlEditor } from 'Common/Html';
|
import { HtmlEditor } from 'Common/Html';
|
||||||
import { timeToNode } from 'Common/Momentor';
|
import { timeToNode } from 'Common/Translator';
|
||||||
import { elementById } from 'Common/Globals';
|
import { elementById, addEventsListeners, dropdowns } from 'Common/Globals';
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
|
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
||||||
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
import { moveMessagesToFolder } from 'Common/Folders';
|
||||||
|
import { setExpandedFolder } from 'Model/FolderCollection';
|
||||||
|
|
||||||
const rlContentType = 'snappymail/action',
|
const rlContentType = 'snappymail/action',
|
||||||
|
|
||||||
|
|
@ -27,226 +30,223 @@ const rlContentType = 'snappymail/action',
|
||||||
id: 0,
|
id: 0,
|
||||||
stop: () => clearTimeout(dragTimer.id),
|
stop: () => clearTimeout(dragTimer.id),
|
||||||
start: fn => dragTimer.id = setTimeout(fn, 500)
|
start: fn => dragTimer.id = setTimeout(fn, 500)
|
||||||
};
|
},
|
||||||
|
|
||||||
|
ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
|
||||||
|
|
||||||
let dragImage,
|
let dragImage,
|
||||||
dragData;
|
dragData;
|
||||||
|
|
||||||
ko.bindingHandlers.editor = {
|
Object.assign(ko.bindingHandlers, {
|
||||||
init: (element, fValueAccessor) => {
|
|
||||||
let editor = null;
|
|
||||||
|
|
||||||
const fValue = fValueAccessor(),
|
editor: {
|
||||||
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
|
init: (element, fValueAccessor) => {
|
||||||
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
let editor = null;
|
||||||
fOnReady = () => {
|
|
||||||
fValue.__editor = editor;
|
|
||||||
fUpdateEditorValue();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ko.isObservable(fValue) && HtmlEditor) {
|
const fValue = fValueAccessor(),
|
||||||
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
|
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
|
||||||
|
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
||||||
|
fOnReady = () => {
|
||||||
|
fValue.__editor = editor;
|
||||||
|
fUpdateEditorValue();
|
||||||
|
};
|
||||||
|
|
||||||
fValue.__fetchEditorValue = fUpdateKoValue;
|
if (ko.isObservable(fValue) && HtmlEditor) {
|
||||||
|
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
|
||||||
|
|
||||||
fValue.subscribe(fUpdateEditorValue);
|
fValue.__fetchEditorValue = fUpdateKoValue;
|
||||||
|
|
||||||
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
|
fValue.subscribe(fUpdateEditorValue);
|
||||||
// });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
|
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
|
||||||
ko.bindingHandlers.moment = {
|
// });
|
||||||
init: ttn,
|
}
|
||||||
update: ttn
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.emailsTags = {
|
|
||||||
init: (element, fValueAccessor, fAllBindings) => {
|
|
||||||
const fValue = fValueAccessor();
|
|
||||||
|
|
||||||
element.addresses = new EmailAddressesComponent(element, {
|
|
||||||
focusCallback: value => fValue.focused && fValue.focused(!!value),
|
|
||||||
autoCompleteSource: fAllBindings.get('autoCompleteSource'),
|
|
||||||
onChange: value => fValue(value)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fValue.focused && fValue.focused.subscribe) {
|
|
||||||
fValue.focused.subscribe(value =>
|
|
||||||
element.addresses[value ? 'focus' : 'blur']()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
element.addresses.value = ko.unwrap(fValueAccessor());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start dragging selected messages
|
moment: {
|
||||||
ko.bindingHandlers.dragmessages = {
|
init: ttn,
|
||||||
init: (element, fValueAccessor) => {
|
update: ttn
|
||||||
element.addEventListener("dragstart", e => {
|
},
|
||||||
let data = fValueAccessor()(e);
|
|
||||||
dragImage || (dragImage = elementById('messagesDragImage'));
|
|
||||||
if (data && dragImage && !ThemeStore.isMobile()) {
|
|
||||||
dragImage.querySelector('.text').textContent = data.uids.length;
|
|
||||||
let img = dragImage.querySelector('i');
|
|
||||||
img.classList.toggle('icon-copy', e.ctrlKey);
|
|
||||||
img.classList.toggle('icon-mail', !e.ctrlKey);
|
|
||||||
|
|
||||||
// Else Chrome doesn't show it
|
emailsTags: {
|
||||||
dragImage.style.left = e.clientX + 'px';
|
init: (element, fValueAccessor, fAllBindings) => {
|
||||||
dragImage.style.top = e.clientY + 'px';
|
const fValue = fValueAccessor();
|
||||||
dragImage.style.right = 'auto';
|
|
||||||
|
|
||||||
setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
|
element.addresses = new EmailAddressesComponent(element, {
|
||||||
|
focusCallback: value => fValue.focused && fValue.focused(!!value),
|
||||||
|
autoCompleteSource: fAllBindings.get('autoCompleteSource'),
|
||||||
|
onChange: value => fValue(value)
|
||||||
|
});
|
||||||
|
|
||||||
// Remove the Chrome visibility
|
if (fValue.focused && fValue.focused.subscribe) {
|
||||||
dragImage.style.cssText = '';
|
fValue.focused.subscribe(value =>
|
||||||
} else {
|
element.addresses[value ? 'focus' : 'blur']()
|
||||||
e.preventDefault();
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
update: (element, fValueAccessor) => {
|
||||||
|
element.addresses.value = ko.unwrap(fValueAccessor());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
}, false);
|
// Start dragging selected messages
|
||||||
element.addEventListener("dragend", () => dragData = null);
|
dragmessages: {
|
||||||
element.setAttribute('draggable', true);
|
init: (element, fValueAccessor) => {
|
||||||
}
|
element.addEventListener("dragstart", e => {
|
||||||
};
|
let data = fValueAccessor()(e);
|
||||||
|
dragImage || (dragImage = elementById('messagesDragImage'));
|
||||||
|
if (data && dragImage && !ThemeStore.isMobile()) {
|
||||||
|
dragImage.querySelector('.text').textContent = data.uids.length;
|
||||||
|
let img = dragImage.querySelector('i');
|
||||||
|
img.classList.toggle('icon-copy', e.ctrlKey);
|
||||||
|
img.classList.toggle('icon-mail', !e.ctrlKey);
|
||||||
|
|
||||||
// Drop selected messages on folder
|
// Else Chrome doesn't show it
|
||||||
ko.bindingHandlers.dropmessages = {
|
dragImage.style.left = e.clientX + 'px';
|
||||||
init: (element, fValueAccessor) => {
|
dragImage.style.top = e.clientY + 'px';
|
||||||
const folder = fValueAccessor(),
|
dragImage.style.right = 'auto';
|
||||||
// folder = ko.dataFor(element),
|
|
||||||
fnStop = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
element.classList.remove('droppableHover');
|
|
||||||
dragTimer.stop();
|
|
||||||
},
|
|
||||||
fnHover = e => {
|
|
||||||
if ('messages' === getDragAction(e)) {
|
|
||||||
fnStop(e);
|
|
||||||
element.classList.add('droppableHover');
|
|
||||||
if (folder && folder.collapsed()) {
|
|
||||||
dragTimer.start(() => {
|
|
||||||
folder.collapsed(false);
|
|
||||||
rl.app.setExpandedFolder(folder.fullNameHash, true);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
element.addEventListener("dragenter", fnHover);
|
|
||||||
element.addEventListener("dragover", fnHover);
|
|
||||||
element.addEventListener("dragleave", fnStop);
|
|
||||||
element.addEventListener("drop", e => {
|
|
||||||
fnStop(e);
|
|
||||||
if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
|
|
||||||
let data = dragData.data;
|
|
||||||
if (folder && data && data.folder && isArray(data.uids)) {
|
|
||||||
rl.app.moveMessagesToFolder(data.folder, data.uids, folder.fullNameRaw, data.copy && e.ctrlKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.sortableItem = {
|
setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
|
||||||
init: (element, fValueAccessor) => {
|
|
||||||
let options = ko.unwrap(fValueAccessor()) || {},
|
// Remove the Chrome visibility
|
||||||
parent = element.parentNode,
|
dragImage.style.cssText = '';
|
||||||
fnHover = e => {
|
} else {
|
||||||
if ('sortable' === getDragAction(e)) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
|
}
|
||||||
if (node && node !== dragData.data && parent.contains(node)) {
|
|
||||||
let rect = node.getBoundingClientRect();
|
}, false);
|
||||||
if (rect.top + (rect.height / 2) <= e.clientY) {
|
element.addEventListener("dragend", () => dragData = null);
|
||||||
if (node.nextElementSibling !== dragData.data) {
|
element.setAttribute('draggable', true);
|
||||||
node.after(dragData.data);
|
}
|
||||||
}
|
},
|
||||||
} else if (node.previousElementSibling !== dragData.data) {
|
|
||||||
node.before(dragData.data);
|
// Drop selected messages on folder
|
||||||
|
dropmessages: {
|
||||||
|
init: (element, fValueAccessor) => {
|
||||||
|
const folder = fValueAccessor(),
|
||||||
|
// folder = ko.dataFor(element),
|
||||||
|
fnStop = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
element.classList.remove('droppableHover');
|
||||||
|
dragTimer.stop();
|
||||||
|
},
|
||||||
|
fnHover = e => {
|
||||||
|
if ('messages' === getDragAction(e)) {
|
||||||
|
fnStop(e);
|
||||||
|
element.classList.add('droppableHover');
|
||||||
|
if (folder && folder.collapsed()) {
|
||||||
|
dragTimer.start(() => {
|
||||||
|
folder.collapsed(false);
|
||||||
|
setExpandedFolder(folder.fullName, true);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addEventsListeners(element, {
|
||||||
|
dragenter: fnHover,
|
||||||
|
dragover: fnHover,
|
||||||
|
dragleave: fnStop,
|
||||||
|
drop: e => {
|
||||||
|
fnStop(e);
|
||||||
|
if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
|
||||||
|
let data = dragData.data;
|
||||||
|
if (folder && data && data.folder && isArray(data.uids)) {
|
||||||
|
moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
element.addEventListener("dragstart", e => {
|
|
||||||
dragData = {
|
|
||||||
action: 'sortable',
|
|
||||||
element: element
|
|
||||||
};
|
|
||||||
setDragAction(e, 'sortable', 'move', element, element);
|
|
||||||
element.style.opacity = 0.25;
|
|
||||||
});
|
|
||||||
element.addEventListener("dragend", e => {
|
|
||||||
element.style.opacity = null;
|
|
||||||
if ('sortable' === getDragAction(e)) {
|
|
||||||
dragData.data.style.cssText = '';
|
|
||||||
let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
|
|
||||||
if (row != dragData.data) {
|
|
||||||
row.before(dragData.data);
|
|
||||||
}
|
|
||||||
dragData = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!parent.sortable) {
|
|
||||||
parent.sortable = true;
|
|
||||||
parent.addEventListener("dragenter", fnHover);
|
|
||||||
parent.addEventListener("dragover", fnHover);
|
|
||||||
parent.addEventListener("drop", e => {
|
|
||||||
if ('sortable' === getDragAction(e)) {
|
|
||||||
e.preventDefault();
|
|
||||||
let data = ko.dataFor(dragData.data),
|
|
||||||
from = options.list.indexOf(data),
|
|
||||||
to = [...parent.children].indexOf(dragData.data);
|
|
||||||
if (from != to) {
|
|
||||||
let arr = options.list();
|
|
||||||
arr.splice(to, 0, ...arr.splice(from, 1));
|
|
||||||
options.list(arr);
|
|
||||||
}
|
|
||||||
dragData = null;
|
|
||||||
options.afterMove && options.afterMove();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.initDom = {
|
sortableItem: {
|
||||||
init: (element, fValueAccessor) => fValueAccessor()(element)
|
init: (element, fValueAccessor) => {
|
||||||
};
|
let options = ko.unwrap(fValueAccessor()) || {},
|
||||||
|
parent = element.parentNode,
|
||||||
ko.bindingHandlers.onEsc = {
|
fnHover = e => {
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
if ('sortable' === getDragAction(e)) {
|
||||||
let fn = event => {
|
e.preventDefault();
|
||||||
if ('Escape' == event.key) {
|
let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
|
||||||
element.dispatchEvent(new Event('change'));
|
if (node && node !== dragData.data && parent.contains(node)) {
|
||||||
fValueAccessor().call(viewModel);
|
let rect = node.getBoundingClientRect();
|
||||||
|
if (rect.top + (rect.height / 2) <= e.clientY) {
|
||||||
|
if (node.nextElementSibling !== dragData.data) {
|
||||||
|
node.after(dragData.data);
|
||||||
|
}
|
||||||
|
} else if (node.previousElementSibling !== dragData.data) {
|
||||||
|
node.before(dragData.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addEventsListeners(element, {
|
||||||
|
dragstart: e => {
|
||||||
|
dragData = {
|
||||||
|
action: 'sortable',
|
||||||
|
element: element
|
||||||
|
};
|
||||||
|
setDragAction(e, 'sortable', 'move', element, element);
|
||||||
|
element.style.opacity = 0.25;
|
||||||
|
},
|
||||||
|
dragend: e => {
|
||||||
|
element.style.opacity = null;
|
||||||
|
if ('sortable' === getDragAction(e)) {
|
||||||
|
dragData.data.style.cssText = '';
|
||||||
|
let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
|
||||||
|
if (row != dragData.data) {
|
||||||
|
row.before(dragData.data);
|
||||||
|
}
|
||||||
|
dragData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!parent.sortable) {
|
||||||
|
parent.sortable = true;
|
||||||
|
addEventsListeners(parent, {
|
||||||
|
dragenter: fnHover,
|
||||||
|
dragover: fnHover,
|
||||||
|
drop: e => {
|
||||||
|
if ('sortable' === getDragAction(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
let data = ko.dataFor(dragData.data),
|
||||||
|
from = options.list.indexOf(data),
|
||||||
|
to = [...parent.children].indexOf(dragData.data);
|
||||||
|
if (from != to) {
|
||||||
|
let arr = options.list();
|
||||||
|
arr.splice(to, 0, ...arr.splice(from, 1));
|
||||||
|
options.list(arr);
|
||||||
|
}
|
||||||
|
dragData = null;
|
||||||
|
options.afterMove && options.afterMove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
element.addEventListener('keyup', fn);
|
},
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.registerBootstrapDropdown = {
|
initDom: {
|
||||||
init: element => {
|
init: (element, fValueAccessor) => fValueAccessor()(element)
|
||||||
rl.Dropdowns.register(element);
|
},
|
||||||
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.openDropdownTrigger = {
|
registerBootstrapDropdown: {
|
||||||
update: (element, fValueAccessor) => {
|
init: element => {
|
||||||
if (ko.unwrap(fValueAccessor())) {
|
dropdowns.push(element);
|
||||||
const el = element.ddBtn;
|
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
|
||||||
el.open || el.toggle();
|
}
|
||||||
// el.focus();
|
},
|
||||||
|
|
||||||
rl.Dropdowns.detectVisibility();
|
openDropdownTrigger: {
|
||||||
fValueAccessor()(false);
|
update: (element, fValueAccessor) => {
|
||||||
|
if (ko.unwrap(fValueAccessor())) {
|
||||||
|
const el = element.ddBtn;
|
||||||
|
el.open || el.toggle();
|
||||||
|
// el.focus();
|
||||||
|
|
||||||
|
dropdownsDetectVisibility();
|
||||||
|
fValueAccessor()(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
|
||||||
280
dev/External/ko.js
vendored
280
dev/External/ko.js
vendored
|
|
@ -1,157 +1,171 @@
|
||||||
|
import ko from 'ko';
|
||||||
import { i18nToNodes } from 'Common/Translator';
|
import { i18nToNodes } from 'Common/Translator';
|
||||||
import { doc, createElement } from 'Common/Globals';
|
import { doc, createElement } from 'Common/Globals';
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
import { arrayLength, isFunction } from 'Common/Utils';
|
import { arrayLength, isFunction, forEachObjectEntry } from 'Common/Utils';
|
||||||
|
|
||||||
const
|
export const
|
||||||
koValue = value => !ko.isObservable(value) && isFunction(value) ? value() : ko.unwrap(value);
|
errorTip = (element, value) => value
|
||||||
|
? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100)
|
||||||
|
: element.removeAttribute('data-rainloopErrorTip'),
|
||||||
|
|
||||||
ko.bindingHandlers.tooltipErrorTip = {
|
/**
|
||||||
init: element => {
|
* The value of the pureComputed observable shouldn’t vary based on the
|
||||||
doc.addEventListener('click', () => element.removeAttribute('data-rainloopErrorTip'));
|
* number of evaluations or other “hidden” information. Its value should be
|
||||||
},
|
* based solely on the values of other observables in the application
|
||||||
update: (element, fValueAccessor) => {
|
*/
|
||||||
const value = koValue(fValueAccessor());
|
koComputable = fn => ko.computed(fn, {'pure':true}),
|
||||||
if (value) {
|
|
||||||
setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100);
|
addObservablesTo = (target, observables) =>
|
||||||
} else {
|
forEachObjectEntry(observables, (key, value) =>
|
||||||
element.removeAttribute('data-rainloopErrorTip');
|
target[key] || (target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value)) ),
|
||||||
|
|
||||||
|
addComputablesTo = (target, computables) =>
|
||||||
|
forEachObjectEntry(computables, (key, fn) => target[key] = koComputable(fn)),
|
||||||
|
|
||||||
|
addSubscribablesTo = (target, subscribables) =>
|
||||||
|
forEachObjectEntry(subscribables, (key, fn) => target[key].subscribe(fn)),
|
||||||
|
|
||||||
|
dispose = disposable => disposable && isFunction(disposable.dispose) && disposable.dispose(),
|
||||||
|
|
||||||
|
// With this we don't need delegateRunOnDestroy
|
||||||
|
koArrayWithDestroy = data => {
|
||||||
|
data = ko.observableArray(data);
|
||||||
|
data.subscribe(changes =>
|
||||||
|
changes.forEach(item =>
|
||||||
|
'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy()
|
||||||
|
)
|
||||||
|
, data, 'arrayChange');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(ko.bindingHandlers, {
|
||||||
|
tooltipErrorTip: {
|
||||||
|
init: (element, fValueAccessor) => {
|
||||||
|
doc.addEventListener('click', () => {
|
||||||
|
let value = fValueAccessor();
|
||||||
|
ko.isObservable(value) && !ko.isComputed(value) && value('');
|
||||||
|
errorTip(element);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
update: (element, fValueAccessor) => {
|
||||||
|
let value = ko.unwrap(fValueAccessor());
|
||||||
|
value = isFunction(value) ? value() : value;
|
||||||
|
errorTip(element, value);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.onEnter = {
|
onEnter: {
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
||||||
let fn = event => {
|
let fn = event => {
|
||||||
if ('Enter' == event.key) {
|
if ('Enter' == event.key) {
|
||||||
element.dispatchEvent(new Event('change'));
|
element.dispatchEvent(new Event('change'));
|
||||||
fValueAccessor().call(viewModel);
|
fValueAccessor().call(viewModel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
element.addEventListener('keydown', fn);
|
||||||
|
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
i18nInit: {
|
||||||
|
init: element => i18nToNodes(element)
|
||||||
|
},
|
||||||
|
|
||||||
|
i18nUpdate: {
|
||||||
|
update: (element, fValueAccessor) => {
|
||||||
|
ko.unwrap(fValueAccessor());
|
||||||
|
i18nToNodes(element);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
|
||||||
|
},
|
||||||
|
|
||||||
|
command: {
|
||||||
|
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
|
||||||
|
const command = fValueAccessor();
|
||||||
|
|
||||||
|
if (!command || !command.canExecute) {
|
||||||
|
throw new Error('Value should be a command');
|
||||||
}
|
}
|
||||||
};
|
|
||||||
element.addEventListener('keydown', fn);
|
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.onSpace = {
|
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
element,
|
||||||
let fn = event => {
|
fValueAccessor,
|
||||||
if (' ' == event.key) {
|
fAllBindings,
|
||||||
fValueAccessor().call(viewModel, event);
|
viewModel,
|
||||||
|
bindingContext
|
||||||
|
);
|
||||||
|
},
|
||||||
|
update: (element, fValueAccessor) => {
|
||||||
|
const cl = element.classList,
|
||||||
|
command = fValueAccessor();
|
||||||
|
|
||||||
|
let disabled = !command.canExecute();
|
||||||
|
cl.toggle('disabled', disabled);
|
||||||
|
|
||||||
|
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
|
||||||
|
element.disabled = disabled;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
element.addEventListener('keyup', fn);
|
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.modal = {
|
|
||||||
init: (element, fValueAccessor) => {
|
|
||||||
const close = element.querySelector('.close'),
|
|
||||||
click = () => fValueAccessor()(false);
|
|
||||||
close && close.addEventListener('click.koModal', click);
|
|
||||||
|
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () =>
|
|
||||||
close.removeEventListener('click.koModal', click)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.i18nInit = {
|
|
||||||
init: element => i18nToNodes(element)
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.i18nUpdate = {
|
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
ko.unwrap(fValueAccessor());
|
|
||||||
i18nToNodes(element);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.title = {
|
|
||||||
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
|
|
||||||
};
|
|
||||||
|
|
||||||
ko.bindingHandlers.command = {
|
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
|
|
||||||
const command = fValueAccessor();
|
|
||||||
|
|
||||||
if (!command || !command.enabled || !command.canExecute) {
|
|
||||||
throw new Error('Value should be a command');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
|
||||||
element,
|
|
||||||
fValueAccessor,
|
|
||||||
fAllBindings,
|
|
||||||
viewModel,
|
|
||||||
bindingContext
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
const cl = element.classList,
|
|
||||||
command = fValueAccessor();
|
|
||||||
|
|
||||||
let disabled = !command.enabled();
|
saveTrigger: {
|
||||||
|
init: (element) => {
|
||||||
disabled = disabled || !command.canExecute();
|
let icon = element;
|
||||||
cl.toggle('disabled', disabled);
|
if (element.matches('input,select,textarea')) {
|
||||||
|
element.classList.add('settings-saved-trigger-input');
|
||||||
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
|
element.after(element.saveTriggerIcon = icon = createElement('span'));
|
||||||
element.disabled = disabled;
|
}
|
||||||
}
|
icon.classList.add('settings-save-trigger');
|
||||||
}
|
},
|
||||||
};
|
update: (element, fValueAccessor) => {
|
||||||
|
const value = parseInt(ko.unwrap(fValueAccessor()),10);
|
||||||
ko.bindingHandlers.saveTrigger = {
|
let cl = (element.saveTriggerIcon || element).classList;
|
||||||
init: (element) => {
|
if (element.saveTriggerIcon) {
|
||||||
let icon = element;
|
cl.toggle('saving', value === SaveSettingsStep.Animate);
|
||||||
if (element.matches('input,select,textarea')) {
|
cl.toggle('success', value === SaveSettingsStep.TrueResult);
|
||||||
element.classList.add('settings-saved-trigger-input');
|
cl.toggle('error', value === SaveSettingsStep.FalseResult);
|
||||||
element.after(element.saveTriggerIcon = icon = createElement('span'));
|
}
|
||||||
}
|
cl = element.classList;
|
||||||
icon.classList.add('settings-save-trigger');
|
|
||||||
},
|
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
const value = parseInt(ko.unwrap(fValueAccessor()),10);
|
|
||||||
let cl = (element.saveTriggerIcon || element).classList;
|
|
||||||
if (element.saveTriggerIcon) {
|
|
||||||
cl.toggle('saving', value === SaveSettingsStep.Animate);
|
|
||||||
cl.toggle('success', value === SaveSettingsStep.TrueResult);
|
cl.toggle('success', value === SaveSettingsStep.TrueResult);
|
||||||
cl.toggle('error', value === SaveSettingsStep.FalseResult);
|
cl.toggle('error', value === SaveSettingsStep.FalseResult);
|
||||||
}
|
}
|
||||||
cl = element.classList;
|
|
||||||
cl.toggle('success', value === SaveSettingsStep.TrueResult);
|
|
||||||
cl.toggle('error', value === SaveSettingsStep.FalseResult);
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// extenders
|
// extenders
|
||||||
|
|
||||||
ko.extenders.limitedList = (target, limitedList) => {
|
ko.extenders.limitedList = (target, limitedList) => {
|
||||||
const result = ko
|
const result = ko
|
||||||
.computed({
|
.computed({
|
||||||
read: target,
|
read: target,
|
||||||
write: (newValue) => {
|
write: newValue => {
|
||||||
const currentValue = ko.unwrap(target),
|
let currentValue = target(),
|
||||||
list = ko.unwrap(limitedList);
|
list = ko.unwrap(limitedList);
|
||||||
|
list = arrayLength(list) ? list : [''];
|
||||||
if (arrayLength(list)) {
|
if (!list.includes(newValue)) {
|
||||||
if (list.includes(newValue)) {
|
newValue = list.includes(currentValue, list) ? currentValue : list[0];
|
||||||
target(newValue);
|
target(newValue + ' ');
|
||||||
} else if (list.includes(currentValue, list)) {
|
|
||||||
target(currentValue + ' ');
|
|
||||||
target(currentValue);
|
|
||||||
} else {
|
|
||||||
target(list[0] + ' ');
|
|
||||||
target(list[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
target('');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
target(newValue);
|
||||||
.extend({ notify: 'always' });
|
}
|
||||||
|
})
|
||||||
|
.extend({ notify: 'always' });
|
||||||
|
|
||||||
result(target());
|
result(target());
|
||||||
|
|
||||||
|
|
@ -184,6 +198,6 @@ ko.extenders.falseTimeout = (target, option) => {
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
|
|
||||||
ko.observable.fn.deleteAccessHelper = function() {
|
ko.observable.fn.askDeleteHelper = function() {
|
||||||
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'deleteAccess'] });
|
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,24 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="user-scalable=no"/>
|
<title></title>
|
||||||
<title>{{subject}}</title>
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
font-size: 13px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: #eee;
|
background: rgba(125,128,128,0.3);
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #888;
|
||||||
padding: 0 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
font-size: 16px;
|
font-size: 120%;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header * {
|
header * {
|
||||||
font-size: 12px;
|
margin: 5px 0;
|
||||||
padding: 5px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header time {
|
header time {
|
||||||
|
|
@ -33,30 +26,21 @@ header time {
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 2px solid black;
|
border-left: 2px solid rgba(125,128,128,0.5);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 10px;
|
padding: 0 0 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre, .body-wrp.plain {
|
pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-wrp {
|
body > * {
|
||||||
padding: 15px;
|
padding: 0.5em 1em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body></body>
|
||||||
<header>
|
|
||||||
<h1>{{subject}}</h1>
|
|
||||||
<time>{{date}}</time>
|
|
||||||
<div>{{fromCreds}}</div>
|
|
||||||
<div>{{toLabel}}: {{toCreds}}</div>
|
|
||||||
<div {{ccHide}}>{{ccLabel}}: {{ccCreds}}</div>
|
|
||||||
</header>
|
|
||||||
<div class="body-wrp {{bodyClass}}">{{html}}</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import { isArray, isFunction, addObservablesTo, addComputablesTo } from 'Common/Utils';
|
import { isArray, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
||||||
|
import { dispose, addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
function dispose(disposable) {
|
|
||||||
if (disposable && isFunction(disposable.dispose)) {
|
|
||||||
disposable.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeCast(curValue, newValue) {
|
function typeCast(curValue, newValue) {
|
||||||
if (null != curValue) {
|
if (null != curValue) {
|
||||||
|
|
@ -31,7 +26,7 @@ export class AbstractModel {
|
||||||
throw new Error("Can't instantiate AbstractModel!");
|
throw new Error("Can't instantiate AbstractModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
this.subscribables = [];
|
this.disposables = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
addObservables(observables) {
|
addObservables(observables) {
|
||||||
|
|
@ -43,25 +38,26 @@ export class AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
addSubscribables(subscribables) {
|
addSubscribables(subscribables) {
|
||||||
Object.entries(subscribables).forEach(([key, fn]) => this.subscribables.push( this[key].subscribe(fn) ) );
|
// addSubscribablesTo(this, subscribables);
|
||||||
|
forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called by delegateRunOnDestroy */
|
/** Called by delegateRunOnDestroy */
|
||||||
onDestroy() {
|
onDestroy() {
|
||||||
/** dispose ko subscribables */
|
/** dispose ko subscribables */
|
||||||
this.subscribables.forEach(dispose);
|
this.disposables.forEach(dispose);
|
||||||
/** clear object entries */
|
/** clear object entries */
|
||||||
// Object.entries(this).forEach(([key, value]) => {
|
// forEachObjectEntry(this, (key, value) => {
|
||||||
Object.values(this).forEach(value => {
|
forEachObjectValue(this, value => {
|
||||||
/** clear CollectionModel */
|
/** clear CollectionModel */
|
||||||
let arr = ko.isObservableArray(value) ? value() : value;
|
let arr = ko.isObservableArray(value) ? value() : value;
|
||||||
arr && arr.onDestroy && value.onDestroy();
|
arr && arr.onDestroy && arr.onDestroy();
|
||||||
/** destroy ko.observable/ko.computed? */
|
/** destroy ko.observable/ko.computed? */
|
||||||
dispose(value);
|
// dispose(value);
|
||||||
/** clear object value */
|
/** clear object value */
|
||||||
// this[key] = null; // TODO: issue with Contacts view
|
// this[key] = null; // TODO: issue with Contacts view
|
||||||
});
|
});
|
||||||
// this.subscribables = [];
|
// this.disposables = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -89,9 +85,9 @@ export class AbstractModel {
|
||||||
if (!model.validJson(json)) {
|
if (!model.validJson(json)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Object.entries(json).forEach(([key, value]) => {
|
forEachObjectEntry(json, (key, value) => {
|
||||||
if ('@' !== key[0]) try {
|
if ('@' !== key[0]) try {
|
||||||
key = key[0].toLowerCase() + key.substr(1);
|
key = key[0].toLowerCase() + key.slice(1);
|
||||||
switch (typeof this[key])
|
switch (typeof this[key])
|
||||||
{
|
{
|
||||||
case 'function':
|
case 'function':
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,9 @@ import { isArray, arrayLength } from 'Common/Utils';
|
||||||
|
|
||||||
export class AbstractScreen {
|
export class AbstractScreen {
|
||||||
constructor(screenName, viewModels = []) {
|
constructor(screenName, viewModels = []) {
|
||||||
this.oCross = null;
|
this.__cross = null;
|
||||||
this.sScreenName = screenName;
|
this.screenName = screenName;
|
||||||
this.aViewModels = isArray(viewModels) ? viewModels : [];
|
this.viewModels = isArray(viewModels) ? viewModels : [];
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
get viewModels() {
|
|
||||||
return this.aViewModels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
screenName() {
|
|
||||||
return this.sScreenName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,28 +14,26 @@ export class AbstractScreen {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* @returns {?Object}
|
onBuild(viewModelDom) {}
|
||||||
*/
|
onShow() {}
|
||||||
get __cross() {
|
onHide() {}
|
||||||
return this.oCross;
|
__started
|
||||||
}
|
__builded
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onStart() {
|
onStart() {
|
||||||
if (!this.__started) {
|
const routes = this.routes();
|
||||||
this.__started = true;
|
if (arrayLength(routes)) {
|
||||||
const routes = this.routes();
|
let route = new Crossroads(),
|
||||||
if (arrayLength(routes)) {
|
fMatcher = (this.onRoute || (()=>0)).bind(this);
|
||||||
let route = new Crossroads(),
|
|
||||||
fMatcher = (this.onRoute || (()=>0)).bind(this);
|
|
||||||
|
|
||||||
routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1]));
|
routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1]));
|
||||||
|
|
||||||
this.oCross = route;
|
this.__cross = route;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,38 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { inFocus, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils';
|
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
import { Scope } from 'Common/Enums';
|
import { keyScope, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { keyScope } from 'Common/Globals';
|
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { ViewType } from 'Knoin/Knoin';
|
|
||||||
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
|
|
||||||
class AbstractView {
|
class AbstractView {
|
||||||
constructor(name, templateID, type)
|
constructor(templateID, type)
|
||||||
{
|
{
|
||||||
this.viewModelName = 'View/' + name;
|
// Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
|
||||||
this.viewModelTemplateID = templateID;
|
this.viewModelTemplateID = templateID || this.constructor.name.replace('UserView', '');
|
||||||
this.viewModelPosition = type;
|
this.viewType = type;
|
||||||
|
|
||||||
this.bDisabeCloseOnEsc = false;
|
|
||||||
this.sDefaultScope = Scope.None;
|
|
||||||
this.sCurrentScope = Scope.None;
|
|
||||||
|
|
||||||
this.viewModelVisible = false;
|
|
||||||
this.modalVisibility = ko.observable(false).extend({ rateLimit: 0 });
|
|
||||||
|
|
||||||
this.viewModelName = '';
|
|
||||||
this.viewModelDom = null;
|
this.viewModelDom = null;
|
||||||
|
|
||||||
|
this.keyScope = {
|
||||||
|
scope: 'none',
|
||||||
|
previous: 'none',
|
||||||
|
set: function() {
|
||||||
|
this.previous = keyScope();
|
||||||
|
keyScope(this.scope);
|
||||||
|
},
|
||||||
|
unset: function() {
|
||||||
|
keyScope(this.previous);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* @returns {void}
|
onBuild() {}
|
||||||
*/
|
beforeShow() {} // Happens before: hidden = false
|
||||||
storeAndSetScope() {
|
onShow() {} // Happens after: hidden = false
|
||||||
this.sCurrentScope = keyScope();
|
onHide() {}
|
||||||
keyScope(this.sDefaultScope);
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
restoreScope() {
|
|
||||||
keyScope(this.sCurrentScope);
|
|
||||||
}
|
|
||||||
|
|
||||||
querySelector(selectors) {
|
querySelector(selectors) {
|
||||||
return this.viewModelDom.querySelector(selectors);
|
return this.viewModelDom.querySelector(selectors);
|
||||||
|
|
@ -60,54 +56,115 @@ export class AbstractViewPopup extends AbstractView
|
||||||
{
|
{
|
||||||
constructor(name)
|
constructor(name)
|
||||||
{
|
{
|
||||||
super('Popup/' + name, 'Popups' + name, ViewType.Popup);
|
super('Popups' + name, ViewTypePopup);
|
||||||
if (name in Scope) {
|
this.keyScope.scope = name;
|
||||||
this.sDefaultScope = Scope[name];
|
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
||||||
}
|
shortcuts.add('escape,close', '', name, () => {
|
||||||
}
|
if (this.modalVisible() && false !== this.onClose()) {
|
||||||
/*
|
this.close();
|
||||||
cancelCommand() {}
|
return false;
|
||||||
closeCommand() {}
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
registerPopupKeyDown() {
|
|
||||||
addEventListener('keydown', event => {
|
|
||||||
if (event && this.modalVisibility()) {
|
|
||||||
if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) {
|
|
||||||
this.cancelCommand();
|
|
||||||
return false;
|
|
||||||
} else if ('Backspace' == event.key && !inFocus()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Happens when user hits Escape or Close key
|
||||||
|
// return false to prevent closing
|
||||||
|
onClose() {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
beforeShow() {} // Happens before showModal()
|
||||||
|
onShow() {} // Happens after showModal()
|
||||||
|
afterShow() {} // Happens after showModal() animation transitionend
|
||||||
|
onHide() {} // Happens before animation transitionend
|
||||||
|
afterHide() {} // Happens after animation transitionend
|
||||||
|
|
||||||
|
close() {}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractViewCenter extends AbstractView
|
AbstractViewPopup.showModal = function(params = []) {
|
||||||
{
|
showScreenPopup(this, params);
|
||||||
constructor(name, templateID)
|
}
|
||||||
{
|
|
||||||
super(name, templateID, ViewType.Content);
|
AbstractViewPopup.hidden = function() {
|
||||||
}
|
return !this.__vm || !this.__vm.modalVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractViewLeft extends AbstractView
|
export class AbstractViewLeft extends AbstractView
|
||||||
{
|
{
|
||||||
constructor(name, templateID)
|
constructor(templateID)
|
||||||
{
|
{
|
||||||
super(name, templateID, ViewType.Left);
|
super(templateID, 'Left');
|
||||||
|
this.leftPanelDisabled = leftPanelDisabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractViewRight extends AbstractView
|
export class AbstractViewRight extends AbstractView
|
||||||
{
|
{
|
||||||
constructor(name, templateID)
|
constructor(templateID)
|
||||||
{
|
{
|
||||||
super(name, templateID, ViewType.Right);
|
super(templateID, 'Right');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbstractViewSettings
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
onBuild(viewModelDom) {}
|
||||||
|
beforeShow() {}
|
||||||
|
onShow() {}
|
||||||
|
onHide() {}
|
||||||
|
viewModelDom
|
||||||
|
*/
|
||||||
|
addSetting(name, valueCb)
|
||||||
|
{
|
||||||
|
let prop = name[0].toLowerCase() + name.slice(1),
|
||||||
|
trigger = prop + 'Trigger';
|
||||||
|
addObservablesTo(this, {
|
||||||
|
[prop]: SettingsGet(name),
|
||||||
|
[trigger]: SaveSettingsStep.Idle,
|
||||||
|
});
|
||||||
|
addSubscribablesTo(this, {
|
||||||
|
[prop]: (value => {
|
||||||
|
this[trigger](SaveSettingsStep.Animate);
|
||||||
|
valueCb && valueCb(value);
|
||||||
|
rl.app.Remote.saveSetting(name, value,
|
||||||
|
iError => {
|
||||||
|
this[trigger](iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
|
||||||
|
setTimeout(() => this[trigger](SaveSettingsStep.Idle), 1000);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).debounce(999),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addSettings(names)
|
||||||
|
{
|
||||||
|
names.forEach(name => {
|
||||||
|
let prop = name[0].toLowerCase() + name.slice(1);
|
||||||
|
this[prop] || (this[prop] = ko.observable(SettingsGet(name)));
|
||||||
|
this[prop].subscribe(value => rl.app.Remote.saveSetting(name, value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbstractViewLogin extends AbstractView {
|
||||||
|
constructor(templateID) {
|
||||||
|
super(templateID, 'Content');
|
||||||
|
this.hideSubmitButton = SettingsGet('hideSubmitButton');
|
||||||
|
this.formError = ko.observable(false).extend({ falseTimeout: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
onBuild(dom) {
|
||||||
|
dom.classList.add('LoginView');
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
rl.route.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitForm() {
|
||||||
|
// return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
import { koComputable } from 'External/ko';
|
||||||
|
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
|
||||||
|
import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
||||||
|
|
||||||
import { doc, $htmlCL } from 'Common/Globals';
|
let
|
||||||
import { arrayLength, isFunction } from 'Common/Utils';
|
SCREENS = {},
|
||||||
|
currentScreen = null,
|
||||||
let currentScreen = null,
|
|
||||||
visiblePopups = new Set,
|
|
||||||
defaultScreenName = '';
|
defaultScreenName = '';
|
||||||
|
|
||||||
const SCREENS = {},
|
const
|
||||||
autofocus = dom => {
|
autofocus = dom => {
|
||||||
const af = dom.querySelector('[autofocus]');
|
const af = dom.querySelector('[autofocus]');
|
||||||
af && af.focus();
|
af && af.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
visiblePopups = new Set,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} screenName
|
* @param {string} screenName
|
||||||
* @returns {?Object}
|
* @returns {?Object}
|
||||||
|
|
@ -27,81 +30,96 @@ const SCREENS = {},
|
||||||
buildViewModel = (ViewModelClass, vmScreen) => {
|
buildViewModel = (ViewModelClass, vmScreen) => {
|
||||||
if (ViewModelClass && !ViewModelClass.__builded) {
|
if (ViewModelClass && !ViewModelClass.__builded) {
|
||||||
let vmDom = null;
|
let vmDom = null;
|
||||||
const vm = new ViewModelClass(vmScreen),
|
const
|
||||||
position = vm.viewModelPosition || '',
|
vm = new ViewModelClass(vmScreen),
|
||||||
|
id = vm.viewModelTemplateID,
|
||||||
|
position = vm.viewType || '',
|
||||||
|
dialog = ViewTypePopup === position,
|
||||||
vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
|
vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
|
||||||
|
|
||||||
ViewModelClass.__builded = true;
|
ViewModelClass.__builded = true;
|
||||||
ViewModelClass.__vm = vm;
|
ViewModelClass.__vm = vm;
|
||||||
|
|
||||||
if (vmPlace) {
|
if (vmPlace) {
|
||||||
vmDom = Element.fromHTML('<div class="rl-view-model RL-' + vm.viewModelTemplateID + '" hidden=""></div>');
|
vmDom = Element.fromHTML(dialog
|
||||||
|
? '<dialog id="V-'+ id + '"></dialog>'
|
||||||
|
: '<div id="V-'+ id + '" hidden=""></div>');
|
||||||
vmPlace.append(vmDom);
|
vmPlace.append(vmDom);
|
||||||
|
|
||||||
vm.viewModelDom = vmDom;
|
vm.viewModelDom = vmDom;
|
||||||
ViewModelClass.__dom = vmDom;
|
ViewModelClass.__dom = vmDom;
|
||||||
|
|
||||||
if (ViewType.Popup === position) {
|
if (ViewTypePopup === position) {
|
||||||
vm.cancelCommand = vm.closeCommand = createCommand(() => {
|
vm.close = () => hideScreenPopup(ViewModelClass);
|
||||||
hideScreenPopup(ViewModelClass);
|
|
||||||
});
|
// Firefox / Safari HTMLDialogElement not defined
|
||||||
|
if (!vmDom.showModal) {
|
||||||
|
vmDom.classList.add('polyfill');
|
||||||
|
vmDom.showModal = () => {
|
||||||
|
vmDom.backdrop ||
|
||||||
|
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
|
||||||
|
vmDom.setAttribute('open','');
|
||||||
|
vmDom.open = true;
|
||||||
|
vmDom.returnValue = null;
|
||||||
|
vmDom.backdrop.hidden = false;
|
||||||
|
};
|
||||||
|
vmDom.close = v => {
|
||||||
|
vmDom.backdrop.hidden = true;
|
||||||
|
vmDom.returnValue = v;
|
||||||
|
vmDom.removeAttribute('open', null);
|
||||||
|
vmDom.open = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// show/hide popup/modal
|
// show/hide popup/modal
|
||||||
const endShowHide = e => {
|
const endShowHide = e => {
|
||||||
if (e.target === vmDom) {
|
if (e.target === vmDom) {
|
||||||
if (vmDom.classList.contains('show')) {
|
if (vmDom.classList.contains('animate')) {
|
||||||
autofocus(vmDom);
|
autofocus(vmDom);
|
||||||
vm.onShowWithDelay && vm.onShowWithDelay();
|
vm.afterShow && vm.afterShow();
|
||||||
} else {
|
} else {
|
||||||
vmDom.hidden = true;
|
vmDom.close();
|
||||||
vm.onHideWithDelay && vm.onHideWithDelay();
|
vm.afterHide && vm.afterHide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
vm.modalVisibility.subscribe(value => {
|
vm.modalVisible.subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
visiblePopups.add(vm);
|
visiblePopups.add(vm);
|
||||||
vmDom.style.zIndex = 3000 + popupVisibilityNames().length + 10;
|
vmDom.style.zIndex = 3000 + (visiblePopups.size * 2);
|
||||||
vmDom.hidden = false;
|
vmDom.showModal();
|
||||||
vm.storeAndSetScope();
|
if (vmDom.backdrop) {
|
||||||
popupVisibilityNames.push(vm.viewModelName);
|
vmDom.backdrop.style.zIndex = 3000 + visiblePopups.size;
|
||||||
|
}
|
||||||
|
vm.keyScope.set();
|
||||||
requestAnimationFrame(() => { // wait just before the next paint
|
requestAnimationFrame(() => { // wait just before the next paint
|
||||||
vmDom.offsetHeight; // force a reflow
|
vmDom.offsetHeight; // force a reflow
|
||||||
vmDom.classList.add('show'); // trigger the transitions
|
vmDom.classList.add('animate'); // trigger the transitions
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
visiblePopups.delete(vm);
|
visiblePopups.delete(vm);
|
||||||
vm.onHide && vm.onHide();
|
vm.onHide && vm.onHide();
|
||||||
vmDom.classList.remove('show');
|
vm.keyScope.unset();
|
||||||
vm.restoreScope();
|
vmDom.classList.remove('animate'); // trigger the transitions
|
||||||
popupVisibilityNames(popupVisibilityNames.filter(v=>v!==vm.viewModelName));
|
|
||||||
}
|
}
|
||||||
vmDom.setAttribute('aria-hidden', !value);
|
arePopupsVisible(0 < visiblePopups.size);
|
||||||
});
|
});
|
||||||
if ('ontransitionend' in vmDom) {
|
vmDom.addEventListener('transitionend', endShowHide);
|
||||||
vmDom.addEventListener('transitionend', endShowHide);
|
|
||||||
} else {
|
|
||||||
// For Edge < 79 and mobile browsers
|
|
||||||
vm.modalVisibility.subscribe(() => ()=>setTimeout(endShowHide({target:vmDom}), 500));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ko.applyBindingAccessorsToNode(
|
ko.applyBindingAccessorsToNode(
|
||||||
vmDom,
|
vmDom,
|
||||||
{
|
{
|
||||||
i18nInit: true,
|
i18nInit: true,
|
||||||
template: () => ({ name: vm.viewModelTemplateID })
|
template: () => ({ name: id })
|
||||||
},
|
},
|
||||||
vm
|
vm
|
||||||
);
|
);
|
||||||
|
|
||||||
vm.onBuild && vm.onBuild(vmDom);
|
vm.onBuild && vm.onBuild(vmDom);
|
||||||
if (vm && ViewType.Popup === position) {
|
|
||||||
vm.registerPopupKeyDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchEvent(new CustomEvent('rl-view-model', {detail:vm}));
|
fireEvent('rl-view-model', vm);
|
||||||
} else {
|
} else {
|
||||||
console.log('Cannot find view model position: ' + position);
|
console.log('Cannot find view model position: ' + position);
|
||||||
}
|
}
|
||||||
|
|
@ -110,6 +128,37 @@ const SCREENS = {},
|
||||||
return ViewModelClass && ViewModelClass.__vm;
|
return ViewModelClass && ViewModelClass.__vm;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
forEachViewModel = (screen, fn) => {
|
||||||
|
screen.viewModels.forEach(ViewModelClass => {
|
||||||
|
if (
|
||||||
|
ViewModelClass.__vm &&
|
||||||
|
ViewModelClass.__dom &&
|
||||||
|
ViewTypePopup !== ViewModelClass.__vm.viewType
|
||||||
|
) {
|
||||||
|
fn(ViewModelClass.__vm, ViewModelClass.__dom);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hideScreen = (screenToHide, destroy) => {
|
||||||
|
screenToHide.onHide && screenToHide.onHide();
|
||||||
|
forEachViewModel(screenToHide, (vm, dom) => {
|
||||||
|
dom.hidden = true;
|
||||||
|
vm.onHide && vm.onHide();
|
||||||
|
destroy && vm.viewModelDom.remove();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Function} ViewModelClassToHide
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
hideScreenPopup = ViewModelClassToHide => {
|
||||||
|
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
|
||||||
|
ViewModelClassToHide.__vm.modalVisible(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} screenName
|
* @param {string} screenName
|
||||||
* @param {string} subPart
|
* @param {string} subPart
|
||||||
|
|
@ -125,7 +174,7 @@ const SCREENS = {},
|
||||||
|
|
||||||
// Close all popups
|
// Close all popups
|
||||||
for (let vm of visiblePopups) {
|
for (let vm of visiblePopups) {
|
||||||
vm.closeCommand();
|
false === vm.onClose() || vm.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenName) {
|
if (screenName) {
|
||||||
|
|
@ -154,109 +203,34 @@ const SCREENS = {},
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// hide screen
|
// hide screen
|
||||||
if (currentScreen && !isSameScreen) {
|
if (currentScreen && !isSameScreen) {
|
||||||
currentScreen.onHide && currentScreen.onHide();
|
hideScreen(currentScreen);
|
||||||
currentScreen.onHideWithDelay && setTimeout(()=>currentScreen.onHideWithDelay(), 500);
|
|
||||||
|
|
||||||
if (arrayLength(currentScreen.viewModels)) {
|
|
||||||
currentScreen.viewModels.forEach(ViewModelClass => {
|
|
||||||
if (
|
|
||||||
ViewModelClass.__vm &&
|
|
||||||
ViewModelClass.__dom &&
|
|
||||||
ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
|
|
||||||
) {
|
|
||||||
ViewModelClass.__dom.hidden = true;
|
|
||||||
ViewModelClass.__vm.viewModelVisible = false;
|
|
||||||
|
|
||||||
ViewModelClass.__vm.onHide && ViewModelClass.__vm.onHide();
|
|
||||||
ViewModelClass.__vm.onHideWithDelay && setTimeout(()=>ViewModelClass.__vm.onHideWithDelay(), 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// --
|
// --
|
||||||
|
|
||||||
currentScreen = vmScreen;
|
currentScreen = vmScreen;
|
||||||
|
|
||||||
// show screen
|
// show screen
|
||||||
if (currentScreen && !isSameScreen) {
|
if (!isSameScreen) {
|
||||||
currentScreen.onShow && currentScreen.onShow();
|
vmScreen.onShow && vmScreen.onShow();
|
||||||
|
|
||||||
if (arrayLength(currentScreen.viewModels)) {
|
forEachViewModel(vmScreen, (vm, dom) => {
|
||||||
currentScreen.viewModels.forEach(ViewModelClass => {
|
vm.beforeShow && vm.beforeShow();
|
||||||
if (
|
dom.hidden = false;
|
||||||
ViewModelClass.__vm &&
|
vm.onShow && vm.onShow();
|
||||||
ViewModelClass.__dom &&
|
autofocus(dom);
|
||||||
ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
|
});
|
||||||
) {
|
|
||||||
ViewModelClass.__vm.onBeforeShow && ViewModelClass.__vm.onBeforeShow();
|
|
||||||
|
|
||||||
ViewModelClass.__dom.hidden = false;
|
|
||||||
ViewModelClass.__vm.viewModelVisible = true;
|
|
||||||
|
|
||||||
ViewModelClass.__vm.onShow && ViewModelClass.__vm.onShow();
|
|
||||||
|
|
||||||
autofocus(ViewModelClass.__dom);
|
|
||||||
|
|
||||||
ViewModelClass.__vm.onShowWithDelay && setTimeout(()=>ViewModelClass.__vm.onShowWithDelay, 200);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// --
|
// --
|
||||||
|
|
||||||
vmScreen && vmScreen.__cross && vmScreen.__cross.parse(subPart);
|
vmScreen.__cross && vmScreen.__cross.parse(subPart);
|
||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const
|
export const
|
||||||
popupVisibilityNames = ko.observableArray(),
|
ViewTypePopup = 'Popups',
|
||||||
|
|
||||||
ViewType = {
|
|
||||||
Popup: 'Popups',
|
|
||||||
Left: 'Left',
|
|
||||||
Right: 'Right',
|
|
||||||
Content: 'Content'
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} fExecute
|
|
||||||
* @param {(Function|boolean|null)=} fCanExecute = true
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
createCommand = (fExecute, fCanExecute = true) => {
|
|
||||||
let fResult = fExecute
|
|
||||||
? (...args) => {
|
|
||||||
if (fResult && fResult.canExecute && fResult.canExecute()) {
|
|
||||||
fExecute.apply(null, args);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} : ()=>0;
|
|
||||||
fResult.enabled = ko.observable(true);
|
|
||||||
fResult.isCommand = true;
|
|
||||||
|
|
||||||
if (isFunction(fCanExecute)) {
|
|
||||||
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && fCanExecute.call(null));
|
|
||||||
} else {
|
|
||||||
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && !!fCanExecute);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fResult;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} ViewModelClassToHide
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
hideScreenPopup = ViewModelClassToHide => {
|
|
||||||
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
|
|
||||||
ViewModelClassToHide.__vm.modalVisibility(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getScreenPopupViewModel = ViewModelClassToShow =>
|
|
||||||
(buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom) && ViewModelClassToShow.__vm,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} ViewModelClassToShow
|
* @param {Function} ViewModelClassToShow
|
||||||
|
|
@ -264,48 +238,47 @@ export const
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
showScreenPopup = (ViewModelClassToShow, params = []) => {
|
showScreenPopup = (ViewModelClassToShow, params = []) => {
|
||||||
const vm = getScreenPopupViewModel(ViewModelClassToShow);
|
const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
|
||||||
|
|
||||||
if (vm) {
|
if (vm) {
|
||||||
params = params || [];
|
params = params || [];
|
||||||
|
|
||||||
vm.onBeforeShow && vm.onBeforeShow(...params);
|
vm.beforeShow && vm.beforeShow(...params);
|
||||||
|
|
||||||
vm.modalVisibility(true);
|
vm.modalVisible(true);
|
||||||
|
|
||||||
vm.onShow && vm.onShow(...params);
|
vm.onShow && vm.onShow(...params);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
arePopupsVisible = () => 0 < visiblePopups.size,
|
arePopupsVisible = ko.observable(false),
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} ViewModelClassToShow
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isPopupVisible = ViewModelClassToShow =>
|
|
||||||
ViewModelClassToShow && ViewModelClassToShow.__vm && ViewModelClassToShow.__vm.modalVisibility(),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array} screensClasses
|
* @param {Array} screensClasses
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
startScreens = screensClasses => {
|
startScreens = screensClasses => {
|
||||||
|
hasher.clear();
|
||||||
|
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
|
||||||
|
SCREENS = {};
|
||||||
|
currentScreen = null,
|
||||||
|
defaultScreenName = '';
|
||||||
|
|
||||||
screensClasses.forEach(CScreen => {
|
screensClasses.forEach(CScreen => {
|
||||||
if (CScreen) {
|
if (CScreen) {
|
||||||
const vmScreen = new CScreen(),
|
const vmScreen = new CScreen(),
|
||||||
screenName = vmScreen && vmScreen.screenName();
|
screenName = vmScreen.screenName;
|
||||||
|
defaultScreenName || (defaultScreenName = screenName);
|
||||||
if (screenName) {
|
SCREENS[screenName] = vmScreen;
|
||||||
defaultScreenName || (defaultScreenName = screenName);
|
|
||||||
|
|
||||||
SCREENS[screenName] = vmScreen;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.values(SCREENS).forEach(vmScreen =>
|
forEachObjectValue(SCREENS, vmScreen => {
|
||||||
vmScreen && vmScreen.onStart && vmScreen.onStart()
|
if (!vmScreen.__started) {
|
||||||
);
|
vmScreen.onStart();
|
||||||
|
vmScreen.__started = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const cross = new Crossroads();
|
const cross = new Crossroads();
|
||||||
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
|
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
|
||||||
|
|
@ -314,22 +287,22 @@ export const
|
||||||
hasher.init();
|
hasher.init();
|
||||||
|
|
||||||
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
|
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
|
||||||
setTimeout(() => $htmlCL.add('rl-started-delay'), 200);
|
|
||||||
|
const c = elementById('rl-content'), l = elementById('rl-loading');
|
||||||
|
c && (c.hidden = false);
|
||||||
|
l && l.remove();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by ko.bindingHandlers.command (template data-bind="command: ")
|
||||||
|
* to enable/disable click/submit action.
|
||||||
|
*/
|
||||||
decorateKoCommands = (thisArg, commands) =>
|
decorateKoCommands = (thisArg, commands) =>
|
||||||
Object.entries(commands).forEach(([key, canExecute]) => {
|
forEachObjectEntry(commands, (key, canExecute) => {
|
||||||
let command = thisArg[key],
|
let command = thisArg[key],
|
||||||
fn = (...args) => fn.enabled() && fn.canExecute() && command.apply(thisArg, args);
|
fn = (...args) => fn.canExecute() && command.apply(thisArg, args);
|
||||||
|
|
||||||
// fn.__realCanExecute = canExecute;
|
fn.canExecute = koComputable(() => canExecute.call(thisArg, thisArg));
|
||||||
// fn.isCommand = true;
|
|
||||||
|
|
||||||
fn.enabled = ko.observable(true);
|
|
||||||
|
|
||||||
fn.canExecute = (typeof canExecute === 'function')
|
|
||||||
? ko.computed(() => fn.enabled() && canExecute.call(thisArg, thisArg))
|
|
||||||
: ko.computed(() => fn.enabled());
|
|
||||||
|
|
||||||
thisArg[key] = fn;
|
thisArg[key] = fn;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
132
dev/Mime/Parser.js
Normal file
132
dev/Mime/Parser.js
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
|
||||||
|
const
|
||||||
|
// RFC2045
|
||||||
|
QPDecodeIn = /=([0-9A-F]{2})/g,
|
||||||
|
QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
|
||||||
|
|
||||||
|
export function ParseMime(text)
|
||||||
|
{
|
||||||
|
class MimePart
|
||||||
|
{
|
||||||
|
header(name) {
|
||||||
|
return this.headers && this.headers[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
headerValue(name) {
|
||||||
|
return (this.header(name) || {value:null}).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get raw() {
|
||||||
|
return text.slice(this.start, this.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
get bodyRaw() {
|
||||||
|
return text.slice(this.bodyStart, this.bodyEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
get body() {
|
||||||
|
let body = this.bodyRaw,
|
||||||
|
encoding = this.headerValue('content-transfer-encoding');
|
||||||
|
if ('quoted-printable' == encoding) {
|
||||||
|
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
||||||
|
} else if ('base64' == encoding) {
|
||||||
|
body = atob(body.replace(/\r?\n/g, ''));
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
get dataUrl() {
|
||||||
|
let body = this.bodyRaw,
|
||||||
|
encoding = this.headerValue('content-transfer-encoding');
|
||||||
|
if ('base64' == encoding) {
|
||||||
|
body = body.replace(/\r?\n/g, '');
|
||||||
|
} else {
|
||||||
|
if ('quoted-printable' == encoding) {
|
||||||
|
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
||||||
|
}
|
||||||
|
body = btoa(body);
|
||||||
|
}
|
||||||
|
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(fn) {
|
||||||
|
fn(this);
|
||||||
|
if (this.parts) {
|
||||||
|
this.parts.forEach(part => part.forEach(fn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getByContentType(type) {
|
||||||
|
if (type == this.headerValue('content-type')) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (this.parts) {
|
||||||
|
let i = 0, p = this.parts, part;
|
||||||
|
for (i; i < p.length; ++i) {
|
||||||
|
if ((part = p[i].getByContentType(type))) {
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ParsePart = (mimePart, start_pos = 0, id = '') =>
|
||||||
|
{
|
||||||
|
let part = new MimePart;
|
||||||
|
if (id) {
|
||||||
|
part.id = id;
|
||||||
|
part.start = start_pos;
|
||||||
|
part.end = start_pos + mimePart.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get headers
|
||||||
|
let head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/);
|
||||||
|
if (head) {
|
||||||
|
head = head[0];
|
||||||
|
let headers = {};
|
||||||
|
head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => {
|
||||||
|
let match = header.match(/^([^:]+):\s*([^;]+)/),
|
||||||
|
params = {};
|
||||||
|
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
||||||
|
params[param[1].trim().toLowerCase()] = param[2].trim()
|
||||||
|
);
|
||||||
|
headers[match[1].trim().toLowerCase()] = {
|
||||||
|
value: match[2].trim(),
|
||||||
|
params: params
|
||||||
|
};
|
||||||
|
});
|
||||||
|
part.headers = headers;
|
||||||
|
|
||||||
|
// get body
|
||||||
|
part.bodyStart = start_pos + head.length;
|
||||||
|
part.bodyEnd = start_pos + mimePart.length;
|
||||||
|
|
||||||
|
// get child parts
|
||||||
|
let boundary = headers['content-type'].params.boundary;
|
||||||
|
if (boundary) {
|
||||||
|
part.boundary = boundary;
|
||||||
|
part.parts = [];
|
||||||
|
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
|
||||||
|
body = mimePart.slice(head.length),
|
||||||
|
bodies = body.split(regex),
|
||||||
|
pos = part.bodyStart;
|
||||||
|
[...body.matchAll(regex)].forEach(([boundary], index) => {
|
||||||
|
if (!index) {
|
||||||
|
// Mostly something like: "This is a multi-part message in MIME format."
|
||||||
|
part.bodyText = bodies[0];
|
||||||
|
}
|
||||||
|
// Not the end?
|
||||||
|
if ('--' != boundary.trim().slice(-2)) {
|
||||||
|
pos += bodies[index].length + boundary.length;
|
||||||
|
part.parts.push(ParsePart(bodies[1+index], pos, ((id ? id + '.' : '') + (1+index))));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return part;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ParsePart(text);
|
||||||
|
}
|
||||||
76
dev/Mime/Utils.js
Normal file
76
dev/Mime/Utils.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
|
||||||
|
import { ParseMime } from 'Mime/Parser';
|
||||||
|
import { AttachmentModel } from 'Model/Attachment';
|
||||||
|
import { FileInfo } from 'Common/File';
|
||||||
|
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string data
|
||||||
|
* @param MessageModel message
|
||||||
|
*/
|
||||||
|
export function MimeToMessage(data, message)
|
||||||
|
{
|
||||||
|
let signed;
|
||||||
|
const struct = ParseMime(data);
|
||||||
|
if (struct.headers) {
|
||||||
|
let html = struct.getByContentType('text/html');
|
||||||
|
html = html ? html.body : '';
|
||||||
|
|
||||||
|
struct.forEach(part => {
|
||||||
|
let cd = part.header('content-disposition'),
|
||||||
|
cid = part.header('content-id'),
|
||||||
|
type = part.header('content-type');
|
||||||
|
if (cid || cd) {
|
||||||
|
// if (cd && 'attachment' === cd.value) {
|
||||||
|
let attachment = new AttachmentModel;
|
||||||
|
attachment.mimeType = type.value;
|
||||||
|
attachment.fileName = type.name || (cd && cd.params.filename) || '';
|
||||||
|
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
|
||||||
|
attachment.fileType = FileInfo.getType('', type.value);
|
||||||
|
attachment.url = part.dataUrl;
|
||||||
|
attachment.friendlySize = FileInfo.friendlySize(part.body.length);
|
||||||
|
/*
|
||||||
|
attachment.isThumbnail = false;
|
||||||
|
attachment.contentLocation = '';
|
||||||
|
attachment.download = '';
|
||||||
|
attachment.folder = '';
|
||||||
|
attachment.uid = '';
|
||||||
|
attachment.mimeIndex = part.id;
|
||||||
|
attachment.framed = false;
|
||||||
|
*/
|
||||||
|
attachment.cid = cid ? cid.value : '';
|
||||||
|
if (cid && html) {
|
||||||
|
let cid = 'cid:' + attachment.contentId(),
|
||||||
|
found = html.includes(cid);
|
||||||
|
attachment.isInline(found);
|
||||||
|
attachment.isLinked(found);
|
||||||
|
found && (html = html
|
||||||
|
.replace('src="' + cid + '"', 'src="' + attachment.url + '"')
|
||||||
|
.replace("src='" + cid + "'", "src='" + attachment.url + "'")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
message.attachments.push(attachment);
|
||||||
|
}
|
||||||
|
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
|
||||||
|
signed = {
|
||||||
|
MicAlg: type.micalg,
|
||||||
|
BodyPart: part.parts[0],
|
||||||
|
SigPart: part.parts[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = struct.getByContentType('text/plain');
|
||||||
|
message.plain(text ? text.body : '');
|
||||||
|
message.html(html);
|
||||||
|
} else {
|
||||||
|
message.plain(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) {
|
||||||
|
signed = true;
|
||||||
|
}
|
||||||
|
message.pgpSigned(signed);
|
||||||
|
|
||||||
|
// TODO: Verify instantly?
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray, forEachObjectEntry } from 'Common/Utils';
|
||||||
|
|
||||||
export class AbstractCollectionModel extends Array
|
export class AbstractCollectionModel extends Array
|
||||||
{
|
{
|
||||||
|
|
@ -24,8 +24,7 @@ export class AbstractCollectionModel extends Array
|
||||||
const result = new this();
|
const result = new this();
|
||||||
if (json) {
|
if (json) {
|
||||||
if ('Collection/'+this.name.replace('Model', '') === json['@Object']) {
|
if ('Collection/'+this.name.replace('Model', '') === json['@Object']) {
|
||||||
Object.entries(json).forEach(([key, value]) => '@' !== key[0] && (result[key] = value));
|
forEachObjectEntry(json, (key, value) => '@' !== key[0] && (result[key] = value));
|
||||||
// json[@Count]
|
|
||||||
json = json['@Collection'];
|
json = json['@Collection'];
|
||||||
}
|
}
|
||||||
if (isArray(json)) {
|
if (isArray(json)) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { change } from 'Common/Links';
|
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
export class AccountModel extends AbstractModel {
|
export class AccountModel extends AbstractModel {
|
||||||
|
|
@ -8,23 +6,16 @@ export class AccountModel extends AbstractModel {
|
||||||
* @param {boolean=} canBeDelete = true
|
* @param {boolean=} canBeDelete = true
|
||||||
* @param {number=} count = 0
|
* @param {number=} count = 0
|
||||||
*/
|
*/
|
||||||
constructor(email, canBeDelete = true, count = 0) {
|
constructor(email/*, count = 0*/, isAdditional = true) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.email = email;
|
this.email = email;
|
||||||
|
|
||||||
this.addObservables({
|
this.addObservables({
|
||||||
count: count,
|
// count: count || 0,
|
||||||
deleteAccess: false,
|
askDelete: false,
|
||||||
canBeDeleted: !!canBeDelete
|
isAdditional: isAdditional
|
||||||
});
|
});
|
||||||
this.canBeEdit = this.canBeDeleted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
changeAccountLink() {
|
|
||||||
return change(this.email);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,20 @@ export class AttachmentModel extends AbstractModel {
|
||||||
this.fileNameExt = '';
|
this.fileNameExt = '';
|
||||||
this.fileType = FileType.Unknown;
|
this.fileType = FileType.Unknown;
|
||||||
this.friendlySize = '';
|
this.friendlySize = '';
|
||||||
this.isInline = false;
|
|
||||||
this.isLinked = false;
|
|
||||||
this.isThumbnail = false;
|
this.isThumbnail = false;
|
||||||
this.cid = '';
|
this.cid = '';
|
||||||
this.cidWithoutTags = '';
|
|
||||||
this.contentLocation = '';
|
this.contentLocation = '';
|
||||||
this.download = '';
|
this.download = '';
|
||||||
this.folder = '';
|
this.folder = '';
|
||||||
this.uid = '';
|
this.uid = '';
|
||||||
|
this.url = '';
|
||||||
this.mimeIndex = '';
|
this.mimeIndex = '';
|
||||||
this.framed = false;
|
this.framed = false;
|
||||||
|
|
||||||
|
this.addObservables({
|
||||||
|
isInline: false,
|
||||||
|
isLinked: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -43,7 +46,6 @@ export class AttachmentModel extends AbstractModel {
|
||||||
const attachment = super.reviveFromJson(json);
|
const attachment = super.reviveFromJson(json);
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
|
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
|
||||||
attachment.cidWithoutTags = attachment.cid.replace(/^<+/, '').replace(/>+$/, '');
|
|
||||||
|
|
||||||
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
|
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
|
||||||
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
|
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
|
||||||
|
|
@ -51,6 +53,10 @@ export class AttachmentModel extends AbstractModel {
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contentId() {
|
||||||
|
return this.cid.replace(/^<+|>+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
|
|
@ -122,29 +128,21 @@ export class AttachmentModel extends AbstractModel {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
linkDownload() {
|
linkDownload() {
|
||||||
return attachmentDownload(this.download);
|
return this.url || attachmentDownload(this.download);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
linkPreview() {
|
linkPreview() {
|
||||||
return serverRequestRaw('View', this.download);
|
return this.url || serverRequestRaw('View', this.download);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
linkThumbnail() {
|
|
||||||
return this.hasThumbnail() ? serverRequestRaw('ViewThumbnail', this.download) : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
linkThumbnailPreviewStyle() {
|
linkThumbnailPreviewStyle() {
|
||||||
const link = this.linkThumbnail();
|
return this.hasThumbnail() ? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')' : '';
|
||||||
return link ? 'background:url(' + link + ')' : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -175,7 +173,7 @@ export class AttachmentModel extends AbstractModel {
|
||||||
const localEvent = event.originalEvent || event;
|
const localEvent = event.originalEvent || event;
|
||||||
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
|
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
|
||||||
let link = this.linkDownload();
|
let link = this.linkDownload();
|
||||||
if ('http' !== link.substr(0, 4)) {
|
if ('http' !== link.slice(0, 4)) {
|
||||||
link = location.protocol + '//' + location.host + location.pathname + link;
|
link = location.protocol + '//' + location.host + location.pathname + link;
|
||||||
}
|
}
|
||||||
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);
|
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,20 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(items) {
|
static reviveFromJson(items) {
|
||||||
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
||||||
|
/*
|
||||||
|
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
||||||
|
if (attachments) {
|
||||||
|
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
|
||||||
|
}
|
||||||
|
return attachments;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
hasVisible() {
|
hasVisible() {
|
||||||
return !!this.find(item => !item.isLinked);
|
return !!this.filter(item => !item.isLinked()).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
findByCid(cid) {
|
findByCid(cid) {
|
||||||
cid = cid.replace(/^<+|>+$/, '');
|
cid = cid.replace(/^<+|>+$/g, '');
|
||||||
return this.find(item => cid === item.cidWithoutTags);
|
return this.find(item => cid === item.contentId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,8 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
item.download,
|
item.download,
|
||||||
item.fileName,
|
item.fileName,
|
||||||
item.estimatedSize,
|
item.estimatedSize,
|
||||||
item.isInline,
|
item.isInline(),
|
||||||
item.isLinked,
|
item.isLinked(),
|
||||||
item.cid,
|
item.cid,
|
||||||
item.contentLocation
|
item.contentLocation
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,7 @@ class Tokenizer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmailModel extends AbstractModel {
|
export class EmailModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
* @param {string=} email = ''
|
* @param {string=} email = ''
|
||||||
* @param {string=} name = ''
|
* @param {string=} name = ''
|
||||||
|
|
@ -430,5 +430,3 @@ class EmailModel extends AbstractModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { EmailModel, EmailModel as default };
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,112 @@
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
|
|
||||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||||
import { isArray, pInt } from 'Common/Utils';
|
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import { ClientSideKeyName, FolderType } from 'Common/EnumsUser';
|
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
import * as Cache from 'Common/Cache';
|
import { getFolderFromCacheList, setFolder, setFolderInboxName, setFolderHash } from 'Common/Cache';
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||||
|
|
||||||
import * as Local from 'Storage/Client';
|
import * as Local from 'Storage/Client';
|
||||||
|
|
||||||
import { AppUserStore } from 'Stores/User/App';
|
import { AppUserStore } from 'Stores/User/App';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { isPosNumeric } from 'Common/UtilsUser';
|
import { sortFolders } from 'Common/Folders';
|
||||||
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
|
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
const
|
import { koComputable } from 'External/ko';
|
||||||
normalizeFolder = sFolderFullNameRaw => ('' === sFolderFullNameRaw
|
|
||||||
|| UNUSED_OPTION_VALUE === sFolderFullNameRaw
|
|
||||||
|| null !== Cache.getFolderFromCacheList(sFolderFullNameRaw))
|
|
||||||
? sFolderFullNameRaw
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// index is FolderType value
|
//import { mailBox } from 'Common/Links';
|
||||||
let SystemFolders = [];
|
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
const
|
||||||
|
isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
||||||
|
|
||||||
|
normalizeFolder = sFolderFullName => ('' === sFolderFullName
|
||||||
|
|| UNUSED_OPTION_VALUE === sFolderFullName
|
||||||
|
|| null !== getFolderFromCacheList(sFolderFullName))
|
||||||
|
? sFolderFullName
|
||||||
|
: '',
|
||||||
|
|
||||||
|
SystemFolders = {
|
||||||
|
Inbox: 0,
|
||||||
|
Sent: 0,
|
||||||
|
Drafts: 0,
|
||||||
|
Spam: 0,
|
||||||
|
Trash: 0,
|
||||||
|
Archive: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
kolabTypes = {
|
||||||
|
configuration: 'CONFIGURATION',
|
||||||
|
event: 'CALENDAR',
|
||||||
|
contact: 'CONTACTS',
|
||||||
|
task: 'TASKS',
|
||||||
|
note: 'NOTES',
|
||||||
|
file: 'FILES',
|
||||||
|
journal: 'JOURNAL'
|
||||||
|
},
|
||||||
|
|
||||||
|
getKolabFolderName = type => kolabTypes[type] ? 'Kolab ' + i18n('SETTINGS_FOLDERS/TYPE_' + kolabTypes[type]) : '',
|
||||||
|
|
||||||
|
getSystemFolderName = (type, def) => {
|
||||||
|
switch (type) {
|
||||||
|
case FolderType.Inbox:
|
||||||
|
case FolderType.Sent:
|
||||||
|
case FolderType.Drafts:
|
||||||
|
case FolderType.Trash:
|
||||||
|
case FolderType.Archive:
|
||||||
|
return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME');
|
||||||
|
case FolderType.Spam:
|
||||||
|
return i18n('GLOBAL/SPAM');
|
||||||
|
// no default
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const
|
||||||
|
/**
|
||||||
|
* @param {string} sFullName
|
||||||
|
* @param {boolean} bExpanded
|
||||||
|
*/
|
||||||
|
setExpandedFolder = (sFullName, bExpanded) => {
|
||||||
|
let aExpandedList = Local.get(ClientSideKeyNameExpandedFolders);
|
||||||
|
if (!isArray(aExpandedList)) {
|
||||||
|
aExpandedList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bExpanded) {
|
||||||
|
aExpandedList.includes(sFullName) || aExpandedList.push(sFullName);
|
||||||
|
} else {
|
||||||
|
aExpandedList = aExpandedList.filter(value => value !== sFullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Local.set(ClientSideKeyNameExpandedFolders, aExpandedList);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?Function} fCallback
|
||||||
|
*/
|
||||||
|
loadFolders = fCallback => {
|
||||||
|
// clearTimeout(this.foldersTimeout);
|
||||||
|
Remote.abort('Folders')
|
||||||
|
.post('Folders', FolderUserStore.foldersLoading)
|
||||||
|
.then(data => {
|
||||||
|
data = FolderCollectionModel.reviveFromJson(data.Result);
|
||||||
|
data && data.storeIt();
|
||||||
|
fCallback && fCallback(true);
|
||||||
|
// Repeat every 15 minutes?
|
||||||
|
// this.foldersTimeout = setTimeout(loadFolders, 900000);
|
||||||
|
})
|
||||||
|
.catch(() => fCallback && setTimeout(fCallback, 1, false));
|
||||||
|
};
|
||||||
|
|
||||||
export class FolderCollectionModel extends AbstractCollectionModel
|
export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
{
|
{
|
||||||
|
|
@ -36,11 +114,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.CountRec
|
this.CountRec
|
||||||
this.FoldersHash
|
|
||||||
this.IsThreadsSupported
|
this.IsThreadsSupported
|
||||||
this.Namespace;
|
this.Namespace;
|
||||||
this.Optimized
|
this.Optimized
|
||||||
this.SystemFolders
|
this.SystemFolders
|
||||||
|
this.Capabilities
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -49,53 +127,40 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
* @returns {FolderCollectionModel}
|
* @returns {FolderCollectionModel}
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(object) {
|
static reviveFromJson(object) {
|
||||||
const expandedFolders = Local.get(ClientSideKeyName.ExpandedFolders);
|
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
|
||||||
if (object && object.SystemFolders) {
|
if (object && object.SystemFolders) {
|
||||||
let sf = object.SystemFolders;
|
forEachObjectEntry(SystemFolders, key =>
|
||||||
SystemFolders = [
|
SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
|
||||||
/* USER */ 0,
|
);
|
||||||
/* INBOX */ sf[1],
|
|
||||||
SettingsGet('SentFolder') || sf[2],
|
|
||||||
SettingsGet('DraftFolder') || sf[3],
|
|
||||||
SettingsGet('SpamFolder') || sf[4],
|
|
||||||
SettingsGet('TrashFolder') || sf[5],
|
|
||||||
SettingsGet('ArchiveFolder') || sf[12]
|
|
||||||
// IMPORTANT: sf[10],
|
|
||||||
// FLAGGED: sf[11],
|
|
||||||
// ALL: sf[13]
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.reviveFromJson(object, oFolder => {
|
const result = super.reviveFromJson(object, oFolder => {
|
||||||
let oCacheFolder = Cache.getFolderFromCacheList(oFolder.FullNameRaw);
|
let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
|
||||||
|
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
|
||||||
|
|
||||||
if (oCacheFolder) {
|
if (!oCacheFolder) {
|
||||||
oFolder.SubFolders = FolderCollectionModel.reviveFromJson(oFolder.SubFolders);
|
|
||||||
oFolder.SubFolders && oCacheFolder.subFolders(oFolder.SubFolders);
|
|
||||||
} else {
|
|
||||||
oCacheFolder = FolderModel.reviveFromJson(oFolder);
|
oCacheFolder = FolderModel.reviveFromJson(oFolder);
|
||||||
if (!oCacheFolder)
|
if (!oCacheFolder)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (1 == SystemFolders.indexOf(oFolder.FullNameRaw)) {
|
if (1 == type) {
|
||||||
oCacheFolder.type(FolderType.Inbox);
|
oCacheFolder.type(FolderType.Inbox);
|
||||||
Cache.setFolderInboxName(oFolder.FullNameRaw);
|
setFolderInboxName(oFolder.FullName);
|
||||||
}
|
}
|
||||||
Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullNameRaw, oCacheFolder);
|
setFolder(oCacheFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
let type = SystemFolders.indexOf(oFolder.FullNameRaw);
|
|
||||||
if (1 < type) {
|
if (1 < type) {
|
||||||
oCacheFolder.type(type);
|
oCacheFolder.type(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
oCacheFolder.collapsed(!expandedFolders
|
oCacheFolder.collapsed(!expandedFolders
|
||||||
|| !isArray(expandedFolders)
|
|| !isArray(expandedFolders)
|
||||||
|| !expandedFolders.includes(oCacheFolder.fullNameHash));
|
|| !expandedFolders.includes(oCacheFolder.fullName));
|
||||||
|
|
||||||
if (oFolder.Extended) {
|
if (oFolder.Extended) {
|
||||||
if (oFolder.Extended.Hash) {
|
if (oFolder.Extended.Hash) {
|
||||||
Cache.setFolderHash(oCacheFolder.fullNameRaw, oFolder.Extended.Hash);
|
setFolderHash(oCacheFolder.fullName, oFolder.Extended.Hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null != oFolder.Extended.MessageCount) {
|
if (null != oFolder.Extended.MessageCount) {
|
||||||
|
|
@ -108,89 +173,77 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
}
|
}
|
||||||
return oCacheFolder;
|
return oCacheFolder;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let i = result.length;
|
||||||
|
if (i) {
|
||||||
|
sortFolders(result);
|
||||||
|
try {
|
||||||
|
while (i--) {
|
||||||
|
let folder = result[i], parent = getFolderFromCacheList(folder.parentName);
|
||||||
|
if (parent) {
|
||||||
|
parent.subFolders.unshift(folder);
|
||||||
|
result.splice(i,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
storeIt() {
|
storeIt() {
|
||||||
const cnt = pInt(this.CountRec);
|
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
|
||||||
|
|
||||||
FolderUserStore.displaySpecSetting(0 >= cnt
|
if (!(
|
||||||
|| Math.max(10, Math.min(100, pInt(Settings.app('folderSpecLimit')))) < cnt);
|
|
||||||
|
|
||||||
if (SystemFolders &&
|
|
||||||
!('' +
|
|
||||||
SettingsGet('SentFolder') +
|
SettingsGet('SentFolder') +
|
||||||
SettingsGet('DraftFolder') +
|
SettingsGet('DraftsFolder') +
|
||||||
SettingsGet('SpamFolder') +
|
SettingsGet('SpamFolder') +
|
||||||
SettingsGet('TrashFolder') +
|
SettingsGet('TrashFolder') +
|
||||||
SettingsGet('ArchiveFolder'))
|
SettingsGet('ArchiveFolder')
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
FolderUserStore.saveSystemFolders({
|
FolderUserStore.saveSystemFolders(SystemFolders);
|
||||||
SentFolder: SystemFolders[FolderType.SENT] || null,
|
|
||||||
DraftFolder: SystemFolders[FolderType.DRAFTS] || null,
|
|
||||||
SpamFolder: SystemFolders[FolderType.SPAM] || null,
|
|
||||||
TrashFolder: SystemFolders[FolderType.TRASH] || null,
|
|
||||||
ArchiveFolder: SystemFolders[FolderType.ARCHIVE] || null
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderUserStore.folderList(this);
|
FolderUserStore.folderList(this);
|
||||||
|
|
||||||
if (undefined !== this.Namespace) {
|
FolderUserStore.namespace = this.Namespace;
|
||||||
FolderUserStore.namespace = this.Namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
|
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
|
||||||
|
|
||||||
FolderUserStore.folderListOptimized(!!this.Optimized);
|
FolderUserStore.folderListOptimized(!!this.Optimized);
|
||||||
FolderUserStore.sortSupported(!!this.IsSortSupported);
|
FolderUserStore.quotaUsage(this.quotaUsage);
|
||||||
|
FolderUserStore.quotaLimit(this.quotaLimit);
|
||||||
|
FolderUserStore.capabilities(this.Capabilities);
|
||||||
|
|
||||||
FolderUserStore.sentFolder(normalizeFolder(SettingsGet('SentFolder')));
|
FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
|
||||||
FolderUserStore.draftFolder(normalizeFolder(SettingsGet('DraftFolder')));
|
FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
|
||||||
FolderUserStore.spamFolder(normalizeFolder(SettingsGet('SpamFolder')));
|
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
|
||||||
FolderUserStore.trashFolder(normalizeFolder(SettingsGet('TrashFolder')));
|
FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
|
||||||
FolderUserStore.archiveFolder(normalizeFolder(SettingsGet('ArchiveFolder')));
|
FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
|
||||||
|
|
||||||
// FolderUserStore.folderList.valueHasMutated();
|
// FolderUserStore.folderList.valueHasMutated();
|
||||||
|
|
||||||
Local.set(ClientSideKeyName.FoldersLashHash, this.FoldersHash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSystemFolderName(type, def)
|
|
||||||
{
|
|
||||||
switch (type) {
|
|
||||||
case FolderType.Inbox:
|
|
||||||
return i18n('FOLDER_LIST/INBOX_NAME');
|
|
||||||
case FolderType.Sent:
|
|
||||||
return i18n('FOLDER_LIST/SENT_NAME');
|
|
||||||
case FolderType.Drafts:
|
|
||||||
return i18n('FOLDER_LIST/DRAFTS_NAME');
|
|
||||||
case FolderType.Spam:
|
|
||||||
return i18n('GLOBAL/SPAM');
|
|
||||||
case FolderType.Trash:
|
|
||||||
return i18n('FOLDER_LIST/TRASH_NAME');
|
|
||||||
case FolderType.Archive:
|
|
||||||
return i18n('FOLDER_LIST/ARCHIVE_NAME');
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FolderModel extends AbstractModel {
|
export class FolderModel extends AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.fullName = '';
|
this.fullName = '';
|
||||||
this.fullNameRaw = '';
|
|
||||||
this.fullNameHash = '';
|
|
||||||
this.delimiter = '';
|
this.delimiter = '';
|
||||||
this.namespace = '';
|
|
||||||
this.deep = 0;
|
this.deep = 0;
|
||||||
this.expires = 0;
|
this.expires = 0;
|
||||||
|
this.metadata = {};
|
||||||
|
|
||||||
this.exists = true;
|
this.exists = true;
|
||||||
|
|
||||||
|
// this.hash = '';
|
||||||
|
// this.uidNext = 0;
|
||||||
|
|
||||||
this.addObservables({
|
this.addObservables({
|
||||||
name: '',
|
name: '',
|
||||||
type: FolderType.User,
|
type: FolderType.User,
|
||||||
|
|
@ -200,21 +253,36 @@ export class FolderModel extends AbstractModel {
|
||||||
selected: false,
|
selected: false,
|
||||||
edited: false,
|
edited: false,
|
||||||
subscribed: true,
|
subscribed: true,
|
||||||
checkable: false,
|
checkable: false, // Check for new messages
|
||||||
deleteAccess: false,
|
askDelete: false,
|
||||||
|
|
||||||
nameForEdit: '',
|
nameForEdit: '',
|
||||||
|
errorMsg: '',
|
||||||
|
|
||||||
privateMessageCountAll: 0,
|
privateMessageCountAll: 0,
|
||||||
privateMessageCountUnread: 0,
|
privateMessageCountUnread: 0,
|
||||||
|
|
||||||
collapsedPrivate: true
|
kolabType: null,
|
||||||
|
|
||||||
|
collapsed: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addSubscribables({
|
||||||
|
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subFolders = ko.observableArray(new FolderCollectionModel);
|
this.subFolders = ko.observableArray(new FolderCollectionModel);
|
||||||
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
|
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For url safe '/#/mailbox/...' path
|
||||||
|
*/
|
||||||
|
get fullNameHash() {
|
||||||
|
return this.fullName.replace(/[^a-z0-9._-]+/giu, b64EncodeJSONSafe);
|
||||||
|
// return /^[a-z0-9._-]+$/iu.test(this.fullName) ? this.fullName : b64EncodeJSONSafe(this.fullName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @static
|
* @static
|
||||||
* @param {FetchJsonFolder} json
|
* @param {FetchJsonFolder} json
|
||||||
|
|
@ -223,9 +291,19 @@ export class FolderModel extends AbstractModel {
|
||||||
static reviveFromJson(json) {
|
static reviveFromJson(json) {
|
||||||
const folder = super.reviveFromJson(json);
|
const folder = super.reviveFromJson(json);
|
||||||
if (folder) {
|
if (folder) {
|
||||||
folder.deep = json.FullNameRaw.split(folder.delimiter).length - 1;
|
const path = folder.fullName.split(folder.delimiter),
|
||||||
|
type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
|
||||||
|
|| folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
|
||||||
|
|| ''
|
||||||
|
).split('.')[0];
|
||||||
|
|
||||||
folder.messageCountAll = ko.computed({
|
folder.deep = path.length - 1;
|
||||||
|
path.pop();
|
||||||
|
folder.parentName = path.join(folder.delimiter);
|
||||||
|
|
||||||
|
type && 'mail' != type && folder.kolabType(type);
|
||||||
|
|
||||||
|
folder.messageCountAll = koComputable({
|
||||||
read: folder.privateMessageCountAll,
|
read: folder.privateMessageCountAll,
|
||||||
write: (iValue) => {
|
write: (iValue) => {
|
||||||
if (isPosNumeric(iValue)) {
|
if (isPosNumeric(iValue)) {
|
||||||
|
|
@ -237,7 +315,7 @@ export class FolderModel extends AbstractModel {
|
||||||
})
|
})
|
||||||
.extend({ notify: 'always' });
|
.extend({ notify: 'always' });
|
||||||
|
|
||||||
folder.messageCountUnread = ko.computed({
|
folder.messageCountUnread = koComputable({
|
||||||
read: folder.privateMessageCountUnread,
|
read: folder.privateMessageCountUnread,
|
||||||
write: (value) => {
|
write: (value) => {
|
||||||
if (isPosNumeric(value)) {
|
if (isPosNumeric(value)) {
|
||||||
|
|
@ -254,29 +332,45 @@ export class FolderModel extends AbstractModel {
|
||||||
isInbox: () => FolderType.Inbox === folder.type(),
|
isInbox: () => FolderType.Inbox === folder.type(),
|
||||||
|
|
||||||
isFlagged: () => FolderUserStore.currentFolder() === folder
|
isFlagged: () => FolderUserStore.currentFolder() === folder
|
||||||
&& MessageUserStore.listSearch().trim().includes('is:flagged'),
|
&& MessagelistUserStore.listSearch().includes('flagged'),
|
||||||
|
|
||||||
hasSubscribedSubfolders:
|
hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
|
||||||
() => !!folder.subFolders().find(
|
|
||||||
|
hasSubscriptions: () => folder.subscribed() | !!folder.subFolders().find(
|
||||||
oFolder => {
|
oFolder => {
|
||||||
const subscribed = oFolder.hasSubscriptions();
|
const subscribed = oFolder.hasSubscriptions();
|
||||||
return !oFolder.isSystemFolder() && subscribed;
|
return !oFolder.isSystemFolder() && subscribed;
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
hasSubscriptions: () => folder.subscribed() | folder.hasSubscribedSubfolders(),
|
|
||||||
|
|
||||||
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
|
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
|
||||||
|
|
||||||
visible: () => folder.hasSubscriptions() | !SettingsUserStore.hideUnsubscribed(),
|
isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
|
||||||
|
|
||||||
isSystemFolder: () => FolderType.User !== folder.type(),
|
canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
|
||||||
|
|
||||||
hidden: () => {
|
canBeDeleted: () => folder.canBeSelected() && folder.exists,
|
||||||
let hasSubFolders = folder.hasSubscribedSubfolders();
|
|
||||||
return (folder.isSystemFolder() | !folder.selectable()) && !hasSubFolders;
|
canBeSubscribed: () => folder.selectable()
|
||||||
|
&& !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Folder is visible when:
|
||||||
|
* - hasVisibleSubfolders()
|
||||||
|
* Or when all below conditions are true:
|
||||||
|
* - selectable()
|
||||||
|
* - subscribed() OR hideUnsubscribed = false
|
||||||
|
* - FolderType.User
|
||||||
|
* - not kolabType()
|
||||||
|
*/
|
||||||
|
visible: () => {
|
||||||
|
const selectable = folder.canBeSelected(),
|
||||||
|
visible = (folder.subscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
|
||||||
|
return folder.hasVisibleSubfolders() | visible;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hidden: () => !folder.selectable() && (folder.isSystemFolder() | !folder.hasVisibleSubfolders()),
|
||||||
|
|
||||||
printableUnreadCount: () => {
|
printableUnreadCount: () => {
|
||||||
const count = folder.messageCountAll(),
|
const count = folder.messageCountAll(),
|
||||||
unread = folder.messageCountUnread(),
|
unread = folder.messageCountUnread(),
|
||||||
|
|
@ -299,13 +393,6 @@ export class FolderModel extends AbstractModel {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
canBeDeleted: () => !(folder.isSystemFolder() | !folder.selectable()),
|
|
||||||
|
|
||||||
canBeSubscribed: () => Settings.app('useImapSubscribe')
|
|
||||||
&& !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed() | !folder.selectable()),
|
|
||||||
|
|
||||||
canBeSelected: () => !(folder.isSystemFolder() | !folder.selectable()),
|
|
||||||
|
|
||||||
localName: () => {
|
localName: () => {
|
||||||
let name = folder.name();
|
let name = folder.name();
|
||||||
if (folder.isSystemFolder()) {
|
if (folder.isSystemFolder()) {
|
||||||
|
|
@ -318,28 +405,22 @@ export class FolderModel extends AbstractModel {
|
||||||
manageFolderSystemName: () => {
|
manageFolderSystemName: () => {
|
||||||
if (folder.isSystemFolder()) {
|
if (folder.isSystemFolder()) {
|
||||||
translatorTrigger();
|
translatorTrigger();
|
||||||
let suffix = getSystemFolderName(folder.type(), '');
|
let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
|
||||||
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
|
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
|
||||||
return '(' + suffix + ')';
|
return '(' + suffix + ')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
||||||
collapsed: {
|
|
||||||
read: () => folder.isInbox() && FolderUserStore.singleRootFolder()
|
|
||||||
? false
|
|
||||||
: !folder.hidden() && folder.collapsedPrivate(),
|
|
||||||
write: value => folder.collapsedPrivate(value)
|
|
||||||
},
|
|
||||||
|
|
||||||
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
|
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
|
||||||
|
|
||||||
hasSubscribedUnreadMessagesSubfolders: () =>
|
hasSubscribedUnreadMessagesSubfolders: () =>
|
||||||
!!folder.subFolders().find(
|
!!folder.subFolders().find(
|
||||||
folder => folder.hasUnreadMessages() || folder.hasSubscribedUnreadMessagesSubfolders()
|
folder => folder.hasUnreadMessages() | folder.hasSubscribedUnreadMessagesSubfolders()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
|
||||||
});
|
});
|
||||||
|
|
||||||
folder.addSubscribables({
|
folder.addSubscribables({
|
||||||
|
|
@ -349,7 +430,7 @@ export class FolderModel extends AbstractModel {
|
||||||
|
|
||||||
messageCountUnread: unread => {
|
messageCountUnread: unread => {
|
||||||
if (FolderType.Inbox === folder.type()) {
|
if (FolderType.Inbox === folder.type()) {
|
||||||
dispatchEvent(new CustomEvent('mailbox.inbox-unread-count', {detail:unread}));
|
fireEvent('mailbox.inbox-unread-count', unread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -361,16 +442,9 @@ export class FolderModel extends AbstractModel {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
collapsedCss() {
|
collapsedCss() {
|
||||||
return 'e-collapsed-sign ' + (this.hasSubscribedSubfolders()
|
return 'e-collapsed-sign ' + (this.hasVisibleSubfolders()
|
||||||
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
|
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
|
||||||
: 'icon-none'
|
: 'icon-none'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
printableFullName() {
|
|
||||||
return this.fullName.replace(this.delimiter, ' / ');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import ko from 'ko';
|
import { koComputable } from 'External/ko';
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
|
|
@ -21,10 +21,10 @@ export class IdentityModel extends AbstractModel {
|
||||||
signature: '',
|
signature: '',
|
||||||
signatureInsertBefore: false,
|
signatureInsertBefore: false,
|
||||||
|
|
||||||
deleteAccess: false
|
askDelete: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canBeDeleted = ko.computed(() => !!this.id());
|
this.canBeDeleted = koComputable(() => !!this.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,6 +34,6 @@ export class IdentityModel extends AbstractModel {
|
||||||
const name = this.name(),
|
const name = this.name(),
|
||||||
email = this.email();
|
email = this.email();
|
||||||
|
|
||||||
return name ? name + ' (' + email + ')' : email;
|
return name ? name + ' <' + email + '>' : email;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ import ko from 'ko';
|
||||||
import { MessagePriority } from 'Common/EnumsUser';
|
import { MessagePriority } from 'Common/EnumsUser';
|
||||||
import { i18n } from 'Common/Translator';
|
import { i18n } from 'Common/Translator';
|
||||||
|
|
||||||
import { encodeHtml } from 'Common/Html';
|
import { doc } from 'Common/Globals';
|
||||||
import { isArray, arrayLength } from 'Common/Utils';
|
import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html';
|
||||||
|
import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
|
||||||
import { serverRequestRaw } from 'Common/Links';
|
import { serverRequestRaw } from 'Common/Links';
|
||||||
|
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
|
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
|
||||||
|
|
@ -18,13 +19,17 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
import PreviewHTML from 'Html/PreviewMessage.html';
|
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||||
|
|
||||||
const
|
const
|
||||||
SignedVerifyStatus = {
|
// eslint-disable-next-line max-len
|
||||||
UnknownPublicKeys: -4,
|
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
|
||||||
UnknownPrivateKey: -3,
|
// eslint-disable-next-line max-len
|
||||||
Unverified: -2,
|
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,
|
||||||
Error: -1,
|
|
||||||
None: 0,
|
hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
|
||||||
Success: 1
|
getRealHeight = el => {
|
||||||
|
hcont.innerHTML = el.outerHTML;
|
||||||
|
const result = hcont.clientHeight;
|
||||||
|
hcont.innerHTML = '';
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
replyHelper = (emails, unic, localEmails) => {
|
replyHelper = (emails, unic, localEmails) => {
|
||||||
|
|
@ -36,6 +41,8 @@ const
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
doc.body.append(hcont);
|
||||||
|
|
||||||
export class MessageModel extends AbstractModel {
|
export class MessageModel extends AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -44,6 +51,8 @@ export class MessageModel extends AbstractModel {
|
||||||
|
|
||||||
this.addObservables({
|
this.addObservables({
|
||||||
subject: '',
|
subject: '',
|
||||||
|
plain: '',
|
||||||
|
html: '',
|
||||||
size: 0,
|
size: 0,
|
||||||
spamScore: 0,
|
spamScore: 0,
|
||||||
spamResult: '',
|
spamResult: '',
|
||||||
|
|
@ -56,25 +65,20 @@ export class MessageModel extends AbstractModel {
|
||||||
senderClearEmailsString: '',
|
senderClearEmailsString: '',
|
||||||
|
|
||||||
deleted: false,
|
deleted: false,
|
||||||
isDeleted: false,
|
|
||||||
isUnseen: false,
|
|
||||||
isFlagged: false,
|
|
||||||
isAnswered: false,
|
|
||||||
isForwarded: false,
|
|
||||||
isReadReceipt: false,
|
|
||||||
|
|
||||||
focused: false,
|
focused: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
checked: false,
|
checked: false,
|
||||||
hasAttachments: false,
|
|
||||||
|
|
||||||
isHtml: false,
|
isHtml: false,
|
||||||
hasImages: false,
|
hasImages: false,
|
||||||
|
hasExternals: false,
|
||||||
|
|
||||||
isPgpSigned: false,
|
pgpSigned: null,
|
||||||
isPgpEncrypted: false,
|
pgpVerified: null,
|
||||||
pgpSignedVerifyStatus: SignedVerifyStatus.None,
|
|
||||||
pgpSignedVerifyUser: '',
|
pgpEncrypted: null,
|
||||||
|
pgpDecrypted: false,
|
||||||
|
|
||||||
readReceipt: '',
|
readReceipt: '',
|
||||||
|
|
||||||
|
|
@ -83,14 +87,24 @@ export class MessageModel extends AbstractModel {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
||||||
this.attachmentsSpecData = ko.observableArray();
|
|
||||||
this.threads = ko.observableArray();
|
this.threads = ko.observableArray();
|
||||||
this.unsubsribeLinks = ko.observableArray();
|
this.unsubsribeLinks = ko.observableArray();
|
||||||
|
this.flags = ko.observableArray();
|
||||||
|
|
||||||
this.addComputables({
|
this.addComputables({
|
||||||
attachmentIconClass: () => FileInfo.getCombinedIconClass(this.hasAttachments() ? this.attachmentsSpecData() : []),
|
attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()),
|
||||||
threadsLen: () => this.threads().length,
|
threadsLen: () => this.threads().length,
|
||||||
isImportant: () => MessagePriority.High === this.priority(),
|
isImportant: () => MessagePriority.High === this.priority(),
|
||||||
|
hasAttachments: () => this.attachments().hasVisible(),
|
||||||
|
|
||||||
|
isDeleted: () => this.flags().includes('\\deleted'),
|
||||||
|
isUnseen: () => !this.flags().includes('\\seen') /* || this.flags().includes('\\unseen')*/,
|
||||||
|
isFlagged: () => this.flags().includes('\\flagged'),
|
||||||
|
isAnswered: () => this.flags().includes('\\answered'),
|
||||||
|
isForwarded: () => this.flags().includes('$forwarded'),
|
||||||
|
isReadReceipt: () => this.flags().includes('$mdnsent')
|
||||||
|
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
||||||
|
// isPhishing: () => this.flags().includes('$phishing')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,7 +113,6 @@ export class MessageModel extends AbstractModel {
|
||||||
this.uid = 0;
|
this.uid = 0;
|
||||||
this.hash = '';
|
this.hash = '';
|
||||||
this.requestHash = '';
|
this.requestHash = '';
|
||||||
this.externalProxy = false;
|
|
||||||
this.emails = [];
|
this.emails = [];
|
||||||
this.from = new EmailCollectionModel;
|
this.from = new EmailCollectionModel;
|
||||||
this.to = new EmailCollectionModel;
|
this.to = new EmailCollectionModel;
|
||||||
|
|
@ -117,6 +130,8 @@ export class MessageModel extends AbstractModel {
|
||||||
clear() {
|
clear() {
|
||||||
this._reset();
|
this._reset();
|
||||||
this.subject('');
|
this.subject('');
|
||||||
|
this.html('');
|
||||||
|
this.plain('');
|
||||||
this.size(0);
|
this.size(0);
|
||||||
this.spamScore(0);
|
this.spamScore(0);
|
||||||
this.spamResult('');
|
this.spamResult('');
|
||||||
|
|
@ -129,26 +144,20 @@ export class MessageModel extends AbstractModel {
|
||||||
this.senderClearEmailsString('');
|
this.senderClearEmailsString('');
|
||||||
|
|
||||||
this.deleted(false);
|
this.deleted(false);
|
||||||
this.isDeleted(false);
|
|
||||||
this.isUnseen(false);
|
|
||||||
this.isFlagged(false);
|
|
||||||
this.isAnswered(false);
|
|
||||||
this.isForwarded(false);
|
|
||||||
this.isReadReceipt(false);
|
|
||||||
|
|
||||||
this.selected(false);
|
this.selected(false);
|
||||||
this.checked(false);
|
this.checked(false);
|
||||||
this.hasAttachments(false);
|
|
||||||
this.attachmentsSpecData([]);
|
|
||||||
|
|
||||||
this.isHtml(false);
|
this.isHtml(false);
|
||||||
this.hasImages(false);
|
this.hasImages(false);
|
||||||
|
this.hasExternals(false);
|
||||||
this.attachments(new AttachmentCollectionModel);
|
this.attachments(new AttachmentCollectionModel);
|
||||||
|
|
||||||
this.isPgpSigned(false);
|
this.pgpSigned(null);
|
||||||
this.isPgpEncrypted(false);
|
this.pgpVerified(null);
|
||||||
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
|
|
||||||
this.pgpSignedVerifyUser('');
|
this.pgpEncrypted(null);
|
||||||
|
this.pgpDecrypted(false);
|
||||||
|
|
||||||
this.priority(MessagePriority.Normal);
|
this.priority(MessagePriority.Normal);
|
||||||
this.readReceipt('');
|
this.readReceipt('');
|
||||||
|
|
@ -160,6 +169,11 @@ export class MessageModel extends AbstractModel {
|
||||||
this.hasFlaggedSubMessage(false);
|
this.hasFlaggedSubMessage(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spamStatus() {
|
||||||
|
let spam = this.spamResult();
|
||||||
|
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array} properties
|
* @param {Array} properties
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
|
|
@ -178,7 +192,7 @@ export class MessageModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
computeSenderEmail() {
|
computeSenderEmail() {
|
||||||
const list = [FolderUserStore.sentFolder(), FolderUserStore.draftFolder()].includes(this.folder) ? 'to' : 'from';
|
const list = [FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from';
|
||||||
this.senderEmailsString(this[list].toString(true));
|
this.senderEmailsString(this[list].toString(true));
|
||||||
this.senderClearEmailsString(this[list].toStringClear());
|
this.senderClearEmailsString(this[list].toStringClear());
|
||||||
}
|
}
|
||||||
|
|
@ -192,8 +206,8 @@ export class MessageModel extends AbstractModel {
|
||||||
json.Priority = MessagePriority.Normal;
|
json.Priority = MessagePriority.Normal;
|
||||||
}
|
}
|
||||||
if (super.revivePropertiesFromJson(json)) {
|
if (super.revivePropertiesFromJson(json)) {
|
||||||
// this.foundedCIDs = isArray(json.FoundedCIDs) ? json.FoundedCIDs : [];
|
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
||||||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundedCIDs));
|
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
|
||||||
|
|
||||||
this.computeSenderEmail();
|
this.computeSenderEmail();
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +289,7 @@ export class MessageModel extends AbstractModel {
|
||||||
*/
|
*/
|
||||||
lineAsCss() {
|
lineAsCss() {
|
||||||
let classes = [];
|
let classes = [];
|
||||||
Object.entries({
|
forEachObjectEntry({
|
||||||
deleted: this.deleted(),
|
deleted: this.deleted(),
|
||||||
'deleted-mark': this.isDeleted(),
|
'deleted-mark': this.isDeleted(),
|
||||||
selected: this.selected(),
|
selected: this.selected(),
|
||||||
|
|
@ -286,12 +300,12 @@ export class MessageModel extends AbstractModel {
|
||||||
forwarded: this.isForwarded(),
|
forwarded: this.isForwarded(),
|
||||||
focused: this.focused(),
|
focused: this.focused(),
|
||||||
important: this.isImportant(),
|
important: this.isImportant(),
|
||||||
withAttachments: this.hasAttachments(),
|
withAttachments: !!this.attachments().length,
|
||||||
emptySubject: !this.subject(),
|
emptySubject: !this.subject(),
|
||||||
// hasChildrenMessage: 1 < this.threadsLen(),
|
// hasChildrenMessage: 1 < this.threadsLen(),
|
||||||
hasUnseenSubMessage: this.hasUnseenSubMessage(),
|
hasUnseenSubMessage: this.hasUnseenSubMessage(),
|
||||||
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
|
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
|
||||||
}).forEach(([key, value]) => value && classes.push(key));
|
}, (key, value) => value && classes.push(key));
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,35 +378,138 @@ export class MessageModel extends AbstractModel {
|
||||||
return [toResult, ccResult];
|
return [toResult, ccResult];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
viewHtml() {
|
||||||
* @param {boolean=} print = false
|
const body = this.body;
|
||||||
*/
|
if (body && this.html()) {
|
||||||
|
const contentLocationUrls = {},
|
||||||
|
oAttachments = this.attachments();
|
||||||
|
|
||||||
|
// Get contentLocationUrls
|
||||||
|
oAttachments.forEach(oAttachment => {
|
||||||
|
if (oAttachment.cid && oAttachment.contentLocation) {
|
||||||
|
contentLocationUrls[oAttachment.contentId()] = oAttachment.contentLocation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = cleanHtml(this.html(), contentLocationUrls, SettingsUserStore.removeColors());
|
||||||
|
this.hasExternals(result.hasExternals);
|
||||||
|
// this.hasInternals = result.foundCIDs.length || result.foundContentLocationUrls.length;
|
||||||
|
this.hasImages(body.rlHasImages = !!result.hasExternals);
|
||||||
|
|
||||||
|
// Hide valid inline attachments in message view 'attachments' section
|
||||||
|
oAttachments.forEach(oAttachment => {
|
||||||
|
let cid = oAttachment.contentId(),
|
||||||
|
found = result.foundCIDs.includes(cid);
|
||||||
|
oAttachment.isInline(found);
|
||||||
|
oAttachment.isLinked(found || result.foundContentLocationUrls.includes(oAttachment.contentLocation));
|
||||||
|
});
|
||||||
|
|
||||||
|
body.innerHTML = result.html;
|
||||||
|
|
||||||
|
body.classList.toggle('html', 1);
|
||||||
|
body.classList.toggle('plain', 0);
|
||||||
|
|
||||||
|
// showInternalImages
|
||||||
|
const findAttachmentByCid = cid => this.attachments().findByCid(cid);
|
||||||
|
body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
|
||||||
|
const data = el.dataset;
|
||||||
|
if (data.xSrcCid) {
|
||||||
|
const attachment = findAttachmentByCid(data.xSrcCid);
|
||||||
|
if (attachment && attachment.download) {
|
||||||
|
el.src = attachment.linkPreview();
|
||||||
|
}
|
||||||
|
} else if (data.xSrcLocation) {
|
||||||
|
const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
|
||||||
|
|| findAttachmentByCid(data.xSrcLocation);
|
||||||
|
if (attachment && attachment.download) {
|
||||||
|
el.loading = 'lazy';
|
||||||
|
el.src = attachment.linkPreview();
|
||||||
|
}
|
||||||
|
} else if (data.xStyleCid) {
|
||||||
|
forEachObjectEntry(JSON.parse(data.xStyleCid), (name, cid) => {
|
||||||
|
const attachment = findAttachmentByCid(cid);
|
||||||
|
if (attachment && attachment.linkPreview && name) {
|
||||||
|
el.style[name] = "url('" + attachment.linkPreview() + "')";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (SettingsUserStore.showImages()) {
|
||||||
|
this.showExternalImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isHtml(true);
|
||||||
|
this.initView();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPlain() {
|
||||||
|
const body = this.body;
|
||||||
|
if (body && this.plain()) {
|
||||||
|
body.classList.toggle('html', 0);
|
||||||
|
body.classList.toggle('plain', 1);
|
||||||
|
body.innerHTML = plainToHtml(
|
||||||
|
this.plain()
|
||||||
|
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
|
||||||
|
.trim()
|
||||||
|
)
|
||||||
|
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
|
||||||
|
.replace(email, '$1<a href="mailto:$2">$2</a>');
|
||||||
|
this.isHtml(false);
|
||||||
|
this.hasImages(false);
|
||||||
|
this.initView();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initView() {
|
||||||
|
// init BlockquoteSwitcher
|
||||||
|
this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
|
||||||
|
node.removeAttribute('style')
|
||||||
|
if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
|
||||||
|
let h = node.clientHeight || getRealHeight(node);
|
||||||
|
if (0 === h || 100 < h) {
|
||||||
|
const el = Element.fromHTML('<span class="rlBlockquoteSwitcher">•••</span>');
|
||||||
|
node.classList.add('rl-bq-switcher','hidden-bq');
|
||||||
|
node.before(el);
|
||||||
|
el.addEventListener('click', () => node.classList.toggle('hidden-bq'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
viewPopupMessage(print) {
|
viewPopupMessage(print) {
|
||||||
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
||||||
ccLine = this.ccToLine(false),
|
ccLine = this.ccToLine(false),
|
||||||
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
||||||
win = open(''),
|
win = open(''),
|
||||||
doc = win.document;
|
sdoc = win.document;
|
||||||
doc.write(PreviewHTML
|
let subject = encodeHtml(this.subject()),
|
||||||
.replace(/{{subject}}/g, encodeHtml(this.subject()))
|
mode = this.isHtml() ? 'div' : 'pre',
|
||||||
.replace('{{date}}', encodeHtml(m ? m.format('LLL') : ''))
|
cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '',
|
||||||
.replace('{{fromCreds}}', encodeHtml(this.fromToLine(false)))
|
style = getComputedStyle(doc.querySelector('.messageView')),
|
||||||
.replace('{{toCreds}}', encodeHtml(this.toToLine(false)))
|
prop = property => style.getPropertyValue(property);
|
||||||
.replace('{{toLabel}}', encodeHtml(i18n('GLOBAL/TO')))
|
sdoc.write(PreviewHTML
|
||||||
.replace('{{ccHide}}', ccLine ? '' : 'hidden=""')
|
.replace('<title>', '<title>'+subject)
|
||||||
.replace('{{ccCreds}}', encodeHtml(ccLine))
|
// eslint-disable-next-line max-len
|
||||||
.replace('{{ccLabel}}', encodeHtml(i18n('GLOBAL/CC')))
|
.replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL') : '')}</time><div>${encodeHtml(this.fromToLine(false))}</div><div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.toToLine(false))}</div>${cc}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
|
||||||
.replace('{{bodyClass}}', this.isHtml() ? 'html' : 'plain')
|
|
||||||
.replace('{{html}}', this.bodyAsHTML())
|
|
||||||
);
|
);
|
||||||
|
sdoc.close();
|
||||||
doc.close();
|
|
||||||
|
|
||||||
if (print) {
|
if (print) {
|
||||||
setTimeout(() => win.print(), 100);
|
setTimeout(() => win.print(), 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean=} print = false
|
||||||
|
*/
|
||||||
|
popupMessage() {
|
||||||
|
this.viewPopupMessage(false);
|
||||||
|
}
|
||||||
|
|
||||||
printMessage() {
|
printMessage() {
|
||||||
this.viewPopupMessage(true);
|
this.viewPopupMessage(true);
|
||||||
}
|
}
|
||||||
|
|
@ -408,71 +525,61 @@ export class MessageModel extends AbstractModel {
|
||||||
* @param {MessageModel} message
|
* @param {MessageModel} message
|
||||||
* @returns {MessageModel}
|
* @returns {MessageModel}
|
||||||
*/
|
*/
|
||||||
populateByMessageListItem(message) {
|
static fromMessageListItem(message) {
|
||||||
if (message) {
|
let self = new MessageModel();
|
||||||
this.folder = message.folder;
|
|
||||||
this.uid = message.uid;
|
|
||||||
this.hash = message.hash;
|
|
||||||
this.requestHash = message.requestHash;
|
|
||||||
this.subject(message.subject());
|
|
||||||
|
|
||||||
this.size(message.size());
|
|
||||||
this.spamScore(message.spamScore());
|
|
||||||
this.spamResult(message.spamResult());
|
|
||||||
this.isSpam(message.isSpam());
|
|
||||||
this.hasVirus(message.hasVirus());
|
|
||||||
this.dateTimeStampInUTC(message.dateTimeStampInUTC());
|
|
||||||
this.priority(message.priority());
|
|
||||||
|
|
||||||
this.externalProxy = message.externalProxy;
|
|
||||||
|
|
||||||
this.emails = message.emails;
|
|
||||||
|
|
||||||
this.from = message.from;
|
|
||||||
this.to = message.to;
|
|
||||||
this.cc = message.cc;
|
|
||||||
this.bcc = message.bcc;
|
|
||||||
this.replyTo = message.replyTo;
|
|
||||||
this.deliveredTo = message.deliveredTo;
|
|
||||||
this.unsubsribeLinks(message.unsubsribeLinks);
|
|
||||||
|
|
||||||
this.isUnseen(message.isUnseen());
|
|
||||||
this.isFlagged(message.isFlagged());
|
|
||||||
this.isAnswered(message.isAnswered());
|
|
||||||
this.isForwarded(message.isForwarded());
|
|
||||||
this.isReadReceipt(message.isReadReceipt());
|
|
||||||
this.isDeleted(message.isDeleted());
|
|
||||||
|
|
||||||
this.priority(message.priority());
|
|
||||||
|
|
||||||
this.selected(message.selected());
|
|
||||||
this.checked(message.checked());
|
|
||||||
this.hasAttachments(message.hasAttachments());
|
|
||||||
this.attachmentsSpecData(message.attachmentsSpecData());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.body = null;
|
|
||||||
|
|
||||||
this.draftInfo = [];
|
|
||||||
this.messageId = '';
|
|
||||||
this.inReplyTo = '';
|
|
||||||
this.references = '';
|
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
this.threads(message.threads());
|
self.folder = message.folder;
|
||||||
|
self.uid = message.uid;
|
||||||
|
self.hash = message.hash;
|
||||||
|
self.requestHash = message.requestHash;
|
||||||
|
self.subject(message.subject());
|
||||||
|
self.plain(message.plain());
|
||||||
|
self.html(message.html());
|
||||||
|
|
||||||
|
self.size(message.size());
|
||||||
|
self.spamScore(message.spamScore());
|
||||||
|
self.spamResult(message.spamResult());
|
||||||
|
self.isSpam(message.isSpam());
|
||||||
|
self.hasVirus(message.hasVirus());
|
||||||
|
self.dateTimeStampInUTC(message.dateTimeStampInUTC());
|
||||||
|
self.priority(message.priority());
|
||||||
|
|
||||||
|
self.hasExternals(message.hasExternals());
|
||||||
|
|
||||||
|
self.emails = message.emails;
|
||||||
|
|
||||||
|
self.from = message.from;
|
||||||
|
self.to = message.to;
|
||||||
|
self.cc = message.cc;
|
||||||
|
self.bcc = message.bcc;
|
||||||
|
self.replyTo = message.replyTo;
|
||||||
|
self.deliveredTo = message.deliveredTo;
|
||||||
|
self.unsubsribeLinks(message.unsubsribeLinks);
|
||||||
|
|
||||||
|
self.flags(message.flags());
|
||||||
|
|
||||||
|
self.priority(message.priority());
|
||||||
|
|
||||||
|
self.selected(message.selected());
|
||||||
|
self.checked(message.checked());
|
||||||
|
self.attachments(message.attachments());
|
||||||
|
|
||||||
|
self.threads(message.threads());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.computeSenderEmail();
|
self.computeSenderEmail();
|
||||||
|
|
||||||
return this;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
showExternalImages() {
|
showExternalImages() {
|
||||||
if (this.body && this.body.rlHasImages) {
|
const body = this.body;
|
||||||
|
if (body && this.hasImages()) {
|
||||||
this.hasImages(false);
|
this.hasImages(false);
|
||||||
this.body.rlHasImages = false;
|
body.rlHasImages = false;
|
||||||
|
|
||||||
let body = this.body, attr = this.externalProxy ? 'data-x-additional-src' : 'data-x-src';
|
let attr = 'data-x-src';
|
||||||
body.querySelectorAll('[' + attr + ']').forEach(node => {
|
body.querySelectorAll('[' + attr + ']').forEach(node => {
|
||||||
if (node.matches('img')) {
|
if (node.matches('img')) {
|
||||||
node.loading = 'lazy';
|
node.loading = 'lazy';
|
||||||
|
|
@ -480,74 +587,28 @@ export class MessageModel extends AbstractModel {
|
||||||
node.src = node.getAttribute(attr);
|
node.src = node.getAttribute(attr);
|
||||||
});
|
});
|
||||||
|
|
||||||
attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url';
|
body.querySelectorAll('[data-x-style-url]').forEach(node => {
|
||||||
body.querySelectorAll('[' + attr + ']').forEach(node => {
|
forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
|
||||||
node.setAttribute('style', ((node.getAttribute('style')||'')
|
|
||||||
+ ';' + node.getAttribute(attr))
|
|
||||||
.replace(/^[;\s]+/,''));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showInternalImages() {
|
|
||||||
const body = this.body;
|
|
||||||
if (body && !body.rlInitInternalImages) {
|
|
||||||
const findAttachmentByCid = cid => this.attachments().findByCid(cid);
|
|
||||||
|
|
||||||
body.rlInitInternalImages = true;
|
|
||||||
|
|
||||||
body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
|
|
||||||
const data = el.dataset;
|
|
||||||
if (data.xSrcCid) {
|
|
||||||
const attachment = findAttachmentByCid(data.xSrcCid);
|
|
||||||
if (attachment && attachment.download) {
|
|
||||||
el.src = attachment.linkPreview();
|
|
||||||
}
|
|
||||||
} else if (data.xSrcLocation) {
|
|
||||||
const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
|
|
||||||
|| findAttachmentByCid(data.xSrcLocation);
|
|
||||||
if (attachment && attachment.download) {
|
|
||||||
el.loading = 'lazy';
|
|
||||||
el.src = attachment.linkPreview();
|
|
||||||
}
|
|
||||||
} else if (data.xStyleCid) {
|
|
||||||
const name = data.xStyleCidName,
|
|
||||||
attachment = findAttachmentByCid(data.xStyleCid);
|
|
||||||
if (attachment && attachment.linkPreview && name) {
|
|
||||||
el.setAttribute('style', name + ": url('" + attachment.linkPreview() + "');"
|
|
||||||
+ (el.getAttribute('style') || ''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchDataFromDom() {
|
|
||||||
if (this.body) {
|
|
||||||
this.isHtml(!!this.body.rlIsHtml);
|
|
||||||
this.hasImages(!!this.body.rlHasImages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
bodyAsHTML() {
|
bodyAsHTML() {
|
||||||
if (this.body) {
|
// if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) {
|
||||||
let clone = this.body.cloneNode(true),
|
if (this.body && !this.body.querySelector('iframe')) {
|
||||||
attr = 'data-html-editor-font-wrapper';
|
let clone = this.body.cloneNode(true);
|
||||||
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
|
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
|
||||||
node => node.classList.remove('rl-bq-switcher','hidden-bq')
|
node => node.classList.remove('rl-bq-switcher','hidden-bq')
|
||||||
);
|
);
|
||||||
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
|
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
|
||||||
node => node.remove()
|
node => node.remove()
|
||||||
);
|
);
|
||||||
clone.querySelectorAll('['+attr+']').forEach(
|
|
||||||
node => node.removeAttribute(attr)
|
|
||||||
);
|
|
||||||
return clone.innerHTML;
|
return clone.innerHTML;
|
||||||
}
|
}
|
||||||
return '';
|
return this.html() || plainToHtml(this.plain());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -564,4 +625,5 @@ export class MessageModel extends AbstractModel {
|
||||||
this.isReadReceipt()
|
this.isReadReceipt()
|
||||||
].join(',');
|
].join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import ko from 'ko';
|
|
||||||
|
|
||||||
import { arrayLength } from 'Common/Utils';
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
|
||||||
|
|
||||||
export class OpenPgpKeyModel extends AbstractModel {
|
|
||||||
/**
|
|
||||||
* @param {string} index
|
|
||||||
* @param {string} guID
|
|
||||||
* @param {string} ID
|
|
||||||
* @param {array} IDs
|
|
||||||
* @param {array} userIDs
|
|
||||||
* @param {array} emails
|
|
||||||
* @param {boolean} isPrivate
|
|
||||||
* @param {string} armor
|
|
||||||
* @param {string} userID
|
|
||||||
*/
|
|
||||||
constructor(index, guID, ID, IDs, userIDs, emails, isPrivate, armor, userID) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.index = index;
|
|
||||||
this.id = ID;
|
|
||||||
this.ids = arrayLength(IDs) ? IDs : [ID];
|
|
||||||
this.guid = guID;
|
|
||||||
this.user = '';
|
|
||||||
this.users = userIDs;
|
|
||||||
this.email = '';
|
|
||||||
this.emails = emails;
|
|
||||||
this.armor = armor;
|
|
||||||
this.isPrivate = !!isPrivate;
|
|
||||||
|
|
||||||
this.selectUser(userID);
|
|
||||||
|
|
||||||
this.deleteAccess = ko.observable(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
getNativeKey() {
|
|
||||||
let key = null;
|
|
||||||
try {
|
|
||||||
key = PgpUserStore.openpgp.key.readArmored(this.armor);
|
|
||||||
if (key && !key.err && key.keys && key.keys[0]) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNativeKeys() {
|
|
||||||
const key = this.getNativeKey();
|
|
||||||
return key && key.keys ? key.keys : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
select(pattern, property) {
|
|
||||||
if (this[property]) {
|
|
||||||
const index = this[property].indexOf(pattern);
|
|
||||||
if (-1 !== index) {
|
|
||||||
this.user = this.users[index];
|
|
||||||
this.email = this.emails[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectUser(user) {
|
|
||||||
this.select(user, 'users');
|
|
||||||
}
|
|
||||||
|
|
||||||
selectEmail(email) {
|
|
||||||
this.select(email, 'emails');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +1,28 @@
|
||||||
import { Notification } from 'Common/Enums';
|
import { Notification } from 'Common/Enums';
|
||||||
import { isArray, pInt, pString } from 'Common/Utils';
|
import { isArray, pInt, pString } from 'Common/Utils';
|
||||||
import { serverRequest } from 'Common/Links';
|
import { serverRequest } from 'Common/Links';
|
||||||
|
import { getNotification } from 'Common/Translator';
|
||||||
|
|
||||||
let iJsonErrorCount = 0,
|
let iJsonErrorCount = 0;
|
||||||
iTokenErrorCount = 0;
|
|
||||||
|
|
||||||
const getURL = (add = '') => serverRequest('Json') + add,
|
const getURL = (add = '') => serverRequest('Json') + add,
|
||||||
|
|
||||||
checkResponseError = data => {
|
checkResponseError = data => {
|
||||||
const err = data ? data.ErrorCode : null;
|
const err = data ? data.ErrorCode : null;
|
||||||
if (Notification.InvalidToken === err && 10 < ++iTokenErrorCount) {
|
if (Notification.InvalidToken === err) {
|
||||||
|
alert(getNotification(err));
|
||||||
rl.logoutReload();
|
rl.logoutReload();
|
||||||
} else {
|
} else if ([
|
||||||
if ([
|
Notification.AuthError,
|
||||||
Notification.AuthError,
|
Notification.ConnectionError,
|
||||||
Notification.ConnectionError,
|
Notification.DomainNotAllowed,
|
||||||
Notification.DomainNotAllowed,
|
Notification.AccountNotAllowed,
|
||||||
Notification.AccountNotAllowed,
|
Notification.MailServerError,
|
||||||
Notification.MailServerError,
|
Notification.UnknownNotification,
|
||||||
Notification.UnknownNotification,
|
Notification.UnknownError
|
||||||
Notification.UnknownError
|
].includes(err)
|
||||||
].includes(err)
|
) {
|
||||||
) {
|
if (7 < ++iJsonErrorCount) {
|
||||||
++iJsonErrorCount;
|
|
||||||
}
|
|
||||||
if (data.Logout || 7 < iJsonErrorCount) {
|
|
||||||
rl.logoutReload();
|
rl.logoutReload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +76,45 @@ export class AbstractFetchRemote
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows quicker visual responses to the user.
|
||||||
|
* Can be used to stream lines of json encoded data, but does not work on all servers.
|
||||||
|
* Apache needs 'flushpackets' like in <Proxy "fcgi://...." flushpackets=on></Proxy>
|
||||||
|
*/
|
||||||
|
streamPerLine(fCallback, sGetAdd) {
|
||||||
|
rl.fetch(getURL(sGetAdd))
|
||||||
|
.then(response => response.body)
|
||||||
|
.then(body => {
|
||||||
|
// Firefox TextDecoderStream is not defined
|
||||||
|
// const reader = body.pipeThrough(new TextDecoderStream()).getReader();
|
||||||
|
const reader = body.getReader(),
|
||||||
|
re = /\r\n|\n|\r/gm,
|
||||||
|
utf8decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
function processText({ done, value }) {
|
||||||
|
buffer += value ? utf8decoder.decode(value, {stream: true}) : '';
|
||||||
|
for (;;) {
|
||||||
|
let result = re.exec(buffer);
|
||||||
|
if (!result) {
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.read().then(processText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fCallback(buffer.slice(0, result.index));
|
||||||
|
buffer = buffer.slice(result.index + 1);
|
||||||
|
re.lastIndex = 0;
|
||||||
|
}
|
||||||
|
if (buffer.length) {
|
||||||
|
// last line didn't end in a newline char
|
||||||
|
fCallback(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.read().then(processText);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?Function} fCallback
|
* @param {?Function} fCallback
|
||||||
* @param {string} sAction
|
* @param {string} sAction
|
||||||
|
|
@ -86,7 +123,7 @@ export class AbstractFetchRemote
|
||||||
* @param {string=} sGetAdd = ''
|
* @param {string=} sGetAdd = ''
|
||||||
* @param {Array=} aAbortActions = []
|
* @param {Array=} aAbortActions = []
|
||||||
*/
|
*/
|
||||||
defaultRequest(fCallback, sAction, params, iTimeout, sGetAdd, abortActions) {
|
request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
@ -119,7 +156,7 @@ export class AbstractFetchRemote
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
if (data.Result) {
|
if (data.Result) {
|
||||||
iJsonErrorCount = iTokenErrorCount = 0;
|
iJsonErrorCount = 0;
|
||||||
} else {
|
} else {
|
||||||
checkResponseError(data);
|
checkResponseError(data);
|
||||||
iError = data.ErrorCode || Notification.UnknownError
|
iError = data.ErrorCode || Notification.UnknownError
|
||||||
|
|
@ -141,32 +178,11 @@ export class AbstractFetchRemote
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
noop(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'Noop');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?Function} fCallback
|
* @param {?Function} fCallback
|
||||||
*/
|
*/
|
||||||
getPublicKey(fCallback) {
|
getPublicKey(fCallback) {
|
||||||
this.defaultRequest(fCallback, 'GetPublicKey');
|
this.request('GetPublicKey', fCallback);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sVersion
|
|
||||||
*/
|
|
||||||
jsVersion(fCallback, sVersion) {
|
|
||||||
this.defaultRequest(fCallback, 'Version', {
|
|
||||||
Version: sVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fastResolve(mData) {
|
|
||||||
return Promise.resolve(mData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTrigger(trigger, value) {
|
setTrigger(trigger, value) {
|
||||||
|
|
@ -178,7 +194,7 @@ export class AbstractFetchRemote
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
postRequest(action, fTrigger, params, timeOut) {
|
post(action, fTrigger, params, timeOut) {
|
||||||
this.setTrigger(fTrigger, true);
|
this.setTrigger(fTrigger, true);
|
||||||
return fetchJSON(action, '', params, pInt(timeOut, 30000),
|
return fetchJSON(action, '', params, pInt(timeOut, 30000),
|
||||||
data => {
|
data => {
|
||||||
|
|
|
||||||
|
|
@ -1,206 +1,14 @@
|
||||||
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
|
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
|
||||||
|
|
||||||
class RemoteAdminFetch extends AbstractFetchRemote {
|
class RemoteAdminFetch extends AbstractFetchRemote {
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sLogin
|
|
||||||
* @param {string} sPassword
|
|
||||||
*/
|
|
||||||
adminLogin(fCallback, sLogin, sPassword, sCode) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminLogin', {
|
|
||||||
Login: sLogin,
|
|
||||||
Password: sPassword,
|
|
||||||
TOTP: sCode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @param {?scalar} value
|
||||||
* @param {?Function} fCallback
|
* @param {?Function} fCallback
|
||||||
*/
|
*/
|
||||||
adminLogout(fCallback) {
|
saveSetting(key, value, fCallback) {
|
||||||
this.defaultRequest(fCallback, 'AdminLogout');
|
this.request('AdminSettingsUpdate', fCallback, {[key]: value});
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {?} oData
|
|
||||||
*/
|
|
||||||
saveAdminConfig(fCallback, oData) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminSettingsUpdate', oData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {boolean=} bIncludeAliases = true
|
|
||||||
*/
|
|
||||||
domainList(fCallback, bIncludeAliases = true) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminDomainList', {
|
|
||||||
IncludeAliases: bIncludeAliases ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
packagesList(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminPackagesList');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Object} oPackage
|
|
||||||
*/
|
|
||||||
packageInstall(fCallback, oPackage) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'AdminPackageInstall',
|
|
||||||
{
|
|
||||||
Id: oPackage.id,
|
|
||||||
Type: oPackage.type,
|
|
||||||
File: oPackage.file
|
|
||||||
},
|
|
||||||
60000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Object} oPackage
|
|
||||||
*/
|
|
||||||
packageDelete(fCallback, oPackage) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminPackageDelete', {
|
|
||||||
Id: oPackage.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sName
|
|
||||||
*/
|
|
||||||
domain(fCallback, sName) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminDomainLoad', {
|
|
||||||
Name: sName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sId
|
|
||||||
*/
|
|
||||||
plugin(fCallback, sId) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminPluginLoad', {
|
|
||||||
Id: sId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sName
|
|
||||||
*/
|
|
||||||
domainDelete(fCallback, sName) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminDomainDelete', {
|
|
||||||
Name: sName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sName
|
|
||||||
* @param {boolean} bDisabled
|
|
||||||
*/
|
|
||||||
domainDisable(fCallback, sName, bDisabled) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminDomainDisable', {
|
|
||||||
Name: sName,
|
|
||||||
Disabled: bDisabled ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Object} oConfig
|
|
||||||
*/
|
|
||||||
pluginSettingsUpdate(fCallback, oConfig) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminPluginSettingsUpdate', oConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sId
|
|
||||||
* @param {boolean} bDisabled
|
|
||||||
*/
|
|
||||||
pluginDisable(fCallback, sId, bDisabled) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminPluginDisable', {
|
|
||||||
Id: sId,
|
|
||||||
Disabled: bDisabled ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createDomainAlias(fCallback, sName, sAlias) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminDomainAliasSave', {
|
|
||||||
Name: sName,
|
|
||||||
Alias: sAlias
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createOrUpdateDomain(fCallback, oDomain) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminDomainSave', {
|
|
||||||
Create: oDomain.edit() ? 0 : 1,
|
|
||||||
Name: oDomain.name(),
|
|
||||||
|
|
||||||
IncHost: oDomain.imapServer(),
|
|
||||||
IncPort: oDomain.imapPort(),
|
|
||||||
IncSecure: oDomain.imapSecure(),
|
|
||||||
IncShortLogin: oDomain.imapShortLogin() ? 1 : 0,
|
|
||||||
|
|
||||||
UseSieve: oDomain.useSieve() ? 1 : 0,
|
|
||||||
SieveHost: oDomain.sieveServer(),
|
|
||||||
SievePort: oDomain.sievePort(),
|
|
||||||
SieveSecure: oDomain.sieveSecure(),
|
|
||||||
|
|
||||||
OutHost: oDomain.smtpServer(),
|
|
||||||
OutPort: oDomain.smtpPort(),
|
|
||||||
OutSecure: oDomain.smtpSecure(),
|
|
||||||
OutShortLogin: oDomain.smtpShortLogin() ? 1 : 0,
|
|
||||||
OutAuth: oDomain.smtpAuth() ? 1 : 0,
|
|
||||||
OutSetSender: oDomain.smtpSetSender() ? 1 : 0,
|
|
||||||
OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0,
|
|
||||||
|
|
||||||
WhiteList: oDomain.whiteList()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
testConnectionForDomain(fCallback, oDomain) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminDomainTest', {
|
|
||||||
Name: oDomain.name(),
|
|
||||||
IncHost: oDomain.imapServer(),
|
|
||||||
IncPort: oDomain.imapPort(),
|
|
||||||
IncSecure: oDomain.imapSecure(),
|
|
||||||
UseSieve: oDomain.useSieve() ? 1 : 0,
|
|
||||||
SieveHost: oDomain.sieveServer(),
|
|
||||||
SievePort: oDomain.sievePort(),
|
|
||||||
SieveSecure: oDomain.sieveSecure(),
|
|
||||||
OutHost: oDomain.smtpServer(),
|
|
||||||
OutPort: oDomain.smtpPort(),
|
|
||||||
OutSecure: oDomain.smtpSecure(),
|
|
||||||
OutAuth: oDomain.smtpAuth() ? 1 : 0,
|
|
||||||
OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {?} oData
|
|
||||||
*/
|
|
||||||
testContacts(fCallback, oData) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminContactsTest', oData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {?} oData
|
|
||||||
*/
|
|
||||||
saveNewAdminPassword(fCallback, oData) {
|
|
||||||
this.defaultRequest(fCallback, 'AdminPasswordUpdate', oData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { isArray, arrayLength, pString, pInt } from 'Common/Utils';
|
import { pString, pInt, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getFolderHash,
|
getFolderHash,
|
||||||
getFolderInboxName,
|
|
||||||
getFolderUidNext,
|
getFolderUidNext,
|
||||||
getFolderFromCacheList,
|
getFolderFromCacheList
|
||||||
MessageFlagsCache
|
|
||||||
} from 'Common/Cache';
|
} from 'Common/Cache';
|
||||||
|
|
||||||
import { SettingsGet } from 'Common/Globals';
|
import { SettingsGet } from 'Common/Globals';
|
||||||
|
|
@ -17,173 +15,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
|
||||||
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
|
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
|
||||||
|
|
||||||
import { FolderCollectionModel } from 'Model/FolderCollection';
|
|
||||||
|
|
||||||
//const toUTF8 = window.TextEncoder
|
|
||||||
// ? text => String.fromCharCode(...new TextEncoder().encode(text))
|
|
||||||
// : text => unescape(encodeURIComponent(text)),
|
|
||||||
const urlsafeArray = array => btoa(unescape(encodeURIComponent(array.join('\x00').replace(/\r\n/g, '\n'))))
|
|
||||||
.replace('+', '-')
|
|
||||||
.replace('/', '_')
|
|
||||||
.replace('=', '');
|
|
||||||
|
|
||||||
class RemoteUserFetch extends AbstractFetchRemote {
|
class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
folders(fCallback) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'Folders',
|
|
||||||
{
|
|
||||||
SentFolder: SettingsGet('SentFolder'),
|
|
||||||
DraftFolder: SettingsGet('DraftFolder'),
|
|
||||||
SpamFolder: SettingsGet('SpamFolder'),
|
|
||||||
TrashFolder: SettingsGet('TrashFolder'),
|
|
||||||
ArchiveFolder: SettingsGet('ArchiveFolder')
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
['Folders']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {FormData} oData
|
|
||||||
*/
|
|
||||||
login(fCallback, oData) {
|
|
||||||
this.defaultRequest(fCallback, 'Login', oData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
contactsSync(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'ContactsSync', null, 200000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {boolean} bEnable
|
|
||||||
* @param {string} sUrl
|
|
||||||
* @param {string} sUser
|
|
||||||
* @param {string} sPassword
|
|
||||||
*/
|
|
||||||
saveContactsSyncData(fCallback, bEnable, sUrl, sUser, sPassword) {
|
|
||||||
this.defaultRequest(fCallback, 'SaveContactsSyncData', {
|
|
||||||
Enable: bEnable ? 1 : 0,
|
|
||||||
Url: sUrl,
|
|
||||||
User: sUser,
|
|
||||||
Password: sPassword
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sEmail
|
|
||||||
* @param {string} sPassword
|
|
||||||
* @param {boolean=} bNew
|
|
||||||
*/
|
|
||||||
accountSetup(fCallback, sEmail, sPassword, bNew = true) {
|
|
||||||
this.defaultRequest(fCallback, 'AccountSetup', {
|
|
||||||
Email: sEmail,
|
|
||||||
Password: sPassword,
|
|
||||||
New: bNew ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sEmailToDelete
|
|
||||||
*/
|
|
||||||
accountDelete(fCallback, sEmailToDelete) {
|
|
||||||
this.defaultRequest(fCallback, 'AccountDelete', {
|
|
||||||
EmailToDelete: sEmailToDelete
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Array} aAccounts
|
|
||||||
* @param {Array} aIdentities
|
|
||||||
*/
|
|
||||||
accountsAndIdentitiesSortOrder(fCallback, aAccounts, aIdentities) {
|
|
||||||
this.defaultRequest(fCallback, 'AccountsAndIdentitiesSortOrder', {
|
|
||||||
Accounts: aAccounts,
|
|
||||||
Identities: aIdentities
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sId
|
|
||||||
* @param {string} sEmail
|
|
||||||
* @param {string} sName
|
|
||||||
* @param {string} sReplyTo
|
|
||||||
* @param {string} sBcc
|
|
||||||
* @param {string} sSignature
|
|
||||||
* @param {boolean} bSignatureInsertBefore
|
|
||||||
*/
|
|
||||||
identityUpdate(fCallback, sId, sEmail, sName, sReplyTo, sBcc, sSignature, bSignatureInsertBefore) {
|
|
||||||
this.defaultRequest(fCallback, 'IdentityUpdate', {
|
|
||||||
Id: sId,
|
|
||||||
Email: sEmail,
|
|
||||||
Name: sName,
|
|
||||||
ReplyTo: sReplyTo,
|
|
||||||
Bcc: sBcc,
|
|
||||||
Signature: sSignature,
|
|
||||||
SignatureInsertBefore: bSignatureInsertBefore ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sIdToDelete
|
|
||||||
*/
|
|
||||||
identityDelete(fCallback, sIdToDelete) {
|
|
||||||
this.defaultRequest(fCallback, 'IdentityDelete', {
|
|
||||||
IdToDelete: sIdToDelete
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
accountsAndIdentities(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'AccountsAndIdentities');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {SieveScriptModel} script
|
|
||||||
*/
|
|
||||||
filtersScriptSave(fCallback, script) {
|
|
||||||
this.defaultRequest(fCallback, 'FiltersScriptSave', script.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} name
|
|
||||||
*/
|
|
||||||
filtersScriptActivate(fCallback, name) {
|
|
||||||
this.defaultRequest(fCallback, 'FiltersScriptActivate', {name:name});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} name
|
|
||||||
*/
|
|
||||||
filtersScriptDelete(fCallback, name) {
|
|
||||||
this.defaultRequest(fCallback, 'FiltersScriptDelete', {name:name});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
filtersGet(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'Filters', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?Function} fCallback
|
* @param {?Function} fCallback
|
||||||
|
|
@ -233,23 +65,23 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
*/
|
*/
|
||||||
messageList(fCallback, params, bSilent = false) {
|
messageList(fCallback, params, bSilent = false) {
|
||||||
const
|
const
|
||||||
sFolderFullNameRaw = pString(params.Folder),
|
sFolderFullName = pString(params.Folder),
|
||||||
folderHash = getFolderHash(sFolderFullNameRaw),
|
folderHash = getFolderHash(sFolderFullName);
|
||||||
useThreads = AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
|
|
||||||
inboxUidNext = getFolderInboxName() === sFolderFullNameRaw ? getFolderUidNext(sFolderFullNameRaw) : '';
|
|
||||||
|
|
||||||
params.Folder = sFolderFullNameRaw;
|
|
||||||
params.ThreadUid = useThreads ? params.ThreadUid : 0;
|
|
||||||
params = Object.assign({
|
params = Object.assign({
|
||||||
Folder: '',
|
|
||||||
Offset: 0,
|
Offset: 0,
|
||||||
Limit: SettingsUserStore.messagesPerPage(),
|
Limit: SettingsUserStore.messagesPerPage(),
|
||||||
Search: '',
|
Search: '',
|
||||||
UidNext: inboxUidNext,
|
UidNext: getFolderUidNext(sFolderFullName), // Used to check for new messages
|
||||||
UseThreads: useThreads,
|
Sort: FolderUserStore.sortMode(),
|
||||||
ThreadUid: 0,
|
Hash: folderHash + SettingsGet('AccountHash')
|
||||||
Sort: FolderUserStore.sortMode()
|
|
||||||
}, params);
|
}, params);
|
||||||
|
params.Folder = sFolderFullName;
|
||||||
|
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
|
||||||
|
params.UseThreads = 1;
|
||||||
|
} else {
|
||||||
|
params.ThreadUid = 0;
|
||||||
|
}
|
||||||
|
|
||||||
let sGetAdd = '';
|
let sGetAdd = '';
|
||||||
|
|
||||||
|
|
@ -257,13 +89,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
sGetAdd = 'MessageList/' +
|
sGetAdd = 'MessageList/' +
|
||||||
SUB_QUERY_PREFIX +
|
SUB_QUERY_PREFIX +
|
||||||
'/' +
|
'/' +
|
||||||
urlsafeArray([SettingsGet('ProjectHash'),folderHash].concat(Object.values(params)));
|
b64EncodeJSONSafe(params);
|
||||||
params = {};
|
params = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.defaultRequest(
|
this.request('MessageList',
|
||||||
fCallback,
|
fCallback,
|
||||||
'MessageList',
|
|
||||||
params,
|
params,
|
||||||
30000,
|
30000,
|
||||||
sGetAdd,
|
sGetAdd,
|
||||||
|
|
@ -273,43 +104,27 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?Function} fCallback
|
* @param {?Function} fCallback
|
||||||
* @param {Array} aDownloads
|
* @param {string} sFolderFullName
|
||||||
*/
|
|
||||||
messageUploadAttachments(fCallback, aDownloads) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'MessageUploadAttachments',
|
|
||||||
{
|
|
||||||
Attachments: aDownloads
|
|
||||||
},
|
|
||||||
999000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolderFullNameRaw
|
|
||||||
* @param {number} iUid
|
* @param {number} iUid
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
message(fCallback, sFolderFullNameRaw, iUid) {
|
message(fCallback, sFolderFullName, iUid) {
|
||||||
sFolderFullNameRaw = pString(sFolderFullNameRaw);
|
sFolderFullName = pString(sFolderFullName);
|
||||||
iUid = pInt(iUid);
|
iUid = pInt(iUid);
|
||||||
|
|
||||||
if (getFolderFromCacheList(sFolderFullNameRaw) && 0 < iUid) {
|
if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) {
|
||||||
this.defaultRequest(
|
this.request('Message',
|
||||||
fCallback,
|
fCallback,
|
||||||
'Message',
|
|
||||||
{},
|
{},
|
||||||
null,
|
null,
|
||||||
'Message/' +
|
'Message/' +
|
||||||
SUB_QUERY_PREFIX +
|
SUB_QUERY_PREFIX +
|
||||||
'/' +
|
'/' +
|
||||||
urlsafeArray([
|
b64EncodeJSONSafe([
|
||||||
sFolderFullNameRaw,
|
sFolderFullName,
|
||||||
iUid,
|
iUid,
|
||||||
SettingsGet('ProjectHash'),
|
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
|
||||||
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0
|
SettingsGet('AccountHash')
|
||||||
]),
|
]),
|
||||||
['Message']
|
['Message']
|
||||||
);
|
);
|
||||||
|
|
@ -320,186 +135,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Array} aExternals
|
|
||||||
*/
|
|
||||||
composeUploadExternals(fCallback, aExternals) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'ComposeUploadExternals',
|
|
||||||
{
|
|
||||||
Externals: aExternals
|
|
||||||
},
|
|
||||||
999000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sUrl
|
|
||||||
* @param {string} sAccessToken
|
|
||||||
*/
|
|
||||||
composeUploadDrive(fCallback, sUrl, sAccessToken) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'ComposeUploadDrive',
|
|
||||||
{
|
|
||||||
AccessToken: sAccessToken,
|
|
||||||
Url: sUrl
|
|
||||||
},
|
|
||||||
999000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} folder
|
|
||||||
* @param {Array=} list = []
|
|
||||||
*/
|
|
||||||
folderInformation(fCallback, folder, list = []) {
|
|
||||||
let request = true;
|
|
||||||
const uids = [];
|
|
||||||
|
|
||||||
if (arrayLength(list)) {
|
|
||||||
request = false;
|
|
||||||
list.forEach(messageListItem => {
|
|
||||||
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
|
|
||||||
uids.push(messageListItem.uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageListItem.threads.length) {
|
|
||||||
messageListItem.threads.forEach(uid => {
|
|
||||||
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
|
|
||||||
uids.push(uid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uids.length) {
|
|
||||||
request = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request) {
|
|
||||||
this.defaultRequest(fCallback, 'FolderInformation', {
|
|
||||||
Folder: folder,
|
|
||||||
FlagsUids: isArray(uids) ? uids : [],
|
|
||||||
UidNext: getFolderInboxName() === folder ? getFolderUidNext(folder) : 0
|
|
||||||
});
|
|
||||||
} else if (SettingsUserStore.useThreads()) {
|
|
||||||
rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Array} aFolders
|
|
||||||
*/
|
|
||||||
folderInformationMultiply(fCallback, aFolders) {
|
|
||||||
this.defaultRequest(fCallback, 'FolderInformationMultiply', {
|
|
||||||
Folders: aFolders
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
logout(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'Logout');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolderFullNameRaw
|
|
||||||
* @param {Array} aUids
|
|
||||||
* @param {boolean} bSetFlagged
|
|
||||||
*/
|
|
||||||
messageSetFlagged(fCallback, sFolderFullNameRaw, aUids, bSetFlagged) {
|
|
||||||
this.defaultRequest(fCallback, 'MessageSetFlagged', {
|
|
||||||
Folder: sFolderFullNameRaw,
|
|
||||||
Uids: aUids.join(','),
|
|
||||||
SetAction: bSetFlagged ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolderFullNameRaw
|
|
||||||
* @param {Array} aUids
|
|
||||||
* @param {boolean} bSetSeen
|
|
||||||
*/
|
|
||||||
messageSetSeen(fCallback, sFolderFullNameRaw, aUids, bSetSeen) {
|
|
||||||
this.defaultRequest(fCallback, 'MessageSetSeen', {
|
|
||||||
Folder: sFolderFullNameRaw,
|
|
||||||
Uids: aUids.join(','),
|
|
||||||
SetAction: bSetSeen ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolderFullNameRaw
|
|
||||||
* @param {boolean} bSetSeen
|
|
||||||
* @param {Array} aThreadUids = null
|
|
||||||
*/
|
|
||||||
messageSetSeenToAll(fCallback, sFolderFullNameRaw, bSetSeen, aThreadUids = null) {
|
|
||||||
this.defaultRequest(fCallback, 'MessageSetSeenToAll', {
|
|
||||||
Folder: sFolderFullNameRaw,
|
|
||||||
SetAction: bSetSeen ? 1 : 0,
|
|
||||||
ThreadUids: aThreadUids ? aThreadUids.join(',') : ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Object} oData
|
|
||||||
*/
|
|
||||||
saveMessage(fCallback, oData) {
|
|
||||||
this.defaultRequest(fCallback, 'SaveMessage', oData, 200000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sMessageFolder
|
|
||||||
* @param {number} iMessageUid
|
|
||||||
* @param {string} sReadReceipt
|
|
||||||
* @param {string} sSubject
|
|
||||||
* @param {string} sText
|
|
||||||
*/
|
|
||||||
sendReadReceiptMessage(fCallback, sMessageFolder, iMessageUid, sReadReceipt, sSubject, sText) {
|
|
||||||
this.defaultRequest(fCallback, 'SendReadReceiptMessage', {
|
|
||||||
MessageFolder: sMessageFolder,
|
|
||||||
MessageUid: iMessageUid,
|
|
||||||
ReadReceipt: sReadReceipt,
|
|
||||||
Subject: sSubject,
|
|
||||||
Text: sText
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Object} oData
|
|
||||||
*/
|
|
||||||
sendMessage(fCallback, oData) {
|
|
||||||
this.defaultRequest(fCallback, 'SendMessage', oData, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Object} oData
|
|
||||||
*/
|
|
||||||
saveSystemFolders(fCallback, oData) {
|
|
||||||
this.defaultRequest(fCallback, 'SystemFoldersUpdate', oData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?Function} fCallback
|
* @param {?Function} fCallback
|
||||||
* @param {Object} oData
|
* @param {Object} oData
|
||||||
*/
|
*/
|
||||||
saveSettings(fCallback, oData) {
|
saveSettings(fCallback, oData) {
|
||||||
this.defaultRequest(fCallback, 'SettingsUpdate', oData);
|
this.request('SettingsUpdate', fCallback, oData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -513,234 +154,15 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* @param {?Function} fCallback
|
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
|
||||||
* @param {string} sFolderFullNameRaw
|
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
|
||||||
*/
|
Folder: sPrevFolderFullName,
|
||||||
folderClear(fCallback, sFolderFullNameRaw) {
|
NewFolder: sNewFolderFullName,
|
||||||
this.defaultRequest(fCallback, 'FolderClear', {
|
|
||||||
Folder: sFolderFullNameRaw
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolderFullNameRaw
|
|
||||||
* @param {boolean} bSubscribe
|
|
||||||
*/
|
|
||||||
folderSetSubscribe(fCallback, sFolderFullNameRaw, bSubscribe) {
|
|
||||||
this.defaultRequest(fCallback, 'FolderSubscribe', {
|
|
||||||
Folder: sFolderFullNameRaw,
|
|
||||||
Subscribe: bSubscribe ? 1 : 0
|
Subscribe: bSubscribe ? 1 : 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolderFullNameRaw
|
|
||||||
* @param {boolean} bCheckable
|
|
||||||
*/
|
|
||||||
folderSetCheckable(fCallback, sFolderFullNameRaw, bCheckable) {
|
|
||||||
this.defaultRequest(fCallback, 'FolderCheckable', {
|
|
||||||
Folder: sFolderFullNameRaw,
|
|
||||||
Checkable: bCheckable ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolder
|
|
||||||
* @param {string} sToFolder
|
|
||||||
* @param {Array} aUids
|
|
||||||
* @param {string=} sLearning
|
|
||||||
* @param {boolean=} bMarkAsRead
|
|
||||||
*/
|
|
||||||
messagesMove(fCallback, sFolder, sToFolder, aUids, sLearning, bMarkAsRead) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'MessageMove',
|
|
||||||
{
|
|
||||||
FromFolder: sFolder,
|
|
||||||
ToFolder: sToFolder,
|
|
||||||
Uids: aUids.join(','),
|
|
||||||
MarkAsRead: bMarkAsRead ? 1 : 0,
|
|
||||||
Learning: sLearning || ''
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
['MessageList']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolder
|
|
||||||
* @param {string} sToFolder
|
|
||||||
* @param {Array} aUids
|
|
||||||
*/
|
|
||||||
messagesCopy(fCallback, sFolder, sToFolder, aUids) {
|
|
||||||
this.defaultRequest(fCallback, 'MessageCopy', {
|
|
||||||
FromFolder: sFolder,
|
|
||||||
ToFolder: sToFolder,
|
|
||||||
Uids: aUids.join(',')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sFolder
|
|
||||||
* @param {Array} aUids
|
|
||||||
*/
|
|
||||||
messagesDelete(fCallback, sFolder, aUids) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'MessageDelete',
|
|
||||||
{
|
|
||||||
Folder: sFolder,
|
|
||||||
Uids: aUids.join(',')
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
['MessageList']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
appDelayStart(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'AppDelayStart');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
quota(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'Quota');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {number} iOffset
|
|
||||||
* @param {number} iLimit
|
|
||||||
* @param {string} sSearch
|
|
||||||
*/
|
|
||||||
contacts(fCallback, iOffset, iLimit, sSearch) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'Contacts',
|
|
||||||
{
|
|
||||||
Offset: iOffset,
|
|
||||||
Limit: iLimit,
|
|
||||||
Search: sSearch
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
['Contacts']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sRequestUid
|
|
||||||
* @param {string} sUid
|
|
||||||
* @param {Array} aProperties
|
|
||||||
*/
|
|
||||||
contactSave(fCallback, sRequestUid, sUid, aProperties) {
|
|
||||||
this.defaultRequest(fCallback, 'ContactSave', {
|
|
||||||
RequestUid: sRequestUid,
|
|
||||||
Uid: sUid,
|
|
||||||
Properties: aProperties
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {Array} aUids
|
|
||||||
*/
|
|
||||||
contactsDelete(fCallback, aUids) {
|
|
||||||
this.defaultRequest(fCallback, 'ContactsDelete', {
|
|
||||||
Uids: aUids.join(',')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} sQuery
|
|
||||||
* @param {number} iPage
|
|
||||||
*/
|
|
||||||
suggestions(fCallback, sQuery, iPage) {
|
|
||||||
this.defaultRequest(
|
|
||||||
fCallback,
|
|
||||||
'Suggestions',
|
|
||||||
{
|
|
||||||
Query: sQuery,
|
|
||||||
Page: iPage
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
['Suggestions']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
clearUserBackground(fCallback) {
|
|
||||||
this.defaultRequest(fCallback, 'ClearUserBackground');
|
|
||||||
}
|
|
||||||
|
|
||||||
foldersReload(fCallback) {
|
|
||||||
this.abort('Folders')
|
|
||||||
.postRequest('Folders', FolderUserStore.foldersLoading)
|
|
||||||
.then(data => {
|
|
||||||
data = FolderCollectionModel.reviveFromJson(data.Result);
|
|
||||||
data && data.storeIt();
|
|
||||||
fCallback && fCallback(true);
|
|
||||||
})
|
|
||||||
.catch(() => fCallback && setTimeout(() => fCallback(false), 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
foldersReloadWithTimeout() {
|
|
||||||
this.setTrigger(FolderUserStore.foldersLoading, true);
|
|
||||||
|
|
||||||
clearTimeout(this.foldersTimeout);
|
|
||||||
this.foldersTimeout = setTimeout(() => this.foldersReload(), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
folderDelete(sFolderFullNameRaw) {
|
|
||||||
return this.postRequest('FolderDelete', FolderUserStore.foldersDeleting, {
|
|
||||||
Folder: sFolderFullNameRaw
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
folderCreate(sNewFolderName, sParentName) {
|
|
||||||
return this.postRequest('FolderCreate', FolderUserStore.foldersCreating, {
|
|
||||||
Folder: sNewFolderName,
|
|
||||||
Parent: sParentName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
folderMove(sPrevFolderFullNameRaw, sNewFolderFullName) {
|
|
||||||
return this.postRequest('FolderMove', FolderUserStore.foldersRenaming, {
|
|
||||||
Folder: sPrevFolderFullNameRaw,
|
|
||||||
NewFolder: sNewFolderFullName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
folderRename(sPrevFolderFullNameRaw, sNewFolderName) {
|
|
||||||
return this.postRequest('FolderRename', FolderUserStore.foldersRenaming, {
|
|
||||||
Folder: sPrevFolderFullNameRaw,
|
|
||||||
NewFolderName: sNewFolderName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentsActions(sAction, aHashes, fTrigger) {
|
|
||||||
return this.postRequest('AttachmentsActions', fTrigger, {
|
|
||||||
Do: sAction,
|
|
||||||
Hashes: aHashes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new RemoteUserFetch();
|
export default new RemoteUserFetch();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import ko from 'ko';
|
||||||
|
|
||||||
import { pString } from 'Common/Utils';
|
import { pString } from 'Common/Utils';
|
||||||
import { settings } from 'Common/Links';
|
import { settings } from 'Common/Links';
|
||||||
import { doc, elementById } from 'Common/Globals';
|
import { createElement, elementById } from 'Common/Globals';
|
||||||
|
|
||||||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||||
|
|
||||||
|
|
@ -18,52 +18,38 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
this.menu = ko.observableArray();
|
this.menu = ko.observableArray();
|
||||||
|
|
||||||
this.oCurrentSubScreen = null;
|
this.oCurrentSubScreen = null;
|
||||||
|
|
||||||
this.setupSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function=} fCallback
|
|
||||||
*/
|
|
||||||
setupSettings(fCallback = null) {
|
|
||||||
fCallback && fCallback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoute(subName) {
|
onRoute(subName) {
|
||||||
let settingsScreen = null,
|
let settingsScreen = null,
|
||||||
RoutedSettingsViewModel = null,
|
viewModelDom = null,
|
||||||
viewModelDom = null;
|
RoutedSettingsViewModel = VIEW_MODELS.find(
|
||||||
|
SettingsViewModel => subName === SettingsViewModel.__rlSettingsData.route
|
||||||
RoutedSettingsViewModel = VIEW_MODELS.find(
|
);
|
||||||
SettingsViewModel =>
|
|
||||||
SettingsViewModel && SettingsViewModel.__rlSettingsData && subName === SettingsViewModel.__rlSettingsData.Route
|
|
||||||
);
|
|
||||||
|
|
||||||
if (RoutedSettingsViewModel) {
|
if (RoutedSettingsViewModel) {
|
||||||
if (RoutedSettingsViewModel.__builded && RoutedSettingsViewModel.__vm) {
|
if (RoutedSettingsViewModel.__vm) {
|
||||||
settingsScreen = RoutedSettingsViewModel.__vm;
|
settingsScreen = RoutedSettingsViewModel.__vm;
|
||||||
} else {
|
} else {
|
||||||
const vmPlace = elementById('rl-settings-subscreen');
|
const vmPlace = elementById('rl-settings-subscreen');
|
||||||
if (vmPlace) {
|
if (vmPlace) {
|
||||||
settingsScreen = new RoutedSettingsViewModel();
|
viewModelDom = createElement('div',{
|
||||||
|
id: 'V-Settings-' + RoutedSettingsViewModel.name.replace(/(User|Admin)Settings/,''),
|
||||||
viewModelDom = Element.fromHTML('<div class="rl-settings-view-model" hidden=""></div>');
|
hidden: ''
|
||||||
|
})
|
||||||
vmPlace.append(viewModelDom);
|
vmPlace.append(viewModelDom);
|
||||||
|
|
||||||
|
settingsScreen = new RoutedSettingsViewModel();
|
||||||
settingsScreen.viewModelDom = viewModelDom;
|
settingsScreen.viewModelDom = viewModelDom;
|
||||||
|
|
||||||
settingsScreen.__rlSettingsData = RoutedSettingsViewModel.__rlSettingsData;
|
|
||||||
|
|
||||||
RoutedSettingsViewModel.__dom = viewModelDom;
|
RoutedSettingsViewModel.__dom = viewModelDom;
|
||||||
RoutedSettingsViewModel.__builded = true;
|
|
||||||
RoutedSettingsViewModel.__vm = settingsScreen;
|
RoutedSettingsViewModel.__vm = settingsScreen;
|
||||||
|
|
||||||
const tmpl = { name: RoutedSettingsViewModel.__rlSettingsData.Template };
|
|
||||||
ko.applyBindingAccessorsToNode(
|
ko.applyBindingAccessorsToNode(
|
||||||
viewModelDom,
|
viewModelDom,
|
||||||
{
|
{
|
||||||
i18nInit: true,
|
i18nInit: true,
|
||||||
template: () => tmpl
|
template: () => ({ name: RoutedSettingsViewModel.__rlSettingsData.template })
|
||||||
},
|
},
|
||||||
settingsScreen
|
settingsScreen
|
||||||
);
|
);
|
||||||
|
|
@ -75,74 +61,55 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsScreen) {
|
if (settingsScreen) {
|
||||||
const o = this;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// hide
|
// hide
|
||||||
if (o.oCurrentSubScreen) {
|
this.onHide();
|
||||||
o.oCurrentSubScreen.onHide && o.oCurrentSubScreen.onHide();
|
|
||||||
o.oCurrentSubScreen.viewModelDom.hidden = true;
|
|
||||||
}
|
|
||||||
// --
|
// --
|
||||||
|
|
||||||
o.oCurrentSubScreen = settingsScreen;
|
this.oCurrentSubScreen = settingsScreen;
|
||||||
|
|
||||||
// show
|
// show
|
||||||
if (o.oCurrentSubScreen) {
|
settingsScreen.beforeShow && settingsScreen.beforeShow();
|
||||||
o.oCurrentSubScreen.onBeforeShow && o.oCurrentSubScreen.onBeforeShow();
|
settingsScreen.viewModelDom.hidden = false;
|
||||||
o.oCurrentSubScreen.viewModelDom.hidden = false;
|
settingsScreen.onShow && settingsScreen.onShow();
|
||||||
o.oCurrentSubScreen.onShow && o.oCurrentSubScreen.onShow();
|
|
||||||
|
|
||||||
o.menu.forEach(item => {
|
this.menu.forEach(item => {
|
||||||
item.selected(
|
item.selected(
|
||||||
settingsScreen &&
|
item.route === RoutedSettingsViewModel.__rlSettingsData.route
|
||||||
settingsScreen.__rlSettingsData &&
|
);
|
||||||
item.route === settingsScreen.__rlSettingsData.Route
|
});
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
doc.querySelector('#rl-content .b-settings .b-content').scrollTop = 0;
|
(elementById('rl-settings-subscreen') || {}).scrollTop = 0;
|
||||||
}
|
|
||||||
// --
|
// --
|
||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rl.route.setHash(settings(), false, true);
|
hasher.replaceHash(settings());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onHide() {
|
onHide() {
|
||||||
if (this.oCurrentSubScreen && this.oCurrentSubScreen.viewModelDom) {
|
let subScreen = this.oCurrentSubScreen;
|
||||||
this.oCurrentSubScreen.onHide && this.oCurrentSubScreen.onHide();
|
if (subScreen) {
|
||||||
this.oCurrentSubScreen.viewModelDom.hidden = true;
|
subScreen.onHide && subScreen.onHide();
|
||||||
|
subScreen.viewModelDom.hidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild() {
|
onBuild() {
|
||||||
VIEW_MODELS.forEach(SettingsViewModel => {
|
VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel.__rlSettingsData));
|
||||||
if (
|
|
||||||
SettingsViewModel &&
|
|
||||||
SettingsViewModel.__rlSettingsData
|
|
||||||
) {
|
|
||||||
this.menu.push({
|
|
||||||
route: SettingsViewModel.__rlSettingsData.Route,
|
|
||||||
label: SettingsViewModel.__rlSettingsData.Label,
|
|
||||||
selected: ko.observable(false)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
routes() {
|
routes() {
|
||||||
const DefaultViewModel = VIEW_MODELS.find(
|
const DefaultViewModel = VIEW_MODELS.find(
|
||||||
SettingsViewModel =>
|
SettingsViewModel => SettingsViewModel.__rlSettingsData.isDefault
|
||||||
SettingsViewModel && SettingsViewModel.__rlSettingsData && SettingsViewModel.__rlSettingsData.IsDefault
|
|
||||||
),
|
),
|
||||||
defaultRoute =
|
defaultRoute =
|
||||||
DefaultViewModel && DefaultViewModel.__rlSettingsData ? DefaultViewModel.__rlSettingsData.Route : 'general',
|
DefaultViewModel ? DefaultViewModel.__rlSettingsData.route : 'general',
|
||||||
rules = {
|
rules = {
|
||||||
subname: /^(.*)$/,
|
subname: /^(.*)$/,
|
||||||
normalize_: (rquest, vals) => {
|
normalize_: (rquest, vals) => {
|
||||||
vals.subname = undefined === vals.subname ? defaultRoute : pString(vals.subname);
|
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
|
||||||
return [vals.subname];
|
return [vals.subname];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -164,11 +131,13 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
|
export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
|
||||||
|
let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
|
||||||
SettingsViewModelClass.__rlSettingsData = {
|
SettingsViewModelClass.__rlSettingsData = {
|
||||||
Label: labelName,
|
label: labelName || 'SETTINGS_LABELS/LABEL_' + name.toUpperCase() + '_NAME',
|
||||||
Template: template,
|
route: route || name.toLowerCase(),
|
||||||
Route: route,
|
selected: ko.observable(false),
|
||||||
IsDefault: !!isDefault
|
template: template || SettingsViewModelClass.name,
|
||||||
|
isDefault: !!isDefault
|
||||||
};
|
};
|
||||||
|
|
||||||
VIEW_MODELS.push(SettingsViewModelClass);
|
VIEW_MODELS.push(SettingsViewModelClass);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||||
|
|
||||||
import { LoginAdminView } from 'View/Admin/Login';
|
import { AdminLoginView } from 'View/Admin/Login';
|
||||||
|
|
||||||
export class LoginAdminScreen extends AbstractScreen {
|
export class LoginAdminScreen extends AbstractScreen {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('login', [LoginAdminView]);
|
super('login', [AdminLoginView]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@ import { runSettingsViewModelHooks } from 'Common/Plugins';
|
||||||
|
|
||||||
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||||
|
|
||||||
import { GeneralAdminSettings } from 'Settings/Admin/General';
|
import { AdminSettingsGeneral } from 'Settings/Admin/General';
|
||||||
import { DomainsAdminSettings } from 'Settings/Admin/Domains';
|
import { AdminSettingsDomains } from 'Settings/Admin/Domains';
|
||||||
import { LoginAdminSettings } from 'Settings/Admin/Login';
|
import { AdminSettingsLogin } from 'Settings/Admin/Login';
|
||||||
import { ContactsAdminSettings } from 'Settings/Admin/Contacts';
|
import { AdminSettingsContacts } from 'Settings/Admin/Contacts';
|
||||||
import { SecurityAdminSettings } from 'Settings/Admin/Security';
|
import { AdminSettingsSecurity } from 'Settings/Admin/Security';
|
||||||
import { PackagesAdminSettings } from 'Settings/Admin/Packages';
|
import { AdminSettingsPackages } from 'Settings/Admin/Packages';
|
||||||
import { AboutAdminSettings } from 'Settings/Admin/About';
|
import { AdminSettingsAbout } from 'Settings/Admin/About';
|
||||||
import { BrandingAdminSettings } from 'Settings/Admin/Branding';
|
import { AdminSettingsBranding } from 'Settings/Admin/Branding';
|
||||||
|
import { AdminSettingsConfig } from 'Settings/Admin/Config';
|
||||||
|
|
||||||
import { MenuSettingsAdminView } from 'View/Admin/Settings/Menu';
|
import { MenuSettingsAdminView } from 'View/Admin/Settings/Menu';
|
||||||
import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
|
import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
|
||||||
|
|
@ -17,40 +18,22 @@ import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
|
||||||
export class SettingsAdminScreen extends AbstractSettingsScreen {
|
export class SettingsAdminScreen extends AbstractSettingsScreen {
|
||||||
constructor() {
|
constructor() {
|
||||||
super([MenuSettingsAdminView, PaneSettingsAdminView]);
|
super([MenuSettingsAdminView, PaneSettingsAdminView]);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function=} fCallback = null
|
|
||||||
*/
|
|
||||||
setupSettings(fCallback = null) {
|
|
||||||
settingsAddViewModel(
|
|
||||||
GeneralAdminSettings,
|
|
||||||
'AdminSettingsGeneral',
|
|
||||||
'TABS_LABELS/LABEL_GENERAL_NAME',
|
|
||||||
'general',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
[
|
[
|
||||||
[DomainsAdminSettings, 'Domains'],
|
AdminSettingsGeneral,
|
||||||
[LoginAdminSettings, 'Login'],
|
AdminSettingsDomains,
|
||||||
[BrandingAdminSettings, 'Branding'],
|
AdminSettingsLogin,
|
||||||
[ContactsAdminSettings, 'Contacts'],
|
AdminSettingsBranding,
|
||||||
[SecurityAdminSettings, 'Security'],
|
AdminSettingsContacts,
|
||||||
[PackagesAdminSettings, 'Packages'],
|
AdminSettingsSecurity,
|
||||||
[AboutAdminSettings, 'About'],
|
AdminSettingsPackages,
|
||||||
].forEach(item =>
|
AdminSettingsConfig,
|
||||||
settingsAddViewModel(
|
AdminSettingsAbout
|
||||||
item[0],
|
].forEach((item, index) =>
|
||||||
'AdminSettings'+item[1],
|
settingsAddViewModel(item, 0, 0, 0, 0 === index)
|
||||||
'TABS_LABELS/LABEL_'+item[1].toUpperCase()+'_NAME',
|
|
||||||
item[1].toLowerCase()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
runSettingsViewModelHooks(true);
|
runSettingsViewModelHooks(true);
|
||||||
|
|
||||||
fCallback && fCallback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,32 @@
|
||||||
import { Scope } from 'Common/Enums';
|
import { Scope } from 'Common/Enums';
|
||||||
import { doc, leftPanelDisabled, moveAction, Settings } from 'Common/Globals';
|
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
|
||||||
|
import { doc, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals';
|
||||||
import { pString, pInt } from 'Common/Utils';
|
import { pString, pInt } from 'Common/Utils';
|
||||||
import { getFolderFromCacheList, getFolderFullNameRaw, getFolderInboxName } from 'Common/Cache';
|
import { setLayoutResizer } from 'Common/UtilsUser';
|
||||||
|
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
|
||||||
import { i18n } from 'Common/Translator';
|
import { i18n } from 'Common/Translator';
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import { AppUserStore } from 'Stores/User/App';
|
import { AppUserStore } from 'Stores/User/App';
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
import { SystemDropDownMailBoxUserView } from 'View/User/MailBox/SystemDropDown';
|
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
|
||||||
import { FolderListMailBoxUserView } from 'View/User/MailBox/FolderList';
|
import { MailFolderList } from 'View/User/MailBox/FolderList';
|
||||||
import { MessageListMailBoxUserView } from 'View/User/MailBox/MessageList';
|
import { MailMessageList } from 'View/User/MailBox/MessageList';
|
||||||
import { MessageViewMailBoxUserView } from 'View/User/MailBox/MessageView';
|
import { MailMessageView } from 'View/User/MailBox/MessageView';
|
||||||
|
|
||||||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||||
|
|
||||||
export class MailBoxUserScreen extends AbstractScreen {
|
export class MailBoxUserScreen extends AbstractScreen {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('mailbox', [
|
super('mailbox', [
|
||||||
SystemDropDownMailBoxUserView,
|
SystemDropDownUserView,
|
||||||
FolderListMailBoxUserView,
|
MailFolderList,
|
||||||
MessageListMailBoxUserView,
|
MailMessageList,
|
||||||
MessageViewMailBoxUserView
|
MailMessageView
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +51,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
onShow() {
|
onShow() {
|
||||||
this.updateWindowTitle();
|
this.updateWindowTitle();
|
||||||
|
|
||||||
AppUserStore.focusedState(Scope.None);
|
AppUserStore.focusedState('none');
|
||||||
AppUserStore.focusedState(Scope.MessageList);
|
AppUserStore.focusedState(Scope.MessageList);
|
||||||
|
|
||||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||||
|
|
@ -61,17 +64,17 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onRoute(folderHash, page, search) {
|
onRoute(folderHash, page, search) {
|
||||||
const folder = getFolderFromCacheList(getFolderFullNameRaw(folderHash.replace(/~([\d]+)$/, '')));
|
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
|
||||||
if (folder) {
|
if (folder) {
|
||||||
let threadUid = folderHash.replace(/^.+~(\d+)$/, '$1');
|
let threadUid = folderHash.replace(/^.+~(\d+)$/, '$1');
|
||||||
|
|
||||||
FolderUserStore.currentFolder(folder);
|
FolderUserStore.currentFolder(folder);
|
||||||
|
|
||||||
MessageUserStore.listPage(1 > page ? 1 : page);
|
MessagelistUserStore.page(1 > page ? 1 : page);
|
||||||
MessageUserStore.listSearch(search);
|
MessagelistUserStore.listSearch(search);
|
||||||
MessageUserStore.listThreadUid((folderHash === threadUid) ? 0 : pInt(threadUid));
|
MessagelistUserStore.threadUid((folderHash === threadUid) ? 0 : pInt(threadUid));
|
||||||
|
|
||||||
rl.app.reloadMessageList();
|
MessagelistUserStore.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,27 +82,41 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onStart() {
|
onStart() {
|
||||||
if (!this.__started) {
|
super.onStart();
|
||||||
super.onStart();
|
|
||||||
|
|
||||||
addEventListener('mailbox.inbox-unread-count', e => {
|
addEventListener('mailbox.inbox-unread-count', e => {
|
||||||
FolderUserStore.foldersInboxUnreadCount(e.detail);
|
FolderUserStore.foldersInboxUnreadCount(e.detail);
|
||||||
|
/* // Disabled in SystemDropDown.html
|
||||||
const email = AccountUserStore.email();
|
const email = AccountUserStore.email();
|
||||||
AccountUserStore.accounts.forEach(item =>
|
AccountUserStore.accounts.forEach(item =>
|
||||||
item && email === item.email && item.count(e.detail)
|
item && email === item.email && item.count(e.detail)
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
this.updateWindowTitle();
|
this.updateWindowTitle();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onBuild() {
|
onBuild() {
|
||||||
setTimeout(() => rl.app.initHorizontalLayoutResizer(), 1);
|
setTimeout(() => {
|
||||||
|
// initMailboxLayoutResizer
|
||||||
|
const top = elementById('V-MailMessageList'),
|
||||||
|
bottom = elementById('V-MailMessageView'),
|
||||||
|
fToggle = () => {
|
||||||
|
let layout = SettingsUserStore.layout();
|
||||||
|
setLayoutResizer(top, bottom, ClientSideKeyNameMessageListSize,
|
||||||
|
(ThemeStore.isMobile() || Layout.NoPreview === layout)
|
||||||
|
? 0
|
||||||
|
: (Layout.SidePreview === layout ? 'Width' : 'Height')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if (top && bottom) {
|
||||||
|
fToggle();
|
||||||
|
addEventListener('rl-layout', fToggle);
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
|
||||||
doc.addEventListener('click', event =>
|
doc.addEventListener('click', event =>
|
||||||
event.target.closest('#rl-right') && moveAction(false)
|
event.target.closest('#rl-right') && moveAction(false)
|
||||||
|
|
@ -107,19 +124,23 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Parse link as generated by mailBox()
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
routes() {
|
routes() {
|
||||||
const
|
const
|
||||||
folder = (request, vals) => request ? decodeURI(pString(vals[0])) : getFolderInboxName(),
|
folder = (request, vals) => request ? pString(vals[0]) : getFolderInboxName(),
|
||||||
fNormS = (request, vals) => [folder(request, vals), request ? pInt(vals[1]) : 1, decodeURI(pString(vals[2]))];
|
fNormS = (request, vals) => [folder(request, vals), request ? pInt(vals[1]) : 1, decodeURI(pString(vals[2]))];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
// Folder: INBOX | INBOX.sub | Sent | fullNameHash
|
||||||
[/^([^/]*)$/, { normalize_: fNormS }],
|
[/^([^/]*)$/, { normalize_: fNormS }],
|
||||||
[/^([a-zA-Z0-9~]+)\/(.+)\/?$/, { normalize_: (request, vals) =>
|
// Search: {folder}/{string}
|
||||||
|
[/^([a-zA-Z0-9.~_-]+)\/(.+)\/?$/, { normalize_: (request, vals) =>
|
||||||
[folder(request, vals), 1, decodeURI(pString(vals[1]))]
|
[folder(request, vals), 1, decodeURI(pString(vals[1]))]
|
||||||
}],
|
}],
|
||||||
[/^([a-zA-Z0-9~]+)\/p([1-9][0-9]*)(?:\/(.+)\/?)?$/, { normalize_: fNormS }]
|
// Page: {folder}/p{int}(/{search})?
|
||||||
|
[/^([a-zA-Z0-9.~_-]+)\/p([1-9][0-9]*)(?:\/(.+))?$/, { normalize_: fNormS }]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Capa, Scope } from 'Common/Enums';
|
import { Scope } from 'Common/Enums';
|
||||||
import { keyScope, leftPanelDisabled, Settings } from 'Common/Globals';
|
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
|
||||||
import { runSettingsViewModelHooks } from 'Common/Plugins';
|
import { runSettingsViewModelHooks } from 'Common/Plugins';
|
||||||
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||||
|
|
||||||
|
|
@ -9,23 +9,59 @@ import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||||
|
|
||||||
import { GeneralUserSettings } from 'Settings/User/General';
|
import { UserSettingsGeneral } from 'Settings/User/General';
|
||||||
import { ContactsUserSettings } from 'Settings/User/Contacts';
|
import { UserSettingsContacts } from 'Settings/User/Contacts';
|
||||||
import { AccountsUserSettings } from 'Settings/User/Accounts';
|
import { UserSettingsAccounts } from 'Settings/User/Accounts';
|
||||||
import { FiltersUserSettings } from 'Settings/User/Filters';
|
import { UserSettingsFilters } from 'Settings/User/Filters';
|
||||||
import { SecurityUserSettings } from 'Settings/User/Security';
|
import { UserSettingsSecurity } from 'Settings/User/Security';
|
||||||
import { TemplatesUserSettings } from 'Settings/User/Templates';
|
import { UserSettingsFolders } from 'Settings/User/Folders';
|
||||||
import { FoldersUserSettings } from 'Settings/User/Folders';
|
import { UserSettingsThemes } from 'Settings/User/Themes';
|
||||||
import { ThemesUserSettings } from 'Settings/User/Themes';
|
|
||||||
import { OpenPgpUserSettings } from 'Settings/User/OpenPgp';
|
|
||||||
|
|
||||||
import { SystemDropDownSettingsUserView } from 'View/User/Settings/SystemDropDown';
|
import { UserSettingsTemplates } from 'Settings/User/Templates';
|
||||||
import { MenuSettingsUserView } from 'View/User/Settings/Menu';
|
|
||||||
import { PaneSettingsUserView } from 'View/User/Settings/Pane';
|
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
|
||||||
|
import { SettingsMenuUserView } from 'View/User/Settings/Menu';
|
||||||
|
import { SettingsPaneUserView } from 'View/User/Settings/Pane';
|
||||||
|
|
||||||
export class SettingsUserScreen extends AbstractSettingsScreen {
|
export class SettingsUserScreen extends AbstractSettingsScreen {
|
||||||
constructor() {
|
constructor() {
|
||||||
super([SystemDropDownSettingsUserView, MenuSettingsUserView, PaneSettingsUserView]);
|
super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
|
||||||
|
|
||||||
|
const views = [
|
||||||
|
UserSettingsGeneral
|
||||||
|
];
|
||||||
|
|
||||||
|
if (AppUserStore.allowContacts()) {
|
||||||
|
views.push(UserSettingsContacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsCapa('AdditionalAccounts') || SettingsCapa('Identities')) {
|
||||||
|
views.push(UserSettingsAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsCapa('Sieve')) {
|
||||||
|
views.push(UserSettingsFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsCapa('AutoLogout') || SettingsCapa('OpenPGP') || SettingsCapa('GnuPG')) {
|
||||||
|
views.push(UserSettingsSecurity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsCapa('Templates')) {
|
||||||
|
views.push(UserSettingsTemplates);
|
||||||
|
}
|
||||||
|
|
||||||
|
views.push(UserSettingsFolders);
|
||||||
|
|
||||||
|
if (SettingsCapa('Themes')) {
|
||||||
|
views.push(UserSettingsThemes);
|
||||||
|
}
|
||||||
|
|
||||||
|
views.forEach((item, index) =>
|
||||||
|
settingsAddViewModel(item, item.name.replace('User', ''), 0, 0, 0 === index)
|
||||||
|
);
|
||||||
|
|
||||||
|
runSettingsViewModelHooks(false);
|
||||||
|
|
||||||
initOnStartOrLangChange(
|
initOnStartOrLangChange(
|
||||||
() => this.sSettingsTitle = i18n('TITLES/SETTINGS'),
|
() => this.sSettingsTitle = i18n('TITLES/SETTINGS'),
|
||||||
|
|
@ -33,67 +69,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function=} fCallback
|
|
||||||
*/
|
|
||||||
setupSettings(fCallback = null) {
|
|
||||||
if (!Settings.capa(Capa.Settings)) {
|
|
||||||
fCallback && fCallback();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsAddViewModel(GeneralUserSettings, 'SettingsGeneral', 'SETTINGS_LABELS/LABEL_GENERAL_NAME', 'general', true);
|
|
||||||
|
|
||||||
if (AppUserStore.allowContacts()) {
|
|
||||||
settingsAddViewModel(ContactsUserSettings, 'SettingsContacts', 'SETTINGS_LABELS/LABEL_CONTACTS_NAME', 'contacts');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Settings.capa(Capa.AdditionalAccounts) || Settings.capa(Capa.Identities)) {
|
|
||||||
settingsAddViewModel(
|
|
||||||
AccountsUserSettings,
|
|
||||||
'SettingsAccounts',
|
|
||||||
Settings.capa(Capa.AdditionalAccounts)
|
|
||||||
? 'SETTINGS_LABELS/LABEL_ACCOUNTS_NAME'
|
|
||||||
: 'SETTINGS_LABELS/LABEL_IDENTITIES_NAME',
|
|
||||||
'accounts'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Settings.capa(Capa.Sieve)) {
|
|
||||||
settingsAddViewModel(FiltersUserSettings, 'SettingsFilters', 'SETTINGS_LABELS/LABEL_FILTERS_NAME', 'filters');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Settings.capa(Capa.AutoLogout)) {
|
|
||||||
settingsAddViewModel(SecurityUserSettings, 'SettingsSecurity', 'SETTINGS_LABELS/LABEL_SECURITY_NAME', 'security');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Settings.capa(Capa.Templates)) {
|
|
||||||
settingsAddViewModel(
|
|
||||||
TemplatesUserSettings,
|
|
||||||
'SettingsTemplates',
|
|
||||||
'SETTINGS_LABELS/LABEL_TEMPLATES_NAME',
|
|
||||||
'templates'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsAddViewModel(FoldersUserSettings, 'SettingsFolders', 'SETTINGS_LABELS/LABEL_FOLDERS_NAME', 'folders');
|
|
||||||
|
|
||||||
if (Settings.capa(Capa.Themes)) {
|
|
||||||
settingsAddViewModel(ThemesUserSettings, 'SettingsThemes', 'SETTINGS_LABELS/LABEL_THEMES_NAME', 'themes');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Settings.capa(Capa.OpenPGP)) {
|
|
||||||
settingsAddViewModel(OpenPgpUserSettings, 'SettingsOpenPGP', 'OpenPGP', 'openpgp');
|
|
||||||
}
|
|
||||||
|
|
||||||
runSettingsViewModelHooks(false);
|
|
||||||
|
|
||||||
fCallback && fCallback();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.setSettingsTitle();
|
this.setSettingsTitle();
|
||||||
keyScope(Scope.Settings);
|
keyScope(Scope.Settings);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { Settings } from 'Common/Globals';
|
import { Settings } from 'Common/Globals';
|
||||||
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
export class AboutAdminSettings {
|
export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.version = ko.observable(Settings.app('version'));
|
this.version = Settings.app('version');
|
||||||
|
this.phpextensions = ko.observableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBuild() {
|
||||||
|
Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,10 @@
|
||||||
import ko from 'ko';
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
import { SettingsGet } from 'Common/Globals';
|
export class AdminSettingsBranding extends AbstractViewSettings {
|
||||||
import { settingsSaveHelperSimpleFunction } from 'Common/Utils';
|
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
|
||||||
|
|
||||||
export class BrandingAdminSettings {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.title = ko.observable(SettingsGet('Title')).idleTrigger();
|
super();
|
||||||
this.loadingDesc = ko.observable(SettingsGet('LoadingDescription')).idleTrigger();
|
this.addSetting('Title');
|
||||||
this.faviconUrl = ko.observable(SettingsGet('FaviconUrl')).idleTrigger();
|
this.addSetting('LoadingDescription');
|
||||||
|
this.addSetting('FaviconUrl');
|
||||||
this.title.subscribe(value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.title.trigger, this), {
|
|
||||||
Title: value.trim()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.loadingDesc.subscribe(value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.loadingDesc.trigger, this), {
|
|
||||||
LoadingDescription: value.trim()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.faviconUrl.subscribe(value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.faviconUrl.trigger, this), {
|
|
||||||
FaviconUrl: value.trim()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
dev/Settings/Admin/Config.js
Normal file
48
dev/Settings/Admin/Config.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import ko from 'ko';
|
||||||
|
|
||||||
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
import { forEachObjectEntry } from 'Common/Utils';
|
||||||
|
|
||||||
|
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.config = ko.observableArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeShow() {
|
||||||
|
Remote.request('AdminSettingsGet', (iError, data) => {
|
||||||
|
if (!iError) {
|
||||||
|
const cfg = [],
|
||||||
|
getInputType = (value, pass) => {
|
||||||
|
switch (typeof value)
|
||||||
|
{
|
||||||
|
case 'boolean': return 'checkbox';
|
||||||
|
case 'number': return 'number';
|
||||||
|
}
|
||||||
|
return pass ? 'password' : 'text';
|
||||||
|
};
|
||||||
|
forEachObjectEntry(data.Result, (key, items) => {
|
||||||
|
const section = {
|
||||||
|
name: key,
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
forEachObjectEntry(items, (skey, item) => {
|
||||||
|
section.items.push({
|
||||||
|
key: `config[${key}][${skey}]`,
|
||||||
|
name: skey,
|
||||||
|
value: item[0],
|
||||||
|
type: getInputType(item[0], skey.includes('password')),
|
||||||
|
comment: item[1]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cfg.push(section);
|
||||||
|
});
|
||||||
|
this.config(cfg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig(form) {
|
||||||
|
Remote.post('AdminSettingsSet', null, new FormData(form));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,35 +1,30 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
|
||||||
import { SettingsGet } from 'Common/Globals';
|
import { SettingsGet } from 'Common/Globals';
|
||||||
import {
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
settingsSaveHelperSimpleFunction,
|
import { addObservablesTo } from 'External/ko';
|
||||||
defaultOptionsAfterRender,
|
|
||||||
addObservablesTo,
|
|
||||||
addSubscribablesTo
|
|
||||||
} from 'Common/Utils';
|
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
import { decorateKoCommands } from 'Knoin/Knoin';
|
import { decorateKoCommands } from 'Knoin/Knoin';
|
||||||
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
export class ContactsAdminSettings {
|
export class AdminSettingsContacts extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
|
|
||||||
|
this.addSetting('ContactsPdoDsn');
|
||||||
|
this.addSetting('ContactsPdoUser');
|
||||||
|
this.addSetting('ContactsPdoPassword');
|
||||||
|
this.addSetting('ContactsPdoType', () => {
|
||||||
|
this.testContactsSuccess(false);
|
||||||
|
this.testContactsError(false);
|
||||||
|
this.testContactsErrorMessage('');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addSettings(['ContactsEnable','ContactsSync']);
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
enableContacts: !!SettingsGet('ContactsEnable'),
|
|
||||||
contactsSync: !!SettingsGet('ContactsSync'),
|
|
||||||
contactsType: SettingsGet('ContactsPdoType'),
|
|
||||||
|
|
||||||
pdoDsn: SettingsGet('ContactsPdoDsn'),
|
|
||||||
pdoUser: SettingsGet('ContactsPdoUser'),
|
|
||||||
pdoPassword: SettingsGet('ContactsPdoPassword'),
|
|
||||||
|
|
||||||
pdoDsnTrigger: SaveSettingsStep.Idle,
|
|
||||||
pdoUserTrigger: SaveSettingsStep.Idle,
|
|
||||||
pdoPasswordTrigger: SaveSettingsStep.Idle,
|
|
||||||
contactsTypeTrigger: SaveSettingsStep.Idle,
|
|
||||||
|
|
||||||
testing: false,
|
testing: false,
|
||||||
testContactsSuccess: false,
|
testContactsSuccess: false,
|
||||||
testContactsError: false,
|
testContactsError: false,
|
||||||
|
|
@ -54,59 +49,23 @@ export class ContactsAdminSettings {
|
||||||
|
|
||||||
this.mainContactsType = ko
|
this.mainContactsType = ko
|
||||||
.computed({
|
.computed({
|
||||||
read: this.contactsType,
|
read: this.contactsPdoType,
|
||||||
write: value => {
|
write: value => {
|
||||||
if (value !== this.contactsType()) {
|
if (value !== this.contactsPdoType()) {
|
||||||
if (supportedTypes.includes(value)) {
|
if (supportedTypes.includes(value)) {
|
||||||
this.contactsType(value);
|
this.contactsPdoType(value);
|
||||||
} else if (types.length) {
|
} else if (types.length) {
|
||||||
this.contactsType('');
|
this.contactsPdoType('');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.contactsType.valueHasMutated();
|
this.contactsPdoType.valueHasMutated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.extend({ notify: 'always' });
|
.extend({ notify: 'always' });
|
||||||
|
|
||||||
addSubscribablesTo(this, {
|
|
||||||
enableContacts: value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
ContactsEnable: value ? 1 : 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
contactsSync: value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
ContactsSync: value ? 1 : 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
contactsType: value => {
|
|
||||||
this.testContactsSuccess(false);
|
|
||||||
this.testContactsError(false);
|
|
||||||
this.testContactsErrorMessage('');
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.contactsTypeTrigger, this), {
|
|
||||||
ContactsPdoType: value.trim()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
pdoDsn: value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoDsnTrigger, this), {
|
|
||||||
ContactsPdoDsn: value.trim()
|
|
||||||
}),
|
|
||||||
|
|
||||||
pdoUser: value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoUserTrigger, this), {
|
|
||||||
ContactsPdoUser: value.trim()
|
|
||||||
}),
|
|
||||||
|
|
||||||
pdoPassword: value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoPasswordTrigger, this), {
|
|
||||||
ContactsPdoPassword: value.trim()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
decorateKoCommands(this, {
|
decorateKoCommands(this, {
|
||||||
testContactsCommand: self => self.pdoDsn() && self.pdoUser()
|
testContactsCommand: self => self.contactsPdoDsn() && self.contactsPdoUser()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,29 +75,31 @@ export class ContactsAdminSettings {
|
||||||
this.testContactsErrorMessage('');
|
this.testContactsErrorMessage('');
|
||||||
this.testing(true);
|
this.testing(true);
|
||||||
|
|
||||||
Remote.testContacts((iError, data) => {
|
Remote.request('AdminContactsTest',
|
||||||
this.testContactsSuccess(false);
|
(iError, data) => {
|
||||||
this.testContactsError(false);
|
this.testContactsSuccess(false);
|
||||||
this.testContactsErrorMessage('');
|
this.testContactsError(false);
|
||||||
|
this.testContactsErrorMessage('');
|
||||||
|
|
||||||
if (!iError && data.Result.Result) {
|
if (!iError && data.Result.Result) {
|
||||||
this.testContactsSuccess(true);
|
this.testContactsSuccess(true);
|
||||||
} else {
|
|
||||||
this.testContactsError(true);
|
|
||||||
if (data && data.Result) {
|
|
||||||
this.testContactsErrorMessage(data.Result.Message || '');
|
|
||||||
} else {
|
} else {
|
||||||
this.testContactsErrorMessage('');
|
this.testContactsError(true);
|
||||||
|
if (data && data.Result) {
|
||||||
|
this.testContactsErrorMessage(data.Result.Message || '');
|
||||||
|
} else {
|
||||||
|
this.testContactsErrorMessage('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.testing(false);
|
this.testing(false);
|
||||||
}, {
|
}, {
|
||||||
ContactsPdoType: this.contactsType(),
|
ContactsPdoType: this.contactsPdoType(),
|
||||||
ContactsPdoDsn: this.pdoDsn(),
|
ContactsPdoDsn: this.contactsPdoDsn(),
|
||||||
ContactsPdoUser: this.pdoUser(),
|
ContactsPdoUser: this.contactsPdoUser(),
|
||||||
ContactsPdoPassword: this.pdoPassword()
|
ContactsPdoPassword: this.contactsPdoPassword()
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ import Remote from 'Remote/Admin/Fetch';
|
||||||
import { DomainPopupView } from 'View/Popup/Domain';
|
import { DomainPopupView } from 'View/Popup/Domain';
|
||||||
import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
|
import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
|
||||||
|
|
||||||
export class DomainsAdminSettings {
|
export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.domains = DomainAdminStore;
|
this.domains = DomainAdminStore;
|
||||||
|
|
||||||
this.domainForDeletion = ko.observable(null).deleteAccessHelper();
|
this.domainForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
createDomain() {
|
createDomain() {
|
||||||
|
|
@ -26,19 +26,29 @@ export class DomainsAdminSettings {
|
||||||
deleteDomain(domain) {
|
deleteDomain(domain) {
|
||||||
DomainAdminStore.remove(domain);
|
DomainAdminStore.remove(domain);
|
||||||
Remote.domainDelete(DomainAdminStore.fetch, domain.name);
|
Remote.domainDelete(DomainAdminStore.fetch, domain.name);
|
||||||
|
Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
|
||||||
|
Name: domain.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
disableDomain(domain) {
|
disableDomain(domain) {
|
||||||
domain.disabled(!domain.disabled());
|
domain.disabled(!domain.disabled());
|
||||||
Remote.domainDisable(DomainAdminStore.fetch, domain.name, domain.disabled());
|
Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
|
||||||
|
Name: domain.name,
|
||||||
|
Disabled: domain.disabled() ? 1 : 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild(oDom) {
|
onBuild(oDom) {
|
||||||
oDom.addEventListener('click', event => {
|
oDom.addEventListener('click', event => {
|
||||||
let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom);
|
let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom);
|
||||||
el && ko.dataFor(el) && Remote.domain(
|
el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
|
||||||
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]), ko.dataFor(el).name
|
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
|
||||||
|
{
|
||||||
|
Name: ko.dataFor(el).name
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
DomainAdminStore.fetch();
|
DomainAdminStore.fetch();
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,29 @@ import ko from 'ko';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isArray,
|
isArray,
|
||||||
pInt,
|
|
||||||
settingsSaveHelperSimpleFunction,
|
|
||||||
changeTheme,
|
changeTheme,
|
||||||
convertThemeName,
|
convertThemeName
|
||||||
addObservablesTo,
|
|
||||||
addSubscribablesTo,
|
|
||||||
addComputablesTo
|
|
||||||
} from 'Common/Utils';
|
} from 'Common/Utils';
|
||||||
|
|
||||||
import { Capa, SaveSettingsStep } from 'Common/Enums';
|
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
|
||||||
import { reload as translatorReload, convertLangName } from 'Common/Translator';
|
|
||||||
|
|
||||||
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
|
import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||||
|
import { translatorReload, convertLangName } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
import LanguagesPopupView from 'View/Popup/Languages';
|
import { LanguagesPopupView } from 'View/Popup/Languages';
|
||||||
|
|
||||||
export class GeneralAdminSettings {
|
export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
this.language = LanguageStore.language;
|
this.language = LanguageStore.language;
|
||||||
this.languages = LanguageStore.languages;
|
this.languages = LanguageStore.languages;
|
||||||
|
|
||||||
|
|
@ -37,18 +37,17 @@ export class GeneralAdminSettings {
|
||||||
this.theme = ThemeStore.theme;
|
this.theme = ThemeStore.theme;
|
||||||
this.themes = ThemeStore.themes;
|
this.themes = ThemeStore.themes;
|
||||||
|
|
||||||
|
this.addSettings(['AllowLanguagesOnSettings','NewMoveToFolder']);
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
allowLanguagesOnSettings: !!SettingsGet('AllowLanguagesOnSettings'),
|
|
||||||
newMoveToFolder: !!SettingsGet('NewMoveToFolder'),
|
|
||||||
attachmentLimitTrigger: SaveSettingsStep.Idle,
|
attachmentLimitTrigger: SaveSettingsStep.Idle,
|
||||||
languageTrigger: SaveSettingsStep.Idle,
|
|
||||||
themeTrigger: SaveSettingsStep.Idle,
|
themeTrigger: SaveSettingsStep.Idle,
|
||||||
capaThemes: Settings.capa(Capa.Themes),
|
capaThemes: SettingsCapa('Themes'),
|
||||||
capaUserBackground: Settings.capa(Capa.UserBackground),
|
capaUserBackground: SettingsCapa('UserBackground'),
|
||||||
capaAdditionalAccounts: Settings.capa(Capa.AdditionalAccounts),
|
capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
|
||||||
capaIdentities: Settings.capa(Capa.Identities),
|
capaIdentities: SettingsCapa('Identities'),
|
||||||
capaAttachmentThumbnails: Settings.capa(Capa.AttachmentThumbnails),
|
capaAttachmentThumbnails: SettingsCapa('AttachmentThumbnails'),
|
||||||
capaTemplates: Settings.capa(Capa.Templates),
|
capaTemplates: SettingsCapa('Templates'),
|
||||||
dataFolderAccess: false
|
dataFolderAccess: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -60,10 +59,14 @@ export class GeneralAdminSettings {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
this.mainAttachmentLimit = ko
|
this.attachmentLimit = ko
|
||||||
.observable(pInt(SettingsGet('AttachmentLimit')) / (1024 * 1024))
|
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
|
||||||
.extend({ debounce: 500 });
|
.extend({ debounce: 500 });
|
||||||
|
|
||||||
|
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.uploadDataDesc =
|
||||||
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size)
|
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size)
|
||||||
|
|
@ -88,55 +91,27 @@ export class GeneralAdminSettings {
|
||||||
this.languageAdminTrigger(saveSettingsStep);
|
this.languageAdminTrigger(saveSettingsStep);
|
||||||
setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000);
|
setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000);
|
||||||
},
|
},
|
||||||
fSaveBoolHelper = (key, fn) =>
|
fSaveHelper = key => value => Remote.saveSetting(key, value);
|
||||||
value => {
|
|
||||||
const data = {};
|
|
||||||
data[key] = value ? 1 : 0;
|
|
||||||
Remote.saveAdminConfig(fn, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
addSubscribablesTo(this, {
|
addSubscribablesTo(this, {
|
||||||
mainAttachmentLimit: value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.attachmentLimitTrigger, this), {
|
|
||||||
AttachmentLimit: pInt(value)
|
|
||||||
}),
|
|
||||||
|
|
||||||
language: value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.languageTrigger, this), {
|
|
||||||
Language: value.trim()
|
|
||||||
}),
|
|
||||||
|
|
||||||
languageAdmin: value => {
|
languageAdmin: value => {
|
||||||
this.languageAdminTrigger(SaveSettingsStep.Animate);
|
this.languageAdminTrigger(SaveSettingsStep.Animate);
|
||||||
translatorReload(true, value)
|
translatorReload(true, value)
|
||||||
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
|
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
|
||||||
.then(() => Remote.saveAdminConfig(null, {
|
.then(() => Remote.saveSetting('LanguageAdmin', value));
|
||||||
LanguageAdmin: value.trim()
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
theme: value => {
|
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
|
||||||
changeTheme(value, this.themeTrigger);
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.themeTrigger, this), {
|
|
||||||
Theme: value.trim()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
capaAdditionalAccounts: fSaveBoolHelper('CapaAdditionalAccounts'),
|
capaIdentities: fSaveHelper('CapaIdentities'),
|
||||||
|
|
||||||
capaIdentities: fSaveBoolHelper('CapaIdentities'),
|
capaTemplates: fSaveHelper('CapaTemplates'),
|
||||||
|
|
||||||
capaTemplates: fSaveBoolHelper('CapaTemplates'),
|
capaAttachmentThumbnails: fSaveHelper('CapaAttachmentThumbnails'),
|
||||||
|
|
||||||
capaAttachmentThumbnails: fSaveBoolHelper('CapaAttachmentThumbnails'),
|
capaThemes: fSaveHelper('CapaThemes'),
|
||||||
|
|
||||||
capaThemes: fSaveBoolHelper('CapaThemes'),
|
capaUserBackground: fSaveHelper('CapaUserBackground')
|
||||||
|
|
||||||
capaUserBackground: fSaveBoolHelper('CapaUserBackground'),
|
|
||||||
|
|
||||||
allowLanguagesOnSettings: fSaveBoolHelper('AllowLanguagesOnSettings'),
|
|
||||||
|
|
||||||
newMoveToFolder: fSaveBoolHelper('NewMoveToFolder')
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,9 @@
|
||||||
import ko from 'ko';
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
export class AdminSettingsLogin extends AbstractViewSettings {
|
||||||
import { settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
|
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
|
||||||
|
|
||||||
export class LoginAdminSettings {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
addObservablesTo(this, {
|
super();
|
||||||
determineUserLanguage: !!SettingsGet('DetermineUserLanguage'),
|
this.addSetting('LoginDefaultDomain');
|
||||||
determineUserDomain: !!SettingsGet('DetermineUserDomain'),
|
this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin','hideSubmitButton']);
|
||||||
allowLanguagesOnLogin: !!SettingsGet('AllowLanguagesOnLogin'),
|
|
||||||
hideSubmitButton: !!Settings.app('hideSubmitButton')
|
|
||||||
});
|
|
||||||
|
|
||||||
this.defaultDomain = ko.observable(SettingsGet('LoginDefaultDomain')).idleTrigger();
|
|
||||||
|
|
||||||
addSubscribablesTo(this, {
|
|
||||||
determineUserLanguage: value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
DetermineUserLanguage: value ? 1 : 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
determineUserDomain: value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
DetermineUserDomain: value ? 1 : 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
allowLanguagesOnLogin: value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
AllowLanguagesOnLogin: value ? 1 : 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
hideSubmitButton: value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
hideSubmitButton: value ? 1 : 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
defaultDomain: value =>
|
|
||||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.defaultDomain.trigger, this), {
|
|
||||||
LoginDefaultDomain: value.trim()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,28 @@ import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { PluginPopupView } from 'View/Popup/Plugin';
|
import { PluginPopupView } from 'View/Popup/Plugin';
|
||||||
import { SettingsGet } from 'Common/Globals';
|
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
import { addComputablesTo } from 'Common/Utils';
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
export class PackagesAdminSettings {
|
export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.packagesError = ko.observable('');
|
super();
|
||||||
|
|
||||||
|
this.addSettings(['EnabledPlugins']);
|
||||||
|
|
||||||
|
addObservablesTo(this, {
|
||||||
|
packagesError: ''
|
||||||
|
});
|
||||||
|
|
||||||
this.packages = PackageAdminStore;
|
this.packages = PackageAdminStore;
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
packagesCurrent: () => PackageAdminStore.filter(item => item && item.installed && !item.canBeUpdated),
|
packagesCurrent: () => PackageAdminStore().filter(item => item && item.installed && !item.canBeUpdated),
|
||||||
packagesAvailableForUpdate: () => PackageAdminStore.filter(item => item && item.installed && !!item.canBeUpdated),
|
packagesUpdate: () => PackageAdminStore().filter(item => item && item.installed && item.canBeUpdated),
|
||||||
packagesAvailableForInstallation: () => PackageAdminStore.filter(item => item && !item.installed),
|
packagesAvailable: () => PackageAdminStore().filter(item => item && !item.installed),
|
||||||
|
|
||||||
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
|
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
|
||||||
});
|
});
|
||||||
|
|
||||||
this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
|
|
||||||
this.enabledPlugins.subscribe(value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
EnabledPlugins: value ? 1 : 0
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
|
@ -44,7 +43,12 @@ export class PackagesAdminSettings {
|
||||||
// configurePlugin
|
// configurePlugin
|
||||||
let el = event.target.closestWithin('.package-configure', oDom),
|
let el = event.target.closestWithin('.package-configure', oDom),
|
||||||
data = el ? ko.dataFor(el) : 0;
|
data = el ? ko.dataFor(el) : 0;
|
||||||
data && Remote.plugin((iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]), data.id)
|
data && Remote.request('AdminPluginLoad',
|
||||||
|
(iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
|
||||||
|
{
|
||||||
|
Id: data.id
|
||||||
|
}
|
||||||
|
);
|
||||||
// disablePlugin
|
// disablePlugin
|
||||||
el = event.target.closestWithin('.package-active', oDom);
|
el = event.target.closestWithin('.package-active', oDom);
|
||||||
data = el ? ko.dataFor(el) : 0;
|
data = el ? ko.dataFor(el) : 0;
|
||||||
|
|
@ -77,31 +81,49 @@ export class PackagesAdminSettings {
|
||||||
deletePackage(packageToDelete) {
|
deletePackage(packageToDelete) {
|
||||||
if (packageToDelete) {
|
if (packageToDelete) {
|
||||||
packageToDelete.loading(true);
|
packageToDelete.loading(true);
|
||||||
Remote.packageDelete(this.requestHelper(packageToDelete, false), packageToDelete);
|
Remote.request('AdminPackageDelete',
|
||||||
|
this.requestHelper(packageToDelete, false),
|
||||||
|
{
|
||||||
|
Id: packageToDelete.id
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
installPackage(packageToInstall) {
|
installPackage(packageToInstall) {
|
||||||
if (packageToInstall) {
|
if (packageToInstall) {
|
||||||
packageToInstall.loading(true);
|
packageToInstall.loading(true);
|
||||||
Remote.packageInstall(this.requestHelper(packageToInstall, true), packageToInstall);
|
Remote.request('AdminPackageInstall',
|
||||||
|
this.requestHelper(packageToInstall, true),
|
||||||
|
{
|
||||||
|
Id: packageToInstall.id,
|
||||||
|
Type: packageToInstall.type,
|
||||||
|
File: packageToInstall.file
|
||||||
|
},
|
||||||
|
60000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disablePlugin(plugin) {
|
disablePlugin(plugin) {
|
||||||
let b = plugin.enabled();
|
let disable = plugin.enabled();
|
||||||
plugin.enabled(!b);
|
plugin.enabled(!disable);
|
||||||
Remote.pluginDisable((iError, data) => {
|
Remote.request('AdminPluginDisable',
|
||||||
if (iError) {
|
(iError, data) => {
|
||||||
plugin.enabled(b);
|
if (iError) {
|
||||||
this.packagesError(
|
plugin.enabled(disable);
|
||||||
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
|
this.packagesError(
|
||||||
? data.ErrorMessage
|
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
|
||||||
: getNotification(iError)
|
? data.ErrorMessage
|
||||||
);
|
: getNotification(iError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// PackageAdminStore.fetch();
|
||||||
|
}, {
|
||||||
|
Id: plugin.id,
|
||||||
|
Disabled: disable ? 1 : 0
|
||||||
}
|
}
|
||||||
// PackageAdminStore.fetch();
|
);
|
||||||
}, plugin.id, b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,40 @@
|
||||||
import { Capa } from 'Common/Enums';
|
import { SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { addObservablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
import { addObservablesTo, addSubscribablesTo } from 'Common/Utils';
|
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
import { decorateKoCommands } from 'Knoin/Knoin';
|
import { decorateKoCommands } from 'Knoin/Knoin';
|
||||||
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
export class SecurityAdminSettings {
|
export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
|
||||||
|
|
||||||
this.weakPassword = rl.app.weakPassword;
|
this.weakPassword = rl.app.weakPassword;
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
useLocalProxyForExternalImages: !!SettingsGet('UseLocalProxyForExternalImages'),
|
|
||||||
|
|
||||||
verifySslCertificate: !!SettingsGet('VerifySslCertificate'),
|
|
||||||
allowSelfSigned: !!SettingsGet('AllowSelfSigned'),
|
|
||||||
|
|
||||||
adminLogin: SettingsGet('AdminLogin'),
|
adminLogin: SettingsGet('AdminLogin'),
|
||||||
adminLoginError: false,
|
adminLoginError: false,
|
||||||
adminPassword: '',
|
adminPassword: '',
|
||||||
adminPasswordNew: '',
|
adminPasswordNew: '',
|
||||||
adminPasswordNew2: '',
|
adminPasswordNew2: '',
|
||||||
adminPasswordNewError: false,
|
adminPasswordNewError: false,
|
||||||
|
adminTOTP: SettingsGet('AdminTOTP'),
|
||||||
|
|
||||||
adminPasswordUpdateError: false,
|
adminPasswordUpdateError: false,
|
||||||
adminPasswordUpdateSuccess: false,
|
adminPasswordUpdateSuccess: false,
|
||||||
|
|
||||||
capaOpenPGP: Settings.capa(Capa.OpenPGP)
|
capaOpenPGP: SettingsCapa('OpenPGP')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
this.adminPasswordUpdateError(false);
|
||||||
|
this.adminPasswordUpdateSuccess(false);
|
||||||
|
this.adminPasswordNewError(false);
|
||||||
|
};
|
||||||
|
|
||||||
addSubscribablesTo(this, {
|
addSubscribablesTo(this, {
|
||||||
adminPassword: () => {
|
adminPassword: () => {
|
||||||
this.adminPasswordUpdateError(false);
|
this.adminPasswordUpdateError(false);
|
||||||
|
|
@ -37,39 +43,11 @@ export class SecurityAdminSettings {
|
||||||
|
|
||||||
adminLogin: () => this.adminLoginError(false),
|
adminLogin: () => this.adminLoginError(false),
|
||||||
|
|
||||||
adminPasswordNew: () => {
|
adminPasswordNew: reset,
|
||||||
this.adminPasswordUpdateError(false);
|
|
||||||
this.adminPasswordUpdateSuccess(false);
|
|
||||||
this.adminPasswordNewError(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
adminPasswordNew2: () => {
|
adminPasswordNew2: reset,
|
||||||
this.adminPasswordUpdateError(false);
|
|
||||||
this.adminPasswordUpdateSuccess(false);
|
|
||||||
this.adminPasswordNewError(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
capaOpenPGP: value =>
|
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
CapaOpenPGP: value ? 1 : 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
useLocalProxyForExternalImages: value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
UseLocalProxyForExternalImages: value ? 1 : 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
verifySslCertificate: value => {
|
|
||||||
value => value || this.allowSelfSigned(true);
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
VerifySslCertificate: value ? 1 : 0
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
allowSelfSigned: value =>
|
|
||||||
Remote.saveAdminConfig(null, {
|
|
||||||
AllowSelfSigned: value ? 1 : 0
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
decorateKoCommands(this, {
|
decorateKoCommands(this, {
|
||||||
|
|
@ -91,7 +69,7 @@ export class SecurityAdminSettings {
|
||||||
this.adminPasswordUpdateError(false);
|
this.adminPasswordUpdateError(false);
|
||||||
this.adminPasswordUpdateSuccess(false);
|
this.adminPasswordUpdateSuccess(false);
|
||||||
|
|
||||||
Remote.saveNewAdminPassword((iError, data) => {
|
Remote.request('AdminPasswordUpdate', (iError, data) => {
|
||||||
if (iError) {
|
if (iError) {
|
||||||
this.adminPasswordUpdateError(true);
|
this.adminPasswordUpdateError(true);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -106,7 +84,8 @@ export class SecurityAdminSettings {
|
||||||
}, {
|
}, {
|
||||||
'Login': this.adminLogin(),
|
'Login': this.adminLogin(),
|
||||||
'Password': this.adminPassword(),
|
'Password': this.adminPassword(),
|
||||||
'NewPassword': this.adminPasswordNew()
|
'NewPassword': this.adminPasswordNew(),
|
||||||
|
'TOTP': this.adminTOTP()
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { Capa } from 'Common/Enums';
|
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
||||||
import { Settings } from 'Common/Globals';
|
|
||||||
|
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
|
|
@ -12,17 +11,18 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { AccountPopupView } from 'View/Popup/Account';
|
import { AccountPopupView } from 'View/Popup/Account';
|
||||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||||
|
|
||||||
export class AccountsUserSettings {
|
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.allowAdditionalAccount = Settings.capa(Capa.AdditionalAccounts);
|
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
|
||||||
this.allowIdentities = Settings.capa(Capa.Identities);
|
this.allowIdentities = SettingsCapa('Identities');
|
||||||
|
|
||||||
this.accounts = AccountUserStore.accounts;
|
this.accounts = AccountUserStore.accounts;
|
||||||
this.loading = AccountUserStore.loading;
|
this.loading = AccountUserStore.loading;
|
||||||
this.identities = IdentityUserStore;
|
this.identities = IdentityUserStore;
|
||||||
|
this.mainEmail = SettingsGet('MainEmail');
|
||||||
|
|
||||||
this.accountForDeletion = ko.observable(null).deleteAccessHelper();
|
this.accountForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
this.identityForDeletion = ko.observable(null).deleteAccessHelper();
|
this.identityForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
addNewAccount() {
|
addNewAccount() {
|
||||||
|
|
@ -30,7 +30,7 @@ export class AccountsUserSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
editAccount(account) {
|
editAccount(account) {
|
||||||
if (account && account.canBeEdit()) {
|
if (account && account.isAdditional()) {
|
||||||
showScreenPopup(AccountPopupView, [account]);
|
showScreenPopup(AccountPopupView, [account]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,19 +48,21 @@ export class AccountsUserSettings {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
deleteAccount(accountToRemove) {
|
deleteAccount(accountToRemove) {
|
||||||
if (accountToRemove && accountToRemove.deleteAccess()) {
|
if (accountToRemove && accountToRemove.askDelete()) {
|
||||||
this.accountForDeletion(null);
|
this.accountForDeletion(null);
|
||||||
if (accountToRemove) {
|
if (accountToRemove) {
|
||||||
this.accounts.remove((account) => accountToRemove === account);
|
this.accounts.remove((account) => accountToRemove === account);
|
||||||
|
|
||||||
Remote.accountDelete((iError, data) => {
|
Remote.request('AccountDelete', (iError, data) => {
|
||||||
if (!iError && data.Reload) {
|
if (!iError && data.Reload) {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.reload(), 1);
|
setTimeout(() => location.reload(), 1);
|
||||||
} else {
|
} else {
|
||||||
rl.app.accountsAndIdentities();
|
rl.app.accountsAndIdentities();
|
||||||
}
|
}
|
||||||
}, accountToRemove.email);
|
}, {
|
||||||
|
EmailToDelete: accountToRemove.email
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,18 +72,23 @@ export class AccountsUserSettings {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
deleteIdentity(identityToRemove) {
|
deleteIdentity(identityToRemove) {
|
||||||
if (identityToRemove && identityToRemove.deleteAccess()) {
|
if (identityToRemove && identityToRemove.askDelete()) {
|
||||||
this.identityForDeletion(null);
|
this.identityForDeletion(null);
|
||||||
|
|
||||||
if (identityToRemove) {
|
if (identityToRemove) {
|
||||||
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
|
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
|
||||||
Remote.identityDelete(() => rl.app.accountsAndIdentities(), identityToRemove.id());
|
Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
|
||||||
|
IdToDelete: identityToRemove.id()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accountsAndIdentitiesAfterMove() {
|
accountsAndIdentitiesAfterMove() {
|
||||||
Remote.accountsAndIdentitiesSortOrder(null, AccountUserStore.getEmailAddresses(), IdentityUserStore.getIDS());
|
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
||||||
|
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('MainEmail')),
|
||||||
|
Identities: IdentityUserStore.getIDS()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild(oDom) {
|
onBuild(oDom) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
import { koComputable } from 'External/ko';
|
||||||
|
|
||||||
import { SettingsGet } from 'Common/Globals';
|
import { SettingsGet } from 'Common/Globals';
|
||||||
import { ContactUserStore } from 'Stores/User/Contact';
|
import { ContactUserStore } from 'Stores/User/Contact';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
export class ContactsUserSettings {
|
export class UserSettingsContacts /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.contactsAutosave = ko.observable(!!SettingsGet('ContactsAutosave'));
|
this.contactsAutosave = ko.observable(!!SettingsGet('ContactsAutosave'));
|
||||||
|
|
||||||
|
|
@ -14,8 +15,7 @@ export class ContactsUserSettings {
|
||||||
this.contactsSyncUser = ContactUserStore.syncUser;
|
this.contactsSyncUser = ContactUserStore.syncUser;
|
||||||
this.contactsSyncPass = ContactUserStore.syncPass;
|
this.contactsSyncPass = ContactUserStore.syncPass;
|
||||||
|
|
||||||
this.saveTrigger = ko
|
this.saveTrigger = koComputable(() =>
|
||||||
.computed(() =>
|
|
||||||
[
|
[
|
||||||
ContactUserStore.enableSync() ? '1' : '0',
|
ContactUserStore.enableSync() ? '1' : '0',
|
||||||
ContactUserStore.syncUrl(),
|
ContactUserStore.syncUrl(),
|
||||||
|
|
@ -26,19 +26,16 @@ export class ContactsUserSettings {
|
||||||
.extend({ debounce: 500 });
|
.extend({ debounce: 500 });
|
||||||
|
|
||||||
this.contactsAutosave.subscribe(value =>
|
this.contactsAutosave.subscribe(value =>
|
||||||
Remote.saveSettings(null, {
|
Remote.saveSettings(null, { ContactsAutosave: value })
|
||||||
ContactsAutosave: value ? 1 : 0
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.saveTrigger.subscribe(() =>
|
this.saveTrigger.subscribe(() =>
|
||||||
Remote.saveContactsSyncData(
|
Remote.request('SaveContactsSyncData', null, {
|
||||||
null,
|
Enable: ContactUserStore.enableSync() ? 1 : 0,
|
||||||
ContactUserStore.enableSync(),
|
Url: ContactUserStore.syncUrl(),
|
||||||
ContactUserStore.syncUrl(),
|
User: ContactUserStore.syncUser(),
|
||||||
ContactUserStore.syncUser(),
|
Password: ContactUserStore.syncPass()
|
||||||
ContactUserStore.syncPass()
|
})
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,44 @@
|
||||||
import ko from 'ko';
|
import { addObservablesTo } from 'External/ko';
|
||||||
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
import { SettingsGet } from 'Common/Globals';
|
||||||
|
|
||||||
import { getNotification } from 'Common/Translator';
|
//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
||||||
import { addObservablesTo } from 'Common/Utils';
|
export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
||||||
import { delegateRunOnDestroy } from 'Common/UtilsUser';
|
|
||||||
|
|
||||||
import { SieveUserStore } from 'Stores/User/Sieve';
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
|
||||||
|
|
||||||
import { SieveScriptModel } from 'Model/SieveScript';
|
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
|
||||||
|
|
||||||
import { SieveScriptPopupView } from 'View/Popup/SieveScript';
|
|
||||||
|
|
||||||
export class FiltersUserSettings {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.scripts = SieveUserStore.scripts;
|
this.scripts = ko.observableArray();
|
||||||
this.loading = ko.observable(false).extend({ debounce: 200 });
|
this.loading = ko.observable(true).extend({ debounce: 200 });
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
serverError: false,
|
serverError: false,
|
||||||
serverErrorDesc: ''
|
serverErrorDesc: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scriptForDeletion = ko.observable(null).deleteAccessHelper();
|
rl.loadScript(SettingsGet('StaticLibsJs').replace('/libs.', '/sieve.')).then(() => {
|
||||||
}
|
const Sieve = window.Sieve;
|
||||||
|
Sieve.folderList = FolderUserStore.folderList;
|
||||||
|
Sieve.serverError.subscribe(value => this.serverError(value));
|
||||||
|
Sieve.serverErrorDesc.subscribe(value => this.serverErrorDesc(value));
|
||||||
|
Sieve.loading.subscribe(value => this.loading(value));
|
||||||
|
Sieve.scripts.subscribe(value => this.scripts(value));
|
||||||
|
Sieve.updateList();
|
||||||
|
}).catch(e => console.error(e));
|
||||||
|
|
||||||
setError(text) {
|
this.scriptForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
this.serverError(true);
|
|
||||||
this.serverErrorDesc(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateList() {
|
|
||||||
if (!this.loading()) {
|
|
||||||
this.loading(true);
|
|
||||||
this.serverError(false);
|
|
||||||
|
|
||||||
Remote.filtersGet((iError, data) => {
|
|
||||||
this.loading(false);
|
|
||||||
this.scripts([]);
|
|
||||||
|
|
||||||
if (iError) {
|
|
||||||
SieveUserStore.capa([]);
|
|
||||||
this.setError(getNotification(iError));
|
|
||||||
} else {
|
|
||||||
SieveUserStore.capa(data.Result.Capa);
|
|
||||||
/*
|
|
||||||
this.scripts(
|
|
||||||
data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
Object.values(data.Result.Scripts).forEach(value => {
|
|
||||||
value = SieveScriptModel.reviveFromJson(value);
|
|
||||||
value && this.scripts.push(value)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addScript() {
|
addScript() {
|
||||||
showScreenPopup(SieveScriptPopupView, [new SieveScriptModel()]);
|
this.editScript();
|
||||||
}
|
}
|
||||||
|
|
||||||
editScript(script) {
|
editScript(script) {
|
||||||
showScreenPopup(SieveScriptPopupView, [script]);
|
window.Sieve.ScriptView.showModal(script ? [script] : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteScript(script) {
|
deleteScript(script) {
|
||||||
this.serverError(false);
|
window.Sieve.deleteScript(script);
|
||||||
Remote.filtersScriptDelete(
|
|
||||||
(iError, data) => {
|
|
||||||
if (iError) {
|
|
||||||
this.setError((data && data.ErrorMessageAdditional) || getNotification(iError));
|
|
||||||
} else {
|
|
||||||
this.scripts.remove(script);
|
|
||||||
delegateRunOnDestroy(script);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
script.name()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleScript(script) {
|
toggleScript(script) {
|
||||||
let name = script.active() ? '' : script.name();
|
window.Sieve.toggleScript(script);
|
||||||
this.serverError(false);
|
|
||||||
Remote.filtersScriptActivate(
|
|
||||||
(iError, data) => {
|
|
||||||
if (iError) {
|
|
||||||
this.setError((data && data.ErrorMessageAdditional) || iError)
|
|
||||||
} else {
|
|
||||||
this.scripts.forEach(script => script.active(script.name() === name));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild(oDom) {
|
onBuild(oDom) {
|
||||||
|
|
@ -106,6 +50,6 @@ export class FiltersUserSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.updateList();
|
window.Sieve && window.Sieve.updateList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
import { koComputable } from 'External/ko';
|
||||||
|
|
||||||
import { Notification } from 'Common/Enums';
|
import { Notification } from 'Common/Enums';
|
||||||
import { ClientSideKeyName } from 'Common/EnumsUser';
|
import { FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
import { Settings } from 'Common/Globals';
|
import { SettingsCapa } from 'Common/Globals';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
|
|
||||||
import { removeFolderFromCacheList } from 'Common/Cache';
|
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
|
||||||
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
import * as Local from 'Storage/Client';
|
import { sortFolders } from 'Common/Folders';
|
||||||
|
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||||
|
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
@ -18,16 +20,36 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||||
|
import { loadFolders } from 'Model/FolderCollection';
|
||||||
|
|
||||||
export class FoldersUserSettings {
|
const folderForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
|
|
||||||
|
export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.showKolab = koComputable(() => FolderUserStore.hasCapability('METADATA') && SettingsCapa('Kolab'));
|
||||||
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
|
this.kolabTypeOptions = ko.observableArray();
|
||||||
|
let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key);
|
||||||
|
initOnStartOrLangChange(()=>{
|
||||||
|
this.kolabTypeOptions([
|
||||||
|
{ id: '', name: '' },
|
||||||
|
{ id: 'event', name: i18nFilter('CALENDAR') },
|
||||||
|
{ id: 'contact', name: i18nFilter('CONTACTS') },
|
||||||
|
{ id: 'task', name: i18nFilter('TASKS') },
|
||||||
|
{ id: 'note', name: i18nFilter('NOTES') },
|
||||||
|
{ id: 'file', name: i18nFilter('FILES') },
|
||||||
|
{ id: 'journal', name: i18nFilter('JOURNAL') },
|
||||||
|
{ id: 'configuration', name: i18nFilter('CONFIGURATION') }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
|
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
|
||||||
this.folderList = FolderUserStore.folderList;
|
this.folderList = FolderUserStore.folderList;
|
||||||
this.folderListOptimized = FolderUserStore.folderListOptimized;
|
this.folderListOptimized = FolderUserStore.folderListOptimized;
|
||||||
this.folderListError = FolderUserStore.folderListError;
|
this.folderListError = FolderUserStore.folderListError;
|
||||||
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
|
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
|
||||||
|
|
||||||
this.loading = ko.computed(() => {
|
this.loading = koComputable(() => {
|
||||||
const loading = FolderUserStore.foldersLoading(),
|
const loading = FolderUserStore.foldersLoading(),
|
||||||
creating = FolderUserStore.foldersCreating(),
|
creating = FolderUserStore.foldersCreating(),
|
||||||
deleting = FolderUserStore.foldersDeleting(),
|
deleting = FolderUserStore.foldersDeleting(),
|
||||||
|
|
@ -36,28 +58,43 @@ export class FoldersUserSettings {
|
||||||
return loading || creating || deleting || renaming;
|
return loading || creating || deleting || renaming;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.folderForDeletion = ko.observable(null).deleteAccessHelper();
|
this.folderForDeletion = folderForDeletion;
|
||||||
|
|
||||||
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'edited'] });
|
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'edited'] });
|
||||||
|
|
||||||
this.useImapSubscribe = Settings.app('useImapSubscribe');
|
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
|
||||||
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value ? 1 : 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
folderEditOnEnter(folder) {
|
folderEditOnEnter(folder) {
|
||||||
const nameToEdit = folder ? folder.nameForEdit().trim() : '';
|
const nameToEdit = folder ? folder.nameForEdit().trim() : '';
|
||||||
|
|
||||||
if (nameToEdit && folder.name() !== nameToEdit) {
|
if (nameToEdit && folder.name() !== nameToEdit) {
|
||||||
Local.set(ClientSideKeyName.FoldersLashHash, '');
|
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
||||||
|
Folder: folder.fullName,
|
||||||
rl.app.foldersPromisesActionHelper(
|
NewFolderName: nameToEdit,
|
||||||
Remote.folderRename(folder.fullNameRaw, nameToEdit),
|
Subscribe: folder.subscribed() ? 1 : 0
|
||||||
Notification.CantRenameFolder
|
})
|
||||||
);
|
.then(data => {
|
||||||
|
folder.name(nameToEdit/*data.Name*/);
|
||||||
removeFolderFromCacheList(folder.fullNameRaw);
|
if (folder.subFolders.length) {
|
||||||
|
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
||||||
folder.name(nameToEdit);
|
// 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
folder.edited(false);
|
folder.edited(false);
|
||||||
|
|
@ -85,48 +122,69 @@ export class FoldersUserSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFolder(folderToRemove) {
|
deleteFolder(folderToRemove) {
|
||||||
if (
|
if (folderToRemove
|
||||||
folderToRemove &&
|
&& folderToRemove.canBeDeleted()
|
||||||
folderToRemove.canBeDeleted() &&
|
&& folderToRemove.askDelete()
|
||||||
folderToRemove.deleteAccess() &&
|
|
||||||
0 === folderToRemove.privateMessageCountAll()
|
|
||||||
) {
|
) {
|
||||||
this.folderForDeletion(null);
|
if (0 < folderToRemove.privateMessageCountAll()) {
|
||||||
|
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||||
|
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||||
|
} else {
|
||||||
|
folderForDeletion(null);
|
||||||
|
|
||||||
if (folderToRemove) {
|
if (folderToRemove) {
|
||||||
Local.set(ClientSideKeyName.FoldersLashHash, '');
|
Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
|
||||||
|
Folder: folderToRemove.fullName
|
||||||
// rl.app.foldersPromisesActionHelper
|
}).then(
|
||||||
Remote.abort('Folders')
|
() => {
|
||||||
.folderDelete(folderToRemove.fullNameRaw)
|
// folderToRemove.flags.push('\\nonexistent');
|
||||||
.then(
|
folderToRemove.selectable(false);
|
||||||
() => {
|
// folderToRemove.subscribed(false);
|
||||||
folderToRemove.selectable(false)
|
// folderToRemove.checkable(false);
|
||||||
removeFolderFromCacheList(folderToRemove.fullNameRaw);
|
if (!folderToRemove.subFolders.length) {
|
||||||
},
|
removeFolderFromCacheList(folderToRemove.fullName);
|
||||||
error => {
|
const folder = getFolderFromCacheList(folderToRemove.parentName);
|
||||||
FolderUserStore.folderListError(
|
(folder ? folder.subFolders : FolderUserStore.folderList).remove(folderToRemove);
|
||||||
getNotification(error.code, '', Notification.CantDeleteFolder)
|
}
|
||||||
+ '.\n' + error.message
|
},
|
||||||
);
|
error => {
|
||||||
}
|
FolderUserStore.folderListError(
|
||||||
);
|
getNotification(error.code, '', Notification.CantDeleteFolder)
|
||||||
|
+ '.\n' + error.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (0 < folderToRemove.privateMessageCountAll()) {
|
|
||||||
FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleFolderKolabType(folder, event) {
|
||||||
|
let type = event.target.value;
|
||||||
|
// TODO: append '.default' ?
|
||||||
|
Remote.request('FolderSetMetadata', null, {
|
||||||
|
Folder: folder.fullName,
|
||||||
|
Key: FolderMetadataKeys.KolabFolderType,
|
||||||
|
Value: type
|
||||||
|
});
|
||||||
|
folder.kolabType(type);
|
||||||
|
}
|
||||||
|
|
||||||
toggleFolderSubscription(folder) {
|
toggleFolderSubscription(folder) {
|
||||||
let subscribe = !folder.subscribed();
|
let subscribe = !folder.subscribed();
|
||||||
Local.set(ClientSideKeyName.FoldersLashHash, '');
|
Remote.request('FolderSubscribe', null, {
|
||||||
Remote.folderSetSubscribe(()=>0, folder.fullNameRaw, subscribe);
|
Folder: folder.fullName,
|
||||||
|
Subscribe: subscribe ? 1 : 0
|
||||||
|
});
|
||||||
folder.subscribed(subscribe);
|
folder.subscribed(subscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFolderCheckable(folder) {
|
toggleFolderCheckable(folder) {
|
||||||
let checkable = !folder.checkable();
|
let checkable = !folder.checkable();
|
||||||
Remote.folderSetCheckable(()=>0, folder.fullNameRaw, checkable);
|
Remote.request('FolderCheckable', null, {
|
||||||
|
Folder: folder.fullName,
|
||||||
|
Checkable: checkable ? 1 : 0
|
||||||
|
});
|
||||||
folder.checkable(checkable);
|
folder.checkable(checkable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ import ko from 'ko';
|
||||||
import { SaveSettingsStep } from 'Common/Enums';
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
import { EditorDefaultType, Layout } from 'Common/EnumsUser';
|
import { EditorDefaultType, Layout } from 'Common/EnumsUser';
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { Settings, SettingsGet } from 'Common/Globals';
|
||||||
import { isArray, settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo, addComputablesTo } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
import { i18n, trigger as translatorTrigger, reload as translatorReload, convertLangName } from 'Common/Translator';
|
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
import { i18n, trigger as translatorTrigger, translatorReload, convertLangName } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { AppUserStore } from 'Stores/User/App';
|
import { AppUserStore } from 'Stores/User/App';
|
||||||
|
|
@ -14,14 +16,17 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||||
import { LanguagesPopupView } from 'View/Popup/Languages';
|
import { LanguagesPopupView } from 'View/Popup/Languages';
|
||||||
|
|
||||||
export class GeneralUserSettings {
|
export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
this.language = LanguageStore.language;
|
this.language = LanguageStore.language;
|
||||||
this.languages = LanguageStore.languages;
|
this.languages = LanguageStore.languages;
|
||||||
this.messageReadDelay = SettingsUserStore.messageReadDelay;
|
this.messageReadDelay = SettingsUserStore.messageReadDelay;
|
||||||
|
|
@ -30,13 +35,14 @@ export class GeneralUserSettings {
|
||||||
this.editorDefaultType = SettingsUserStore.editorDefaultType;
|
this.editorDefaultType = SettingsUserStore.editorDefaultType;
|
||||||
this.layout = SettingsUserStore.layout;
|
this.layout = SettingsUserStore.layout;
|
||||||
|
|
||||||
this.enableSoundNotification = NotificationUserStore.enableSoundNotification;
|
this.soundNotification = NotificationUserStore.enableSoundNotification;
|
||||||
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
||||||
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
|
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
|
||||||
|
|
||||||
this.enableDesktopNotification = NotificationUserStore.enableDesktopNotification;
|
this.desktopNotification = NotificationUserStore.enableDesktopNotification;
|
||||||
this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed;
|
this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed;
|
||||||
|
|
||||||
|
this.viewHTML = SettingsUserStore.viewHTML;
|
||||||
this.showImages = SettingsUserStore.showImages;
|
this.showImages = SettingsUserStore.showImages;
|
||||||
this.removeColors = SettingsUserStore.removeColors;
|
this.removeColors = SettingsUserStore.removeColors;
|
||||||
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
|
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
|
||||||
|
|
@ -45,14 +51,7 @@ export class GeneralUserSettings {
|
||||||
this.replySameFolder = SettingsUserStore.replySameFolder;
|
this.replySameFolder = SettingsUserStore.replySameFolder;
|
||||||
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
||||||
|
|
||||||
this.languageTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
|
this.languageTrigger = ko.observable(SaveSettingsStep.Idle);
|
||||||
|
|
||||||
addObservablesTo(this, {
|
|
||||||
mppTrigger: SaveSettingsStep.Idle,
|
|
||||||
messageReadDelayTrigger: SaveSettingsStep.Idle,
|
|
||||||
editorDefaultTypeTrigger: SaveSettingsStep.Idle,
|
|
||||||
layoutTrigger: SaveSettingsStep.Idle
|
|
||||||
});
|
|
||||||
|
|
||||||
this.identities = IdentityUserStore;
|
this.identities = IdentityUserStore;
|
||||||
|
|
||||||
|
|
@ -89,58 +88,43 @@ export class GeneralUserSettings {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addSetting('EditorDefaultType');
|
||||||
|
this.addSetting('MessageReadDelay');
|
||||||
|
this.addSetting('MessagesPerPage');
|
||||||
|
this.addSetting('Layout', () => MessagelistUserStore([]));
|
||||||
|
|
||||||
|
this.addSettings(['ViewHTML', 'ShowImages', 'UseCheckboxesInList', 'ReplySameFolder',
|
||||||
|
'DesktopNotifications', 'SoundNotification']);
|
||||||
|
|
||||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||||
this.languageTrigger(saveSettingsStep);
|
this.languageTrigger(saveSettingsStep);
|
||||||
setTimeout(() => this.languageTrigger(SaveSettingsStep.Idle), 1000);
|
setTimeout(() => this.languageTrigger(SaveSettingsStep.Idle), 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
addSubscribablesTo(this, {
|
addSubscribablesTo(this, {
|
||||||
language: value => {
|
language: value => {
|
||||||
this.languageTrigger(SaveSettingsStep.Animate);
|
this.languageTrigger(SaveSettingsStep.Animate);
|
||||||
translatorReload(false, value)
|
translatorReload(false, value)
|
||||||
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult),
|
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
|
||||||
fReloadLanguageHelper(SaveSettingsStep.FalseResult))
|
|
||||||
.then(() => Remote.saveSetting('Language', value));
|
.then(() => Remote.saveSetting('Language', value));
|
||||||
},
|
},
|
||||||
|
|
||||||
editorDefaultType: value => Remote.saveSetting('EditorDefaultType', value,
|
|
||||||
settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this)),
|
|
||||||
|
|
||||||
messageReadDelay: value => Remote.saveSetting('MessageReadDelay', value,
|
|
||||||
settingsSaveHelperSimpleFunction(this.messageReadDelayTrigger, this)),
|
|
||||||
|
|
||||||
messagesPerPage: value => Remote.saveSetting('MPP', value,
|
|
||||||
settingsSaveHelperSimpleFunction(this.mppTrigger, this)),
|
|
||||||
|
|
||||||
showImages: value => Remote.saveSetting('ShowImages', value ? 1 : 0),
|
|
||||||
|
|
||||||
removeColors: value => {
|
removeColors: value => {
|
||||||
let dom = MessageUserStore.messagesBodiesDom();
|
let dom = MessageUserStore.bodiesDom();
|
||||||
if (dom) {
|
if (dom) {
|
||||||
dom.innerHTML = '';
|
dom.innerHTML = '';
|
||||||
}
|
}
|
||||||
Remote.saveSetting('RemoveColors', value ? 1 : 0);
|
Remote.saveSetting('RemoveColors', value);
|
||||||
},
|
},
|
||||||
|
|
||||||
useCheckboxesInList: value => Remote.saveSetting('UseCheckboxesInList', value ? 1 : 0),
|
|
||||||
|
|
||||||
enableDesktopNotification: value => Remote.saveSetting('DesktopNotifications', value ? 1 : 0),
|
|
||||||
|
|
||||||
enableSoundNotification: value => Remote.saveSetting('SoundNotification', value ? 1 : 0),
|
|
||||||
notificationSound: value => {
|
notificationSound: value => {
|
||||||
Remote.saveSetting('NotificationSound', value);
|
Remote.saveSetting('NotificationSound', value);
|
||||||
Settings.set('NotificationSound', value);
|
Settings.set('NotificationSound', value);
|
||||||
},
|
},
|
||||||
|
|
||||||
replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0),
|
|
||||||
|
|
||||||
useThreads: value => {
|
useThreads: value => {
|
||||||
MessageUserStore.list([]);
|
MessagelistUserStore([]);
|
||||||
Remote.saveSetting('UseThreads', value ? 1 : 0);
|
Remote.saveSetting('UseThreads', value);
|
||||||
},
|
|
||||||
|
|
||||||
layout: value => {
|
|
||||||
MessageUserStore.list([]);
|
|
||||||
Remote.saveSetting('Layout', value, settingsSaveHelperSimpleFunction(this.layoutTrigger, this));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import ko from 'ko';
|
|
||||||
|
|
||||||
import { delegateRunOnDestroy } from 'Common/UtilsUser';
|
|
||||||
|
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
|
||||||
|
|
||||||
import { AddOpenPgpKeyPopupView } from 'View/Popup/AddOpenPgpKey';
|
|
||||||
import { NewOpenPgpKeyPopupView } from 'View/Popup/NewOpenPgpKey';
|
|
||||||
import { ViewOpenPgpKeyPopupView } from 'View/Popup/ViewOpenPgpKey';
|
|
||||||
|
|
||||||
export class OpenPgpUserSettings {
|
|
||||||
constructor() {
|
|
||||||
this.openpgpkeys = PgpUserStore.openpgpkeys;
|
|
||||||
this.openpgpkeysPublic = PgpUserStore.openpgpkeysPublic;
|
|
||||||
this.openpgpkeysPrivate = PgpUserStore.openpgpkeysPrivate;
|
|
||||||
|
|
||||||
this.openPgpKeyForDeletion = ko.observable(null).deleteAccessHelper();
|
|
||||||
|
|
||||||
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
|
|
||||||
|
|
||||||
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value ? 1 : 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
addOpenPgpKey() {
|
|
||||||
showScreenPopup(AddOpenPgpKeyPopupView);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateOpenPgpKey() {
|
|
||||||
showScreenPopup(NewOpenPgpKeyPopupView);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewOpenPgpKey(openPgpKey) {
|
|
||||||
if (openPgpKey) {
|
|
||||||
showScreenPopup(ViewOpenPgpKeyPopupView, [openPgpKey]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {OpenPgpKeyModel} openPgpKeyToRemove
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
deleteOpenPgpKey(openPgpKeyToRemove) {
|
|
||||||
if (openPgpKeyToRemove && openPgpKeyToRemove.deleteAccess()) {
|
|
||||||
this.openPgpKeyForDeletion(null);
|
|
||||||
|
|
||||||
if (openPgpKeyToRemove && PgpUserStore.openpgpKeyring) {
|
|
||||||
const findedItem = PgpUserStore.openpgpkeys.find(key => openPgpKeyToRemove === key);
|
|
||||||
if (findedItem) {
|
|
||||||
PgpUserStore.openpgpkeys.remove(findedItem);
|
|
||||||
delegateRunOnDestroy(findedItem);
|
|
||||||
|
|
||||||
PgpUserStore.openpgpKeyring[findedItem.isPrivate ? 'privateKeys' : 'publicKeys'].removeForId(findedItem.guid);
|
|
||||||
|
|
||||||
PgpUserStore.openpgpKeyring.store();
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.app.reloadOpenPgpKeys();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
import ko from 'ko';
|
import { koComputable } from 'External/ko';
|
||||||
|
|
||||||
import { pInt, settingsSaveHelperSimpleFunction } from 'Common/Utils';
|
import { SettingsCapa } from 'Common/Globals';
|
||||||
import { Capa, SaveSettingsStep } from 'Common/Enums';
|
|
||||||
import { Settings } from 'Common/Globals';
|
|
||||||
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
|
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
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 Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
export class SecurityUserSettings {
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
|
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||||
|
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
||||||
|
|
||||||
|
export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.capaAutoLogout = Settings.capa(Capa.AutoLogout);
|
super();
|
||||||
|
|
||||||
|
this.capaAutoLogout = SettingsCapa('AutoLogout');
|
||||||
|
|
||||||
this.autoLogout = SettingsUserStore.autoLogout;
|
this.autoLogout = SettingsUserStore.autoLogout;
|
||||||
this.autoLogoutTrigger = ko.observable(SaveSettingsStep.Idle);
|
|
||||||
|
|
||||||
let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params);
|
let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params);
|
||||||
this.autoLogoutOptions = ko.computed(() => {
|
this.autoLogoutOptions = koComputable(() => {
|
||||||
translatorTrigger();
|
translatorTrigger();
|
||||||
return [
|
return [
|
||||||
{ id: 0, name: i18nLogout('NEVER_OPTION_NAME') },
|
{ id: 0, name: i18nLogout('NEVER_OPTION_NAME') },
|
||||||
|
|
@ -32,10 +41,37 @@ export class SecurityUserSettings {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.capaAutoLogout) {
|
if (this.capaAutoLogout) {
|
||||||
this.autoLogout.subscribe(value => Remote.saveSetting(
|
this.addSetting('AutoLogout');
|
||||||
'AutoLogout', pInt(value),
|
|
||||||
settingsSaveHelperSimpleFunction(this.autoLogoutTrigger, this)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
|
||||||
|
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
|
||||||
|
|
||||||
|
this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
|
||||||
|
this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
showScreenPopup(OpenPgpImportPopupView);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOpenPgpKey() {
|
||||||
|
showScreenPopup(OpenPgpGeneratePopupView);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBuild() {
|
||||||
|
/**
|
||||||
|
* Create an iframe to display the Mailvelope keyring settings.
|
||||||
|
* The iframe will be injected into the container identified by selector.
|
||||||
|
*/
|
||||||
|
window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { TemplatePopupView } from 'View/Popup/Template';
|
import { TemplatePopupView } from 'View/Popup/Template';
|
||||||
import { addComputablesTo } from 'Common/Utils';
|
import { addComputablesTo } from 'Common/Utils';
|
||||||
|
|
||||||
export class TemplatesUserSettings {
|
export class UserSettingsTemplates {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.templates = TemplateUserStore.templates;
|
this.templates = TemplateUserStore.templates;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,37 @@
|
||||||
import ko from 'ko';
|
import { addObservablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { SaveSettingsStep, UploadErrorCode, Capa } from 'Common/Enums';
|
import { SaveSettingsStep, UploadErrorCode } from 'Common/Enums';
|
||||||
import { changeTheme, convertThemeName } from 'Common/Utils';
|
import { changeTheme, convertThemeName } from 'Common/Utils';
|
||||||
import { themePreviewLink, serverRequest } from 'Common/Links';
|
import { themePreviewLink, serverRequest } from 'Common/Links';
|
||||||
import { i18n } from 'Common/Translator';
|
import { i18n } from 'Common/Translator';
|
||||||
import { Settings } from 'Common/Globals';
|
import { SettingsCapa } from 'Common/Globals';
|
||||||
|
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
export class ThemesUserSettings {
|
const themeBackground = {
|
||||||
|
name: ThemeStore.userBackgroundName,
|
||||||
|
hash: ThemeStore.userBackgroundHash
|
||||||
|
};
|
||||||
|
addObservablesTo(themeBackground, {
|
||||||
|
uploaderButton: null,
|
||||||
|
loading: false,
|
||||||
|
error: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.theme = ThemeStore.theme;
|
this.theme = ThemeStore.theme;
|
||||||
this.themes = ThemeStore.themes;
|
this.themes = ThemeStore.themes;
|
||||||
this.themesObjects = ko.observableArray();
|
this.themesObjects = ko.observableArray();
|
||||||
|
|
||||||
this.background = {};
|
themeBackground.enabled = SettingsCapa('UserBackground');
|
||||||
this.background.name = ThemeStore.userBackgroundName;
|
this.background = themeBackground;
|
||||||
this.background.hash = ThemeStore.userBackgroundHash;
|
|
||||||
this.background.uploaderButton = ko.observable(null);
|
|
||||||
this.background.loading = ko.observable(false);
|
|
||||||
this.background.error = ko.observable('');
|
|
||||||
|
|
||||||
this.capaUserBackground = ko.observable(Settings.capa(Capa.UserBackground));
|
|
||||||
|
|
||||||
this.themeTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
|
this.themeTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
|
||||||
|
|
||||||
this.theme.subscribe((value) => {
|
ThemeStore.theme.subscribe(value => {
|
||||||
this.themesObjects.forEach(theme => {
|
this.themesObjects.forEach(theme => {
|
||||||
theme.selected(value === theme.name);
|
theme.selected(value === theme.name);
|
||||||
});
|
});
|
||||||
|
|
@ -41,10 +45,10 @@ export class ThemesUserSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild() {
|
onBuild() {
|
||||||
const currentTheme = this.theme();
|
const currentTheme = ThemeStore.theme();
|
||||||
|
|
||||||
this.themesObjects(
|
this.themesObjects(
|
||||||
this.themes.map(theme => ({
|
ThemeStore.themes.map(theme => ({
|
||||||
name: theme,
|
name: theme,
|
||||||
nameDisplay: convertThemeName(theme),
|
nameDisplay: convertThemeName(theme),
|
||||||
selected: ko.observable(theme === currentTheme),
|
selected: ko.observable(theme === currentTheme),
|
||||||
|
|
@ -54,28 +58,28 @@ export class ThemesUserSettings {
|
||||||
|
|
||||||
// initUploader
|
// initUploader
|
||||||
|
|
||||||
if (this.background.uploaderButton() && this.capaUserBackground()) {
|
if (themeBackground.uploaderButton() && themeBackground.enabled) {
|
||||||
const oJua = new Jua({
|
const oJua = new Jua({
|
||||||
action: serverRequest('UploadBackground'),
|
action: serverRequest('UploadBackground'),
|
||||||
limit: 1,
|
limit: 1,
|
||||||
clickElement: this.background.uploaderButton()
|
clickElement: themeBackground.uploaderButton()
|
||||||
});
|
});
|
||||||
|
|
||||||
oJua
|
oJua
|
||||||
.on('onStart', () => {
|
.on('onStart', () => {
|
||||||
this.background.loading(true);
|
themeBackground.loading(true);
|
||||||
this.background.error('');
|
themeBackground.error('');
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.on('onComplete', (id, result, data) => {
|
.on('onComplete', (id, result, data) => {
|
||||||
this.background.loading(false);
|
themeBackground.loading(false);
|
||||||
|
|
||||||
if (result && id && data && data.Result && data.Result.Name && data.Result.Hash) {
|
if (result && id && data && data.Result && data.Result.Name && data.Result.Hash) {
|
||||||
this.background.name(data.Result.Name);
|
themeBackground.name(data.Result.Name);
|
||||||
this.background.hash(data.Result.Hash);
|
themeBackground.hash(data.Result.Hash);
|
||||||
} else {
|
} else {
|
||||||
this.background.name('');
|
themeBackground.name('');
|
||||||
this.background.hash('');
|
themeBackground.hash('');
|
||||||
|
|
||||||
let errorMsg = '';
|
let errorMsg = '';
|
||||||
if (data.ErrorCode) {
|
if (data.ErrorCode) {
|
||||||
|
|
@ -94,7 +98,7 @@ export class ThemesUserSettings {
|
||||||
errorMsg = data.ErrorMessage;
|
errorMsg = data.ErrorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.background.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
|
themeBackground.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -103,14 +107,14 @@ export class ThemesUserSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.background.error('');
|
themeBackground.error('');
|
||||||
}
|
}
|
||||||
|
|
||||||
clearBackground() {
|
clearBackground() {
|
||||||
if (this.capaUserBackground()) {
|
if (themeBackground.enabled) {
|
||||||
Remote.clearUserBackground(() => {
|
Remote.request('ClearUserBackground', () => {
|
||||||
this.background.name('');
|
themeBackground.name('');
|
||||||
this.background.hash('');
|
themeBackground.hash('');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
185
dev/Sieve/Commands.js
Normal file
185
dev/Sieve/Commands.js
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5228#section-2.9
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { capa } from 'Sieve/Utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarCommand,
|
||||||
|
GrammarString,
|
||||||
|
GrammarStringList,
|
||||||
|
GrammarQuotedString
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5228#section-3.1
|
||||||
|
* Usage:
|
||||||
|
* if <test1: test> <block1: block>
|
||||||
|
* elsif <test2: test> <block2: block>
|
||||||
|
* else <block3: block>
|
||||||
|
*/
|
||||||
|
export class ConditionalCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.test = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return this.identifier + ' ' + this.test + ' ' + this.commands;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
public function pushArguments(array $args): void
|
||||||
|
{
|
||||||
|
print_r($args);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IfCommand extends ConditionalCommand
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ElsIfCommand extends ConditionalCommand
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ElseCommand extends ConditionalCommand
|
||||||
|
{
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return this.identifier + ' ' + this.commands;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5228#section-3.2
|
||||||
|
*/
|
||||||
|
export class RequireCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.capabilities = new GrammarStringList();
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'require ' + this.capabilities.toString() + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
if (args[0] instanceof GrammarStringList) {
|
||||||
|
this.capabilities = args[0];
|
||||||
|
} else if (args[0] instanceof GrammarQuotedString) {
|
||||||
|
this.capabilities.push(args[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5228#section-3.3
|
||||||
|
*/
|
||||||
|
export class StopCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5228#section-4.1
|
||||||
|
*/
|
||||||
|
export class FileIntoCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
// QuotedString / MultiLine
|
||||||
|
this._mailbox = new GrammarQuotedString();
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'fileinto'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'fileinto '
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3894
|
||||||
|
+ ((this.copy && capa.includes('copy')) ? ':copy ' : '')
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
|
||||||
|
+ ((this.create && capa.includes('mailbox')) ? ':create ' : '')
|
||||||
|
+ this._mailbox
|
||||||
|
+ ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
get mailbox()
|
||||||
|
{
|
||||||
|
return this._mailbox.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set mailbox(value)
|
||||||
|
{
|
||||||
|
this._mailbox.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
if (args[0] instanceof GrammarString) {
|
||||||
|
this._mailbox = args[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5228#section-4.2
|
||||||
|
*/
|
||||||
|
export class RedirectCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
// QuotedString / MultiLine
|
||||||
|
this._address = new GrammarQuotedString();
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'redirect '
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3894
|
||||||
|
+ ((this.copy && capa.includes('copy')) ? ':copy ' : '')
|
||||||
|
+ this._address
|
||||||
|
+ ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
get address()
|
||||||
|
{
|
||||||
|
return this._address.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set address(value)
|
||||||
|
{
|
||||||
|
this._address.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
if (args[0] instanceof GrammarString) {
|
||||||
|
this._address = args[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5228#section-4.3
|
||||||
|
*/
|
||||||
|
export class KeepCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5228#section-4.4
|
||||||
|
*/
|
||||||
|
export class DiscardCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
}
|
||||||
45
dev/Sieve/Extensions/rfc5173.js
Normal file
45
dev/Sieve/Extensions/rfc5173.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5173
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarString,
|
||||||
|
GrammarStringList,
|
||||||
|
GrammarTest
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
export class BodyTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.body_transform = ''; // :raw, :content <string-list>, :text
|
||||||
|
this.key_list = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'body'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'body'
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ ' ' + this.body_transform
|
||||||
|
+ ' ' + this.key_list.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (':raw' === arg || ':text' === arg) {
|
||||||
|
this.body_transform = arg;
|
||||||
|
} else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
|
||||||
|
if (':content' === args[i-1]) {
|
||||||
|
this.body_transform = ':content ' + arg;
|
||||||
|
} else {
|
||||||
|
this[args[i+1] ? 'content_list' : 'key_list'] = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
36
dev/Sieve/Extensions/rfc5183.js
Normal file
36
dev/Sieve/Extensions/rfc5183.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5183
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarStringList,
|
||||||
|
GrammarTest
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
export class EnvironmentTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.name = new GrammarQuotedString;
|
||||||
|
this.key_list = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'environment'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'environment'
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ ' ' + this.name
|
||||||
|
+ ' ' + this.key_list.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.key_list = args.pop();
|
||||||
|
this.name = args.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
71
dev/Sieve/Extensions/rfc5229.js
Normal file
71
dev/Sieve/Extensions/rfc5229.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5229
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarCommand,
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarStringList,
|
||||||
|
GrammarTest
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
export class SetCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.modifiers = [];
|
||||||
|
this._name = new GrammarQuotedString;
|
||||||
|
this._value = new GrammarQuotedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'variables'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'set'
|
||||||
|
+ ' ' + this.modifiers.join(' ')
|
||||||
|
+ ' ' + this._name
|
||||||
|
+ ' ' + this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return this._name.value; }
|
||||||
|
set name(str) { this._name.value = str; }
|
||||||
|
|
||||||
|
get value() { return this._value.value; }
|
||||||
|
set value(str) { this._value.value = str; }
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
|
||||||
|
args.includes(modifier) && this.modifiers.push(modifier);
|
||||||
|
});
|
||||||
|
this._value = args.pop();
|
||||||
|
this._name = args.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StringTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.source = new GrammarStringList;
|
||||||
|
this.key_list = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'string'
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.source.toString()
|
||||||
|
+ ' ' + this.key_list.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.key_list = args.pop();
|
||||||
|
this.source = args.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
dev/Sieve/Extensions/rfc5230.js
Normal file
87
dev/Sieve/Extensions/rfc5230.js
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5230
|
||||||
|
* https://tools.ietf.org/html/rfc6131
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { capa } from 'Sieve/Utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarCommand,
|
||||||
|
GrammarNumber,
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarStringList
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
export class VacationCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._days = new GrammarNumber;
|
||||||
|
this._seconds = new GrammarNumber;
|
||||||
|
this._subject = new GrammarQuotedString;
|
||||||
|
this._from = new GrammarQuotedString;
|
||||||
|
this.addresses = new GrammarStringList;
|
||||||
|
this.mime = false;
|
||||||
|
this._handle = new GrammarQuotedString;
|
||||||
|
this._reason = new GrammarQuotedString; // QuotedString / MultiLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// get require() { return ['vacation','vacation-seconds']; }
|
||||||
|
get require() { return 'vacation'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
let result = 'vacation';
|
||||||
|
if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
|
||||||
|
result += ' :seconds ' + this._seconds;
|
||||||
|
} else if (0 < this._days.value) {
|
||||||
|
result += ' :days ' + this._days;
|
||||||
|
}
|
||||||
|
if (this._subject.length) {
|
||||||
|
result += ' :subject ' + this._subject;
|
||||||
|
}
|
||||||
|
if (this._from.length) {
|
||||||
|
result += ' :from ' + this.arguments[':from'];
|
||||||
|
}
|
||||||
|
if (this.addresses.length) {
|
||||||
|
result += ' :addresses ' + this.addresses.toString();
|
||||||
|
}
|
||||||
|
if (this.mime) {
|
||||||
|
result += ' :mime';
|
||||||
|
}
|
||||||
|
if (this._handle.length) {
|
||||||
|
result += ' :handle ' + this._handle;
|
||||||
|
}
|
||||||
|
return result + ' ' + this._reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
get days() { return this._days.value; }
|
||||||
|
get seconds() { return this._seconds.value; }
|
||||||
|
get subject() { return this._subject.value; }
|
||||||
|
get from() { return this._from.value; }
|
||||||
|
get handle() { return this._handle.value; }
|
||||||
|
get reason() { return this._reason.value; }
|
||||||
|
|
||||||
|
set days(int) { this._days.value = int; }
|
||||||
|
set seconds(int) { this._seconds.value = int; }
|
||||||
|
set subject(str) { this._subject.value = str; }
|
||||||
|
set from(str) { this._from.value = str; }
|
||||||
|
set handle(str) { this._handle.value = str; }
|
||||||
|
set reason(str) { this._reason.value = str; }
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this._reason.value = args.pop().value; // GrammarQuotedString
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (':mime' === arg) {
|
||||||
|
this.mime = true;
|
||||||
|
} else if (':addresses' === args[i-1]) {
|
||||||
|
this.addresses = arg; // GrammarStringList
|
||||||
|
} else if (':' === args[i-1][0]) {
|
||||||
|
// :days, :seconds, :subject, :from, :handle
|
||||||
|
this[args[i-1].replace(':','_')].value = arg.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
94
dev/Sieve/Extensions/rfc5232.js
Normal file
94
dev/Sieve/Extensions/rfc5232.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5232
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarCommand,
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarString,
|
||||||
|
GrammarStringList,
|
||||||
|
GrammarTest
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
class FlagCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._variablename = new GrammarQuotedString;
|
||||||
|
this.list_of_flags = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'imap4flags'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return this.identifier + ' ' + this._variablename + ' ' + this.list_of_flags.toString() + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
get variablename()
|
||||||
|
{
|
||||||
|
return this._variablename.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set variablename(value)
|
||||||
|
{
|
||||||
|
this._variablename.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
if (args[1]) {
|
||||||
|
if (args[0] instanceof GrammarQuotedString) {
|
||||||
|
this._variablename = args[0];
|
||||||
|
}
|
||||||
|
if (args[1] instanceof GrammarString) {
|
||||||
|
this.list_of_flags = args[1];
|
||||||
|
}
|
||||||
|
} else if (args[0] instanceof GrammarString) {
|
||||||
|
this.list_of_flags = args[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SetFlagCommand extends FlagCommand
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddFlagCommand extends FlagCommand
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoveFlagCommand extends FlagCommand
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasFlagTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.variable_list = new GrammarStringList;
|
||||||
|
this.list_of_flags = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'imap4flags'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'hasflag'
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.variable_list.toString()
|
||||||
|
+ ' ' + this.list_of_flags.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
|
||||||
|
this[args[i+1] ? 'variable_list' : 'list_of_flags'] = arg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
70
dev/Sieve/Extensions/rfc5235.js
Normal file
70
dev/Sieve/Extensions/rfc5235.js
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5235
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarString,
|
||||||
|
GrammarTest
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
export class SpamTestTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.percent = false, // 0 - 100 else 0 - 10
|
||||||
|
this.value = new GrammarQuotedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
|
||||||
|
get require() { return /:value|:count/.test(this.match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'spamtest'
|
||||||
|
+ (this.percent ? ' :percent' : '')
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ ' ' + this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
args.forEach(arg => {
|
||||||
|
if (':percent' === arg) {
|
||||||
|
this.percent = true;
|
||||||
|
} else if (arg instanceof GrammarString) {
|
||||||
|
this.value = arg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VirusTestTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.value = new GrammarQuotedString; // 1 - 5
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'virustest'
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ ' ' + this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
args.forEach(arg => {
|
||||||
|
if (arg instanceof GrammarString) {
|
||||||
|
this.value = arg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
93
dev/Sieve/Extensions/rfc5260.js
Normal file
93
dev/Sieve/Extensions/rfc5260.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5260
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarNumber,
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarStringList,
|
||||||
|
GrammarTest
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
export class DateTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.zone = new GrammarQuotedString;
|
||||||
|
this.originalzone = false;
|
||||||
|
this.header_name = new GrammarQuotedString;
|
||||||
|
this.date_part = new GrammarQuotedString;
|
||||||
|
this.key_list = new GrammarStringList;
|
||||||
|
// rfc5260#section-6
|
||||||
|
this.index = new GrammarNumber;
|
||||||
|
this.last = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get require() { return ['date','index']; }
|
||||||
|
get require() { return 'date'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'date'
|
||||||
|
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||||
|
+ (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : ''))
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ ' ' + this.header_name
|
||||||
|
+ ' ' + this.date_part
|
||||||
|
+ ' ' + this.key_list.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.key_list = args.pop();
|
||||||
|
this.date_part = args.pop();
|
||||||
|
this.header_name = args.pop();
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (':originalzone' === arg) {
|
||||||
|
this.originalzone = true;
|
||||||
|
} else if (':last' === arg) {
|
||||||
|
this.last = true;
|
||||||
|
} else if (':zone' === args[i-1]) {
|
||||||
|
this.zone.value = arg.value;
|
||||||
|
} else if (':index' === args[i-1]) {
|
||||||
|
this.index.value = arg.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CurrentDateTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.zone = new GrammarQuotedString;
|
||||||
|
this.date_part = new GrammarQuotedString;
|
||||||
|
this.key_list = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'date'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'currentdate'
|
||||||
|
+ (this.zone.length ? ' :zone ' + this.zone : '')
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ ' ' + this.date_part
|
||||||
|
+ ' ' + this.key_list.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.key_list = args.pop();
|
||||||
|
this.date_part = args.pop();
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (':zone' === args[i-1]) {
|
||||||
|
this.zone.value = arg.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
85
dev/Sieve/Extensions/rfc5293.js
Normal file
85
dev/Sieve/Extensions/rfc5293.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5293
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarCommand,
|
||||||
|
GrammarNumber,
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarString,
|
||||||
|
GrammarStringList
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
export class AddHeaderCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.last = false;
|
||||||
|
this.field_name = new GrammarQuotedString;
|
||||||
|
this.value = new GrammarQuotedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'editheader'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return this.identifier
|
||||||
|
+ (this.last ? ' :last' : '')
|
||||||
|
+ ' ' + this.field_name
|
||||||
|
+ ' ' + this.value + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.value = args.pop();
|
||||||
|
this.field_name = args.pop();
|
||||||
|
this.last = args.includes(':last');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeleteHeaderCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.index = new GrammarNumber;
|
||||||
|
this.last = false;
|
||||||
|
this.comparator = '',
|
||||||
|
this.match_type = ':is',
|
||||||
|
this.field_name = new GrammarQuotedString;
|
||||||
|
this.value_patterns = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'editheader'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return this.identifier
|
||||||
|
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ ' ' + this.match_type
|
||||||
|
+ ' ' + this.field_name
|
||||||
|
+ ' ' + this.value_patterns + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
let l = args.length - 1;
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (':last' === arg) {
|
||||||
|
this.last = true;
|
||||||
|
} else if (':index' === args[i-1]) {
|
||||||
|
this.index.value = arg.value;
|
||||||
|
args[i] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (args[l-1] instanceof GrammarString) {
|
||||||
|
this.field_name = args[l-1];
|
||||||
|
this.value_patterns = args[l];
|
||||||
|
} else {
|
||||||
|
this.field_name = args[l];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
dev/Sieve/Extensions/rfc5429.js
Normal file
81
dev/Sieve/Extensions/rfc5429.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5429
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarCommand,
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarString
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5429#section-2.1
|
||||||
|
*/
|
||||||
|
export class ErejectCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._reason = new GrammarQuotedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'ereject'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'ereject ' + this._reason + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
get reason()
|
||||||
|
{
|
||||||
|
return this._reason.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set reason(value)
|
||||||
|
{
|
||||||
|
this._reason.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
if (args[0] instanceof GrammarString) {
|
||||||
|
this._reason = args[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5429#section-2.2
|
||||||
|
*/
|
||||||
|
export class RejectCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._reason = new GrammarQuotedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'reject'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'reject ' + this._reason + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
get reason()
|
||||||
|
{
|
||||||
|
return this._reason.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set reason(value)
|
||||||
|
{
|
||||||
|
this._reason.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
if (args[0] instanceof GrammarString) {
|
||||||
|
this._reason = args[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
dev/Sieve/Extensions/rfc5435.js
Normal file
123
dev/Sieve/Extensions/rfc5435.js
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5435
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarCommand,
|
||||||
|
GrammarNumber,
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarStringList,
|
||||||
|
GrammarTest
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc5435#section-3
|
||||||
|
*/
|
||||||
|
export class NotifyCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._method = new GrammarQuotedString;
|
||||||
|
this._from = new GrammarQuotedString;
|
||||||
|
this._importance = new GrammarNumber;
|
||||||
|
this.options = new GrammarStringList;
|
||||||
|
this._message = new GrammarQuotedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
get method() { return this._method.value; }
|
||||||
|
get from() { return this._from.value; }
|
||||||
|
get importance() { return this._importance.value; }
|
||||||
|
get message() { return this._message.value; }
|
||||||
|
|
||||||
|
set method(str) { this._method.value = str; }
|
||||||
|
set from(str) { this._from.value = str; }
|
||||||
|
set importance(int) { this._importance.value = int; }
|
||||||
|
set message(str) { this._message.value = str; }
|
||||||
|
|
||||||
|
get require() { return 'enotify'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
let result = 'notify';
|
||||||
|
if (this._from.value) {
|
||||||
|
result += ' :from ' + this._from;
|
||||||
|
}
|
||||||
|
if (0 < this._importance.value) {
|
||||||
|
result += ' :importance ' + this._importance;
|
||||||
|
}
|
||||||
|
if (this.options.length) {
|
||||||
|
result += ' :options ' + this.options;
|
||||||
|
}
|
||||||
|
if (this._message.value) {
|
||||||
|
result += ' :message ' + this._message;
|
||||||
|
}
|
||||||
|
return result + ' ' + this._method;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this._method.value = args.pop().value; // GrammarQuotedString
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (':options' === args[i-1]) {
|
||||||
|
this.options = arg; // GrammarStringList
|
||||||
|
} else if (':' === args[i-1][0]) {
|
||||||
|
// :from, :importance, :message
|
||||||
|
this[args[i-1].replace(':','_')].value = arg.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc5435#section-4
|
||||||
|
*/
|
||||||
|
export class ValidNotifyMethodTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.notification_uris = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'valid_notify_method ' + this.notification_uris;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.notification_uris = args.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc5435#section-5
|
||||||
|
*/
|
||||||
|
export class NotifyMethodCapabilityTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.notification_uri = new GrammarQuotedString;
|
||||||
|
this.notification_capability = new GrammarQuotedString;
|
||||||
|
this.key_list = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'valid_notify_method '
|
||||||
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
|
+ (this.match_type ? ' ' + this.match_type : '')
|
||||||
|
+ this.notification_uri
|
||||||
|
+ this.notification_capability
|
||||||
|
+ this.key_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.key_list = args.pop();
|
||||||
|
this.notification_capability = args.pop();
|
||||||
|
this.notification_uri = args.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
58
dev/Sieve/Extensions/rfc5463.js
Normal file
58
dev/Sieve/Extensions/rfc5463.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5463
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrammarCommand,
|
||||||
|
GrammarTest,
|
||||||
|
GrammarQuotedString,
|
||||||
|
GrammarStringList
|
||||||
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc5463#section-4
|
||||||
|
*/
|
||||||
|
export class IHaveTest extends GrammarTest
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.capabilities = new GrammarStringList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'ihave'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'ihave ' + this.capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.capabilities = args.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc5463#section-5
|
||||||
|
*/
|
||||||
|
export class ErrorCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.message = new GrammarQuotedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return 'ihave'; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return 'error ' + this.message + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
pushArguments(args)
|
||||||
|
{
|
||||||
|
this.message = args.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
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