From c09e4fead9f5c2d2db5e06b195433645074f4fe4 Mon Sep 17 00:00:00 2001 From: PhilW Date: Sun, 21 Jan 2018 10:26:08 +0000 Subject: [PATCH] initial commit --- CHANGELOG | 4 + README.md | 66 ++++++ composer.json | 31 +++ localization/en_GB.inc | 15 ++ localization/en_US.inc | 15 ++ skins/elastic/menu.html | 54 +++++ skins/elastic/swipe.less | 141 ++++++++++++ skins/elastic/swipe.min.css | 1 + swipe.js | 419 ++++++++++++++++++++++++++++++++++++ swipe.php | 82 +++++++ 10 files changed, 828 insertions(+) create mode 100644 CHANGELOG create mode 100644 README.md create mode 100644 composer.json create mode 100644 localization/en_GB.inc create mode 100644 localization/en_US.inc create mode 100644 skins/elastic/menu.html create mode 100644 skins/elastic/swipe.less create mode 100644 skins/elastic/swipe.min.css create mode 100644 swipe.js create mode 100644 swipe.php diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..a02332c --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,4 @@ +Roundcube Webmail Swipe +======================= + + * Created plugin \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..48beff5 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +Roundcube Webmail Swipe +======================= +This plugin adds left/right/down swipe actions to entries in the the message +list on touch devices (tables/phones) with browsers that support touch events. + +ATTENTION +--------- +This is just a snapshot from the GIT repository and is **NOT A STABLE version +of Swipe**. It is Intended for use with the **GIT-master** version of +Roundcube and it may not be compatible with older versions. Stable versions of +Swipe are available from the [Roundcube plugin repository][rcplugrepo] +(for 1.4 and above) or the [releases section][releases] of the GitHub +repository. + +License +------- +This plugin is released under the [GNU General Public License Version 3+][gpl]. + +Even if skins might contain some programming work, they are not considered +as a linked part of the plugin and therefore skins DO NOT fall under the +provisions of the GPL license. See the README file located in the core skins +folder for details on the skin license. + +Install +------- +* Place this plugin folder into plugins directory of Roundcube +* Add swipe to $config['plugins'] in your Roundcube config + +**NB:** When downloading the plugin from GitHub you will need to create a +directory called skin and place the files in there, ignoring the root +directory in the downloaded archive. + +Supported skins +--------------- +* Elastic + +Configuration +------------- +To set the default actions add `$config['swipe_left']`, `$config['swipe_right']` +and `$config['swipe_down']` to your Roundcube config file. For example: +`$config['swipe_left'] = 'delete';`. Users can configure the actions, overriding +the defaults, from the List options menu. + +Supported actions +----------------- +The following actions are available for left/right swipe: + +* `archive` - Archive the message (Requires the Roundcube Archive plugin) +* `delete` - Delete the message +* `flagged` - Mark the message as flagged/unflagged +* `forward` - Forward the message +* `move` - Move the message to a chosen folder +* `read` - Mark the message as read/unread +* `reply` - Reply to the message +* `replyall` - Reply all to the message +* `select` - Select/deselect the message +* `none` - Swipe disabled + +The following actions are available for down swipe: + +* `checkmail` - Check for new messages in the current folder +* `none` - Swipe disabled + +[rcplugrepo]: https://plugins.roundcube.net/packages/johndoh/swipe +[releases]: https://github.com/johndoh/roundcube-swipe/releases +[gpl]: https://www.gnu.org/licenses/gpl.html \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8f03e8b --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "johndoh/swipe", + "description": "Adds swipe actions to the message list of Roundcube", + "keywords": ["swipe","gesture"], + "homepage": "https://github.com/johndoh/roundcube-swipe/", + "license": "GPL-3.0", + "type": "roundcube-plugin", + "version": "0.1-git", + "authors": [ + { + "name": "Philip Weir", + "email": "roundcube@tehinterweb.co.uk", + "role": "Developer" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=5.2.1", + "roundcube/plugin-installer": ">=0.1.2" + }, + "extra": { + "roundcube": { + "min-version": "1.4" + } + } +} \ No newline at end of file diff --git a/localization/en_GB.inc b/localization/en_GB.inc new file mode 100644 index 0000000..e9a53a6 --- /dev/null +++ b/localization/en_GB.inc @@ -0,0 +1,15 @@ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ \ No newline at end of file diff --git a/skins/elastic/swipe.less b/skins/elastic/swipe.less new file mode 100644 index 0000000..5a2959d --- /dev/null +++ b/skins/elastic/swipe.less @@ -0,0 +1,141 @@ +/** + * Swipe plugin styles + */ + +@import (reference) "../../../../skins/elastic/styles/variables"; +@import (reference) "../../../../skins/elastic/styles/mixins"; + +#swipe-action { + position: absolute; + background-color: @color-black-shade-bg; + color: @color-black; + display: flex; + align-items: center; + border-collapse: collapse; + + &.checkmail, + &.select, + &.deselect { + background-color: @color-btn-secondary-background; + color: @color-btn-secondary; + } + + &.delete { + background-color: @color-btn-danger-background; + color: @color-btn-danger; + } + + &.flagged, + &.unflagged, + &.read, + &.unread { + background-color: @color-btn-primary-background; + color: @color-btn-primary; + } + + &.forward, + &.reply, + &.replyall { + background-color: @color-success; + color: #fff; + } + + &.move, + &.archive { + background-color: @color-warning; + color: #fff; + } + + > div { + &.left { + position: absolute; + right: 0.5em; + } + + &.down { + margin: 0 auto; + + > span::before { + width: auto; + float: none; + margin: 0; + margin-bottom: 0.2em; + padding: 0; + } + } + + > span { + line-height: 100%; + font-size: 1.2em; + + &::before { + .font-icon-class; + padding: 0 1.25em 0 0.5em; + } + + &.checkmail::before { + content: @fa-var-sync; + } + + &.delete::before { + content: @fa-var-trash-alt; + } + + &.flagged::before { + content: @fa-var-flag; + } + + &.forward::before { + content: @fa-var-share; + } + + &.unflagged::before { + .font-icon-regular(@fa-var-flag); + } + + &.move::before { + content: @fa-var-arrows-alt; + } + + &.read::before { + .font-icon-regular(@fa-var-star); + } + + &.unread::before { + content: @fa-var-star; + } + + &.reply::before { + content: @fa-var-reply; + } + + &.replyall::before { + content: @fa-var-reply-all; + } + + &.select::before { + .font-icon-regular(@fa-var-check-square); + } + + &.deselect::before { + .font-icon-regular(@fa-var-square); + } + + &.archive::before { + content: @fa-var-archive; + } + } + } +} + +.swipe-active { + background-color:@color-layout-list-background; +} + +#messagelist.swipe-active { + height: 100%; +} + +.swipe-noscroll { + overflow: hidden !important; +} \ No newline at end of file diff --git a/skins/elastic/swipe.min.css b/skins/elastic/swipe.min.css new file mode 100644 index 0000000..2a8469a --- /dev/null +++ b/skins/elastic/swipe.min.css @@ -0,0 +1 @@ +#swipe-action{position:absolute;background-color:#f1f3f4;color:#161b1d;display:flex;align-items:center;border-collapse:collapse}#swipe-action.checkmail,#swipe-action.select,#swipe-action.deselect{background-color:#8b9fa7;color:#fff}#swipe-action.delete{background-color:#ff5552;color:#fff}#swipe-action.flagged,#swipe-action.unflagged,#swipe-action.read,#swipe-action.unread{background-color:#37beff;color:#fff}#swipe-action.forward,#swipe-action.reply,#swipe-action.replyall{background-color:#41b849;color:#fff}#swipe-action.move,#swipe-action.archive{background-color:#ffd452;color:#fff}#swipe-action>div.left{position:absolute;right:.5em}#swipe-action>div.down{margin:0 auto}#swipe-action>div.down>span::before{width:auto;float:none;margin:0;margin-bottom:.2em;padding:0}#swipe-action>div>span{line-height:100%;font-size:1.2em}#swipe-action>div>span::before{font-size:1.25em;display:block;float:left;margin:0 .25rem 0 0;width:1.18em;height:1em;font-family:'Icons';font-style:normal;font-weight:900;text-decoration:inherit;text-align:center;speak:none;font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;padding:0 1.25em 0 .5em}#swipe-action>div>span.checkmail::before{content:"\f021"}#swipe-action>div>span.delete::before{content:"\f2ed"}#swipe-action>div>span.flagged::before{content:"\f024"}#swipe-action>div>span.forward::before{content:"\f064"}#swipe-action>div>span.unflagged::before{content:"\f024";font-weight:400}#swipe-action>div>span.move::before{content:"\f0b2"}#swipe-action>div>span.read::before{content:"\f005";font-weight:400}#swipe-action>div>span.unread::before{content:"\f005"}#swipe-action>div>span.reply::before{content:"\f3e5"}#swipe-action>div>span.replyall::before{content:"\f122"}#swipe-action>div>span.select::before{content:"\f14a";font-weight:400}#swipe-action>div>span.deselect::before{content:"\f0c8";font-weight:400}#swipe-action>div>span.archive::before{content:"\f187"}.swipe-active{background-color:#fff}#messagelist.swipe-active{height:100%}.swipe-noscroll{overflow:hidden !important} \ No newline at end of file diff --git a/swipe.js b/swipe.js new file mode 100644 index 0000000..56055f1 --- /dev/null +++ b/swipe.js @@ -0,0 +1,419 @@ +/** + * Swipe plugin script + * + * @licstart The following is the entire license notice for the + * JavaScript code in this file. + * + * Copyright (C) 2018 Philip Weir + * + * The JavaScript code in this page is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * @licend The above is the entire license notice + * for the JavaScript code in this file. + */ + +rcube_webmail.prototype.swipe_position_target = function(obj, pos, vertical) { + var translate = ''; + + if (pos) + translate = (vertical ? 'translatey' : 'translatex') + '('+ pos +'px)'; + + $(obj).css({ + '-webkit-transform': translate, + '-ms-transform': translate, + 'transform': translate + }); +}; + +rcube_webmail.prototype.swipe_list_selection = function(uid, show, prev_sel) { + // make the system think no preview pane exists while we do some fake message selects + // to enable/disable relevant commands for current selection + var prev_contentframe = rcmail.env.contentframe, i; + rcmail.env.contentframe = null; + + if (show) { + if (rcmail.message_list.selection.length == 0 || !rcmail.message_list.in_selection(uid)) { + prev_sel = prev_sel ? prev_sel : rcmail.message_list.get_selection(); + rcmail.message_list.clear_selection(); + rcmail.message_list.highlight_row(uid, true); + } + } + else if (prev_sel) { + rcmail.message_list.clear_selection(); + + for (i in prev_sel) + rcmail[this.list_object].highlight_row(prev_sel[i], true); + } + else { + rcmail.message_list.clear_selection(); + } + + rcmail.env.contentframe = prev_contentframe; + + return prev_sel; +}; + +rcube_webmail.prototype.swipe_select_action = function(direction, obj) { + var action = { + 'class': '', + 'text': '', + 'callback': null + }; + + if (rcmail.env.swipe_actions[direction] == 'checkmail') { + action.class = 'checkmail'; + action.text = 'refresh'; + action.callback = function(uid, obj, e) { rcmail.command('checkmail'); }; + } + else if (rcmail.env.swipe_actions[direction] == 'delete') { + action.class = 'delete'; + action.text = 'delete'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + var prev_sel = rcmail.swipe_list_selection(uid, true); + + // enable command + var prev_command = rcmail.commands['delete']; + rcmail.enable_command('delete', true); + var result = rcmail.command('delete', '', obj, e); + + rcmail.enable_command('delete', prev_command); + rcmail.swipe_list_selection(uid, false, prev_sel); + }; + } + else if (rcmail.env.swipe_actions[direction] == 'flagged') { + if (obj.hasClass('flagged')) { + action.class = 'unflagged'; + action.text = 'swipe.markasunflagged'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.mark_message('unflagged', uid); + }; + } + else { + action.class = 'flagged'; + action.text = 'swipe.markasflagged'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.mark_message('flagged', uid); + }; + } + } + else if (rcmail.env.swipe_actions[direction] == 'forward') { + action.class = 'forward'; + action.text = 'forward'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.enable_command('forward', true); + rcmail.env.uid = uid; + rcmail.command('forward', '', obj, e); + }; + } + else if (rcmail.env.swipe_actions[direction] == 'move') { + action.class = 'move'; + action.text = 'moveto'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.enable_command('move', true); + $('#' + rcmail.buttons['move'][0].id).click(); + }; + } + else if (rcmail.env.swipe_actions[direction] == 'read') { + if (obj.hasClass('unread')) { + action.class = 'read'; + action.text = 'swipe.markasread'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.mark_message('read', uid); + }; + } + else { + action.class = 'unread'; + action.text = 'swipe.markasunread'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.mark_message('unread', uid); + }; + } + } + else if (rcmail.env.swipe_actions[direction] == 'reply') { + action.class = 'reply'; + action.text = 'reply'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.enable_command('reply', true); + rcmail.env.uid = uid; + rcmail.command('reply', '', obj, e); + }; + } + else if (rcmail.env.swipe_actions[direction] == 'replyall') { + action.class = 'replyall'; + action.text = 'replyall'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.enable_command('reply-all', true); + rcmail.env.uid = uid; + rcmail.command('reply-all', '', obj, e); + }; + } + else if (rcmail.env.swipe_actions[direction] == 'select') { + if (obj.hasClass('selected')) { + action.class = 'deselect'; + action.text = 'swipe.deselect'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + rcmail.message_list.highlight_row(uid, true); + + if (rcmail.message_list.get_selection().length == 0) + $(rcmail.gui_objects.messagelist).removeClass('withselection'); + }; + } + else { + action.class = 'select'; + action.text = 'select'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + $(rcmail.gui_objects.messagelist).addClass('withselection'); + rcmail.message_list.highlight_row(uid, true); + }; + } + } + else if (rcmail.env.swipe_actions[direction] == 'archive' && rcmail.env.archive_folder) { + action.class = 'archive'; + action.text = 'archive.buttontext'; + action.callback = function(uid, obj, e) { + if (!uid) + return; + + var prev_sel = rcmail.swipe_list_selection(uid, true); + + // enable command + var prev_command = rcmail.commands['plugin.archive']; + rcmail.enable_command('plugin.archive', true); + var result = rcmail.command('plugin.archive', '', obj, e); + + rcmail.enable_command('plugin.archive', prev_command); + rcmail.swipe_list_selection(uid, false, prev_sel); + }; + } + + return action; +}; + +rcube_webmail.prototype.swipe_event = function(opts) { + var touchstart = {}; + + // swipe down on message list container + opts.source_obj + .on('touchstart', function(e) { + touchstart.x = e.originalEvent.targetTouches[0].pageX; + touchstart.y = e.originalEvent.targetTouches[0].pageY; + }) + .on('touchmove', function(e) { + // make sure no other swipes are active + if (rcmail.env.swipe_active && rcmail.env.swipe_active != opts.axis) + return; + + var changeX = e.originalEvent.targetTouches[0].pageX - touchstart.x; + var changeY = e.originalEvent.targetTouches[0].pageY - touchstart.y; + + // stop the message row from sliding off the screen completely + if (opts.axis == 'vertical') { + changeY = Math.min(opts.maxmove, changeY); + } + else { + changeX = changeX < 0 ? Math.max(opts.maxmove * -1, changeX) : Math.min(opts.maxmove, changeX); + } + + if ((opts.axis == 'vertical' && (((changeX < 5 && changeX > -5) && opts.source_obj.scrollTop() == 0) || opts.target_obj.hasClass('swipe-active'))) || + ((opts.axis == 'horizontal' && ((changeY < 5 && changeY > -5) || opts.target_obj.hasClass('swipe-active'))))) { + // do not allow swipe up + if (opts.axis == 'vertical' && changeY < 0) + return + + var direction = (opts.axis == 'vertical' ? 'down' : (changeX < 0 ? 'left' : 'right')); + var action = rcmail.swipe_select_action(direction, opts.source_obj); + + // skip if there is no event + if (!action.callback) + return; + + $('#swipe-action') + .data('callback', action.callback) + .children('div') + .removeClass() + .addClass(direction) + .children('span') + .removeClass() + .addClass(action.class) + .text(rcmail.gettext(action.text)); + + if (!opts.target_obj.hasClass('swipe-active')) { + var action_style = opts.action_sytle(opts.target_obj); + $('#swipe-action').css({ + 'top': action_style.top, + 'left': action_style.left, + 'width': action_style.width, + 'height': action_style.height + }).show(); + opts.target_obj.addClass('swipe-active'); + opts.source_obj.addClass('swipe-noscroll'); + rcmail.env.swipe_active = opts.axis; // set the active swipe + } + + // the user must swipe a certain about before the action is activated, try to prevent accidental actions + if ((opts.axis == 'vertical' && changeY > opts.minmove) || + (opts.axis == 'horizontal' && (changeX < (opts.minmove * -1) || changeX > opts.minmove))) { + $('#swipe-action').addClass(action.class); + } + else { + // reset the swipe if the user takes the row back to the start + $('#swipe-action').removeClass(); + $('#swipe-action').data('callback', null); + } + + rcmail.swipe_position_target(opts.target_obj, opts.axis == 'vertical' ? changeY : changeX, opts.axis == 'vertical'); + + if (opts.parent_obj) + opts.parent_obj.on('touchmove', rcube_event.cancel); + } + }) + .on('touchend', function(e) { + if (rcmail.env.swipe_active && rcmail.env.swipe_active == opts.axis && opts.target_obj.hasClass('swipe-active')) { + rcmail.swipe_position_target(opts.target_obj, 0, opts.axis == 'vertical'); + + var callback = null; + if (callback = $('#swipe-action').data('callback')) + callback(opts.uid, opts.target_obj, e); + + $('#swipe-action').removeClass().hide(); + opts.target_obj.removeClass('swipe-active'); + opts.source_obj.removeClass('swipe-noscroll'); + rcmail.env.swipe_active = null; + + if (opts.parent_obj) + opts.parent_obj.off('touchmove', rcube_event.cancel); + } + }); +} + +$(document).ready(function() { + if (window.rcmail && bw.touch && !((bw.ie || bw.edge) && bw.pointer)) { + rcmail.addEventListener('init', function() { + var messagelist_container = $(rcmail.gui_objects.messagelist).parent(); + if (rcmail.message_list.draggable || !messagelist_container[0].addEventListener) + return; + + rcmail.env.swipe_parent = messagelist_container; + rcmail.env.swipe_parent.prepend($('
').attr('id', 'swipe-action').html($('
').append($(''))).hide()); + + // down swipe on message list container + var swipe_config = { + 'source_obj': rcmail.env.swipe_parent, + 'axis': 'vertical', + 'minmove': $(window).height() * 0.1, + 'maxmove': $(window).height() * 0.2, + 'action_sytle': function(o) { + return { + 'top': o.children('tbody').position().top, + 'left': o.children('tbody').position().left, + 'width': o.children('tbody').width() + 'px', + 'height': $(window).height() * 0.2 + 'px' + }; + }, + 'target_obj': $(rcmail.gui_objects.messagelist), + 'uid': null, + 'parent_obj': rcmail.env.swipe_parent.parent() + }; + + rcmail.swipe_event(swipe_config); + }); + + // right/left swipe on message list + rcmail.addEventListener('insertrow', function(props) { + if (rcmail.message_list.draggable || !$('#' + props.row.id)[0].addEventListener) + return; + + var swipe_config = { + 'source_obj': $('#' + props.row.id), + 'axis': 'horizontal', + 'minmove': $('#' + props.row.id).width() * 0.25, + 'maxmove': $('#' + props.row.id).width() * 0.6, + 'action_sytle': function(o) { + return { + 'top': o.position().top, + 'left': o.position().left, + 'width': o.width() + 'px', + 'height': (o.height() - 2) + 'px' // subtract the border + }; + }, + 'target_obj': $('#' + props.row.id), + 'uid': props.uid, + 'parent_obj': rcmail.env.swipe_parent + }; + + rcmail.swipe_event(swipe_config); + }); + + // add swipe options to list options menu + rcmail.addEventListener('menu-open', function(p) { + if (p.name == $('#swipeoptions-menu').data('options-menuname')) { + if (!rcmail.message_list.draggable) { + // set form values + $.each(['left', 'right', 'down'], function() { + $('select[name="swipe_' + this + '"]:visible').val(rcmail.env.swipe_actions[this]); + }); + $('fieldset.swipe').show(); + } + else { + $('fieldset.swipe').hide(); + } + } + }); + + // save swipe options + rcmail.set_list_options_core = rcmail.set_list_options; + rcmail.set_list_options = function(cols, sort_col, sort_order, threads, layout) + { + var post = {}; + $.each(['left', 'right', 'down'], function() { + if ($('select[name="swipe_' + this + '"]:visible').val() != rcmail.env.swipe_actions[this]) { + rcmail.env.swipe_actions[this] = $('select[name="swipe_' + this + '"]:visible').val(); + post['swipe_' + this] = rcmail.env.swipe_actions[this]; + } + }); + + if (!$.isEmptyObject(post)) + rcmail.http_post('plugin.swipe.save_settings', post); + + rcmail.set_list_options_core(cols, sort_col, sort_order, threads, layout); + } + + $('#swipeoptions-menu > fieldset').appendTo('#' + $('#swipeoptions-menu').data('options-menuid')); + } +}); \ No newline at end of file diff --git a/swipe.php b/swipe.php new file mode 100644 index 0000000..307ce39 --- /dev/null +++ b/swipe.php @@ -0,0 +1,82 @@ +menu_file = '/' . $this->local_skin_path() . '/menu.html'; + + if (is_file(slashify($this->home) . $this->menu_file)) { + if ($rcmail->output->type == 'html') { + $this->api->output->set_env('swipe_actions', array( + 'left' => $rcmail->config->get('swipe_left', 'none'), + 'right' => $rcmail->config->get('swipe_right', 'none'), + 'down' => $rcmail->config->get('swipe_down', 'none') + )); + $this->add_texts('localization/', true); + $this->api->output->add_label('none', 'refresh', 'moveto', 'reply', 'replyall', 'forward', 'select'); + $this->include_stylesheet($this->local_skin_path() . '/swipe.css'); + $this->include_script('swipe.js'); + $this->add_hook('render_page', array($this, 'options_menu')); + } + + $this->register_action('plugin.swipe.save_settings', array($this, 'save_settings')); + } + } + + public function options_menu($args) + { + // Other plugins may use template parsing method, this causes more than one render_page execution. + // We have to make sure the menu is added only once (when content is going to be written to client). + if (!$args['write']) { + return; + } + + // add additional menus from skins folder + $html = $this->api->output->just_parse("menu_file\" skinpath=\"plugins/swipe\" />"); + $this->api->output->add_footer($html); + } + + public function save_settings() + { + $config = array(); + foreach (array('left', 'right', 'down') as $direction) { + if ($prop = rcube_utils::get_input_value('swipe_' . $direction, rcube_utils::INPUT_POST)) { + $config['swipe_' . $direction] = $prop; + } + } + + if (count($config) > 0) { + rcube::get_instance()->user->save_prefs($config); + } + } +}