1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a new, empty conversation list controller.
 26  * @constructor
 27  * @class
 28  * This class manages the conversations mail view. Conversations are listed, and any
 29  * conversation with more than one message is expandable. Expanding a conversation
 30  * shows its messages in the list just below it.
 31  *
 32  * @author Conrad Damon
 33  *
 34  * @param {DwtControl}					container					the containing shell
 35  * @param {ZmApp}						mailApp						the containing application
 36  * @param {constant}					type						type of controller
 37  * @param {string}						sessionId					the session id
 38  * @param {ZmSearchResultsController}	searchResultsController		containing controller
 39  * 
 40  * @extends		ZmDoublePaneController
 41  */
 42 ZmConvListController = function(container, mailApp, type, sessionId, searchResultsController) {
 43 	ZmDoublePaneController.apply(this, arguments);
 44 };
 45 
 46 ZmConvListController.prototype = new ZmDoublePaneController;
 47 ZmConvListController.prototype.constructor = ZmConvListController;
 48 
 49 ZmConvListController.prototype.isZmConvListController = true;
 50 ZmConvListController.prototype.toString = function() { return "ZmConvListController"; };
 51 
 52 ZmMailListController.ACTION_CODE_WHICH[ZmKeyMap.FIRST_UNREAD_MSG]	= DwtKeyMap.SELECT_FIRST;
 53 ZmMailListController.ACTION_CODE_WHICH[ZmKeyMap.LAST_UNREAD_MSG]	= DwtKeyMap.SELECT_LAST;
 54 ZmMailListController.ACTION_CODE_WHICH[ZmKeyMap.NEXT_UNREAD_MSG]	= DwtKeyMap.SELECT_NEXT;
 55 ZmMailListController.ACTION_CODE_WHICH[ZmKeyMap.PREV_UNREAD_MSG]	= DwtKeyMap.SELECT_PREV;
 56 
 57 ZmMailListController.GROUP_BY_SETTING[ZmId.VIEW_CONVLIST]	= ZmSetting.GROUP_BY_CONV;
 58 
 59 // view menu
 60 ZmMailListController.GROUP_BY_ICON[ZmId.VIEW_CONVLIST]		= "ConversationView";
 61 ZmMailListController.GROUP_BY_MSG_KEY[ZmId.VIEW_CONVLIST]	= "byConversation";
 62 ZmMailListController.GROUP_BY_SHORTCUT[ZmId.VIEW_CONVLIST]	= ZmKeyMap.VIEW_BY_CONV;
 63 ZmMailListController.GROUP_BY_VIEWS.push(ZmId.VIEW_CONVLIST);
 64 
 65 // Public methods
 66 
 67 ZmConvListController.getDefaultViewType =
 68 function() {
 69 	return ZmId.VIEW_CONVLIST;
 70 };
 71 ZmConvListController.prototype.getDefaultViewType = ZmConvListController.getDefaultViewType;
 72 
 73 /**
 74  * Displays the given conversation list in a two-pane view.
 75  *
 76  * @param {ZmSearchResult}	searchResults		the current search results
 77  */
 78 ZmConvListController.prototype.show =
 79 function(searchResults, force) {
 80 	
 81 	if (!force && !this.popShield(null, this.show.bind(this, searchResults, true))) {
 82 		return;
 83 	}
 84 	
 85 	ZmDoublePaneController.prototype.show.call(this, searchResults, searchResults.getResults(ZmItem.CONV));
 86 	if (!appCtxt.isExternalAccount() && !this.isSearchResults && !(searchResults && searchResults.search && searchResults.search.isDefaultToMessageView)) {
 87 		appCtxt.set(ZmSetting.GROUP_MAIL_BY, ZmSetting.GROUP_BY_CONV);
 88 	}
 89 };
 90 
 91 /**
 92  * Handles switching the order of messages within expanded convs.
 93  *
 94  * @param view		[constant]*		the id of the new order
 95  * @param force		[boolean]		if true, always redraw view
 96  */
 97 ZmConvListController.prototype.switchView =
 98 function(view, force) {
 99 
100 	if (view == ZmSearch.DATE_DESC || view == ZmSearch.DATE_ASC) {
101 		if (!force && !this.popShield(null, this.switchView.bind(this, view, true))) {
102 			return;
103 		}
104 		if ((appCtxt.get(ZmSetting.CONVERSATION_ORDER) != view) || force) {
105 			appCtxt.set(ZmSetting.CONVERSATION_ORDER, view);
106 			if (this._currentViewType == ZmId.VIEW_CONVLIST) {
107 				this._mailListView.redoExpansion();
108 			}
109 			var itemView = this.getItemView();
110 			var conv = itemView && itemView.getItem();
111 			if (conv) {
112 				itemView.set(conv);
113 			}
114 		}
115 	} else {
116 		ZmDoublePaneController.prototype.switchView.apply(this, arguments);
117 	}
118 };
119 
120 // Internally we manage two maps, one for CLV and one for CV2 (if applicable)
121 ZmConvListController.prototype.getKeyMapName = function() {
122 	// if user is quick replying, don't use the mapping of conv/mail list - so Ctrl+Z works
123 	return this._convView && this._convView.isActiveQuickReply() ? ZmKeyMap.MAP_QUICK_REPLY : ZmKeyMap.MAP_CONVERSATION_LIST;
124 };
125 
126 ZmConvListController.prototype.handleKeyAction =
127 function(actionCode, ev) {
128 
129 	DBG.println(AjxDebug.DBG3, "ZmConvListController.handleKeyAction");
130 	
131 	var mlv = this._mailListView,
132 	    capsuleEl = DwtUiEvent.getTargetWithClass(ev, 'ZmMailMsgCapsuleView'),
133         activeEl = document.activeElement,
134         isFooterActionLink = activeEl && activeEl.id.indexOf(ZmId.MV_MSG_FOOTER) !== -1;
135 	
136 	switch (actionCode) {
137 
138         case DwtKeyMap.DBLCLICK:
139             // if link has focus, Enter should be same as click
140             if (isFooterActionLink) {
141                 activeEl.click();
142             }
143             else {
144                 return ZmDoublePaneController.prototype.handleKeyAction.apply(this, arguments);
145             }
146             break;
147 
148 		case ZmKeyMap.EXPAND:
149 		case ZmKeyMap.COLLAPSE:
150 			if (capsuleEl) {
151                 // if a footer link has focus, move among those links
152                 if (isFooterActionLink) {
153                     var msgView = DwtControl.findControl(activeEl);
154                     if (msgView && msgView.isZmMailMsgCapsuleView) {
155                         msgView._focusLink(actionCode === ZmKeyMap.COLLAPSE, activeEl);
156                     }
157                 }
158                 // otherwise expand or collapse the msg view
159                 else {
160                     var capsule = DwtControl.fromElement(capsuleEl);
161                     if ((actionCode === ZmKeyMap.EXPAND) !== capsule.isExpanded()) {
162                         capsule._toggleExpansion();
163                     }
164                 }
165 
166 				break;
167 			}
168 //			if (mlv.getSelectionCount() != 1) { return false; }
169 			var item = mlv.getItemFromElement(mlv._kbAnchor);
170 			if (!item) {
171                 return false;
172             }
173 			if ((actionCode == ZmKeyMap.EXPAND) != mlv.isExpanded(item)) {
174 				mlv._expandItem(item);
175 			}
176 			break;
177 
178 		case ZmKeyMap.TOGGLE:
179 			if (capsuleEl) {
180 				DwtControl.fromElement(capsuleEl)._toggleExpansion();
181 				break;
182 			}
183 //			if (mlv.getSelectionCount() != 1) { return false; }
184 			var item = mlv.getItemFromElement(mlv._kbAnchor);
185 			if (!item) { return false; }
186 			if (mlv._isExpandable(item)) {
187 				mlv._expandItem(item);
188 			}
189 			break;
190 
191 		case ZmKeyMap.EXPAND_ALL:
192 		case ZmKeyMap.COLLAPSE_ALL:
193 			var expand = (actionCode == ZmKeyMap.EXPAND_ALL);
194 			if (capsuleEl) {
195 				DwtControl.fromElement(capsuleEl).parent.setExpanded(expand);
196 			}
197             else {
198 				mlv._expandAll(expand);
199 			}
200 			break;
201 
202 		case ZmKeyMap.NEXT_UNREAD_MSG:
203 		case ZmKeyMap.PREV_UNREAD_MSG:
204 			this.lastListAction = actionCode;
205 			var selItem, noBump = false;
206 			if (mlv.getSelectionCount() == 1) {
207 				var sel = mlv.getSelection();
208 				selItem = sel[0];
209 				if (selItem && mlv._isExpandable(selItem)) {
210 					noBump = true;
211 				}
212 			}
213 
214 		case ZmKeyMap.FIRST_UNREAD_MSG:
215 		case ZmKeyMap.LAST_UNREAD_MSG:
216 			var item = (selItem && selItem.type == ZmItem.MSG && noBump) ? selItem :
217 					   this._getUnreadItem(ZmMailListController.ACTION_CODE_WHICH[actionCode], null, noBump);
218 			if (!item) { return; }
219 			if (!mlv.isExpanded(item) && mlv._isExpandable(item)) {
220 				var callback = new AjxCallback(this, this._handleResponseExpand, [actionCode]);
221 				if (item.type == ZmItem.MSG) {
222 					this._expand({conv:appCtxt.getById(item.cid), msg:item, offset:mlv._msgOffset[item.id], callback:callback});
223 				} else {
224 					this._expand({conv:item, callback:callback});
225 				}
226 			} else if (item) {
227 				this._selectItem(mlv, item);
228 			}
229 			break;
230 		
231 		case ZmKeyMap.KEEP_READING:
232 			return this._keepReading(false, ev);
233 			break;
234 
235 		// these are for quick reply
236 		case ZmKeyMap.SEND:
237 			if (!appCtxt.get(ZmSetting.USE_SEND_MSG_SHORTCUT)) {
238 				break;
239 			}
240 			var itemView = this.getItemView();
241 			if (itemView && itemView._sendListener) {
242 				itemView._sendListener();
243 			}
244 			break;
245 
246 		// do this last since we want CANCEL to bubble up if not handled
247 		case ZmKeyMap.CANCEL:
248 			var itemView = this.getItemView();
249 			if (itemView && itemView._cancelListener && itemView._replyView && itemView._replyView.getVisible()) {
250 				itemView._cancelListener();
251 				break;
252 			}
253 
254 		default:
255 			return ZmDoublePaneController.prototype.handleKeyAction.apply(this, arguments);
256 	}
257 	return true;
258 };
259 
260 ZmConvListController.prototype._handleResponseExpand =
261 function(actionCode) {
262 	var unreadItem = this._getUnreadItem(ZmMailListController.ACTION_CODE_WHICH[actionCode], ZmItem.MSG);
263 	if (unreadItem) {
264 		this._selectItem(this._mailListView, unreadItem);
265 	}
266 };
267 
268 ZmConvListController.prototype._keepReading =
269 function(check, ev) {
270 
271 	if (!this.isReadingPaneOn() || !this._itemViewCurrent()) { return false; }
272 	var mlv = this._mailListView;
273 	if (!mlv || mlv.getSelectionCount() != 1) { return false; }
274 	
275 	var result = false;
276 	var itemView = this.getItemView();
277 	// conv view
278 	if (itemView && itemView.isZmConvView2) {
279 		result = itemView._keepReading(check);
280 		result = result || (check ? !!(this._getUnreadItem(DwtKeyMap.SELECT_NEXT)) :
281 									   this.handleKeyAction(ZmKeyMap.NEXT_UNREAD, ev));
282 	}
283 	// msg view (within an expanded conv)
284 	else if (itemView && itemView.isZmMailMsgView) {
285 		var result = itemView._keepReading(check);
286 		if (!check || !result) {
287 			// go to next unread msg in this expanded conv, otherwise next unread conv
288 			var msg = mlv.getSelection()[0];
289 			var conv = msg && appCtxt.getById(msg.cid);
290 			var msgList = conv && conv.msgs && conv.msgs.getArray();
291 			var msgFound, item;
292 			if (msgList && msgList.length) {
293 				for (var i = 0; i < msgList.length; i++) {
294 					var m = msgList[i];
295 					msgFound = msgFound || (m.id == msg.id);
296 					if (msgFound && m.isUnread) {
297 						item = m;
298 						break;
299 					}
300 				}
301 			}
302 			if (item) {
303 				result = true;
304 				if (!check) {
305 					this._selectItem(mlv, item);
306 				}
307 			}
308 			else {
309 				result = check ? !!(this._getUnreadItem(DwtKeyMap.SELECT_NEXT)) :
310 									this.handleKeyAction(ZmKeyMap.NEXT_UNREAD, ev);
311 			}
312 		}
313 	}
314 	if (!check && result) {
315 		this._checkKeepReading();
316 	}
317 	return result;
318 };
319 
320 /**
321  * Override to handle paging among msgs within an expanded conv.
322  * 
323  * TODO: handle msg paging (current item is expandable msg)
324  * 
325  * @private
326  */
327 ZmConvListController.prototype.pageItemSilently =
328 function(currentItem, forward) {
329 	if (!currentItem) { return; }
330 	if (currentItem.type == ZmItem.CONV) {
331 		ZmMailListController.prototype.pageItemSilently.apply(this, arguments);
332 		return;
333 	}
334 	
335 	var conv = appCtxt.getById(currentItem.cid);
336 	if (!(conv && conv.msgs)) { return; }
337 	var found = false;
338 	var list = conv.msgs.getArray();
339 	for (var i = 0, count = list.length; i < count; i++) {
340 		if (list[i] == currentItem) {
341 			found = true;
342 			break;
343 		}
344 	}
345 	if (!found) { return; }
346 	
347 	var msgIdx = forward ? i + 1 : i - 1;
348 	if (msgIdx >= 0 && msgIdx < list.length) {
349 		var msg = list[msgIdx];
350 		var clv = this._listView[this._currentViewId];
351 		clv.emulateDblClick(msg);
352 	}
353 };
354 
355 // Private methods
356 
357 ZmConvListController.prototype._createDoublePaneView = 
358 function() {
359 	var dpv = new ZmConvDoublePaneView({
360 		parent:		this._container,
361 		posStyle:	Dwt.ABSOLUTE_STYLE,
362 		controller:	this,
363 		dropTgt:	this._dropTgt
364 	});
365 	this._convView = dpv._itemView;
366 	return dpv;
367 };
368 
369 ZmConvListController.prototype._paginate = 
370 function(view, bPageForward, convIdx, limit) {
371 	view = view || this._currentViewId;
372 	return ZmDoublePaneController.prototype._paginate.call(this, view, bPageForward, convIdx, limit);
373 };
374 
375 ZmConvListController.prototype._resetNavToolBarButtons =
376 function(view) {
377 	view = view || this.getCurrentViewId();
378 	ZmDoublePaneController.prototype._resetNavToolBarButtons.call(this, view);
379 	if (!this._navToolBar[view]) { return; }
380 	this._navToolBar[view].setToolTip(ZmOperation.PAGE_BACK, ZmMsg.previousPage);
381 	this._navToolBar[view].setToolTip(ZmOperation.PAGE_FORWARD, ZmMsg.nextPage);
382 };
383 
384 ZmConvListController.prototype._setupConvOrderMenu =
385 function(view, menu) {
386 
387 	var convOrderMenuItem = menu.createMenuItem(Dwt.getNextId("CONV_ORDER_"), {
388 			text:   ZmMsg.expandConversations,
389 			style:  DwtMenuItem.NO_STYLE
390 		}),
391 		convOrderMenu = new ZmPopupMenu(convOrderMenuItem);
392 
393 	var ids = [ ZmMailListController.CONV_ORDER_DESC, ZmMailListController.CONV_ORDER_ASC ];
394 	var setting = appCtxt.get(ZmSetting.CONVERSATION_ORDER);
395 	var miParams = {
396 		style:          DwtMenuItem.RADIO_STYLE,
397 		radioGroupId:   "CO"
398 	};
399 	for (var i = 0; i < ids.length; i++) {
400 		var id = ids[i];
401 		if (!convOrderMenu._menuItems[id]) {
402 			miParams.text = ZmMailListController.CONV_ORDER_TEXT[id];
403 			var mi = convOrderMenu.createMenuItem(id, miParams);
404 			mi.setData(ZmOperation.MENUITEM_ID, id);
405 			mi.addSelectionListener(this._listeners[ZmOperation.VIEW]);
406 			mi.setChecked((setting == id), true);
407 		}
408 	}
409 
410 	convOrderMenuItem.setMenu(convOrderMenu);
411 
412 	return convOrderMenu;
413 };
414 
415 // no support for showing total items, which are msgs
416 ZmConvListController.prototype._getNumTotal = function() { return null; }
417 
418 ZmConvListController.prototype._preUnloadCallback =
419 function(view) {
420 	return !(this._convView && this._convView.isDirty());
421 };
422 
423 ZmConvListController.prototype._preHideCallback =
424 function(viewId, force, newViewId) {
425 	return force ? true : this.popShield(viewId, null, newViewId);
426 };
427 
428 ZmConvListController.prototype._getActionMenuOps = function() {
429 
430 	var list = ZmDoublePaneController.prototype._getActionMenuOps.apply(this, arguments),
431 		index = AjxUtil.indexOf(list, ZmOperation.FORWARD);
432 
433 	if (index !== -1) {
434 		list.splice(index + 1, 0, ZmOperation.FORWARD_CONV);
435 	}
436 	return list;
437 };
438 
439 ZmConvListController.prototype._getSecondaryToolBarOps = function() {
440 
441 	var list = ZmDoublePaneController.prototype._getSecondaryToolBarOps.apply(this, arguments),
442 		index = AjxUtil.indexOf(list, ZmOperation.EDIT_AS_NEW);
443 
444 	if (index !== -1 && appCtxt.get(ZmSetting.FORWARD_MENU_ENABLED)) {
445 		list.splice(index + 1, 0, ZmOperation.FORWARD_CONV);
446 	}
447 	return list;
448 };
449 
450 ZmConvListController.prototype._resetOperations = function(parent, num) {
451 	ZmDoublePaneController.prototype._resetOperations.apply(this, arguments);
452 	this._resetForwardConv(parent, num);
453 };
454 
455 ZmConvListController.prototype._resetForwardConv = function(parent, num) {
456 
457 	var doShow = true,      // show if 'forward conv' applies at all
458 		doEnable = false;   // enable if conv has multiple msgs
459 
460 	if (num == null || num === 1) {
461 
462 		var mlv = this._mailListView,
463 			item = this._conv || mlv.getSelection()[0];
464 
465 		if (item && item.type === ZmItem.CONV) {
466 			if (mlv && mlv._getDisplayedMsgCount(item) > 1) {
467 				doEnable = true;
468 			}
469 		}
470 		else {
471 			doShow = false;
472 		}
473 	}
474 	var op = parent.getOp(ZmOperation.FORWARD_CONV);
475 	if (op) {
476 		op.setVisible(doShow);
477 		parent.enable(ZmOperation.FORWARD_CONV, doEnable);
478 	}
479 };
480 
481 
482 /**
483  * Figure out if the given view change is destructive. If so, put up pop shield.
484  * 
485  * @param {string}		viewId		ID of view being hidden
486  * @param {function}	callback	function to call if user agrees to leave
487  * @param {string}		newViewId	ID of view that will be shown
488  */
489 ZmConvListController.prototype.popShield =
490 function(viewId, callback, newViewId) {
491 
492 	var newViewType = newViewId && appCtxt.getViewTypeFromId(newViewId);
493 	var switchingView = (newViewType == ZmId.VIEW_TRAD);
494 	if (this._convView && this._convView.isDirty() && (!newViewType || switchingView)) {
495 		var ps = this._popShield = this._popShield || appCtxt.getYesNoMsgDialog();
496 		ps.reset();
497 		ps.setMessage(ZmMsg.convViewCancel, DwtMessageDialog.WARNING_STYLE);
498 		ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldYesCallback, this, [switchingView, callback]);
499 		ps.registerCallback(DwtDialog.NO_BUTTON, this._popShieldNoCallback, this, [switchingView, callback]);
500 		ps.popup();
501 		return false;
502 	}
503 	else {
504 		return true;
505 	}
506 };
507 
508 // yes, I want to leave even though I've typed some text
509 ZmConvListController.prototype._popShieldYesCallback =
510 function(switchingView, callback) {
511 	this._convView._replyView.reset();
512 	this._popShield.popdown();
513 	if (switchingView) {
514 		// tell app view mgr it's okay to show TV
515 		appCtxt.getAppViewMgr().showPendingView(true);
516 	}
517 	else if (callback) {
518 		callback();
519 	}
520 };
521 
522 // no, I don't want to leave
523 ZmConvListController.prototype._popShieldNoCallback =
524 function(switchingView, callback) {
525 	this._popShield.popdown();
526 	if (switchingView) {
527 		// attempt to switch to TV was canceled - need to undo changes
528 		this._updateViewMenu(ZmId.VIEW_CONVLIST);
529 		if (!appCtxt.isExternalAccount() && !this.isSearchResults && !this._currentSearch.isDefaultToMessageView) {
530 			this._app.setGroupMailBy(ZmMailListController.GROUP_BY_SETTING[ZmId.VIEW_CONVLIST], true);
531 		}
532 	}
533 	//check if this is due to new selected item and it's different than current - if so we need to revert in the list.
534 	var selection = this.getSelection();
535 	var listSelectedItem = selection && selection.length && selection[0];
536 	var conv = this._convView._item;
537 	if (conv.id !== listSelectedItem.id) {
538 		this.getListView().setSelection(conv, true); //skip notification so item is not re-set in the reading pane (or infinite pop shield loop :) )
539 	}
540 	appCtxt.getKeyboardMgr().grabFocus(this._convView._replyView._input);
541 };
542 
543 ZmConvListController.prototype._listSelectionListener =
544 function(ev) {
545 
546 	var item = ev.item;
547 	if (!item) { return; }
548 	
549 	this._mailListView._selectedMsg = null;
550 	if (ev.field == ZmItem.F_EXPAND && this._mailListView._isExpandable(item)) {
551 		this._toggle(item);
552 		return true;
553 	}
554 
555 	return ZmDoublePaneController.prototype._listSelectionListener.apply(this, arguments);
556 };
557 
558 ZmConvListController.prototype._handleConvLoaded =
559 function(conv) {
560 	var msg = conv.getFirstHotMsg();
561 	var item = msg || conv;
562 	this._showItem(item);
563 };
564 
565 ZmConvListController.prototype._showItem =
566 function(item) {
567 	if (item.type == ZmItem.MSG) {
568 		AjxDispatcher.run("GetMsgController", item && item.nId).show(item, this, null, true);
569 	}
570 	else {
571 		AjxDispatcher.run("GetConvController").show(item, this, null, true);
572 	}
573 
574 };
575 
576 
577 ZmConvListController.prototype._menuPopdownActionListener =
578 function(ev) {
579 	ZmDoublePaneController.prototype._menuPopdownActionListener.apply(this, arguments);
580 	this._mailListView._selectedMsg = null;
581 };
582 
583 ZmConvListController.prototype._setSelectedItem =
584 function() {
585 	
586 	var selCnt = this._listView[this._currentViewId].getSelectionCount();
587 	if (selCnt == 1) {
588 		var sel = this._listView[this._currentViewId].getSelection();
589 		var item = (sel && sel.length) ? sel[0] : null;
590 		if (item.type == ZmItem.CONV) {
591 			Dwt.setLoadingTime("ZmConv", new Date());
592 			var convParams = {};
593 			convParams.markRead = this._handleMarkRead(item, true);
594 			if (this.isSearchResults) {
595 				convParams.fetch = ZmSetting.CONV_FETCH_MATCHES;
596 			}
597 			else {
598 				convParams.fetch = ZmSetting.CONV_FETCH_UNREAD_OR_FIRST;
599 				convParams.query = this._currentSearch.query;
600 			}
601 			// if the conv's unread state changed, load it again so we get the correct expanded msg bodies
602 			convParams.forceLoad = item.unreadHasChanged;
603 			item.load(convParams, this._handleResponseSetSelectedItem.bind(this, item));
604 		} else {
605 			ZmDoublePaneController.prototype._setSelectedItem.apply(this, arguments);
606 		}
607 	}
608 };
609 
610 ZmConvListController.prototype._handleResponseSetSelectedItem =
611 function(item) {
612 
613 	if (item.type === ZmItem.CONV && this.isReadingPaneOn()) {
614 		// make sure list view has this item
615 		var lv = this._listView[this._currentViewId];
616 		if (lv.hasItem(item.id)) {
617 			this._displayItem(item);
618 		}
619 		item.unreadHasChanged = false;
620 	}
621 	else {
622 		ZmDoublePaneController.prototype._handleResponseSetSelectedItem.call(this, item);
623 	}
624 };
625 
626 ZmConvListController.prototype._getTagMenuMsg = 
627 function(num, items) {
628 	var type = this._getLabelType(items);
629 	return AjxMessageFormat.format((type == ZmItem.MSG) ? ZmMsg.tagMessages : ZmMsg.tagConversations, num);
630 };
631 
632 ZmConvListController.prototype._getMoveDialogTitle = 
633 function(num, items) {
634 	var type = this._getLabelType(items);
635 	return AjxMessageFormat.format((type == ZmItem.MSG) ? ZmMsg.moveMessages : ZmMsg.moveConversations, num);
636 };
637 
638 ZmConvListController.prototype._getLabelType = 
639 function(items) {
640 	if (!(items && items.length)) { return ZmItem.MSG; }
641 	for (var i = 0; i < items.length; i++) {
642 		if (items[i].type == ZmItem.MSG) {
643 			return ZmItem.MSG;
644 		}
645 	}
646 	return ZmItem.CONV;
647 };
648 
649 /**
650  * Returns the first matching msg in the conv, if available. No request will
651  * be made to the server if the conv has not been loaded.
652  */
653 ZmConvListController.prototype.getMsg =
654 function(params) {
655 	
656 	// First see if action is being performed on a msg in the conv view in the reading pane
657 	var lv = this._listView[this._currentViewId];
658 	var msg = lv && lv._selectedMsg;
659 	if (msg && DwtMenu.menuShowing()) {
660 		return msg;
661 	}
662 	
663 	var sel = lv.getSelection();
664 	var item = (sel && sel.length) ? sel[0] : null;
665 	if (item) {
666 		if (item.type == ZmItem.CONV) {
667 			return item.getFirstHotMsg(params);
668 		} else if (item.type == ZmItem.MSG) {
669 			return ZmDoublePaneController.prototype.getMsg.apply(this, arguments);
670 		}
671 	}
672 	return null;
673 };
674 
675 /**
676  * Returns the first matching msg in the conv. The conv will be loaded if necessary.
677  */
678 ZmConvListController.prototype._getLoadedMsg =
679 function(params, callback) {
680 	params = params || {};
681 	var sel = this._listView[this._currentViewId].getSelection();
682 	var item = (sel && sel.length) ? sel[0] : null;
683 	if (item) {
684 		if (item.type == ZmItem.CONV) {
685 			params.markRead = (params.markRead != null) ? params.markRead : this._handleMarkRead(item, true);
686 			var respCallback = new AjxCallback(this, this._handleResponseGetLoadedMsg, callback);
687 			item.getFirstHotMsg(params, respCallback);
688 		} else if (item.type == ZmItem.MSG) {
689 			ZmDoublePaneController.prototype._getLoadedMsg.apply(this, arguments);
690 		}
691 	} else {
692 		callback.run();
693 	}
694 };
695 
696 ZmConvListController.prototype._handleResponseGetLoadedMsg =
697 function(callback, msg) {
698 	callback.run(msg);
699 };
700 
701 ZmConvListController.prototype._getSelectedMsg =
702 function(callback) {
703 	var item = this._listView[this._currentViewId].getSelection()[0];
704 	if (!item) { return null; }
705 	
706 	return (item.type == ZmItem.CONV) ? item.getFirstHotMsg(null, callback) : item;
707 };
708 
709 ZmConvListController.prototype._displayItem =
710 function(item) {
711 
712 	// cancel timed mark read action on previous conv
713 	appCtxt.killMarkReadTimer();
714 
715 	var curItem = this._doublePaneView.getItem();
716 	item.waitOnMarkRead = true;
717 	this._doublePaneView.setItem(item);
718 	item.waitOnMarkRead = false;
719 	if (!(curItem && item.id == curItem.id)) {
720 		this._handleMarkRead(item);
721 	}
722 };
723 
724 ZmConvListController.prototype._toggle =
725 function(item) {
726 	if (this._mailListView.isExpanded(item)) {
727 		this._collapse(item);
728 	} else {
729 		var conv = item, msg = null, offset = 0;
730 		if (item.type == ZmItem.MSG) {
731 			conv = appCtxt.getById(item.cid);
732 			msg = item;
733 			offset = this._mailListView._msgOffset[item.id];
734 		}
735 		this._expand({
736 			conv:   conv,
737 			msg:    msg,
738 			offset: offset
739 		});
740 	}
741 };
742 
743 /**
744  * Expands the given conv or msg, performing a search to get items if necessary.
745  *
746  * @param params		[hash]			hash of params:
747  *        conv			[ZmConv]		conv to expand
748  *        msg			[ZmMailMsg]		msg to expand (get next page of msgs for conv)
749  *        offset		[int]			index of msg in conv
750  *        callback		[AjxCallback]	callback to run when done
751  */
752 ZmConvListController.prototype._expand =
753 function(params) {
754 
755 	var conv = params.conv;
756 	var offset = params.offset || 0;
757 	var respCallback = new AjxCallback(this, this._handleResponseLoadItem, [params]);
758 	var pageWasCached = false;
759 	if (offset) {
760 		if (this._paginateConv(conv, offset, respCallback)) {
761 			// page was cached, callback won't be run
762 			this._handleResponseLoadItem(params, new ZmCsfeResult(conv.msgs));
763 		}
764 	} else if (!conv._loaded) {
765 		conv.load(null, respCallback);
766 	} else {
767 		// re-expanding first page of msgs
768 		this._handleResponseLoadItem(params, new ZmCsfeResult(conv.msgs));
769 	}
770 };
771 
772 ZmConvListController.prototype._handleResponseLoadItem =
773 function(params, result) {
774 	if (result) {
775 		this._mailListView._expand(params.conv, params.msg);
776 	}
777 	if (params.callback) {
778 		params.callback.run();
779 	}
780 };
781 
782 /**
783  * Adapted from ZmListController::_paginate
784  */
785 ZmConvListController.prototype._paginateConv =
786 function(conv, offset, callback) {
787 
788 	var list = conv.msgs;
789 	// see if we're out of msgs and the server has more
790 	var limit = appCtxt.get(ZmSetting.CONVERSATION_PAGE_SIZE);
791 	if (offset && list && ((offset + limit > list.size()) && list.hasMore())) {
792 		// figure out how many items we need to fetch
793 		var delta = (offset + limit) - list.size();
794 		var max = delta < limit && delta > 0 ? delta : limit;
795 		if (max < limit) {
796 			offset = ((offset + limit) - max) + 1;
797 		}
798 		var respCallback = new AjxCallback(this, this._handleResponsePaginateConv, [conv, offset, callback]);
799 		conv.load({offset:offset, limit:limit}, respCallback);
800 		return false;
801 	} else {
802 		return true;
803 	}
804 };
805 
806 ZmConvListController.prototype._handleResponsePaginateConv =
807 function(conv, offset, callback, result) {
808 
809 	if (!conv.msgs) { return; }
810 
811 	var searchResult = result.getResponse();
812 	conv.msgs.setHasMore(searchResult.getAttribute("more"));
813 	var newList = searchResult.getResults(ZmItem.MSG).getVector();
814 	conv.msgs.cache(offset, newList);
815 	if (callback) {
816 		callback.run(result);
817 	}
818 };
819 
820 ZmConvListController.prototype._collapse =
821 function(item) {
822 	if (this._mailListView._rowsArePresent(item)) {	
823 		this._mailListView._collapse(item);
824 	} else {
825 		// reset state and expand instead
826 		this._toggle(item);
827 	}
828 };
829 
830 // Actions
831 //
832 // Since a selection might contain both convs and msgs, we need to split them up and
833 // invoke the action for each type separately.
834 
835 /**
836  * Takes the given list of items (convs and msgs) and splits it into one list of each
837  * type. Since an action applied to a conv is also applied to its msgs, we remove any
838  * msgs whose owning conv is also in the list.
839  */
840 ZmConvListController.prototype._divvyItems =
841 function(items) {
842 	var convs = [], msgs = [];
843 	var convIds = {};
844 	for (var i = 0; i < items.length; i++) {
845 		var item = items[i];
846 		if (item.type == ZmItem.CONV) {
847 			convs.push(item);
848 			convIds[item.id] = true;
849 		} else {
850 			msgs.push(item);
851 		}
852 	}
853 	var msgs1 = [];
854 	for (var i = 0; i < msgs.length; i++) {
855 		if (!convIds[msgs[i].cid]) {
856 			msgs1.push(msgs[i]);
857 		}
858 	}
859 	var lists = {};
860 	lists[ZmItem.MSG] = msgs1;	
861 	lists[ZmItem.CONV] = convs;
862 	
863 	return lists;
864 };
865 
866 /**
867  * Need to make sure conv's msg list has current copy of draft.
868  * 
869  * @param msg	[ZmMailMsg]		saved draft
870  */
871 ZmConvListController.prototype._draftSaved =
872 function(msg, resp) {
873 
874     if (resp) {
875         msg = msg || new ZmMailMsg();
876         msg._loadFromDom(resp);
877     }
878     var conv = appCtxt.getById(msg.cid);
879 	if (conv && conv.msgs && conv.msgs.size()) {
880 		var a = conv.msgs.getArray();
881 		for (var i = 0; i < a.length; i++) {
882 			if (a[i].id == msg.id) {
883 				a[i] = msg;
884 			}
885 		}
886 	}
887 	ZmDoublePaneController.prototype._draftSaved.apply(this, [msg]);
888 };
889 
890 ZmConvListController.prototype._redrawDraftItemRows =
891 function(msg) {
892 	var lv = this._listView[this._currentViewId];
893 	var conv = appCtxt.getById(msg.cid);
894 	if (conv) {
895 		conv._loadFromMsg(msg);	// update conv
896 		lv.redrawItem(conv);
897 		lv.setSelection(conv, true);
898 	}
899 	// don't think a draft conv is ever expandable, but try anyway
900 	lv.redrawItem(msg);
901 };
902 
903 // override to do nothing if we are deleting/moving a msg within conv view in the reading pane
904 ZmConvListController.prototype._getNextItemToSelect =
905 function(omit) {
906 	var lv = this._listView[this._currentViewId];
907 	return (lv && lv._selectedMsg) ? null : ZmDoublePaneController.prototype._getNextItemToSelect.apply(this, arguments);
908 };
909 
910 /**
911  * Splits the given items into two lists, one of convs and one of msgs, and
912  * applies the given method and args to each.
913  *
914  * @param items		[array]			list of convs and/or msgs
915  * @param method	[string]		name of function to call in parent class
916  * @param args		[array]			additional args to pass to function
917  */
918 ZmConvListController.prototype._applyAction =
919 function(items, method, args) {
920 	args = args ? args : [];
921 	var lists = this._divvyItems(items);
922 	var hasMsgs = false;
923 	if (lists[ZmItem.MSG] && lists[ZmItem.MSG].length) {
924 		args.unshift(lists[ZmItem.MSG]);
925 		ZmDoublePaneController.prototype[method].apply(this, args);
926 		hasMsgs = true;
927 	}
928 	if (lists[ZmItem.CONV] && lists[ZmItem.CONV].length) {
929 		if (hasMsgs) {
930 			args[0] = lists[ZmItem.CONV];
931 		}
932 		else {
933 			args.unshift(lists[ZmItem.CONV]);
934 		}
935 		ZmDoublePaneController.prototype[method].apply(this, args);
936 	}
937 };
938 
939 ZmConvListController.prototype._doFlag =
940 function(items, on) {
941 	if (on !== true && on !== false) {
942 		on = !items[0].isFlagged;
943 	}
944 	this._applyAction(items, "_doFlag", [on]);
945 };
946 
947 ZmConvListController.prototype._doMsgPriority = 
948 function(items) {
949 	var on = !items[0].isPriority;
950 	this._applyAction(items, "_doMsgPriority", [on]);
951 };
952 
953 ZmConvListController.prototype._doTag =
954 function(items, tag, doTag) {
955 	this._applyAction(items, "_doTag", [tag, doTag]);
956 };
957 
958 ZmConvListController.prototype._doRemoveAllTags =
959 function(items) {
960 	this._applyAction(items, "_doRemoveAllTags");
961 };
962 
963 ZmConvListController.prototype._doDelete =
964 function(items, hardDelete, attrs) {
965 	this._applyAction(items, "_doDelete", [hardDelete, attrs]);
966 };
967 
968 ZmConvListController.prototype._doMove =
969 function(items, folder, attrs, isShiftKey) {
970 	this._applyAction(items, "_doMove", [folder, attrs, isShiftKey]);
971 };
972 
973 ZmConvListController.prototype._doMarkRead =
974 function(items, on, callback, forceCallback) {
975 	this._applyAction(items, "_doMarkRead", [on, callback, forceCallback]);
976 };
977 
978 ZmConvListController.prototype._doMarkMute =
979 function(items, on, callback, forceCallback) {
980 	this._applyAction(items, "_doMarkMute", [on, callback, forceCallback]);
981 };
982 
983 ZmConvListController.prototype._doSpam =
984 function(items, markAsSpam, folder) {
985 	this._applyAction(items, "_doSpam", [markAsSpam, folder]);
986 };
987 
988 // Callbacks
989 
990 ZmConvListController.prototype._handleResponsePaginate = 
991 function(view, saveSelection, loadIndex, offset, result, ignoreResetSelection) {
992 	// bug fix #5134 - overload to ignore resetting the selection since it is handled by setView
993 	ZmListController.prototype._handleResponsePaginate.call(this, view, saveSelection, loadIndex, offset, result, true);
994 };
995