Index: skins/default/templates/mail.html
===================================================================
--- skins/default/templates/mail.html (revision 2976)
+++ skins/default/templates/mail.html (working copy)
@@ -58,7 +58,8 @@
forwardedrepliedIcon="/images/icons/forwarded_replied.png"
attachmentIcon="/images/icons/attachment.png"
flaggedIcon="/images/icons/flagged.png"
- unflaggedIcon="/images/icons/blank.gif" />
+ unflaggedIcon="/images/icons/blank.gif"
+ unreadchildrenIcon="/images/icons/unread_children.png" />
@@ -87,6 +88,13 @@
+
+
+ :
+
+
+
+ |
:
Index: skins/default/mail.css
===================================================================
--- skins/default/mail.css (revision 2976)
+++ skins/default/mail.css (working copy)
@@ -318,6 +318,9 @@
#mailboxcontrols a,
#mailboxcontrols a:active,
#mailboxcontrols a:visited,
+#threadcontrols a,
+#threadcontrols a:active,
+#threadcontrols a:visited,
td.formlinks a,
td.formlinks a:visited
{
@@ -332,6 +335,12 @@
#mailboxcontrols a.active,
#mailboxcontrols a.active:active,
#mailboxcontrols a.active:visited,
+#threadcontrols a,
+#threadcontrols a:active,
+#threadcontrols a:visited,
+ul.toolbarmenu li a.active,
+ul.toolbarmenu li a.active:active,
+ul.toolbarmenu li a.active:visited,
td.formlinks a,
td.formlinks a:visited
{
@@ -339,12 +348,14 @@
}
#listcontrols a.active:hover,
-#mailboxcontrols a.active:hover
+#mailboxcontrols a.active:hover,
+#threadcontrols a.active:hover
{
text-decoration: underline;
}
-#listcontrols
+#listcontrols,
+#threadcontrols
{
padding-right: 2em;
}
@@ -655,6 +666,7 @@
}
#listcontrols,
+#threadcontrols,
#countcontrols,
#quotabox
{
@@ -781,6 +793,30 @@
cursor: pointer;
}
+#messagelist tr td div
+{
+ display: table-cell; /* For FireFox and Opera */
+ display: inline-block; /* For Opera and IE */
+ width: 15px;
+ height: 15px;
+}
+
+#messagelist tr td div.collapsed,
+#messagelist tr td div.expanded
+{
+ cursor: pointer;
+}
+
+#messagelist tr td div.collapsed
+{
+ background: url(images/icons/collapsed.png) center center no-repeat;
+}
+
+#messagelist tr td div.expanded
+{
+ background: url(images/icons/expanded.png) center center no-repeat;
+}
+
#messagelist tbody tr td.flag img:hover,
#messagelist thead tr td.flag img
{
Index: index.php
===================================================================
--- index.php (revision 2976)
+++ index.php (working copy)
@@ -224,6 +224,8 @@
'delete-folder' => 'manage_folders.inc',
'subscribe' => 'manage_folders.inc',
'unsubscribe' => 'manage_folders.inc',
+ 'enable-threading' => 'manage_folders.inc',
+ 'disable-threading' => 'manage_folders.inc',
'add-identity' => 'edit_identity.inc',
)
);
Index: SQL/mysql.initial.sql
===================================================================
--- SQL/mysql.initial.sql (revision 2976)
+++ SQL/mysql.initial.sql (working copy)
@@ -63,6 +63,29 @@
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+-- Table structure for table `threads`
+
+CREATE TABLE `threads` (
+ `thread_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `mailbox` varchar(128) /*!40101 CHARACTER SET ascii COLLATE ascii_general_ci */ NOT NULL,
+ `idx` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `has_children` tinyint(1) NOT NULL DEFAULT '0',
+ `is_root` tinyint(1) NOT NULL DEFAULT '0',
+ `root` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `depth` int(4) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY(`thread_id`),
+ UNIQUE `uniqueness` (`user_id`, `mailbox`, `idx`),
+ INDEX `is_root_index` (`is_root`),
+ INDEX `root_index` (`root`),
+ CONSTRAINT `user_id_fk_threads` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`)
+ /*!40008
+ ON DELETE CASCADE
+ ON UPDATE CASCADE */
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
-- Table structure for table `cache`
CREATE TABLE `cache` (
Index: config/main.inc.php.dist
===================================================================
--- config/main.inc.php.dist (revision 2976)
+++ config/main.inc.php.dist (working copy)
@@ -45,6 +45,10 @@
// this is recommended if the IMAP server does not run on the same machine
$rcmail_config['enable_caching'] = TRUE;
+// enable caching of threads in the local database.
+// this is not recommended unless you are using SQLite
+$rcmail_config['enable_thread_caching'] = FALSE;
+
// lifetime of message cache
// possible units: s, m, h, d, w
$rcmail_config['message_cache_lifetime'] = '10d';
@@ -76,6 +80,16 @@
$rcmail_config['imap_root'] = null;
$rcmail_config['imap_delimiter'] = null;
+// The default IMAP message THREAD retrieval algorithm.
+// A common one for threading would be REFERENCES.
+// Make sure that your IMAP server supports this!
+$rcmail_config['imap_thread_algorithm'] = 'REFERENCES';
+
+// 0 - Do not expand threads
+// 1 - Expand all threads automatically
+// 2 - Expand only threads with unread messages
+$rcmail_config['autoexpand_threads'] = 0;
+
// Automatically add this domain to user names for login
// Only for IMAP servers that require full e-mail addresses for login
// Specify an array with 'host' => 'domain' values to support multiple hosts
@@ -340,7 +354,7 @@
$rcmail_config['mime_magic'] = '/usr/share/misc/magic';
// default sort col
-$rcmail_config['message_sort_col'] = 'date';
+$rcmail_config['message_sort_col'] = 'default';
// default sort order
$rcmail_config['message_sort_order'] = 'DESC';
@@ -403,6 +417,9 @@
// default setting if preview pane is enabled
$rcmail_config['preview_pane'] = FALSE;
+// show messages in threads
+$rcmail_config['message_threading'] = false;
+
// focus new window if new message arrives
$rcmail_config['focus_on_new_message'] = true;
Index: program/include/rcube_imap.php
===================================================================
--- program/include/rcube_imap.php (revision 2976)
+++ program/include/rcube_imap.php (working copy)
@@ -52,6 +52,7 @@
var $sort_order = 'DESC';
var $delimiter = NULL;
var $caching_enabled = FALSE;
+ var $thread_caching_enabled = FALSE;
var $default_charset = 'ISO-8859-1';
var $struct_charset = NULL;
var $default_folders = array('INBOX');
@@ -70,6 +71,7 @@
var $debug_level = 1;
var $error_code = 0;
var $options = array('auth_method' => 'check');
+ var $threading = false;
private $host, $user, $pass, $port, $ssl;
@@ -491,7 +493,7 @@
$mailbox = $this->mailbox;
// count search set
- if ($this->search_string && $mailbox == $this->mailbox && $mode == 'ALL' && !$force)
+ if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force)
return count((array)$this->search_set);
$a_mailbox_cache = $this->get_cache('messagecount');
@@ -500,8 +502,12 @@
if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
return $a_mailbox_cache[$mailbox][$mode];
+ if ($this->get_capability('thread='.rcmail::get_instance()->config->get('imap_thread_algorithm')) &&
+ $mode == 'THREADS')
+ $count = $this->_threadcount($mailbox);
+
// RECENT count is fetched a bit different
- if ($mode == 'RECENT')
+ else if ($mode == 'RECENT')
$count = iil_C_CheckForRecent($this->conn, $mailbox);
// use SEARCH for message counting
@@ -537,7 +543,26 @@
return (int)$count;
}
+ function _threadcount($mailbox)
+ {
+ if (isset ($this->cache['__threads']['tree']))
+ return count($this->cache['__threads']['tree']);
+ if ($this->check_thread_cache_status($mailbox) > 0) {
+ $sql_result = $this->db->query("SELECT COUNT(*)
+ FROM " .
+ get_table_name('threads') . "
+ WHERE user_id=? AND mailbox=?",
+ $_SESSION['user_id'],
+ $mailbox);
+ if ($sql_arr = $this->db->fetch_array($sql_result))
+ return $sql_arr[0];
+ }
+ list ($thread_tree, $msg_depth, $has_children) = iil_C_Thread($this->conn, $mailbox, rcmail::get_instance()->config->get('imap_thread_algorithm'), 'ALL');
+ $this->update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children);
+ return count($thread_tree);
+ }
+
/**
* Public method for listing headers
* convert mailbox name with root dir first
@@ -574,66 +599,119 @@
$this->_set_sort_order($sort_field, $sort_order);
+ $message_threading = $this->get_capability('thread='.rcmail::get_instance()->config->get('imap_thread_algorithm')) &&
+ rcmail::get_instance()->imap->threading;
+
$page = $page ? $page : $this->list_page;
$cache_key = $mailbox.'.msg';
$cache_status = $this->check_cache_status($mailbox, $cache_key);
+ $headers_sorted = FALSE;
- // cache is OK, we can get all messages from local cache
- if ($cache_status>0)
+ if ($this->caching_enabled && $cache_status==-1 && !$recursive)
{
- $start_msg = ($page-1) * $this->page_size;
- $a_msg_headers = $this->get_message_cache($cache_key, $start_msg, $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
- $result = array_values($a_msg_headers);
- if ($slice)
- $result = array_slice($result, -$slice, $slice);
- return $result;
- }
- // cache is dirty, sync it
- else if ($this->caching_enabled && $cache_status==-1 && !$recursive)
- {
$this->sync_header_index($mailbox);
return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, TRUE, $slice);
}
- // retrieve headers from IMAP
- $a_msg_headers = array();
+ if ($message_threading) {
+ $thread_cache_status = $this->check_thread_cache_status($mailbox);
+ if ($thread_cache_status > 0 && $cache_status > 0) {
+ // use a JOIN of the threads and messages tables to get a sorted list of roots
+ $start_msg = ($page-1) * $this->page_size;
+ $roots = $this->get_thread_cache_roots_sorted($mailbox, $start_msg, $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
+ } else {
+ if ($thread_cache_status > 0) {
+ // get a list of thread roots from the cache
+ $roots = $this->get_thread_cache_roots($mailbox);
+ } else {
+ if (isset ($this->cache['__threads']['tree'])) {
+ $thread_tree = $this->cache['__threads']['tree'];
+ $msg_depth = $this->cache['__threads']['depth'];
+ $has_children = $this->cache['__threads']['has_children'];
+ } else {
+ list ($thread_tree, $msg_depth, $has_children) = iil_C_Thread($this->conn, $mailbox, rcmail::get_instance()->config->get('imap_thread_algorithm'), 'ALL');
+ }
+ $this->update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children);
+ // the keys of thread_tree are the thread roots
+ $roots = array_keys($thread_tree);
+ }
- if ($this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
- {
- list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
- $max = max($msg_index);
- $msg_index = array_slice($msg_index, $begin, $end-$begin);
+ if ($this->sort_field != 'default' &&
+ $this->get_capability('sort') && ($sort_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : ''))) {
+ // sort the roots according to the order defined by $sort_index
+ $sorter = new rcube_thread_root_sorter();
+ $sorter->set_sequence_numbers($sort_index);
+ $sorter->sort_threads($roots);
+ }
- if ($slice)
- $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
+ // get the roots for this page
+ list($begin, $end) = $this->_get_message_range(count($roots), $page);
+ $roots = array_slice($roots, $begin, $end - $begin);
+ if ($this->sort_order == 'DESC')
+ $roots = array_reverse($roots);
+ }
- // fetch reqested headers from server
- $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
- }
- else
- {
- $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", $this->sort_field, $this->skip_deleted);
+ if ($thread_cache_status > 0) {
+ // get the children of $roots from the cache
+ $threads = $this->get_thread_cache_threads($mailbox, $roots);
+ $msg_index = array ();
+ $msg_depth = array ();
+ $has_children = array ();
+ foreach ($threads as $thread) {
+ foreach ($thread as $msg) {
+ $idx = $msg['idx'];
+ $msg_index[] = $idx;
+ $msg_depth[$idx] = $msg['depth'];
+ $has_children[$idx] = $msg['has_children'];
+ }
+ }
+ } else {
+ // flatten the thread tree
+ $msg_index = array ();
+ foreach ($roots as $root) {
+ $msg_index[] = $root;
+ $msg_index = array_merge($msg_index, array_keys_recursive($thread_tree[$root]));
+ }
+ }
+ } else { // no threading
+ if ($this->sort_field != 'default' &&
+ $this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : ''))) {
+ // nothing more to do
+ } else {
+ $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", ($this->sort_field == 'default') ? '' : $this->sort_field, $this->skip_deleted);
- if (empty($a_index))
- return array();
+ if (empty($a_index))
+ return array();
- asort($a_index); // ASC
- $msg_index = array_keys($a_index);
- $max = max($msg_index);
- list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
- $msg_index = array_slice($msg_index, $begin, $end-$begin);
+ asort($a_index); // ASC
+ $msg_index = array_keys($a_index);
+ }
+ $max = max($msg_index);
+ list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
+ $msg_index = array_slice($msg_index, $begin, $end-$begin);
+ if ($this->sort_order == 'DESC')
+ $msg_index = array_reverse($msg_index);
+ }
- if ($slice)
- $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
+ // fetch requested headers from server
+ $msgs = join(",", $msg_index);
+ // cache is OK, we can get all messages from local cache
+ if ($cache_status > 0) {
+ // if $message_threading is true then no point sorting them as they will need re-sorting later
+ $a_msg_headers = $this->get_message_cache($cache_key, $message_threading?null:$this->sort_field, $this->sort_order, $msgs);
+ // if $message_threading is true then $a_msg_headers will need re-sorting as the tree structure won't have been preserved'
+ $headers_sorted = !$message_threading;
+ } else {
+ // retrieve headers from IMAP
+ $a_msg_headers = array();
+ $this->_fetch_headers($mailbox, $msgs, $a_msg_headers, $cache_key);
+ // delete cached messages with a higher index than $max+1
+ // Changed $max to $max+1 to fix this bug : #1484295
+ if ($message_threading)
+ $max = $this->_messagecount($mailbox, 'ALL');
+ $this->clear_message_cache($cache_key, $max + 1);
+ }
- // fetch reqested headers from server
- $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
- }
-
- // delete cached messages with a higher index than $max+1
- // Changed $max to $max+1 to fix this bug : #1484295
- $this->clear_message_cache($cache_key, $max + 1);
-
// kick child process to sync cache
// ...
@@ -641,13 +719,32 @@
if (!is_array($a_msg_headers) || empty($a_msg_headers))
return array();
- // use this class for message sorting
- $sorter = new rcube_header_sorter();
- $sorter->set_sequence_numbers($msg_index);
- $sorter->sort_headers($a_msg_headers);
+ if (!$headers_sorted)
+ {
+ // use this class for message sorting
+ $sorter = new rcube_header_sorter();
+ $sorter->set_sequence_numbers($msg_index);
+ $sorter->sort_headers($a_msg_headers);
+ }
- if ($this->sort_order == 'DESC')
- $a_msg_headers = array_reverse($a_msg_headers);
+ if ($message_threading) {
+ // Set depth, has_children and unread_children fields in headers
+ $parents = array();
+ foreach ($a_msg_headers as $uid => $headers) {
+ $id = $headers->id;
+ $depth = $msg_depth[$id];
+ $parents = array_slice($parents, 0, $depth - 1);
+ if (!$headers->seen) {
+ foreach ($parents as $parent)
+ $a_msg_headers[$parent]->unread_children++;
+ }
+ if (!empty($parents))
+ $a_msg_headers[$uid]->parent_uid = end($parents);
+ array_push($parents, $uid);
+ $a_msg_headers[$uid]->depth = $depth;
+ $a_msg_headers[$uid]->has_children = $has_children[$id];
+ }
+ }
return array_values($a_msg_headers);
}
@@ -888,7 +985,9 @@
}
// fetch complete message index
- if ($this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
+ $msg_count = $this->_messagecount($mailbox);
+ if ($this->sort_field != 'default' &&
+ $this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
{
if ($this->sort_order == 'DESC')
$a_index = array_reverse($a_index);
@@ -897,11 +996,11 @@
}
else
{
- $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", $this->sort_field, $this->skip_deleted);
+ $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", ($this->sort_field == 'default') ? '' : $this->sort_field, $this->skip_deleted);
if ($this->sort_order=="ASC")
asort($a_index);
- else if ($this->sort_order=="DESC")
+ else if ($this->sort_field == 'default' || $this->sort_order=="DESC")
arsort($a_index);
$this->cache[$key] = array_keys($a_index);
@@ -1032,7 +1131,7 @@
if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
$criteria = 'UNDELETED '.$criteria;
- if ($sort_field && $this->get_capability('sort'))
+ if ($sort_field && $sort_field != 'default' && $this->get_capability('sort'))
{
$charset = $charset ? $charset : $this->default_charset;
$a_messages = iil_C_Sort($this->conn, $mailbox, $sort_field, $criteria, FALSE, $charset);
@@ -2057,6 +2156,17 @@
/**
* @access private
*/
+ function set_thread_caching($set)
+ {
+ if ($set && is_object($this->db))
+ $this->thread_caching_enabled = TRUE;
+ else
+ $this->thread_caching_enabled = FALSE;
+ }
+
+ /**
+ * @access private
+ */
function get_cache($key)
{
// read cache
@@ -2269,25 +2379,29 @@
/**
* @access private
*/
- private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
+ private function get_message_cache($key, $sort_field, $sort_order, $msgs)
{
- $cache_key = "$key:$from:$to:$sort_field:$sort_order";
+ $cache_key = "$key:$sort_field:$sort_order:$msgs";
$db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
- if (!in_array($sort_field, $db_header_fields))
- $sort_field = 'idx';
-
if ($this->caching_enabled && !isset($this->cache[$cache_key]))
{
$this->cache[$cache_key] = array();
+ if (is_null($sort_field)) {
+ $sort = '';
+ } else {
+ if (!in_array($sort_field, $db_header_fields))
+ $sort_field = 'idx';
+ $sort = "ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".
+ strtoupper($sort_order);
+ }
$sql_result = $this->db->limitquery(
"SELECT idx, uid, headers
FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?
- ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
- $from,
- $to-$from,
+ AND idx IN ($msgs)
+ $sort",
$_SESSION['user_id'],
$key);
@@ -2488,6 +2602,180 @@
}
+ /* --------------------------------
+ * thread caching methods
+ * --------------------------------*/
+
+
+ /**
+ * Checks if the thread cache is up-to-date
+ *
+ * @param string Mailbox name
+ * @return int 0 = ok, -1 = invalid, -2 = cache disabled
+ */
+ function check_thread_cache_status($mailbox)
+ {
+ if (!$this->caching_enabled)
+ return -2;
+
+ $cache_uid = $this->get_cache('thread_cache_last_uid');
+ $cache_count = $this->get_cache('thread_cache_msg_count');
+ $msg_count = $this->_messagecount($mailbox, 'ALL', TRUE);
+
+ if ($cache_count[$mailbox] == $msg_count) {
+ if ($msg_count == 0)
+ return 1;
+
+ // get highest index
+ $header = iil_C_FetchHeader($this->conn, $mailbox, "$msg_count");
+
+ // uids of highest message matches -> cache seems OK
+ if ($cache_uid[$mailbox] == $header->uid)
+ return 1;
+
+ // cache is invaid
+ return -1;
+ } else {
+ return -1;
+ }
+ }
+
+ function update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children)
+ {
+ if (empty ($mailbox))
+ return;
+
+ // add to internal (fast) cache
+ $this->cache['__threads'] = array();
+ $this->cache['__threads']['tree'] = $thread_tree;
+ $this->cache['__threads']['depth'] = $msg_depth;
+ $this->cache['__threads']['has_children'] = $has_children;
+
+ // no further caching
+ if (!$this->thread_caching_enabled)
+ return;
+
+ $this->db->query("DELETE FROM " .
+ get_table_name('threads') ."
+ WHERE user_id = ?
+ AND mailbox = ?", $_SESSION['user_id'], $mailbox);
+ foreach ($thread_tree as $root => $subtree) {
+ $this->db->query("INSERT INTO " .
+ get_table_name('threads') . "
+ (user_id, mailbox, idx, has_children, is_root, root, depth)
+ VALUES (?, ?, ?, ?, ?, ?, ?)", $_SESSION['user_id'], $mailbox, $root, $has_children[$root] ? 1 : 0, 1, $root, $msg_depth[$root]);
+ $children = array_keys_recursive($subtree);
+ foreach ($children as $idx) {
+ $this->db->query("INSERT INTO " .
+ get_table_name('threads') . "
+ (user_id, mailbox, idx, has_children, is_root, root, depth)
+ VALUES (?, ?, ?, ?, ?, ?, ?)", $_SESSION['user_id'], $mailbox, $idx, $has_children[$idx] ? 1 : 0, 0, $root, $msg_depth[$idx]);
+ }
+ }
+
+ $cache_uid = $this->get_cache('thread_cache_last_uid');
+ $cache_count = $this->get_cache('thread_cache_msg_count');
+ $msg_count = $this->_messagecount($mailbox);
+ if ($msg_count > 0) {
+ $header = iil_C_FetchHeader($this->conn, $mailbox, "$msg_count");
+ $cache_uid[$mailbox] = $header->uid;
+ }
+ $cache_count[$mailbox] = $msg_count;
+ $this->update_cache('thread_cache_last_uid', $cache_uid);
+ $this->update_cache('thread_cache_msg_count', $cache_count);
+ }
+
+ function get_thread_cache_roots($mailbox)
+ {
+ if (!$this->thread_caching_enabled || empty ($mailbox))
+ return array ();
+
+ $cache_key = "threads:$mailbox";
+
+ if ($this->thread_caching_enabled && !isset ($this->cache[$cache_key])) {
+ $this->cache[$cache_key] = array ();
+
+ $sql_result = $this->db->query("SELECT idx
+ FROM " .
+ get_table_name('threads') . "
+ WHERE user_id=?
+ AND mailbox=?
+ AND is_root", $_SESSION['user_id'], $mailbox);
+
+ while ($sql_arr = $this->db->fetch_array($sql_result))
+ $this->cache[$cache_key][] = $sql_arr[0];
+ }
+
+ return $this->cache[$cache_key];
+ }
+
+ function get_thread_cache_roots_sorted($mailbox, $from, $to, $sort_field, $sort_order)
+ {
+ // empty key -> empty array
+ if (!$this->thread_caching_enabled || !$this->caching_enabled || empty ($mailbox))
+ return array ();
+
+ $cache_key = "threads:$mailbox:$from:$to:$sort_field:$sort_order";
+
+ if ($this->thread_caching_enabled && $this->caching_enabled && !isset ($this->cache[$cache_key])) {
+ $this->cache[$cache_key] = array ();
+
+ if ($sort_field == 'default')
+ $sort_field = 'thread_id';
+ $sql_result = $this->db->limitquery("SELECT threads.idx
+ FROM " .
+ get_table_name('threads') . "
+ INNER JOIN ".
+ get_table_name('messages') . "
+ ON threads.idx = messages.idx
+ AND threads.user_id = messages.user_id
+ WHERE threads.user_id=?
+ AND mailbox=?
+ AND cache_key=?
+ AND is_root
+ ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".
+ strtoupper($sort_order),
+ $from,
+ $to-$from,
+ $_SESSION['user_id'], $mailbox, $mailbox.'.msg');
+
+ while ($sql_arr = $this->db->fetch_array($sql_result))
+ $this->cache[$cache_key][] = $sql_arr[0];
+ }
+
+ return $this->cache[$cache_key];
+ }
+
+ function get_thread_cache_threads($mailbox, $roots)
+ {
+ // empty key -> empty array
+ if (!$this->thread_caching_enabled || empty ($mailbox))
+ return array ();
+
+ $cache_key = "threads:$mailbox:" . join(',', $roots);
+
+ if ($this->thread_caching_enabled && !isset ($this->cache[$cache_key])) {
+ $this->cache[$cache_key] = array ();
+ // setup the key order according to $roots
+ foreach ($roots as $root)
+ $this->cache[$cache_key][$root] = array();
+
+ $sql_result = $this->db->query("SELECT idx, has_children, root, depth
+ FROM " .
+ get_table_name('threads') . "
+ WHERE user_id=?
+ AND mailbox=?
+ AND root IN (" . join(',', $roots) .")
+ ORDER BY thread_id", $_SESSION['user_id'], $mailbox);
+
+ while ($sql_arr = $this->db->fetch_assoc($sql_result))
+ $this->cache[$cache_key][$sql_arr['root']][] = $sql_arr;
+ }
+
+ return $this->cache[$cache_key];
+ }
+
+
/* --------------------------------
* encoding/decoding methods
* --------------------------------*/
@@ -2943,6 +3231,10 @@
// add incremental value to messagecount
$a_mailbox_cache[$mailbox][$mode] += $increment;
+ // If ALL changes then THREADS may be invalid
+ if ($mode == 'ALL')
+ unset ($a_mailbox_cache[$mailbox]['THREADS']);
+
// there's something wrong, delete from cache
if ($a_mailbox_cache[$mailbox][$mode] < 0)
unset($a_mailbox_cache[$mailbox][$mode]);
@@ -3117,3 +3409,115 @@
return $posa - $posb;
}
}
+
+
+function array_keys_recursive($array)
+{
+ $keys = array ();
+ foreach ($array as $key => $child)
+ {
+ $keys[] = $key;
+ $keys = array_merge($keys, array_keys_recursive($child));
+ }
+ return $keys;
+}
+
+
+/**
+ * Class for sorting a thread tree in a predetermined order.
+ *
+ * @package Mail
+ * @author Chris January
+ */
+class rcube_thread_sorter
+{
+ var $sequence_numbers = array ();
+
+ /**
+ * Set the predetermined sort order.
+ *
+ * @param array Numerically indexed array of IMAP message sequence numbers
+ */
+ function set_sequence_numbers($seqnums)
+ {
+ $this->sequence_numbers = array_flip($seqnums);
+ }
+
+ /**
+ * Sort the array of header objects
+ *
+ * @param threads A thread tree
+ */
+ function sort_threads(& $threads)
+ {
+ uksort($threads, array (
+ $this,
+ "compare_seqnums"
+ ));
+ foreach (array_keys($threads) as $root)
+ {
+ $this->sort_threads($threads[$root]);
+ }
+ }
+
+ /**
+ * Sort method called by uasort()
+ */
+ function compare_seqnums($seqa, $seqb)
+ {
+ // find each sequence number in my ordered list
+ $posa = isset ($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
+ $posb = isset ($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
+
+ // return the relative position as the comparison value
+ return $posa - $posb;
+ }
+}
+
+
+/**
+ * Class for sorting thread roots in a predetermined order.
+ *
+ * @package Mail
+ * @author Chris January
+ */
+class rcube_thread_root_sorter
+{
+ var $sequence_numbers = array ();
+
+ /**
+ * Set the predetermined sort order.
+ *
+ * @param array Numerically indexed array of IMAP message sequence numbers
+ */
+ function set_sequence_numbers($seqnums)
+ {
+ $this->sequence_numbers = array_flip($seqnums);
+ }
+
+ /**
+ * Sort the array of header objects
+ *
+ * @param threads A thread tree
+ */
+ function sort_threads(& $threads)
+ {
+ usort($threads, array (
+ $this,
+ "compare_seqnums"
+ ));
+ }
+
+ /**
+ * Sort method called by uasort()
+ */
+ function compare_seqnums($seqa, $seqb)
+ {
+ // find each sequence number in my ordered list
+ $posa = isset ($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
+ $posb = isset ($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
+
+ // return the relative position as the comparison value
+ return $posa - $posb;
+ }
+}
Index: program/include/rcmail.php
===================================================================
--- program/include/rcmail.php (revision 2976)
+++ program/include/rcmail.php (working copy)
@@ -371,6 +371,10 @@
if ($this->config->get('enable_caching')) {
$this->imap->set_caching(true);
}
+
+ if ($this->config->get('enable_thread_caching')) {
+ $this->imap->set_thread_caching(true);
+ }
// set pagesize from config
$this->imap->set_pagesize($this->config->get('pagesize', 50));
Index: program/lib/imap.inc
===================================================================
--- program/lib/imap.inc (revision 2976)
+++ program/lib/imap.inc (working copy)
@@ -173,6 +173,9 @@
var $forwarded = false;
var $junk = false;
var $flagged = false;
+ var $has_children = false;
+ var $depth = 0;
+ var $unread_children = 0;
var $others = array();
}
@@ -1861,6 +1864,82 @@
return $result;
}
+// Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
+// 7 times instead :-) See comments on http://uk2.php.net/references and this article:
+// http://derickrethans.nl/files/phparch-php-variables-article.pdf
+function iil_ParseThread($str, $begin, $end, $root, $parent, $depth, &$depthmap, &$haschildren) {
+ $node = array();
+ if ($str[$begin] != '(') {
+ $stop = $begin + strspn($str, "1234567890", $begin, $end - $begin);
+ $msg = substr($str, $begin, $stop - $begin);
+ if ($msg == 0)
+ return $node;
+ if (is_null($root))
+ $root = $msg;
+ $depthmap[$msg] = $depth;
+ $haschildren[$msg] = false;
+ if (!is_null($parent))
+ $haschildren[$parent] = true;
+ if ($stop + 1 < $end)
+ $node[$msg] = iil_ParseThread($str, $stop + 1, $end, $root, $msg, $depth + 1, $depthmap, $haschildren);
+ else
+ $node[$msg] = array();
+ } else {
+ $off = $begin;
+ while ($off < $end) {
+ $start = $off;
+ $off++;
+ $n = 1;
+ while ($n > 0) {
+ $p = strpos($str, ')', $off);
+ if ($p === false) {
+ error_log('Mismatched brackets parsing IMAP THREAD response:');
+ error_log(substr($str, ($begin < 10)?0:($begin - 10), $end - $begin + 20));
+ error_log(str_repeat(' ', $off - (($begin < 10)?0:($begin - 10))));
+ return $node;
+ }
+ $p1 = strpos($str, '(', $off);
+ if ($p1 !== false && $p1 < $p) {
+ $off = $p1 + 1;
+ $n++;
+ } else {
+ $off = $p + 1;
+ $n--;
+ }
+ }
+ $node += iil_ParseThread($str, $start + 1, $off - 1, $root, $parent, $depth, $depthmap, $haschildren);
+ }
+ }
+
+ return $node;
+}
+
+function iil_C_Thread(&$conn, $folder, $algorithm, $criteria) {
+ $fp = $conn->fp;
+ if (iil_C_Select($conn, $folder)) {
+ $query = 'thrd1 THREAD ' . chop($algorithm). ' UTF-8 ' . chop($criteria);
+ iil_PutLineC($fp, $query);
+ do {
+ $line=trim(iil_ReadLine($fp, 10000));
+ if (eregi("^\* THREAD", $line)) {
+ $str = trim(substr($line, 8));
+ $depthmap = array();
+ $haschildren = array();
+ $tree = iil_ParseThread($str, 0, strlen($str), null, null, 1, $depthmap, $haschildren);
+ }
+ } while (!iil_StartsWith($line, 'thrd1', true));
+
+ $result_code = iil_ParseResult($line);
+ if ($result_code == 0) {
+ return array($tree, $depthmap, $haschildren);
+ }
+ $conn->error = 'iil_C_Thread: ' . $line . "\n";
+ return false;
+ }
+ $conn->error = "iil_C_Thread: Couldn't select \"$folder\"\n";
+ return false;
+}
+
function iil_C_Search(&$conn, $folder, $criteria) {
if (iil_C_Select($conn, $folder)) {
Index: program/localization/en_GB/labels.inc
===================================================================
--- program/localization/en_GB/labels.inc (revision 2976)
+++ program/localization/en_GB/labels.inc (working copy)
@@ -45,6 +45,7 @@
$labels['reply-to'] = 'Reply-To';
$labels['mailboxlist'] = 'Folders';
$labels['messagesfromto'] = 'Messages $from to $to of $count';
+$labels['threadsfromto'] = 'Threads $from to $to of $count';
$labels['messagenrof'] = 'Message $nr of $count';
$labels['moveto'] = 'Move to...';
$labels['download'] = 'Download';
@@ -124,6 +125,13 @@
$labels['unanswered'] = 'Unanswered';
$labels['deleted'] = 'Deleted';
$labels['invert'] = 'Invert';
+$labels['threads'] = 'Threads';
+$labels['expand-all'] = 'Expand All';
+$labels['collapse-all'] = 'Collapse All';
+$labels['autoexpand_threads'] = 'Autoexpand threads';
+$labels['do_expand'] = 'All threads';
+$labels['dont_expand'] = 'None';
+$labels['expand_only_unread'] = 'Only with unread messages';
$labels['filter'] = 'Filter';
$labels['compact'] = 'Compact';
$labels['empty'] = 'Empty';
@@ -253,6 +261,7 @@
$labels['foldername'] = 'Folder name';
$labels['subscribed'] = 'Subscribed';
$labels['messagecount'] = 'Messages';
+$labels['threaded'] = 'Threaded';
$labels['create'] = 'Create';
$labels['createfolder'] = 'Create new folder';
$labels['rename'] = 'Rename';
Index: program/localization/ru_RU/labels.inc
===================================================================
--- program/localization/ru_RU/labels.inc (revision 2976)
+++ program/localization/ru_RU/labels.inc (working copy)
@@ -45,6 +45,7 @@
$labels['reply-to'] = 'Ответить';
$labels['mailboxlist'] = 'Папки';
$labels['messagesfromto'] = 'Сообщения с $from по $to из $count';
+$labels['threadsfromto'] = 'Обсуждения с $from по $to из $count';
$labels['messagenrof'] = 'Сообщение $nr из $count';
$labels['moveto'] = 'Переместить в...';
$labels['download'] = 'Загрузить';
@@ -126,6 +127,13 @@
$labels['deleted'] = 'Удаленное';
$labels['invert'] = 'Инвертное';
$labels['filter'] = 'Фильтр';
+$labels['threads'] = 'Обсуждения';
+$labels['expand-all'] = 'Развернуть';
+$labels['collapse-all'] = 'Свернуть';
+$labels['autoexpand_threads'] = 'Разворачивать обсуждения';
+$labels['do_expand'] = 'Все обсуждения';
+$labels['dont_expand'] = 'Не разворачивать';
+$labels['expand_only_unread'] = 'Только с новыми сообщениями';
$labels['compact'] = 'Сжать';
$labels['empty'] = 'Опустошить';
$labels['purge'] = 'Очистить';
@@ -261,6 +269,7 @@
$labels['foldername'] = 'Имя папки';
$labels['subscribed'] = 'Подписан';
$labels['messagecount'] = 'Сообщения';
+$labels['threaded'] = 'Обсуждения';
$labels['create'] = 'Создать';
$labels['createfolder'] = 'Создать новую папку';
$labels['rename'] = 'Переименовать';
Index: program/localization/en_US/labels.inc
===================================================================
--- program/localization/en_US/labels.inc (revision 2976)
+++ program/localization/en_US/labels.inc (working copy)
@@ -56,6 +56,7 @@
$labels['mailboxlist'] = 'Folders';
$labels['messagesfromto'] = 'Messages $from to $to of $count';
+$labels['threadsfromto'] = 'Threads $from to $to of $count';
$labels['messagenrof'] = 'Message $nr of $count';
$labels['moveto'] = 'Move to...';
@@ -154,6 +155,15 @@
$labels['invert'] = 'Invert';
$labels['filter'] = 'Filter';
+$labels['threads'] = 'Threads';
+$labels['expand-all'] = 'Expand All';
+$labels['collapse-all'] = 'Collapse All';
+
+$labels['autoexpand_threads'] = 'Autoexpand threads';
+$labels['do_expand'] = 'All threads';
+$labels['dont_expand'] = 'None';
+$labels['expand_only_unread'] = 'Only with unread messages';
+
$labels['compact'] = 'Compact';
$labels['empty'] = 'Empty';
$labels['purge'] = 'Purge';
@@ -317,6 +327,7 @@
$labels['foldername'] = 'Folder name';
$labels['subscribed'] = 'Subscribed';
$labels['messagecount'] = 'Messages';
+$labels['threaded'] = 'Threaded';
$labels['create'] = 'Create';
$labels['createfolder'] = 'Create new folder';
$labels['rename'] = 'Rename';
Index: program/js/list.js
===================================================================
--- program/js/list.js (revision 2976)
+++ program/js/list.js (working copy)
@@ -76,7 +76,7 @@
for(var r=0; r= p.depth - 1)
+ {
+ last_expanded_parent_depth = p.depth;
+ new_row.style.display = 'table-row';
+ }
+ }
+ else
+ if (row && (! p || p.depth <= depth))
+ break;
+ }
+ }
+ }
+ new_row = new_row.nextSibling;
+ }
+
+ return false;
+},
+
+
+collapse_all: function(row)
+{
+ var depth, new_row;
+ var r;
+
+ if (row)
+ {
+ row.expanded = false;
+ depth = row.depth;
+ new_row = row.obj.nextSibling;
+ }
+ else
+ {
+ var tbody = this.list.tBodies[0];
+ new_row = tbody.firstChild;
+ depth = 0;
+ }
+
+ while (new_row) {
+ if (new_row.nodeType == 1)
+ {
+ var r = this.rows[new_row.uid];
+ if (row && r.depth <= depth)
+ break;
+ if (row || r.depth > 1)
+ new_row.style.display = 'none';
+ if (r.has_children) {
+ r.expanded = false;
+ var expando = document.getElementById('rcmexpando' + r.uid);
+ if (expando)
+ expando.className = 'collapsed';
+ }
+ }
+ new_row = new_row.nextSibling;
+ }
+
+ // Add nested messages to selection on collapse
+ if (row)
+ this.select_thread(row.uid, false);
+
+ return false;
+},
+
+expand_all: function(row)
+{
+ var depth, new_row;
+ var r;
+
+ if (row)
+ {
+ // Remove nested messages from selection on expand if only one thread is selected
+ if (this.get_single_selection() == row.uid)
+ this.select_thread(row.uid, true);
+
+ row.expanded = true;
+ depth = row.depth;
+ new_row = row.obj.nextSibling;
+ }
+ else
+ {
+ var tbody = this.list.tBodies[0];
+ new_row = tbody.firstChild;
+ depth = 0;
+ }
+
+ while (new_row)
+ {
+ if (new_row.nodeType == 1)
+ {
+ var r = this.rows[new_row.uid];
+ if (row && r.depth <= depth)
+ break;
+ new_row.style.display = 'table-row';
+ if (r.has_children)
+ {
+ r.expanded = true;
+ var expando = document.getElementById('rcmexpando' + r.uid);
+ if (expando)
+ expando.className = 'expanded';
+ }
+ }
+ new_row = new_row.nextSibling;
+ }
+ return false;
+},
+
+expand_unread: function()
+{
+ var tbody = this.list.tBodies[0];
+ new_row = tbody.firstChild;
+ var r;
+ var p;
+
+ while (new_row) {
+ if (new_row.nodeType == 1)
+ {
+ r = this.rows[new_row.uid];
+ p = this.rows[r.parent_uid];
+ if ((r.has_children && r.unread_children > 0) || (!r.has_children && r.unread))
+ {
+ new_row.style.display = 'table-row';
+ if (r.has_children)
+ {
+ r.expanded = true;
+ var expando = document.getElementById('rcmexpando' + r.uid);
+ if (expando)
+ expando.className = 'expanded';
+ }
+ }
+ else if (p && p.unread_children > 0)
+ {
+ // Show all neighbours with the same parent at the same depth level (parent is expanded)
+ new_row.style.display = 'table-row';
+ }
+ }
+ new_row = new_row.nextSibling;
+ }
+ return false;
+},
+
/**
* get next/previous/last rows that are not hidden
*/
@@ -492,7 +727,7 @@
if (this.selection[n]==id)
return true;
- return false;
+ return false;
},
@@ -513,7 +748,7 @@
if (!filter || (this.rows[n] && this.rows[n][filter] == true))
{
this.last_selected = n;
- this.highlight_row(n, true);
+ this.highlight_row(n, true, true);
}
else if (this.rows[n])
{
@@ -543,7 +778,7 @@
var select_before = this.selection.join(',');
for (var n in this.rows)
- this.highlight_row(n, true);
+ this.highlight_row(n, true, true);
// trigger event if selection changed
if (this.selection.join(',') != select_before)
@@ -590,9 +825,9 @@
/**
* Getter for the selection array
*/
-get_selection: function()
+get_selection: function(action)
{
- return this.selection;
+ return this.selection;
},
@@ -603,15 +838,61 @@
{
if (this.selection.length == 1)
return this.selection[0];
+ else if (this.selection.length > 1)
+ { // Return first element only if it is a threaded list and only one thread is in a selection
+ if (!this.rows[this.selection[0]].depth)
+ return null;
+ for (var n = 1; n < this.selection.length; n++)
+ if (this.rows[this.selection[n]] && this.rows[this.selection[n]].depth)
+ if (this.rows[this.selection[n]].depth <= this.rows[this.selection[0]].depth)
+ return null;
+ return this.selection[0];
+ }
else
return null;
},
+/**
+ * Helper function to expand selection to nested rows of a thread
+ */
+select_thread: function(id, unselect)
+{
+ var current_row = this.rows[id];
+ var starting_depth;
+ if (current_row.has_children && !current_row.expanded)
+ {
+ // move to the next row and record the depth
+ id = current_row.obj.nextElementSibling.uid;
+ current_row = this.rows[id];
+ starting_depth = current_row.depth;
+ do
+ {
+ if (unselect == true && this.in_selection(id)) // unselect row
+ {
+ var p = find_in_array(id, this.selection);
+ var a_pre = this.selection.slice(0, p);
+ var a_post = this.selection.slice(p+1, this.selection.length);
+ this.selection = a_pre.concat(a_post);
+ $(this.rows[id].obj).removeClass('selected').removeClass('unfocused');
+ }
+ else if (unselect != true && !this.in_selection(id)) // select row
+ {
+ this.selection[this.selection.length] = id;
+ $(this.rows[id].obj).addClass('selected');
+ }
+ if (!current_row.obj.nextElementSibling)
+ break;
+ id = current_row.obj.nextElementSibling.uid;
+ current_row = this.rows[id];
+ } while (current_row.depth >= starting_depth);
+ }
+},
+
/**
* Highlight/unhighlight a row
*/
-highlight_row: function(id, multiple)
+highlight_row: function(id, multiple, skip_threads)
{
if (this.rows[id] && !multiple)
{
@@ -620,6 +901,8 @@
this.clear_selection();
this.selection[0] = id;
$(this.rows[id].obj).addClass('selected');
+ if (!skip_threads)
+ this.select_thread(id);
}
}
else if (this.rows[id])
@@ -628,6 +911,8 @@
{
this.selection[this.selection.length] = id;
$(this.rows[id].obj).addClass('selected');
+ if (!skip_threads)
+ this.select_thread(id);
}
else // unselect row
{
@@ -636,6 +921,8 @@
var a_post = this.selection.slice(p+1, this.selection.length);
this.selection = a_pre.concat(a_post);
$(this.rows[id].obj).removeClass('selected').removeClass('unfocused');
+ if (!skip_threads)
+ this.select_thread(id, true);
}
}
},
@@ -661,6 +948,16 @@
// Stop propagation so that the browser doesn't scroll
rcube_event.cancel(e);
return this.use_arrow_key(keyCode, mod_key);
+ case 61:
+ case 107: // Plus sign on a numeric keypad (fc11 + firefox 3.5.2)
+ case 109:
+ case 32:
+ // Stop propagation
+ rcube_event.cancel(e);
+ var ret = this.use_plusminus_key(keyCode, mod_key);
+ this.key_pressed = keyCode;
+ this.triggerEvent('keypress');
+ return ret;
default:
this.shiftkey = e.shiftKey;
this.key_pressed = keyCode;
@@ -688,6 +985,10 @@
case 38:
case 63233:
case 63232:
+ case 61:
+ case 107:
+ case 109:
+ case 32:
if (!rcube_event.get_modifier(e) && this.focused)
return rcube_event.cancel(e);
@@ -722,6 +1023,34 @@
/**
+ * Special handling method for +/- keys
+ */
+use_plusminus_key: function(keyCode, mod_key)
+{
+ var last_selected_row = this.rows[this.last_selected];
+ if (!last_selected_row)
+ return;
+ if (keyCode == 32)
+ keyCode = last_selected_row.expanded ? 109 : 61;
+ if (keyCode == 61 || keyCode == 107)
+ if (mod_key == CONTROL_KEY)
+ this.expand_all(last_selected_row);
+ else
+ this.expand(last_selected_row);
+ else
+ if (mod_key == CONTROL_KEY)
+ this.collapse_all(last_selected_row);
+ else
+ this.collapse(last_selected_row);
+ var expando = document.getElementById('rcmexpando' + last_selected_row.uid);
+ if (expando)
+ expando.className = last_selected_row.expanded?'expanded':'collapsed';
+
+ return false;
+},
+
+
+/**
* Try to scroll the list to make the specified row visible
*/
scrollto: function(id)
@@ -766,17 +1095,17 @@
break;
}
- if (this.rows[this.selection[n]].obj)
+ if (obj = this.rows[this.selection[n]].obj)
{
- obj = this.rows[this.selection[n]].obj;
subject = '';
for(c=0, i=0; i= 0 && this.subject_col == c)))
+ if (((node = obj.childNodes[i].childNodes[obj.childNodes[i].childElementCount - 1]) ||
+ (node = obj.childNodes[i].firstChild) && (node.nodeType==3 || node.nodeName=='A')) &&
+ (this.subject_col < 0 || (this.subject_col >= 0 && this.subject_col == c)))
{
if (n == 0) {
if (node.nodeType == 3)
Index: program/js/app.js
===================================================================
--- program/js/app.js (revision 2976)
+++ program/js/app.js (working copy)
@@ -179,8 +179,22 @@
this.gui_objects.mailcontframe.onmousedown = function(e){ return p.click_on_list(e); };
else
this.message_list.focus();
+
+ switch (this.env.autoexpand_threads) {
+ case 2:
+ this.message_list.expand_unread();
+ break;
+ case 1:
+ this.message_list.expand_all();
+ break;
+ case 0:
+ default:
+ break;
+ }
+ this.message_list.expand(null);
+ this.save_expand_state();
}
-
+
if (this.env.coltypes)
this.set_message_coltypes(this.env.coltypes);
@@ -244,8 +258,13 @@
}
if (this.env.messagecount)
- this.enable_command('select-all', 'select-none', 'expunge', true);
+ this.enable_command('select-all', 'select-none', 'expunge', 'expand-all', 'collapse-all', true);
+ if (this.env.threaded_display) {
+ this.set_threaded(true);
+ } else {
+ this.set_threaded(false);
+ }
if (this.purge_mailbox_test())
this.enable_command('purge', true);
@@ -333,7 +352,7 @@
this.enable_command('save', 'delete', 'edit', true);
}
else if (this.env.action=='folders')
- this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', 'delete-folder', true);
+ this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', 'delete-folder', 'enable-threading', 'disable-threading', true);
if (this.gui_objects.identitieslist)
{
@@ -424,6 +443,11 @@
row.replied = this.env.messages[uid].replied ? true : false;
row.flagged = this.env.messages[uid].flagged ? true : false;
row.forwarded = this.env.messages[uid].forwarded ? true : false;
+ row.has_children = this.env.messages[uid].has_children ? true : false;
+ row.expanded = this.env.messages[uid].expanded ? true : false;
+ row.depth = this.env.messages[uid].depth;
+ row.unread_children = this.env.messages[uid].unread_children;
+ row.parent_uid = this.env.messages[uid].parent_uid;
}
// set eventhandler to message icon
@@ -440,7 +464,7 @@
{
var found;
if((found = find_in_array('flag', this.env.coltypes)) >= 0)
- this.set_env('flagged_col', found+1);
+ this.set_env('flagged_col', found);
}
// set eventhandler to flag icon, if icon found
@@ -453,6 +477,15 @@
}
this.triggerEvent('insertrow', { uid:uid, row:row });
+
+ // expando is handled here rather than in rcube_list_widget so that the
+ // expanded state may be persisted in this.env.messages
+ var expando = document.getElementById('rcmexpando' + uid);
+ if (expando != null)
+ {
+ var p = this;
+ expando.onmousedown = function(e) { return p.expando_clicked(e, uid); };
+ }
};
// init message compose form: set focus and eventhandlers
@@ -613,13 +646,32 @@
case 'sort':
- var sort_order, sort_col = props;
+ var sort_order = null, sort_col = props;
- if (this.env.sort_col==sort_col)
+ // date column cycles between date and default
+ if (sort_col == 'date' && this.env.sort_col=='date' && this.env.sort_order=='ASC')
+ {
+ sort_col = 'default';
+ sort_order = 'DESC';
+ }
+ else if (sort_col == 'date' && this.env.sort_col=='default' && this.env.sort_order=='DESC')
+ {
+ sort_col = 'date';
+ sort_order = 'DESC';
+ }
+ // no sort order specified: toggle
+ else if (this.env.sort_col==sort_col)
+ {
sort_order = this.env.sort_order=='ASC' ? 'DESC' : 'ASC';
+ }
else
- sort_order = 'ASC';
-
+ {
+ sort_order = 'ASC';
+ }
+
+ if (this.env.sort_col==sort_col && this.env.sort_order==sort_order)
+ break;
+
// set table header class
$('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase()));
$('#rcm'+sort_col).addClass('sorted'+sort_order);
@@ -742,7 +794,22 @@
case 'delete':
// mail task
if (this.task=='mail')
+ {
this.delete_messages();
+ if (this.env.threaded_display)
+ {
+ // It is very hard to re-thread message list if some messages were removed from
+ // the middle of a thread (we need to fully implement RFC5256 algorythm, because
+ // we are a "disconnected client" if we re-threading without a servers help)
+ // Reload message list
+ var sort_param;
+ if (this.env.sort_col && this.env.sort_order)
+ {
+ sort_param = this.env.sort_col +'_'+ this.env.sort_order
+ }
+ this.list_mailbox(this.env.mailbox, this.env.current_page, sort_param);
+ }
+ }
// addressbook task
else if (this.task=='addressbook')
this.delete_contacts();
@@ -756,7 +823,19 @@
case 'move':
case 'moveto':
if (this.task == 'mail')
+ {
this.move_messages(props);
+ if (this.env.threaded_display)
+ {
+ // The same as for 'delete'
+ var sort_param;
+ if (this.env.sort_col && this.env.sort_order)
+ {
+ sort_param = this.env.sort_col +'_'+ this.env.sort_order
+ }
+ this.list_mailbox(this.env.mailbox, this.env.current_page, sort_param);
+ }
+ }
else if (this.task == 'addressbook' && this.drag_active)
this.copy_contact(null, props);
break;
@@ -846,6 +925,16 @@
this.message_list.clear_selection();
break;
+ case 'expand-all':
+ this.message_list.expand_all();
+ this.save_expand_state();
+ break;
+
+ case 'collapse-all':
+ this.message_list.collapse_all();
+ this.save_expand_state();
+ break;
+
case 'nextmessage':
if (this.env.next_uid)
this.show_message(this.env.next_uid, false, this.env.action=='preview');
@@ -1107,7 +1196,15 @@
case 'unsubscribe':
this.unsubscribe_folder(props);
break;
-
+
+ case 'enable-threading':
+ this.enable_threading(props);
+ break;
+
+ case 'disable-threading':
+ this.disable_threading(props);
+ break;
+
case 'create-folder':
this.create_folder(props);
break;
@@ -1441,7 +1538,7 @@
if (this.preview_timer)
clearTimeout(this.preview_timer);
- var selected = list.selection.length==1;
+ var selected = list.get_single_selection() != null;
// Hide certain command buttons when Drafts folder is selected
if (this.env.mailbox == this.env.drafts_mailbox)
@@ -1483,6 +1580,8 @@
this.command('delete');
else if (list.key_pressed == list.BACKSPACE_KEY)
this.command('delete');
+ else if (list.key_pressed == 32 || list.key_pressed == 107 || list.key_pressed == 109 || list.key_pressed == 61)
+ this.save_expand_state();
else
list.shiftkey = false;
};
@@ -1545,6 +1644,7 @@
if (action == 'preview' && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread)
{
this.set_message(id, 'unread', false);
+ this.update_parents(id, 'read');
if (this.env.unread_counts[this.env.mailbox])
{
this.env.unread_counts[this.env.mailbox] -= 1;
@@ -1730,6 +1830,24 @@
|| this.env.mailbox.match('^' + RegExp.escape(this.env.junk_mailbox) + RegExp.escape(this.env.delimiter))));
};
+ // update parents in a thread
+ this.update_parents = function(uid, flag)
+ {
+ var r = this.message_list.rows[uid];
+ if (r.parent_uid) {
+ var p = this.message_list.rows[r.parent_uid];
+ if (flag == 'read' && p.unread_children > 0) {
+ p.unread_children--;
+ } else if (flag == 'unread') {
+ p.unread_children++;
+ } else {
+ return;
+ }
+ this.set_message_icon(r.parent_uid);
+ this.update_parents(r.parent_uid, flag);
+ }
+ };
+
// set message icon
this.set_message_icon = function(uid)
{
@@ -1738,8 +1856,10 @@
if (!rows[uid])
return false;
-
- if (rows[uid].deleted && this.env.deletedicon)
+ if (!rows[uid].unread && rows[uid].unread_children > 0 && this.env.unreadchildrenicon) {
+ icn_src = this.env.unreadchildrenicon;
+ }
+ else if (rows[uid].deleted && this.env.deletedicon)
icn_src = this.env.deletedicon;
else if (rows[uid].replied && this.env.repliedicon)
{
@@ -1940,7 +2060,7 @@
{
var a_uids = new Array();
var r_uids = new Array();
- var selection = this.message_list ? this.message_list.get_selection() : new Array();
+ var selection = this.message_list ? this.message_list.get_selection('mark') : new Array();
if (uid)
a_uids[0] = uid;
@@ -2000,6 +2120,9 @@
this.set_message(a_uids[i], 'unread', (flag=='unread' ? true : false));
this.http_post('mark', '_uid='+a_uids.join(',')+'&_flag='+flag);
+
+ for (var i=0; i 0 && this.env.unreadchildrenicon)
+ icon = this.env.unreadchildrenicon;
+ else if (flags.deleted && this.env.deletedicon)
icon = this.env.deletedicon;
else if (flags.replied && this.env.repliedicon)
{
@@ -3779,29 +3954,45 @@
icon = this.env.forwardedicon;
else if(flags.unread && this.env.unreadicon)
icon = this.env.unreadicon;
+ var tree = '';
+ if (depth > 0)
+ {
+ // XXX: This assumes that div width is hardcoded to 15px,
+ // Chris did it a bit differently in an original patch, he was adding so much divs as depth is
+ // I replaced logic in list.js:drag_mouse_move() so subject text is picked defferently, so
+ // either method of could be used (that was only the one problem I noted with these added divs).
+ // The same is true for an offline list (program/steps/mail/func.inc:rcmail_message_list()).
+ // Bubble
+ var width = (depth - 1) * 15;
+ tree += '   ';
+ tree += flags.has_children?' ':' ';
+ if (depth > 1)
+ row.style.display = 'none';
+ }
- // add icon col
- var col = document.createElement('td');
- col.className = 'icon';
- col.innerHTML = icon ? ' ' : '';
- row.appendChild(col);
-
+ tree += icon ? ' ' : '';
+
// add each submitted col
for (var n = 0; n < this.coltypes.length; n++) {
var c = this.coltypes[n];
col = document.createElement('td');
col.className = String(c).toLowerCase();
-
- if (c=='flag') {
+
+ var html;
+ if (c=='flag')
+ {
if (flags.flagged && this.env.flaggedicon)
- col.innerHTML = ' ';
+ html = ' ';
else if(!flags.flagged && this.env.unflaggedicon)
- col.innerHTML = ' ';
- }
+ html = ' ';
+ }
else if (c=='attachment')
- col.innerHTML = (attachment && this.env.attachmenticon ? ' ' : ' ');
+ html = attachment && this.env.attachmenticon ? ' ' : ' ';
else
- col.innerHTML = cols[c];
+ html = cols[c];
+ if (n == 0)
+ html = tree + html;
+ col.innerHTML = html;
row.appendChild(col);
}
@@ -3816,6 +4007,7 @@
}
};
+
// messages list handling in background (for performance)
this.offline_message_list = function(flag)
{
@@ -3823,6 +4015,12 @@
this.message_list.set_background_mode(flag);
};
+ // expand any threads that were open
+ this.expand_threads = function()
+ {
+ this.message_list.expand(null);
+ }
+
// replace content of row count display
this.set_rowcount = function(text)
{
@@ -3913,6 +4111,14 @@
}
};
+ // replace content of row count display
+ this.set_threaded = function(threaded)
+ {
+ var controls = document.getElementById("threadcontrols");
+ if (controls)
+ controls.style.display = threaded?'inline':'none';
+ };
+
// notifies that a new message(s) has hit the mailbox
this.new_message_focus = function()
{
@@ -4106,7 +4312,8 @@
// disable commands useless when mailbox is empty
this.enable_command('show', 'reply', 'reply-all', 'forward', 'moveto', 'delete',
'mark', 'viewsource', 'open', 'edit', 'download', 'print', 'load-attachment',
- 'purge', 'expunge', 'select-all', 'select-none', 'sort', false);
+ 'purge', 'expunge', 'select-all', 'select-none', 'sort', 'expand-all',
+ 'colapse-all', false);
}
break;
@@ -4116,7 +4323,7 @@
if (this.task == 'mail') {
if (this.message_list && response.action == 'list')
this.msglist_select(this.message_list);
- this.enable_command('show', 'expunge', 'select-all', 'select-none', 'sort', (this.env.messagecount > 0));
+ this.enable_command('show', 'expunge', 'select-all', 'select-none', 'sort', 'expand-all', 'collapse-all', (this.env.messagecount > 0));
this.enable_command('purge', this.purge_mailbox_test());
if (response.action == 'list')
@@ -4249,7 +4456,6 @@
} // end object rcube_webmail
-
// copy event engine prototype
rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
Index: program/steps/settings/save_prefs.inc
===================================================================
--- program/steps/settings/save_prefs.inc (revision 2976)
+++ program/steps/settings/save_prefs.inc (working copy)
@@ -41,6 +41,7 @@
$a_user_prefs = array(
'focus_on_new_message' => isset($_POST['_focus_on_new_message']) ? TRUE : FALSE,
'preview_pane' => isset($_POST['_preview_pane']) ? TRUE : FALSE,
+ 'autoexpand_threads' => isset($_POST['_autoexpand_threads']) ? intval($_POST['_autoexpand_threads']) : 0,
'mdn_requests' => isset($_POST['_mdn_requests']) ? intval($_POST['_mdn_requests']) : 0,
'keep_alive' => isset($_POST['_keep_alive']) ? intval($_POST['_keep_alive'])*60 : $CONFIG['keep_alive'],
'check_all_folders' => isset($_POST['_check_all_folders']) ? TRUE : FALSE,
Index: program/steps/settings/edit_prefs.inc
===================================================================
--- program/steps/settings/edit_prefs.inc (revision 2976)
+++ program/steps/settings/edit_prefs.inc (working copy)
@@ -201,6 +201,21 @@
);
}
+// $RCMAIL->imap_init(true);
+
+ if (!isset($no_override['autoexpand_threads'])) {
+ $field_id = 'rcmfd_autoexpand_threads';
+ $select_autoexpand_threads = new html_select(array('name' => '_autoexpand_threads', 'id' => $field_id));
+ $select_autoexpand_threads->add(rcube_label('dont_expand'), 0);
+ $select_autoexpand_threads->add(rcube_label('do_expand'), 1);
+ $select_autoexpand_threads->add(rcube_label('expand_only_unread'), 2);
+
+ $blocks['main']['options']['autoexpand_threads'] = array(
+ 'title' => html::label($field_id, Q(rcube_label('autoexpand_threads'))),
+ 'content' => $select_autoexpand_threads->show($config['autoexpand_threads']),
+ );
+ }
+
if (!isset($no_override['mdn_requests'])) {
$field_id = 'rcmfd_mdn_requests';
$select_mdn_requests = new html_select(array('name' => '_mdn_requests', 'id' => $field_id));
Index: program/steps/settings/manage_folders.inc
===================================================================
--- program/steps/settings/manage_folders.inc (revision 2976)
+++ program/steps/settings/manage_folders.inc (working copy)
@@ -38,6 +38,28 @@
$IMAP->unsubscribe(array($mbox));
}
+// enable threading for one or more mailboxes
+else if ($RCMAIL->action=='enable-threading')
+ {
+ if ($mbox = get_input_value('_mbox', RCUBE_INPUT_POST, false, 'UTF7-IMAP'))
+ $a_user_prefs = $USER->get_prefs();
+ if (!is_array($a_user_prefs['message_threading']))
+ $a_user_prefs['message_threading'] = array();
+ $a_user_prefs['message_threading'][$mbox] = true;
+ $USER->save_prefs($a_user_prefs);
+ }
+
+// enable threading for one or more mailboxes
+else if ($RCMAIL->action=='disable-threading')
+ {
+ if ($mbox = get_input_value('_mbox', RCUBE_INPUT_POST, false, 'UTF7-IMAP'))
+ $a_user_prefs = $USER->get_prefs();
+ if (!is_array($a_user_prefs['message_threading']))
+ $a_user_prefs['message_threading'] = array();
+ unset($a_user_prefs['message_threading'][$mbox]);
+ $USER->save_prefs($a_user_prefs);
+ }
+
// create a new mailbox
else if ($RCMAIL->action=='create-folder')
{
@@ -161,6 +183,8 @@
{
global $IMAP, $CONFIG, $OUTPUT;
+ $threading_supported = $IMAP->get_capability('thread=references');
+
list($form_start, $form_end) = get_form_tags($attrib, 'folders');
unset($attrib['form']);
@@ -173,6 +197,8 @@
$table->add_header('name', rcube_label('foldername'));
$table->add_header('msgcount', rcube_label('messagecount'));
$table->add_header('subscribed', rcube_label('subscribed'));
+ if ($threading_supported)
+ $table->add_header('threaded', rcube_label('threaded'));
$table->add_header('rename', ' ');
$table->add_header('delete', ' ');
@@ -182,6 +208,7 @@
$a_unsubscribed = $IMAP->list_unsubscribed();
$a_subscribed = $IMAP->list_mailboxes();
+ $a_threaded =rcmail::get_instance()->config->get('message_threading', array());
$delimiter = $IMAP->get_hierarchy_delimiter();
$a_js_folders = $seen_folders = $list_folders = array();
@@ -211,6 +238,10 @@
'name' => '_subscribed[]',
'onclick' => JS_OBJECT_NAME.".command(this.checked?'subscribe':'unsubscribe',this.value)",
));
+ $checkbox_threaded = new html_checkbox(array(
+ 'name' => '_threaded[]',
+ 'onclick' => JS_OBJECT_NAME.".command(this.checked?'enable-threading':'disable-threading',this.value)",
+ ));
if (!empty($attrib['deleteicon']))
$del_button = html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete')));
@@ -226,6 +257,7 @@
foreach ($list_folders as $i => $folder) {
$idx = $i + 1;
$subscribed = in_array($folder['id'], $a_subscribed);
+ $threaded = $a_threaded[$folder['id']];
$protected = ($CONFIG['protect_default_folders'] == true && in_array($folder['id'], $CONFIG['default_imap_folders']));
$classes = array($i%2 ? 'even' : 'odd');
$folder_js = JQ($folder['id']);
@@ -238,9 +270,13 @@
$table->add_row(array('id' => 'rcmrow'.$idx, 'class' => join(' ', $classes)));
$table->add('name', Q($display_folder));
- $table->add('msgcount', ($folder['virtual'] ? '' : $IMAP->messagecount($folder['id'])));
+ $table->add('msgcount', ($folder['virtual'] ? '' : $IMAP->messagecount($folder['id']))); // XXX: Use THREADS or ALL?
$table->add('subscribed', ($protected || $folder['virtual']) ? ($subscribed ? ' •' : ' ') :
$checkbox_subscribe->show(($subscribed ? $folder_utf8 : ''), array('value' => $folder_utf8)));
+ if ($IMAP->get_capability('thread=references')) {
+ $table->add('threaded',
+ $checkbox_threaded->show(($threaded ? $folder_utf8 : ''), array('value' => $folder_utf8)));
+ }
// add rename and delete buttons
if (!$protected && !$folder['virtual']) {
Index: program/steps/mail/rss.inc
===================================================================
--- program/steps/mail/rss.inc (revision 2976)
+++ program/steps/mail/rss.inc (working copy)
@@ -42,7 +42,7 @@
$webmail_url .= '?_task=mail';
$messagecount_unread = $IMAP->messagecount('INBOX', 'UNSEEN', TRUE);
-$messagecount = $IMAP->messagecount();
+$messagecount = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
$sort_col = 'date';
$sort_order = 'DESC';
Index: program/steps/mail/list.inc
===================================================================
--- program/steps/mail/list.inc (revision 2976)
+++ program/steps/mail/list.inc (working copy)
@@ -57,7 +57,7 @@
// fetch message headers
-if ($count = $IMAP->messagecount($mbox_name, 'ALL', !empty($_REQUEST['_refresh'])))
+if ($count = $IMAP->messagecount($mbox_name, rcmail::get_instance()->imap->threading?'THREADS':'ALL', !empty($_REQUEST['_refresh'])))
$a_headers = $IMAP->list_headers($mbox_name, NULL, $sort_col, $sort_order);
// update mailboxlist
@@ -69,6 +69,7 @@
$OUTPUT->set_env('pagecount', $pages);
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($count));
$OUTPUT->command('set_mailboxname', rcmail_get_mailbox_name_text());
+$OUTPUT->command('set_threaded', rcmail::get_instance()->imap->threading);
// add message rows
if (isset($a_headers) && count($a_headers))
@@ -82,6 +83,23 @@
else
$OUTPUT->show_message('nomessagesfound', 'notice');
+// deal with threaded view
+if (rcmail::get_instance()->imap->threading) {
+ switch ($RCMAIL->config->get('autoexpand_threads')) {
+ case 2:
+ $OUTPUT->command('message_list.expand_unread');
+ break;
+ case 1:
+ $OUTPUT->command('message_list.expand_all');
+ break;
+ case 0:
+ default:
+ break;
+ }
+ $OUTPUT->command('expand_threads');
+ $OUTPUT->command('save_expand_state');
+}
+
// send response
$OUTPUT->send();
Index: program/steps/mail/check_recent.inc
===================================================================
--- program/steps/mail/check_recent.inc (revision 2976)
+++ program/steps/mail/check_recent.inc (working copy)
@@ -28,9 +28,9 @@
// refresh saved search set
if (($search_request = get_input_value('_search', RCUBE_INPUT_GPC)) && isset($_SESSION['search'][$search_request])) {
$_SESSION['search'][$search_request] = $IMAP->refresh_search();
- $all_count = $IMAP->messagecount();
+ $all_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
} else {
- $all_count = $IMAP->messagecount(NULL, 'ALL', TRUE);
+ $all_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL', TRUE);
}
$unread_count = $IMAP->messagecount(NULL, 'UNSEEN', TRUE);
@@ -55,18 +55,27 @@
if (empty($_GET['_list']))
continue;
- // use SEARCH/SORT to find recent messages
- $search_str = 'RECENT';
- if ($search_request)
- $search_str .= ' '.$IMAP->search_string;
+ if (rcmail::get_instance()->imap->threading) {
+ $OUTPUT->command('message_list.clear');
+ $sort_col = isset($_SESSION['sort_col']) ? $_SESSION['sort_col'] : $CONFIG['message_sort_col'];
+ $sort_order = isset($_SESSION['sort_order']) ? $_SESSION['sort_order'] : $CONFIG['message_sort_order'];
+ $result_h = $IMAP->list_headers($mbox_name, NULL, $sort_col, $sort_order);
+ // add to the list
+ rcmail_js_message_list($result_h);
+ } else {
+ // use SEARCH/SORT to find recent messages
+ $search_str = 'RECENT';
+ if ($search_request)
+ $search_str .= ' '.$IMAP->search_string;
- $result = $IMAP->search($mbox_name, $search_str, NULL, 'date');
+ $result = $IMAP->search($mbox_name, $search_str, NULL, 'date');
- if ($result) {
- // get the headers
- $result_h = $IMAP->list_headers($mbox_name, 1, 'date', 'DESC');
- // add to the list
- rcmail_js_message_list($result_h, true, false);
+ if ($result) {
+ // get the headers
+ $result_h = $IMAP->list_headers($mbox_name, 1, 'date', 'DESC');
+ // add to the list
+ rcmail_js_message_list($result_h, TRUE);
+ }
}
}
else {
Index: program/steps/mail/move_del.inc
===================================================================
--- program/steps/mail/move_del.inc (revision 2976)
+++ program/steps/mail/move_del.inc (working copy)
@@ -24,7 +24,7 @@
return;
// count messages before changing anything
-$old_count = $IMAP->messagecount();
+$old_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
$old_pages = ceil($old_count / $IMAP->page_size);
// move messages
@@ -82,7 +82,7 @@
}
else
{
- $msg_count = $IMAP->messagecount();
+ $msg_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
$pages = ceil($msg_count / $IMAP->page_size);
$nextpage_count = $old_count - $IMAP->page_size * $IMAP->list_page;
$remaining = $msg_count - $IMAP->page_size * ($IMAP->list_page - 1);
@@ -122,6 +122,11 @@
$sort_order = isset($_SESSION['sort_order']) ? $_SESSION['sort_order'] : $CONFIG['message_sort_order'];
$a_headers = $IMAP->list_headers($mbox, NULL, $sort_col, $sort_order, $count);
+ if ($_SESSION['threads'])
+ // TODO: count number of roots deleted and slice that many roots from the end of $a_headers
+ $OUTPUT->command('message_list.clear');
+ else
+ $a_headers = array_slice($a_headers, -$count, $count);
rcmail_js_message_list($a_headers, false, false);
}
Index: program/steps/mail/func.inc
===================================================================
--- program/steps/mail/func.inc (revision 2976)
+++ program/steps/mail/func.inc (working copy)
@@ -42,6 +42,8 @@
$IMAP->set_mailbox(($_SESSION['mbox'] = $mbox));
else
$_SESSION['mbox'] = $IMAP->get_mailbox_name();
+$a_message_threading = $RCMAIL->config->get('message_threading', array());
+rcmail::get_instance()->imap->threading = $a_message_threading[$_SESSION['mbox']];
if (!empty($_GET['_page']))
$IMAP->set_page(($_SESSION['page'] = intval($_GET['_page'])));
@@ -79,13 +81,14 @@
$OUTPUT->set_env('search_mods', $_SESSION['search_mods'] ? $_SESSION['search_mods'] : array('subject'=>'subject'));
// make sure the message count is refreshed (for default view)
- $IMAP->messagecount($mbox_name, 'ALL', true);
+ $IMAP->messagecount($mbox_name, rcmail::get_instance()->imap->threading?'THREADS':'ALL', true);
}
// set current mailbox in client environment
$OUTPUT->set_env('mailbox', $mbox_name);
$OUTPUT->set_env('quota', $IMAP->get_capability('quota'));
$OUTPUT->set_env('delimiter', $IMAP->get_hierarchy_delimiter());
+ $OUTPUT->set_env('threaded_display', rcmail::get_instance()->imap->threading);
if ($CONFIG['flag_for_deletion'])
$OUTPUT->set_env('flag_for_deletion', true);
@@ -119,7 +122,6 @@
$skin_path = $CONFIG['skin_path'];
$image_tag = ' ';
-
// check to see if we have some settings for sorting
$sort_col = $_SESSION['sort_col'];
$sort_order = $_SESSION['sort_order'];
@@ -161,7 +163,6 @@
// add col definition
$out .= '';
- $out .= '';
foreach ($a_show_cols as $col)
$out .= ($col!='attachment') ? sprintf('', $col) : '';
@@ -169,7 +170,7 @@
$out .= "\n";
// add table title
- $out .= "\n| | \n";
+ $out .= "\n";
$javascript = '';
foreach ($a_show_cols as $col)
@@ -265,9 +266,16 @@
$js_row_arr['forwarded'] = true;
if ($header->flagged)
$js_row_arr['flagged'] = true;
+ if ($header->has_children)
+ $js_row_arr['has_children'] = true;
+ $js_row_arr['depth'] = $header->depth;
+ $js_row_arr['parent_uid'] = $header->parent_uid;
+ $js_row_arr['unread_children'] = $header->unread_children;
- // set message icon
- if ($attrib['deletedicon'] && $header->deleted)
+ // set message icon
+ if ($header->seen && $attrib['unreadchildrenicon'] && $header->unread_children > 0)
+ $message_icon = $attrib['unreadchildrenicon'];
+ else if ($attrib['deletedicon'] && $header->deleted)
$message_icon = $attrib['deletedicon'];
else if ($attrib['repliedicon'] && $header->answered)
{
@@ -292,18 +300,34 @@
if ($attrib['attachmenticon'] && preg_match("/multipart\/m/i", $header->ctype))
$attach_icon = $attrib['attachmenticon'];
- $out .= sprintf(' '."\n",
+ $out .= sprintf(' '."\n",
$header->uid,
$header->seen ? '' : ' unread',
$header->deleted ? ' deleted' : '',
$header->flagged ? ' flagged' : '',
- $zebra_class);
+ $zebra_class,
+ ($header->depth > 1) ? ' style="display: none"' : '');
- $out .= sprintf("| %s | \n", $message_icon ? sprintf($image_tag, $skin_path, $message_icon, '') : '');
+ $tree = '';
+ $depth = $header->depth;
+ if ($depth > 0)
+ {
+ // XXX: This assumes that div width is hardcoded to 15px,
+ // Chris did it a bit differently in an original patch, he was adding so much divs as depth is
+ // I replaced logic in list.js:drag_mouse_move() so subject text is picked defferently, so
+ // either method of could be used (that was only the one problem I noted with these added divs).
+ // The same is true for an online list (program/js/app.js:add_message_row()).
+ // Bubble
+ $width = ($depth - 1) * 15;
+ $tree .= '';
+ $tree .= $header->has_children?' ':' ';
+ }
+ $tree .= $message_icon ? sprintf($image_tag, $skin_path, $message_icon, '') : '';
$IMAP->set_charset(!empty($header->charset) ? $header->charset : $CONFIG['default_charset']);
// format each col
+ $first = true;
foreach ($a_show_cols as $col)
{
if ($col=='from' || $col=='to')
@@ -325,6 +349,10 @@
else
$cont = Q($header->$col);
+ if ($first) {
+ $first = false;
+ $cont = $tree . $cont;
+ }
if ($col!='attachment')
$out .= '' . $cont . " | \n";
else
@@ -340,11 +368,12 @@
// complete message table
$out .= "\n";
- $message_count = $IMAP->messagecount();
+ $message_count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
// set client env
$OUTPUT->add_gui_object('mailcontframe', 'mailcontframe');
$OUTPUT->add_gui_object('messagelist', $attrib['id']);
+ $OUTPUT->set_env('autoexpand_threads', $CONFIG['autoexpand_threads']);
$OUTPUT->set_env('messagecount', $message_count);
$OUTPUT->set_env('current_page', $IMAP->list_page);
$OUTPUT->set_env('pagecount', ceil($message_count/$IMAP->page_size));
@@ -369,6 +398,8 @@
$OUTPUT->set_env('flaggedicon', $skin_path . $attrib['flaggedicon']);
if ($attrib['unflaggedicon'])
$OUTPUT->set_env('unflaggedicon', $skin_path . $attrib['unflaggedicon']);
+ if ($attrib['unreadchildrenicon'])
+ $OUTPUT->set_env('unreadchildrenicon', $skin_path . $attrib['unreadchildrenicon']);
$OUTPUT->set_env('messages', $a_js_message_arr);
$OUTPUT->set_env('coltypes', $a_show_cols);
@@ -406,6 +437,12 @@
if ($browser->ie && $replace)
$OUTPUT->command('offline_message_list', true);
+ // remove 'attachment' and 'flag' columns, we don't need them here
+ if(($key = array_search('attachment', $a_show_cols)) !== FALSE)
+ unset($a_show_cols[$key]);
+ if(($key = array_search('flag', $a_show_cols)) !== FALSE)
+ unset($a_show_cols[$key]);
+
// loop through message headers
foreach ($a_headers as $n => $header)
{
@@ -417,12 +454,6 @@
$IMAP->set_charset(!empty($header->charset) ? $header->charset : $CONFIG['default_charset']);
- // remove 'attachment' and 'flag' columns, we don't need them here
- if(($key = array_search('attachment', $a_show_cols)) !== FALSE)
- unset($a_show_cols[$key]);
- if(($key = array_search('flag', $a_show_cols)) !== FALSE)
- unset($a_show_cols[$key]);
-
// format each col; similar as in rcmail_message_list()
foreach ($a_show_cols as $col)
{
@@ -446,6 +477,13 @@
$a_msg_cols[$col] = $cont;
}
+ if ($header->depth)
+ $a_msg_cols["depth"] = $header->depth;
+ if ($header->parent_uid)
+ $a_msg_cols["parent_uid"] = $header->parent_uid;
+ if ($header->has_children)
+ $a_msg_flags["has_children"] = $header->has_children;
+ $a_msg_cols["unread_children"] = $header->unread_children;
if ($header->deleted)
$a_msg_flags['deleted'] = 1;
if (!$header->seen)
@@ -467,6 +505,8 @@
if ($browser->ie && $replace)
$OUTPUT->command('offline_message_list', false);
+
+ $OUTPUT->command('expand_threads');
}
@@ -578,19 +618,19 @@
{
return rcube_label(array('name' => 'messagenrof',
'vars' => array('nr' => $MESSAGE->index+1,
- 'count' => $count!==NULL ? $count : $IMAP->messagecount())));
+ 'count' => $count!==NULL ? $count : $IMAP->messagecount(NULL, 'ALL')))); // Only messages, no threads here
}
if ($page===NULL)
$page = $IMAP->list_page;
$start_msg = ($page-1) * $IMAP->page_size + 1;
- $max = $count!==NULL ? $count : $IMAP->messagecount();
+ $max = $count!==NULL ? $count : $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
if ($max==0)
$out = rcube_label('mailboxempty');
else
- $out = rcube_label(array('name' => 'messagesfromto',
+ $out = rcube_label(array('name' => rcmail::get_instance()->imap->threading?'threadsfromto':'messagesfromto',
'vars' => array('from' => $start_msg,
'to' => min($max, $start_msg + $IMAP->page_size - 1),
'count' => $max)));
Index: program/steps/mail/search.inc
===================================================================
--- program/steps/mail/search.inc (revision 2976)
+++ program/steps/mail/search.inc (working copy)
@@ -99,7 +99,7 @@
// Get the headers
$result_h = $IMAP->list_headers($mbox, 1, $_SESSION['sort_col'], $_SESSION['sort_order']);
-$count = $IMAP->messagecount();
+$count = $IMAP->messagecount(NULL, rcmail::get_instance()->imap->threading?'THREADS':'ALL');
// save search results in session
if (!is_array($_SESSION['search']))
|