Request #8223: Add keyboard shortcut to move to next unseen message
authorMichael M Slusarz <slusarz@curecanti.org>
Tue, 15 Dec 2009 01:27:21 +0000 (18:27 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Tue, 15 Dec 2009 01:27:26 +0000 (18:27 -0700)
imp/ajax.php
imp/docs/CHANGES
imp/js/DimpBase.js
imp/js/ViewPort.js
imp/lib/Views/ListMessages.php
imp/templates/index/index-dimp.inc

index dcbd5cb..240930b 100644 (file)
@@ -94,8 +94,9 @@ function _getListMessages($mbox, $change)
     } else {
         $search = Horde_Serialize::unserialize($search, Horde_Serialize::JSON);
         $args += array(
-            'search_uid' => $search->imapuid,
-            'search_view' => $search->view,
+            'search_uid' => isset($search->imapuid) ? $search->imapuid : null,
+            'search_unseen' => isset($search->unseen) ? $search->unseen : null,
+            'search_view' => isset($search->view) ? $search->view : null
         );
     }
 
index 8596e32..d221d78 100644 (file)
@@ -3,6 +3,7 @@ v5.0-git
 --------
 
 [mms] Add auto-save draft to IMP (Request #7656).
+[mms] Add keyboard shortcut to move to next unseen message (Request #8223).
 [mms] Add hook to dynamically change mailbox label (Request #6734).
 [mms] Improved address expansion in MIMP.
 [mms] Load folders on-demand in sidebar (DIMP).
index 9f1dc29..96676c7 100644 (file)
@@ -1274,7 +1274,7 @@ var DimpBase = {
         }
 
         sel = this.viewport.createSelection('dataob', r);
-        unseen = Number($(this.getFolderId(r.view)).retrieve('u'));
+        unseen = this.getUnseenCount(r.view);
 
         unseen += setflag ? -1 : 1;
         this.updateFlag(sel, '\\seen', setflag);
@@ -1282,6 +1282,11 @@ var DimpBase = {
         this.updateUnseenStatus(r.view, unseen);
     },
 
+    getUnseenCount: function(mbox)
+    {
+        return Number($(this.getFolderId(mbox)).retrieve('u'));
+    },
+
     updateUnseenStatus: function(mbox, unseen)
     {
         if (this.viewport) {
@@ -1528,7 +1533,8 @@ var DimpBase = {
     /* Keydown event handler */
     keydownHandler: function(e)
     {
-        var co, form, h, pp, ps, r, row, rowoff, sel,
+        var all, cnt, co, form, h, need, pp, ps, r, row, rownum, rowoff, sel,
+            tmp, vsel,
             elt = e.element(),
             kc = e.keyCode || e.charCode;
 
@@ -1672,6 +1678,79 @@ var DimpBase = {
                 e.stop();
             }
             break;
+
+        case 78: // N
+        case 110: // n
+            if (e.shiftKey && !this.isSearch(this.folder)) {
+                cnt = this.getUnseenCount(this.folder);
+                if (Object.isUndefined(cnt) || cnt) {
+                    vsel = this.viewport.getSelection();
+                    row = vsel.search({ flag: { include: '\\seen' } }).get('rownum');
+                    all = (vsel.size() == this.viewport.getMetaData('total_rows'));
+
+                    if (all ||
+                        (!Object.isUndefined(cnt) && row.size() == cnt)) {
+                        // Here we either have the entire mailbox in buffer,
+                        // or all unseen messages are in the buffer.
+                        if (sel.size()) {
+                            tmp = sel.get('rownum').last();
+                            if (tmp) {
+                                rownum = row.detect(function(r) {
+                                    return tmp < r;
+                                });
+                            }
+                        } else {
+                            rownum = tmp = row.first();
+                        }
+                    } else {
+                        // Here there is no guarantee that the next unseen
+                        // message will appear in the current buffer. Need to
+                        // determine if any gaps are between last selected
+                        // message and next unseen message in buffer.
+                        vsel = vsel.get('rownum');
+
+                        if (sel.size()) {
+                            // We know that the selected rows are in the
+                            // buffer.
+                            tmp = sel.get('rownum').last();
+                        } else if (vsel.include(1)) {
+                            // If no selected rows, start searching from the
+                            // first entry.
+                            tmp = 0;
+                        } else {
+                            // First message is not in current buffer.
+                            need = true;
+                        }
+
+                        if (!need) {
+                            rownum = vsel.detect(function(r) {
+                                if (r > tmp) {
+                                    if (++tmp != r) {
+                                        // We have found a gap.
+                                        need = true;
+                                        throw $break;
+                                    }
+                                    return row.include(tmp);
+                                }
+                            });
+
+                            if (!need && !rownum) {
+                                need = (tmp !== this.viewport.getMetaData('total_rows'));
+                            }
+                        }
+
+                        if (need) {
+                            this.viewport.select(null, { search: { unseen: 1 } });
+                        }
+                    }
+
+                    if (rownum) {
+                        this.moveSelected(rownum, true);
+                    }
+                }
+                e.stop();
+            }
+            break;
         }
     },
 
index 43de85e..ec0e9e4 100644 (file)
@@ -212,8 +212,14 @@ var ViewPort = Class.create({
             this.page_size = ps;
         }
 
-        if (this.view && (view != this.view)) {
+        if (this.view) {
             if (!background) {
+                // If this is the same view, send an ajax request with the
+                // given parameters.
+                if (view == this.view) {
+                    return this._fetchBuffer({ search: search });
+                }
+
                 // Need to store current buffer to save current offset
                 buffer = this._getBuffer();
                 buffer.setMetaData({ offset: this.currentOffset() }, true);
@@ -497,6 +503,7 @@ var ViewPort = Class.create({
     //
     // OPTIONAL:
     //   background: (boolean) Do fetch in background
+    //   callback: (function) A callback to run when the request is complete
     //   initial: (boolean) Is this the initial access to this view?
     //   nearing: (string) TODO [only used w/offset]
     //   params: (object) Parameters to add to outgoing URL
@@ -589,6 +596,12 @@ var ViewPort = Class.create({
             params.update({ slice: rlist.first() + ':' + rlist.last() });
         }
 
+        if (opts.callback) {
+            tmp = b.getMetaData('callback') || $H();
+            tmp.set(r_id, opts.callback);
+            b.setMetaData({ callback: tmp }, true);
+        }
+
         if (!opts.background) {
             this.active_req = r_id;
             this._handleWait();
@@ -744,7 +757,7 @@ var ViewPort = Class.create({
         this.isbusy = true;
         this._clearWait();
 
-        var offset,
+        var callback, offset,
             buffer = this._getBuffer(r.view),
             llist = buffer.getMetaData('llist') || $H();
 
@@ -766,8 +779,15 @@ var ViewPort = Class.create({
         if (r.requestid &&
             r.requestid == this.active_req) {
             this.active_req = null;
+            callback = buffer.getMetaData('callback');
             offset = buffer.getMetaData('req_offset');
-            buffer.setMetaData({ req_offset: undefined });
+
+            if (callback && callback.get(r.requestid)) {
+                callback.get(r.requestid)(r);
+                callback.unset(r.requestid);
+            }
+
+            buffer.setMetaData({ callback: undefined, req_offset: undefined }, true);
 
             if (this.opts.onEndFetch) {
                 this.opts.onEndFetch();
@@ -1108,9 +1128,18 @@ var ViewPort = Class.create({
         return this.createSelection('uid', buffer ? buffer.getAllUIDs() : [], view);
     },
 
-    // vs = (Viewport_Selection | array) A Viewport_Selection object -or-, if
+    getMissing: function(view)
+    {
+        var buffer = this._getBuffer(view);
+        if (!buffer) {
+            return '';
+        }
+        return this.createSelection('uid', buffer ? buffer.getAllUIDs() : [], view);
+    },
+
+    // vs = (Viewport_Selection | array) A Viewport_Selection object -or- if
     //       opts.range is set, an array of row numbers.
-    // opts = (object) TODO [add, range]
+    // opts = (object) TODO [add, range, search]
     select: function(vs, opts)
     {
         opts = opts || {};
@@ -1118,6 +1147,21 @@ var ViewPort = Class.create({
         var b = this._getBuffer(),
             sel, slice;
 
+        if (opts.search) {
+            if (this.opts.onFetch) {
+                this.opts.onFetch();
+            }
+
+            return this._fetchBuffer({
+                callback: function(r) {
+                    if (r.rownum) {
+                        this.select(this.createSelection('rownum', [ r.rownum ]), { add: opts.add, range: opts.range });
+                    }
+                }.bind(this),
+                search: opts.search
+            });
+        }
+
         if (opts.range) {
             slice = this.createSelection('rownum', vs);
             if (vs.size() != slice.size()) {
@@ -1125,8 +1169,7 @@ var ViewPort = Class.create({
                     this.opts.onFetch();
                 }
 
-                this._ajaxRequest({ rangeslice: 1, slice: vs.min() + ':' + vs.size() });
-                return;
+                return this._ajaxRequest({ rangeslice: 1, slice: vs.min() + ':' + vs.size() });
             }
             vs = slice;
         }
index f0ef826..76aa515 100644 (file)
@@ -95,42 +95,6 @@ class IMP_Views_ListMessages
         $result->cacheid = $imp_mailbox->getCacheID();
         $result->totalrows = $msgcount;
 
-        /* If this is the initial request for a mailbox, figure out the
-         * starting location based on user's preferences. */
-        $rownum = $args['initial']
-            ? intval($imp_mailbox->mailboxStart($msgcount))
-            : null;
-
-        /* Determine the row slice to process. */
-        if (isset($args['search_uid']) || !is_null($rownum)) {
-            if (is_null($rownum)) {
-                $rownum = 1;
-                foreach (array_keys($sorted_list['s'], $args['search_uid']) as $val) {
-                    if (empty($sorted_list['m'][$val]) ||
-                        ($sorted_list['m'][$val] == $args['search_mbox'])) {
-                        $rownum = $val;
-                        break;
-                    }
-                }
-            }
-
-            $slice_start = $rownum - $args['before'];
-            $slice_end = $rownum + $args['after'];
-            if ($slice_start < 1) {
-                $slice_end += abs($slice_start) + 1;
-            } elseif ($slice_end > $msgcount) {
-                $slice_start -= $slice_end - $msgcount;
-            }
-
-            $result->rownum = $rownum;
-        } else {
-            $slice_start = $args['slice_start'];
-            $slice_end = $args['slice_end'];
-        }
-
-        $slice_start = max(1, $slice_start);
-        $slice_end = min($msgcount, $slice_end);
-
         /* Mail-specific viewport information. */
         $md = &$result->metadata;
         if (!IMP::threadSortAvailable($mbox)) {
@@ -198,6 +162,53 @@ class IMP_Views_ListMessages
             $cached = array_flip($cached);
         }
 
+        if (isset($args['search_unseen'])) {
+            /* Do an unseen search.  We know what messages the browser
+             * doesn't have based on $cached. Thus, search for the first
+             * unseen message not located in $cached. */
+            $unseen_search = $imp_mailbox->unseenMessages(Horde_Imap_Client::SORT_RESULTS_MATCH, true);
+            if (!($uid_search = array_diff($unseen_search['match'], array_keys($cached)))) {
+                return $result;
+            }
+            $rownum = array_search(reset($uid_search), $sorted_list['s']);
+        } else {
+            /* If this is the initial request for a mailbox, figure out the
+             * starting location based on user's preferences. */
+            $rownum = $args['initial']
+                ? intval($imp_mailbox->mailboxStart($msgcount))
+                : null;
+        }
+
+        /* Determine the row slice to process. */
+        if (isset($args['search_uid']) || !is_null($rownum)) {
+            if (is_null($rownum)) {
+                $rownum = 1;
+                foreach (array_keys($sorted_list['s'], $args['search_uid']) as $val) {
+                    if (empty($sorted_list['m'][$val]) ||
+                        ($sorted_list['m'][$val] == $args['search_mbox'])) {
+                        $rownum = $val;
+                        break;
+                    }
+                }
+            }
+
+            $slice_start = $rownum - $args['before'];
+            $slice_end = $rownum + $args['after'];
+            if ($slice_start < 1) {
+                $slice_end += abs($slice_start) + 1;
+            } elseif ($slice_end > $msgcount) {
+                $slice_start -= $slice_end - $msgcount;
+            }
+
+            $result->rownum = $rownum;
+        } else {
+            $slice_start = $args['slice_start'];
+            $slice_end = $args['slice_end'];
+        }
+
+        $slice_start = max(1, $slice_start);
+        $slice_end = min($msgcount, $slice_end);
+
         /* Generate the message list and the UID -> rownumber list. */
         $data = $msglist = $rowlist = array();
         foreach (range($slice_start, $slice_end) as $key) {
index d1576fa..6b972bd 100644 (file)
@@ -210,6 +210,7 @@ function _simpleButton($id, $text, $image)
       <span class="kbd"><?php echo _("Alt") ?></span> + <span class="kbd"><?php echo _("PgUp") ?></span> / <span class="kbd"><?php echo _("PgDown") ?></span> : <?php echo _("Scroll up/down through the display of the previewed message.") ?><br />
       <span class="kbd"><?php echo _("Home") ?></span> / <span class="kbd"><?php echo _("End") ?></span> : <?php echo  _("Move to the beginning/end of the message list.") ?><br />
       <span class="kbd"><?php echo _("Del") ?></span> : <?php echo _("Delete the currently selected message(s).") ?> <?php printf(_("%s will delete the current message and move to the next message if a single message is selected."), '<span class="kbd">' . _("Shift") . '</span> + <span class="kbd">' . _("Del") . '</span>') ?><br />
+      <span class="kbd"><?php echo _("Shift") ?></span> + <span class="kbd"><?php echo _("N") ?></span> : <?php echo _("Move the the next unseen message (non-search folders only).") ?></span><br />
       <span class="kbd"><?php echo _("Enter") ?></span> : <?php echo _("Open message in a popup window.") ?><br />
       <span class="kbd"><?php echo _("Ctrl") ?></span> + <span class="kbd"><?php echo 'A' ?></span> : <?php echo _("Select all messages in the current mailbox.") ?><br />
      </div>