diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php index 3be3ab90..2c478558 100644 --- a/plugins/kolab_notes/kolab_notes.php +++ b/plugins/kolab_notes/kolab_notes.php @@ -34,6 +34,7 @@ class kolab_notes extends rcube_plugin private $lists; private $folders; private $cache = array(); + private $message_notes = array(); /** * Required startup method of a Roundcube plugin @@ -68,6 +69,8 @@ class kolab_notes extends rcube_plugin $this->rc->load_language($_SESSION['language'], array('notes.notes' => $this->gettext('navtitle'))); // add label for task title if ($args['task'] == 'notes') { + $this->add_hook('storage_init', array($this, 'storage_init')); + // register task actions $this->register_action('index', array($this, 'notes_view')); $this->register_action('fetch', array($this, 'notes_fetch')); @@ -77,6 +80,8 @@ class kolab_notes extends rcube_plugin $this->register_action('dialog-ui', array($this, 'dialog_view')); } else if ($args['task'] == 'mail') { + $this->add_hook('storage_init', array($this, 'storage_init')); + $this->add_hook('message_load', array($this, 'mail_message_load')); $this->add_hook('message_compose', array($this, 'mail_message_compose')); $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); @@ -93,18 +98,35 @@ class kolab_notes extends rcube_plugin ))), 'messagemenu'); - $this->api->output->add_label('kolab_notes.appendnote', 'save', 'cancel'); + $this->api->output->add_label('kolab_notes.appendnote', 'kolab_notes.editnote', 'kolab_notes.deletenotesconfirm', 'kolab_notes.entertitle', 'save', 'delete', 'cancel', 'close'); $this->include_script('notes_mail.js'); } } if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || in_array($args['action'], array('folder-acl','dialog-ui')))) { - require_once($this->home . '/kolab_notes_ui.php'); - $this->ui = new kolab_notes_ui($this); - $this->ui->init(); + $this->load_ui(); } } + /** + * Hook into IMAP FETCH HEADER.FIELDS command and request MESSAGE-ID + */ + public function storage_init($p) + { + $p['fetch_headers'] = trim($p['fetch_headers'] . ' MESSAGE-ID'); + return $p; + } + + /** + * Load and initialize UI class + */ + private function load_ui() + { + require_once($this->home . '/kolab_notes_ui.php'); + $this->ui = new kolab_notes_ui($this); + $this->ui->init(); + } + /** * Read available calendars for the current user and store them internally */ @@ -205,8 +227,8 @@ class kolab_notes extends rcube_plugin // attempt to create a default folder for this user if (empty($this->lists)) { - #if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true))) - # $this->_read_lists(true); + $folder = kolab_storage::folder_update(array('name' => 'Notes', 'type' => 'note', 'default' => true, 'subscribed' => true)); + $this->_read_lists(true); } return $this->lists; @@ -234,10 +256,10 @@ class kolab_notes extends rcube_plugin // resolve message reference if ($msgref = rcube_utils::get_input_value('_msg', RCUBE_INPUT_GPC, true)) { $storage = $this->rc->get_storage(); - $storage->set_options(array('all_headers' => true)); list($uid, $folder) = explode('-', $msgref, 2); if ($message = $storage->get_message_headers($msgref)) { $this->rc->output->set_env('kolab_notes_template', array( + '_from_mail' => true, 'title' => $message->get('subject'), 'links' => array(array( 'uri' => $this->get_message_uri($message, $folder), @@ -346,7 +368,7 @@ class kolab_notes extends rcube_plugin // encode for client use if (is_array($data)) { - $this->_client_encode($data); + $this->_client_encode($data, true); } $this->rc->output->command('plugin.render_note', $data); @@ -393,7 +415,7 @@ class kolab_notes extends rcube_plugin /** * Helper method to encode the given note record for use in the client */ - private function _client_encode(&$note) + private function _client_encode(&$note, $resolve = false) { foreach ($note as $key => $prop) { if ($key[0] == '_' || $key == 'x-custom') { @@ -413,6 +435,14 @@ class kolab_notes extends rcube_plugin $note['html'] = $this->_wash_html($note['description']); } + // resolve message links + if (is_array($note['links'])) { + $me = $this; + $note['links'] = array_map(function($link) use ($me, $resolve){ + return $me->get_message_reference($link, $resolve) ?: array('uri' => $link); + }, $note['links']); + } + return $note; } @@ -680,11 +710,47 @@ class kolab_notes extends rcube_plugin } /** - * Handler for 'messagebody_html' hooks + * Lookup backend storage and find notes tagged with the given message-ID + */ + public function mail_message_load($p) + { + $this->message = $p['object']; + $this->message_notes = array(); + + // TODO: only query for notes if message was flagged with $KolabNotes ? + + $message_id = trim($p['object']->headers->messageID, '<> '); + if ($message_id && $p['object']->uid) { + $query = array(array('tags','=','ref:' . $message_id)); + foreach (kolab_storage::select($query, 'note') as $record) { + $record['list'] = kolab_storage::folder_id($record['_mailbox']); + $this->message_notes[] = $record; + } + } + } + + /** + * Handler for 'messagebody_html' hook */ public function mail_messagebody_html($args) { - + $html = ''; + foreach ($this->message_notes as $note) { + $html .= html::a(array( + 'href' => $this->rc->url(array('task' => 'notes', '_list' => $note['list'], '_id' => $note['uid'])), + 'class' => 'kolabnotesref', + 'rel' => $note['uid'] . '@' . $note['list'], + 'target' => '_blank', + ), Q($note['title'])); + } + + // prepend note links to message body + if ($html) { + $this->load_ui(); + $args['content'] = html::div('kolabmessagenotes', $html) . $args['content']; + } + + return $args; } /** @@ -714,7 +780,7 @@ class kolab_notes extends rcube_plugin 'Subject' => $note['title'], 'Date' => $note['changed']->format('r'), )); - console($note); + if ($this->is_html($note)) { $message->setHTMLBody($note['description']); @@ -735,9 +801,79 @@ class kolab_notes extends rcube_plugin return $message->getMessage(); } + /** + * Build a URI representing the given message reference + */ private function get_message_uri($headers, $folder) { - return sprintf('imap://%s/%s#%s', $headers->folder ?: $folder, $headers->uid, urlencode($headers->messageID)); + return sprintf('imap://%s/%s?message-id=%s&subject=%s', + $headers->folder ?: $folder, + $headers->uid, + urlencode($headers->messageID), + urlencode($headers->get('subject')) + ); + } + + /** + * Extract message reference components from the given URI + */ + private function parse_message_uri($uri) + { + $url = parse_url($uri); + if ($url['scheme'] == 'imap') { + parse_str($url['query'], $param); + $linkref = array( + 'uri' => $uri, + 'message_id' => $param['message-id'] ?: urldecode($url['fragment']), + 'subject' => $param['subject'], + ); + + $path = explode('/', $url['host'] . $url['path']); + $linkref['msguid'] = array_pop($path); + $linkref['folder'] = join('/', $path); + + return $linkref; + } + + return false; + } + + /** + * Resolve the email message reference from the given URI + */ + public function get_message_reference($uri, $resolve = false) + { + if ($linkref = $this->parse_message_uri($uri)) { + // fetch message subject + if ($resolve || empty($linkref['subject'])) { + $imap = $this->rc->get_storage(); + if (!($message = $imap->get_message_headers($linrkef['msguid'], $linkref['folder']))) { + // try to find message using the message_id fragment + $index = $imap->search_once($imap->list_folders_subscribed('', '*', 'mail', null, true), 'HEADER MESSAGE-ID ' . $linkref['message_id']); + if ($index->count()) { + $uid = reset($index->get()); + $folder = $index->get_parameters('MAILBOX'); + if ($message = $imap->get_message_headers($uid, $folder)) { + // replace metadata + $linkref['subject'] = $message->get('subject'); + $linkref['msguid'] = $message->uid; + $linkref['folder'] = $message->folder; + $linkref['uri'] = $this->get_message_uri($message, $folder); + } + } + } + } + + $linkref['mailurl'] = $this->rc->url(array( + 'task' => 'mail', + 'action' => 'show', + 'mbox' => $linkref['folder'], + 'uid' => $linkref['msguid'], + 'rel' => 'note', + )); + } + + return $linkref; } /** @@ -749,6 +885,14 @@ class kolab_notes extends rcube_plugin // TODO: handle attachments + // convert link references into simple URIs + if (array_key_exists('links', $note)) { + $object['links'] = array_map(function($link){ return is_array($link) ? $link['uri'] : strval($link); }, $note['links']); + } + else { + $object['links'] = $old['links']; + } + // clean up HTML content $object['description'] = $this->_wash_html($note['description']); $is_html = true; diff --git a/plugins/kolab_notes/localization/en_US.inc b/plugins/kolab_notes/localization/en_US.inc index ccb737d1..5e3d1bbe 100644 --- a/plugins/kolab_notes/localization/en_US.inc +++ b/plugins/kolab_notes/localization/en_US.inc @@ -24,7 +24,8 @@ $labels['tabsharing'] = 'Sharing'; $labels['discard'] = 'Discard'; $labels['abort'] = 'Abort'; $labels['unsavedchanges'] = 'Unsaved Changes!'; -$labels['appendnote'] = 'Add a note'; +$labels['appendnote'] = 'Add a Note'; +$labels['editnote'] = 'Edit Note'; $labels['savein'] = 'Save in'; $labels['savingdata'] = 'Saving data...'; diff --git a/plugins/kolab_notes/notes.js b/plugins/kolab_notes/notes.js index c05be5a0..41dcac15 100644 --- a/plugins/kolab_notes/notes.js +++ b/plugins/kolab_notes/notes.js @@ -236,6 +236,7 @@ function rcube_kolab_notes_ui(settings) function init_dialog() { rcmail.register_command('save', save_note, true); + rcmail.addEventListener('plugin.render_note', render_note); rcmail.addEventListener('plugin.update_note', function(data){ data.id = rcmail.html_identifier_encode(data.uid); notesdata[data.id] = data; @@ -259,19 +260,24 @@ function rcube_kolab_notes_ui(settings) } init_editor(); - setTimeout(function(){ - me.selected_note = $.extend({ - list: me.selected_list, - uid: null, - title: rcmail.gettext('newnote','kolab_notes'), - description: '', - categories: [], - created: rcmail.gettext('now', 'kolab_notes'), - changed: rcmail.gettext('now', 'kolab_notes') - }, rcmail.env.kolab_notes_template || {}); - render_note(me.selected_note); - - }, 100); + if (settings.selected_uid) { + me.selected_list = settings.selected_list; + edit_note(settings.selected_uid); + } + else { + setTimeout(function(){ + me.selected_note = $.extend({ + list: me.selected_list, + uid: null, + title: rcmail.gettext('newnote','kolab_notes'), + description: '', + categories: [], + created: rcmail.gettext('now', 'kolab_notes'), + changed: rcmail.gettext('now', 'kolab_notes') + }, rcmail.env.kolab_notes_template || {}); + render_note(me.selected_note); + }, 100); + } } this.init_dialog = init_dialog; @@ -319,16 +325,7 @@ function rcube_kolab_notes_ui(settings) // register click handler for message links $(rcmail.gui_objects.notesattachmentslist).on('click', 'li a.messagelink', function(){ - var uri = $(this).attr('rel'); - if (uri && uri.match(/imap:\/\/.+/)) { - var path = uri.split('#')[0].substr(7).split('/'), - uid = path.pop(), - folder = path.join('/'); - if (folder && uid) { - rcmail.open_window(rcmail.url('mail/show', { _mbox: folder, _uid: uid, _rel: 'note' })); - } - } - + rcmail.open_window(this.href); return false; }); } @@ -715,13 +712,14 @@ function rcube_kolab_notes_ui(settings) return; } - var list = me.notebooks[data.list] || me.notebooks[me.selected_list]; + var list = me.notebooks[data.list] || me.notebooks[me.selected_list] || {}; content = $('#notecontent').val(data.description), readonly = data.readonly || !list.editable, attachmentslist = $(rcmail.gui_objects.notesattachmentslist).html(''); $('.notetitle', rcmail.gui_objects.noteviewtitle).val(data.title).prop('disabled', readonly); $('.dates .notecreated', rcmail.gui_objects.noteviewtitle).html(Q(data.created || '')); $('.dates .notechanged', rcmail.gui_objects.noteviewtitle).html(Q(data.changed || '')); + $(rcmail.gui_objects.notebooks).filter('select').val(list.id); if (data.created || data.changed) { $('.dates', rcmail.gui_objects.noteviewtitle).show(); } @@ -753,15 +751,24 @@ function rcube_kolab_notes_ui(settings) if (data.links) { $.each(data.links, function(i, link){ - $('
  • ').addClass('link') + var li = $('
  • ').addClass('link') .addClass('message eml') .append($('') - .attr('href', '#show') - .attr('rel', link.uri) + .attr('href', link.mailurl) .addClass('messagelink') .html(Q(link.subject || link.message_id || link.uri)) ) .appendTo(attachmentslist); +/* + if (!readonly && !data._from_mail) { + $('') + .attr('href', '#delete') + .attr('title', rcmail.gettext('delete')) + .addClass('delete') + .html(rcmail.gettext('delete')) + .appendTo(li); + } +*/ }); } @@ -770,6 +777,9 @@ function rcube_kolab_notes_ui(settings) .on('click', function(){ $('.tagline .placeholder').hide(); }); } + if (!data.list) + data.list = list.id; + me.selected_note = data; me.selected_note.id = rcmail.html_identifier_encode(data.uid); rcmail.enable_command('save', list.editable && !data.readonly); @@ -781,7 +791,7 @@ function rcube_kolab_notes_ui(settings) html = text2html(html); } - var node, editor = tinyMCE.get('notecontent'); + var node, editor = tinyMCE.get('notecontent'), is_html = false; if (!readonly && editor) { $(rcmail.gui_objects.notesdetailview).hide(); $(rcmail.gui_objects.noteseditform).show(); @@ -795,13 +805,17 @@ function rcube_kolab_notes_ui(settings) $('.notetitle', rcmail.gui_objects.noteviewtitle).focus().select(); // read possibly re-formatted content back from editor for later comparison - me.selected_note.description = editor.getContent({ format:'html' }) + me.selected_note.description = editor.getContent({ format:'html' }); + is_html = true; } else { $(rcmail.gui_objects.noteseditform).hide(); $(rcmail.gui_objects.notesdetailview).html(html).show(); } + // notify subscribers + rcmail.triggerEvent('kolab_notes_render', { data:data, readonly:readonly, html:is_html }); + // Trigger resize (needed for proper editor resizing) $(window).resize(); } @@ -1010,6 +1024,11 @@ function rcube_kolab_notes_ui(settings) if (me.selected_note.links) savedata.links = me.selected_note.links; + // add reference to old list if changed + if (me.selected_note.list && savedata.list != me.selected_note.list) { + savedata._fromlist = me.selected_note.list; + } + // do some input validation if (savedata.title == '') { alert(rcmail.gettext('entertitle', 'kolab_notes')); @@ -1032,11 +1051,12 @@ function rcube_kolab_notes_ui(settings) */ function get_save_data() { - var editor = tinyMCE.get('notecontent'); + var editor = tinyMCE.get('notecontent'), + listselect = $('option:selected', rcmail.gui_objects.notebooks); var savedata = { title: trim($('.notetitle', rcmail.gui_objects.noteviewtitle).val()), description: editor ? editor.getContent({ format:'html' }) : $('#notecontent').val(), - list: me.selected_note.list || me.selected_list, + list: listselect.length ? listselect.val() : me.selected_note.list || me.selected_list, uid: me.selected_note.uid, categories: [] }; @@ -1068,7 +1088,8 @@ function rcube_kolab_notes_ui(settings) return savedata.title != me.selected_note.title || savedata.description != me.selected_note.description - || savedata.categories.join(',') != (me.selected_note.categories || []).join(','); + || savedata.categories.join(',') != (me.selected_note.categories || []).join(',') + || savedata.list != me.selected_note.list; } /** diff --git a/plugins/kolab_notes/skins/larry/notes.css b/plugins/kolab_notes/skins/larry/notes.css index 96d283b4..96e962f8 100644 --- a/plugins/kolab_notes/skins/larry/notes.css +++ b/plugins/kolab_notes/skins/larry/notes.css @@ -103,7 +103,7 @@ } .notesview .toolbarmenu.iconized .selected span.icon { - background: url(sprites.png) -5px -109px no-repeat; + background: url(sprites.png) -4px -110px no-repeat; } .notesview #notedetailsbox { @@ -137,16 +137,18 @@ width: 100%; } -.notesdialog #noteform { - bottom: 30px; -} - .notesview #notedetails { padding: 8px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; + border-bottom: 1px solid #dfdfdf; +} + +.notesdialog #noteform, +.notesdialog #notedetails { + bottom: 30px; } .notesview #notedetails pre { @@ -266,12 +268,16 @@ position: absolute; left: 0; right: 0; - bottom: 0; + bottom: 41px; height: 27px; background: #fff; padding-left: 10px; } +.notesdialog #notereferences { + bottom: 0; +} + .notesview #notebooks li { margin: 0; height: 20px; @@ -369,4 +375,42 @@ -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; +} + +.notesview .attachmentslist li.message.eml { + display: inline-block; + background-image: url(sprites.png); + background-position: -6px -128px; + margin-right: 1em; +} + +.notesview .attachmentslist li.message a.messagelink { + padding-left: 24px; +} + +ul.toolbarmenu li .appendnote span.icon { + background-image: url(sprites.png); + background-position: -4px -170px; +} + +div.kolabmessagenotes { + margin: 8px 8px; + padding: 4px 8px; + border: 1px solid #dfdfdf; + background: #fafafa; + border-radius: 4px; +} + +div.kolabmessagenotes a.kolabnotesref { + display: inline-block; + color: #333; + font-weight: bold; + padding: 3px 15px 2px 22px; + text-shadow: 0px 1px 1px #fff; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 12em; + background: url(sprites.png) -6px -151px no-repeat; } \ No newline at end of file diff --git a/plugins/kolab_notes/skins/larry/sprites.png b/plugins/kolab_notes/skins/larry/sprites.png index 184cef41..62aff656 100644 Binary files a/plugins/kolab_notes/skins/larry/sprites.png and b/plugins/kolab_notes/skins/larry/sprites.png differ diff --git a/plugins/kolab_notes/skins/larry/templates/notes.html b/plugins/kolab_notes/skins/larry/templates/notes.html index 75fa1889..8f35b61f 100644 --- a/plugins/kolab_notes/skins/larry/templates/notes.html +++ b/plugins/kolab_notes/skins/larry/templates/notes.html @@ -60,6 +60,9 @@ +
    + +
    @@ -115,9 +118,16 @@ $(document).ready(function(e){ var form = $('#noteform, #notedetails'), content = $('#notecontent'), header = $('#notedetailstitle'), + footer = $('#notereferences'), w, h; + if ($('#attachment-list li').length) + footer.show(); + else + footer.hide(); + form.css('top', header.outerHeight()+'px'); + form.css('bottom', ((footer.is(':visible') ? footer.outerHeight()+4 : 0) + 41) + 'px'); w = form.outerWidth(); h = form.outerHeight();