mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-09-20 07:35:55 +08:00
Merge branch 'master' into plugin-hooks
# Conflicts: # dev/Common/Plugins.js # dev/Knoin/Knoin.js # dev/Remote/AbstractFetch.js # dev/View/User/Login.js
This commit is contained in:
commit
827cfa5051
|
@ -15,7 +15,7 @@ RUN pecl install xxtea-1.0.11 && \
|
|||
|
||||
RUN docker-php-ext-configure intl && \
|
||||
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
|
||||
|
||||
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 && \
|
||||
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
||||
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 && \
|
||||
php -m
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
; SnappyMail Webmail configuration file
|
||||
; SnappyMail configuration file
|
||||
; Please don't add custom parameters here, those will be overwritten
|
||||
|
||||
[webmail]
|
||||
|
@ -27,38 +27,27 @@ allow_languages_on_settings = On
|
|||
allow_additional_accounts = On
|
||||
allow_additional_identities = On
|
||||
|
||||
; Number of messages displayed on page by default
|
||||
; Number of messages displayed on page by default
|
||||
messages_per_page = 20
|
||||
|
||||
; Mark message read after N seconds
|
||||
message_read_delay = 5
|
||||
|
||||
; File size limit (MB) for file upload on compose screen
|
||||
; 0 for unlimited.
|
||||
attachment_size_limit = 25
|
||||
attachment_size_limit = 2
|
||||
|
||||
[interface]
|
||||
show_attachment_thumbnail = On
|
||||
use_native_scrollbars = Off
|
||||
new_move_to_folder_button = On
|
||||
|
||||
[branding]
|
||||
login_logo = ""
|
||||
login_background = ""
|
||||
login_desc = ""
|
||||
login_css = ""
|
||||
user_css = ""
|
||||
user_logo = ""
|
||||
user_logo_title = ""
|
||||
user_logo_message = ""
|
||||
user_iframe_message = ""
|
||||
welcome_page_url = ""
|
||||
welcome_page_display = "none"
|
||||
|
||||
[contacts]
|
||||
; Enable contacts
|
||||
enable = Off
|
||||
allow_sync = On
|
||||
allow_sync = Off
|
||||
sync_interval = 20
|
||||
type = "sqlite"
|
||||
pdo_dsn = "mysql:host=127.0.0.1;port=3306;dbname=snappymail"
|
||||
pdo_dsn = "host=127.0.0.1;port=3306;dbname=snappymail"
|
||||
pdo_user = "root"
|
||||
pdo_password = ""
|
||||
suggestions_limit = 30
|
||||
|
@ -67,23 +56,23 @@ suggestions_limit = 30
|
|||
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
||||
csrf_protection = On
|
||||
custom_server_signature = "SnappyMail"
|
||||
x_frame_options_header = ""
|
||||
x_frame_options_header = "DENY"
|
||||
x_xss_protection_header = "1; mode=block"
|
||||
openpgp = Off
|
||||
|
||||
; Login and password for web admin panel
|
||||
admin_login = "admin"
|
||||
admin_password = "12345"
|
||||
admin_password = ""
|
||||
admin_totp = ""
|
||||
|
||||
; Access settings
|
||||
allow_admin_panel = On
|
||||
allow_two_factor_auth = Off
|
||||
force_two_factor_auth = Off
|
||||
hide_x_mailer_header = Off
|
||||
hide_x_mailer_header = On
|
||||
admin_panel_host = ""
|
||||
admin_panel_key = "admin"
|
||||
content_security_policy = ""
|
||||
core_install_access_domain = ""
|
||||
csp_report = Off
|
||||
encrypt_cipher = "aes-256-cbc-hmac-sha1"
|
||||
|
||||
[ssl]
|
||||
; Require verification of SSL certificate used.
|
||||
|
@ -102,20 +91,12 @@ capath = ""
|
|||
client_cert = ""
|
||||
|
||||
[capa]
|
||||
folders = On
|
||||
composer = On
|
||||
contacts = On
|
||||
settings = On
|
||||
quota = On
|
||||
help = On
|
||||
reload = On
|
||||
search = On
|
||||
search_adv = On
|
||||
filters = On
|
||||
x-templates = Off
|
||||
dangerous_actions = On
|
||||
message_actions = On
|
||||
messagelist_actions = On
|
||||
attachments_actions = On
|
||||
|
||||
[login]
|
||||
|
@ -125,10 +106,7 @@ default_domain = ""
|
|||
allow_languages_on_login = On
|
||||
determine_user_language = On
|
||||
determine_user_domain = Off
|
||||
welcome_page = Off
|
||||
hide_submit_button = On
|
||||
forgot_password_link_url = ""
|
||||
registration_link_url = ""
|
||||
login_lowercase = On
|
||||
|
||||
; This option allows webmail to remember the logged in user
|
||||
|
@ -155,6 +133,7 @@ view_editor_type = "Html"
|
|||
view_layout = 1
|
||||
view_use_checkboxes = On
|
||||
autologout = 30
|
||||
view_html = On
|
||||
show_images = Off
|
||||
contacts_autosave = On
|
||||
mail_use_threads = Off
|
||||
|
@ -165,6 +144,17 @@ mail_reply_same_folder = Off
|
|||
; Enable logging
|
||||
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)
|
||||
write_on_error_only = Off
|
||||
|
||||
|
@ -177,9 +167,8 @@ write_on_timeout_only = 0
|
|||
; Required for development purposes only.
|
||||
; Disabling this option is not recommended.
|
||||
hide_passwords = On
|
||||
time_offset = "0"
|
||||
time_zone = "UTC"
|
||||
session_filter = ""
|
||||
sentry_dsn = ""
|
||||
|
||||
; Log filename.
|
||||
; For security reasons, some characters are removed from filename.
|
||||
|
@ -207,6 +196,7 @@ sentry_dsn = ""
|
|||
; filename = "log-{date:Y-m-d}.txt"
|
||||
; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log"
|
||||
; filename = "{user:email}-{date:Y-m-d}.txt"
|
||||
; filename = "syslog"
|
||||
filename = "log-{date:Y-m-d}.txt"
|
||||
|
||||
; Enable auth logging in a separate file (for fail2ban)
|
||||
|
@ -214,35 +204,13 @@ auth_logging = Off
|
|||
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}"
|
||||
|
||||
; Enable auth logging to syslog for fail2ban
|
||||
auth_syslog = On
|
||||
|
||||
[debug]
|
||||
; Special option required for development purposes
|
||||
enable = Off
|
||||
|
||||
[social]
|
||||
; Google
|
||||
google_enable = Off
|
||||
google_enable_auth = Off
|
||||
google_enable_auth_gmail = Off
|
||||
google_enable_drive = Off
|
||||
google_enable_preview = Off
|
||||
google_client_id = ""
|
||||
google_client_secret = ""
|
||||
google_api_key = ""
|
||||
|
||||
; Facebook
|
||||
fb_enable = Off
|
||||
fb_app_id = ""
|
||||
fb_app_secret = ""
|
||||
|
||||
; Twitter
|
||||
twitter_enable = Off
|
||||
twitter_consumer_key = ""
|
||||
twitter_consumer_secret = ""
|
||||
|
||||
; Dropbox
|
||||
dropbox_enable = Off
|
||||
dropbox_api_key = ""
|
||||
|
||||
[cache]
|
||||
; The section controls caching of the entire application.
|
||||
;
|
||||
|
@ -252,7 +220,7 @@ enable = On
|
|||
; Additional caching key. If changed, cache is purged
|
||||
index = "v1"
|
||||
|
||||
; Can be: files, APC, memcache, redis (beta)
|
||||
; Can be: files, APCU, memcache, redis (beta)
|
||||
fast_cache_driver = "files"
|
||||
|
||||
; Additional caching key. If changed, fast cache is purged
|
||||
|
@ -268,33 +236,21 @@ http_expires = 3600
|
|||
server_uids = On
|
||||
|
||||
[labs]
|
||||
; Experimental settings. Handle with care.
|
||||
;
|
||||
allow_mobile_version = On
|
||||
ignore_folders_subscription = Off
|
||||
check_new_password_strength = On
|
||||
update_channel = "stable"
|
||||
allow_gravatar = On
|
||||
allow_prefetch = On
|
||||
allow_smart_html_links = On
|
||||
allow_prefetch = Off
|
||||
cache_system_data = On
|
||||
date_from_headers = On
|
||||
autocreate_system_folders = On
|
||||
autocreate_system_folders = Off
|
||||
allow_message_append = Off
|
||||
disable_iconv_if_mbstring_supported = Off
|
||||
login_fault_delay = 1
|
||||
log_ajax_response_write_limit = 300
|
||||
allow_html_editor_source_button = Off
|
||||
allow_html_editor_biti_buttons = Off
|
||||
allow_ctrl_enter_on_compose = On
|
||||
try_to_detect_hidden_images = Off
|
||||
hide_dangerous_actions = Off
|
||||
use_app_debug_js = Off
|
||||
use_mobile_version_for_tablets = Off
|
||||
use_app_debug_css = Off
|
||||
use_imap_sort = On
|
||||
use_imap_force_selection = Off
|
||||
use_imap_list_subscribe = On
|
||||
use_imap_thread = On
|
||||
use_imap_move = Off
|
||||
use_imap_expunge_all_on_delete = Off
|
||||
|
@ -309,51 +265,41 @@ imap_message_all_headers = Off
|
|||
imap_large_thread_limit = 50
|
||||
imap_folder_list_limit = 200
|
||||
imap_show_login_alert = On
|
||||
imap_use_auth_plain = On
|
||||
imap_use_auth_cram_md5 = Off
|
||||
imap_use_list_status = On
|
||||
imap_timeout = 300
|
||||
smtp_show_server_errors = Off
|
||||
smtp_use_auth_plain = On
|
||||
smtp_use_auth_cram_md5 = Off
|
||||
sieve_allow_raw_script = Off
|
||||
sieve_utf8_folder_name = On
|
||||
smtp_timeout = 60
|
||||
sieve_auth_plain_initial = On
|
||||
sieve_allow_fileinto_inbox = Off
|
||||
imap_timeout = 300
|
||||
smtp_timeout = 60
|
||||
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_additional_parameters = Off
|
||||
favicon_status = On
|
||||
folders_spec_limit = 50
|
||||
owncloud_save_folder = "Attachments"
|
||||
owncloud_suggestions = On
|
||||
curl_proxy = ""
|
||||
curl_proxy_auth = ""
|
||||
in_iframe = Off
|
||||
force_https = Off
|
||||
custom_login_link = ""
|
||||
custom_logout_link = ""
|
||||
allow_external_login = Off
|
||||
allow_external_sso = Off
|
||||
external_sso_key = ""
|
||||
http_client_ip_check_proxy = Off
|
||||
fast_cache_memcache_host = "127.0.0.1"
|
||||
fast_cache_memcache_port = 11211
|
||||
fast_cache_redis_host = "127.0.0.1"
|
||||
fast_cache_redis_port = 6379
|
||||
use_local_proxy_for_external_images = Off
|
||||
use_local_proxy_for_external_images = On
|
||||
detect_image_exif_orientation = On
|
||||
cookie_default_path = ""
|
||||
cookie_default_secure = Off
|
||||
check_new_messages = On
|
||||
replace_env_in_configuration = ""
|
||||
startup_url = ""
|
||||
strict_html_parser = Off
|
||||
allow_cmd = Off
|
||||
boundary_prefix = ""
|
||||
kolab_enabled = Off
|
||||
dev_email = ""
|
||||
dev_password = ""
|
||||
|
||||
[version]
|
||||
current = "1.14.0"
|
||||
saved = "Wed, 08 Apr 2020 16:37:27 +0000"
|
||||
current = "2.13.4"
|
||||
saved = "Fri, 04 Mar 2022 08:55:26 +0000"
|
||||
|
|
|
@ -9,7 +9,7 @@ module.exports = {
|
|||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es6: true
|
||||
es2020: true
|
||||
},
|
||||
globals: {
|
||||
// SnappyMail
|
||||
|
@ -34,10 +34,10 @@ module.exports = {
|
|||
'Crossroads': "readonly",
|
||||
// vendors/jua
|
||||
'Jua': "readonly",
|
||||
// vendors/qr.js
|
||||
'qr': "readonly",
|
||||
// vendors/bootstrap/bootstrap.native.js
|
||||
'BSN': "readonly"
|
||||
'BSN': "readonly",
|
||||
// Mailvelope
|
||||
'mailvelope': "readonly"
|
||||
},
|
||||
// http://eslint.org/docs/rules/
|
||||
rules: {
|
||||
|
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1,3 +1,2 @@
|
|||
github: the-djmaze
|
||||
community_bridge: SnappyMail
|
||||
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -18,3 +18,11 @@
|
|||
/include.php
|
||||
.idea/
|
||||
.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
|
57
.htaccess
57
.htaccess
|
@ -1,6 +1,8 @@
|
|||
RewriteEngine On
|
||||
# Redirect cPanel
|
||||
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
# Redirect cPanel
|
||||
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
|
@ -18,51 +20,18 @@ RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
|||
</IfModule>
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
Header set Cache-Control "public, max-age=31536000"
|
||||
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
|
||||
Header set Referrer-Policy "no-referrer"
|
||||
# Header set Cache-Control "public, max-age=31536000"
|
||||
# Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
|
||||
# Header set Referrer-Policy "no-referrer"
|
||||
Header set Strict-Transport-Security "max-age=31536000"
|
||||
Header set imagetoolbar "no"
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
Header set X-Frame-Options "DENY"
|
||||
Header set X-XSS-Protection "1; mode=block"
|
||||
# Header set X-Content-Type-Options "nosniff"
|
||||
# Header set X-Frame-Options "DENY"
|
||||
# Header set X-XSS-Protection "1; mode=block"
|
||||
Header set Service-Worker-Allowed "/"
|
||||
|
||||
RewriteCond %{HTTP:Accept-encoding} br
|
||||
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
||||
RewriteRule "^(.+)" "$1\.br" [L,T=text/javascript,QSA]
|
||||
|
||||
RewriteCond %{HTTP:Accept-encoding} gzip
|
||||
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
|
||||
RewriteRule "^(.+)" "$1\.gz" [L,T=text/javascript,QSA]
|
||||
|
||||
RewriteCond %{HTTP:Accept-encoding} br
|
||||
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
||||
RewriteRule "^(.+)" "$1\.br" [L,T=text/css,QSA]
|
||||
|
||||
RewriteCond %{HTTP:Accept-encoding} gzip
|
||||
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
|
||||
RewriteRule "^(.+)" "$1\.gz" [L,T=text/css,QSA]
|
||||
|
||||
<FilesMatch "(\.js\.br|\.css\.br)$">
|
||||
SetEnv no-gzip 1
|
||||
SetEnv no-brotli 1
|
||||
# Serve correct encoding type.
|
||||
Header append Content-Encoding br
|
||||
# Force proxies to cache brotli &
|
||||
# non-brotli css/js files separately.
|
||||
Header append Vary Accept-Encoding
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "(\.js\.gz|\.css\.gz)$">
|
||||
SetEnv no-gzip 1
|
||||
SetEnv no-brotli 1
|
||||
# Serve correct encoding type.
|
||||
Header append Content-Encoding gzip
|
||||
# Force proxies to cache gzipped &
|
||||
# non-gzipped css/js files separately.
|
||||
Header append Vary Accept-Encoding
|
||||
</FilesMatch>
|
||||
# Google FLoC
|
||||
# Header set Permissions-Policy "interest-cohort=()"
|
||||
</IfModule>
|
||||
|
||||
#<IfModule mod_brotli.c>
|
||||
|
|
15
.tx/config
15
.tx/config
|
@ -1,15 +0,0 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
minimum_perc = 60
|
||||
type = YAML
|
||||
|
||||
[snappymail-webmail.snappymail-webmail]
|
||||
file_filter = snappymail/v/0.0.0/app/localization/webmail/<lang>.yml
|
||||
source_file = snappymail/v/0.0.0/app/localization/webmail/_source.en.yml
|
||||
source_lang = en
|
||||
|
||||
[snappymail-webmail.snappymail-admin]
|
||||
file_filter = snappymail/v/0.0.0/app/localization/admin/<lang>.yml
|
||||
source_file = snappymail/v/0.0.0/app/localization/admin/_source.en.yml
|
||||
source_lang = en
|
||||
|
|
@ -7,14 +7,15 @@
|
|||
|
||||
**Getting started**
|
||||
|
||||
1. Install node.js - `https://nodejs.org/download/`
|
||||
2. Install yarn - `https://yarnpkg.com/en/docs/install`
|
||||
3. Install gulp - `npm install gulp -g`
|
||||
4. Fork snappymail - `https://github.com/the-djmaze/snappymail/issues/new#fork-destination-box`
|
||||
5. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
|
||||
6. `cd snappymail`
|
||||
7. Install install all dependencies - `yarn install`
|
||||
8. Run gulp - `gulp`
|
||||
1. Install PHP
|
||||
2. Install node.js - `https://nodejs.org/download/`
|
||||
3. Install yarn - `https://yarnpkg.com/en/docs/install`
|
||||
4. Install gulp - `npm install gulp -g`
|
||||
5. Fork snappymail from https://github.com/the-djmaze/snappymail
|
||||
6. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
|
||||
7. `cd snappymail`
|
||||
8. Install all dependencies - `yarn install`
|
||||
9. Run gulp - `gulp`
|
||||
|
||||
---
|
||||
|
||||
|
@ -30,6 +31,17 @@
|
|||
1. Edit data/\_data_/\_default_/configs/application.ini
|
||||
2. Set 'cache_system_data' to Off
|
||||
|
||||
**Release**
|
||||
|
||||
1. Install gzip
|
||||
2. Install brotli
|
||||
3. php release.php
|
||||
|
||||
Options:
|
||||
* `php release.php --aur` = Build Arch Linux package
|
||||
* `php release.php --docker` = Build Docker instance
|
||||
* `php release.php --plugins` = Build plugins
|
||||
|
||||
---
|
||||
|
||||
If you have any questions, open an issue or email support@snappymail.eu.
|
||||
|
|
54
Makefile
54
Makefile
|
@ -1,75 +1,75 @@
|
|||
#!make
|
||||
|
||||
rebuild: _down
|
||||
docker-compose build --no-cache
|
||||
docker compose build --no-cache
|
||||
|
||||
up: _up status
|
||||
_up:
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
|
||||
stop: _stop status
|
||||
_stop:
|
||||
docker-compose stop
|
||||
docker compose stop
|
||||
|
||||
down: _down status
|
||||
_down:
|
||||
docker-compose down
|
||||
docker compose down
|
||||
|
||||
restart: _stop _up status
|
||||
|
||||
status:
|
||||
@docker-compose ps
|
||||
@docker compose ps
|
||||
|
||||
tx:
|
||||
@docker-compose run --no-deps --rm tx tx pull -a -s -f -d
|
||||
@docker compose run --no-deps --rm tx tx pull -a -s -f -d
|
||||
|
||||
console-node:
|
||||
@docker-compose run --no-deps --rm node sh
|
||||
@docker compose run --no-deps --rm node sh
|
||||
console-tx:
|
||||
@docker-compose run --no-deps --rm tx sh
|
||||
@docker compose run --no-deps --rm tx sh
|
||||
console-php:
|
||||
@docker-compose exec php sh
|
||||
@docker compose exec php sh
|
||||
console: console-node
|
||||
|
||||
logs:
|
||||
@docker-compose logs --tail=100 -f
|
||||
@docker compose logs --tail=100 -f
|
||||
logs-db:
|
||||
@docker-compose logs --tail=100 -f db
|
||||
@docker compose logs --tail=100 -f db
|
||||
logs-php:
|
||||
@docker-compose logs --tail=100 -f php
|
||||
@docker compose logs --tail=100 -f php
|
||||
logs-node:
|
||||
@docker-compose logs --tail=100 -f node
|
||||
@docker compose logs --tail=100 -f node
|
||||
logs-nginx:
|
||||
@docker-compose logs --tail=100 -f nginx
|
||||
@docker compose logs --tail=100 -f nginx
|
||||
logs-mail:
|
||||
@docker-compose logs --tail=100 -f mail
|
||||
@docker compose logs --tail=100 -f mail
|
||||
logs-tx:
|
||||
@docker-compose logs --tail=100 -f tx
|
||||
@docker compose logs --tail=100 -f tx
|
||||
|
||||
rl-lint:
|
||||
@docker-compose run --no-deps --rm node gulp lint
|
||||
@docker compose run --no-deps --rm node gulp lint
|
||||
rl-dev:
|
||||
@docker-compose run --no-deps --rm node npm run watch-js
|
||||
@docker compose run --no-deps --rm node npm run watch-js
|
||||
rl-compile:
|
||||
@docker-compose run --no-deps --rm node gulp build
|
||||
@docker compose run --no-deps --rm node gulp build
|
||||
rl-compile-with-source:
|
||||
@docker-compose run --no-deps --rm node gulp build --source
|
||||
@docker compose run --no-deps --rm node gulp build --source
|
||||
rl-watch-css:
|
||||
@docker-compose run --no-deps --rm node npm run watch-css
|
||||
@docker compose run --no-deps --rm node npm run watch-css
|
||||
rl-watch-js:
|
||||
@docker-compose run --no-deps --rm node npm run watch-js
|
||||
@docker compose run --no-deps --rm node npm run watch-js
|
||||
|
||||
rl-build:
|
||||
@docker-compose run --no-deps --rm node gulp all
|
||||
@docker compose run --no-deps --rm node gulp all
|
||||
rl-build-pro:
|
||||
@docker-compose run --no-deps --rm node gulp all --pro
|
||||
@docker compose run --no-deps --rm node gulp all --pro
|
||||
|
||||
yarn-install:
|
||||
@docker-compose run --no-deps --rm node yarn install
|
||||
@docker compose run --no-deps --rm node yarn install
|
||||
yarn-outdated:
|
||||
@docker-compose run --no-deps --rm node yarn outdated
|
||||
@docker compose run --no-deps --rm node yarn outdated
|
||||
yarn-upgrade:
|
||||
@docker-compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
|
||||
@docker compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
|
||||
|
||||
gpg:
|
||||
docker run -it --rm -w=/var/www \
|
||||
|
|
118
README.md
118
README.md
|
@ -1,6 +1,6 @@
|
|||
<div align="center">
|
||||
<a href="https://github.com/the-djmaze/snappymail">
|
||||
<img width="200" heigth="200" src="https://snappymail.eu/static/img/logo-256x256.png">
|
||||
<img src="https://snappymail.eu/static/img/logo-256x256-white.png">
|
||||
</a>
|
||||
<br>
|
||||
<h1>SnappyMail</h1>
|
||||
|
@ -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)**.
|
||||
http://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
Copyright (c) 2020 - 2021 SnappyMail
|
||||
Copyright (c) 2020 - 2022 SnappyMail
|
||||
Copyright (c) 2013 - 2021 RainLoop
|
||||
|
||||
## Modifications
|
||||
|
@ -45,24 +45,25 @@ This fork of RainLoop has the following changes:
|
|||
* Admin uses password_hash/password_verify
|
||||
* Auth failed attempts written to syslog
|
||||
* Added Fail2ban instructions
|
||||
* ES2015
|
||||
* ES2018
|
||||
* PHP 7.3+ required
|
||||
* PHP mbstring extension required
|
||||
* 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"
|
||||
* Removed BackwardCapability (class \RainLoop\Account)
|
||||
* Removed ChangePassword (re-implemented as plugin)
|
||||
* Removed OAuth support
|
||||
* Removed POP3 support
|
||||
* Removed background video support
|
||||
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
||||
* Removed Spyc yaml
|
||||
* Replaced gulp-uglify with gulp-terser
|
||||
* CRLF => LF line endings
|
||||
* Embed boot.js and boot.css into index.html
|
||||
* 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
|
||||
* Split Admin specific JavaScript code from User code
|
||||
* Split Sieve specific JavaScript code from User code
|
||||
* JSON reviver
|
||||
* Better memory garbage collection management
|
||||
* Added serviceworker for Notifications
|
||||
|
@ -71,27 +72,49 @@ This fork of RainLoop has the following changes:
|
|||
* Replaced webpack with rollup
|
||||
* No user-agent detection (use device width)
|
||||
* Added support to load plugins as .phar
|
||||
* Replaced old Sabre library
|
||||
* AddressBook Contacts support MySQL/MariaDB utf8mb4
|
||||
* Prevent Google FLoC
|
||||
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
||||
* 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
|
||||
|
||||
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy.
|
||||
Supported are:
|
||||
|
||||
* Chrome 69+
|
||||
* Edge 79+
|
||||
* Firefox 69+
|
||||
* Opera 56+
|
||||
* Safari 12+
|
||||
|
||||
|
||||
### Removal of old JavaScript
|
||||
|
||||
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer.
|
||||
|
||||
The result is faster and smaller download code (good for mobile networks).
|
||||
|
||||
Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill for array.flat().
|
||||
|
||||
* Added dev/prototype.js for some additional features
|
||||
* boot.js without webpack overhead
|
||||
* Modified Jua.js to be without jQuery
|
||||
* Replaced ProgressJS with simple native dropin
|
||||
* Replaced Autolinker with simple https/email detection
|
||||
* 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 bootstrap.js with native drop-in replacement
|
||||
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
|
||||
* Replaced *Ajax with *Fetch classes because we use the Fetch API, not jQuery.ajax
|
||||
* Replaced knockoutjs 3.4 with a modified 3.5.1
|
||||
* Replaced [knockoutjs](https://github.com/knockout/knockout) 3.4 with a modified 3.5.1
|
||||
* Replaced knockout-sortable with native HTML5 drag&drop
|
||||
* Replaced simplestatemanager with CSS @media
|
||||
* Replaced inputosaurus with own code
|
||||
|
@ -110,31 +133,35 @@ Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill
|
|||
* Removed momentjs (use Intl)
|
||||
* Removed opentip (use CSS)
|
||||
* Removed non-community (aka Prem/Premium/License) code
|
||||
* Removed ProgressJS
|
||||
|
||||
|
||||
RainLoop 1.15 vs SnappyMail
|
||||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.158.025 | 94.549 |
|
||||
|app.js |4.215.733 | 458.762 |
|
||||
|boot.js | 672.433 | 4.554 |
|
||||
|libs.js | 647.679 | 218.511 |
|
||||
|admin.js |2.158.025 | 79.018 |
|
||||
|app.js |4.215.733 | 407.697 |
|
||||
|boot.js | 672.433 | 2.025 |
|
||||
|libs.js | 647.679 | 200.131 |
|
||||
|sieve.js | 0 | 75.642 |
|
||||
|polyfills.js | 325.908 | 0 |
|
||||
|serviceworker.js | 0 | 285 |
|
||||
|TOTAL |8.019.778 | 776.661 |
|
||||
|TOTAL |8.019.778 | 764.798 |
|
||||
|
||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|admin.min.js | 255.514 | 49.871 | 73.899 | 14.771 | 60.674 | 13.213 |
|
||||
|app.min.js | 516.000 | 235.474 |140.430 | 69.223 |110.657 | 58.398 |
|
||||
|boot.min.js | 66.456 | 2.442 | 22.553 | 1.371 | 20.043 | 1.178 |
|
||||
|libs.min.js | 574.626 | 106.574 |177.280 | 38.600 |151.855 | 34.532 |
|
||||
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|
||||
|TOTAL |1.445.204 | 394.361 |425.477 |123.965 |353.301 |107.321 |
|
||||
|TOTAL (no admin) |1.189.690 | 344.490 |351.061 |109.194 |292.627 | 94.108 |
|
||||
|admin.min.js | 255.514 | 39.256 | 73.899 | 13.076 | 60.674 | 11.702 |
|
||||
|app.min.js | 516.000 | 194.148 |140.430 | 62.297 |110.657 | 53.432 |
|
||||
|boot.min.js | 66.456 | 1.252 | 22.553 | 782 | 20.043 | 631 |
|
||||
|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 |
|
||||
|TOTAL user |1.189.690 | 291.601 |351.061 | 98.601 |292.627 | 85.809 |
|
||||
|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 66% smaller and faster than traditional RainLoop.
|
||||
For a user its around 70% smaller and faster than traditional RainLoop.
|
||||
|
||||
### CSS changes
|
||||
|
||||
|
@ -142,7 +169,7 @@ For a user its around 66% smaller and faster than traditional RainLoop.
|
|||
* Themes work in mobile mode
|
||||
* Bugfix invalid/conflicting css rules
|
||||
* Use flexbox
|
||||
* Split app.css to have seperate admin.css
|
||||
* Split app.css to have separate admin.css
|
||||
* Remove oldschool 'float'
|
||||
* Remove unused css
|
||||
* Removed html.no-css
|
||||
|
@ -151,13 +178,7 @@ For a user its around 66% smaller and faster than traditional RainLoop.
|
|||
* Removed Internet Explorer from normalize.css
|
||||
* Removed node_modules/opentip/css/opentip.css
|
||||
* Removed node_modules/pikaday/css/pikaday.css
|
||||
* Removed vendors/bootstrap/less/breadcrumbs.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 unused vendors/bootstrap/less/*
|
||||
* Removed vendors/jquery-nanoscroller/nanoscroller.css
|
||||
* Removed vendors/jquery-letterfx/jquery-letterfx.min.css
|
||||
* Removed vendors/Progress.js/minified/progressjs.min.css
|
||||
|
@ -165,13 +186,28 @@ For a user its around 66% smaller and faster than traditional RainLoop.
|
|||
|
||||
|
||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||
|------------ |-------: |-------: |------: |------: |--------: |
|
||||
|app.css | 340.334 | 106.864 | 46,959 | 18.751 | 16.110 |
|
||||
|app.min.css | 274.791 | 88.077 | 39.618 | 16.788 | 14.787 |
|
||||
|boot.css | | 2.066 | | 913 | 742 |
|
||||
|boot.min.css | | 1.696 | | 818 | 664 |
|
||||
|admin.css | | 46.337 | | 9.221 | 8.058 |
|
||||
|admin.min.css | | 37.251 | | 8.178 | 7.269 |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.334 | 80.865 | 46.959 | 16.751 | 14.420 |
|
||||
|app.min.css | 274.791 | 65.086 | 39.618 | 14.855 | 13.088 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 29.977 | | 6.795 | 5.900 |
|
||||
|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
|
||||
|
@ -184,7 +220,7 @@ Still TODO:
|
|||
|
||||
| | normal | min | gzip | min gzip |
|
||||
|-------- |-------: |-------: |------: |--------: |
|
||||
|squire | 128.826 | 47.074 | 33.671 | 15.596 |
|
||||
|squire | 122.321 | 41.906 | 31.867 | 14.330 |
|
||||
|ckeditor | ? | 520.035 | ? | 155.916 |
|
||||
|
||||
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
|
||||
|
|
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.
|
36
_include.php
36
_include.php
|
@ -4,29 +4,33 @@
|
|||
|
||||
//header('Strict-Transport-Security: max-age=31536000');
|
||||
|
||||
// Uncomment to use gzip encoded output
|
||||
/**
|
||||
* Uncomment to use gzip compressed output
|
||||
*/
|
||||
//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);
|
||||
|
||||
/**
|
||||
* Custom 'data' folder path
|
||||
* @return string
|
||||
* Uncomment to disable APCU.
|
||||
*/
|
||||
function __get_custom_data_full_path()
|
||||
{
|
||||
return '';
|
||||
return dirname(__DIR__) . '/snappymail-data';
|
||||
return '/var/external-snappymail-data-folder';
|
||||
}
|
||||
//define('APP_USE_APCU_CACHE', false);
|
||||
|
||||
/**
|
||||
* Custom 'data' folder path
|
||||
*/
|
||||
//define('APP_DATA_FOLDER_PATH', dirname(__DIR__) . '/snappymail-data/');
|
||||
//define('APP_DATA_FOLDER_PATH', '/var/external-snappymail-data-folder/');
|
||||
|
||||
/**
|
||||
* Additional configuration file name
|
||||
* @return string
|
||||
*/
|
||||
function __get_additional_configuration_name()
|
||||
{
|
||||
return '';
|
||||
return defined('APP_SITE') && 0 < strlen(APP_SITE) ? APP_SITE.'.ini' : '';
|
||||
}
|
||||
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
|
||||
|
|
50
assets/.htaccess
Normal file
50
assets/.htaccess
Normal file
|
@ -0,0 +1,50 @@
|
|||
<IfModule mod_headers.c>
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
RewriteCond %{HTTP:Accept-encoding} br
|
||||
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
||||
RewriteRule "^(.+\.js)$" "$1\.br" [L,T=application/javascript,QSA]
|
||||
|
||||
RewriteCond %{HTTP:Accept-encoding} gzip
|
||||
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
|
||||
RewriteRule "^(.+\.js)$" "$1\.gz" [L,T=application/javascript,QSA]
|
||||
|
||||
RewriteCond %{HTTP:Accept-encoding} br
|
||||
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
||||
RewriteRule "^(.+\.css)$" "$1\.br" [L,T=text/css,QSA]
|
||||
|
||||
RewriteCond %{HTTP:Accept-encoding} gzip
|
||||
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
|
||||
RewriteRule "^(.+\.css)$" "$1\.gz" [L,T=text/css,QSA]
|
||||
|
||||
<FilesMatch "(\.js\.br|\.css\.br)$">
|
||||
SetEnv no-gzip 1
|
||||
SetEnv no-brotli 1
|
||||
# Serve correct encoding type.
|
||||
Header append Content-Encoding br
|
||||
# Force proxies to cache brotli &
|
||||
# non-brotli css/js files separately.
|
||||
Header append Vary Accept-Encoding
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "(\.js\.gz|\.css\.gz)$">
|
||||
SetEnv no-gzip 1
|
||||
SetEnv no-brotli 1
|
||||
# Serve correct encoding type.
|
||||
Header append Content-Encoding gzip
|
||||
# Force proxies to cache gzipped &
|
||||
# non-gzipped css/js files separately.
|
||||
Header append Vary Accept-Encoding
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "(\.js\.br|\.js\.gz)$">
|
||||
Header set Content-Type "application/javascript; charset=utf-8"
|
||||
ForceType application/javascript
|
||||
</FilesMatch>
|
||||
<FilesMatch "(\.css\.br|\.css\.gz)$">
|
||||
Header set Content-Type "text/css; charset=utf-8"
|
||||
ForceType text/css
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
</IfModule>
|
BIN
assets/sounds/alert.mp3
Normal file
BIN
assets/sounds/alert.mp3
Normal file
Binary file not shown.
BIN
assets/sounds/alert.ogg
Normal file
BIN
assets/sounds/alert.ogg
Normal file
Binary file not shown.
BIN
assets/sounds/ping.mp3
Normal file
BIN
assets/sounds/ping.mp3
Normal file
Binary file not shown.
BIN
assets/sounds/ping.ogg
Normal file
BIN
assets/sounds/ping.ogg
Normal file
Binary file not shown.
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 {
|
||||
elementById,
|
||||
Settings
|
||||
} from 'Common/Globals';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { changeTheme } from 'Common/Utils';
|
||||
|
||||
import { logoutLink } from 'Common/Links';
|
||||
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
||||
|
@ -25,12 +23,9 @@ export class AbstractApp {
|
|||
this.Remote = Remote;
|
||||
}
|
||||
|
||||
logoutReload(close = false) {
|
||||
logoutReload() {
|
||||
const url = logoutLink();
|
||||
|
||||
rl.hash.clear();
|
||||
close && window.close && window.close();
|
||||
|
||||
if (location.href !== url) {
|
||||
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 100);
|
||||
} else {
|
||||
|
@ -38,25 +33,27 @@ export class AbstractApp {
|
|||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
// rl.adminArea() || !translatorReload(false, );
|
||||
rl.adminArea() || (
|
||||
LanguageStore.language(SettingsGet('Language'))
|
||||
& ThemeStore.populate()
|
||||
& changeTheme(SettingsGet('Theme'))
|
||||
);
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
bootstart() {
|
||||
const register = (key, ClassObject, templateID) => ko.components.register(key, {
|
||||
template: { element: templateID || (key + 'Component') },
|
||||
viewModel: {
|
||||
createViewModel: (params, componentInfo) => {
|
||||
params = params || {};
|
||||
params.element = null;
|
||||
|
||||
if (componentInfo && componentInfo.element) {
|
||||
params.component = componentInfo;
|
||||
params.element = componentInfo.element;
|
||||
|
||||
i18nToNodes(componentInfo.element);
|
||||
|
||||
if (params.inline && ko.unwrap(params.inline)) {
|
||||
params.element.style.display = 'inline-block';
|
||||
}
|
||||
i18nToNodes(componentInfo.element);
|
||||
if (params.inline) {
|
||||
componentInfo.element.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
return new ClassObject(params);
|
||||
}
|
||||
}
|
||||
|
@ -72,14 +69,7 @@ export class AbstractApp {
|
|||
|
||||
LanguageStore.populate();
|
||||
ThemeStore.populate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
hideLoading() {
|
||||
elementById('rl-content').hidden = false;
|
||||
elementById('rl-loading').remove();
|
||||
this.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'External/Admin/ko';
|
||||
import 'External/ko';
|
||||
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
|
||||
|
@ -16,12 +16,8 @@ class AdminApp extends AbstractApp {
|
|||
this.weakPassword = ko.observable(false);
|
||||
}
|
||||
|
||||
bootstart() {
|
||||
super.bootstart();
|
||||
|
||||
this.hideLoading();
|
||||
|
||||
if (!Settings.app('allowAdminPanel')) {
|
||||
start() {
|
||||
if (!Settings.app('adminAllowed')) {
|
||||
rl.route.root();
|
||||
setTimeout(() => location.href = '/', 1);
|
||||
} else if (SettingsGet('Auth')) {
|
||||
|
@ -30,8 +26,6 @@ class AdminApp extends AbstractApp {
|
|||
} else {
|
||||
startScreens([LoginAdminScreen]);
|
||||
}
|
||||
|
||||
progressJs.end();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
940
dev/App/User.js
940
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 { doc } from 'Common/Globals';
|
||||
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
|
||||
|
||||
let notificator = null,
|
||||
player = null,
|
||||
|
@ -12,7 +12,7 @@ let notificator = null,
|
|||
player.src = url;
|
||||
player.play();
|
||||
name = name.trim();
|
||||
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'}));
|
||||
fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -73,8 +73,7 @@ export const SMAudio = new class {
|
|||
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
|
||||
if (player) {
|
||||
const stopFn = () => this.pause();
|
||||
player.addEventListener('ended', stopFn);
|
||||
player.addEventListener('error', stopFn);
|
||||
addEventsListener(player, ['ended','error'], stopFn);
|
||||
addEventListener('audio.api.stop', stopFn);
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +88,7 @@ export const SMAudio = new class {
|
|||
|
||||
pause() {
|
||||
player && player.pause();
|
||||
dispatchEvent(new CustomEvent('audio.stop'));
|
||||
fireEvent('audio.stop');
|
||||
}
|
||||
|
||||
playMp3(url, name) {
|
||||
|
@ -106,11 +105,11 @@ export const SMAudio = new class {
|
|||
|
||||
playNotification(silent) {
|
||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||
if (!notificator) {
|
||||
notificator = createNewObject();
|
||||
notificator.src = Links.staticLink('sounds/new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
|
||||
}
|
||||
notificator = notificator || createNewObject();
|
||||
if (notificator) {
|
||||
notificator.src = Links.staticLink('sounds/'
|
||||
+ SettingsGet('NotificationSound')
|
||||
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
||||
notificator.volume = silent ? 0.01 : 1;
|
||||
notificator.play();
|
||||
}
|
||||
|
|
|
@ -1,174 +1,157 @@
|
|||
import { MessageSetAction } from 'Common/EnumsUser';
|
||||
import { isNonEmptyArray, pInt } from 'Common/Utils';
|
||||
import { isArray } from 'Common/Utils';
|
||||
|
||||
let FOLDERS_CACHE = {},
|
||||
FOLDERS_NAME_CACHE = {},
|
||||
FOLDERS_HASH_CACHE = {},
|
||||
FOLDERS_UID_NEXT_CACHE = {},
|
||||
MESSAGE_FLAGS_CACHE = {},
|
||||
NEW_MESSAGE_CACHE = {},
|
||||
REQUESTED_MESSAGE_CACHE = {},
|
||||
inboxFolderName = 'INBOX';
|
||||
|
||||
const REQUESTED_MESSAGE_CACHE = {};
|
||||
export const
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clearCache = () => {
|
||||
FOLDERS_CACHE = {};
|
||||
FOLDERS_NAME_CACHE = {};
|
||||
MESSAGE_FLAGS_CACHE = {};
|
||||
NEW_MESSAGE_CACHE = {};
|
||||
REQUESTED_MESSAGE_CACHE = {};
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
export function clear() {
|
||||
FOLDERS_CACHE = {};
|
||||
FOLDERS_NAME_CACHE = {};
|
||||
FOLDERS_HASH_CACHE = {};
|
||||
FOLDERS_UID_NEXT_CACHE = {};
|
||||
MESSAGE_FLAGS_CACHE = {};
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @returns {string}
|
||||
*/
|
||||
getMessageKey = (folderFullName, uid) => `${folderFullName}#${uid}`,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} uid
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getMessageKey(folderFullNameRaw, uid) {
|
||||
return `${folderFullNameRaw}#${uid}`;
|
||||
}
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
*/
|
||||
addRequestedMessage = (folder, uid) => REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true,
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
*/
|
||||
export function addRequestedMessage(folder, uid) {
|
||||
REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true;
|
||||
}
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasRequestedMessage(folder, uid) {
|
||||
return true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)];
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
*/
|
||||
addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} uid
|
||||
*/
|
||||
export function addNewMessageCache(folderFullNameRaw, uid) {
|
||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true;
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
*/
|
||||
hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
|
||||
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
|
||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} uid
|
||||
*/
|
||||
export function hasNewMessageAndRemoveFromCache(folderFullNameRaw, uid) {
|
||||
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
|
||||
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clearNewMessageCache = () => NEW_MESSAGE_CACHE = {},
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
export function clearNewMessageCache() {
|
||||
NEW_MESSAGE_CACHE = {};
|
||||
}
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderInboxName = () => inboxFolderName,
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFolderInboxName() {
|
||||
return inboxFolderName;
|
||||
}
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
setFolderInboxName = name => inboxFolderName = name,
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function setFolderInboxName(name) {
|
||||
inboxFolderName = name;
|
||||
}
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderFullName = folderHash =>
|
||||
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
|
||||
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFolderFullNameRaw(folderHash) {
|
||||
return folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '';
|
||||
}
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @param {string} folderFullName
|
||||
* @param {?FolderModel} folder
|
||||
*/
|
||||
setFolder = folder => {
|
||||
folder.hash = '';
|
||||
FOLDERS_CACHE[folder.fullName] = folder;
|
||||
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {?FolderModel} folder
|
||||
*/
|
||||
export function setFolder(folderHash, folderFullNameRaw, folder) {
|
||||
FOLDERS_CACHE[folderFullNameRaw] = folder;
|
||||
FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderHash = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFolderHash(folderFullNameRaw) {
|
||||
return folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '';
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} folderHash
|
||||
*/
|
||||
setFolderHash = (folderFullName, folderHash) =>
|
||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} folderHash
|
||||
*/
|
||||
export function setFolderHash(folderFullNameRaw, folderHash) {
|
||||
if (folderFullNameRaw) {
|
||||
FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {string}
|
||||
*/
|
||||
getFolderUidNext = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFolderUidNext(folderFullNameRaw) {
|
||||
return folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
|
||||
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
|
||||
: '';
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uidNext
|
||||
*/
|
||||
setFolderUidNext = (folderFullName, uidNext) =>
|
||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @param {string} uidNext
|
||||
*/
|
||||
export function setFolderUidNext(folderFullNameRaw, uidNext) {
|
||||
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext;
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {?FolderModel}
|
||||
*/
|
||||
getFolderFromCacheList = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
* @returns {?FolderModel}
|
||||
*/
|
||||
export function getFolderFromCacheList(folderFullNameRaw) {
|
||||
return folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folderFullNameRaw
|
||||
*/
|
||||
export function removeFolderFromCacheList(folderFullNameRaw) {
|
||||
delete FOLDERS_CACHE[folderFullNameRaw];
|
||||
}
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
*/
|
||||
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
|
||||
|
||||
export class MessageFlagsCache
|
||||
{
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @param {string} flag
|
||||
* @returns {bool}
|
||||
*/
|
||||
static hasFlag(folderFullName, uid, flag) {
|
||||
return MESSAGE_FLAGS_CACHE[folderFullName]
|
||||
&& MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||
&& MESSAGE_FLAGS_CACHE[folderFullName][uid].includes(flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @returns {?Array}
|
||||
*/
|
||||
static getFor(folderFullName, uid) {
|
||||
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||
: null;
|
||||
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,11 +159,13 @@ export class MessageFlagsCache
|
|||
* @param {string} uid
|
||||
* @param {Array} flagsCache
|
||||
*/
|
||||
static setFor(folderFullName, uid, flagsCache) {
|
||||
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||
static setFor(folderFullName, uid, flags) {
|
||||
if (isArray(flags)) {
|
||||
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||
}
|
||||
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
|
||||
}
|
||||
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,39 +181,24 @@ export class MessageFlagsCache
|
|||
static initMessage(message) {
|
||||
if (message) {
|
||||
const uid = message.uid,
|
||||
flags = this.getFor(message.folder, uid);
|
||||
flags = this.getFor(message.folder, uid),
|
||||
thread = message.threads;
|
||||
|
||||
if (flags && flags.length) {
|
||||
message.isFlagged(!!flags[1]);
|
||||
|
||||
if (!message.isSimpleMessage) {
|
||||
message.isUnseen(!!flags[0]);
|
||||
message.isAnswered(!!flags[2]);
|
||||
message.isForwarded(!!flags[3]);
|
||||
message.isReadReceipt(!!flags[4]);
|
||||
message.isDeleted(!!flags[5]);
|
||||
}
|
||||
if (isArray(flags)) {
|
||||
message.flags(flags);
|
||||
}
|
||||
|
||||
if (message.threads.length) {
|
||||
const unseenSubUid = message.threads.find(sSubUid => {
|
||||
if (uid !== sSubUid) {
|
||||
const subFlags = this.getFor(message.folder, sSubUid);
|
||||
return subFlags && subFlags.length && !!subFlags[0];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (thread.length) {
|
||||
const unseenSubUid = thread.find(iSubUid =>
|
||||
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
|
||||
);
|
||||
|
||||
const flaggedSubUid = message.threads.find(sSubUid => {
|
||||
if (uid !== sSubUid) {
|
||||
const subFlags = this.getFor(message.folder, sSubUid);
|
||||
return subFlags && subFlags.length && !!subFlags[1];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const flaggedSubUid = thread.find(iSubUid =>
|
||||
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
|
||||
);
|
||||
|
||||
message.hasUnseenSubMessage(unseenSubUid && 0 < pInt(unseenSubUid));
|
||||
message.hasFlaggedSubMessage(flaggedSubUid && 0 < pInt(flaggedSubUid));
|
||||
message.hasUnseenSubMessage(!!unseenSubUid);
|
||||
message.hasFlaggedSubMessage(!!flaggedSubUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -238,25 +208,7 @@ export class MessageFlagsCache
|
|||
*/
|
||||
static store(message) {
|
||||
if (message) {
|
||||
this.setFor(message.folder, message.uid, [
|
||||
message.isUnseen(),
|
||||
message.isFlagged(),
|
||||
message.isAnswered(),
|
||||
message.isForwarded(),
|
||||
message.isReadReceipt(),
|
||||
message.isDeleted()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {string} uid
|
||||
* @param {Array} flags
|
||||
*/
|
||||
static storeByFolderAndUid(folder, uid, flags) {
|
||||
if (isNonEmptyArray(flags)) {
|
||||
this.setFor(folder, uid, flags);
|
||||
this.setFor(message.folder, message.uid, message.flags());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,33 +218,30 @@ export class MessageFlagsCache
|
|||
* @param {number} setAction
|
||||
*/
|
||||
static storeBySetAction(folder, uid, setAction) {
|
||||
let unread = 0;
|
||||
const flags = this.getFor(folder, uid);
|
||||
let flags = this.getFor(folder, uid) || [];
|
||||
const
|
||||
unread = flags.includes('\\seen') ? 0 : 1,
|
||||
add = item => flags.includes(item) || flags.push(item),
|
||||
remove = item => flags = flags.filter(flag => flag != item);
|
||||
|
||||
if (isNonEmptyArray(flags)) {
|
||||
if (flags[0]) {
|
||||
unread = 1;
|
||||
}
|
||||
|
||||
switch (setAction) {
|
||||
case MessageSetAction.SetSeen:
|
||||
flags[0] = false;
|
||||
break;
|
||||
case MessageSetAction.UnsetSeen:
|
||||
flags[0] = true;
|
||||
break;
|
||||
case MessageSetAction.SetFlag:
|
||||
flags[1] = true;
|
||||
break;
|
||||
case MessageSetAction.UnsetFlag:
|
||||
flags[1] = false;
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
this.setFor(folder, uid, flags);
|
||||
switch (setAction) {
|
||||
case MessageSetAction.SetSeen:
|
||||
add('\\seen');
|
||||
break;
|
||||
case MessageSetAction.UnsetSeen:
|
||||
remove('\\seen');
|
||||
break;
|
||||
case MessageSetAction.SetFlag:
|
||||
add('\\flagged');
|
||||
break;
|
||||
case MessageSetAction.UnsetFlag:
|
||||
remove('\\flagged');
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
this.setFor(folder, uid, flags);
|
||||
|
||||
return unread;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
export const MESSAGES_PER_PAGE_VALUES = [10, 20, 30, 50, 100];
|
||||
|
||||
export const UNUSED_OPTION_VALUE = '__UNUSE__';
|
||||
|
|
|
@ -1,85 +1,53 @@
|
|||
/* eslint quote-props: 0 */
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const Capa = {
|
||||
OpenPGP: 'OPEN_PGP',
|
||||
Prefetch: 'PREFETCH',
|
||||
Composer: 'COMPOSER',
|
||||
Contacts: 'CONTACTS',
|
||||
Reload: 'RELOAD',
|
||||
Search: 'SEARCH',
|
||||
SearchAdv: 'SEARCH_ADV',
|
||||
MessageActions: 'MESSAGE_ACTIONS',
|
||||
MessageListActions: 'MESSAGELIST_ACTIONS',
|
||||
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
|
||||
DangerousActions: 'DANGEROUS_ACTIONS',
|
||||
Settings: 'SETTINGS',
|
||||
Help: 'HELP',
|
||||
Themes: 'THEMES',
|
||||
UserBackground: 'USER_BACKGROUND',
|
||||
Sieve: 'SIEVE',
|
||||
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
|
||||
Templates: 'TEMPLATES',
|
||||
AutoLogout: 'AUTOLOGOUT',
|
||||
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
|
||||
Identities: 'IDENTITIES'
|
||||
};
|
||||
export const
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const Scope = {
|
||||
All: 'all',
|
||||
None: 'none',
|
||||
Contacts: 'Contacts',
|
||||
Scope = {
|
||||
MessageList: 'MessageList',
|
||||
FolderList: 'FolderList',
|
||||
MessageView: 'MessageView',
|
||||
Compose: 'Compose',
|
||||
Settings: 'Settings',
|
||||
Menu: 'Menu',
|
||||
ComposeOpenPgp: 'ComposeOpenPgp',
|
||||
MessageOpenPgp: 'MessageOpenPgp',
|
||||
ViewOpenPgpKey: 'ViewOpenPgpKey',
|
||||
KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
|
||||
Ask: 'Ask'
|
||||
};
|
||||
Settings: 'Settings'
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const UploadErrorCode = {
|
||||
UploadErrorCode = {
|
||||
Normal: 0,
|
||||
FileIsTooBig: 1,
|
||||
FilePartiallyUploaded: 2,
|
||||
NoFileUploaded: 3,
|
||||
MissingTempFolder: 4,
|
||||
OnSavingFile: 5,
|
||||
FilePartiallyUploaded: 3,
|
||||
NoFileUploaded: 4,
|
||||
MissingTempFolder: 6,
|
||||
OnSavingFile: 7,
|
||||
FileType: 98,
|
||||
Unknown: 99
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const SaveSettingsStep = {
|
||||
SaveSettingsStep = {
|
||||
Animate: -2,
|
||||
Idle: -1,
|
||||
TrueResult: 1,
|
||||
FalseResult: 0
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const Notification = {
|
||||
Notification = {
|
||||
RequestError: 1,
|
||||
RequestAborted: 2,
|
||||
|
||||
// Global
|
||||
InvalidToken: 101,
|
||||
AuthError: 102,
|
||||
|
||||
// User
|
||||
ConnectionError: 104,
|
||||
DomainNotAllowed: 109,
|
||||
AccountNotAllowed: 110,
|
||||
|
@ -110,20 +78,15 @@ export const Notification = {
|
|||
CantDeleteNonEmptyFolder: 405,
|
||||
|
||||
// CantSaveSettings: 501,
|
||||
CantSavePluginSettings: 502,
|
||||
|
||||
DomainAlreadyExists: 601,
|
||||
|
||||
CantInstallPackage: 701,
|
||||
CantDeletePackage: 702,
|
||||
InvalidPluginPackage: 703,
|
||||
UnsupportedPluginPackage: 704,
|
||||
|
||||
DemoSendMessageError: 750,
|
||||
DemoAccountError: 751,
|
||||
|
||||
AccountAlreadyExists: 801,
|
||||
AccountDoesNotExist: 802,
|
||||
AccountSwitchFailed: 803,
|
||||
|
||||
MailServerError: 901,
|
||||
ClientViewError: 902,
|
||||
|
@ -134,5 +97,12 @@ export const Notification = {
|
|||
// JsonTimeout: 953,
|
||||
|
||||
UnknownNotification: 998,
|
||||
UnknownError: 999
|
||||
UnknownError: 999,
|
||||
|
||||
// Admin
|
||||
CantInstallPackage: 701,
|
||||
CantDeletePackage: 702,
|
||||
InvalidPluginPackage: 703,
|
||||
UnsupportedPluginPackage: 704,
|
||||
CantSavePluginSettings: 705
|
||||
};
|
||||
|
|
|
@ -4,14 +4,28 @@
|
|||
* @enum {number}
|
||||
*/
|
||||
export const FolderType = {
|
||||
Inbox: 10,
|
||||
SentItems: 11,
|
||||
Draft: 12,
|
||||
Trash: 13,
|
||||
Spam: 14,
|
||||
Archive: 15,
|
||||
NotSpam: 80,
|
||||
User: 99
|
||||
User: 0,
|
||||
Inbox: 1,
|
||||
Sent: 2,
|
||||
Drafts: 3,
|
||||
Spam: 4, // JUNK
|
||||
Trash: 5,
|
||||
Archive: 6,
|
||||
NotSpam: 80
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const FolderMetadataKeys = {
|
||||
// RFC 5464
|
||||
Comment: '/private/comment',
|
||||
CommentShared: '/shared/comment',
|
||||
// RFC 6154
|
||||
SpecialUse: '/private/specialuse',
|
||||
// Kolab
|
||||
KolabFolderType: '/private/vendor/kolab/folder-type',
|
||||
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -34,13 +48,13 @@ export const FolderSortMode = {
|
|||
* @enum {string}
|
||||
*/
|
||||
export const ComposeType = {
|
||||
Empty: 'empty',
|
||||
Reply: 'reply',
|
||||
ReplyAll: 'replyall',
|
||||
Forward: 'forward',
|
||||
ForwardAsAttachment: 'forward-as-attachment',
|
||||
Draft: 'draft',
|
||||
EditAsNew: 'editasnew'
|
||||
Empty: 0,
|
||||
Reply: 1,
|
||||
ReplyAll: 2,
|
||||
Forward: 3,
|
||||
ForwardAsAttachment: 4,
|
||||
Draft: 5,
|
||||
EditAsNew: 6
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -58,19 +72,14 @@ export const SetSystemFoldersNotification = {
|
|||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const ClientSideKeyName = {
|
||||
FoldersLashHash: 0,
|
||||
MessagesInboxLastHash: 1,
|
||||
MailBoxListSize: 2,
|
||||
ExpandedFolders: 3,
|
||||
FolderListSize: 4,
|
||||
MessageListSize: 5,
|
||||
LastReplyAction: 6,
|
||||
LastSignMe: 7,
|
||||
ComposeLastIdentityID: 8,
|
||||
MessageHeaderFullInfo: 9,
|
||||
MessageAttachmentControls: 10
|
||||
};
|
||||
export const
|
||||
ClientSideKeyNameExpandedFolders = 3,
|
||||
ClientSideKeyNameFolderListSize = 4,
|
||||
ClientSideKeyNameMessageListSize = 5,
|
||||
ClientSideKeyNameLastReplyAction = 6,
|
||||
ClientSideKeyNameLastSignMe = 7,
|
||||
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
||||
ClientSideKeyNameMessageAttachmentControls = 10;
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint key-spacing: 0 */
|
||||
/* eslint quote-props: 0 */
|
||||
|
||||
import { isNonEmptyArray } from 'Common/Utils';
|
||||
import { arrayLength } from 'Common/Utils';
|
||||
|
||||
const
|
||||
cache = {},
|
||||
|
@ -9,6 +9,7 @@ const
|
|||
msOffice = app+'vnd.openxmlformats-officedocument.',
|
||||
openDoc = app+'vnd.oasis.opendocument.',
|
||||
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
|
||||
lowerCase = text => text.toLowerCase().trim(),
|
||||
|
||||
exts = {
|
||||
eml: 'message/rfc822',
|
||||
|
@ -123,13 +124,13 @@ export const FileInfo = {
|
|||
* @returns {string}
|
||||
*/
|
||||
getExtension: fileName => {
|
||||
fileName = fileName.toLowerCase().trim();
|
||||
fileName = lowerCase(fileName);
|
||||
const result = fileName.split('.').pop();
|
||||
return result === fileName ? '' : result;
|
||||
},
|
||||
|
||||
getContentType: fileName => {
|
||||
fileName = fileName.toLowerCase().trim();
|
||||
fileName = lowerCase(fileName);
|
||||
if ('winmail.dat' === fileName) {
|
||||
return app + 'ms-tnef';
|
||||
}
|
||||
|
@ -158,8 +159,8 @@ export const FileInfo = {
|
|||
* @returns {string}
|
||||
*/
|
||||
getType: (ext, mimeType) => {
|
||||
ext = ext.toLowerCase().trim();
|
||||
mimeType = mimeType.toLowerCase().trim().replace('csv/plain', 'text/csv');
|
||||
ext = lowerCase(ext);
|
||||
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
|
||||
|
||||
let key = ext + mimeType;
|
||||
if (cache[key]) {
|
||||
|
@ -249,10 +250,10 @@ export const FileInfo = {
|
|||
* @param {string} sFileType
|
||||
* @returns {string}
|
||||
*/
|
||||
getCombinedIconClass: data => {
|
||||
if (isNonEmptyArray(data)) {
|
||||
getAttachmentsIconClass: data => {
|
||||
if (arrayLength(data)) {
|
||||
let icons = data
|
||||
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '')
|
||||
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
|
||||
.validUnique();
|
||||
|
||||
return (icons && 1 === icons.length && 'icon-file' !== icons[0])
|
||||
|
|
259
dev/Common/Folders.js
Normal file
259
dev/Common/Folders.js
Normal file
|
@ -0,0 +1,259 @@
|
|||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
import {
|
||||
MessageFlagsCache,
|
||||
setFolderHash,
|
||||
getFolderHash,
|
||||
getFolderInboxName,
|
||||
getFolderFromCacheList,
|
||||
getFolderUidNext
|
||||
} from 'Common/Cache';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export const
|
||||
|
||||
sortFolders = folders => {
|
||||
try {
|
||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
folders.sort((a, b) =>
|
||||
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
* @param {string} folder
|
||||
* @param {Array=} list = []
|
||||
*/
|
||||
fetchFolderInformation = (fCallback, folder, list = []) => {
|
||||
let fetch = !arrayLength(list);
|
||||
const uids = [];
|
||||
|
||||
if (!fetch) {
|
||||
list.forEach(messageListItem => {
|
||||
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
|
||||
uids.push(messageListItem.uid);
|
||||
}
|
||||
|
||||
if (messageListItem.threads.length) {
|
||||
messageListItem.threads.forEach(uid => {
|
||||
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
|
||||
uids.push(uid);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
fetch = uids.length;
|
||||
}
|
||||
|
||||
if (fetch) {
|
||||
Remote.request('FolderInformation', fCallback, {
|
||||
Folder: folder,
|
||||
FlagsUids: uids,
|
||||
UidNext: getFolderUidNext(folder) // Used to check for new messages
|
||||
});
|
||||
} else if (SettingsUserStore.useThreads()) {
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Array=} aDisabled
|
||||
* @param {Array=} aHeaderLines
|
||||
* @param {Function=} fRenameCallback
|
||||
* @param {Function=} fDisableCallback
|
||||
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
|
||||
* @returns {Array}
|
||||
*/
|
||||
folderListOptionsBuilder = (
|
||||
aDisabled,
|
||||
aHeaderLines,
|
||||
fRenameCallback,
|
||||
fDisableCallback,
|
||||
bNoSelectSelectable,
|
||||
aList = FolderUserStore.folderList()
|
||||
) => {
|
||||
const
|
||||
aResult = [],
|
||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||
// FolderSystemPopupView should always be true
|
||||
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
||||
|
||||
foldersWalk = folders => {
|
||||
folders.forEach(oItem => {
|
||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||
aResult.push({
|
||||
id: oItem.fullName,
|
||||
name:
|
||||
sDeepPrefix.repeat(oItem.deep) +
|
||||
fRenameCallback(oItem),
|
||||
system: false,
|
||||
disabled: !bNoSelectSelectable && (
|
||||
!oItem.selectable() ||
|
||||
aDisabled.includes(oItem.fullName) ||
|
||||
fDisableCallback(oItem))
|
||||
});
|
||||
}
|
||||
|
||||
if (oItem.subFolders.length) {
|
||||
foldersWalk(oItem.subFolders());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
fDisableCallback = fDisableCallback || (() => false);
|
||||
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
||||
isArray(aDisabled) || (aDisabled = []);
|
||||
|
||||
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
||||
aResult.push({
|
||||
id: line[0],
|
||||
name: line[1],
|
||||
system: false,
|
||||
disabled: false
|
||||
})
|
||||
);
|
||||
|
||||
foldersWalk(aList);
|
||||
|
||||
return aResult;
|
||||
},
|
||||
|
||||
// Every 5 minutes
|
||||
refreshFoldersInterval = 300000,
|
||||
|
||||
/**
|
||||
* @param {boolean=} boot = false
|
||||
*/
|
||||
folderInformationMultiply = (boot = false) => {
|
||||
const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
|
||||
if (arrayLength(folders)) {
|
||||
Remote.request('FolderInformationMultiply', (iError, oData) => {
|
||||
if (!iError && arrayLength(oData.Result)) {
|
||||
const utc = Date.now();
|
||||
oData.Result.forEach(item => {
|
||||
const hash = getFolderHash(item.Folder),
|
||||
folder = getFolderFromCacheList(item.Folder);
|
||||
|
||||
if (folder) {
|
||||
folder.expires = utc;
|
||||
|
||||
setFolderHash(item.Folder, item.Hash);
|
||||
|
||||
folder.messageCountAll(item.MessageCount);
|
||||
|
||||
let unreadCountChange = folder.messageCountUnread() !== item.MessageUnseenCount;
|
||||
|
||||
folder.messageCountUnread(item.MessageUnseenCount);
|
||||
|
||||
if (unreadCountChange) {
|
||||
MessageFlagsCache.clearFolder(folder.fullName);
|
||||
}
|
||||
|
||||
if (!hash || item.Hash !== hash) {
|
||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
}
|
||||
} else if (unreadCountChange
|
||||
&& folder.fullName === FolderUserStore.currentFolderFullName()
|
||||
&& MessagelistUserStore.length) {
|
||||
rl.app.folderInformation(folder.fullName, MessagelistUserStore());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (boot) {
|
||||
setTimeout(() => folderInformationMultiply(true), 2000);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
Folders: folders
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
moveOrDeleteResponseHelper = (iError, oData) => {
|
||||
if (iError) {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
alert(getNotification(iError));
|
||||
} else if (FolderUserStore.currentFolder()) {
|
||||
if (2 === arrayLength(oData.Result)) {
|
||||
setFolderHash(oData.Result[0], oData.Result[1]);
|
||||
} else {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
}
|
||||
MessagelistUserStore.reload(!MessagelistUserStore.length);
|
||||
}
|
||||
},
|
||||
|
||||
messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
|
||||
const
|
||||
sSpamFolder = FolderUserStore.spamFolder(),
|
||||
isSpam = sSpamFolder === toFolderFullName,
|
||||
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
|
||||
|
||||
Remote.request('MessageMove',
|
||||
moveOrDeleteResponseHelper,
|
||||
{
|
||||
FromFolder: fromFolderFullName,
|
||||
ToFolder: toFolderFullName,
|
||||
Uids: uidsForMove.join(','),
|
||||
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
|
||||
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
||||
},
|
||||
null,
|
||||
'',
|
||||
['MessageList']
|
||||
);
|
||||
},
|
||||
|
||||
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
|
||||
Remote.request('MessageDelete',
|
||||
moveOrDeleteResponseHelper,
|
||||
{
|
||||
Folder: sFromFolderFullName,
|
||||
Uids: aUidForRemove.join(',')
|
||||
},
|
||||
null,
|
||||
'',
|
||||
['MessageList']
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} sFromFolderFullName
|
||||
* @param {Array} aUidForMove
|
||||
* @param {string} sToFolderFullName
|
||||
* @param {boolean=} bCopy = false
|
||||
*/
|
||||
moveMessagesToFolder = (sFromFolderFullName, aUidForMove, sToFolderFullName, bCopy) => {
|
||||
if (sFromFolderFullName !== sToFolderFullName && arrayLength(aUidForMove)) {
|
||||
const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
|
||||
oToFolder = getFolderFromCacheList(sToFolderFullName);
|
||||
|
||||
if (oFromFolder && oToFolder) {
|
||||
if (bCopy) {
|
||||
Remote.request('MessageCopy', null, {
|
||||
FromFolder: oFromFolder.fullName,
|
||||
ToFolder: oToFolder.fullName,
|
||||
Uids: aUidForMove.join(',')
|
||||
});
|
||||
} else {
|
||||
messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, aUidForMove);
|
||||
}
|
||||
|
||||
MessagelistUserStore.removeMessagesFromList(oFromFolder.fullName, aUidForMove, oToFolder.fullName, bCopy);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
|
@ -1,58 +1,78 @@
|
|||
import ko from 'ko';
|
||||
import { Scope } from 'Common/Enums';
|
||||
|
||||
export const doc = document;
|
||||
let keyScopeFake = 'all';
|
||||
|
||||
export const $htmlCL = doc.documentElement.classList;
|
||||
export const
|
||||
ScopeMenu = 'Menu',
|
||||
|
||||
export const elementById = id => doc.getElementById(id);
|
||||
doc = document,
|
||||
|
||||
export const Settings = rl.settings;
|
||||
export const SettingsGet = rl.settings.get;
|
||||
$htmlCL = doc.documentElement.classList,
|
||||
|
||||
export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 });
|
||||
elementById = id => doc.getElementById(id),
|
||||
|
||||
export const moveAction = ko.observable(false);
|
||||
export const leftPanelDisabled = ko.observable(false);
|
||||
exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
||||
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
|
||||
|
||||
export const createElement = (name, attr) => {
|
||||
let el = doc.createElement(name);
|
||||
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
|
||||
return el;
|
||||
};
|
||||
Settings = rl.settings,
|
||||
SettingsGet = Settings.get,
|
||||
SettingsCapa = Settings.capa,
|
||||
|
||||
dropdowns = [],
|
||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
||||
|
||||
moveAction = ko.observable(false),
|
||||
leftPanelDisabled = ko.observable(false),
|
||||
|
||||
createElement = (name, attr) => {
|
||||
let el = doc.createElement(name);
|
||||
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
|
||||
return el;
|
||||
},
|
||||
|
||||
fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})),
|
||||
|
||||
formFieldFocused = () => doc.activeElement && doc.activeElement.matches('input,textarea'),
|
||||
|
||||
addShortcut = (...args) => shortcuts.add(...args),
|
||||
|
||||
registerShortcut = (keys, modifiers, scopes, method) =>
|
||||
addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
|
||||
|
||||
addEventsListener = (element, events, fn, options) =>
|
||||
events.forEach(event => element.addEventListener(event, fn, options)),
|
||||
|
||||
addEventsListeners = (element, events) =>
|
||||
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
|
||||
|
||||
// keys / shortcuts
|
||||
keyScopeReal = ko.observable('all'),
|
||||
keyScope = value => {
|
||||
if (!value) {
|
||||
return keyScopeFake;
|
||||
}
|
||||
if (ScopeMenu !== value) {
|
||||
keyScopeFake = value;
|
||||
if (dropdownVisibility()) {
|
||||
value = ScopeMenu;
|
||||
}
|
||||
}
|
||||
keyScopeReal(value);
|
||||
shortcuts.setScope(value);
|
||||
};
|
||||
|
||||
dropdownVisibility.subscribe(value => {
|
||||
if (value) {
|
||||
keyScope(ScopeMenu);
|
||||
} else if (ScopeMenu === shortcuts.getScope()) {
|
||||
keyScope(keyScopeFake);
|
||||
}
|
||||
});
|
||||
|
||||
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
|
||||
leftPanelDisabled.subscribe(value => {
|
||||
value && moveAction() && moveAction(false);
|
||||
$htmlCL.toggle('rl-left-panel-disabled', value);
|
||||
});
|
||||
|
||||
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
|
||||
|
||||
// keys
|
||||
export const keyScopeReal = ko.observable(Scope.All);
|
||||
|
||||
export const keyScope = (()=>{
|
||||
let keyScopeFake = Scope.All;
|
||||
dropdownVisibility.subscribe(value => {
|
||||
if (value) {
|
||||
keyScope(Scope.Menu);
|
||||
} else if (Scope.Menu === shortcuts.getScope()) {
|
||||
keyScope(keyScopeFake);
|
||||
}
|
||||
});
|
||||
return ko.computed({
|
||||
read: () => keyScopeFake,
|
||||
write: value => {
|
||||
if (Scope.Menu !== value) {
|
||||
keyScopeFake = value;
|
||||
if (dropdownVisibility()) {
|
||||
value = Scope.Menu;
|
||||
}
|
||||
}
|
||||
|
||||
keyScopeReal(value);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
keyScopeReal.subscribe(value => shortcuts.setScope(value));
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { createElement, SettingsGet } from 'Common/Globals';
|
||||
import { forEachObjectEntry, pInt } from 'Common/Utils';
|
||||
import { proxy } from 'Common/Links';
|
||||
|
||||
const
|
||||
tpl = createElement('template'),
|
||||
htmlre = /[&<>"']/g,
|
||||
htmlmap = {
|
||||
'&': '&',
|
||||
|
@ -6,17 +11,489 @@ const
|
|||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
},
|
||||
|
||||
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
|
||||
|
||||
// Strip utm_* tracking
|
||||
stripTracking = text => text.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1').replace(/&&+/, '');
|
||||
|
||||
export const
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]),
|
||||
|
||||
/**
|
||||
* Clears the Message Html for viewing
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
cleanHtml = (html, contentLocationUrls, removeColors) => {
|
||||
const
|
||||
debug = false, // Config()->Get('debug', 'enable', false);
|
||||
useProxy = !!SettingsGet('UseLocalProxyForExternalImages'),
|
||||
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
|
||||
|
||||
result = {
|
||||
hasExternals: false,
|
||||
foundCIDs: [],
|
||||
foundContentLocationUrls: []
|
||||
},
|
||||
|
||||
// convert body attributes to CSS
|
||||
tasks = {
|
||||
link: value => {
|
||||
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
|
||||
tpl.content.querySelectorAll('a').forEach(node => node.style.color = value)
|
||||
}
|
||||
},
|
||||
text: (value, node) => node.style.color = value,
|
||||
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
|
||||
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
|
||||
bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
|
||||
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
|
||||
},
|
||||
allowedAttributes = [
|
||||
// defaults
|
||||
'name',
|
||||
'dir', 'lang', 'style', 'title',
|
||||
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
|
||||
'border', 'bordercolor', 'charset', 'direction',
|
||||
// a
|
||||
'download', 'hreflang',
|
||||
// body
|
||||
'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
|
||||
// col
|
||||
'align', 'valign',
|
||||
// font
|
||||
'color', 'face', 'size',
|
||||
// hr
|
||||
'noshade',
|
||||
// img
|
||||
'hspace', 'sizes', 'srcset', 'vspace',
|
||||
// meter
|
||||
'low', 'high', 'optimum', 'value',
|
||||
// ol
|
||||
'reversed', 'start',
|
||||
// table
|
||||
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
|
||||
// th
|
||||
'abbr', 'scope',
|
||||
// td
|
||||
'colspan', 'rowspan', 'headers'
|
||||
],
|
||||
disallowedTags = [
|
||||
'HEAD','STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
|
||||
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
|
||||
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
|
||||
],
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG','O:P','TABLE'
|
||||
];
|
||||
|
||||
tpl.innerHTML = html
|
||||
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
|
||||
.replace(/<!doctype[^>]*>/gi, '')
|
||||
.replace(/<\?xml[^>]*\?>/gi, '')
|
||||
// Not supported by <template> element
|
||||
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
||||
.replace(/<\/?(html|head)[^>]*>/gi, '')
|
||||
.trim();
|
||||
html = '';
|
||||
|
||||
// \MailSo\Base\HtmlUtils::ClearComments()
|
||||
// https://github.com/the-djmaze/snappymail/issues/187
|
||||
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
|
||||
while (nodeIterator.nextNode()) {
|
||||
nodeIterator.referenceNode.remove();
|
||||
}
|
||||
|
||||
tpl.content.querySelectorAll('*').forEach(oElement => {
|
||||
const name = oElement.tagName,
|
||||
oStyle = oElement.style;
|
||||
|
||||
// \MailSo\Base\HtmlUtils::ClearTags()
|
||||
if (disallowedTags.includes(name)
|
||||
|| 'none' == oStyle.display
|
||||
|| 'hidden' == oStyle.visibility
|
||||
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
|
||||
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
||||
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
|
||||
// || ('0' === oStyle.opacity
|
||||
|| (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim() && !oElement.querySelector('img')))
|
||||
) {
|
||||
oElement.remove();
|
||||
return;
|
||||
}
|
||||
// if (['CENTER','FORM'].includes(name)) {
|
||||
if ('FORM' === name || 'O:P' === name) {
|
||||
replaceWithChildren(oElement);
|
||||
return;
|
||||
}
|
||||
/*
|
||||
// Idea to allow CSS
|
||||
if ('STYLE' === name) {
|
||||
msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
|
||||
oElement.textContent = oElement.textContent
|
||||
.replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
|
||||
.replace(/(background-)color:[^};]+/g, '');
|
||||
return;
|
||||
}
|
||||
*/
|
||||
const aAttrsForRemove = [],
|
||||
hasAttribute = name => oElement.hasAttribute(name),
|
||||
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
|
||||
setAttribute = (name, value) => oElement.setAttribute(name, value),
|
||||
delAttribute = name => oElement.removeAttribute(name);
|
||||
|
||||
if ('mail-body' === oElement.className) {
|
||||
forEachObjectEntry(tasks, (name, cb) => {
|
||||
if (hasAttribute(name)) {
|
||||
cb(getAttribute(name), oElement);
|
||||
delAttribute(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (oElement.hasAttributes()) {
|
||||
let i = oElement.attributes.length;
|
||||
while (i--) {
|
||||
let sAttrName = oElement.attributes[i].name.toLowerCase();
|
||||
if (!allowedAttributes.includes(sAttrName)) {
|
||||
delAttribute(sAttrName);
|
||||
aAttrsForRemove.push(sAttrName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
if ('TABLE' === name) {
|
||||
if (hasAttribute('width')) {
|
||||
oStyle.width = getAttribute('width');
|
||||
delAttribute('width');
|
||||
}
|
||||
value = oStyle.width;
|
||||
if (value && !value.includes('%')) {
|
||||
oStyle.maxWidth = value;
|
||||
oStyle.width = '100%';
|
||||
}
|
||||
}
|
||||
|
||||
else if ('A' === name) {
|
||||
value = oElement.href;
|
||||
if (!/^([a-z]+):/i.test(value)) {
|
||||
setAttribute('data-x-broken-href', value);
|
||||
delAttribute('href');
|
||||
} else {
|
||||
oElement.href = stripTracking(value);
|
||||
setAttribute('target', '_blank');
|
||||
setAttribute('rel', 'external nofollow noopener noreferrer');
|
||||
}
|
||||
setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
// SVG xlink:href
|
||||
/*
|
||||
if (hasAttribute('xlink:href')) {
|
||||
delAttribute('xlink:href');
|
||||
}
|
||||
*/
|
||||
|
||||
let skipStyle = false;
|
||||
if (hasAttribute('src')) {
|
||||
value = getAttribute('src');
|
||||
delAttribute('src');
|
||||
|
||||
if (detectHiddenImages
|
||||
&& 'IMG' === name
|
||||
&& (('' != getAttribute('height') && 3 > pInt(getAttribute('height')))
|
||||
|| ('' != getAttribute('width') && 3 > pInt(getAttribute('width')))
|
||||
|| [
|
||||
'email.microsoftemail.com/open',
|
||||
'github.com/notifications/beacon/',
|
||||
'mandrillapp.com/track/open',
|
||||
'list-manage.com/track/open'
|
||||
].filter(uri => value.toLowerCase().includes(uri)).length
|
||||
)) {
|
||||
skipStyle = true;
|
||||
setAttribute('style', 'display:none');
|
||||
setAttribute('data-x-hidden-src', value);
|
||||
}
|
||||
else if (contentLocationUrls[value])
|
||||
{
|
||||
setAttribute('data-x-src-location', value);
|
||||
result.foundContentLocationUrls.push(value);
|
||||
}
|
||||
else if ('cid:' === value.slice(0, 4))
|
||||
{
|
||||
setAttribute('data-x-src-cid', value.slice(4));
|
||||
result.foundCIDs.push(value.slice(4));
|
||||
}
|
||||
else if (/^https?:\/\//i.test(value) || '//' === value.slice(0, 2))
|
||||
{
|
||||
setAttribute('data-x-src', useProxy ? proxy(value) : value);
|
||||
result.hasExternals = true;
|
||||
}
|
||||
else if ('data:image/' === value.slice(0, 11))
|
||||
{
|
||||
setAttribute('src', value);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAttribute('data-x-broken-src', value);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAttribute('background')) {
|
||||
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
|
||||
delAttribute('background');
|
||||
}
|
||||
|
||||
if (hasAttribute('bgcolor')) {
|
||||
oStyle.backgroundColor = getAttribute('bgcolor');
|
||||
delAttribute('bgcolor');
|
||||
}
|
||||
|
||||
if (hasAttribute('color')) {
|
||||
oStyle.color = getAttribute('color');
|
||||
delAttribute('color');
|
||||
}
|
||||
|
||||
if (!skipStyle) {
|
||||
/*
|
||||
if ('fixed' === oStyle.position) {
|
||||
oStyle.position = 'absolute';
|
||||
}
|
||||
*/
|
||||
oStyle.removeProperty('behavior');
|
||||
oStyle.removeProperty('cursor');
|
||||
oStyle.removeProperty('min-width');
|
||||
|
||||
const urls = {
|
||||
cid: [], // 'data-x-style-cid'
|
||||
remote: [], // 'data-x-style-url'
|
||||
broken: [] // 'data-x-broken-style-src'
|
||||
};
|
||||
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
|
||||
if (oStyle[property]) {
|
||||
let value = oStyle[property],
|
||||
found = value.match(/url\s*\(([^)]+)\)/gi);
|
||||
if (found) {
|
||||
oStyle[property] = null;
|
||||
found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
|
||||
let lowerUrl = found.toLowerCase();
|
||||
if ('cid:' === lowerUrl.slice(0, 4)) {
|
||||
found = found.slice(4);
|
||||
urls.cid[property] = found
|
||||
result.foundCIDs.push(found);
|
||||
} else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
|
||||
result.hasExternals = true;
|
||||
urls.remote[property] = useProxy ? proxy(found) : found;
|
||||
} else if ('data:image/' === lowerUrl.slice(0, 11)) {
|
||||
oStyle[property] = value;
|
||||
} else {
|
||||
urls.broken[property] = found;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// oStyle.removeProperty('background-image');
|
||||
// oStyle.removeProperty('list-style-image');
|
||||
|
||||
if (urls.cid.length) {
|
||||
setAttribute('data-x-style-cid', JSON.stringify(urls.cid));
|
||||
}
|
||||
if (urls.remote.length) {
|
||||
setAttribute('data-x-style-url', JSON.stringify(urls.remote));
|
||||
}
|
||||
if (urls.broken.length) {
|
||||
setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
|
||||
}
|
||||
|
||||
if (11 > pInt(oStyle.fontSize)) {
|
||||
oStyle.removeProperty('font-size');
|
||||
}
|
||||
|
||||
// Removes background and color
|
||||
// Many e-mails incorrectly only define one, not both
|
||||
// And in dark theme mode this kills the readability
|
||||
if (removeColors) {
|
||||
oStyle.removeProperty('background-color');
|
||||
oStyle.removeProperty('background-image');
|
||||
oStyle.removeProperty('color');
|
||||
}
|
||||
|
||||
// Drop Microsoft Office style properties
|
||||
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
|
||||
}
|
||||
|
||||
if (debug && aAttrsForRemove.length) {
|
||||
setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
|
||||
}
|
||||
});
|
||||
|
||||
// return tpl.content.firstChild;
|
||||
result.html = tpl.innerHTML.trim();
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @returns {string}
|
||||
*/
|
||||
htmlToPlain = html => {
|
||||
const
|
||||
hr = '⎯'.repeat(64),
|
||||
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
|
||||
blockquotes = node => {
|
||||
let bq;
|
||||
while ((bq = node.querySelector('blockquote'))) {
|
||||
// Convert child blockquote first
|
||||
blockquotes(bq);
|
||||
// Convert blockquote
|
||||
// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '> ');
|
||||
// replaceWithChildren(bq);
|
||||
bq.replaceWith(
|
||||
'\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
html = html
|
||||
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
|
||||
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
|
||||
.replace(/\r?\n/g, '')
|
||||
.replace(/\s+/gm, ' ');
|
||||
|
||||
while (/<(div|tr)[\s>]/i.test(html)) {
|
||||
html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
||||
}
|
||||
while (/<\/(div|tr)[\s>]/i.test(html)) {
|
||||
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
||||
}
|
||||
|
||||
tpl.innerHTML = html
|
||||
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
||||
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||
|
||||
// Convert line-breaks
|
||||
forEach('br', br => br.replaceWith('\n'));
|
||||
|
||||
// lines
|
||||
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
|
||||
|
||||
// headings
|
||||
forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
|
||||
|
||||
// paragraphs
|
||||
forEach('p', node => {
|
||||
node.prepend('\n\n');
|
||||
if ('' == node.textContent.trim()) {
|
||||
node.remove();
|
||||
} else {
|
||||
node.after('\n\n');
|
||||
}
|
||||
});
|
||||
|
||||
// proper indenting and numbering of (un)ordered lists
|
||||
forEach('ol,ul', node => {
|
||||
let prefix = '',
|
||||
parent = node,
|
||||
ordered = 'OL' == node.tagName,
|
||||
i = 0;
|
||||
while (parent && parent.parentNode && parent.parentNode.closest) {
|
||||
parent = parent.parentNode.closest('ol,ul');
|
||||
parent && (prefix = ' ' + prefix);
|
||||
}
|
||||
node.querySelectorAll(':scope > li').forEach(li => {
|
||||
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
|
||||
});
|
||||
node.prepend('\n\n');
|
||||
node.after('\n\n');
|
||||
});
|
||||
|
||||
// Convert anchors
|
||||
forEach('a', a => {
|
||||
let txt = a.textContent, href = a.href;
|
||||
return a.replaceWith((txt.trim() == href ? txt : txt + ' ' + href + ' '));
|
||||
});
|
||||
|
||||
// Bold
|
||||
forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
|
||||
// Italic
|
||||
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
||||
|
||||
// Blockquotes must be last
|
||||
blockquotes(tpl.content);
|
||||
|
||||
return (tpl.content.textContent || '').replace(/\n{3,}/gm, '\n\n').trim();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} plain
|
||||
* @param {boolean} findEmailAndLinksInText = false
|
||||
* @returns {string}
|
||||
*/
|
||||
plainToHtml = plain => {
|
||||
plain = stripTracking(plain)
|
||||
.toString()
|
||||
.replace(/\r/g, '')
|
||||
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
|
||||
|
||||
let bIn = false,
|
||||
bDo = true,
|
||||
bStart = true,
|
||||
aNextText = [],
|
||||
aText = plain.split('\n');
|
||||
|
||||
do {
|
||||
bDo = false;
|
||||
aNextText = [];
|
||||
aText.forEach(sLine => {
|
||||
bStart = '>' === sLine.slice(0, 1);
|
||||
if (bStart && !bIn) {
|
||||
bDo = true;
|
||||
bIn = true;
|
||||
aNextText.push('~~~blockquote~~~');
|
||||
aNextText.push(sLine.slice(1));
|
||||
} else if (!bStart && bIn) {
|
||||
if (sLine) {
|
||||
bIn = false;
|
||||
aNextText.push('~~~/blockquote~~~');
|
||||
aNextText.push(sLine);
|
||||
} else {
|
||||
aNextText.push(sLine);
|
||||
}
|
||||
} else if (bStart && bIn) {
|
||||
aNextText.push(sLine.slice(1));
|
||||
} else {
|
||||
aNextText.push(sLine);
|
||||
}
|
||||
});
|
||||
|
||||
if (bIn) {
|
||||
bIn = false;
|
||||
aNextText.push('~~~/blockquote~~~');
|
||||
}
|
||||
|
||||
aText = aNextText;
|
||||
} while (bDo);
|
||||
|
||||
return aText.join('\n')
|
||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
|
||||
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
|
||||
.replace(/\n/g, '<br>');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
export function encodeHtml(text) {
|
||||
return (text && text.toString ? text.toString() : ''+text).replace(htmlre, m => htmlmap[m]);
|
||||
}
|
||||
|
||||
class HtmlEditor {
|
||||
export class HtmlEditor {
|
||||
/**
|
||||
* @param {Object} element
|
||||
* @param {Function=} onBlur
|
||||
|
@ -24,25 +501,37 @@ class HtmlEditor {
|
|||
* @param {Function=} onModeChange
|
||||
*/
|
||||
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
|
||||
this.editor;
|
||||
this.blurTimer = 0;
|
||||
|
||||
this.__resizable = false;
|
||||
this.__inited = false;
|
||||
|
||||
this.onBlur = onBlur;
|
||||
this.onReady = onReady;
|
||||
this.onModeChange = onModeChange;
|
||||
|
||||
this.element = element;
|
||||
if (element) {
|
||||
let editor;
|
||||
|
||||
this.resize = (() => {
|
||||
try {
|
||||
this.editor && this.__resizable && this.editor.resize(element.clientWidth, element.clientHeight);
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
}).throttle(100);
|
||||
onReady = onReady ? [onReady] : [];
|
||||
this.onReady = fn => onReady.push(fn);
|
||||
const readyCallback = () => {
|
||||
this.editor = editor;
|
||||
this.onReady = fn => fn();
|
||||
onReady.forEach(fn => fn());
|
||||
};
|
||||
|
||||
this.init();
|
||||
if (rl.createWYSIWYG) {
|
||||
editor = rl.createWYSIWYG(element, readyCallback);
|
||||
}
|
||||
if (!editor) {
|
||||
editor = new SquireUI(element);
|
||||
setTimeout(readyCallback, 1);
|
||||
}
|
||||
|
||||
editor.on('blur', () => this.blurTrigger());
|
||||
editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
|
||||
editor.on('mode', () => {
|
||||
this.blurTrigger();
|
||||
this.onModeChange && this.onModeChange(!this.isPlain());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
blurTrigger() {
|
||||
|
@ -70,9 +559,9 @@ class HtmlEditor {
|
|||
* @returns {void}
|
||||
*/
|
||||
clearCachedSignature() {
|
||||
this.editor && this.editor.execCommand('insertSignature', {
|
||||
this.onReady(() => this.editor.execCommand('insertSignature', {
|
||||
clearCache: true
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,75 +571,66 @@ class HtmlEditor {
|
|||
* @returns {void}
|
||||
*/
|
||||
setSignature(signature, html, insertBefore = false) {
|
||||
this.editor && this.editor.execCommand('insertSignature', {
|
||||
this.onReady(() => this.editor.execCommand('insertSignature', {
|
||||
isHtml: html,
|
||||
insertBefore: insertBefore,
|
||||
signature: signature
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} wrapIsHtml = false
|
||||
* @returns {string}
|
||||
*/
|
||||
getData(wrapIsHtml = false) {
|
||||
getData() {
|
||||
let result = '';
|
||||
if (this.editor) {
|
||||
try {
|
||||
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
|
||||
result = this.editor.__plain.getRawData();
|
||||
} else {
|
||||
result = wrapIsHtml
|
||||
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
|
||||
this.editor.getData() +
|
||||
'</div>'
|
||||
: this.editor.getData();
|
||||
result = this.editor.getData();
|
||||
}
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} wrapIsHtml = false
|
||||
* @returns {string}
|
||||
*/
|
||||
getDataWithHtmlMark(wrapIsHtml = false) {
|
||||
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml);
|
||||
getDataWithHtmlMark() {
|
||||
return (this.isHtml() ? ':HTML:' : '') + this.getData();
|
||||
}
|
||||
|
||||
modeWysiwyg() {
|
||||
try {
|
||||
this.editor && this.editor.setMode('wysiwyg');
|
||||
} catch (e) { console.error(e); }
|
||||
this.onReady(() => this.editor.setMode('wysiwyg'));
|
||||
}
|
||||
modePlain() {
|
||||
try {
|
||||
this.editor && this.editor.setMode('plain');
|
||||
} catch (e) { console.error(e); }
|
||||
this.onReady(() => this.editor.setMode('plain'));
|
||||
}
|
||||
|
||||
setHtmlOrPlain(text) {
|
||||
if (':HTML:' === text.substr(0, 6)) {
|
||||
this.setHtml(text.substr(6));
|
||||
if (':HTML:' === text.slice(0, 6)) {
|
||||
this.setHtml(text.slice(6));
|
||||
} else {
|
||||
this.setPlain(text);
|
||||
}
|
||||
}
|
||||
|
||||
setData(mode, data) {
|
||||
if (this.editor && this.__inited) {
|
||||
this.onReady(() => {
|
||||
const editor = this.editor;
|
||||
this.clearCachedSignature();
|
||||
try {
|
||||
this.editor.setMode(mode);
|
||||
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
|
||||
this.editor.__plain.setRawData(data);
|
||||
editor.setMode(mode);
|
||||
if (this.isPlain() && editor.plugins.plain && editor.__plain) {
|
||||
editor.__plain.setRawData(data);
|
||||
} else {
|
||||
this.editor.setData(data);
|
||||
editor.setData(data);
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setHtml(html) {
|
||||
|
@ -161,46 +641,8 @@ class HtmlEditor {
|
|||
this.setData('plain', txt);
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.element && !this.editor) {
|
||||
const onReady = () => {
|
||||
if (this.editor.removeMenuItem) {
|
||||
this.editor.removeMenuItem('cut');
|
||||
this.editor.removeMenuItem('copy');
|
||||
this.editor.removeMenuItem('paste');
|
||||
}
|
||||
|
||||
this.__resizable = true;
|
||||
this.__inited = true;
|
||||
|
||||
this.resize();
|
||||
|
||||
this.onReady && this.onReady();
|
||||
};
|
||||
|
||||
if (rl.createWYSIWYG) {
|
||||
this.editor = rl.createWYSIWYG(this.element, onReady);
|
||||
}
|
||||
if (!this.editor) {
|
||||
this.editor = new SquireUI(this.element);
|
||||
setTimeout(onReady,1);
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.on('blur', () => this.blurTrigger());
|
||||
this.editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
|
||||
this.editor.on('mode', () => {
|
||||
this.blurTrigger();
|
||||
this.onModeChange && this.onModeChange(!this.isPlain());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
try {
|
||||
this.editor && this.editor.focus();
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
this.onReady(() => this.editor.focus());
|
||||
}
|
||||
|
||||
hasFocus() {
|
||||
|
@ -212,14 +654,15 @@ class HtmlEditor {
|
|||
}
|
||||
|
||||
blur() {
|
||||
try {
|
||||
this.editor && this.editor.focusManager.blur(true);
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
this.onReady(() => this.editor.focusManager.blur(true));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setHtml('');
|
||||
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
|
||||
}
|
||||
}
|
||||
|
||||
export { HtmlEditor, HtmlEditor as default };
|
||||
rl.Utils = {
|
||||
htmlToPlain: htmlToPlain,
|
||||
plainToHtml: plainToHtml
|
||||
};
|
||||
|
|
|
@ -1,191 +1,140 @@
|
|||
import { pString, pInt } from 'Common/Utils';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { Settings } from 'Common/Globals';
|
||||
|
||||
const
|
||||
ROOT = './',
|
||||
HASH_PREFIX = '#/',
|
||||
SERVER_PREFIX = './?',
|
||||
VERSION = Settings.app('version'),
|
||||
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
|
||||
VERSION_PREFIX = () => Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
|
||||
|
||||
getHash = () => SettingsGet('AuthAccountHash') || '0';
|
||||
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export const SUB_QUERY_PREFIX = '&q[]=';
|
||||
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
|
||||
|
||||
/**
|
||||
* @param {string=} startupUrl
|
||||
* @returns {string}
|
||||
*/
|
||||
export function root(startupUrl = '') {
|
||||
return HASH_PREFIX + pString(startupUrl);
|
||||
}
|
||||
export const
|
||||
SUB_QUERY_PREFIX = '&q[]=',
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function logoutLink() {
|
||||
return (rl.adminArea() && !Settings.app('adminHostUse'))
|
||||
? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
|
||||
: ROOT;
|
||||
}
|
||||
/**
|
||||
* @param {string=} startupUrl
|
||||
* @returns {string}
|
||||
*/
|
||||
root = () => HASH_PREFIX,
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {string} hash
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serverRequestRaw(type, hash, customSpecSuffix) {
|
||||
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||
+ (null == customSpecSuffix ? getHash() : customSpecSuffix) + '/'
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
logoutLink = () => adminPath() ? prefix() : './',
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {string} hash
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
serverRequestRaw = (type, hash) =>
|
||||
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||
+ '0/' // AuthAccountHash ?
|
||||
+ (type
|
||||
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
||||
: '')
|
||||
;
|
||||
}
|
||||
: ''),
|
||||
|
||||
/**
|
||||
* @param {string} download
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
export function attachmentDownload(download, customSpecSuffix) {
|
||||
return serverRequestRaw('Download', download, customSpecSuffix);
|
||||
}
|
||||
/**
|
||||
* @param {string} download
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
attachmentDownload = (download, customSpecSuffix) =>
|
||||
serverRequestRaw('Download', download, customSpecSuffix),
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serverRequest(type) {
|
||||
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
|
||||
}
|
||||
proxy = url =>
|
||||
SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
/*
|
||||
return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
|
||||
'Rnd' => \md5(\microtime(true)),
|
||||
'Token' => Utils::GetConnectionToken(),
|
||||
'Url' => $sUrl
|
||||
)).'/';
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} email
|
||||
* @returns {string}
|
||||
*/
|
||||
export function change(email) {
|
||||
return serverRequest('Change') + encodeURIComponent(email) + '/';
|
||||
}
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {string}
|
||||
*/
|
||||
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
||||
|
||||
/**
|
||||
* @param {string} hash
|
||||
* @returns {string}
|
||||
*/
|
||||
export function userBackground(hash) {
|
||||
return serverRequestRaw('UserBackground', hash);
|
||||
}
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {boolean} isAdmin
|
||||
* @returns {string}
|
||||
*/
|
||||
langLink = (lang, isAdmin) =>
|
||||
SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/',
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {boolean} isAdmin
|
||||
* @returns {string}
|
||||
*/
|
||||
export function langLink(lang, isAdmin) {
|
||||
return SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/';
|
||||
}
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
staticLink = path => VERSION_PREFIX() + 'static/' + path,
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
export function staticLink(path) {
|
||||
return VERSION_PREFIX + 'static/' + path;
|
||||
}
|
||||
/**
|
||||
* @param {string} theme
|
||||
* @returns {string}
|
||||
*/
|
||||
themePreviewLink = theme => {
|
||||
let prefix = VERSION_PREFIX();
|
||||
if ('@custom' === theme.slice(-7)) {
|
||||
theme = theme.slice(0, theme.length - 7).trim();
|
||||
prefix = Settings.app('webPath') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function openPgpJs() {
|
||||
return staticLink('js/min/openpgp.min.js');
|
||||
}
|
||||
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function openPgpWorkerJs() {
|
||||
return staticLink('js/min/openpgp.worker.min.js');
|
||||
}
|
||||
/**
|
||||
* @param {string} inboxFolderName = 'INBOX'
|
||||
* @returns {string}
|
||||
*/
|
||||
mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName,
|
||||
|
||||
/**
|
||||
* @param {string} theme
|
||||
* @returns {string}
|
||||
*/
|
||||
export function themePreviewLink(theme) {
|
||||
let prefix = VERSION_PREFIX;
|
||||
if ('@custom' === theme.substr(-7)) {
|
||||
theme = theme.substr(0, theme.length - 7).trim();
|
||||
prefix = Settings.app('webPath') || '';
|
||||
}
|
||||
/**
|
||||
* @param {string=} screenName = ''
|
||||
* @returns {string}
|
||||
*/
|
||||
settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''),
|
||||
|
||||
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
|
||||
}
|
||||
/**
|
||||
* @param {string=} screenName
|
||||
* @returns {string}
|
||||
*/
|
||||
admin = screenName => HASH_PREFIX + (
|
||||
'AdminDomains' == screenName ? 'domains'
|
||||
: 'AdminSecurity' == screenName ? 'security'
|
||||
: ''
|
||||
),
|
||||
|
||||
/**
|
||||
* @param {string} inboxFolderName = 'INBOX'
|
||||
* @returns {string}
|
||||
*/
|
||||
export function mailbox(inboxFolderName = 'INBOX') {
|
||||
return HASH_PREFIX + 'mailbox/' + inboxFolderName;
|
||||
}
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {number=} page = 1
|
||||
* @param {string=} search = ''
|
||||
* @param {number=} threadUid = 0
|
||||
* @returns {string}
|
||||
*/
|
||||
mailBox = (folder, page, search, threadUid) => {
|
||||
let result = [HASH_PREFIX + 'mailbox'];
|
||||
|
||||
/**
|
||||
* @param {string=} screenName = ''
|
||||
* @returns {string}
|
||||
*/
|
||||
export function settings(screenName = '') {
|
||||
return HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');
|
||||
}
|
||||
if (folder) {
|
||||
result.push(folder + (threadUid ? '~' + threadUid : ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} screenName
|
||||
* @returns {string}
|
||||
*/
|
||||
export function admin(screenName) {
|
||||
let result = HASH_PREFIX;
|
||||
switch (screenName) {
|
||||
case 'AdminDomains':
|
||||
result += 'domains';
|
||||
break;
|
||||
case 'AdminSecurity':
|
||||
result += 'security';
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
page = pInt(page, 1);
|
||||
if (1 < page) {
|
||||
result.push('p' + page);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {number=} page = 1
|
||||
* @param {string=} search = ''
|
||||
* @param {string=} threadUid = ''
|
||||
* @returns {string}
|
||||
*/
|
||||
export function mailBox(folder, page = 1, search = '', threadUid = '') {
|
||||
page = pInt(page, 1);
|
||||
search = pString(search);
|
||||
|
||||
let result = HASH_PREFIX + 'mailbox/';
|
||||
|
||||
if (folder) {
|
||||
const resultThreadUid = pInt(threadUid);
|
||||
result += encodeURI(folder) + (0 < resultThreadUid ? '~' + resultThreadUid : '');
|
||||
}
|
||||
|
||||
if (1 < page) {
|
||||
result = result.replace(/\/+$/, '') + '/p' + page;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
result = result.replace(/\/+$/, '') + '/' + encodeURI(search);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
search = pString(search);
|
||||
if (search) {
|
||||
result.push(encodeURI(search));
|
||||
}
|
||||
|
||||
return result.join('/');
|
||||
};
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import { doc } from 'Common/Globals';
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
||||
export function timestampToString(timeStampInUTC, formatStr) {
|
||||
const now = Date.now(),
|
||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||
|
||||
if (31536000000 < time) {
|
||||
const m = new Date(time);
|
||||
switch (formatStr) {
|
||||
case 'FROMNOW':
|
||||
return m.fromNow();
|
||||
case 'SHORT': {
|
||||
if (4 >= (now - time) / 3600000)
|
||||
return m.fromNow();
|
||||
const mt = m.getTime(), date = new Date,
|
||||
dt = date.setHours(0,0,0,0);
|
||||
if (mt > dt)
|
||||
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
||||
if (mt > dt - 86400000)
|
||||
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
||||
if (date.getFullYear() === m.getFullYear())
|
||||
return m.format('d M');
|
||||
return m.format('LL');
|
||||
}
|
||||
case 'FULL':
|
||||
return m.format('LLL');
|
||||
default:
|
||||
return m.format(formatStr);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function timeToNode(element, time) {
|
||||
try {
|
||||
time = time || (Date.parse(element.dateTime) / 1000);
|
||||
if (time) {
|
||||
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
||||
|
||||
let key = element.dataset.momentFormat;
|
||||
if (key) {
|
||||
element.textContent = timestampToString(time, key);
|
||||
}
|
||||
|
||||
if ((key = element.dataset.momentFormatTitle)) {
|
||||
element.title = timestampToString(time, key);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// prevent knockout crashes
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener('reload-time', () => setTimeout(() =>
|
||||
doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
|
||||
, 1)
|
||||
);
|
|
@ -1,5 +1,6 @@
|
|||
import { settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||
import { isArray, isFunction } from 'Common/Utils';
|
||||
|
||||
const SIMPLE_HOOKS = {},
|
||||
|
@ -36,7 +37,7 @@ export function runHook(name, args = []) {
|
|||
* @param {?number=} timeout
|
||||
*/
|
||||
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
|
||||
rl.app && rl.app.Remote.defaultRequest(callback, 'Plugin' + action, parameters, timeout);
|
||||
rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -78,3 +79,5 @@ rl.pluginSettingsGet = (pluginSection, name) => {
|
|||
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
|
||||
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
|
||||
};
|
||||
|
||||
rl.pluginPopupView = AbstractViewPopup;
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
import ko from 'ko';
|
||||
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
/*
|
||||
oCallbacks:
|
||||
ItemSelect
|
||||
MiddleClick
|
||||
AutoSelect
|
||||
ItemGetUid
|
||||
UpOrDown
|
||||
*/
|
||||
|
||||
export class Selector {
|
||||
/**
|
||||
|
@ -19,8 +30,8 @@ export class Selector {
|
|||
sItemFocusedSelector
|
||||
) {
|
||||
this.list = koList;
|
||||
this.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
|
||||
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
|
||||
this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
|
||||
this.isListChecked = koComputable(() => 0 < this.listChecked().length);
|
||||
|
||||
this.focusedItem = koFocusedItem || ko.observable(null);
|
||||
this.selectedItem = koSelectedItem || ko.observable(null);
|
||||
|
@ -209,11 +220,9 @@ export class Selector {
|
|||
|
||||
itemSelected(item) {
|
||||
if (this.isListChecked()) {
|
||||
if (!item) {
|
||||
(this.oCallbacks.onItemSelect || (()=>{}))(item || null);
|
||||
}
|
||||
item || (this.oCallbacks.ItemSelect || (()=>0))(null);
|
||||
} else if (item) {
|
||||
(this.oCallbacks.onItemSelect || (()=>{}))(item);
|
||||
(this.oCallbacks.ItemSelect || (()=>0))(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,13 +235,17 @@ export class Selector {
|
|||
this.oContentScrollable = contentScrollable;
|
||||
|
||||
if (contentScrollable) {
|
||||
contentScrollable.addEventListener('click', event => {
|
||||
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||
el && this.actionClick(ko.dataFor(el), event);
|
||||
let getItem = selector => {
|
||||
let el = event.target.closestWithin(selector, contentScrollable);
|
||||
return el ? ko.dataFor(el) : null;
|
||||
};
|
||||
|
||||
el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable);
|
||||
if (el) {
|
||||
const item = ko.dataFor(el);
|
||||
addEventsListeners(contentScrollable, {
|
||||
click: event => {
|
||||
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||
el && this.actionClick(ko.dataFor(el), event);
|
||||
|
||||
const item = getItem(this.sItemCheckedSelector);
|
||||
if (item) {
|
||||
if (event.shiftKey) {
|
||||
this.actionClick(item, event);
|
||||
|
@ -241,26 +254,33 @@ export class Selector {
|
|||
item.checked(!item.checked());
|
||||
}
|
||||
}
|
||||
},
|
||||
auxclick: event => {
|
||||
if (1 == event.button) {
|
||||
const item = getItem(this.sItemSelector);
|
||||
if (item) {
|
||||
this.focusedItem(item);
|
||||
(this.oCallbacks.MiddleClick || (()=>0))(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
shortcuts.add('enter,open', '', keyScope, () => {
|
||||
registerShortcut('enter,open', '', keyScope, () => {
|
||||
const focused = this.focusedItem();
|
||||
if (focused && !focused.selected()) {
|
||||
this.actionClick(focused);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
shortcuts.add('arrowup,arrowdown', 'meta', keyScope, () => false);
|
||||
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
|
||||
|
||||
shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => {
|
||||
addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
|
||||
this.newSelectPosition(event.key, true);
|
||||
return false;
|
||||
});
|
||||
shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
||||
registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
||||
this.newSelectPosition(event.key, false);
|
||||
return false;
|
||||
});
|
||||
|
@ -271,7 +291,7 @@ export class Selector {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
autoSelect() {
|
||||
return !!(this.oCallbacks.onAutoSelect || (()=>true))();
|
||||
return !!(this.oCallbacks.AutoSelect || (()=>1))();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -281,7 +301,7 @@ export class Selector {
|
|||
getItemUid(item) {
|
||||
let uid = '';
|
||||
|
||||
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
|
||||
const getItemUidCallback = this.oCallbacks.ItemGetUid || null;
|
||||
if (getItemUidCallback && item) {
|
||||
uid = getItemUidCallback(item);
|
||||
}
|
||||
|
@ -312,7 +332,7 @@ export class Selector {
|
|||
} else if (++i < listLen) {
|
||||
result = list[i];
|
||||
}
|
||||
result || (this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey);
|
||||
result || (this.oCallbacks.UpOrDown || (()=>0))('ArrowUp' === sEventKey);
|
||||
} else if ('Home' === sEventKey) {
|
||||
result = list[0];
|
||||
} else if ('End' === sEventKey) {
|
||||
|
|
|
@ -2,158 +2,211 @@ import ko from 'ko';
|
|||
import { Notification, UploadErrorCode } from 'Common/Enums';
|
||||
import { langLink } from 'Common/Links';
|
||||
import { doc, createElement } from 'Common/Globals';
|
||||
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
||||
|
||||
let I18N_DATA = window.snappymailI18N || {};
|
||||
let I18N_DATA = {};
|
||||
|
||||
export const trigger = ko.observable(false);
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Object=} valueList
|
||||
* @param {string=} defaulValue
|
||||
* @returns {string}
|
||||
*/
|
||||
export function i18n(key, valueList, defaulValue) {
|
||||
let path = key.split('/');
|
||||
if (!I18N_DATA[path[0]] || !path[1]) {
|
||||
return defaulValue || key;
|
||||
}
|
||||
let result = I18N_DATA[path[0]][path[1]] || defaulValue || key;
|
||||
if (valueList) {
|
||||
Object.entries(valueList).forEach(([key, value]) => {
|
||||
result = result.replace('%' + key + '%', value);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const i18nToNode = element => {
|
||||
const key = element.dataset.i18n;
|
||||
if (key) {
|
||||
if ('[' === key.substr(0, 1)) {
|
||||
switch (key.substr(0, 6)) {
|
||||
case '[html]':
|
||||
element.innerHTML = i18n(key.substr(6));
|
||||
break;
|
||||
case '[place':
|
||||
element.placeholder = i18n(key.substr(13));
|
||||
break;
|
||||
case '[title':
|
||||
element.title = i18n(key.substr(7));
|
||||
break;
|
||||
// no default
|
||||
const
|
||||
i18nToNode = element => {
|
||||
const key = element.dataset.i18n;
|
||||
if (key) {
|
||||
if ('[' === key.slice(0, 1)) {
|
||||
switch (key.slice(0, 6)) {
|
||||
case '[html]':
|
||||
element.innerHTML = i18n(key.slice(6));
|
||||
break;
|
||||
case '[place':
|
||||
element.placeholder = i18n(key.slice(13));
|
||||
break;
|
||||
case '[title':
|
||||
element.title = i18n(key.slice(7));
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
} else {
|
||||
element.textContent = i18n(key);
|
||||
}
|
||||
} else {
|
||||
element.textContent = i18n(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
init = () => {
|
||||
if (rl.I18N) {
|
||||
I18N_DATA = rl.I18N;
|
||||
Date.defineRelativeTimeFormat(rl.relativeTime || {});
|
||||
rl.I18N = null;
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
|
||||
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
||||
|
||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} elements
|
||||
* @param {boolean=} animate = false
|
||||
*/
|
||||
export function i18nToNodes(element) {
|
||||
setTimeout(() =>
|
||||
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
|
||||
, 1);
|
||||
}
|
||||
export const
|
||||
trigger = ko.observable(false),
|
||||
|
||||
/**
|
||||
* @param {Function} startCallback
|
||||
* @param {Function=} langCallback = null
|
||||
*/
|
||||
export function initOnStartOrLangChange(startCallback, langCallback = null) {
|
||||
startCallback && startCallback();
|
||||
startCallback && trigger.subscribe(startCallback);
|
||||
langCallback && trigger.subscribe(langCallback);
|
||||
}
|
||||
|
||||
function getNotificationMessage(code) {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
if (key) {
|
||||
key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
|
||||
return I18N_DATA.NOTIFICATIONS[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} code
|
||||
* @param {*=} message = ''
|
||||
* @param {*=} defCode = null
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getNotification(code, message = '', defCode = 0) {
|
||||
code = parseInt(code, 10) || 0;
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return getNotificationMessage(code)
|
||||
|| getNotificationMessage(parseInt(defCode, 10))
|
||||
|| '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getUploadErrorDescByCode(code) {
|
||||
let result = 'UNKNOWN';
|
||||
code = parseInt(code, 10) || 0;
|
||||
switch (code) {
|
||||
case UploadErrorCode.FileIsTooBig:
|
||||
case UploadErrorCode.FilePartiallyUploaded:
|
||||
case UploadErrorCode.NoFileUploaded:
|
||||
case UploadErrorCode.MissingTempFolder:
|
||||
case UploadErrorCode.OnSavingFile:
|
||||
case UploadErrorCode.FileType:
|
||||
result = i18nKey(getKeyByValue(UploadErrorCode, code));
|
||||
break;
|
||||
}
|
||||
return i18n('UPLOAD/ERROR_' + result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} admin
|
||||
* @param {string} language
|
||||
*/
|
||||
export function reload(admin, language) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = createElement('script');
|
||||
script.onload = () => {
|
||||
// reload the data
|
||||
if (window.snappymailI18N) {
|
||||
I18N_DATA = window.snappymailI18N;
|
||||
i18nToNodes(doc);
|
||||
dispatchEvent(new CustomEvent('reload-time'));
|
||||
trigger(!trigger());
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Object=} valueList
|
||||
* @param {string=} defaulValue
|
||||
* @returns {string}
|
||||
*/
|
||||
i18n = (key, valueList, defaulValue) => {
|
||||
let result = defaulValue || key;
|
||||
if (key) {
|
||||
let path = key.split('/');
|
||||
if (I18N_DATA[path[0]] && path[1]) {
|
||||
result = I18N_DATA[path[0]][path[1]] || result;
|
||||
}
|
||||
window.snappymailI18N = null;
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
||||
script.src = langLink(language, admin);
|
||||
// script.async = true;
|
||||
doc.head.append(script);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (valueList) {
|
||||
forEachObjectEntry(valueList, (key, value) => {
|
||||
result = result.replace('%' + key + '%', value);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} language
|
||||
* @param {boolean=} isEng = false
|
||||
* @returns {string}
|
||||
*/
|
||||
export function convertLangName(language, isEng = false) {
|
||||
return i18n(
|
||||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
||||
null,
|
||||
language
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {Object} elements
|
||||
* @param {boolean=} animate = false
|
||||
*/
|
||||
i18nToNodes = element =>
|
||||
setTimeout(() =>
|
||||
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
|
||||
, 1),
|
||||
|
||||
timestampToString = (timeStampInUTC, formatStr) => {
|
||||
const now = Date.now(),
|
||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||
|
||||
if (31536000000 < time) {
|
||||
const m = new Date(time);
|
||||
switch (formatStr) {
|
||||
case 'FROMNOW':
|
||||
return m.fromNow();
|
||||
case 'SHORT': {
|
||||
if (4 >= (now - time) / 3600000)
|
||||
return m.fromNow();
|
||||
const mt = m.getTime(), date = new Date,
|
||||
dt = date.setHours(0,0,0,0);
|
||||
if (mt > dt)
|
||||
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
||||
if (mt > dt - 86400000)
|
||||
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
||||
if (date.getFullYear() === m.getFullYear())
|
||||
return m.format('d M');
|
||||
return m.format('LL');
|
||||
}
|
||||
case 'FULL':
|
||||
return m.format('LLL');
|
||||
default:
|
||||
return m.format(formatStr);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
timeToNode = (element, time) => {
|
||||
try {
|
||||
if (time) {
|
||||
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
||||
} else {
|
||||
time = Date.parse(element.dateTime) / 1000;
|
||||
}
|
||||
|
||||
let key = element.dataset.momentFormat;
|
||||
if (key) {
|
||||
element.textContent = timestampToString(time, key);
|
||||
}
|
||||
|
||||
if ((key = element.dataset.momentFormatTitle)) {
|
||||
element.title = timestampToString(time, key);
|
||||
}
|
||||
} catch (e) {
|
||||
// prevent knockout crashes
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
reloadTime = () => setTimeout(() =>
|
||||
doc.querySelectorAll('time').forEach(element => timeToNode(element))
|
||||
, 1),
|
||||
|
||||
/**
|
||||
* @param {Function} startCallback
|
||||
* @param {Function=} langCallback = null
|
||||
*/
|
||||
initOnStartOrLangChange = (startCallback, langCallback = null) => {
|
||||
startCallback && startCallback();
|
||||
startCallback && trigger.subscribe(startCallback);
|
||||
langCallback && trigger.subscribe(langCallback);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} code
|
||||
* @param {*=} message = ''
|
||||
* @param {*=} defCode = null
|
||||
* @returns {string}
|
||||
*/
|
||||
getNotification = (code, message = '', defCode = 0) => {
|
||||
code = parseInt(code, 10) || 0;
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return getNotificationMessage(code)
|
||||
|| getNotificationMessage(parseInt(defCode, 10))
|
||||
|| '';
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {*} code
|
||||
* @returns {string}
|
||||
*/
|
||||
getUploadErrorDescByCode = code => {
|
||||
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
|
||||
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {boolean} admin
|
||||
* @param {string} language
|
||||
*/
|
||||
translatorReload = (admin, language) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const script = createElement('script');
|
||||
script.onload = () => {
|
||||
// reload the data
|
||||
if (init()) {
|
||||
i18nToNodes(doc);
|
||||
admin || reloadTime();
|
||||
trigger(!trigger());
|
||||
}
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
||||
script.src = langLink(language, admin);
|
||||
// script.async = true;
|
||||
doc.head.append(script);
|
||||
}),
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} language
|
||||
* @param {boolean=} isEng = false
|
||||
* @returns {string}
|
||||
*/
|
||||
convertLangName = (language, isEng = false) =>
|
||||
i18n(
|
||||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
||||
null,
|
||||
language
|
||||
);
|
||||
|
||||
init();
|
||||
|
|
|
@ -1,107 +1,50 @@
|
|||
import { SaveSettingsStep } from 'Common/Enums';
|
||||
import { doc, createElement, elementById } from 'Common/Globals';
|
||||
|
||||
export const
|
||||
isArray = Array.isArray,
|
||||
isNonEmptyArray = array => isArray(array) && array.length,
|
||||
isFunction = v => typeof v === 'function';
|
||||
|
||||
/**
|
||||
* @param {*} value
|
||||
* @param {number=} defaultValue = 0
|
||||
* @returns {number}
|
||||
*/
|
||||
export function pInt(value, defaultValue = 0) {
|
||||
value = parseInt(value, 10);
|
||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function pString(value) {
|
||||
return null != value ? '' + value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function inFocus() {
|
||||
try {
|
||||
return doc.activeElement && doc.activeElement.matches(
|
||||
'input,textarea,.cke_editable'
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} theme
|
||||
* @returns {string}
|
||||
*/
|
||||
export const convertThemeName = theme => {
|
||||
if ('@custom' === theme.substr(-7)) {
|
||||
theme = theme.substr(0, theme.length - 7).trim();
|
||||
}
|
||||
|
||||
return theme
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} domOption
|
||||
* @param {object} item
|
||||
* @returns {void}
|
||||
*/
|
||||
export function defaultOptionsAfterRender(domItem, item) {
|
||||
if (item && undefined !== item.disabled && domItem) {
|
||||
domItem.classList.toggle('disabled', domItem.disabled = item.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} koTrigger
|
||||
* @param {mixed} context
|
||||
* @returns {mixed}
|
||||
*/
|
||||
export function settingsSaveHelperSimpleFunction(koTrigger, context) {
|
||||
return iError => {
|
||||
koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
|
||||
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
|
||||
};
|
||||
}
|
||||
import { elementById } from 'Common/Globals';
|
||||
|
||||
let __themeTimer = 0,
|
||||
__themeJson = null;
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {function=} themeTrigger = noop
|
||||
* @returns {void}
|
||||
*/
|
||||
export function changeTheme(value, themeTrigger = ()=>{}) {
|
||||
const themeLink = elementById('app-theme-link'),
|
||||
clearTimer = () => {
|
||||
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
|
||||
__themeJson = null;
|
||||
};
|
||||
export const
|
||||
isArray = Array.isArray,
|
||||
arrayLength = array => isArray(array) && array.length,
|
||||
isFunction = v => typeof v === 'function',
|
||||
pString = value => null != value ? '' + value : '',
|
||||
|
||||
let themeStyle = elementById('app-theme-style'),
|
||||
url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
|
||||
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||
|
||||
if (url) {
|
||||
url = url.toString()
|
||||
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
|
||||
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
|
||||
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
|
||||
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
|
||||
|
||||
if ('Json/' !== url.substr(-5)) {
|
||||
url += 'Json/';
|
||||
}
|
||||
pInt = (value, defaultValue = 0) => {
|
||||
value = parseInt(value, 10);
|
||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
||||
},
|
||||
|
||||
convertThemeName = theme => theme
|
||||
.replace(/@custom$/, '')
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
||||
.trim(),
|
||||
|
||||
defaultOptionsAfterRender = (domItem, item) =>
|
||||
domItem && item && undefined !== item.disabled
|
||||
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
|
||||
|
||||
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
||||
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
|
||||
/* // Without deprecated 'unescape':
|
||||
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
|
||||
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
||||
)),
|
||||
*/
|
||||
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
|
||||
changeTheme = (value, themeTrigger = ()=>0) => {
|
||||
const themeStyle = elementById('app-theme-style'),
|
||||
clearTimer = () => {
|
||||
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
|
||||
__themeJson = null;
|
||||
},
|
||||
url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
|
||||
|
||||
clearTimeout(__themeTimer);
|
||||
|
||||
|
@ -117,36 +60,12 @@ export function changeTheme(value, themeTrigger = ()=>{}) {
|
|||
}
|
||||
rl.fetchJSON(url, init)
|
||||
.then(data => {
|
||||
if (data && isArray(data) && 2 === data.length) {
|
||||
if (themeLink && !themeStyle) {
|
||||
themeStyle = createElement('style');
|
||||
themeStyle.id = 'app-theme-style';
|
||||
themeLink.after(themeStyle);
|
||||
themeLink.remove();
|
||||
}
|
||||
|
||||
if (themeStyle) {
|
||||
themeStyle.textContent = data[1];
|
||||
themeStyle.dataset.href = url;
|
||||
themeStyle.dataset.theme = data[0];
|
||||
}
|
||||
|
||||
if (2 === arrayLength(data)) {
|
||||
themeStyle.textContent = data[1];
|
||||
themeTrigger(SaveSettingsStep.TrueResult);
|
||||
}
|
||||
})
|
||||
.then(clearTimer, clearTimer);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
export function addObservablesTo(target, observables) {
|
||||
Object.entries(observables).forEach(([key, value]) =>
|
||||
target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) );
|
||||
}
|
||||
|
||||
export function addComputablesTo(target, computables) {
|
||||
Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn));
|
||||
}
|
||||
|
||||
export function addSubscribablesTo(target, subscribables) {
|
||||
Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn));
|
||||
}
|
||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
||||
|
|
|
@ -1,341 +1,46 @@
|
|||
import { ComposeType, FolderType } from 'Common/EnumsUser';
|
||||
import { MessageFlagsCache, addRequestedMessage } from 'Common/Cache';
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility } from 'Common/Globals';
|
||||
import { plainToHtml } from 'Common/Html';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { encodeHtml } from 'Common/Html';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { createElement } from 'Common/Globals';
|
||||
import { MessageModel } from 'Model/Message';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import * as Local from 'Storage/Client';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export const
|
||||
|
||||
dropdownsDetectVisibility = (() =>
|
||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||
).debounce(50),
|
||||
|
||||
/**
|
||||
* @param {(string|number)} value
|
||||
* @param {boolean=} includeZero = true
|
||||
* @param {string} link
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPosNumeric(value, includeZero = true) {
|
||||
return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {number=} len = 100
|
||||
* @returns {string}
|
||||
*/
|
||||
function splitPlainText(text, len = 100) {
|
||||
let prefix = '',
|
||||
subText = '',
|
||||
result = text,
|
||||
spacePos = 0,
|
||||
newLinePos = 0;
|
||||
|
||||
while (result.length > len) {
|
||||
subText = result.substr(0, len);
|
||||
spacePos = subText.lastIndexOf(' ');
|
||||
newLinePos = subText.lastIndexOf('\n');
|
||||
|
||||
if (-1 !== newLinePos) {
|
||||
spacePos = newLinePos;
|
||||
}
|
||||
|
||||
if (-1 === spacePos) {
|
||||
spacePos = len;
|
||||
}
|
||||
|
||||
prefix += subText.substr(0, spacePos) + '\n';
|
||||
result = result.substr(spacePos + 1);
|
||||
download = (link, name = "") => {
|
||||
if (ThemeStore.isMobile()) {
|
||||
open(link, '_self');
|
||||
focus();
|
||||
} else {
|
||||
const oLink = createElement('a');
|
||||
oLink.href = link;
|
||||
oLink.target = '_blank';
|
||||
oLink.download = name;
|
||||
doc.body.appendChild(oLink).click();
|
||||
oLink.remove();
|
||||
}
|
||||
|
||||
return prefix + result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @returns {string}
|
||||
*/
|
||||
export function htmlToPlain(html) {
|
||||
let pos = 0,
|
||||
limit = 0,
|
||||
iP1 = 0,
|
||||
iP2 = 0,
|
||||
iP3 = 0,
|
||||
text = '';
|
||||
|
||||
const convertBlockquote = (blockquoteText) => {
|
||||
blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
|
||||
return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
|
||||
args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
|
||||
);
|
||||
};
|
||||
|
||||
const convertDivs = (...args) => {
|
||||
if (args && 1 < args.length) {
|
||||
let divText = args[1].trim();
|
||||
if (divText.length) {
|
||||
divText = divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs);
|
||||
divText = '\n' + divText.trim() + '\n';
|
||||
}
|
||||
|
||||
return divText;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const
|
||||
tpl = createElement('template'),
|
||||
convertPre = (...args) =>
|
||||
args && 1 < args.length
|
||||
? args[1]
|
||||
.toString()
|
||||
.replace(/[\n]/gm, '<br/>')
|
||||
.replace(/[\r]/gm, '')
|
||||
: '',
|
||||
fixAttibuteValue = (...args) => (args && 1 < args.length ? '' + args[1] + encodeHtml(args[2]) : ''),
|
||||
convertLinks = (...args) => (args && 1 < args.length ? args[1].trim() : '');
|
||||
|
||||
tpl.innerHTML = html
|
||||
.replace(/<p[^>]*><\/p>/gi, '')
|
||||
.replace(/<pre[^>]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre)
|
||||
.replace(/[\s]+/gm, ' ')
|
||||
.replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue)
|
||||
.replace(/<br[^>]*>/gim, '\n')
|
||||
.replace(/<\/h[\d]>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n\n')
|
||||
.replace(/<ul[^>]*>/gim, '\n')
|
||||
.replace(/<\/ul>/gi, '\n')
|
||||
.replace(/<li[^>]*>/gim, ' * ')
|
||||
.replace(/<\/li>/gi, '\n')
|
||||
.replace(/<\/td>/gi, '\n')
|
||||
.replace(/<\/tr>/gi, '\n')
|
||||
.replace(/<hr[^>]*>/gim, '\n_______________________________\n\n')
|
||||
.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs)
|
||||
.replace(/<blockquote[^>]*>/gim, '\n__bq__start__\n')
|
||||
.replace(/<\/blockquote>/gim, '\n__bq__end__\n')
|
||||
.replace(/<a [^>]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks)
|
||||
.replace(/<\/div>/gi, '\n')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/<[^>]*>/gm, '');
|
||||
|
||||
text = splitPlainText(tpl.content.textContent
|
||||
.replace(/\n[ \t]+/gm, '\n')
|
||||
.replace(/[\n]{3,}/gm, '\n\n')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/&/gi, '&')
|
||||
);
|
||||
|
||||
pos = 0;
|
||||
limit = 800;
|
||||
|
||||
while (0 < limit) {
|
||||
--limit;
|
||||
iP1 = text.indexOf('__bq__start__', pos);
|
||||
if (-1 < iP1) {
|
||||
iP2 = text.indexOf('__bq__start__', iP1 + 5);
|
||||
iP3 = text.indexOf('__bq__end__', iP1 + 5);
|
||||
|
||||
if ((-1 === iP2 || iP3 < iP2) && iP1 < iP3) {
|
||||
text = text.substr(0, iP1) + convertBlockquote(text.substring(iP1 + 13, iP3)) + text.substr(iP3 + 11);
|
||||
pos = 0;
|
||||
} else if (-1 < iP2 && iP2 < iP3) {
|
||||
pos = iP2 - 1;
|
||||
} else {
|
||||
pos = 0;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return text.replace(/__bq__start__|__bq__end__/gm, '').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} plain
|
||||
* @param {boolean} findEmailAndLinksInText = false
|
||||
* @returns {string}
|
||||
*/
|
||||
export function plainToHtml(plain) {
|
||||
plain = plain.toString().replace(/\r/g, '');
|
||||
plain = plain.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
|
||||
|
||||
let bIn = false,
|
||||
bDo = true,
|
||||
bStart = true,
|
||||
aNextText = [],
|
||||
aText = plain.split('\n');
|
||||
|
||||
do {
|
||||
bDo = false;
|
||||
aNextText = [];
|
||||
aText.forEach(sLine => {
|
||||
bStart = '>' === sLine.substr(0, 1);
|
||||
if (bStart && !bIn) {
|
||||
bDo = true;
|
||||
bIn = true;
|
||||
aNextText.push('~~~blockquote~~~');
|
||||
aNextText.push(sLine.substr(1));
|
||||
} else if (!bStart && bIn) {
|
||||
if (sLine) {
|
||||
bIn = false;
|
||||
aNextText.push('~~~/blockquote~~~');
|
||||
aNextText.push(sLine);
|
||||
} else {
|
||||
aNextText.push(sLine);
|
||||
}
|
||||
} else if (bStart && bIn) {
|
||||
aNextText.push(sLine.substr(1));
|
||||
} else {
|
||||
aNextText.push(sLine);
|
||||
}
|
||||
});
|
||||
|
||||
if (bIn) {
|
||||
bIn = false;
|
||||
aNextText.push('~~~/blockquote~~~');
|
||||
}
|
||||
|
||||
aText = aNextText;
|
||||
} while (bDo);
|
||||
|
||||
return aText.join('\n')
|
||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(/~~~blockquote~~~[\s]*/g, '<blockquote>')
|
||||
.replace(/[\s]*~~~\/blockquote~~~/g, '</blockquote>')
|
||||
.replace(/\n/g, '<br/>');
|
||||
}
|
||||
|
||||
rl.Utils = {
|
||||
htmlToPlain: htmlToPlain,
|
||||
plainToHtml: plainToHtml
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array} aSystem
|
||||
* @param {Array} aList
|
||||
* @param {Array=} aDisabled
|
||||
* @param {Array=} aHeaderLines
|
||||
* @param {Function=} fDisableCallback
|
||||
* @param {Function=} fRenameCallback
|
||||
* @param {boolean=} bSystem
|
||||
* @param {boolean=} bBuildUnvisible
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function folderListOptionsBuilder(
|
||||
aSystem,
|
||||
aList,
|
||||
aDisabled,
|
||||
aHeaderLines,
|
||||
fDisableCallback,
|
||||
fRenameCallback,
|
||||
bSystem,
|
||||
bBuildUnvisible
|
||||
) {
|
||||
let /**
|
||||
* @type {?FolderModel}
|
||||
*/
|
||||
bSep = false,
|
||||
aResult = [];
|
||||
|
||||
const sDeepPrefix = '\u00A0\u00A0\u00A0';
|
||||
|
||||
bBuildUnvisible = undefined === bBuildUnvisible ? false : !!bBuildUnvisible;
|
||||
bSystem = null == bSystem ? 0 < aSystem.length : bSystem;
|
||||
fDisableCallback = null != fDisableCallback ? fDisableCallback : null;
|
||||
fRenameCallback = null != fRenameCallback ? fRenameCallback : null;
|
||||
|
||||
if (!isArray(aDisabled)) {
|
||||
aDisabled = [];
|
||||
}
|
||||
|
||||
if (!isArray(aHeaderLines)) {
|
||||
aHeaderLines = [];
|
||||
}
|
||||
|
||||
aHeaderLines.forEach(line => {
|
||||
aResult.push({
|
||||
id: line[0],
|
||||
name: line[1],
|
||||
system: false,
|
||||
dividerbar: false,
|
||||
disabled: false
|
||||
});
|
||||
});
|
||||
|
||||
bSep = true;
|
||||
aSystem.forEach(oItem => {
|
||||
aResult.push({
|
||||
id: oItem.fullNameRaw,
|
||||
name: fRenameCallback ? fRenameCallback(oItem) : oItem.name(),
|
||||
system: true,
|
||||
dividerbar: bSep,
|
||||
disabled:
|
||||
!oItem.selectable ||
|
||||
aDisabled.includes(oItem.fullNameRaw) ||
|
||||
(fDisableCallback ? fDisableCallback(oItem) : false)
|
||||
});
|
||||
bSep = false;
|
||||
});
|
||||
|
||||
bSep = true;
|
||||
aList.forEach(oItem => {
|
||||
// if (oItem.subscribed() || !oItem.exists || bBuildUnvisible)
|
||||
if (
|
||||
(oItem.subscribed() || !oItem.exists || bBuildUnvisible) &&
|
||||
(oItem.selectable || oItem.hasSubscribedSubfolders())
|
||||
) {
|
||||
if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubscribedSubfolders()) {
|
||||
aResult.push({
|
||||
id: oItem.fullNameRaw,
|
||||
name:
|
||||
sDeepPrefix.repeat(oItem.deep + 1) +
|
||||
(fRenameCallback ? fRenameCallback(oItem) : oItem.name()),
|
||||
system: false,
|
||||
dividerbar: bSep,
|
||||
disabled:
|
||||
!oItem.selectable ||
|
||||
aDisabled.includes(oItem.fullNameRaw) ||
|
||||
(fDisableCallback ? fDisableCallback(oItem) : false)
|
||||
});
|
||||
bSep = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (oItem.subscribed() && oItem.subFolders.length) {
|
||||
aResult = aResult.concat(
|
||||
folderListOptionsBuilder(
|
||||
[],
|
||||
oItem.subFolders(),
|
||||
aDisabled,
|
||||
[],
|
||||
fDisableCallback,
|
||||
fRenameCallback,
|
||||
bSystem,
|
||||
bBuildUnvisible
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
|
||||
* @param {Object|Array} objectOrObjects
|
||||
* @returns {void}
|
||||
*/
|
||||
export function delegateRunOnDestroy(objectOrObjects) {
|
||||
objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
|
||||
obj => obj.onDestroy && obj.onDestroy()
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {function}
|
||||
*/
|
||||
export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
||||
computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||
return () => {
|
||||
const currentPage = koCurrentPage(),
|
||||
pageCount = koPageCount(),
|
||||
|
@ -395,13 +100,13 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
|||
if (3 === prev) {
|
||||
fAdd(2, false);
|
||||
} else if (3 < prev) {
|
||||
fAdd(Math.round((prev - 1) / 2), false, '...');
|
||||
fAdd(Math.round((prev - 1) / 2), false, '…');
|
||||
}
|
||||
|
||||
if (pageCount - 2 === next) {
|
||||
fAdd(pageCount - 1, true);
|
||||
} else if (pageCount - 2 > next) {
|
||||
fAdd(Math.round((pageCount + next) / 2), true, '...');
|
||||
fAdd(Math.round((pageCount + next) / 2), true, '…');
|
||||
}
|
||||
|
||||
// first and last
|
||||
|
@ -416,81 +121,253 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
|||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} mailToUrl
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function mailToHelper(mailToUrl) {
|
||||
if (
|
||||
mailToUrl &&
|
||||
'mailto:' ===
|
||||
mailToUrl
|
||||
.toString()
|
||||
.substr(0, 7)
|
||||
.toLowerCase()
|
||||
) {
|
||||
mailToUrl = mailToUrl.toString().substr(7);
|
||||
mailToHelper = mailToUrl => {
|
||||
if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) {
|
||||
mailToUrl = mailToUrl.slice(7).split('?');
|
||||
|
||||
let to = [],
|
||||
cc = null,
|
||||
bcc = null,
|
||||
params = {};
|
||||
|
||||
const email = mailToUrl.replace(/\?.+$/, ''),
|
||||
query = mailToUrl.replace(/^[^?]*\?/, '');
|
||||
|
||||
query.split('&').forEach(temp => {
|
||||
temp = temp.split('=');
|
||||
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
|
||||
});
|
||||
|
||||
if (undefined !== params.to) {
|
||||
to = EmailModel.parseEmailLine(decodeURIComponent(email + ',' + params.to));
|
||||
to = Object.values(
|
||||
to.reduce((result, value) => {
|
||||
if (value) {
|
||||
if (result[value.email]) {
|
||||
if (!result[value.email].name) {
|
||||
result[value.email] = value;
|
||||
}
|
||||
} else {
|
||||
result[value.email] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, {})
|
||||
);
|
||||
} else {
|
||||
to = EmailModel.parseEmailLine(email);
|
||||
}
|
||||
|
||||
if (undefined !== params.cc) {
|
||||
cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc));
|
||||
}
|
||||
|
||||
if (undefined !== params.bcc) {
|
||||
bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc));
|
||||
}
|
||||
const
|
||||
email = mailToUrl[0],
|
||||
params = new URLSearchParams(mailToUrl[1]),
|
||||
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
|
||||
|
||||
showMessageComposer([
|
||||
ComposeType.Empty,
|
||||
null,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
null == params.subject ? null : decodeURIComponent(params.subject),
|
||||
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
|
||||
params.get('to')
|
||||
? Object.values(
|
||||
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
|
||||
if (value) {
|
||||
if (result[value.email]) {
|
||||
if (!result[value.email].name) {
|
||||
result[value.email] = value;
|
||||
}
|
||||
} else {
|
||||
result[value.email] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, {})
|
||||
)
|
||||
: EmailModel.parseEmailLine(email),
|
||||
toEmailModel(params.get('cc')),
|
||||
toEmailModel(params.get('bcc')),
|
||||
params.get('subject'),
|
||||
plainToHtml(params.get('body') || '')
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
export function showMessageComposer(params = [])
|
||||
showMessageComposer = (params = []) =>
|
||||
{
|
||||
rl.app.showMessageComposer(params);
|
||||
}
|
||||
},
|
||||
|
||||
initFullscreen = (el, fn) =>
|
||||
{
|
||||
let event = 'fullscreenchange';
|
||||
if (!el.requestFullscreen && el.webkitRequestFullscreen) {
|
||||
el.requestFullscreen = el.webkitRequestFullscreen;
|
||||
event = 'webkit'+event;
|
||||
}
|
||||
if (el.requestFullscreen) {
|
||||
el.addEventListener(event, fn);
|
||||
return el;
|
||||
}
|
||||
},
|
||||
|
||||
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
||||
{
|
||||
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
||||
target.removeAttribute('style');
|
||||
source.removeAttribute('style');
|
||||
}
|
||||
// source.classList.toggle('resizable', mode);
|
||||
if (mode) {
|
||||
const length = Local.get(sClientSideKeyName+mode);
|
||||
if (!source.layoutResizer) {
|
||||
const resizer = createElement('div', {'class':'resizer'}),
|
||||
size = {},
|
||||
store = () => {
|
||||
if ('Width' == resizer.mode) {
|
||||
target.style.left = source.offsetWidth + 'px';
|
||||
Local.set(resizer.key+resizer.mode, source.offsetWidth);
|
||||
} else {
|
||||
target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
|
||||
Local.set(resizer.key+resizer.mode, source.offsetHeight);
|
||||
}
|
||||
},
|
||||
cssint = s => {
|
||||
let value = getComputedStyle(source, null)[s].replace('px', '');
|
||||
if (value.includes('%')) {
|
||||
value = source.parentElement['offset'+resizer.mode]
|
||||
* value.replace('%', '') / 100;
|
||||
}
|
||||
return parseFloat(value);
|
||||
};
|
||||
source.layoutResizer = resizer;
|
||||
source.append(resizer);
|
||||
resizer.addEventListener('mousedown', {
|
||||
handleEvent: function(e) {
|
||||
if ('mousedown' == e.type) {
|
||||
const lmode = resizer.mode.toLowerCase();
|
||||
e.preventDefault();
|
||||
size.pos = ('width' == lmode) ? e.pageX : e.pageY;
|
||||
size.min = cssint('min-'+lmode);
|
||||
size.max = cssint('max-'+lmode);
|
||||
size.org = cssint(lmode);
|
||||
addEventListener('mousemove', this);
|
||||
addEventListener('mouseup', this);
|
||||
} else if ('mousemove' == e.type) {
|
||||
const lmode = resizer.mode.toLowerCase(),
|
||||
length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
|
||||
if (length >= size.min && length <= size.max ) {
|
||||
source.style[lmode] = length + 'px';
|
||||
source.observer || store();
|
||||
}
|
||||
} else if ('mouseup' == e.type) {
|
||||
removeEventListener('mousemove', this);
|
||||
removeEventListener('mouseup', this);
|
||||
}
|
||||
}
|
||||
});
|
||||
source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
|
||||
}
|
||||
source.layoutResizer.mode = mode;
|
||||
source.layoutResizer.key = sClientSideKeyName;
|
||||
source.observer && source.observer.observe(source, { box: 'border-box' });
|
||||
if (length) {
|
||||
source.style[mode] = length + 'px';
|
||||
}
|
||||
} else {
|
||||
source.observer && source.observer.disconnect();
|
||||
}
|
||||
},
|
||||
|
||||
populateMessageBody = (oMessage, preload) => {
|
||||
if (oMessage) {
|
||||
preload || MessageUserStore.hideMessageBodies();
|
||||
preload || MessageUserStore.loading(true);
|
||||
Remote.message((iError, oData/*, bCached*/) => {
|
||||
if (iError) {
|
||||
if (Notification.RequestAborted !== iError && !preload) {
|
||||
MessageUserStore.message(null);
|
||||
MessageUserStore.error(getNotification(iError));
|
||||
}
|
||||
} else {
|
||||
oMessage = preload ? oMessage : null;
|
||||
let
|
||||
isNew = false,
|
||||
json = oData && oData.Result,
|
||||
message = oMessage || MessageUserStore.message();
|
||||
|
||||
if (
|
||||
json &&
|
||||
MessageModel.validJson(json) &&
|
||||
message &&
|
||||
message.folder === json.Folder
|
||||
) {
|
||||
const threads = message.threads(),
|
||||
messagesDom = MessageUserStore.bodiesDom();
|
||||
if (!oMessage && message.uid != json.Uid && threads.includes(json.Uid)) {
|
||||
message = MessageModel.reviveFromJson(json);
|
||||
if (message) {
|
||||
message.threads(threads);
|
||||
MessageFlagsCache.initMessage(message);
|
||||
|
||||
// Set clone
|
||||
MessageUserStore.message(MessageModel.fromMessageListItem(message));
|
||||
message = MessageUserStore.message();
|
||||
|
||||
isNew = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (message && message.uid == json.Uid) {
|
||||
oMessage || MessageUserStore.error('');
|
||||
/*
|
||||
if (bCached) {
|
||||
delete json.Flags;
|
||||
}
|
||||
*/
|
||||
isNew || message.revivePropertiesFromJson(json);
|
||||
addRequestedMessage(message.folder, message.uid);
|
||||
if (messagesDom) {
|
||||
let id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''),
|
||||
body = elementById(id);
|
||||
if (body) {
|
||||
message.body = body;
|
||||
message.isHtml(body.classList.contains('html'));
|
||||
message.hasImages(body.rlHasImages);
|
||||
} else {
|
||||
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
||||
+ (message.pgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (message.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
+ '">'
|
||||
+ '</div>');
|
||||
message.body = body;
|
||||
if (!SettingsUserStore.viewHTML() || !message.viewHtml()) {
|
||||
message.viewPlain();
|
||||
}
|
||||
|
||||
MessageUserStore.purgeMessageBodyCache();
|
||||
}
|
||||
|
||||
messagesDom.append(body);
|
||||
|
||||
if (!oMessage) {
|
||||
MessageUserStore.activeDom(message.body);
|
||||
MessageUserStore.hideMessageBodies();
|
||||
message.body.hidden = false;
|
||||
}
|
||||
oMessage && message.viewPopupMessage();
|
||||
}
|
||||
|
||||
MessageFlagsCache.initMessage(message);
|
||||
if (message.isUnseen()) {
|
||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||
() => MessagelistUserStore.setAction(message.folder, MessageSetAction.SetSeen, [message]),
|
||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||
);
|
||||
}
|
||||
|
||||
if (message && isNew) {
|
||||
let selectedMessage = MessagelistUserStore.selectedMessage();
|
||||
if (
|
||||
selectedMessage &&
|
||||
(message.folder !== selectedMessage.folder || message.uid != selectedMessage.uid)
|
||||
) {
|
||||
MessagelistUserStore.selectedMessage(null);
|
||||
if (1 === MessagelistUserStore.length) {
|
||||
MessagelistUserStore.focusedMessage(null);
|
||||
}
|
||||
} else if (!selectedMessage) {
|
||||
selectedMessage = MessagelistUserStore.find(
|
||||
subMessage =>
|
||||
subMessage &&
|
||||
subMessage.folder === message.folder &&
|
||||
subMessage.uid == message.uid
|
||||
);
|
||||
|
||||
if (selectedMessage) {
|
||||
MessagelistUserStore.selectedMessage(selectedMessage);
|
||||
MessagelistUserStore.focusedMessage(selectedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
preload || MessageUserStore.loading(false);
|
||||
}, oMessage.folder, oMessage.uid);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
export class AbstractComponent {
|
||||
constructor() {
|
||||
this.disposable = [];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.forEach((funcToDispose) => {
|
||||
if (funcToDispose && funcToDispose.dispose) {
|
||||
funcToDispose.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,27 +1,15 @@
|
|||
import ko from 'ko';
|
||||
import { AbstractComponent } from 'Component/Abstract';
|
||||
|
||||
class AbstractCheckbox extends AbstractComponent {
|
||||
export class AbstractCheckbox {
|
||||
/**
|
||||
* @param {Object} params = {}
|
||||
*/
|
||||
constructor(params = {}) {
|
||||
super();
|
||||
this.value = ko.isObservable(params.value) ? params.value
|
||||
: ko.observable(!!params.value);
|
||||
|
||||
this.value = params.value;
|
||||
if (undefined === this.value || !this.value.subscribe) {
|
||||
this.value = ko.observable(!!this.value);
|
||||
}
|
||||
|
||||
this.enable = params.enable;
|
||||
if (undefined === this.enable || !this.enable.subscribe) {
|
||||
this.enable = ko.observable(undefined === this.enable || !!this.enable);
|
||||
}
|
||||
|
||||
this.disable = params.disable;
|
||||
if (undefined === this.disable || !this.disable.subscribe) {
|
||||
this.disable = ko.observable(!!this.disable);
|
||||
}
|
||||
this.enable = ko.isObservable(params.enable) ? params.enable
|
||||
: ko.observable(undefined === params.enable || !!params.enable);
|
||||
|
||||
this.label = params.label || '';
|
||||
this.inline = !!params.inline;
|
||||
|
@ -30,10 +18,6 @@ class AbstractCheckbox extends AbstractComponent {
|
|||
}
|
||||
|
||||
click() {
|
||||
if (this.enable() && !this.disable()) {
|
||||
this.value(!this.value());
|
||||
}
|
||||
this.enable() && this.value(!this.value());
|
||||
}
|
||||
}
|
||||
|
||||
export { AbstractCheckbox };
|
||||
|
|
|
@ -1,61 +1,57 @@
|
|||
import ko from 'ko';
|
||||
import { pInt } from 'Common/Utils';
|
||||
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
|
||||
*/
|
||||
constructor(params) {
|
||||
super();
|
||||
|
||||
this.value = params.value || '';
|
||||
this.size = params.size || 0;
|
||||
this.label = params.label || '';
|
||||
this.preLabel = params.preLabel || '';
|
||||
this.enable = undefined === params.enable ? true : params.enable;
|
||||
this.enable = null == params.enable ? true : params.enable;
|
||||
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null;
|
||||
this.placeholder = params.placeholder || '';
|
||||
|
||||
this.labeled = undefined !== params.label;
|
||||
this.preLabeled = undefined !== params.preLabel;
|
||||
this.triggered = undefined !== params.trigger && !!this.trigger;
|
||||
|
||||
this.classForTrigger = ko.observable('');
|
||||
|
||||
this.className = ko.computed(() => {
|
||||
const size = ko.unwrap(this.size),
|
||||
suffixValue = this.trigger ? ' ' + ('settings-saved-trigger-input ' + this.classForTrigger()).trim() : '';
|
||||
return (0 < size ? 'span' + size : '') + suffixValue;
|
||||
});
|
||||
|
||||
if (undefined !== params.width && params.element) {
|
||||
params.element.querySelectorAll('input,select,textarea').forEach(node => node.style.width = params.width);
|
||||
}
|
||||
|
||||
this.disposable.push(this.className);
|
||||
this.labeled = null != params.label;
|
||||
|
||||
let size = 0 < params.size ? 'span' + params.size : '';
|
||||
if (this.trigger) {
|
||||
this.setTriggerState(this.trigger());
|
||||
const
|
||||
classForTrigger = ko.observable(''),
|
||||
setTriggerState = value => {
|
||||
switch (pInt(value)) {
|
||||
case SaveSettingsStep.TrueResult:
|
||||
classForTrigger('success');
|
||||
break;
|
||||
case SaveSettingsStep.FalseResult:
|
||||
classForTrigger('error');
|
||||
break;
|
||||
default:
|
||||
classForTrigger('');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.disposable.push(this.trigger.subscribe(this.setTriggerState, this));
|
||||
setTriggerState(this.trigger());
|
||||
|
||||
this.className = koComputable(() =>
|
||||
(size + ' settings-saved-trigger-input ' + classForTrigger()).trim()
|
||||
);
|
||||
|
||||
this.disposables = [
|
||||
this.trigger.subscribe(setTriggerState, this),
|
||||
this.className
|
||||
];
|
||||
} else {
|
||||
this.className = size;
|
||||
this.disposables = [];
|
||||
}
|
||||
}
|
||||
|
||||
setTriggerState(value) {
|
||||
switch (pInt(value)) {
|
||||
case SaveSettingsStep.TrueResult:
|
||||
this.classForTrigger('success');
|
||||
break;
|
||||
case SaveSettingsStep.FalseResult:
|
||||
this.classForTrigger('error');
|
||||
break;
|
||||
default:
|
||||
this.classForTrigger('');
|
||||
break;
|
||||
}
|
||||
dispose() {
|
||||
this.disposables.forEach(dispose);
|
||||
}
|
||||
}
|
||||
|
||||
export { AbstractInput };
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { doc, createElement } from 'Common/Globals';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
|
||||
const contentType = 'snappymail/emailaddress',
|
||||
|
@ -17,7 +16,7 @@ export class EmailAddressesComponent {
|
|||
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 Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
|
||||
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
|
||||
|
@ -42,49 +41,55 @@ export class EmailAddressesComponent {
|
|||
// Create the elements
|
||||
self.ul = createElement('ul',{class:"emailaddresses"});
|
||||
|
||||
self.ul.addEventListener('click', e => self._focus(e));
|
||||
self.ul.addEventListener('dblclick', e => self._editTag(e));
|
||||
self.ul.addEventListener("dragenter", fnDrag);
|
||||
self.ul.addEventListener("dragover", fnDrag);
|
||||
self.ul.addEventListener("drop", e => {
|
||||
if (validDropzone() && dragAddress.value) {
|
||||
e.preventDefault();
|
||||
dragAddress.source._removeDraggedTag(dragAddress.li);
|
||||
self._parseValue(dragAddress.value);
|
||||
addEventsListeners(self.ul, {
|
||||
click: e => self._focus(e),
|
||||
dblclick: e => self._editTag(e),
|
||||
dragenter: fnDrag,
|
||||
dragover: fnDrag,
|
||||
drop: e => {
|
||||
if (validDropzone() && dragAddress.value) {
|
||||
e.preventDefault();
|
||||
dragAddress.source._removeDraggedTag(dragAddress.li);
|
||||
self._parseValue(dragAddress.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.input = createElement('input',{type:"text", list:datalist.id,
|
||||
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
|
||||
|
||||
self.input.addEventListener('focus', () => self._focusTrigger(true));
|
||||
self.input.addEventListener('blur', () => {
|
||||
// prevent autoComplete menu click from causing a false 'blur'
|
||||
self._parseInput(true);
|
||||
self._focusTrigger(false);
|
||||
});
|
||||
self.input.addEventListener('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();
|
||||
addEventsListeners(self.input, {
|
||||
focus: () => {
|
||||
self._focusTrigger(true);
|
||||
self.input.value || self._resetDatalist();
|
||||
},
|
||||
blur: () => {
|
||||
// prevent autoComplete menu click from causing a false 'blur'
|
||||
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
|
||||
if (element.placeholder) {
|
||||
|
@ -116,7 +121,7 @@ export class EmailAddressesComponent {
|
|||
)
|
||||
}
|
||||
}).throttle(500)
|
||||
: () => {};
|
||||
: () => 0;
|
||||
}
|
||||
|
||||
_focusTrigger(bValue) {
|
||||
|
@ -138,20 +143,56 @@ export class EmailAddressesComponent {
|
|||
|
||||
_parseValue(val) {
|
||||
if (val) {
|
||||
var self = this,
|
||||
values = [];
|
||||
|
||||
const v = val.trim(),
|
||||
hook = (v && [',', ';', '\n'].includes(v.substr(-1)))
|
||||
? EmailModel.splitEmailLine(val)
|
||||
: null;
|
||||
|
||||
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
||||
.flat(Infinity)
|
||||
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
|
||||
const self = this,
|
||||
v = val.trim(),
|
||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
|
||||
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
||||
.flat(Infinity)
|
||||
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -202,56 +243,6 @@ export class EmailAddressesComponent {
|
|||
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() {
|
||||
return this._chosenValues.map(v => v.value).join(',');
|
||||
}
|
||||
|
@ -276,66 +267,70 @@ export class EmailAddressesComponent {
|
|||
|
||||
el = createElement('a',{href:'#', class:'ficon'});
|
||||
el.append('✖');
|
||||
el.addEventListener('click', e => self._removeTag(e, li));
|
||||
el.addEventListener('focus', () => li.className = 'emailaddresses-selected');
|
||||
el.addEventListener('blur', () => li.className = null);
|
||||
el.addEventListener('keydown', e => {
|
||||
switch (e.key) {
|
||||
case 'Delete':
|
||||
case 'Backspace':
|
||||
self._removeTag(e, li);
|
||||
break;
|
||||
addEventsListeners(el, {
|
||||
click: e => self._removeTag(e, li),
|
||||
focus: () => li.className = 'emailaddresses-selected',
|
||||
blur: () => li.className = null,
|
||||
keydown: e => {
|
||||
switch (e.key) {
|
||||
case 'Delete':
|
||||
case 'Backspace':
|
||||
self._removeTag(e, li);
|
||||
break;
|
||||
|
||||
// 'e' - edit tag (removes tag and places value into visible input
|
||||
case 'e':
|
||||
case 'Enter':
|
||||
self._editTag(e);
|
||||
break;
|
||||
// 'e' - edit tag (removes tag and places value into visible input
|
||||
case 'e':
|
||||
case 'Enter':
|
||||
self._editTag(e);
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
// select the previous tag or input if no more tags exist
|
||||
var previous = el.closest('li').previousElementSibling;
|
||||
if (previous.matches('li')) {
|
||||
previous.querySelector('a').focus();
|
||||
} else {
|
||||
self.focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
// select the previous tag or input if no more tags exist
|
||||
var previous = el.closest('li').previousElementSibling;
|
||||
if (previous.matches('li')) {
|
||||
previous.querySelector('a').focus();
|
||||
} else {
|
||||
self.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
// select the next tag or input if no more tags exist
|
||||
var next = el.closest('li').nextElementSibling;
|
||||
if (next !== this.inputCont) {
|
||||
next.querySelector('a').focus();
|
||||
} else {
|
||||
this.focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
// select the next tag or input if no more tags exist
|
||||
var next = el.closest('li').nextElementSibling;
|
||||
if (next !== this.inputCont) {
|
||||
next.querySelector('a').focus();
|
||||
} else {
|
||||
this.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
self._focus(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
self._focus(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
li.append(el);
|
||||
|
||||
li.emailaddress = v;
|
||||
|
||||
li.addEventListener("dragstart", e => {
|
||||
dragAddress = {
|
||||
source: self,
|
||||
li: li,
|
||||
value: li.emailaddress.obj.toLine()
|
||||
};
|
||||
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
|
||||
e.dataTransfer.setData('text/plain', contentType);
|
||||
// e.dataTransfer.setDragImage(li, 0, 0);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
li.style.opacity = 0.25;
|
||||
});
|
||||
li.addEventListener("dragend", () => {
|
||||
dragAddress = null;
|
||||
li.style.cssText = '';
|
||||
addEventsListeners(li, {
|
||||
dragstart: e => {
|
||||
dragAddress = {
|
||||
source: self,
|
||||
li: li,
|
||||
value: li.emailaddress.obj.toLine()
|
||||
};
|
||||
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
|
||||
e.dataTransfer.setData('text/plain', contentType);
|
||||
// e.dataTransfer.setDragImage(li, 0, 0);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
li.style.opacity = 0.25;
|
||||
},
|
||||
dragend: () => {
|
||||
dragAddress = null;
|
||||
li.style.cssText = '';
|
||||
}
|
||||
});
|
||||
|
||||
self.inputCont.before(li);
|
||||
|
|
|
@ -1,41 +1,3 @@
|
|||
import ko from 'ko';
|
||||
import { AbstractCheckbox } from 'Component/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.animationBoxSetTrue = this.animationBoxSetTrue.bind(this);
|
||||
this.animationCheckmarkSetTrue = this.animationCheckmarkSetTrue.bind(this);
|
||||
|
||||
this.disposable.push(
|
||||
this.value.subscribe((value) => {
|
||||
this.triggerAnimation(value);
|
||||
}, this)
|
||||
);
|
||||
}
|
||||
|
||||
animationBoxSetTrue() {
|
||||
this.animationBox(true);
|
||||
}
|
||||
|
||||
animationCheckmarkSetTrue() {
|
||||
this.animationCheckmark(true);
|
||||
}
|
||||
|
||||
triggerAnimation(box) {
|
||||
if (box) {
|
||||
this.animationBoxSetTrue();
|
||||
setTimeout(this.animationCheckmarkSetTrue, 200);
|
||||
} else {
|
||||
this.animationCheckmarkSetTrue();
|
||||
setTimeout(this.animationBoxSetTrue, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
export class CheckboxMaterialDesignComponent extends AbstractCheckbox {}
|
||||
|
|
|
@ -13,11 +13,7 @@ export class SelectComponent extends AbstractInput {
|
|||
|
||||
this.optionsText = params.optionsText || null;
|
||||
this.optionsValue = params.optionsValue || null;
|
||||
this.optionsCaption = params.optionsCaption || null;
|
||||
|
||||
if (this.optionsCaption) {
|
||||
this.optionsCaption = i18n(this.optionsCaption);
|
||||
}
|
||||
this.optionsCaption = i18n(params.optionsCaption || null);
|
||||
|
||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||
}
|
||||
|
|
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;
|
||||
};
|
171
dev/External/SquireUI.js
vendored
171
dev/External/SquireUI.js
vendored
|
@ -9,13 +9,16 @@ const
|
|||
|
||||
i18n = (str, def) => rl.i18n(str) || def,
|
||||
|
||||
ctrlKey = shortcuts.getMetaKey().replace('meta','⌘') + ' + ',
|
||||
ctrlKey = shortcuts.getMetaKey() + ' + ',
|
||||
|
||||
tpl = doc.createElement('template'),
|
||||
clr = doc.createElement('input'),
|
||||
createElement = name => doc.createElement(name),
|
||||
|
||||
tpl = createElement('template'),
|
||||
clr = createElement('input'),
|
||||
|
||||
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 => {
|
||||
let frag = doc.createDocumentFragment();
|
||||
|
@ -33,13 +36,12 @@ const
|
|||
addLinks: true // allow_smart_html_links
|
||||
*/
|
||||
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
||||
tpl.innerHTML = html
|
||||
tpl.innerHTML = (html||'')
|
||||
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
|
||||
.replace(/<!--[^>]+-->/g,'')
|
||||
.replace(/<span[^>]*>\s*<\/span>/gi,'')
|
||||
.trim();
|
||||
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
||||
tpl.querySelectorAll('[data-x-div-type]').forEach(el => el.replaceWith(getFragmentOfChildren(el)));
|
||||
if (isPaste) {
|
||||
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
|
||||
tpl.querySelectorAll('*').forEach(el => {
|
||||
|
@ -57,66 +59,6 @@ const
|
|||
}
|
||||
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";
|
||||
|
@ -127,9 +69,9 @@ class SquireUI
|
|||
{
|
||||
constructor(container) {
|
||||
const
|
||||
doClr = fn => () => {
|
||||
doClr = name => () => {
|
||||
clr.value = '';
|
||||
clr.onchange = () => squire[fn](clr.value);
|
||||
clr.onchange = () => squire.setStyle({[name]:clr.value});
|
||||
clr.click();
|
||||
},
|
||||
|
||||
|
@ -171,29 +113,15 @@ class SquireUI
|
|||
colors: {
|
||||
textColor: {
|
||||
html: 'A<sub>▾</sub>',
|
||||
cmd: doClr('setTextColor'),
|
||||
cmd: doClr('color'),
|
||||
hint: 'Text color'
|
||||
},
|
||||
backgroundColor: {
|
||||
html: '🎨', /* ▧ */
|
||||
cmd: doClr('setBackgroundColor'),
|
||||
cmd: doClr('backgroundColor'),
|
||||
hint: 'Background color'
|
||||
},
|
||||
},
|
||||
/*
|
||||
bidi: {
|
||||
bdoLtr: {
|
||||
html: '‎𝐁',
|
||||
cmd: () => this.doAction('bold','B'),
|
||||
hint: 'Bold'
|
||||
},
|
||||
bdoRtl: {
|
||||
html: '‏𝐁',
|
||||
cmd: () => this.doAction('bold','B'),
|
||||
hint: 'Bold'
|
||||
}
|
||||
},
|
||||
*/
|
||||
inline: {
|
||||
bold: {
|
||||
html: 'B',
|
||||
|
@ -318,10 +246,10 @@ class SquireUI
|
|||
}
|
||||
},
|
||||
|
||||
plain = doc.createElement('textarea'),
|
||||
wysiwyg = doc.createElement('div'),
|
||||
toolbar = doc.createElement('div'),
|
||||
browseImage = doc.createElement('input'),
|
||||
plain = createElement('textarea'),
|
||||
wysiwyg = createElement('div'),
|
||||
toolbar = createElement('div'),
|
||||
browseImage = createElement('input'),
|
||||
squire = new Squire(wysiwyg, SquireDefaultConfig);
|
||||
|
||||
browseImage.type = 'file';
|
||||
|
@ -336,7 +264,7 @@ class SquireUI
|
|||
}
|
||||
|
||||
plain.className = 'squire-plain';
|
||||
wysiwyg.className = 'squire-wysiwyg cke_editable';
|
||||
wysiwyg.className = 'squire-wysiwyg';
|
||||
this.mode = ''; // 'plain' | 'wysiwyg'
|
||||
this.__plain = {
|
||||
getRawData: () => this.plain.value,
|
||||
|
@ -349,25 +277,24 @@ class SquireUI
|
|||
this.wysiwyg = wysiwyg;
|
||||
|
||||
toolbar.className = 'squire-toolbar btn-toolbar';
|
||||
let touchTap;
|
||||
for (let group in actions) {
|
||||
let group, action, touchTap;
|
||||
for (group in actions) {
|
||||
/*
|
||||
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
|
||||
continue;
|
||||
}
|
||||
let toolgroup = doc.createElement('div');
|
||||
*/
|
||||
let toolgroup = createElement('div');
|
||||
toolgroup.className = 'btn-group';
|
||||
toolgroup.id = 'squire-toolgroup-'+group;
|
||||
for (let action in actions[group]) {
|
||||
if ('source' == action && !rl.settings.app('allowHtmlEditorSourceButton')) {
|
||||
continue;
|
||||
}
|
||||
for (action in actions[group]) {
|
||||
let cfg = actions[group][action], input, ev = 'click';
|
||||
if (cfg.input) {
|
||||
input = doc.createElement('input');
|
||||
input = createElement('input');
|
||||
input.type = cfg.input;
|
||||
ev = 'change';
|
||||
} else if (cfg.select) {
|
||||
input = doc.createElement('select');
|
||||
input = createElement('select');
|
||||
input.className = 'btn';
|
||||
if (Array.isArray(cfg.select)) {
|
||||
cfg.select.forEach(value => {
|
||||
|
@ -377,7 +304,7 @@ class SquireUI
|
|||
});
|
||||
} else {
|
||||
Object.entries(cfg.select).forEach(([label, options]) => {
|
||||
let group = doc.createElement('optgroup');
|
||||
let group = createElement('optgroup');
|
||||
group.label = label;
|
||||
Object.entries(options).forEach(([text, value]) => {
|
||||
var option = new Option(text, value);
|
||||
|
@ -389,7 +316,7 @@ class SquireUI
|
|||
}
|
||||
ev = 'input';
|
||||
} else {
|
||||
input = doc.createElement('button');
|
||||
input = createElement('button');
|
||||
input.type = 'button';
|
||||
input.className = 'btn';
|
||||
input.innerHTML = cfg.html;
|
||||
|
@ -412,6 +339,7 @@ class SquireUI
|
|||
input.title = ctrlKey + cfg.key;
|
||||
}
|
||||
input.dataset.action = action;
|
||||
input.tabIndex = -1;
|
||||
cfg.input = input;
|
||||
toolgroup.append(input);
|
||||
}
|
||||
|
@ -425,8 +353,8 @@ class SquireUI
|
|||
changes.redo.input.disabled = !state.canRedo;
|
||||
});
|
||||
|
||||
squire.addEventListener('focus', () => shortcuts.off());
|
||||
squire.addEventListener('blur', () => shortcuts.on());
|
||||
// squire.addEventListener('focus', () => shortcuts.off());
|
||||
// squire.addEventListener('blur', () => shortcuts.on());
|
||||
|
||||
container.append(toolbar, wysiwyg, plain);
|
||||
|
||||
|
@ -477,9 +405,9 @@ class SquireUI
|
|||
let cl = this.container.classList;
|
||||
cl.remove('squire-mode-'+this.mode);
|
||||
if ('plain' == mode) {
|
||||
this.plain.value = rl.Utils.htmlToPlain(this.squire.getHTML(), true).trim();
|
||||
this.plain.value = htmlToPlain(this.squire.getHTML(), true);
|
||||
} else {
|
||||
this.setData(rl.Utils.plainToHtml(this.plain.value, true));
|
||||
this.setData(plainToHtml(this.plain.value, true));
|
||||
mode = 'wysiwyg';
|
||||
}
|
||||
this.mode = mode; // 'wysiwyg' or 'plain'
|
||||
|
@ -509,19 +437,32 @@ class SquireUI
|
|||
}, cfg);
|
||||
|
||||
if (cfg.clearCache) {
|
||||
this.__previous_signature = null;
|
||||
this._prev_txt_sig = null;
|
||||
} else try {
|
||||
const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature;
|
||||
if ('plain' === this.mode) {
|
||||
if (cfg.isHtml) {
|
||||
cfg.signature = rl.Utils.htmlToPlain(cfg.signature);
|
||||
let
|
||||
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 {
|
||||
if (!cfg.isHtml) {
|
||||
cfg.signature = rl.Utils.plainToHtml(cfg.signature);
|
||||
}
|
||||
this.setData(rl_signature_replacer(this, this.getData(), cfg.signature, true, cfg.insertBefore));
|
||||
const squire = this.squire,
|
||||
root = squire.getRoot(),
|
||||
range = squire.getSelection(),
|
||||
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) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@ -540,12 +481,6 @@ class SquireUI
|
|||
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;
|
||||
|
|
391
dev/External/User/ko.js
vendored
391
dev/External/User/ko.js
vendored
|
@ -1,11 +1,14 @@
|
|||
import 'External/ko';
|
||||
import ko from 'ko';
|
||||
import { HtmlEditor } from 'Common/Html';
|
||||
import { timeToNode } from 'Common/Momentor';
|
||||
import { elementById } from 'Common/Globals';
|
||||
import { timeToNode } from 'Common/Translator';
|
||||
import { elementById, addEventsListeners, dropdowns } from 'Common/Globals';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
||||
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { moveMessagesToFolder } from 'Common/Folders';
|
||||
import { setExpandedFolder } from 'Model/FolderCollection';
|
||||
|
||||
const rlContentType = 'snappymail/action',
|
||||
|
||||
|
@ -27,233 +30,223 @@ const rlContentType = 'snappymail/action',
|
|||
id: 0,
|
||||
stop: () => clearTimeout(dragTimer.id),
|
||||
start: fn => dragTimer.id = setTimeout(fn, 500)
|
||||
};
|
||||
},
|
||||
|
||||
ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
|
||||
|
||||
let dragImage,
|
||||
dragData;
|
||||
|
||||
ko.bindingHandlers.editor = {
|
||||
init: (element, fValueAccessor) => {
|
||||
let editor = null;
|
||||
Object.assign(ko.bindingHandlers, {
|
||||
|
||||
const fValue = fValueAccessor(),
|
||||
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
|
||||
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
||||
fOnReady = () => {
|
||||
fValue.__editor = editor;
|
||||
fUpdateEditorValue();
|
||||
};
|
||||
editor: {
|
||||
init: (element, fValueAccessor) => {
|
||||
let editor = null;
|
||||
|
||||
if (ko.isObservable(fValue) && HtmlEditor) {
|
||||
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
|
||||
const fValue = fValueAccessor(),
|
||||
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.bindingHandlers.moment = {
|
||||
init: ttn,
|
||||
update: ttn
|
||||
};
|
||||
|
||||
ko.bindingHandlers.emailsTags = {
|
||||
init: (element, fValueAccessor, fAllBindingsAccessor) => {
|
||||
const fValue = fValueAccessor(),
|
||||
fAllBindings = fAllBindingsAccessor();
|
||||
|
||||
element.addresses = new EmailAddressesComponent(element, {
|
||||
focusCallback: value => fValue.focused && fValue.focused(!!value),
|
||||
autoCompleteSource: fAllBindings.autoCompleteSource || null,
|
||||
onChange: value => fValue(value)
|
||||
});
|
||||
|
||||
if (fValue.focused && fValue.focused.subscribe) {
|
||||
fValue.focused.subscribe(value =>
|
||||
element.addresses[value ? 'focus' : 'blur']()
|
||||
);
|
||||
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
|
||||
// });
|
||||
}
|
||||
}
|
||||
},
|
||||
update: (element, fValueAccessor) => {
|
||||
element.addresses.value = ko.unwrap(fValueAccessor());
|
||||
}
|
||||
};
|
||||
|
||||
// Start dragging selected messages
|
||||
ko.bindingHandlers.dragmessages = {
|
||||
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);
|
||||
moment: {
|
||||
init: ttn,
|
||||
update: ttn
|
||||
},
|
||||
|
||||
// Else Chrome doesn't show it
|
||||
dragImage.style.left = e.clientX + 'px';
|
||||
dragImage.style.top = e.clientY + 'px';
|
||||
dragImage.style.right = 'auto';
|
||||
emailsTags: {
|
||||
init: (element, fValueAccessor, fAllBindings) => {
|
||||
const fValue = fValueAccessor();
|
||||
|
||||
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
|
||||
dragImage.style.cssText = '';
|
||||
} else {
|
||||
e.preventDefault();
|
||||
if (fValue.focused && fValue.focused.subscribe) {
|
||||
fValue.focused.subscribe(value =>
|
||||
element.addresses[value ? 'focus' : 'blur']()
|
||||
);
|
||||
}
|
||||
},
|
||||
update: (element, fValueAccessor) => {
|
||||
element.addresses.value = ko.unwrap(fValueAccessor());
|
||||
}
|
||||
},
|
||||
|
||||
}, false);
|
||||
element.addEventListener("dragend", () => dragData = null);
|
||||
element.setAttribute('draggable', true);
|
||||
}
|
||||
};
|
||||
// Start dragging selected messages
|
||||
dragmessages: {
|
||||
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
|
||||
ko.bindingHandlers.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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
// Else Chrome doesn't show it
|
||||
dragImage.style.left = e.clientX + 'px';
|
||||
dragImage.style.top = e.clientY + 'px';
|
||||
dragImage.style.right = 'auto';
|
||||
|
||||
ko.bindingHandlers.sortableItem = {
|
||||
init: (element, fValueAccessor) => {
|
||||
let options = ko.unwrap(fValueAccessor()) || {},
|
||||
parent = element.parentNode,
|
||||
fnHover = e => {
|
||||
if ('sortable' === getDragAction(e)) {
|
||||
setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
|
||||
|
||||
// Remove the Chrome visibility
|
||||
dragImage.style.cssText = '';
|
||||
} else {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
}, false);
|
||||
element.addEventListener("dragend", () => dragData = null);
|
||||
element.setAttribute('draggable', true);
|
||||
}
|
||||
},
|
||||
|
||||
// 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 = {
|
||||
init: (element, fValueAccessor) => fValueAccessor()(element)
|
||||
};
|
||||
|
||||
ko.bindingHandlers.onEsc = {
|
||||
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
|
||||
let fn = event => {
|
||||
if ('Escape' == event.key) {
|
||||
element.dispatchEvent(new Event('change'));
|
||||
fValueAccessor().call(viewModel);
|
||||
sortableItem: {
|
||||
init: (element, fValueAccessor) => {
|
||||
let options = ko.unwrap(fValueAccessor()) || {},
|
||||
parent = element.parentNode,
|
||||
fnHover = e => {
|
||||
if ('sortable' === getDragAction(e)) {
|
||||
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();
|
||||
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 = {
|
||||
init: element => {
|
||||
rl.Dropdowns.register(element);
|
||||
element.ddBtn = new BSN.Dropdown(element.querySelector('[data-toggle="dropdown"]'));
|
||||
}
|
||||
};
|
||||
initDom: {
|
||||
init: (element, fValueAccessor) => fValueAccessor()(element)
|
||||
},
|
||||
|
||||
ko.bindingHandlers.openDropdownTrigger = {
|
||||
update: (element, fValueAccessor) => {
|
||||
if (ko.unwrap(fValueAccessor())) {
|
||||
const el = element.ddBtn;
|
||||
el.open || el.toggle();
|
||||
// el.focus();
|
||||
registerBootstrapDropdown: {
|
||||
init: element => {
|
||||
dropdowns.push(element);
|
||||
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
|
||||
}
|
||||
},
|
||||
|
||||
rl.Dropdowns.detectVisibility();
|
||||
fValueAccessor()(false);
|
||||
openDropdownTrigger: {
|
||||
update: (element, fValueAccessor) => {
|
||||
if (ko.unwrap(fValueAccessor())) {
|
||||
const el = element.ddBtn;
|
||||
el.open || el.toggle();
|
||||
// el.focus();
|
||||
|
||||
dropdownsDetectVisibility();
|
||||
fValueAccessor()(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ko.bindingHandlers.dropdownCloser = {
|
||||
init: element => element.closest('.dropdown').addEventListener('click', event =>
|
||||
event.target.closestWithin('.e-item', element) && element.ddBtn.toggle()
|
||||
)
|
||||
};
|
||||
});
|
||||
|
|
23
dev/External/ifvisible.js
vendored
23
dev/External/ifvisible.js
vendored
|
@ -1,37 +1,36 @@
|
|||
(doc => {
|
||||
let visible = "visible",
|
||||
let idle = 'idle',
|
||||
visible = 'visible',
|
||||
status = visible,
|
||||
timer = 0,
|
||||
wakeUp = () => {
|
||||
clearTimeout(timer);
|
||||
if (status !== visible) {
|
||||
status = visible;
|
||||
}
|
||||
status = visible;
|
||||
timer = setTimeout(() => {
|
||||
if (status === visible) {
|
||||
status = "idle";
|
||||
dispatchEvent(new CustomEvent("idle"));
|
||||
status = idle;
|
||||
dispatchEvent(new CustomEvent(idle));
|
||||
}
|
||||
}, 10000);
|
||||
},
|
||||
init = () => {
|
||||
init = ()=>{};
|
||||
init = () => 0;
|
||||
// Safari
|
||||
addEventListener('pagehide', () => status = "hidden");
|
||||
addEventListener('pagehide', () => status = 'hidden');
|
||||
// Else
|
||||
doc.addEventListener("visibilitychange", () => {
|
||||
doc.addEventListener('visibilitychange', () => {
|
||||
status = doc.visibilityState;
|
||||
doc.hidden || wakeUp();
|
||||
});
|
||||
wakeUp();
|
||||
["mousemove","keyup","touchstart"].forEach(t => doc.addEventListener(t, wakeUp));
|
||||
["scroll","pageshow"].forEach(t => addEventListener(t, wakeUp));
|
||||
['mousemove','keyup','touchstart'].forEach(t => doc.addEventListener(t, wakeUp));
|
||||
['scroll','pageshow'].forEach(t => addEventListener(t, wakeUp));
|
||||
};
|
||||
|
||||
this.ifvisible = {
|
||||
idle: callback => {
|
||||
init();
|
||||
addEventListener("idle", callback);
|
||||
addEventListener(idle, callback);
|
||||
},
|
||||
now: () => {
|
||||
init();
|
||||
|
|
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 { doc, createElement } from 'Common/Globals';
|
||||
import { SaveSettingsStep } from 'Common/Enums';
|
||||
import { isNonEmptyArray, isFunction } from 'Common/Utils';
|
||||
import { arrayLength, isFunction, forEachObjectEntry } from 'Common/Utils';
|
||||
|
||||
const
|
||||
koValue = value => !ko.isObservable(value) && isFunction(value) ? value() : ko.unwrap(value);
|
||||
export const
|
||||
errorTip = (element, value) => value
|
||||
? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100)
|
||||
: element.removeAttribute('data-rainloopErrorTip'),
|
||||
|
||||
ko.bindingHandlers.tooltipErrorTip = {
|
||||
init: element => {
|
||||
doc.addEventListener('click', () => element.removeAttribute('data-rainloopErrorTip'));
|
||||
},
|
||||
update: (element, fValueAccessor) => {
|
||||
const value = koValue(fValueAccessor());
|
||||
if (value) {
|
||||
setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100);
|
||||
} else {
|
||||
element.removeAttribute('data-rainloopErrorTip');
|
||||
/**
|
||||
* The value of the pureComputed observable shouldn’t vary based on the
|
||||
* number of evaluations or other “hidden” information. Its value should be
|
||||
* based solely on the values of other observables in the application
|
||||
*/
|
||||
koComputable = fn => ko.computed(fn, {'pure':true}),
|
||||
|
||||
addObservablesTo = (target, observables) =>
|
||||
forEachObjectEntry(observables, (key, value) =>
|
||||
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 = {
|
||||
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
|
||||
let fn = event => {
|
||||
if ('Enter' == event.key) {
|
||||
element.dispatchEvent(new Event('change'));
|
||||
fValueAccessor().call(viewModel);
|
||||
onEnter: {
|
||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
||||
let fn = event => {
|
||||
if ('Enter' == event.key) {
|
||||
element.dispatchEvent(new Event('change'));
|
||||
fValueAccessor().call(viewModel);
|
||||
}
|
||||
};
|
||||
element.addEventListener('keydown', fn);
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
||||
}
|
||||
},
|
||||
|
||||
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 = {
|
||||
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
|
||||
let fn = event => {
|
||||
if (' ' == event.key) {
|
||||
fValueAccessor().call(viewModel, event);
|
||||
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.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, fAllBindingsAccessor, 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,
|
||||
fAllBindingsAccessor,
|
||||
viewModel,
|
||||
bindingContext
|
||||
);
|
||||
},
|
||||
update: (element, fValueAccessor) => {
|
||||
const cl = element.classList,
|
||||
command = fValueAccessor();
|
||||
|
||||
let disabled = !command.enabled();
|
||||
|
||||
disabled = disabled || !command.canExecute();
|
||||
cl.toggle('disabled', disabled);
|
||||
|
||||
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
|
||||
element.disabled = disabled;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ko.bindingHandlers.saveTrigger = {
|
||||
init: (element) => {
|
||||
let icon = element;
|
||||
if (element.matches('input,select,textarea')) {
|
||||
element.classList.add('settings-saved-trigger-input');
|
||||
element.after(element.saveTriggerIcon = icon = createElement('span'));
|
||||
}
|
||||
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);
|
||||
saveTrigger: {
|
||||
init: (element) => {
|
||||
let icon = element;
|
||||
if (element.matches('input,select,textarea')) {
|
||||
element.classList.add('settings-saved-trigger-input');
|
||||
element.after(element.saveTriggerIcon = icon = createElement('span'));
|
||||
}
|
||||
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('error', value === SaveSettingsStep.FalseResult);
|
||||
}
|
||||
cl = element.classList;
|
||||
cl.toggle('success', value === SaveSettingsStep.TrueResult);
|
||||
cl.toggle('error', value === SaveSettingsStep.FalseResult);
|
||||
}
|
||||
cl = element.classList;
|
||||
cl.toggle('success', value === SaveSettingsStep.TrueResult);
|
||||
cl.toggle('error', value === SaveSettingsStep.FalseResult);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// extenders
|
||||
|
||||
ko.extenders.limitedList = (target, limitedList) => {
|
||||
const result = ko
|
||||
.computed({
|
||||
read: target,
|
||||
write: (newValue) => {
|
||||
const currentValue = ko.unwrap(target),
|
||||
list = ko.unwrap(limitedList);
|
||||
|
||||
if (isNonEmptyArray(list)) {
|
||||
if (list.includes(newValue)) {
|
||||
target(newValue);
|
||||
} else if (list.includes(currentValue, list)) {
|
||||
target(currentValue + ' ');
|
||||
target(currentValue);
|
||||
} else {
|
||||
target(list[0] + ' ');
|
||||
target(list[0]);
|
||||
}
|
||||
} else {
|
||||
target('');
|
||||
}
|
||||
.computed({
|
||||
read: target,
|
||||
write: newValue => {
|
||||
let currentValue = target(),
|
||||
list = ko.unwrap(limitedList);
|
||||
list = arrayLength(list) ? list : [''];
|
||||
if (!list.includes(newValue)) {
|
||||
newValue = list.includes(currentValue, list) ? currentValue : list[0];
|
||||
target(newValue + ' ');
|
||||
}
|
||||
})
|
||||
.extend({ notify: 'always' });
|
||||
target(newValue);
|
||||
}
|
||||
})
|
||||
.extend({ notify: 'always' });
|
||||
|
||||
result(target());
|
||||
|
||||
|
@ -184,6 +198,6 @@ ko.extenders.falseTimeout = (target, option) => {
|
|||
|
||||
// functions
|
||||
|
||||
ko.observable.fn.deleteAccessHelper = function() {
|
||||
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'deleteAccess'] });
|
||||
ko.observable.fn.askDeleteHelper = function() {
|
||||
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
|
||||
};
|
||||
|
|
|
@ -1,31 +1,24 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="user-scalable=no"/>
|
||||
<title>{{subject}}</title>
|
||||
<title></title>
|
||||
<style>
|
||||
html, body {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #eee;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 0 15px;
|
||||
background: rgba(125,128,128,0.3);
|
||||
border-bottom: 1px solid #888;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
header * {
|
||||
font-size: 12px;
|
||||
padding: 5px 0;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
header time {
|
||||
|
@ -33,30 +26,21 @@ header time {
|
|||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid black;
|
||||
border-left: 2px solid rgba(125,128,128,0.5);
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
|
||||
pre, .body-wrp.plain {
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.body-wrp {
|
||||
padding: 15px;
|
||||
body > * {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<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>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
import { isArray, isFunction, addObservablesTo, addComputablesTo } from 'Common/Utils';
|
||||
|
||||
function dispose(disposable) {
|
||||
if (disposable && isFunction(disposable.dispose)) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
import { isArray, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
||||
import { dispose, addObservablesTo, addComputablesTo } from 'External/ko';
|
||||
|
||||
function typeCast(curValue, newValue) {
|
||||
switch (typeof curValue)
|
||||
{
|
||||
case 'boolean': return 0 != newValue && !!newValue;
|
||||
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
|
||||
case 'string': return null != newValue ? '' + newValue : '';
|
||||
case 'object':
|
||||
if (curValue.constructor.reviveFromJson) {
|
||||
return curValue.constructor.reviveFromJson(newValue);
|
||||
if (null != curValue) {
|
||||
switch (typeof curValue)
|
||||
{
|
||||
case 'boolean': return 0 != newValue && !!newValue;
|
||||
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
|
||||
case 'string': return null != newValue ? '' + newValue : '';
|
||||
case 'object':
|
||||
if (curValue.constructor.reviveFromJson) {
|
||||
return curValue.constructor.reviveFromJson(newValue);
|
||||
}
|
||||
if (isArray(curValue) && !isArray(newValue))
|
||||
return [];
|
||||
}
|
||||
if (isArray(curValue) && !isArray(newValue))
|
||||
return [];
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
|
@ -29,7 +26,7 @@ export class AbstractModel {
|
|||
throw new Error("Can't instantiate AbstractModel!");
|
||||
}
|
||||
*/
|
||||
this.subscribables = [];
|
||||
this.disposables = [];
|
||||
}
|
||||
|
||||
addObservables(observables) {
|
||||
|
@ -41,25 +38,26 @@ export class AbstractModel {
|
|||
}
|
||||
|
||||
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 */
|
||||
onDestroy() {
|
||||
/** dispose ko subscribables */
|
||||
this.subscribables.forEach(dispose);
|
||||
this.disposables.forEach(dispose);
|
||||
/** clear object entries */
|
||||
// Object.entries(this).forEach(([key, value]) => {
|
||||
Object.values(this).forEach(value => {
|
||||
// forEachObjectEntry(this, (key, value) => {
|
||||
forEachObjectValue(this, value => {
|
||||
/** clear CollectionModel */
|
||||
let arr = ko.isObservableArray(value) ? value() : value;
|
||||
arr && arr.onDestroy && value.onDestroy();
|
||||
arr && arr.onDestroy && arr.onDestroy();
|
||||
/** destroy ko.observable/ko.computed? */
|
||||
dispose(value);
|
||||
// dispose(value);
|
||||
/** clear object value */
|
||||
// this[key] = null; // TODO: issue with Contacts view
|
||||
});
|
||||
// this.subscribables = [];
|
||||
// this.disposables = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,39 +82,37 @@ export class AbstractModel {
|
|||
|
||||
revivePropertiesFromJson(json) {
|
||||
let model = this.constructor;
|
||||
try {
|
||||
if (!model.validJson(json)) {
|
||||
return false;
|
||||
}
|
||||
Object.entries(json).forEach(([key, value]) => {
|
||||
if ('@' !== key[0]) {
|
||||
key = key[0].toLowerCase() + key.substr(1);
|
||||
switch (typeof this[key])
|
||||
{
|
||||
case 'function':
|
||||
if (ko.isObservable(this[key])) {
|
||||
this[key](typeCast(this[key](), value));
|
||||
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
|
||||
}
|
||||
// else console.log(model.name + '.' + key + ' is a function');
|
||||
break;
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'object':
|
||||
case 'string':
|
||||
this[key] = typeCast(this[key], value);
|
||||
break;
|
||||
// fall through
|
||||
case 'undefined':
|
||||
default:
|
||||
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(model.name);
|
||||
console.error(e);
|
||||
if (!model.validJson(json)) {
|
||||
return false;
|
||||
}
|
||||
forEachObjectEntry(json, (key, value) => {
|
||||
if ('@' !== key[0]) try {
|
||||
key = key[0].toLowerCase() + key.slice(1);
|
||||
switch (typeof this[key])
|
||||
{
|
||||
case 'function':
|
||||
if (ko.isObservable(this[key])) {
|
||||
this[key](typeCast(this[key](), value));
|
||||
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
|
||||
}
|
||||
// else console.log(model.name + '.' + key + ' is a function');
|
||||
break;
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'object':
|
||||
case 'string':
|
||||
this[key] = typeCast(this[key], value);
|
||||
break;
|
||||
// fall through
|
||||
case 'undefined':
|
||||
default:
|
||||
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(model.name + '.' + key);
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,10 @@
|
|||
import { isArray, isNonEmptyArray } from 'Common/Utils';
|
||||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
|
||||
export class AbstractScreen {
|
||||
constructor(screenName, viewModels = []) {
|
||||
this.oCross = null;
|
||||
this.sScreenName = screenName;
|
||||
this.aViewModels = isArray(viewModels) ? viewModels : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array}
|
||||
*/
|
||||
get viewModels() {
|
||||
return this.aViewModels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
screenName() {
|
||||
return this.sScreenName;
|
||||
this.__cross = null;
|
||||
this.screenName = screenName;
|
||||
this.viewModels = isArray(viewModels) ? viewModels : [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,28 +14,26 @@ export class AbstractScreen {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {?Object}
|
||||
*/
|
||||
get __cross() {
|
||||
return this.oCross;
|
||||
}
|
||||
/*
|
||||
onBuild(viewModelDom) {}
|
||||
onShow() {}
|
||||
onHide() {}
|
||||
__started
|
||||
__builded
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
onStart() {
|
||||
if (!this.__started) {
|
||||
this.__started = true;
|
||||
const routes = this.routes();
|
||||
if (isNonEmptyArray(routes)) {
|
||||
let route = new Crossroads(),
|
||||
fMatcher = (this.onRoute || (()=>{})).bind(this);
|
||||
const routes = this.routes();
|
||||
if (arrayLength(routes)) {
|
||||
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,45 +1,38 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { inFocus, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils';
|
||||
import { Scope } from 'Common/Enums';
|
||||
import { keyScope } from 'Common/Globals';
|
||||
import { ViewType } from 'Knoin/Knoin';
|
||||
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||
import { keyScope, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
||||
|
||||
import { SaveSettingsStep } from 'Common/Enums';
|
||||
|
||||
class AbstractView {
|
||||
constructor(name, templateID, type)
|
||||
constructor(templateID, type)
|
||||
{
|
||||
this.viewModelName = 'View/' + name;
|
||||
this.viewModelTemplateID = templateID;
|
||||
this.viewModelPosition = 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 = '';
|
||||
// Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
|
||||
this.viewModelTemplateID = templateID || this.constructor.name.replace('UserView', '');
|
||||
this.viewType = type;
|
||||
this.viewModelDom = null;
|
||||
|
||||
this.keyScope = {
|
||||
scope: 'none',
|
||||
previous: 'none',
|
||||
set: function() {
|
||||
this.previous = keyScope();
|
||||
keyScope(this.scope);
|
||||
},
|
||||
unset: function() {
|
||||
keyScope(this.previous);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
storeAndSetScope() {
|
||||
this.sCurrentScope = keyScope();
|
||||
keyScope(this.sDefaultScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
restoreScope() {
|
||||
keyScope(this.sCurrentScope);
|
||||
}
|
||||
|
||||
cancelCommand() {}
|
||||
closeCommand() {}
|
||||
/*
|
||||
onBuild() {}
|
||||
beforeShow() {} // Happens before: hidden = false
|
||||
onShow() {} // Happens after: hidden = false
|
||||
onHide() {}
|
||||
*/
|
||||
|
||||
querySelector(selectors) {
|
||||
return this.viewModelDom.querySelector(selectors);
|
||||
|
@ -63,51 +56,115 @@ export class AbstractViewPopup extends AbstractView
|
|||
{
|
||||
constructor(name)
|
||||
{
|
||||
super('Popup/' + name, 'Popups' + name, ViewType.Popup);
|
||||
if (name in Scope) {
|
||||
this.sDefaultScope = Scope[name];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
registerPopupKeyDown() {
|
||||
addEventListener('keydown', event => {
|
||||
if (event && this.modalVisibility && this.modalVisibility()) {
|
||||
if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) {
|
||||
this.cancelCommand && this.cancelCommand();
|
||||
return false;
|
||||
} else if ('Backspace' == event.key && !inFocus()) {
|
||||
return false;
|
||||
}
|
||||
super('Popups' + name, ViewTypePopup);
|
||||
this.keyScope.scope = name;
|
||||
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
||||
shortcuts.add('escape,close', '', name, () => {
|
||||
if (this.modalVisible() && false !== this.onClose()) {
|
||||
this.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
constructor(name, templateID)
|
||||
{
|
||||
super(name, templateID, ViewType.Center);
|
||||
}
|
||||
AbstractViewPopup.showModal = function(params = []) {
|
||||
showScreenPopup(this, params);
|
||||
}
|
||||
|
||||
AbstractViewPopup.hidden = function() {
|
||||
return !this.__vm || !this.__vm.modalVisible();
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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,370 +1,310 @@
|
|||
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';
|
||||
import { runHook } from 'Common/Plugins';
|
||||
import { isNonEmptyArray, isFunction } from 'Common/Utils';
|
||||
|
||||
let currentScreen = null,
|
||||
let
|
||||
SCREENS = {},
|
||||
currentScreen = null,
|
||||
defaultScreenName = '';
|
||||
|
||||
const SCREENS = {},
|
||||
const
|
||||
autofocus = dom => {
|
||||
const af = dom.querySelector('[autofocus]');
|
||||
af && af.focus();
|
||||
};
|
||||
},
|
||||
|
||||
export const popupVisibilityNames = ko.observableArray([]);
|
||||
visiblePopups = new Set,
|
||||
|
||||
export const ViewType = {
|
||||
Popup: 'Popups',
|
||||
Left: 'Left',
|
||||
Right: 'Right',
|
||||
Center: 'Center'
|
||||
};
|
||||
/**
|
||||
* @param {string} screenName
|
||||
* @returns {?Object}
|
||||
*/
|
||||
screen = screenName => screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null,
|
||||
|
||||
/**
|
||||
* @param {Function} fExecute
|
||||
* @param {(Function|boolean|null)=} fCanExecute = true
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function createCommand(fExecute, fCanExecute = true) {
|
||||
let fResult = null;
|
||||
/**
|
||||
* @param {Function} ViewModelClass
|
||||
* @param {Object=} vmScreen
|
||||
* @returns {*}
|
||||
*/
|
||||
buildViewModel = (ViewModelClass, vmScreen) => {
|
||||
if (ViewModelClass && !ViewModelClass.__builded) {
|
||||
let vmDom = null;
|
||||
const
|
||||
vm = new ViewModelClass(vmScreen),
|
||||
id = vm.viewModelTemplateID,
|
||||
position = vm.viewType || '',
|
||||
dialog = ViewTypePopup === position,
|
||||
vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
|
||||
|
||||
fResult = fExecute
|
||||
? (...args) => {
|
||||
if (fResult && fResult.canExecute && fResult.canExecute()) {
|
||||
fExecute.apply(null, args);
|
||||
}
|
||||
return false;
|
||||
} : ()=>{};
|
||||
fResult.enabled = ko.observable(true);
|
||||
fResult.isCommand = true;
|
||||
ViewModelClass.__builded = true;
|
||||
ViewModelClass.__vm = vm;
|
||||
|
||||
if (isFunction(fCanExecute)) {
|
||||
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && fCanExecute.call(null));
|
||||
} else {
|
||||
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && !!fCanExecute);
|
||||
}
|
||||
if (vmPlace) {
|
||||
vmDom = Element.fromHTML(dialog
|
||||
? '<dialog id="V-'+ id + '"></dialog>'
|
||||
: '<div id="V-'+ id + '" hidden=""></div>');
|
||||
vmPlace.append(vmDom);
|
||||
|
||||
return fResult;
|
||||
}
|
||||
vm.viewModelDom = vmDom;
|
||||
ViewModelClass.__dom = vmDom;
|
||||
|
||||
/**
|
||||
* @param {string} screenName
|
||||
* @returns {?Object}
|
||||
*/
|
||||
function screen(screenName) {
|
||||
return screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null;
|
||||
}
|
||||
if (ViewTypePopup === position) {
|
||||
vm.close = () => hideScreenPopup(ViewModelClass);
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClassToHide
|
||||
* @returns {void}
|
||||
*/
|
||||
export function hideScreenPopup(ViewModelClassToHide) {
|
||||
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
|
||||
ViewModelClassToHide.__vm.modalVisibility(false);
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClass
|
||||
* @param {Object=} vmScreen
|
||||
* @returns {*}
|
||||
*/
|
||||
function buildViewModel(ViewModelClass, vmScreen) {
|
||||
if (ViewModelClass && !ViewModelClass.__builded) {
|
||||
let vmDom = null;
|
||||
const vm = new ViewModelClass(vmScreen),
|
||||
position = vm.viewModelPosition || '',
|
||||
vmPlace = position ? doc.querySelector('#rl-content #rl-' + position.toLowerCase()) : null;
|
||||
|
||||
ViewModelClass.__builded = true;
|
||||
ViewModelClass.__vm = vm;
|
||||
|
||||
if (vmPlace) {
|
||||
vmDom = Element.fromHTML('<div class="rl-view-model RL-' + vm.viewModelTemplateID + '" hidden=""></div>');
|
||||
vmPlace.append(vmDom);
|
||||
|
||||
vm.viewModelDom = vmDom;
|
||||
ViewModelClass.__dom = vmDom;
|
||||
|
||||
if (ViewType.Popup === position) {
|
||||
vm.cancelCommand = vm.closeCommand = createCommand(() => {
|
||||
hideScreenPopup(ViewModelClass);
|
||||
});
|
||||
|
||||
// show/hide popup/modal
|
||||
const endShowHide = e => {
|
||||
if (e.target === vmDom) {
|
||||
if (vmDom.classList.contains('show')) {
|
||||
autofocus(vmDom);
|
||||
vm.onShowWithDelay && vm.onShowWithDelay();
|
||||
} else {
|
||||
vmDom.hidden = true;
|
||||
vm.onHideWithDelay && vm.onHideWithDelay();
|
||||
// show/hide popup/modal
|
||||
const endShowHide = e => {
|
||||
if (e.target === vmDom) {
|
||||
if (vmDom.classList.contains('animate')) {
|
||||
autofocus(vmDom);
|
||||
vm.afterShow && vm.afterShow();
|
||||
} else {
|
||||
vmDom.close();
|
||||
vm.afterHide && vm.afterHide();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
vm.modalVisibility.subscribe(value => {
|
||||
if (value) {
|
||||
vmDom.style.zIndex = 3000 + popupVisibilityNames().length + 10;
|
||||
vmDom.hidden = false;
|
||||
vm.storeAndSetScope();
|
||||
popupVisibilityNames.push(vm.viewModelName);
|
||||
requestAnimationFrame(() => { // wait just before the next paint
|
||||
vmDom.offsetHeight; // force a reflow
|
||||
vmDom.classList.add('show'); // trigger the transitions
|
||||
});
|
||||
} else {
|
||||
vm.onHide && vm.onHide();
|
||||
vmDom.classList.remove('show');
|
||||
vm.restoreScope();
|
||||
popupVisibilityNames(popupVisibilityNames.filter(v=>v!==vm.viewModelName));
|
||||
}
|
||||
vmDom.setAttribute('aria-hidden', !value);
|
||||
});
|
||||
if ('ontransitionend' in vmDom) {
|
||||
vm.modalVisible.subscribe(value => {
|
||||
if (value) {
|
||||
visiblePopups.add(vm);
|
||||
vmDom.style.zIndex = 3000 + (visiblePopups.size * 2);
|
||||
vmDom.showModal();
|
||||
if (vmDom.backdrop) {
|
||||
vmDom.backdrop.style.zIndex = 3000 + visiblePopups.size;
|
||||
}
|
||||
vm.keyScope.set();
|
||||
requestAnimationFrame(() => { // wait just before the next paint
|
||||
vmDom.offsetHeight; // force a reflow
|
||||
vmDom.classList.add('animate'); // trigger the transitions
|
||||
});
|
||||
} else {
|
||||
visiblePopups.delete(vm);
|
||||
vm.onHide && vm.onHide();
|
||||
vm.keyScope.unset();
|
||||
vmDom.classList.remove('animate'); // trigger the transitions
|
||||
}
|
||||
arePopupsVisible(0 < visiblePopups.size);
|
||||
});
|
||||
vmDom.addEventListener('transitionend', endShowHide);
|
||||
} else {
|
||||
// For Edge < 79 and mobile browsers
|
||||
vm.modalVisibility.subscribe(() => ()=>setTimeout(endShowHide({target:vmDom}), 500));
|
||||
}
|
||||
}
|
||||
|
||||
ko.applyBindingAccessorsToNode(
|
||||
vmDom,
|
||||
{
|
||||
i18nInit: true,
|
||||
template: () => ({ name: vm.viewModelTemplateID })
|
||||
},
|
||||
vm
|
||||
);
|
||||
|
||||
vm.onBuild && vm.onBuild(vmDom);
|
||||
if (vm && ViewType.Popup === position) {
|
||||
vm.registerPopupKeyDown();
|
||||
}
|
||||
} else {
|
||||
console.log('Cannot find view model position: ' + position);
|
||||
}
|
||||
}
|
||||
|
||||
return ViewModelClass && ViewModelClass.__vm;
|
||||
}
|
||||
|
||||
function getScreenPopupViewModel(ViewModelClassToShow) {
|
||||
return (buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom) && ViewModelClassToShow.__vm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClassToShow
|
||||
* @param {Array=} params
|
||||
* @returns {void}
|
||||
*/
|
||||
export function showScreenPopup(ViewModelClassToShow, params = []) {
|
||||
const vm = getScreenPopupViewModel(ViewModelClassToShow);
|
||||
if (vm) {
|
||||
params = params || [];
|
||||
|
||||
vm.onBeforeShow && vm.onBeforeShow(...params);
|
||||
|
||||
vm.modalVisibility(true);
|
||||
|
||||
vm.onShow && vm.onShow(...params);
|
||||
|
||||
runHook('view-model-on-show', [vm.name, vm.__vm, params]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClassToShow
|
||||
* @returns {void}
|
||||
*/
|
||||
export function warmUpScreenPopup(ViewModelClassToShow) {
|
||||
const vm = getScreenPopupViewModel(ViewModelClassToShow);
|
||||
vm && vm.onWarmUp && vm.onWarmUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClassToShow
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPopupVisible(ViewModelClassToShow) {
|
||||
return ViewModelClassToShow && ViewModelClassToShow.__vm && ViewModelClassToShow.__vm.modalVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} screenName
|
||||
* @param {string} subPart
|
||||
* @returns {void}
|
||||
*/
|
||||
function screenOnRoute(screenName, subPart) {
|
||||
let vmScreen = null,
|
||||
isSameScreen = false;
|
||||
|
||||
if (null == screenName || '' == screenName) {
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
|
||||
if (screenName) {
|
||||
vmScreen = screen(screenName);
|
||||
if (!vmScreen) {
|
||||
vmScreen = screen(defaultScreenName);
|
||||
if (vmScreen) {
|
||||
subPart = screenName + '/' + subPart;
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
}
|
||||
|
||||
if (vmScreen && vmScreen.__started) {
|
||||
isSameScreen = currentScreen && vmScreen === currentScreen;
|
||||
|
||||
if (!vmScreen.__builded) {
|
||||
vmScreen.__builded = true;
|
||||
|
||||
vmScreen.viewModels.forEach(ViewModelClass =>
|
||||
buildViewModel(ViewModelClass, vmScreen)
|
||||
ko.applyBindingAccessorsToNode(
|
||||
vmDom,
|
||||
{
|
||||
i18nInit: true,
|
||||
template: () => ({ name: id })
|
||||
},
|
||||
vm
|
||||
);
|
||||
|
||||
vmScreen.onBuild && vmScreen.onBuild();
|
||||
vm.onBuild && vm.onBuild(vmDom);
|
||||
|
||||
fireEvent('rl-view-model', vm);
|
||||
} else {
|
||||
console.log('Cannot find view model position: ' + position);
|
||||
}
|
||||
}
|
||||
|
||||
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} subPart
|
||||
* @returns {void}
|
||||
*/
|
||||
screenOnRoute = (screenName, subPart) => {
|
||||
let vmScreen = null,
|
||||
isSameScreen = false;
|
||||
|
||||
if (null == screenName || '' == screenName) {
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
|
||||
// Close all popups
|
||||
for (let vm of visiblePopups) {
|
||||
false === vm.onClose() || vm.close();
|
||||
}
|
||||
|
||||
if (screenName) {
|
||||
vmScreen = screen(screenName);
|
||||
if (!vmScreen) {
|
||||
vmScreen = screen(defaultScreenName);
|
||||
if (vmScreen) {
|
||||
subPart = screenName + '/' + subPart;
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// hide screen
|
||||
if (currentScreen && !isSameScreen) {
|
||||
currentScreen.onHide && currentScreen.onHide();
|
||||
currentScreen.onHideWithDelay && setTimeout(()=>currentScreen.onHideWithDelay(), 500);
|
||||
if (vmScreen && vmScreen.__started) {
|
||||
isSameScreen = currentScreen && vmScreen === currentScreen;
|
||||
|
||||
if (isNonEmptyArray(currentScreen.viewModels)) {
|
||||
currentScreen.viewModels.forEach(ViewModelClass => {
|
||||
if (
|
||||
ViewModelClass.__vm &&
|
||||
ViewModelClass.__dom &&
|
||||
ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
|
||||
) {
|
||||
ViewModelClass.__dom.hidden = true;
|
||||
ViewModelClass.__vm.viewModelVisible = false;
|
||||
if (!vmScreen.__builded) {
|
||||
vmScreen.__builded = true;
|
||||
|
||||
ViewModelClass.__vm.onHide && ViewModelClass.__vm.onHide();
|
||||
ViewModelClass.__vm.onHideWithDelay && setTimeout(()=>ViewModelClass.__vm.onHideWithDelay(), 500);
|
||||
}
|
||||
vmScreen.viewModels.forEach(ViewModelClass =>
|
||||
buildViewModel(ViewModelClass, vmScreen)
|
||||
);
|
||||
|
||||
vmScreen.onBuild && vmScreen.onBuild();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// hide screen
|
||||
if (currentScreen && !isSameScreen) {
|
||||
hideScreen(currentScreen);
|
||||
}
|
||||
// --
|
||||
|
||||
currentScreen = vmScreen;
|
||||
|
||||
// show screen
|
||||
if (!isSameScreen) {
|
||||
vmScreen.onShow && vmScreen.onShow();
|
||||
|
||||
forEachViewModel(vmScreen, (vm, dom) => {
|
||||
vm.beforeShow && vm.beforeShow();
|
||||
dom.hidden = false;
|
||||
vm.onShow && vm.onShow();
|
||||
autofocus(dom);
|
||||
});
|
||||
}
|
||||
}
|
||||
// --
|
||||
// --
|
||||
|
||||
currentScreen = vmScreen;
|
||||
|
||||
// show screen
|
||||
if (currentScreen && !isSameScreen) {
|
||||
currentScreen.onShow && currentScreen.onShow();
|
||||
|
||||
if (isNonEmptyArray(currentScreen.viewModels)) {
|
||||
currentScreen.viewModels.forEach(ViewModelClass => {
|
||||
if (
|
||||
ViewModelClass.__vm &&
|
||||
ViewModelClass.__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);
|
||||
|
||||
runHook('view-model-on-show', [ViewModelClass.name, ViewModelClass.__vm, params]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// --
|
||||
|
||||
vmScreen && vmScreen.__cross && vmScreen.__cross.parse(subPart);
|
||||
}, 1);
|
||||
vmScreen.__cross && vmScreen.__cross.parse(subPart);
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array} screensClasses
|
||||
* @returns {void}
|
||||
*/
|
||||
export function startScreens(screensClasses) {
|
||||
screensClasses.forEach(CScreen => {
|
||||
if (CScreen) {
|
||||
const vmScreen = new CScreen(),
|
||||
screenName = vmScreen && vmScreen.screenName();
|
||||
|
||||
if (screenName) {
|
||||
export const
|
||||
ViewTypePopup = 'Popups',
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClassToShow
|
||||
* @param {Array=} params
|
||||
* @returns {void}
|
||||
*/
|
||||
showScreenPopup = (ViewModelClassToShow, params = []) => {
|
||||
const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
|
||||
|
||||
if (vm) {
|
||||
params = params || [];
|
||||
|
||||
vm.beforeShow && vm.beforeShow(...params);
|
||||
|
||||
vm.modalVisible(true);
|
||||
|
||||
vm.onShow && vm.onShow(...params);
|
||||
}
|
||||
},
|
||||
|
||||
arePopupsVisible = ko.observable(false),
|
||||
|
||||
/**
|
||||
* @param {Array} screensClasses
|
||||
* @returns {void}
|
||||
*/
|
||||
startScreens = screensClasses => {
|
||||
hasher.clear();
|
||||
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
|
||||
SCREENS = {};
|
||||
currentScreen = null,
|
||||
defaultScreenName = '';
|
||||
|
||||
screensClasses.forEach(CScreen => {
|
||||
if (CScreen) {
|
||||
const vmScreen = new CScreen(),
|
||||
screenName = vmScreen.screenName;
|
||||
defaultScreenName || (defaultScreenName = screenName);
|
||||
|
||||
SCREENS[screenName] = vmScreen;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(SCREENS).forEach(vmScreen =>
|
||||
vmScreen && vmScreen.onStart && vmScreen.onStart()
|
||||
);
|
||||
|
||||
const cross = new Crossroads();
|
||||
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
|
||||
|
||||
hasher.changed.add(cross.parse.bind(cross));
|
||||
hasher.init();
|
||||
|
||||
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
|
||||
setTimeout(() => $htmlCL.add('rl-started-delay'), 200);
|
||||
}
|
||||
|
||||
function decorateKoCommands(thisArg, commands) {
|
||||
Object.entries(commands).forEach(([key, canExecute]) => {
|
||||
let command = thisArg[key],
|
||||
fn = (...args) => fn.enabled() && fn.canExecute() && command.apply(thisArg, args);
|
||||
|
||||
// fn.__realCanExecute = canExecute;
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
ko.decorateCommands = decorateKoCommands;
|
||||
|
||||
/**
|
||||
* @param {miced} $items
|
||||
* @returns {Function}
|
||||
*/
|
||||
function settingsMenuKeysHandler(items) {
|
||||
return ((event, handler)=>{
|
||||
let index = items.length;
|
||||
if (event && index) {
|
||||
while (index-- && !items[index].matches('.selected'));
|
||||
if (handler && 'arrowup' === handler.shortcut) {
|
||||
index && --index;
|
||||
} else if (index < items.length - 1) {
|
||||
++index;
|
||||
forEachObjectValue(SCREENS, vmScreen => {
|
||||
if (!vmScreen.__started) {
|
||||
vmScreen.onStart();
|
||||
vmScreen.__started = true;
|
||||
}
|
||||
});
|
||||
|
||||
const resultHash = items[index].href;
|
||||
resultHash && rl.route.setHash(resultHash, false, true);
|
||||
}
|
||||
}).throttle(200);
|
||||
}
|
||||
const cross = new Crossroads();
|
||||
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
|
||||
|
||||
export {
|
||||
decorateKoCommands,
|
||||
settingsMenuKeysHandler
|
||||
};
|
||||
hasher.add(cross.parse.bind(cross));
|
||||
hasher.init();
|
||||
|
||||
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
|
||||
|
||||
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) =>
|
||||
forEachObjectEntry(commands, (key, canExecute) => {
|
||||
let command = thisArg[key],
|
||||
fn = (...args) => fn.canExecute() && command.apply(thisArg, args);
|
||||
|
||||
fn.canExecute = koComputable(() => canExecute.call(thisArg, thisArg));
|
||||
|
||||
thisArg[key] = fn;
|
||||
});
|
||||
|
||||
ko.decorateCommands = decorateKoCommands;
|
||||
|
|
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
|
||||
{
|
||||
|
@ -24,8 +24,7 @@ export class AbstractCollectionModel extends Array
|
|||
const result = new this();
|
||||
if (json) {
|
||||
if ('Collection/'+this.name.replace('Model', '') === json['@Object']) {
|
||||
Object.entries(json).forEach(([key, value]) => '@' !== key[0] && (result[key] = value));
|
||||
// json[@Count]
|
||||
forEachObjectEntry(json, (key, value) => '@' !== key[0] && (result[key] = value));
|
||||
json = json['@Collection'];
|
||||
}
|
||||
if (isArray(json)) {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { change } from 'Common/Links';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
export class AccountModel extends AbstractModel {
|
||||
|
@ -8,23 +6,16 @@ export class AccountModel extends AbstractModel {
|
|||
* @param {boolean=} canBeDelete = true
|
||||
* @param {number=} count = 0
|
||||
*/
|
||||
constructor(email, canBeDelete = true, count = 0) {
|
||||
constructor(email/*, count = 0*/, isAdditional = true) {
|
||||
super();
|
||||
|
||||
this.email = email;
|
||||
|
||||
this.addObservables({
|
||||
count: count,
|
||||
deleteAccess: false,
|
||||
canBeDeleted: !!canBeDelete
|
||||
// count: count || 0,
|
||||
askDelete: false,
|
||||
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.fileType = FileType.Unknown;
|
||||
this.friendlySize = '';
|
||||
this.isInline = false;
|
||||
this.isLinked = false;
|
||||
this.isThumbnail = false;
|
||||
this.cid = '';
|
||||
this.cidWithoutTags = '';
|
||||
this.contentLocation = '';
|
||||
this.download = '';
|
||||
this.folder = '';
|
||||
this.uid = '';
|
||||
this.url = '';
|
||||
this.mimeIndex = '';
|
||||
this.framed = false;
|
||||
|
||||
this.addObservables({
|
||||
isInline: false,
|
||||
isLinked: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,7 +46,6 @@ export class AttachmentModel extends AbstractModel {
|
|||
const attachment = super.reviveFromJson(json);
|
||||
if (attachment) {
|
||||
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
|
||||
attachment.cidWithoutTags = attachment.cid.replace(/^<+/, '').replace(/>+$/, '');
|
||||
|
||||
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
|
||||
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
|
||||
|
@ -51,6 +53,10 @@ export class AttachmentModel extends AbstractModel {
|
|||
return attachment;
|
||||
}
|
||||
|
||||
contentId() {
|
||||
return this.cid.replace(/^<+|>+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
@ -122,29 +128,21 @@ export class AttachmentModel extends AbstractModel {
|
|||
* @returns {string}
|
||||
*/
|
||||
linkDownload() {
|
||||
return attachmentDownload(this.download);
|
||||
return this.url || attachmentDownload(this.download);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
linkPreview() {
|
||||
return serverRequestRaw('View', this.download);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
linkThumbnail() {
|
||||
return this.hasThumbnail() ? serverRequestRaw('ViewThumbnail', this.download) : '';
|
||||
return this.url || serverRequestRaw('View', this.download);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
linkThumbnailPreviewStyle() {
|
||||
const link = this.linkThumbnail();
|
||||
return link ? 'background:url(' + link + ')' : '';
|
||||
return this.hasThumbnail() ? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -175,7 +173,7 @@ export class AttachmentModel extends AbstractModel {
|
|||
const localEvent = event.originalEvent || event;
|
||||
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
|
||||
let link = this.linkDownload();
|
||||
if ('http' !== link.substr(0, 4)) {
|
||||
if ('http' !== link.slice(0, 4)) {
|
||||
link = location.protocol + '//' + location.host + location.pathname + link;
|
||||
}
|
||||
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);
|
||||
|
|
|
@ -11,13 +11,20 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
|||
*/
|
||||
static reviveFromJson(items) {
|
||||
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}
|
||||
*/
|
||||
hasVisible() {
|
||||
return !!this.find(item => !item.isLinked);
|
||||
return !!this.filter(item => !item.isLinked()).length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
|||
* @returns {*}
|
||||
*/
|
||||
findByCid(cid) {
|
||||
cid = cid.replace(/^<+|>+$/, '');
|
||||
return this.find(item => cid === item.cidWithoutTags);
|
||||
cid = cid.replace(/^<+|>+$/g, '');
|
||||
return this.find(item => cid === item.contentId());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,8 +65,8 @@ export class ComposeAttachmentModel extends AbstractModel {
|
|||
item.download,
|
||||
item.fileName,
|
||||
item.estimatedSize,
|
||||
item.isInline,
|
||||
item.isLinked,
|
||||
item.isInline(),
|
||||
item.isLinked(),
|
||||
item.cid,
|
||||
item.contentLocation
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isNonEmptyArray } from 'Common/Utils';
|
||||
import { arrayLength } from 'Common/Utils';
|
||||
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
|
@ -26,7 +26,7 @@ export class ContactModel extends AbstractModel {
|
|||
let name = '',
|
||||
email = '';
|
||||
|
||||
if (isNonEmptyArray(this.properties)) {
|
||||
if (arrayLength(this.properties)) {
|
||||
this.properties.forEach(property => {
|
||||
if (property) {
|
||||
if (ContactPropertyType.FirstName === property.type()) {
|
||||
|
@ -52,7 +52,7 @@ export class ContactModel extends AbstractModel {
|
|||
const contact = super.reviveFromJson(json);
|
||||
if (contact) {
|
||||
let list = [];
|
||||
if (isNonEmptyArray(json.properties)) {
|
||||
if (arrayLength(json.properties)) {
|
||||
json.properties.forEach(property => {
|
||||
property = ContactPropertyModel.reviveFromJson(property);
|
||||
property && list.push(property);
|
||||
|
|
|
@ -260,7 +260,7 @@ class Tokenizer
|
|||
}
|
||||
}
|
||||
|
||||
class EmailModel extends AbstractModel {
|
||||
export class EmailModel extends AbstractModel {
|
||||
/**
|
||||
* @param {string=} email = ''
|
||||
* @param {string=} name = ''
|
||||
|
@ -430,5 +430,3 @@ class EmailModel extends AbstractModel {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { EmailModel, EmailModel as default };
|
||||
|
|
|
@ -1,42 +1,112 @@
|
|||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||
|
||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||
import { isArray, pInt } from 'Common/Utils';
|
||||
import { ClientSideKeyName, FolderType } from 'Common/EnumsUser';
|
||||
import * as Cache from 'Common/Cache';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||
import { getFolderFromCacheList, setFolder, setFolderInboxName, setFolderHash } from 'Common/Cache';
|
||||
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||
|
||||
import * as Local from 'Storage/Client';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import ko from 'ko';
|
||||
|
||||
import { isPosNumeric } from 'Common/UtilsUser';
|
||||
import { sortFolders } from 'Common/Folders';
|
||||
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
//import { mailBox } from 'Common/Links';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
const
|
||||
ServerFolderType = {
|
||||
USER: 0,
|
||||
INBOX: 1,
|
||||
SENT: 2,
|
||||
DRAFTS: 3,
|
||||
JUNK: 4,
|
||||
TRASH: 5,
|
||||
IMPORTANT: 10,
|
||||
FLAGGED: 11,
|
||||
ALL: 12
|
||||
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
|
||||
},
|
||||
|
||||
normalizeFolder = sFolderFullNameRaw => ('' === sFolderFullNameRaw
|
||||
|| UNUSED_OPTION_VALUE === sFolderFullNameRaw
|
||||
|| null !== Cache.getFolderFromCacheList(sFolderFullNameRaw))
|
||||
? sFolderFullNameRaw
|
||||
: '';
|
||||
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
|
||||
{
|
||||
|
@ -44,11 +114,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
constructor() {
|
||||
super();
|
||||
this.CountRec
|
||||
this.FoldersHash
|
||||
this.IsThreadsSupported
|
||||
this.Namespace;
|
||||
this.Optimized
|
||||
this.SystemFolders
|
||||
this.Capabilities
|
||||
}
|
||||
*/
|
||||
|
||||
|
@ -57,164 +127,162 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
* @returns {FolderCollectionModel}
|
||||
*/
|
||||
static reviveFromJson(object) {
|
||||
const expandedFolders = Local.get(ClientSideKeyName.ExpandedFolders);
|
||||
return super.reviveFromJson(object, (oFolder, self) => {
|
||||
let oCacheFolder = Cache.getFolderFromCacheList(oFolder.FullNameRaw);
|
||||
/*
|
||||
if (oCacheFolder) {
|
||||
oFolder.SubFolders = FolderCollectionModel.reviveFromJson(oFolder.SubFolders);
|
||||
oFolder.SubFolders && oCacheFolder.subFolders(oFolder.SubFolders);
|
||||
}
|
||||
*/
|
||||
if (!oCacheFolder && (oCacheFolder = FolderModel.reviveFromJson(oFolder))) {
|
||||
if (oFolder.FullNameRaw == self.SystemFolders[ServerFolderType.INBOX]) {
|
||||
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
|
||||
if (object && object.SystemFolders) {
|
||||
forEachObjectEntry(SystemFolders, key =>
|
||||
SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
|
||||
);
|
||||
}
|
||||
|
||||
const result = super.reviveFromJson(object, oFolder => {
|
||||
let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
|
||||
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
|
||||
|
||||
if (!oCacheFolder) {
|
||||
oCacheFolder = FolderModel.reviveFromJson(oFolder);
|
||||
if (!oCacheFolder)
|
||||
return null;
|
||||
|
||||
if (1 == type) {
|
||||
oCacheFolder.type(FolderType.Inbox);
|
||||
Cache.setFolderInboxName(oFolder.FullNameRaw);
|
||||
setFolderInboxName(oFolder.FullName);
|
||||
}
|
||||
Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullNameRaw, oCacheFolder);
|
||||
setFolder(oCacheFolder);
|
||||
}
|
||||
|
||||
if (oCacheFolder) {
|
||||
oCacheFolder.collapsed(!expandedFolders
|
||||
|| !isArray(expandedFolders)
|
||||
|| !expandedFolders.includes(oCacheFolder.fullNameHash));
|
||||
if (1 < type) {
|
||||
oCacheFolder.type(type);
|
||||
}
|
||||
|
||||
if (oFolder.Extended) {
|
||||
if (oFolder.Extended.Hash) {
|
||||
Cache.setFolderHash(oCacheFolder.fullNameRaw, oFolder.Extended.Hash);
|
||||
}
|
||||
oCacheFolder.collapsed(!expandedFolders
|
||||
|| !isArray(expandedFolders)
|
||||
|| !expandedFolders.includes(oCacheFolder.fullName));
|
||||
|
||||
if (null != oFolder.Extended.MessageCount) {
|
||||
oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
|
||||
}
|
||||
if (oFolder.Extended) {
|
||||
if (oFolder.Extended.Hash) {
|
||||
setFolderHash(oCacheFolder.fullName, oFolder.Extended.Hash);
|
||||
}
|
||||
|
||||
if (null != oFolder.Extended.MessageUnseenCount) {
|
||||
oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
|
||||
}
|
||||
if (null != oFolder.Extended.MessageCount) {
|
||||
oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
|
||||
}
|
||||
|
||||
if (null != oFolder.Extended.MessageUnseenCount) {
|
||||
oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
|
||||
}
|
||||
}
|
||||
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() {
|
||||
const cnt = pInt(this.CountRec);
|
||||
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
|
||||
|
||||
let limit = pInt(Settings.app('folderSpecLimit'));
|
||||
limit = 100 < limit ? 100 : 10 > limit ? 10 : limit;
|
||||
|
||||
FolderUserStore.displaySpecSetting(0 >= cnt || limit < cnt);
|
||||
if (!(
|
||||
SettingsGet('SentFolder') +
|
||||
SettingsGet('DraftsFolder') +
|
||||
SettingsGet('SpamFolder') +
|
||||
SettingsGet('TrashFolder') +
|
||||
SettingsGet('ArchiveFolder')
|
||||
)
|
||||
) {
|
||||
FolderUserStore.saveSystemFolders(SystemFolders);
|
||||
}
|
||||
|
||||
FolderUserStore.folderList(this);
|
||||
|
||||
if (undefined !== this.Namespace) {
|
||||
FolderUserStore.namespace = this.Namespace;
|
||||
}
|
||||
FolderUserStore.namespace = this.Namespace;
|
||||
|
||||
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
|
||||
|
||||
FolderUserStore.folderListOptimized(!!this.Optimized);
|
||||
FolderUserStore.sortSupported(!!this.IsSortSupported);
|
||||
FolderUserStore.quotaUsage(this.quotaUsage);
|
||||
FolderUserStore.quotaLimit(this.quotaLimit);
|
||||
FolderUserStore.capabilities(this.Capabilities);
|
||||
|
||||
let update = false;
|
||||
FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
|
||||
FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
|
||||
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
|
||||
FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
|
||||
FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
|
||||
|
||||
if (
|
||||
this.SystemFolders &&
|
||||
!('' +
|
||||
SettingsGet('SentFolder') +
|
||||
SettingsGet('DraftFolder') +
|
||||
SettingsGet('SpamFolder') +
|
||||
SettingsGet('TrashFolder') +
|
||||
SettingsGet('ArchiveFolder'))
|
||||
) {
|
||||
Settings.set('SentFolder', this.SystemFolders[ServerFolderType.SENT] || null);
|
||||
Settings.set('DraftFolder', this.SystemFolders[ServerFolderType.DRAFTS] || null);
|
||||
Settings.set('SpamFolder', this.SystemFolders[ServerFolderType.JUNK] || null);
|
||||
Settings.set('TrashFolder', this.SystemFolders[ServerFolderType.TRASH] || null);
|
||||
Settings.set('ArchiveFolder', this.SystemFolders[ServerFolderType.ALL] || null);
|
||||
|
||||
update = true;
|
||||
}
|
||||
|
||||
FolderUserStore.sentFolder(normalizeFolder(SettingsGet('SentFolder')));
|
||||
FolderUserStore.draftFolder(normalizeFolder(SettingsGet('DraftFolder')));
|
||||
FolderUserStore.spamFolder(normalizeFolder(SettingsGet('SpamFolder')));
|
||||
FolderUserStore.trashFolder(normalizeFolder(SettingsGet('TrashFolder')));
|
||||
FolderUserStore.archiveFolder(normalizeFolder(SettingsGet('ArchiveFolder')));
|
||||
|
||||
if (update) {
|
||||
rl.app.Remote.saveSystemFolders(()=>{}, {
|
||||
SentFolder: FolderUserStore.sentFolder(),
|
||||
DraftFolder: FolderUserStore.draftFolder(),
|
||||
SpamFolder: FolderUserStore.spamFolder(),
|
||||
TrashFolder: FolderUserStore.trashFolder(),
|
||||
ArchiveFolder: FolderUserStore.archiveFolder()
|
||||
});
|
||||
}
|
||||
|
||||
Local.set(ClientSideKeyName.FoldersLashHash, this.FoldersHash);
|
||||
// FolderUserStore.folderList.valueHasMutated();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getSystemFolderName(type, def)
|
||||
{
|
||||
switch (type) {
|
||||
case FolderType.Inbox:
|
||||
return i18n('FOLDER_LIST/INBOX_NAME');
|
||||
case FolderType.SentItems:
|
||||
return i18n('FOLDER_LIST/SENT_NAME');
|
||||
case FolderType.Draft:
|
||||
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 {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.fullName = '';
|
||||
this.fullNameRaw = '';
|
||||
this.fullNameHash = '';
|
||||
this.delimiter = '';
|
||||
this.namespace = '';
|
||||
this.deep = 0;
|
||||
this.expires = 0;
|
||||
this.metadata = {};
|
||||
|
||||
this.selectable = false;
|
||||
this.exists = true;
|
||||
|
||||
// this.hash = '';
|
||||
// this.uidNext = 0;
|
||||
|
||||
this.addObservables({
|
||||
name: '',
|
||||
type: FolderType.User,
|
||||
selectable: false,
|
||||
|
||||
focused: false,
|
||||
selected: false,
|
||||
edited: false,
|
||||
subscribed: true,
|
||||
checkable: false,
|
||||
deleteAccess: false,
|
||||
checkable: false, // Check for new messages
|
||||
askDelete: false,
|
||||
|
||||
nameForEdit: '',
|
||||
errorMsg: '',
|
||||
|
||||
privateMessageCountAll: 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.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
|
||||
* @param {FetchJsonFolder} json
|
||||
|
@ -223,12 +291,22 @@ export class FolderModel extends AbstractModel {
|
|||
static reviveFromJson(json) {
|
||||
const folder = super.reviveFromJson(json);
|
||||
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,
|
||||
write: (iValue) => {
|
||||
if (isPosNumeric(iValue, true)) {
|
||||
if (isPosNumeric(iValue)) {
|
||||
folder.privateMessageCountAll(iValue);
|
||||
} else {
|
||||
folder.privateMessageCountAll.valueHasMutated();
|
||||
|
@ -237,10 +315,10 @@ export class FolderModel extends AbstractModel {
|
|||
})
|
||||
.extend({ notify: 'always' });
|
||||
|
||||
folder.messageCountUnread = ko.computed({
|
||||
folder.messageCountUnread = koComputable({
|
||||
read: folder.privateMessageCountUnread,
|
||||
write: (value) => {
|
||||
if (isPosNumeric(value, true)) {
|
||||
if (isPosNumeric(value)) {
|
||||
folder.privateMessageCountUnread(value);
|
||||
} else {
|
||||
folder.privateMessageCountUnread.valueHasMutated();
|
||||
|
@ -253,61 +331,68 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
isInbox: () => FolderType.Inbox === folder.type(),
|
||||
|
||||
hasSubscribedSubfolders:
|
||||
() =>
|
||||
!!folder.subFolders.find(
|
||||
oFolder => (oFolder.subscribed() || oFolder.hasSubscribedSubfolders()) && !oFolder.isSystemFolder()
|
||||
),
|
||||
isFlagged: () => FolderUserStore.currentFolder() === folder
|
||||
&& MessagelistUserStore.listSearch().includes('flagged'),
|
||||
|
||||
canBeEdited: () => FolderType.User === folder.type() && folder.exists && folder.selectable,
|
||||
hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
|
||||
|
||||
hasSubscriptions: () => folder.subscribed() | !!folder.subFolders().find(
|
||||
oFolder => {
|
||||
const subscribed = oFolder.hasSubscriptions();
|
||||
return !oFolder.isSystemFolder() && subscribed;
|
||||
}
|
||||
),
|
||||
|
||||
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
|
||||
|
||||
isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
|
||||
|
||||
canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
|
||||
|
||||
canBeDeleted: () => folder.canBeSelected() && folder.exists,
|
||||
|
||||
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 isSubscribed = folder.subscribed(),
|
||||
isSubFolders = folder.hasSubscribedSubfolders();
|
||||
|
||||
return isSubscribed || (isSubFolders && (!folder.exists || !folder.selectable));
|
||||
const selectable = folder.canBeSelected(),
|
||||
visible = (folder.subscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
|
||||
return folder.hasVisibleSubfolders() | visible;
|
||||
},
|
||||
|
||||
isSystemFolder: () => FolderType.User !== folder.type(),
|
||||
|
||||
hidden: () => {
|
||||
const isSystem = folder.isSystemFolder(),
|
||||
isSubFolders = folder.hasSubscribedSubfolders();
|
||||
|
||||
return (isSystem && !isSubFolders) || (!folder.selectable && !isSubFolders);
|
||||
},
|
||||
hidden: () => !folder.selectable() && (folder.isSystemFolder() | !folder.hasVisibleSubfolders()),
|
||||
|
||||
printableUnreadCount: () => {
|
||||
const count = folder.messageCountAll(),
|
||||
unread = folder.messageCountUnread(),
|
||||
type = folder.type();
|
||||
|
||||
if (0 < count) {
|
||||
if (FolderType.Draft === type) {
|
||||
return '' + count;
|
||||
if (count) {
|
||||
if (FolderType.Drafts === type) {
|
||||
return count;
|
||||
}
|
||||
if (
|
||||
0 < unread &&
|
||||
unread &&
|
||||
FolderType.Trash !== type &&
|
||||
FolderType.Archive !== type &&
|
||||
FolderType.SentItems !== type
|
||||
FolderType.Sent !== type
|
||||
) {
|
||||
return '' + unread;
|
||||
return unread;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
return null;
|
||||
},
|
||||
|
||||
canBeDeleted: () => !folder.isSystemFolder() && !folder.subFolders.length,
|
||||
|
||||
canBeSubscribed: () => !folder.isSystemFolder()
|
||||
&& SettingsUserStore.hideUnsubscribed()
|
||||
&& folder.selectable
|
||||
&& Settings.app('useImapSubscribe'),
|
||||
|
||||
canBeSelected: () => !folder.isSystemFolder() && folder.selectable,
|
||||
|
||||
localName: () => {
|
||||
let name = folder.name();
|
||||
if (folder.isSystemFolder()) {
|
||||
|
@ -320,28 +405,22 @@ export class FolderModel extends AbstractModel {
|
|||
manageFolderSystemName: () => {
|
||||
if (folder.isSystemFolder()) {
|
||||
translatorTrigger();
|
||||
let suffix = getSystemFolderName(folder.type(), '');
|
||||
let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
|
||||
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
|
||||
return '(' + suffix + ')';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
collapsed: {
|
||||
read: () => !folder.hidden() && folder.collapsedPrivate(),
|
||||
write: (value) => {
|
||||
folder.collapsedPrivate(value);
|
||||
}
|
||||
},
|
||||
|
||||
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
|
||||
|
||||
hasSubscribedUnreadMessagesSubfolders: () =>
|
||||
!!folder.subFolders.find(
|
||||
folder => folder.hasUnreadMessages() || folder.hasSubscribedUnreadMessagesSubfolders()
|
||||
)
|
||||
!!folder.subFolders().find(
|
||||
folder => folder.hasUnreadMessages() | folder.hasSubscribedUnreadMessagesSubfolders()
|
||||
)
|
||||
|
||||
// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
|
||||
});
|
||||
|
||||
folder.addSubscribables({
|
||||
|
@ -351,7 +430,7 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
messageCountUnread: unread => {
|
||||
if (FolderType.Inbox === folder.type()) {
|
||||
dispatchEvent(new CustomEvent('mailbox.inbox-unread-count', {detail:unread}));
|
||||
fireEvent('mailbox.inbox-unread-count', unread);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -363,16 +442,9 @@ export class FolderModel extends AbstractModel {
|
|||
* @returns {string}
|
||||
*/
|
||||
collapsedCss() {
|
||||
return 'e-collapsed-sign ' + (this.hasSubscribedSubfolders()
|
||||
return 'e-collapsed-sign ' + (this.hasVisibleSubfolders()
|
||||
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
|
||||
: 'icon-none'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
printableFullName() {
|
||||
return this.fullName.split(this.delimiter).join(' / ');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ko from 'ko';
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
|
@ -21,10 +21,10 @@ export class IdentityModel extends AbstractModel {
|
|||
signature: '',
|
||||
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(),
|
||||
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 { i18n } from 'Common/Translator';
|
||||
|
||||
import { encodeHtml } from 'Common/Html';
|
||||
import { isArray, isNonEmptyArray } from 'Common/Utils';
|
||||
|
||||
import { doc } from 'Common/Globals';
|
||||
import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html';
|
||||
import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
|
||||
import { serverRequestRaw } from 'Common/Links';
|
||||
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import { FileInfo } from 'Common/File';
|
||||
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
|
||||
|
@ -18,13 +19,17 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
|||
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||
|
||||
const
|
||||
SignedVerifyStatus = {
|
||||
UnknownPublicKeys: -4,
|
||||
UnknownPrivateKey: -3,
|
||||
Unverified: -2,
|
||||
Error: -1,
|
||||
None: 0,
|
||||
Success: 1
|
||||
// eslint-disable-next-line max-len
|
||||
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
|
||||
// eslint-disable-next-line max-len
|
||||
email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
|
||||
|
||||
hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
|
||||
getRealHeight = el => {
|
||||
hcont.innerHTML = el.outerHTML;
|
||||
const result = hcont.clientHeight;
|
||||
hcont.innerHTML = '';
|
||||
return result;
|
||||
},
|
||||
|
||||
replyHelper = (emails, unic, localEmails) => {
|
||||
|
@ -36,6 +41,8 @@ const
|
|||
});
|
||||
};
|
||||
|
||||
doc.body.append(hcont);
|
||||
|
||||
export class MessageModel extends AbstractModel {
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -44,38 +51,34 @@ export class MessageModel extends AbstractModel {
|
|||
|
||||
this.addObservables({
|
||||
subject: '',
|
||||
plain: '',
|
||||
html: '',
|
||||
size: 0,
|
||||
spamScore: 0,
|
||||
spamResult: '',
|
||||
isSpam: false,
|
||||
hasVirus: null, // or boolean when scanned
|
||||
dateTimeStampInUTC: 0,
|
||||
priority: MessagePriority.Normal,
|
||||
|
||||
senderEmailsString: '',
|
||||
senderClearEmailsString: '',
|
||||
|
||||
newForAnimation: false,
|
||||
|
||||
deleted: false,
|
||||
isDeleted: false,
|
||||
isUnseen: false,
|
||||
isFlagged: false,
|
||||
isAnswered: false,
|
||||
isForwarded: false,
|
||||
isReadReceipt: false,
|
||||
|
||||
focused: false,
|
||||
selected: false,
|
||||
checked: false,
|
||||
hasAttachments: false,
|
||||
|
||||
isHtml: false,
|
||||
hasImages: false,
|
||||
hasExternals: false,
|
||||
|
||||
isPgpSigned: false,
|
||||
isPgpEncrypted: false,
|
||||
pgpSignedVerifyStatus: SignedVerifyStatus.None,
|
||||
pgpSignedVerifyUser: '',
|
||||
pgpSigned: null,
|
||||
pgpVerified: null,
|
||||
|
||||
pgpEncrypted: null,
|
||||
pgpDecrypted: false,
|
||||
|
||||
readReceipt: '',
|
||||
|
||||
|
@ -84,22 +87,32 @@ export class MessageModel extends AbstractModel {
|
|||
});
|
||||
|
||||
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
||||
this.attachmentsSpecData = ko.observableArray();
|
||||
this.threads = ko.observableArray();
|
||||
this.unsubsribeLinks = ko.observableArray();
|
||||
this.flags = ko.observableArray();
|
||||
|
||||
this.addComputables({
|
||||
attachmentIconClass: () => FileInfo.getCombinedIconClass(this.hasAttachments() ? this.attachmentsSpecData() : []),
|
||||
attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()),
|
||||
threadsLen: () => this.threads().length,
|
||||
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')
|
||||
});
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this.folder = '';
|
||||
this.uid = '';
|
||||
this.uid = 0;
|
||||
this.hash = '';
|
||||
this.requestHash = '';
|
||||
this.externalProxy = false;
|
||||
this.emails = [];
|
||||
this.from = new EmailCollectionModel;
|
||||
this.to = new EmailCollectionModel;
|
||||
|
@ -107,7 +120,6 @@ export class MessageModel extends AbstractModel {
|
|||
this.bcc = new EmailCollectionModel;
|
||||
this.replyTo = new EmailCollectionModel;
|
||||
this.deliveredTo = new EmailCollectionModel;
|
||||
this.unsubsribeLinks = [];
|
||||
this.body = null;
|
||||
this.draftInfo = [];
|
||||
this.messageId = '';
|
||||
|
@ -118,49 +130,50 @@ export class MessageModel extends AbstractModel {
|
|||
clear() {
|
||||
this._reset();
|
||||
this.subject('');
|
||||
this.html('');
|
||||
this.plain('');
|
||||
this.size(0);
|
||||
this.spamScore(0);
|
||||
this.spamResult('');
|
||||
this.isSpam(false);
|
||||
this.hasVirus(null);
|
||||
this.dateTimeStampInUTC(0);
|
||||
this.priority(MessagePriority.Normal);
|
||||
|
||||
this.senderEmailsString('');
|
||||
this.senderClearEmailsString('');
|
||||
|
||||
this.newForAnimation(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.checked(false);
|
||||
this.hasAttachments(false);
|
||||
this.attachmentsSpecData([]);
|
||||
|
||||
this.isHtml(false);
|
||||
this.hasImages(false);
|
||||
this.hasExternals(false);
|
||||
this.attachments(new AttachmentCollectionModel);
|
||||
|
||||
this.isPgpSigned(false);
|
||||
this.isPgpEncrypted(false);
|
||||
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
|
||||
this.pgpSignedVerifyUser('');
|
||||
this.pgpSigned(null);
|
||||
this.pgpVerified(null);
|
||||
|
||||
this.pgpEncrypted(null);
|
||||
this.pgpDecrypted(false);
|
||||
|
||||
this.priority(MessagePriority.Normal);
|
||||
this.readReceipt('');
|
||||
|
||||
this.threads([]);
|
||||
this.unsubsribeLinks([]);
|
||||
|
||||
this.hasUnseenSubMessage(false);
|
||||
this.hasFlaggedSubMessage(false);
|
||||
}
|
||||
|
||||
spamStatus() {
|
||||
let spam = this.spamResult();
|
||||
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} properties
|
||||
* @returns {Array}
|
||||
|
@ -179,7 +192,7 @@ export class MessageModel extends AbstractModel {
|
|||
}
|
||||
|
||||
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.senderClearEmailsString(this[list].toStringClear());
|
||||
}
|
||||
|
@ -189,13 +202,12 @@ export class MessageModel extends AbstractModel {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
revivePropertiesFromJson(json) {
|
||||
if ('Priority' in json) {
|
||||
let p = parseInt(json.Priority, 10);
|
||||
json.Priority = MessagePriority.High == p || MessagePriority.Low == p ? p : MessagePriority.Normal;
|
||||
if ('Priority' in json && ![MessagePriority.High, MessagePriority.Low].includes(json.Priority)) {
|
||||
json.Priority = MessagePriority.Normal;
|
||||
}
|
||||
if (super.revivePropertiesFromJson(json)) {
|
||||
// this.foundedCIDs = isArray(json.FoundedCIDs) ? json.FoundedCIDs : [];
|
||||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundedCIDs));
|
||||
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
||||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
|
||||
|
||||
this.computeSenderEmail();
|
||||
}
|
||||
|
@ -205,14 +217,14 @@ export class MessageModel extends AbstractModel {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
hasUnsubsribeLinks() {
|
||||
return this.unsubsribeLinks && this.unsubsribeLinks.length;
|
||||
return this.unsubsribeLinks().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getFirstUnsubsribeLink() {
|
||||
return this.unsubsribeLinks && this.unsubsribeLinks.length ? this.unsubsribeLinks[0] || '' : '';
|
||||
return this.unsubsribeLinks()[0] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -229,7 +241,7 @@ export class MessageModel extends AbstractModel {
|
|||
*/
|
||||
fromDkimData() {
|
||||
let result = ['none', ''];
|
||||
if (isNonEmptyArray(this.from) && 1 === this.from.length && this.from[0] && this.from[0].dkimStatus) {
|
||||
if (1 === arrayLength(this.from) && this.from[0] && this.from[0].dkimStatus) {
|
||||
result = [this.from[0].dkimStatus, this.from[0].dkimValue || ''];
|
||||
}
|
||||
|
||||
|
@ -277,7 +289,7 @@ export class MessageModel extends AbstractModel {
|
|||
*/
|
||||
lineAsCss() {
|
||||
let classes = [];
|
||||
Object.entries({
|
||||
forEachObjectEntry({
|
||||
deleted: this.deleted(),
|
||||
'deleted-mark': this.isDeleted(),
|
||||
selected: this.selected(),
|
||||
|
@ -288,13 +300,12 @@ export class MessageModel extends AbstractModel {
|
|||
forwarded: this.isForwarded(),
|
||||
focused: this.focused(),
|
||||
important: this.isImportant(),
|
||||
withAttachments: this.hasAttachments(),
|
||||
new: this.newForAnimation(),
|
||||
withAttachments: !!this.attachments().length,
|
||||
emptySubject: !this.subject(),
|
||||
// hasChildrenMessage: 1 < this.threadsLen(),
|
||||
hasUnseenSubMessage: this.hasUnseenSubMessage(),
|
||||
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
|
||||
}).forEach(([key, value]) => value && classes.push(key));
|
||||
}, (key, value) => value && classes.push(key));
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
|
@ -367,35 +378,138 @@ export class MessageModel extends AbstractModel {
|
|||
return [toResult, ccResult];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} print = false
|
||||
*/
|
||||
viewHtml() {
|
||||
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) {
|
||||
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
||||
ccLine = this.ccToLine(false),
|
||||
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
||||
win = open(''),
|
||||
doc = win.document;
|
||||
doc.write(PreviewHTML
|
||||
.replace(/{{subject}}/g, encodeHtml(this.subject()))
|
||||
.replace('{{date}}', encodeHtml(m ? m.format('LLL') : ''))
|
||||
.replace('{{fromCreds}}', encodeHtml(this.fromToLine(false)))
|
||||
.replace('{{toCreds}}', encodeHtml(this.toToLine(false)))
|
||||
.replace('{{toLabel}}', encodeHtml(i18n('GLOBAL/TO')))
|
||||
.replace('{{ccHide}}', ccLine ? '' : 'hidden=""')
|
||||
.replace('{{ccCreds}}', encodeHtml(ccLine))
|
||||
.replace('{{ccLabel}}', encodeHtml(i18n('GLOBAL/CC')))
|
||||
.replace('{{bodyClass}}', this.isHtml() ? 'html' : 'plain')
|
||||
.replace('{{html}}', this.bodyAsHTML())
|
||||
sdoc = win.document;
|
||||
let subject = encodeHtml(this.subject()),
|
||||
mode = this.isHtml() ? 'div' : 'pre',
|
||||
cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '',
|
||||
style = getComputedStyle(doc.querySelector('.messageView')),
|
||||
prop = property => style.getPropertyValue(property);
|
||||
sdoc.write(PreviewHTML
|
||||
.replace('<title>', '<title>'+subject)
|
||||
// eslint-disable-next-line max-len
|
||||
.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}>`)
|
||||
);
|
||||
|
||||
doc.close();
|
||||
sdoc.close();
|
||||
|
||||
if (print) {
|
||||
setTimeout(() => win.print(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} print = false
|
||||
*/
|
||||
popupMessage() {
|
||||
this.viewPopupMessage(false);
|
||||
}
|
||||
|
||||
printMessage() {
|
||||
this.viewPopupMessage(true);
|
||||
}
|
||||
|
@ -411,70 +525,61 @@ export class MessageModel extends AbstractModel {
|
|||
* @param {MessageModel} message
|
||||
* @returns {MessageModel}
|
||||
*/
|
||||
populateByMessageListItem(message) {
|
||||
if (message) {
|
||||
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.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 = '';
|
||||
static fromMessageListItem(message) {
|
||||
let self = new MessageModel();
|
||||
|
||||
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() {
|
||||
if (this.body && this.body.rlHasImages) {
|
||||
const body = this.body;
|
||||
if (body && this.hasImages()) {
|
||||
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 => {
|
||||
if (node.matches('img')) {
|
||||
node.loading = 'lazy';
|
||||
|
@ -482,81 +587,28 @@ export class MessageModel extends AbstractModel {
|
|||
node.src = node.getAttribute(attr);
|
||||
});
|
||||
|
||||
attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url';
|
||||
body.querySelectorAll('[' + attr + ']').forEach(node => {
|
||||
node.setAttribute('style', ((node.getAttribute('style')||'')
|
||||
+ ';' + node.getAttribute(attr))
|
||||
.replace(/^[;\s]+/,''));
|
||||
body.querySelectorAll('[data-x-style-url]').forEach(node => {
|
||||
forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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') || ''));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
storeDataInDom() {
|
||||
if (this.body) {
|
||||
this.body.rlIsHtml = !!this.isHtml();
|
||||
this.body.rlHasImages = !!this.hasImages();
|
||||
}
|
||||
}
|
||||
|
||||
fetchDataFromDom() {
|
||||
if (this.body) {
|
||||
this.isHtml(!!this.body.rlIsHtml);
|
||||
this.hasImages(!!this.body.rlHasImages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
bodyAsHTML() {
|
||||
if (this.body) {
|
||||
let clone = this.body.cloneNode(true),
|
||||
attr = 'data-html-editor-font-wrapper';
|
||||
// if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) {
|
||||
if (this.body && !this.body.querySelector('iframe')) {
|
||||
let clone = this.body.cloneNode(true);
|
||||
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
|
||||
node => node.classList.remove('rl-bq-switcher','hidden-bq')
|
||||
);
|
||||
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
|
||||
node => node.remove()
|
||||
);
|
||||
clone.querySelectorAll('['+attr+']').forEach(
|
||||
node => node.removeAttribute(attr)
|
||||
);
|
||||
return clone.innerHTML;
|
||||
}
|
||||
return '';
|
||||
return this.html() || plainToHtml(this.plain());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -573,4 +625,5 @@ export class MessageModel extends AbstractModel {
|
|||
this.isReadReceipt()
|
||||
].join(',');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
if (message) {
|
||||
if (hasNewMessageAndRemoveFromCache(message.folder, message.uid) && 5 >= newCount) {
|
||||
++newCount;
|
||||
message.newForAnimation(true);
|
||||
}
|
||||
|
||||
message.deleted(false);
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { isNonEmptyArray } 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 = isNonEmptyArray(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,23 +0,0 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
export class TemplateModel extends AbstractModel {
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {string} name
|
||||
* @param {string} body
|
||||
*/
|
||||
constructor(id = '', name = '', body = '') {
|
||||
super();
|
||||
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.body = body;
|
||||
this.populated = true;
|
||||
|
||||
this.deleteAccess = ko.observable(false);
|
||||
}
|
||||
|
||||
// static reviveFromJson(json) {}
|
||||
}
|
|
@ -1,41 +1,30 @@
|
|||
import { Notification } from 'Common/Enums';
|
||||
import { Settings } from 'Common/Globals';
|
||||
import { isArray, pInt, pString } from 'Common/Utils';
|
||||
import { serverRequest } from 'Common/Links';
|
||||
import { runHook } from 'Common/Plugins';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
let iJsonErrorCount = 0,
|
||||
iTokenErrorCount = 0;
|
||||
let iJsonErrorCount = 0;
|
||||
|
||||
const getURL = (add = '') => serverRequest('Json') + add,
|
||||
|
||||
updateToken = data => {
|
||||
if (data.UpdateToken) {
|
||||
rl.hash.set();
|
||||
Settings.set('AuthAccountHash', data.UpdateToken);
|
||||
}
|
||||
},
|
||||
|
||||
checkResponseError = data => {
|
||||
const err = data ? data.ErrorCode : null;
|
||||
if (Notification.InvalidToken === err && 10 < ++iTokenErrorCount) {
|
||||
if (Notification.InvalidToken === err) {
|
||||
alert(getNotification(err));
|
||||
rl.logoutReload();
|
||||
} else {
|
||||
if ([
|
||||
Notification.AuthError,
|
||||
Notification.ConnectionError,
|
||||
Notification.DomainNotAllowed,
|
||||
Notification.AccountNotAllowed,
|
||||
Notification.MailServerError,
|
||||
Notification.UnknownNotification,
|
||||
Notification.UnknownError
|
||||
].includes(err)
|
||||
) {
|
||||
++iJsonErrorCount;
|
||||
}
|
||||
if (data.ClearAuth || data.Logout || 7 < iJsonErrorCount) {
|
||||
rl.hash.clear();
|
||||
data.ClearAuth || rl.logoutReload();
|
||||
} else if ([
|
||||
Notification.AuthError,
|
||||
Notification.ConnectionError,
|
||||
Notification.DomainNotAllowed,
|
||||
Notification.AccountNotAllowed,
|
||||
Notification.MailServerError,
|
||||
Notification.UnknownNotification,
|
||||
Notification.UnknownError
|
||||
].includes(err)
|
||||
) {
|
||||
if (7 < ++iJsonErrorCount) {
|
||||
rl.logoutReload();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -57,7 +46,11 @@ abort = (sAction, bClearOnly) => {
|
|||
fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
|
||||
sGetAdd = pString(sGetAdd);
|
||||
params = params || {};
|
||||
params.Action = action;
|
||||
if (params instanceof FormData) {
|
||||
params.set('Action', action);
|
||||
} else {
|
||||
params.Action = action;
|
||||
}
|
||||
let init = {};
|
||||
if (window.AbortController) {
|
||||
abort(action);
|
||||
|
@ -69,6 +62,14 @@ fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
|
|||
return rl.fetchJSON(getURL(sGetAdd), init, sGetAdd ? null : params).then(jsonCallback);
|
||||
};
|
||||
|
||||
class FetchError extends Error
|
||||
{
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.code = code || Notification.JsonFalse;
|
||||
}
|
||||
}
|
||||
|
||||
export class AbstractFetchRemote
|
||||
{
|
||||
abort(sAction, bClearOnly) {
|
||||
|
@ -76,6 +77,45 @@ export class AbstractFetchRemote
|
|||
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 {string} sAction
|
||||
|
@ -84,7 +124,7 @@ export class AbstractFetchRemote
|
|||
* @param {string=} sGetAdd = ''
|
||||
* @param {Array=} aAbortActions = []
|
||||
*/
|
||||
defaultRequest(fCallback, sAction, params, iTimeout, sGetAdd, abortActions) {
|
||||
request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
|
||||
params = params || {};
|
||||
|
||||
const start = Date.now();
|
||||
|
@ -100,12 +140,8 @@ export class AbstractFetchRemote
|
|||
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
||||
data => {
|
||||
let cached = false;
|
||||
if (data) {
|
||||
if (data.Time) {
|
||||
cached = pInt(data.Time) > Date.now() - start;
|
||||
}
|
||||
|
||||
updateToken(data);
|
||||
if (data && data.Time) {
|
||||
cached = pInt(data.Time) > Date.now() - start;
|
||||
}
|
||||
|
||||
let iError = 0;
|
||||
|
@ -123,7 +159,7 @@ export class AbstractFetchRemote
|
|||
}
|
||||
*/
|
||||
if (data.Result) {
|
||||
iJsonErrorCount = iTokenErrorCount = 0;
|
||||
iJsonErrorCount = 0;
|
||||
} else {
|
||||
checkResponseError(data);
|
||||
iError = data.ErrorCode || Notification.UnknownError
|
||||
|
@ -153,32 +189,11 @@ export class AbstractFetchRemote
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
*/
|
||||
noop(fCallback) {
|
||||
this.defaultRequest(fCallback, 'Noop');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
*/
|
||||
getPublicKey(fCallback) {
|
||||
this.defaultRequest(fCallback, 'GetPublicKey');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
* @param {string} sVersion
|
||||
*/
|
||||
jsVersion(fCallback, sVersion) {
|
||||
this.defaultRequest(fCallback, 'Version', {
|
||||
Version: sVersion
|
||||
});
|
||||
}
|
||||
|
||||
fastResolve(mData) {
|
||||
return Promise.resolve(mData);
|
||||
this.request('GetPublicKey', fCallback);
|
||||
}
|
||||
|
||||
setTrigger(trigger, value) {
|
||||
|
@ -190,17 +205,15 @@ export class AbstractFetchRemote
|
|||
}
|
||||
}
|
||||
|
||||
postRequest(action, fTrigger, params, timeOut) {
|
||||
post(action, fTrigger, params, timeOut) {
|
||||
this.setTrigger(fTrigger, true);
|
||||
return fetchJSON(action, '', params, pInt(timeOut, 30000),
|
||||
data => {
|
||||
abort(action, true);
|
||||
|
||||
if (!data) {
|
||||
return Promise.reject(Notification.JsonParse);
|
||||
return Promise.reject(new FetchError(Notification.JsonParse));
|
||||
}
|
||||
|
||||
updateToken(data);
|
||||
/*
|
||||
let isCached = false, type = '';
|
||||
if (data && data.Time) {
|
||||
|
@ -223,8 +236,10 @@ export class AbstractFetchRemote
|
|||
|
||||
if (!data.Result || action !== data.Action) {
|
||||
checkResponseError(data);
|
||||
const err = data ? data.ErrorCode : 0;
|
||||
return Promise.reject(err || Notification.JsonFalse);
|
||||
return Promise.reject(new FetchError(
|
||||
data ? data.ErrorCode : 0,
|
||||
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
|
||||
));
|
||||
}
|
||||
|
||||
return data;
|
||||
|
|
|
@ -1,212 +1,14 @@
|
|||
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
|
||||
|
||||
class RemoteAdminFetch extends AbstractFetchRemote {
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
* @param {string} sLogin
|
||||
* @param {string} sPassword
|
||||
*/
|
||||
adminLogin(fCallback, sLogin, sPassword) {
|
||||
this.defaultRequest(fCallback, 'AdminLogin', {
|
||||
Login: sLogin,
|
||||
Password: sPassword
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {?scalar} value
|
||||
* @param {?Function} fCallback
|
||||
*/
|
||||
adminLogout(fCallback) {
|
||||
this.defaultRequest(fCallback, 'AdminLogout');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
pluginList(fCallback) {
|
||||
this.defaultRequest(fCallback, 'AdminPluginList');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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} sName
|
||||
*/
|
||||
plugin(fCallback, sName) {
|
||||
this.defaultRequest(fCallback, 'AdminPluginLoad', {
|
||||
Name: sName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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} sName
|
||||
* @param {boolean} bDisabled
|
||||
*/
|
||||
pluginDisable(fCallback, sName, bDisabled) {
|
||||
this.defaultRequest(fCallback, 'AdminPluginDisable', {
|
||||
Name: sName,
|
||||
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);
|
||||
saveSetting(key, value, fCallback) {
|
||||
this.request('AdminSettingsUpdate', fCallback, {[key]: value});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { isArray, isNonEmptyArray, pString, pInt } from 'Common/Utils';
|
||||
import { pString, pInt, b64EncodeJSONSafe } from 'Common/Utils';
|
||||
|
||||
import {
|
||||
getFolderHash,
|
||||
getFolderInboxName,
|
||||
getFolderUidNext,
|
||||
getFolderFromCacheList,
|
||||
MessageFlagsCache
|
||||
getFolderFromCacheList
|
||||
} from 'Common/Cache';
|
||||
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
|
@ -17,224 +15,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
|
|||
|
||||
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 {
|
||||
/**
|
||||
* @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 {string} sEmail
|
||||
* @param {string} sLogin
|
||||
* @param {string} sPassword
|
||||
* @param {boolean} bSignMe
|
||||
* @param {string=} sLanguage
|
||||
*/
|
||||
login(fCallback, sEmail, sPassword, bSignMe, sLanguage) {
|
||||
this.defaultRequest(fCallback, 'Login', {
|
||||
Email: sEmail,
|
||||
Login: '',
|
||||
Password: sPassword,
|
||||
Language: sLanguage || '',
|
||||
SignMe: bSignMe ? 1 : 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
templates(fCallback) {
|
||||
this.defaultRequest(fCallback, 'Templates', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} fCallback
|
||||
* @param {string} sID
|
||||
*/
|
||||
templateGetById(fCallback, sID) {
|
||||
this.defaultRequest(fCallback, 'TemplateGetByID', {
|
||||
ID: sID
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} fCallback
|
||||
* @param {string} sID
|
||||
*/
|
||||
templateDelete(fCallback, sID) {
|
||||
this.defaultRequest(fCallback, 'TemplateDelete', {
|
||||
IdToDelete: sID
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} fCallback
|
||||
* @param {string} sID
|
||||
* @param {string} sName
|
||||
* @param {string} sBody
|
||||
*/
|
||||
templateSetup(fCallback, sID, sName, sBody) {
|
||||
this.defaultRequest(fCallback, 'TemplateSetup', {
|
||||
ID: sID,
|
||||
Name: sName,
|
||||
Body: sBody
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function} fCallback
|
||||
|
@ -243,23 +24,23 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
*/
|
||||
messageList(fCallback, params, bSilent = false) {
|
||||
const
|
||||
sFolderFullNameRaw = pString(params.Folder),
|
||||
folderHash = getFolderHash(sFolderFullNameRaw),
|
||||
useThreads = AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
|
||||
inboxUidNext = getFolderInboxName() === sFolderFullNameRaw ? getFolderUidNext(sFolderFullNameRaw) : '';
|
||||
sFolderFullName = pString(params.Folder),
|
||||
folderHash = getFolderHash(sFolderFullName);
|
||||
|
||||
params.Folder = sFolderFullNameRaw;
|
||||
params.ThreadUid = useThreads ? params.ThreadUid : '';
|
||||
params = Object.assign({
|
||||
Folder: '',
|
||||
Offset: 0,
|
||||
Limit: SettingsUserStore.messagesPerPage(),
|
||||
Search: '',
|
||||
UidNext: inboxUidNext,
|
||||
UseThreads: useThreads,
|
||||
ThreadUid: '',
|
||||
Sort: FolderUserStore.sortMode()
|
||||
UidNext: getFolderUidNext(sFolderFullName), // Used to check for new messages
|
||||
Sort: FolderUserStore.sortMode(),
|
||||
Hash: folderHash + SettingsGet('AccountHash')
|
||||
}, params);
|
||||
params.Folder = sFolderFullName;
|
||||
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
|
||||
params.UseThreads = 1;
|
||||
} else {
|
||||
params.ThreadUid = 0;
|
||||
}
|
||||
|
||||
let sGetAdd = '';
|
||||
|
||||
|
@ -267,13 +48,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
sGetAdd = 'MessageList/' +
|
||||
SUB_QUERY_PREFIX +
|
||||
'/' +
|
||||
urlsafeArray([SettingsGet('ProjectHash'),folderHash].concat(Object.values(params)));
|
||||
b64EncodeJSONSafe(params);
|
||||
params = {};
|
||||
}
|
||||
|
||||
this.defaultRequest(
|
||||
this.request('MessageList',
|
||||
fCallback,
|
||||
'MessageList',
|
||||
params,
|
||||
30000,
|
||||
sGetAdd,
|
||||
|
@ -283,43 +63,27 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
* @param {Array} aDownloads
|
||||
*/
|
||||
messageUploadAttachments(fCallback, aDownloads) {
|
||||
this.defaultRequest(
|
||||
fCallback,
|
||||
'MessageUploadAttachments',
|
||||
{
|
||||
Attachments: aDownloads
|
||||
},
|
||||
999000
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
* @param {string} sFolderFullNameRaw
|
||||
* @param {string} sFolderFullName
|
||||
* @param {number} iUid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
message(fCallback, sFolderFullNameRaw, iUid) {
|
||||
sFolderFullNameRaw = pString(sFolderFullNameRaw);
|
||||
message(fCallback, sFolderFullName, iUid) {
|
||||
sFolderFullName = pString(sFolderFullName);
|
||||
iUid = pInt(iUid);
|
||||
|
||||
if (getFolderFromCacheList(sFolderFullNameRaw) && 0 < iUid) {
|
||||
this.defaultRequest(
|
||||
if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) {
|
||||
this.request('Message',
|
||||
fCallback,
|
||||
'Message',
|
||||
{},
|
||||
null,
|
||||
'Message/' +
|
||||
SUB_QUERY_PREFIX +
|
||||
'/' +
|
||||
urlsafeArray([
|
||||
sFolderFullNameRaw,
|
||||
b64EncodeJSONSafe([
|
||||
sFolderFullName,
|
||||
iUid,
|
||||
SettingsGet('ProjectHash'),
|
||||
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0
|
||||
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
|
||||
SettingsGet('AccountHash')
|
||||
]),
|
||||
['Message']
|
||||
);
|
||||
|
@ -330,186 +94,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
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 (isNonEmptyArray(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.join(',') : '',
|
||||
UidNext: getFolderInboxName() === folder ? getFolderUidNext(folder) : ''
|
||||
});
|
||||
} 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 {string} sMessageUid
|
||||
* @param {string} sReadReceipt
|
||||
* @param {string} sSubject
|
||||
* @param {string} sText
|
||||
*/
|
||||
sendReadReceiptMessage(fCallback, sMessageFolder, sMessageUid, sReadReceipt, sSubject, sText) {
|
||||
this.defaultRequest(fCallback, 'SendReadReceiptMessage', {
|
||||
MessageFolder: sMessageFolder,
|
||||
MessageUid: sMessageUid,
|
||||
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 {Object} oData
|
||||
*/
|
||||
saveSettings(fCallback, oData) {
|
||||
this.defaultRequest(fCallback, 'SettingsUpdate', oData);
|
||||
this.request('SettingsUpdate', fCallback, oData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -523,234 +113,15 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
* @param {string} sFolderFullNameRaw
|
||||
*/
|
||||
folderClear(fCallback, sFolderFullNameRaw) {
|
||||
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,
|
||||
/*
|
||||
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
|
||||
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
|
||||
Folder: sPrevFolderFullName,
|
||||
NewFolder: sNewFolderFullName,
|
||||
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();
|
||||
|
|
|
@ -2,7 +2,7 @@ import ko from 'ko';
|
|||
|
||||
import { pString } from 'Common/Utils';
|
||||
import { settings } from 'Common/Links';
|
||||
import { doc, elementById } from 'Common/Globals';
|
||||
import { createElement, elementById } from 'Common/Globals';
|
||||
|
||||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||
|
||||
|
@ -18,52 +18,38 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
this.menu = ko.observableArray();
|
||||
|
||||
this.oCurrentSubScreen = null;
|
||||
|
||||
this.setupSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function=} fCallback
|
||||
*/
|
||||
setupSettings(fCallback = null) {
|
||||
fCallback && fCallback();
|
||||
}
|
||||
|
||||
onRoute(subName) {
|
||||
let settingsScreen = null,
|
||||
RoutedSettingsViewModel = null,
|
||||
viewModelDom = null;
|
||||
|
||||
RoutedSettingsViewModel = VIEW_MODELS.find(
|
||||
SettingsViewModel =>
|
||||
SettingsViewModel && SettingsViewModel.__rlSettingsData && subName === SettingsViewModel.__rlSettingsData.Route
|
||||
);
|
||||
viewModelDom = null,
|
||||
RoutedSettingsViewModel = VIEW_MODELS.find(
|
||||
SettingsViewModel => subName === SettingsViewModel.__rlSettingsData.route
|
||||
);
|
||||
|
||||
if (RoutedSettingsViewModel) {
|
||||
if (RoutedSettingsViewModel.__builded && RoutedSettingsViewModel.__vm) {
|
||||
if (RoutedSettingsViewModel.__vm) {
|
||||
settingsScreen = RoutedSettingsViewModel.__vm;
|
||||
} else {
|
||||
const vmPlace = elementById('rl-settings-subscreen');
|
||||
if (vmPlace) {
|
||||
settingsScreen = new RoutedSettingsViewModel();
|
||||
|
||||
viewModelDom = Element.fromHTML('<div class="rl-settings-view-model" hidden=""></div>');
|
||||
viewModelDom = createElement('div',{
|
||||
id: 'V-Settings-' + RoutedSettingsViewModel.name.replace(/(User|Admin)Settings/,''),
|
||||
hidden: ''
|
||||
})
|
||||
vmPlace.append(viewModelDom);
|
||||
|
||||
settingsScreen = new RoutedSettingsViewModel();
|
||||
settingsScreen.viewModelDom = viewModelDom;
|
||||
|
||||
settingsScreen.__rlSettingsData = RoutedSettingsViewModel.__rlSettingsData;
|
||||
|
||||
RoutedSettingsViewModel.__dom = viewModelDom;
|
||||
RoutedSettingsViewModel.__builded = true;
|
||||
RoutedSettingsViewModel.__vm = settingsScreen;
|
||||
|
||||
const tmpl = { name: RoutedSettingsViewModel.__rlSettingsData.Template };
|
||||
ko.applyBindingAccessorsToNode(
|
||||
viewModelDom,
|
||||
{
|
||||
i18nInit: true,
|
||||
template: () => tmpl
|
||||
template: () => ({ name: RoutedSettingsViewModel.__rlSettingsData.template })
|
||||
},
|
||||
settingsScreen
|
||||
);
|
||||
|
@ -75,75 +61,55 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
}
|
||||
|
||||
if (settingsScreen) {
|
||||
const o = this;
|
||||
setTimeout(() => {
|
||||
// hide
|
||||
if (o.oCurrentSubScreen) {
|
||||
o.oCurrentSubScreen.onHide && o.oCurrentSubScreen.onHide();
|
||||
o.oCurrentSubScreen.viewModelDom.hidden = true;
|
||||
}
|
||||
this.onHide();
|
||||
// --
|
||||
|
||||
o.oCurrentSubScreen = settingsScreen;
|
||||
this.oCurrentSubScreen = settingsScreen;
|
||||
|
||||
// show
|
||||
if (o.oCurrentSubScreen) {
|
||||
o.oCurrentSubScreen.onBeforeShow && o.oCurrentSubScreen.onBeforeShow();
|
||||
o.oCurrentSubScreen.viewModelDom.hidden = false;
|
||||
o.oCurrentSubScreen.onShow && o.oCurrentSubScreen.onShow();
|
||||
settingsScreen.beforeShow && settingsScreen.beforeShow();
|
||||
settingsScreen.viewModelDom.hidden = false;
|
||||
settingsScreen.onShow && settingsScreen.onShow();
|
||||
|
||||
o.menu.forEach(item => {
|
||||
item.selected(
|
||||
settingsScreen &&
|
||||
settingsScreen.__rlSettingsData &&
|
||||
item.route === settingsScreen.__rlSettingsData.Route
|
||||
);
|
||||
});
|
||||
this.menu.forEach(item => {
|
||||
item.selected(
|
||||
item.route === RoutedSettingsViewModel.__rlSettingsData.route
|
||||
);
|
||||
});
|
||||
|
||||
doc.querySelector('#rl-content .b-settings .b-content').scrollTop = 0;
|
||||
}
|
||||
(elementById('rl-settings-subscreen') || {}).scrollTop = 0;
|
||||
// --
|
||||
}, 1);
|
||||
}
|
||||
} else {
|
||||
rl.route.setHash(settings(), false, true);
|
||||
hasher.replaceHash(settings());
|
||||
}
|
||||
}
|
||||
|
||||
onHide() {
|
||||
if (this.oCurrentSubScreen && this.oCurrentSubScreen.viewModelDom) {
|
||||
this.oCurrentSubScreen.onHide && this.oCurrentSubScreen.onHide();
|
||||
this.oCurrentSubScreen.viewModelDom.hidden = true;
|
||||
let subScreen = this.oCurrentSubScreen;
|
||||
if (subScreen) {
|
||||
subScreen.onHide && subScreen.onHide();
|
||||
subScreen.viewModelDom.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
onBuild() {
|
||||
VIEW_MODELS.forEach(SettingsViewModel => {
|
||||
if (
|
||||
SettingsViewModel &&
|
||||
SettingsViewModel.__rlSettingsData
|
||||
) {
|
||||
this.menu.push({
|
||||
route: SettingsViewModel.__rlSettingsData.Route,
|
||||
label: SettingsViewModel.__rlSettingsData.Label,
|
||||
selected: ko.observable(false),
|
||||
disabled: false
|
||||
});
|
||||
}
|
||||
});
|
||||
VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel.__rlSettingsData));
|
||||
}
|
||||
|
||||
routes() {
|
||||
const DefaultViewModel = VIEW_MODELS.find(
|
||||
SettingsViewModel =>
|
||||
SettingsViewModel && SettingsViewModel.__rlSettingsData && SettingsViewModel.__rlSettingsData.IsDefault
|
||||
SettingsViewModel => SettingsViewModel.__rlSettingsData.isDefault
|
||||
),
|
||||
defaultRoute =
|
||||
DefaultViewModel && DefaultViewModel.__rlSettingsData ? DefaultViewModel.__rlSettingsData.Route : 'general',
|
||||
DefaultViewModel ? DefaultViewModel.__rlSettingsData.route : 'general',
|
||||
rules = {
|
||||
subname: /^(.*)$/,
|
||||
normalize_: (rquest, vals) => {
|
||||
vals.subname = undefined === vals.subname ? defaultRoute : pString(vals.subname);
|
||||
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
|
||||
return [vals.subname];
|
||||
}
|
||||
};
|
||||
|
@ -165,11 +131,13 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
* @returns {void}
|
||||
*/
|
||||
export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
|
||||
let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
|
||||
SettingsViewModelClass.__rlSettingsData = {
|
||||
Label: labelName,
|
||||
Template: template,
|
||||
Route: route,
|
||||
IsDefault: !!isDefault
|
||||
label: labelName || 'SETTINGS_LABELS/LABEL_' + name.toUpperCase() + '_NAME',
|
||||
route: route || name.toLowerCase(),
|
||||
selected: ko.observable(false),
|
||||
template: template || SettingsViewModelClass.name,
|
||||
isDefault: !!isDefault
|
||||
};
|
||||
|
||||
VIEW_MODELS.push(SettingsViewModelClass);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||
|
||||
import { LoginAdminView } from 'View/Admin/Login';
|
||||
import { AdminLoginView } from 'View/Admin/Login';
|
||||
|
||||
export class LoginAdminScreen extends AbstractScreen {
|
||||
constructor() {
|
||||
super('login', [LoginAdminView]);
|
||||
super('login', [AdminLoginView]);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
|
|
|
@ -2,15 +2,15 @@ import { runSettingsViewModelHooks } from 'Common/Plugins';
|
|||
|
||||
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||
|
||||
import { GeneralAdminSettings } from 'Settings/Admin/General';
|
||||
import { DomainsAdminSettings } from 'Settings/Admin/Domains';
|
||||
import { LoginAdminSettings } from 'Settings/Admin/Login';
|
||||
import { ContactsAdminSettings } from 'Settings/Admin/Contacts';
|
||||
import { SecurityAdminSettings } from 'Settings/Admin/Security';
|
||||
import { PluginsAdminSettings } from 'Settings/Admin/Plugins';
|
||||
import { PackagesAdminSettings } from 'Settings/Admin/Packages';
|
||||
import { AboutAdminSettings } from 'Settings/Admin/About';
|
||||
import { BrandingAdminSettings } from 'Settings/Admin/Branding';
|
||||
import { AdminSettingsGeneral } from 'Settings/Admin/General';
|
||||
import { AdminSettingsDomains } from 'Settings/Admin/Domains';
|
||||
import { AdminSettingsLogin } from 'Settings/Admin/Login';
|
||||
import { AdminSettingsContacts } from 'Settings/Admin/Contacts';
|
||||
import { AdminSettingsSecurity } from 'Settings/Admin/Security';
|
||||
import { AdminSettingsPackages } from 'Settings/Admin/Packages';
|
||||
import { AdminSettingsAbout } from 'Settings/Admin/About';
|
||||
import { AdminSettingsBranding } from 'Settings/Admin/Branding';
|
||||
import { AdminSettingsConfig } from 'Settings/Admin/Config';
|
||||
|
||||
import { MenuSettingsAdminView } from 'View/Admin/Settings/Menu';
|
||||
import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
|
||||
|
@ -18,41 +18,22 @@ import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
|
|||
export class SettingsAdminScreen extends AbstractSettingsScreen {
|
||||
constructor() {
|
||||
super([MenuSettingsAdminView, PaneSettingsAdminView]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Function=} fCallback = null
|
||||
*/
|
||||
setupSettings(fCallback = null) {
|
||||
settingsAddViewModel(
|
||||
GeneralAdminSettings,
|
||||
'AdminSettingsGeneral',
|
||||
'TABS_LABELS/LABEL_GENERAL_NAME',
|
||||
'general',
|
||||
true
|
||||
);
|
||||
|
||||
[
|
||||
[DomainsAdminSettings, 'Domains'],
|
||||
[LoginAdminSettings, 'Login'],
|
||||
[BrandingAdminSettings, 'Branding'],
|
||||
[ContactsAdminSettings, 'Contacts'],
|
||||
[SecurityAdminSettings, 'Security'],
|
||||
[PluginsAdminSettings, 'Plugins'],
|
||||
[PackagesAdminSettings, 'Packages'],
|
||||
[AboutAdminSettings, 'About'],
|
||||
].forEach(item =>
|
||||
settingsAddViewModel(
|
||||
item[0],
|
||||
'AdminSettings'+item[1],
|
||||
'TABS_LABELS/LABEL_'+item[1].toUpperCase()+'_NAME',
|
||||
item[1].toLowerCase()
|
||||
)
|
||||
AdminSettingsGeneral,
|
||||
AdminSettingsDomains,
|
||||
AdminSettingsLogin,
|
||||
AdminSettingsBranding,
|
||||
AdminSettingsContacts,
|
||||
AdminSettingsSecurity,
|
||||
AdminSettingsPackages,
|
||||
AdminSettingsConfig,
|
||||
AdminSettingsAbout
|
||||
].forEach((item, index) =>
|
||||
settingsAddViewModel(item, 0, 0, 0, 0 === index)
|
||||
);
|
||||
|
||||
runSettingsViewModelHooks(true);
|
||||
|
||||
fCallback && fCallback();
|
||||
}
|
||||
|
||||
onShow() {
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
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 { getFolderFromCacheList, getFolderFullNameRaw, getFolderInboxName } from 'Common/Cache';
|
||||
import { setLayoutResizer } from 'Common/UtilsUser';
|
||||
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
|
||||
import { i18n } from 'Common/Translator';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
import { SystemDropDownMailBoxUserView } from 'View/User/MailBox/SystemDropDown';
|
||||
import { FolderListMailBoxUserView } from 'View/User/MailBox/FolderList';
|
||||
import { MessageListMailBoxUserView } from 'View/User/MailBox/MessageList';
|
||||
import { MessageViewMailBoxUserView } from 'View/User/MailBox/MessageView';
|
||||
|
||||
import { warmUpScreenPopup } from 'Knoin/Knoin';
|
||||
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
|
||||
import { MailFolderList } from 'View/User/MailBox/FolderList';
|
||||
import { MailMessageList } from 'View/User/MailBox/MessageList';
|
||||
import { MailMessageView } from 'View/User/MailBox/MessageView';
|
||||
|
||||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||
|
||||
import { ComposePopupView } from 'View/Popup/Compose';
|
||||
|
||||
export class MailBoxUserScreen extends AbstractScreen {
|
||||
constructor() {
|
||||
super('mailbox', [
|
||||
SystemDropDownMailBoxUserView,
|
||||
FolderListMailBoxUserView,
|
||||
MessageListMailBoxUserView,
|
||||
MessageViewMailBoxUserView
|
||||
SystemDropDownUserView,
|
||||
MailFolderList,
|
||||
MailMessageList,
|
||||
MailMessageView
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -52,7 +51,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
onShow() {
|
||||
this.updateWindowTitle();
|
||||
|
||||
AppUserStore.focusedState(Scope.None);
|
||||
AppUserStore.focusedState('none');
|
||||
AppUserStore.focusedState(Scope.MessageList);
|
||||
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
|
@ -65,20 +64,17 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
* @returns {void}
|
||||
*/
|
||||
onRoute(folderHash, page, search) {
|
||||
const folder = getFolderFromCacheList(getFolderFullNameRaw(folderHash.replace(/~([\d]+)$/, '')));
|
||||
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
|
||||
if (folder) {
|
||||
let threadUid = folderHash.replace(/^.+~([\d]+)$/, '$1');
|
||||
if (folderHash === threadUid) {
|
||||
threadUid = '';
|
||||
}
|
||||
let threadUid = folderHash.replace(/^.+~(\d+)$/, '$1');
|
||||
|
||||
FolderUserStore.currentFolder(folder);
|
||||
|
||||
MessageUserStore.listPage(1 > page ? 1 : page);
|
||||
MessageUserStore.listSearch(search);
|
||||
MessageUserStore.listThreadUid(threadUid);
|
||||
MessagelistUserStore.page(1 > page ? 1 : page);
|
||||
MessagelistUserStore.listSearch(search);
|
||||
MessagelistUserStore.threadUid((folderHash === threadUid) ? 0 : pInt(threadUid));
|
||||
|
||||
rl.app.reloadMessageList();
|
||||
MessagelistUserStore.reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,28 +82,41 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
* @returns {void}
|
||||
*/
|
||||
onStart() {
|
||||
if (!this.__started) {
|
||||
super.onStart();
|
||||
setTimeout(() => warmUpScreenPopup(ComposePopupView), 500);
|
||||
super.onStart();
|
||||
|
||||
addEventListener('mailbox.inbox-unread-count', e => {
|
||||
FolderUserStore.foldersInboxUnreadCount(e.detail);
|
||||
|
||||
const email = AccountUserStore.email();
|
||||
AccountUserStore.accounts.forEach(item =>
|
||||
item && email === item.email && item.count(e.detail)
|
||||
);
|
||||
|
||||
this.updateWindowTitle();
|
||||
});
|
||||
}
|
||||
addEventListener('mailbox.inbox-unread-count', e => {
|
||||
FolderUserStore.foldersInboxUnreadCount(e.detail);
|
||||
/* // Disabled in SystemDropDown.html
|
||||
const email = AccountUserStore.email();
|
||||
AccountUserStore.accounts.forEach(item =>
|
||||
item && email === item.email && item.count(e.detail)
|
||||
);
|
||||
*/
|
||||
this.updateWindowTitle();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
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 =>
|
||||
event.target.closest('#rl-right') && moveAction(false)
|
||||
|
@ -115,19 +124,23 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
}
|
||||
|
||||
/**
|
||||
* Parse link as generated by mailBox()
|
||||
* @returns {Array}
|
||||
*/
|
||||
routes() {
|
||||
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]))];
|
||||
|
||||
return [
|
||||
// Folder: INBOX | INBOX.sub | Sent | fullNameHash
|
||||
[/^([^/]*)$/, { 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]))]
|
||||
}],
|
||||
[/^([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 { keyScope, leftPanelDisabled, Settings } from 'Common/Globals';
|
||||
import { Scope } from 'Common/Enums';
|
||||
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
|
||||
import { runSettingsViewModelHooks } from 'Common/Plugins';
|
||||
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||
|
||||
|
@ -9,23 +9,53 @@ import { ThemeStore } from 'Stores/Theme';
|
|||
|
||||
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||
|
||||
import { GeneralUserSettings } from 'Settings/User/General';
|
||||
import { ContactsUserSettings } from 'Settings/User/Contacts';
|
||||
import { AccountsUserSettings } from 'Settings/User/Accounts';
|
||||
import { FiltersUserSettings } from 'Settings/User/Filters';
|
||||
import { SecurityUserSettings } from 'Settings/User/Security';
|
||||
import { TemplatesUserSettings } from 'Settings/User/Templates';
|
||||
import { FoldersUserSettings } from 'Settings/User/Folders';
|
||||
import { ThemesUserSettings } from 'Settings/User/Themes';
|
||||
import { OpenPgpUserSettings } from 'Settings/User/OpenPgp';
|
||||
import { UserSettingsGeneral } from 'Settings/User/General';
|
||||
import { UserSettingsContacts } from 'Settings/User/Contacts';
|
||||
import { UserSettingsAccounts } from 'Settings/User/Accounts';
|
||||
import { UserSettingsFilters } from 'Settings/User/Filters';
|
||||
import { UserSettingsSecurity } from 'Settings/User/Security';
|
||||
import { UserSettingsFolders } from 'Settings/User/Folders';
|
||||
import { UserSettingsThemes } from 'Settings/User/Themes';
|
||||
|
||||
import { SystemDropDownSettingsUserView } from 'View/User/Settings/SystemDropDown';
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
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(
|
||||
() => this.sSettingsTitle = i18n('TITLES/SETTINGS'),
|
||||
|
@ -33,67 +63,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() {
|
||||
this.setSettingsTitle();
|
||||
keyScope(Scope.Settings);
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import ko from 'ko';
|
||||
import { Settings } from 'Common/Globals';
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
export class AboutAdminSettings {
|
||||
export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
||||
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';
|
||||
import { settingsSaveHelperSimpleFunction } from 'Common/Utils';
|
||||
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
export class BrandingAdminSettings {
|
||||
export class AdminSettingsBranding extends AbstractViewSettings {
|
||||
constructor() {
|
||||
this.title = ko.observable(SettingsGet('Title')).idleTrigger();
|
||||
this.loadingDesc = ko.observable(SettingsGet('LoadingDescription')).idleTrigger();
|
||||
this.faviconUrl = ko.observable(SettingsGet('FaviconUrl')).idleTrigger();
|
||||
|
||||
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()
|
||||
})
|
||||
);
|
||||
super();
|
||||
this.addSetting('Title');
|
||||
this.addSetting('LoadingDescription');
|
||||
this.addSetting('FaviconUrl');
|
||||
}
|
||||
}
|
||||
|
|
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 { SaveSettingsStep } from 'Common/Enums';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import {
|
||||
settingsSaveHelperSimpleFunction,
|
||||
defaultOptionsAfterRender,
|
||||
addObservablesTo,
|
||||
addSubscribablesTo
|
||||
} from 'Common/Utils';
|
||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
import { decorateKoCommands } from 'Knoin/Knoin';
|
||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||
|
||||
export class ContactsAdminSettings {
|
||||
export class AdminSettingsContacts extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
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, {
|
||||
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,
|
||||
testContactsSuccess: false,
|
||||
testContactsError: false,
|
||||
|
@ -54,61 +49,23 @@ export class ContactsAdminSettings {
|
|||
|
||||
this.mainContactsType = ko
|
||||
.computed({
|
||||
read: this.contactsType,
|
||||
read: this.contactsPdoType,
|
||||
write: value => {
|
||||
if (value !== this.contactsType()) {
|
||||
if (value !== this.contactsPdoType()) {
|
||||
if (supportedTypes.includes(value)) {
|
||||
this.contactsType(value);
|
||||
this.contactsPdoType(value);
|
||||
} else if (types.length) {
|
||||
this.contactsType('');
|
||||
this.contactsPdoType('');
|
||||
}
|
||||
} else {
|
||||
this.contactsType.valueHasMutated();
|
||||
this.contactsPdoType.valueHasMutated();
|
||||
}
|
||||
}
|
||||
})
|
||||
.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()
|
||||
})
|
||||
})
|
||||
|
||||
this.onTestContactsResponse = this.onTestContactsResponse.bind(this);
|
||||
|
||||
decorateKoCommands(this, {
|
||||
testContactsCommand: self => self.pdoDsn() && self.pdoUser()
|
||||
testContactsCommand: self => self.contactsPdoDsn() && self.contactsPdoUser()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -118,31 +75,31 @@ export class ContactsAdminSettings {
|
|||
this.testContactsErrorMessage('');
|
||||
this.testing(true);
|
||||
|
||||
Remote.testContacts(this.onTestContactsResponse, {
|
||||
ContactsPdoType: this.contactsType(),
|
||||
ContactsPdoDsn: this.pdoDsn(),
|
||||
ContactsPdoUser: this.pdoUser(),
|
||||
ContactsPdoPassword: this.pdoPassword()
|
||||
});
|
||||
}
|
||||
|
||||
onTestContactsResponse(iError, data) {
|
||||
this.testContactsSuccess(false);
|
||||
this.testContactsError(false);
|
||||
this.testContactsErrorMessage('');
|
||||
|
||||
if (!iError && data.Result.Result) {
|
||||
this.testContactsSuccess(true);
|
||||
} else {
|
||||
this.testContactsError(true);
|
||||
if (data && data.Result) {
|
||||
this.testContactsErrorMessage(data.Result.Message || '');
|
||||
} else {
|
||||
Remote.request('AdminContactsTest',
|
||||
(iError, data) => {
|
||||
this.testContactsSuccess(false);
|
||||
this.testContactsError(false);
|
||||
this.testContactsErrorMessage('');
|
||||
}
|
||||
}
|
||||
|
||||
this.testing(false);
|
||||
if (!iError && data.Result.Result) {
|
||||
this.testContactsSuccess(true);
|
||||
} else {
|
||||
this.testContactsError(true);
|
||||
if (data && data.Result) {
|
||||
this.testContactsErrorMessage(data.Result.Message || '');
|
||||
} else {
|
||||
this.testContactsErrorMessage('');
|
||||
}
|
||||
}
|
||||
|
||||
this.testing(false);
|
||||
}, {
|
||||
ContactsPdoType: this.contactsPdoType(),
|
||||
ContactsPdoDsn: this.contactsPdoDsn(),
|
||||
ContactsPdoUser: this.contactsPdoUser(),
|
||||
ContactsPdoPassword: this.contactsPdoPassword()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
|
|
|
@ -8,14 +8,11 @@ import Remote from 'Remote/Admin/Fetch';
|
|||
import { DomainPopupView } from 'View/Popup/Domain';
|
||||
import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
|
||||
|
||||
export class DomainsAdminSettings {
|
||||
export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
||||
constructor() {
|
||||
this.domains = DomainAdminStore;
|
||||
|
||||
this.domainForDeletion = ko.observable(null).deleteAccessHelper();
|
||||
|
||||
this.onDomainListChangeRequest = this.onDomainListChangeRequest.bind(this);
|
||||
this.onDomainLoadRequest = this.onDomainLoadRequest.bind(this);
|
||||
this.domainForDeletion = ko.observable(null).askDeleteHelper();
|
||||
}
|
||||
|
||||
createDomain() {
|
||||
|
@ -28,30 +25,32 @@ export class DomainsAdminSettings {
|
|||
|
||||
deleteDomain(domain) {
|
||||
DomainAdminStore.remove(domain);
|
||||
Remote.domainDelete(this.onDomainListChangeRequest, domain.name);
|
||||
Remote.domainDelete(DomainAdminStore.fetch, domain.name);
|
||||
Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
|
||||
Name: domain.name
|
||||
});
|
||||
}
|
||||
|
||||
disableDomain(domain) {
|
||||
domain.disabled(!domain.disabled());
|
||||
Remote.domainDisable(this.onDomainListChangeRequest, domain.name, domain.disabled());
|
||||
Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
|
||||
Name: domain.name,
|
||||
Disabled: domain.disabled() ? 1 : 0
|
||||
});
|
||||
}
|
||||
|
||||
onBuild(oDom) {
|
||||
oDom.addEventListener('click', event => {
|
||||
let el = event.target.closestWithin('.b-admin-domains-list-table .e-item .e-action', oDom);
|
||||
el && ko.dataFor(el) && Remote.domain(this.onDomainLoadRequest, ko.dataFor(el).name);
|
||||
let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom);
|
||||
el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
|
||||
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
|
||||
{
|
||||
Name: ko.dataFor(el).name
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
DomainAdminStore.fetch();
|
||||
}
|
||||
|
||||
onDomainLoadRequest(iError, oData) {
|
||||
if (!iError) {
|
||||
showScreenPopup(DomainPopupView, [oData.Result]);
|
||||
}
|
||||
}
|
||||
|
||||
onDomainListChangeRequest() {
|
||||
DomainAdminStore.fetch();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,28 +2,29 @@ import ko from 'ko';
|
|||
|
||||
import {
|
||||
isArray,
|
||||
pInt,
|
||||
settingsSaveHelperSimpleFunction,
|
||||
changeTheme,
|
||||
convertThemeName,
|
||||
addObservablesTo,
|
||||
addSubscribablesTo
|
||||
convertThemeName
|
||||
} from 'Common/Utils';
|
||||
|
||||
import { Capa, SaveSettingsStep } from 'Common/Enums';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { reload as translatorReload, convertLangName } from 'Common/Translator';
|
||||
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||
|
||||
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 Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
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() {
|
||||
super();
|
||||
|
||||
this.language = LanguageStore.language;
|
||||
this.languages = LanguageStore.languages;
|
||||
|
||||
|
@ -36,18 +37,16 @@ export class GeneralAdminSettings {
|
|||
this.theme = ThemeStore.theme;
|
||||
this.themes = ThemeStore.themes;
|
||||
|
||||
this.addSettings(['AllowLanguagesOnSettings','NewMoveToFolder']);
|
||||
|
||||
addObservablesTo(this, {
|
||||
allowLanguagesOnSettings: !!SettingsGet('AllowLanguagesOnSettings'),
|
||||
newMoveToFolder: !!SettingsGet('NewMoveToFolder'),
|
||||
attachmentLimitTrigger: SaveSettingsStep.Idle,
|
||||
languageTrigger: SaveSettingsStep.Idle,
|
||||
themeTrigger: SaveSettingsStep.Idle,
|
||||
capaThemes: Settings.capa(Capa.Themes),
|
||||
capaUserBackground: Settings.capa(Capa.UserBackground),
|
||||
capaAdditionalAccounts: Settings.capa(Capa.AdditionalAccounts),
|
||||
capaIdentities: Settings.capa(Capa.Identities),
|
||||
capaAttachmentThumbnails: Settings.capa(Capa.AttachmentThumbnails),
|
||||
capaTemplates: Settings.capa(Capa.Templates),
|
||||
capaThemes: SettingsCapa('Themes'),
|
||||
capaUserBackground: SettingsCapa('UserBackground'),
|
||||
capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
|
||||
capaIdentities: SettingsCapa('Identities'),
|
||||
capaAttachmentThumbnails: SettingsCapa('AttachmentThumbnails'),
|
||||
dataFolderAccess: false
|
||||
});
|
||||
|
||||
|
@ -59,10 +58,14 @@ export class GeneralAdminSettings {
|
|||
}
|
||||
*/
|
||||
|
||||
this.mainAttachmentLimit = ko
|
||||
.observable(pInt(SettingsGet('AttachmentLimit')) / (1024 * 1024))
|
||||
this.attachmentLimit = ko
|
||||
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
|
||||
.extend({ debounce: 500 });
|
||||
|
||||
this.addSetting('Language');
|
||||
this.addSetting('AttachmentLimit');
|
||||
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
|
||||
|
||||
this.uploadData = SettingsGet('PhpUploadSizes');
|
||||
this.uploadDataDesc =
|
||||
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size)
|
||||
|
@ -74,12 +77,12 @@ export class GeneralAdminSettings {
|
|||
].join('')
|
||||
: '';
|
||||
|
||||
this.themesOptions = ko.computed(() =>
|
||||
this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) }))
|
||||
);
|
||||
addComputablesTo(this, {
|
||||
themesOptions: () => this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) })),
|
||||
|
||||
this.languageFullName = ko.computed(() => convertLangName(this.language()));
|
||||
this.languageAdminFullName = ko.computed(() => convertLangName(this.languageAdmin()));
|
||||
languageFullName: () => convertLangName(this.language()),
|
||||
languageAdminFullName: () => convertLangName(this.languageAdmin())
|
||||
});
|
||||
|
||||
this.languageAdminTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
|
||||
|
||||
|
@ -87,55 +90,25 @@ export class GeneralAdminSettings {
|
|||
this.languageAdminTrigger(saveSettingsStep);
|
||||
setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000);
|
||||
},
|
||||
fSaveBoolHelper = (key, fn) =>
|
||||
value => {
|
||||
const data = {};
|
||||
data[key] = value ? 1 : 0;
|
||||
Remote.saveAdminConfig(fn, data);
|
||||
};
|
||||
fSaveHelper = key => value => Remote.saveSetting(key, value);
|
||||
|
||||
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 => {
|
||||
this.languageAdminTrigger(SaveSettingsStep.Animate);
|
||||
translatorReload(true, value)
|
||||
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
|
||||
.then(() => Remote.saveAdminConfig(null, {
|
||||
LanguageAdmin: value.trim()
|
||||
}));
|
||||
.then(() => Remote.saveSetting('LanguageAdmin', value));
|
||||
},
|
||||
|
||||
theme: value => {
|
||||
changeTheme(value, this.themeTrigger);
|
||||
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.themeTrigger, this), {
|
||||
Theme: value.trim()
|
||||
});
|
||||
},
|
||||
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
|
||||
|
||||
capaAdditionalAccounts: fSaveBoolHelper('CapaAdditionalAccounts'),
|
||||
capaIdentities: fSaveHelper('CapaIdentities'),
|
||||
|
||||
capaIdentities: fSaveBoolHelper('CapaIdentities'),
|
||||
capaAttachmentThumbnails: fSaveHelper('CapaAttachmentThumbnails'),
|
||||
|
||||
capaTemplates: fSaveBoolHelper('CapaTemplates'),
|
||||
capaThemes: fSaveHelper('CapaThemes'),
|
||||
|
||||
capaAttachmentThumbnails: fSaveBoolHelper('CapaAttachmentThumbnails'),
|
||||
|
||||
capaThemes: fSaveBoolHelper('CapaThemes'),
|
||||
|
||||
capaUserBackground: fSaveBoolHelper('CapaUserBackground'),
|
||||
|
||||
allowLanguagesOnSettings: fSaveBoolHelper('AllowLanguagesOnSettings'),
|
||||
|
||||
newMoveToFolder: fSaveBoolHelper('NewMoveToFolder')
|
||||
capaUserBackground: fSaveHelper('CapaUserBackground')
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,46 +1,9 @@
|
|||
import ko from 'ko';
|
||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
|
||||
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
export class LoginAdminSettings {
|
||||
export class AdminSettingsLogin extends AbstractViewSettings {
|
||||
constructor() {
|
||||
addObservablesTo(this, {
|
||||
determineUserLanguage: !!SettingsGet('DetermineUserLanguage'),
|
||||
determineUserDomain: !!SettingsGet('DetermineUserDomain'),
|
||||
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()
|
||||
})
|
||||
});
|
||||
super();
|
||||
this.addSetting('LoginDefaultDomain');
|
||||
this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin','hideSubmitButton']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,42 +6,58 @@ import { getNotification } from 'Common/Translator';
|
|||
import { PackageAdminStore } from 'Stores/Admin/Package';
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
export class PackagesAdminSettings {
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
import { PluginPopupView } from 'View/Popup/Plugin';
|
||||
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||
|
||||
export class AdminSettingsPackages extends AbstractViewSettings {
|
||||
constructor() {
|
||||
this.packagesError = ko.observable('');
|
||||
super();
|
||||
|
||||
this.addSettings(['EnabledPlugins']);
|
||||
|
||||
addObservablesTo(this, {
|
||||
packagesError: ''
|
||||
});
|
||||
|
||||
this.packages = PackageAdminStore;
|
||||
|
||||
this.packagesCurrent = ko.computed(() =>
|
||||
PackageAdminStore.filter(item => item && item.installed && !item.compare)
|
||||
);
|
||||
this.packagesAvailableForUpdate = ko.computed(() =>
|
||||
PackageAdminStore.filter(item => item && item.installed && !!item.compare)
|
||||
);
|
||||
this.packagesAvailableForInstallation = ko.computed(() =>
|
||||
PackageAdminStore.filter(item => item && !item.installed)
|
||||
);
|
||||
addComputablesTo(this, {
|
||||
packagesCurrent: () => PackageAdminStore().filter(item => item && item.installed && !item.canBeUpdated),
|
||||
packagesUpdate: () => PackageAdminStore().filter(item => item && item.installed && item.canBeUpdated),
|
||||
packagesAvailable: () => PackageAdminStore().filter(item => item && !item.installed),
|
||||
|
||||
this.visibility = ko.computed(() => (PackageAdminStore.loading() ? 'visible' : 'hidden'));
|
||||
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
|
||||
});
|
||||
}
|
||||
|
||||
onShow() {
|
||||
this.packagesError('');
|
||||
}
|
||||
|
||||
onBuild() {
|
||||
onBuild(oDom) {
|
||||
PackageAdminStore.fetch();
|
||||
|
||||
oDom.addEventListener('click', event => {
|
||||
// configurePlugin
|
||||
let el = event.target.closestWithin('.package-configure', oDom),
|
||||
data = el ? ko.dataFor(el) : 0;
|
||||
data && Remote.request('AdminPluginLoad',
|
||||
(iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
|
||||
{
|
||||
Id: data.id
|
||||
}
|
||||
);
|
||||
// disablePlugin
|
||||
el = event.target.closestWithin('.package-active', oDom);
|
||||
data = el ? ko.dataFor(el) : 0;
|
||||
data && this.disablePlugin(data);
|
||||
});
|
||||
}
|
||||
|
||||
requestHelper(packageToRequest, install) {
|
||||
return (iError, data) => {
|
||||
if (iError) {
|
||||
this.packagesError(
|
||||
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
|
||||
// ':\n' + getNotification(iError);
|
||||
);
|
||||
}
|
||||
|
||||
PackageAdminStore.forEach(item => {
|
||||
if (item && packageToRequest && item.loading && item.loading() && packageToRequest.file === item.file) {
|
||||
packageToRequest.loading(false);
|
||||
|
@ -49,7 +65,12 @@ export class PackagesAdminSettings {
|
|||
}
|
||||
});
|
||||
|
||||
if (!iError && data.Result.Reload) {
|
||||
if (iError) {
|
||||
this.packagesError(
|
||||
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
|
||||
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
|
||||
);
|
||||
} else if (data.Result.Reload) {
|
||||
location.reload();
|
||||
} else {
|
||||
PackageAdminStore.fetch();
|
||||
|
@ -60,14 +81,49 @@ export class PackagesAdminSettings {
|
|||
deletePackage(packageToDelete) {
|
||||
if (packageToDelete) {
|
||||
packageToDelete.loading(true);
|
||||
Remote.packageDelete(this.requestHelper(packageToDelete, false), packageToDelete);
|
||||
Remote.request('AdminPackageDelete',
|
||||
this.requestHelper(packageToDelete, false),
|
||||
{
|
||||
Id: packageToDelete.id
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
installPackage(packageToInstall) {
|
||||
if (packageToInstall) {
|
||||
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) {
|
||||
let disable = plugin.enabled();
|
||||
plugin.enabled(!disable);
|
||||
Remote.request('AdminPluginDisable',
|
||||
(iError, data) => {
|
||||
if (iError) {
|
||||
plugin.enabled(disable);
|
||||
this.packagesError(
|
||||
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
|
||||
? data.ErrorMessage
|
||||
: getNotification(iError)
|
||||
);
|
||||
}
|
||||
// PackageAdminStore.fetch();
|
||||
}, {
|
||||
Id: plugin.id,
|
||||
Disabled: disable ? 1 : 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
|
||||
import { PluginAdminStore } from 'Stores/Admin/Plugin';
|
||||
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
import { PluginPopupView } from 'View/Popup/Plugin';
|
||||
|
||||
export class PluginsAdminSettings {
|
||||
constructor() {
|
||||
this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
|
||||
|
||||
this.plugins = PluginAdminStore;
|
||||
this.pluginsError = PluginAdminStore.error;
|
||||
|
||||
this.onPluginLoadRequest = this.onPluginLoadRequest.bind(this);
|
||||
this.onPluginDisableRequest = this.onPluginDisableRequest.bind(this);
|
||||
|
||||
this.enabledPlugins.subscribe(value =>
|
||||
Remote.saveAdminConfig(null, {
|
||||
EnabledPlugins: value ? 1 : 0
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
disablePlugin(plugin) {
|
||||
plugin.disabled(!plugin.disabled());
|
||||
Remote.pluginDisable(this.onPluginDisableRequest, plugin.name, plugin.disabled());
|
||||
}
|
||||
|
||||
configurePlugin(plugin) {
|
||||
Remote.plugin(this.onPluginLoadRequest, plugin.name);
|
||||
}
|
||||
|
||||
onBuild(oDom) {
|
||||
oDom.addEventListener('click', event => {
|
||||
let el = event.target.closestWithin('.e-item .configure-plugin-action', oDom);
|
||||
el && ko.dataFor(el) && this.configurePlugin(ko.dataFor(el));
|
||||
|
||||
el = event.target.closestWithin('.e-item .disabled-plugin', oDom);
|
||||
el && ko.dataFor(el) && this.disablePlugin(ko.dataFor(el));
|
||||
});
|
||||
}
|
||||
|
||||
onShow() {
|
||||
PluginAdminStore.error('');
|
||||
PluginAdminStore.fetch();
|
||||
}
|
||||
|
||||
onPluginLoadRequest(iError, data) {
|
||||
if (!iError) {
|
||||
showScreenPopup(PluginPopupView, [data.Result]);
|
||||
}
|
||||
}
|
||||
|
||||
onPluginDisableRequest(iError, data) {
|
||||
if (iError) {
|
||||
if (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) {
|
||||
PluginAdminStore.error(data.ErrorMessage);
|
||||
} else {
|
||||
PluginAdminStore.error(getNotification(iError));
|
||||
}
|
||||
}
|
||||
|
||||
PluginAdminStore.fetch();
|
||||
}
|
||||
}
|
|
@ -1,34 +1,40 @@
|
|||
import { Capa } from 'Common/Enums';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { addObservablesTo, addSubscribablesTo } from 'Common/Utils';
|
||||
import { SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||
import { addObservablesTo, addSubscribablesTo } from 'External/ko';
|
||||
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
import { decorateKoCommands } from 'Knoin/Knoin';
|
||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||
|
||||
export class SecurityAdminSettings {
|
||||
export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
|
||||
|
||||
this.weakPassword = rl.app.weakPassword;
|
||||
|
||||
addObservablesTo(this, {
|
||||
useLocalProxyForExternalImages: !!SettingsGet('UseLocalProxyForExternalImages'),
|
||||
|
||||
verifySslCertificate: !!SettingsGet('VerifySslCertificate'),
|
||||
allowSelfSigned: !!SettingsGet('AllowSelfSigned'),
|
||||
|
||||
adminLogin: SettingsGet('AdminLogin'),
|
||||
adminLoginError: false,
|
||||
adminPassword: '',
|
||||
adminPasswordNew: '',
|
||||
adminPasswordNew2: '',
|
||||
adminPasswordNewError: false,
|
||||
adminTOTP: SettingsGet('AdminTOTP'),
|
||||
|
||||
adminPasswordUpdateError: false,
|
||||
adminPasswordUpdateSuccess: false,
|
||||
|
||||
capaOpenPGP: Settings.capa(Capa.OpenPGP)
|
||||
capaOpenPGP: SettingsCapa('OpenPGP')
|
||||
});
|
||||
|
||||
const reset = () => {
|
||||
this.adminPasswordUpdateError(false);
|
||||
this.adminPasswordUpdateSuccess(false);
|
||||
this.adminPasswordNewError(false);
|
||||
};
|
||||
|
||||
addSubscribablesTo(this, {
|
||||
adminPassword: () => {
|
||||
this.adminPasswordUpdateError(false);
|
||||
|
@ -37,43 +43,13 @@ export class SecurityAdminSettings {
|
|||
|
||||
adminLogin: () => this.adminLoginError(false),
|
||||
|
||||
adminPasswordNew: () => {
|
||||
this.adminPasswordUpdateError(false);
|
||||
this.adminPasswordUpdateSuccess(false);
|
||||
this.adminPasswordNewError(false);
|
||||
},
|
||||
adminPasswordNew: reset,
|
||||
|
||||
adminPasswordNew2: () => {
|
||||
this.adminPasswordUpdateError(false);
|
||||
this.adminPasswordUpdateSuccess(false);
|
||||
this.adminPasswordNewError(false);
|
||||
},
|
||||
adminPasswordNew2: reset,
|
||||
|
||||
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
|
||||
})
|
||||
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
||||
});
|
||||
|
||||
this.onNewAdminPasswordResponse = this.onNewAdminPasswordResponse.bind(this);
|
||||
|
||||
decorateKoCommands(this, {
|
||||
saveNewAdminPasswordCommand: self => self.adminLogin().trim() && self.adminPassword()
|
||||
});
|
||||
|
@ -93,29 +69,28 @@ export class SecurityAdminSettings {
|
|||
this.adminPasswordUpdateError(false);
|
||||
this.adminPasswordUpdateSuccess(false);
|
||||
|
||||
Remote.saveNewAdminPassword(this.onNewAdminPasswordResponse, {
|
||||
Remote.request('AdminPasswordUpdate', (iError, data) => {
|
||||
if (iError) {
|
||||
this.adminPasswordUpdateError(true);
|
||||
} else {
|
||||
this.adminPassword('');
|
||||
this.adminPasswordNew('');
|
||||
this.adminPasswordNew2('');
|
||||
|
||||
this.adminPasswordUpdateSuccess(true);
|
||||
|
||||
this.weakPassword(!!data.Result.Weak);
|
||||
}
|
||||
}, {
|
||||
'Login': this.adminLogin(),
|
||||
'Password': this.adminPassword(),
|
||||
'NewPassword': this.adminPasswordNew()
|
||||
'NewPassword': this.adminPasswordNew(),
|
||||
'TOTP': this.adminTOTP()
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onNewAdminPasswordResponse(iError, data) {
|
||||
if (iError) {
|
||||
this.adminPasswordUpdateError(true);
|
||||
} else {
|
||||
this.adminPassword('');
|
||||
this.adminPasswordNew('');
|
||||
this.adminPasswordNew2('');
|
||||
|
||||
this.adminPasswordUpdateSuccess(true);
|
||||
|
||||
this.weakPassword(!!data.Result.Weak);
|
||||
}
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.adminPassword('');
|
||||
this.adminPasswordNew('');
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Capa } from 'Common/Enums';
|
||||
import { Settings } from 'Common/Globals';
|
||||
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
||||
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
|
@ -12,17 +11,18 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
|||
import { AccountPopupView } from 'View/Popup/Account';
|
||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||
|
||||
export class AccountsUserSettings {
|
||||
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||
constructor() {
|
||||
this.allowAdditionalAccount = Settings.capa(Capa.AdditionalAccounts);
|
||||
this.allowIdentities = Settings.capa(Capa.Identities);
|
||||
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
|
||||
this.allowIdentities = SettingsCapa('Identities');
|
||||
|
||||
this.accounts = AccountUserStore.accounts;
|
||||
this.loading = AccountUserStore.loading;
|
||||
this.identities = IdentityUserStore;
|
||||
this.mainEmail = SettingsGet('MainEmail');
|
||||
|
||||
this.accountForDeletion = ko.observable(null).deleteAccessHelper();
|
||||
this.identityForDeletion = ko.observable(null).deleteAccessHelper();
|
||||
this.accountForDeletion = ko.observable(null).askDeleteHelper();
|
||||
this.identityForDeletion = ko.observable(null).askDeleteHelper();
|
||||
}
|
||||
|
||||
addNewAccount() {
|
||||
|
@ -30,7 +30,7 @@ export class AccountsUserSettings {
|
|||
}
|
||||
|
||||
editAccount(account) {
|
||||
if (account && account.canBeEdit()) {
|
||||
if (account && account.isAdditional()) {
|
||||
showScreenPopup(AccountPopupView, [account]);
|
||||
}
|
||||
}
|
||||
|
@ -48,19 +48,21 @@ export class AccountsUserSettings {
|
|||
* @returns {void}
|
||||
*/
|
||||
deleteAccount(accountToRemove) {
|
||||
if (accountToRemove && accountToRemove.deleteAccess()) {
|
||||
if (accountToRemove && accountToRemove.askDelete()) {
|
||||
this.accountForDeletion(null);
|
||||
if (accountToRemove) {
|
||||
this.accounts.remove((account) => accountToRemove === account);
|
||||
|
||||
Remote.accountDelete((iError, data) => {
|
||||
Remote.request('AccountDelete', (iError, data) => {
|
||||
if (!iError && data.Reload) {
|
||||
rl.route.root();
|
||||
setTimeout(() => location.reload(), 1);
|
||||
} else {
|
||||
rl.app.accountsAndIdentities();
|
||||
}
|
||||
}, accountToRemove.email);
|
||||
}, {
|
||||
EmailToDelete: accountToRemove.email
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,26 +72,31 @@ export class AccountsUserSettings {
|
|||
* @returns {void}
|
||||
*/
|
||||
deleteIdentity(identityToRemove) {
|
||||
if (identityToRemove && identityToRemove.deleteAccess()) {
|
||||
if (identityToRemove && identityToRemove.askDelete()) {
|
||||
this.identityForDeletion(null);
|
||||
|
||||
if (identityToRemove) {
|
||||
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
|
||||
Remote.identityDelete(() => rl.app.accountsAndIdentities(), identityToRemove.id());
|
||||
Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
|
||||
IdToDelete: identityToRemove.id()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
oDom.addEventListener('click', event => {
|
||||
let el = event.target.closestWithin('.accounts-list .account-item .e-action', oDom);
|
||||
let el = event.target.closestWithin('.accounts-list .e-action', oDom);
|
||||
el && ko.dataFor(el) && this.editAccount(ko.dataFor(el));
|
||||
|
||||
el = event.target.closestWithin('.identities-list .identity-item .e-action', oDom);
|
||||
el = event.target.closestWithin('.identities-list .e-action', oDom);
|
||||
el && ko.dataFor(el) && this.editIdentity(ko.dataFor(el));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import ko from 'ko';
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { ContactUserStore } from 'Stores/User/Contact';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export class ContactsUserSettings {
|
||||
export class UserSettingsContacts /*extends AbstractViewSettings*/ {
|
||||
constructor() {
|
||||
this.contactsAutosave = ko.observable(!!SettingsGet('ContactsAutosave'));
|
||||
|
||||
|
@ -14,8 +15,7 @@ export class ContactsUserSettings {
|
|||
this.contactsSyncUser = ContactUserStore.syncUser;
|
||||
this.contactsSyncPass = ContactUserStore.syncPass;
|
||||
|
||||
this.saveTrigger = ko
|
||||
.computed(() =>
|
||||
this.saveTrigger = koComputable(() =>
|
||||
[
|
||||
ContactUserStore.enableSync() ? '1' : '0',
|
||||
ContactUserStore.syncUrl(),
|
||||
|
@ -26,19 +26,16 @@ export class ContactsUserSettings {
|
|||
.extend({ debounce: 500 });
|
||||
|
||||
this.contactsAutosave.subscribe(value =>
|
||||
Remote.saveSettings(null, {
|
||||
ContactsAutosave: value ? 1 : 0
|
||||
})
|
||||
Remote.saveSettings(null, { ContactsAutosave: value })
|
||||
);
|
||||
|
||||
this.saveTrigger.subscribe(() =>
|
||||
Remote.saveContactsSyncData(
|
||||
null,
|
||||
ContactUserStore.enableSync(),
|
||||
ContactUserStore.syncUrl(),
|
||||
ContactUserStore.syncUser(),
|
||||
ContactUserStore.syncPass()
|
||||
)
|
||||
Remote.request('SaveContactsSyncData', null, {
|
||||
Enable: ContactUserStore.enableSync() ? 1 : 0,
|
||||
Url: ContactUserStore.syncUrl(),
|
||||
User: ContactUserStore.syncUser(),
|
||||
Password: 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';
|
||||
import { addObservablesTo } from 'Common/Utils';
|
||||
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 {
|
||||
//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
||||
export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
||||
constructor() {
|
||||
this.scripts = SieveUserStore.scripts;
|
||||
this.loading = ko.observable(false).extend({ debounce: 200 });
|
||||
|
||||
this.scripts = ko.observableArray();
|
||||
this.loading = ko.observable(true).extend({ debounce: 200 });
|
||||
addObservablesTo(this, {
|
||||
serverError: false,
|
||||
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.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)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.scriptForDeletion = ko.observable(null).askDeleteHelper();
|
||||
}
|
||||
|
||||
addScript() {
|
||||
showScreenPopup(SieveScriptPopupView, [new SieveScriptModel()]);
|
||||
this.editScript();
|
||||
}
|
||||
|
||||
editScript(script) {
|
||||
showScreenPopup(SieveScriptPopupView, [script]);
|
||||
window.Sieve.ScriptView.showModal(script ? [script] : null);
|
||||
}
|
||||
|
||||
deleteScript(script) {
|
||||
this.serverError(false);
|
||||
Remote.filtersScriptDelete(
|
||||
(iError, data) => {
|
||||
if (iError) {
|
||||
this.setError((data && data.ErrorMessageAdditional) || getNotification(iError));
|
||||
} else {
|
||||
this.scripts.remove(script);
|
||||
delegateRunOnDestroy(script);
|
||||
}
|
||||
},
|
||||
script.name()
|
||||
);
|
||||
window.Sieve.deleteScript(script);
|
||||
}
|
||||
|
||||
toggleScript(script) {
|
||||
let name = script.active() ? '' : script.name();
|
||||
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
|
||||
);
|
||||
window.Sieve.toggleScript(script);
|
||||
}
|
||||
|
||||
onBuild(oDom) {
|
||||
|
@ -106,6 +50,6 @@ export class FiltersUserSettings {
|
|||
}
|
||||
|
||||
onShow() {
|
||||
this.updateList();
|
||||
window.Sieve && window.Sieve.updateList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import ko from 'ko';
|
||||
import { koComputable } from 'External/ko';
|
||||
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { ClientSideKeyName } from 'Common/EnumsUser';
|
||||
import { Settings } from 'Common/Globals';
|
||||
import { FolderMetadataKeys } from 'Common/EnumsUser';
|
||||
import { SettingsCapa } from 'Common/Globals';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import { removeFolderFromCacheList } from 'Common/Cache';
|
||||
|
||||
import * as Local from 'Storage/Client';
|
||||
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
|
||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||
import { sortFolders } from 'Common/Folders';
|
||||
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
@ -18,16 +20,36 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
|||
|
||||
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||
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() {
|
||||
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.folderList = FolderUserStore.folderList;
|
||||
this.folderListOptimized = FolderUserStore.folderListOptimized;
|
||||
this.folderListError = FolderUserStore.folderListError;
|
||||
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
|
||||
|
||||
this.loading = ko.computed(() => {
|
||||
this.loading = koComputable(() => {
|
||||
const loading = FolderUserStore.foldersLoading(),
|
||||
creating = FolderUserStore.foldersCreating(),
|
||||
deleting = FolderUserStore.foldersDeleting(),
|
||||
|
@ -36,28 +58,43 @@ export class FoldersUserSettings {
|
|||
return loading || creating || deleting || renaming;
|
||||
});
|
||||
|
||||
this.folderForDeletion = ko.observable(null).deleteAccessHelper();
|
||||
this.folderForDeletion = folderForDeletion;
|
||||
|
||||
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'edited'] });
|
||||
|
||||
this.useImapSubscribe = Settings.app('useImapSubscribe');
|
||||
this.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value ? 1 : 0));
|
||||
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
|
||||
}
|
||||
|
||||
folderEditOnEnter(folder) {
|
||||
const nameToEdit = folder ? folder.nameForEdit().trim() : '';
|
||||
|
||||
if (nameToEdit && folder.name() !== nameToEdit) {
|
||||
Local.set(ClientSideKeyName.FoldersLashHash, '');
|
||||
|
||||
rl.app.foldersPromisesActionHelper(
|
||||
Remote.folderRename(folder.fullNameRaw, nameToEdit),
|
||||
Notification.CantRenameFolder
|
||||
);
|
||||
|
||||
removeFolderFromCacheList(folder.fullNameRaw);
|
||||
|
||||
folder.name(nameToEdit);
|
||||
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
||||
Folder: folder.fullName,
|
||||
NewFolderName: nameToEdit,
|
||||
Subscribe: folder.subscribed() ? 1 : 0
|
||||
})
|
||||
.then(data => {
|
||||
folder.name(nameToEdit/*data.Name*/);
|
||||
if (folder.subFolders.length) {
|
||||
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
||||
// clearTimeout(Remote.foldersTimeout);
|
||||
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
|
||||
setTimeout(loadFolders, 500);
|
||||
// TODO: rename all subfolders with folder.delimiter to prevent reload?
|
||||
} else {
|
||||
removeFolderFromCacheList(folder.fullName);
|
||||
folder.fullName = data.Result.FullName;
|
||||
setFolder(folder);
|
||||
const parent = getFolderFromCacheList(folder.parentName);
|
||||
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
FolderUserStore.folderListError(
|
||||
getNotification(error.code, '', Notification.CantRenameFolder)
|
||||
+ '.\n' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
folder.edited(false);
|
||||
|
@ -85,58 +122,69 @@ export class FoldersUserSettings {
|
|||
}
|
||||
|
||||
deleteFolder(folderToRemove) {
|
||||
if (
|
||||
folderToRemove &&
|
||||
folderToRemove.canBeDeleted() &&
|
||||
folderToRemove.deleteAccess() &&
|
||||
0 === folderToRemove.privateMessageCountAll()
|
||||
if (folderToRemove
|
||||
&& folderToRemove.canBeDeleted()
|
||||
&& folderToRemove.askDelete()
|
||||
) {
|
||||
this.folderForDeletion(null);
|
||||
if (0 < folderToRemove.privateMessageCountAll()) {
|
||||
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
} else {
|
||||
folderForDeletion(null);
|
||||
|
||||
if (folderToRemove) {
|
||||
const fRemoveFolder = function(folder) {
|
||||
if (folderToRemove === folder) {
|
||||
return true;
|
||||
}
|
||||
folder.subFolders.remove(fRemoveFolder);
|
||||
return false;
|
||||
};
|
||||
|
||||
Local.set(ClientSideKeyName.FoldersLashHash, '');
|
||||
|
||||
FolderUserStore.folderList.remove(fRemoveFolder);
|
||||
|
||||
rl.app.foldersPromisesActionHelper(
|
||||
Remote.folderDelete(folderToRemove.fullNameRaw),
|
||||
Notification.CantDeleteFolder
|
||||
);
|
||||
|
||||
removeFolderFromCacheList(folderToRemove.fullNameRaw);
|
||||
if (folderToRemove) {
|
||||
Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
|
||||
Folder: folderToRemove.fullName
|
||||
}).then(
|
||||
() => {
|
||||
// folderToRemove.flags.push('\\nonexistent');
|
||||
folderToRemove.selectable(false);
|
||||
// folderToRemove.subscribed(false);
|
||||
// folderToRemove.checkable(false);
|
||||
if (!folderToRemove.subFolders.length) {
|
||||
removeFolderFromCacheList(folderToRemove.fullName);
|
||||
const folder = getFolderFromCacheList(folderToRemove.parentName);
|
||||
(folder ? folder.subFolders : FolderUserStore.folderList).remove(folderToRemove);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
FolderUserStore.folderListError(
|
||||
getNotification(error.code, '', Notification.CantDeleteFolder)
|
||||
+ '.\n' + error.message
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (0 < folderToRemove.privateMessageCountAll()) {
|
||||
FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
}
|
||||
}
|
||||
|
||||
subscribeFolder(folder) {
|
||||
Local.set(ClientSideKeyName.FoldersLashHash, '');
|
||||
Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, true);
|
||||
folder.subscribed(true);
|
||||
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);
|
||||
}
|
||||
|
||||
unSubscribeFolder(folder) {
|
||||
Local.set(ClientSideKeyName.FoldersLashHash, '');
|
||||
Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, false);
|
||||
folder.subscribed(false);
|
||||
toggleFolderSubscription(folder) {
|
||||
let subscribe = !folder.subscribed();
|
||||
Remote.request('FolderSubscribe', null, {
|
||||
Folder: folder.fullName,
|
||||
Subscribe: subscribe ? 1 : 0
|
||||
});
|
||||
folder.subscribed(subscribe);
|
||||
}
|
||||
|
||||
checkableTrueFolder(folder) {
|
||||
Remote.folderSetCheckable(()=>{}, folder.fullNameRaw, true);
|
||||
folder.checkable(true);
|
||||
}
|
||||
|
||||
checkableFalseFolder(folder) {
|
||||
Remote.folderSetCheckable(()=>{}, folder.fullNameRaw, false);
|
||||
folder.checkable(false);
|
||||
toggleFolderCheckable(folder) {
|
||||
let checkable = !folder.checkable();
|
||||
Remote.request('FolderCheckable', null, {
|
||||
Folder: folder.fullName,
|
||||
Checkable: checkable ? 1 : 0
|
||||
});
|
||||
folder.checkable(checkable);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { MESSAGES_PER_PAGE_VALUES } from 'Common/Consts';
|
||||
import { SaveSettingsStep } from 'Common/Enums';
|
||||
import { EditorDefaultType, Layout } from 'Common/EnumsUser';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { isArray, settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
|
||||
import { i18n, trigger as translatorTrigger, reload as translatorReload, convertLangName } from 'Common/Translator';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { isArray } from 'Common/Utils';
|
||||
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 { AppUserStore } from 'Stores/User/App';
|
||||
|
@ -15,27 +16,33 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
|||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||
import { LanguagesPopupView } from 'View/Popup/Languages';
|
||||
|
||||
export class GeneralUserSettings {
|
||||
export class UserSettingsGeneral extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.language = LanguageStore.language;
|
||||
this.languages = LanguageStore.languages;
|
||||
this.messageReadDelay = SettingsUserStore.messageReadDelay;
|
||||
this.messagesPerPage = SettingsUserStore.messagesPerPage;
|
||||
this.messagesPerPageArray = MESSAGES_PER_PAGE_VALUES;
|
||||
|
||||
this.editorDefaultType = SettingsUserStore.editorDefaultType;
|
||||
this.layout = SettingsUserStore.layout;
|
||||
|
||||
this.enableSoundNotification = NotificationUserStore.enableSoundNotification;
|
||||
this.soundNotification = NotificationUserStore.enableSoundNotification;
|
||||
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
||||
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
|
||||
|
||||
this.enableDesktopNotification = NotificationUserStore.enableDesktopNotification;
|
||||
this.isDesktopNotificationDenied = NotificationUserStore.isDesktopNotificationDenied;
|
||||
this.desktopNotification = NotificationUserStore.enableDesktopNotification;
|
||||
this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed;
|
||||
|
||||
this.viewHTML = SettingsUserStore.viewHTML;
|
||||
this.showImages = SettingsUserStore.showImages;
|
||||
this.removeColors = SettingsUserStore.removeColors;
|
||||
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
|
||||
|
@ -44,97 +51,87 @@ export class GeneralUserSettings {
|
|||
this.replySameFolder = SettingsUserStore.replySameFolder;
|
||||
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
||||
|
||||
this.languageFullName = ko.computed(() => convertLangName(this.language()));
|
||||
this.languageTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
|
||||
|
||||
addObservablesTo(this, {
|
||||
mppTrigger: SaveSettingsStep.Idle,
|
||||
editorDefaultTypeTrigger: SaveSettingsStep.Idle,
|
||||
layoutTrigger: SaveSettingsStep.Idle
|
||||
});
|
||||
this.languageTrigger = ko.observable(SaveSettingsStep.Idle);
|
||||
|
||||
this.identities = IdentityUserStore;
|
||||
|
||||
this.identityMain = ko.computed(() => {
|
||||
const list = this.identities();
|
||||
return isArray(list) ? list.find(item => item && !item.id()) : null;
|
||||
addComputablesTo(this, {
|
||||
languageFullName: () => convertLangName(this.language()),
|
||||
|
||||
identityMain: () => {
|
||||
const list = this.identities();
|
||||
return isArray(list) ? list.find(item => item && !item.id()) : null;
|
||||
},
|
||||
|
||||
identityMainDesc: () => {
|
||||
const identity = this.identityMain();
|
||||
return identity ? identity.formattedName() : '---';
|
||||
},
|
||||
|
||||
editorDefaultTypes: () => {
|
||||
translatorTrigger();
|
||||
return [
|
||||
{ id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML') },
|
||||
{ id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN') },
|
||||
{ id: EditorDefaultType.HtmlForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML_FORCED') },
|
||||
{ id: EditorDefaultType.PlainForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN_FORCED') }
|
||||
];
|
||||
},
|
||||
|
||||
layoutTypes: () => {
|
||||
translatorTrigger();
|
||||
return [
|
||||
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_NO_SPLIT') },
|
||||
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_VERTICAL_SPLIT') },
|
||||
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_HORIZONTAL_SPLIT') }
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
this.identityMainDesc = ko.computed(() => {
|
||||
const identity = this.identityMain();
|
||||
return identity ? identity.formattedName() : '---';
|
||||
});
|
||||
this.addSetting('EditorDefaultType');
|
||||
this.addSetting('MessageReadDelay');
|
||||
this.addSetting('MessagesPerPage');
|
||||
this.addSetting('Layout', () => MessagelistUserStore([]));
|
||||
|
||||
this.editorDefaultTypes = ko.computed(() => {
|
||||
translatorTrigger();
|
||||
return [
|
||||
{ id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML') },
|
||||
{ id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN') },
|
||||
{ id: EditorDefaultType.HtmlForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML_FORCED') },
|
||||
{ id: EditorDefaultType.PlainForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN_FORCED') }
|
||||
];
|
||||
});
|
||||
|
||||
this.layoutTypes = ko.computed(() => {
|
||||
translatorTrigger();
|
||||
return [
|
||||
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_NO_SPLIT') },
|
||||
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_VERTICAL_SPLIT') },
|
||||
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_HORIZONTAL_SPLIT') }
|
||||
];
|
||||
});
|
||||
this.addSettings(['ViewHTML', 'ShowImages', 'UseCheckboxesInList', 'ReplySameFolder',
|
||||
'DesktopNotifications', 'SoundNotification']);
|
||||
|
||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||
this.languageTrigger(saveSettingsStep);
|
||||
setTimeout(() => this.languageTrigger(SaveSettingsStep.Idle), 1000);
|
||||
};
|
||||
|
||||
addSubscribablesTo(this, {
|
||||
language: value => {
|
||||
this.languageTrigger(SaveSettingsStep.Animate);
|
||||
translatorReload(false, value)
|
||||
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult),
|
||||
fReloadLanguageHelper(SaveSettingsStep.FalseResult))
|
||||
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
|
||||
.then(() => Remote.saveSetting('Language', value));
|
||||
},
|
||||
|
||||
editorDefaultType: value => Remote.saveSetting('EditorDefaultType', value,
|
||||
settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this)),
|
||||
|
||||
messagesPerPage: value => Remote.saveSetting('MPP', value,
|
||||
settingsSaveHelperSimpleFunction(this.mppTrigger, this)),
|
||||
|
||||
showImages: value => Remote.saveSetting('ShowImages', value ? 1 : 0),
|
||||
|
||||
removeColors: value => {
|
||||
MessageUserStore.messagesBodiesDom().innerHTML = '';
|
||||
Remote.saveSetting('RemoveColors', value ? 1 : 0);
|
||||
let dom = MessageUserStore.bodiesDom();
|
||||
if (dom) {
|
||||
dom.innerHTML = '';
|
||||
}
|
||||
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),
|
||||
|
||||
replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0),
|
||||
notificationSound: value => {
|
||||
Remote.saveSetting('NotificationSound', value);
|
||||
Settings.set('NotificationSound', value);
|
||||
},
|
||||
|
||||
useThreads: value => {
|
||||
MessageUserStore.list([]);
|
||||
Remote.saveSetting('UseThreads', value ? 1 : 0);
|
||||
},
|
||||
|
||||
layout: value => {
|
||||
MessageUserStore.list([]);
|
||||
Remote.saveSetting('Layout', value, settingsSaveHelperSimpleFunction(this.layoutTrigger, this));
|
||||
MessagelistUserStore([]);
|
||||
Remote.saveSetting('UseThreads', value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editMainIdentity() {
|
||||
const identity = this.identityMain();
|
||||
if (identity) {
|
||||
showScreenPopup(IdentityPopupView, [identity]);
|
||||
}
|
||||
identity && showScreenPopup(IdentityPopupView, [identity]);
|
||||
}
|
||||
|
||||
testSoundNotification() {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue