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 double pane controller.
 26  * @constructor
 27  * @class
 28  * This class manages the two-pane view. The top pane contains a list view of 
 29  * items, and the bottom pane contains the selected item content.
 30  *
 31  * @author Parag Shah
 32  * 
 33  * @param {DwtControl}					container					the containing shell
 34  * @param {ZmApp}						mailApp						the containing application
 35  * @param {constant}					type						type of controller
 36  * @param {string}						sessionId					the session id
 37  * @param {ZmSearchResultsController}	searchResultsController		containing controller
 38  * 
 39  * @extends		ZmMailListController
 40  */
 41 ZmDoublePaneController = function(container, mailApp, type, sessionId, searchResultsController) {
 42 
 43 	if (arguments.length == 0) { return; }
 44 
 45 	ZmMailListController.apply(this, arguments);
 46 
 47 	if (this.supportsDnD()) {
 48 		this._dragSrc = new DwtDragSource(Dwt.DND_DROP_MOVE);
 49 		this._dragSrc.addDragListener(this._dragListener.bind(this));
 50 	}
 51 	
 52 	this._listSelectionShortcutDelayAction = new AjxTimedAction(this, this._listSelectionTimedAction);
 53 	this._listeners[ZmOperation.KEEP_READING] = this._keepReadingListener.bind(this);
 54 };
 55 
 56 ZmDoublePaneController.prototype = new ZmMailListController;
 57 ZmDoublePaneController.prototype.constructor = ZmDoublePaneController;
 58 
 59 ZmDoublePaneController.prototype.isZmDoublePaneController = true;
 60 ZmDoublePaneController.prototype.toString = function() { return "ZmDoublePaneController"; };
 61 
 62 ZmDoublePaneController.LIST_SELECTION_SHORTCUT_DELAY = 300;
 63 
 64 ZmDoublePaneController.RP_IDS = [ZmSetting.RP_BOTTOM, ZmSetting.RP_RIGHT, ZmSetting.RP_OFF];
 65 
 66 ZmDoublePaneController.DEFAULT_TAB_TEXT = ZmMsg.conversation;
 67 
 68 /**
 69  * Displays the given list of mail items in a two-pane view where one pane shows the list
 70  * and the other shows the currently selected mail item (msg or conv). This method takes
 71  * care of displaying the list. Displaying an item is typically handled via selection.
 72  *
 73  * @param {ZmSearchResults}	results		the current search results
 74  * @param {ZmMailList}		mailList	list of mail items
 75  * @param {AjxCallback}		callback	the client callback
 76  * @param {Boolean}			markRead	if <code>true</code>, mark msg read
 77  * 
 78  */
 79 ZmDoublePaneController.prototype.show =
 80 function(results, mailList, callback, markRead) {
 81 
 82 	ZmMailListController.prototype.show.call(this, results);
 83 	
 84 	var mlv = this._listView[this._currentViewId];
 85 
 86 	// if search was run as a result of a <refresh> block rather than by the user, preserve
 87 	// what's in the reading pane as long as it's still in the list of results
 88 	var s = results && results.search;
 89 	var isRefresh = s && (s.isRefresh || s.isRedo);
 90 	var refreshSelItem = (isRefresh && mlv && mlv.hasItem(s.selectedItem) && s.selectedItem);
 91 	if (this._doublePaneView) {
 92 		if (!refreshSelItem) {
 93 			this._doublePaneView._itemView.reset();
 94 		}
 95 	}
 96 	this.setList(mailList);
 97 	this._setup(this._currentViewId);
 98 	mlv = this._listView[this._currentViewId]; //might have been created in the _setup call
 99 	mlv.reset(); //called to reset the groups (in case "group by" is used, to clear possible previous items in it - bug 77154
100 
101 	this._displayResults(this._currentViewId, null, refreshSelItem);
102 
103 	if (refreshSelItem) {
104 		mlv.setSelection(refreshSelItem, true);
105 		this._resetOperations(this._toolbar[this._currentViewId], 1)
106 	}
107 	else {
108 		var dpv = this._doublePaneView;
109 		var readingPaneOn = this.isReadingPaneOn();
110 		if (dpv.isReadingPaneVisible() != readingPaneOn) {
111 			dpv.setReadingPane();
112 		}
113 		// clear the item view, unless it's showing something selected
114 		if (!this._itemViewCurrent()) {
115 			dpv.clearItem();
116 		}
117 	}
118 
119 	if (callback) {
120 		callback.run();
121 	}
122 };
123 
124 // returns true if the item shown in the reading pane is selected in the list view
125 ZmDoublePaneController.prototype._itemViewCurrent =
126 function() {
127 
128 	var dpv = this._doublePaneView;
129 	var mlv = dpv._mailListView;
130 	mlv._restoreState();
131 	var item = dpv.getItem();
132 	if (item) {
133 		var sel = mlv.getSelection();
134 		for (var i = 0, len = sel.length; i < len; i++) {
135 			var listItem = sel[i];
136 			if (listItem.id == item.id) {
137 				return true;
138 			}
139 		}
140 	}
141 	return false;
142 };
143 
144 ZmDoublePaneController.prototype.switchView =
145 function(view, force) {
146 	if (view === ZmSetting.RP_OFF || view === ZmSetting.RP_BOTTOM || view === ZmSetting.RP_RIGHT) {
147 		this._mailListView._colHeaderActionMenu = null;
148 		var oldView = this._getReadingPanePref();
149 		if (view !== oldView) {
150 			var convView = this._convView;
151 			if (convView) {
152 				var replyView = convView._replyView;
153 				if (replyView && view === ZmSetting.RP_OFF) {
154 					// reset the replyView with the warning before turning off the pane
155 					if (!force && !convView._controller.popShield(null, this.switchView.bind(this, view, true))) {
156 						// redo setChecked on the oldView menu item if user cancels
157 						this._readingPaneViewMenu.getMenuItem(oldView)._setChecked(true);
158 						return;
159 					}
160 					this._readingPaneViewMenu.getMenuItem(view)._setChecked(true);
161 					replyView.reset();
162 				}
163 			}
164 			this._setReadingPanePref(view);
165 			this._doublePaneView.setReadingPane(true);
166 			if (replyView && view !== ZmSetting.RP_OFF) {
167 				replyView._resized();
168 			}
169 		}
170 	} else {
171 		ZmMailListController.prototype.switchView.apply(this, arguments);
172 	}
173 	this._resetNavToolBarButtons();
174 };
175 
176 /**
177  * Clears the conversation view, which actually just clears the message view.
178  */
179 ZmDoublePaneController.prototype.reset =
180 function() {
181 	if (this._doublePaneView) {
182 		this._doublePaneView.reset();
183 	}
184 	var lv = this._listView[this._currentViewId];
185 	if (lv) {
186 		lv._itemToSelect = lv._selectedItem = null;
187 	}
188 };
189 
190 ZmDoublePaneController.prototype._handleResponseSwitchView =
191 function(item) {
192 	this._doublePaneView.setItem(item);
193 };
194 
195 // called after a delete has occurred. 
196 // Return value indicates whether view was popped as a result of a delete
197 ZmDoublePaneController.prototype.handleDelete = 
198 function() {
199 	return false;
200 };
201 
202 ZmDoublePaneController.prototype.handleKeyAction =
203 function(actionCode, ev) {
204 
205 	DBG.println(AjxDebug.DBG3, "ZmDoublePaneController.handleKeyAction");
206 	var lv = this._listView[this._currentViewId];
207 
208 	switch (actionCode) {
209 
210 		case DwtKeyMap.SELECT_NEXT:
211 		case DwtKeyMap.SELECT_PREV:
212 			if (lv) {
213 				return lv.handleKeyAction(actionCode, ev);
214 			}
215 			break;
216 
217 		default:
218 			return ZmMailListController.prototype.handleKeyAction.apply(this, arguments);
219 	}
220 	return true;
221 };
222 
223 // Private and protected methods
224 
225 ZmDoublePaneController.prototype._createDoublePaneView = 
226 function() {
227 	// overload me
228 };
229 
230 // Creates the conv view, which is not a standard list view (it's a two-pane
231 // sort of thing).
232 ZmDoublePaneController.prototype._initialize =
233 function(view) {
234 	// set up double pane view (which creates the MLV and MV)
235 	if (!this._doublePaneView){
236 		var dpv = this._doublePaneView = this._createDoublePaneView();
237 		this._mailListView = dpv.getMailListView();
238 		dpv.addInviteReplyListener(this._inviteReplyListener);
239 		dpv.addShareListener(this._shareListener);
240 		dpv.addSubscribeListener(this._subscribeListener);
241 	}
242 
243 	ZmMailListController.prototype._initialize.call(this, view);
244 };
245 
246 ZmDoublePaneController.prototype._initializeNavToolBar =
247 function(view) {
248 	var toolbar = this._toolbar[view];
249 	this._itemCountText[ZmSetting.RP_BOTTOM] = toolbar.getButton(ZmOperation.TEXT);
250 	if (AjxEnv.isFirefox) {
251 		this._itemCountText[ZmSetting.RP_BOTTOM].setScrollStyle(Dwt.CLIP);
252 	}
253 };
254 
255 ZmDoublePaneController.prototype._getRightSideToolBarOps =
256 function() {
257 	var list = [];
258 	if (appCtxt.isChildWindow) {
259 		return list;
260 	}
261 	list.push(ZmOperation.KEEP_READING);
262 	list.push(ZmOperation.VIEW_MENU);
263 	return list;
264 };
265 
266 ZmDoublePaneController.prototype._getActionMenuOps =
267 function() {
268 	var list = [];
269 	list = list.concat(this._msgOps());
270 	list.push(ZmOperation.SEP);
271 	list = list.concat(this._deleteOps());
272 	list.push(ZmOperation.SEP);
273 	list = list.concat(this._standardActionMenuOps());
274 	list.push(ZmOperation.SEP);
275 	list = list.concat(this._flagOps());
276 	list.push(ZmOperation.SEP);
277     list.push(ZmOperation.REDIRECT);
278     list.push(ZmOperation.EDIT_AS_NEW);		// bug #28717
279 	list.push(ZmOperation.SEP);
280 	list = list.concat(this._createOps());
281 	list.push(ZmOperation.SEP);
282 	list = list.concat(this._otherOps());
283 	if (this.getCurrentViewType() == ZmId.VIEW_TRAD) {
284 		list.push(ZmOperation.SHOW_CONV);
285 	}
286     //list.push(ZmOperation.QUICK_COMMANDS);
287 	return list;
288 };
289 
290 // Returns the already-created message list view.
291 ZmDoublePaneController.prototype._createNewView = 
292 function() {
293 	if (this._mailListView && this._dragSrc) {
294 		this._mailListView.setDragSource(this._dragSrc);
295 	}
296 	return this._mailListView;
297 };
298 
299 /**
300  * Returns the double-pane view.
301  * 
302  * @return {ZmDoublePaneView}	double-pane view
303  */
304 ZmDoublePaneController.prototype.getCurrentView = 
305 function() {
306 	return this._doublePaneView;
307 };
308 ZmDoublePaneController.prototype.getReferenceView = ZmDoublePaneController.prototype.getCurrentView;
309 
310 /**
311  * Returns the item view.
312  * 
313  * @return {ZmMailItemView}	item view
314  */
315 ZmDoublePaneController.prototype.getItemView = 
316 function() {
317 	return this._doublePaneView && this._doublePaneView._itemView;
318 };
319 
320 ZmDoublePaneController.prototype._getTagMenuMsg = 
321 function(num) {
322 	return AjxMessageFormat.format(ZmMsg.tagMessages, num);
323 };
324 
325 ZmDoublePaneController.prototype._getMoveDialogTitle = 
326 function(num) {
327 	return AjxMessageFormat.format(ZmMsg.moveMessages, num);
328 };
329 
330 // Add reading pane to focus ring
331 ZmDoublePaneController.prototype._initializeTabGroup =
332 function(view) {
333 	if (this._tabGroups[view]) { return; }
334 
335 	ZmListController.prototype._initializeTabGroup.apply(this, arguments);
336 
337 	if (this._view[view] !== this.getItemView()) {
338 		this._tabGroups[view].addMember(this.getItemView().getTabGroupMember());
339 	}
340 };
341 
342 ZmDoublePaneController.prototype._setViewContents =
343 function(view) {
344 	this._doublePaneView.setList(this._list);
345 };
346 
347 ZmDoublePaneController.prototype._displayItem =
348 function(item) {
349 
350 	if (!item._loaded) { return; }
351 
352 	// cancel timed mark read action on previous msg
353 	appCtxt.killMarkReadTimer();
354 
355 	this._doublePaneView.setItem(item);
356 	this._handleMarkRead(item);
357 	this._curItem = item;
358 };
359 ZmDoublePaneController.prototype._displayMsg = ZmDoublePaneController.prototype._displayItem;
360 
361 
362 ZmDoublePaneController.prototype._markReadAction =
363 function(msg) {
364 	this._doMarkRead([msg], true);
365 };
366 
367 ZmDoublePaneController.prototype._preHideCallback =
368 function() {
369 	// cancel timed mark read action on view change
370 	appCtxt.killMarkReadTimer();
371 	return ZmController.prototype._preHideCallback.call(this);
372 };
373 
374 // Adds a "Reading Pane" menu to the View menu
375 ZmDoublePaneController.prototype._setupReadingPaneMenu =
376 function(view, menu) {
377 
378 	var readingPaneMenuItem = menu.createMenuItem(Dwt.getNextId("READING_PANE_"), {
379 			text:   ZmMsg.readingPane,
380 			style:  DwtMenuItem.NO_STYLE
381 		}),
382 		readingPaneMenu = new ZmPopupMenu(readingPaneMenuItem);
383 
384 	var miParams = {
385 		text:           ZmMsg.readingPaneAtBottom,
386 		style:          DwtMenuItem.RADIO_STYLE,
387 		radioGroupId:   "RP"
388 	};
389 	var ids = ZmDoublePaneController.RP_IDS;
390 	var pref = this._getReadingPanePref();
391 	for (var i = 0; i < ids.length; i++) {
392 		var id = ids[i];
393 		if (!readingPaneMenu._menuItems[id]) {
394 			miParams.text = ZmMailListController.READING_PANE_TEXT[id];
395 			miParams.image = ZmMailListController.READING_PANE_ICON[id];
396 			var mi = readingPaneMenu.createMenuItem(id, miParams);
397 			mi.setData(ZmOperation.MENUITEM_ID, id);
398 			mi.addSelectionListener(this._listeners[ZmOperation.VIEW]);
399 			if (id == pref) {
400 				mi.setChecked(true, true);
401 			}
402 		}
403 	}
404 
405 	readingPaneMenuItem.setMenu(readingPaneMenu);
406 
407 	return readingPaneMenu;
408 };
409 
410 ZmDoublePaneController.prototype._displayResults =
411 function(view, newTab, refreshSelItem) {
412 
413 	var elements = this.getViewElements(view, this._doublePaneView);
414 	
415 	if (!refreshSelItem) {
416 		this._doublePaneView.setReadingPane();
417 	}
418 
419 	var tabId = newTab && Dwt.getNextId();
420 	this._setView({ view:		view,
421 					noPush:		this.isSearchResults,
422 					viewType:	this._currentViewType,
423 					elements:	elements,
424 					hide:		this._elementsToHide,
425 					tabParams:	newTab && this._getTabParams(tabId, this._tabCallback.bind(this)),
426 					isAppView:	this._isTopLevelView()});
427 	if (this.isSearchResults) {
428 		// if we are switching views, make sure app view mgr is up to date on search view's components
429 		appCtxt.getAppViewMgr().setViewComponents(this.searchResultsController.getCurrentViewId(), elements, true);
430 	}
431 	this._resetNavToolBarButtons(view);
432 
433 	if (newTab) {
434 		var buttonText = (this._conv && this._conv.subject) ? this._conv.subject.substr(0, ZmAppViewMgr.TAB_BUTTON_MAX_TEXT) : ZmDoublePaneController.DEFAULT_TAB_TEXT;
435 		var avm = appCtxt.getAppViewMgr();
436 		avm.setTabTitle(view, buttonText);
437 	}
438 				
439 	// always allow derived classes to reset size after loading
440 	var sz = this._doublePaneView.getSize();
441 	this._doublePaneView._resetSize(sz.x, sz.y);
442 };
443 
444 ZmDoublePaneController.prototype._getTabParams =
445 function(tabId, tabCallback) {
446 	return {
447 		id:				tabId,
448 		image:			"ConvView",
449 		textPrecedence:	85,
450 		tooltip:		ZmDoublePaneController.DEFAULT_TAB_TEXT,
451 		tabCallback:	tabCallback
452 	};
453 };
454 
455 
456 /**
457  * Loads and displays the given message. If the message was unread, it gets marked as
458  * read, and the conversation may be marked as read as well. Note that we request no
459  * busy overlay during the SOAP call - that's so that a subsequent click within the
460  * double-click threshold can be processed. Otherwise, it's very difficult to generate
461  * a double click because the first click triggers a SOAP request and the ensuing busy
462  * overlay blocks the receipt of the second click.
463  * 
464  * @param msg	[ZmMailMsg]		msg to load
465  * 
466  * @private
467  */
468 ZmDoublePaneController.prototype._doGetMsg =
469 function(msg) {
470 	if (!msg) { return; }
471 	if (msg.id == this._pendingMsg) { return; }
472 
473 	msg._loadPending = true;
474 	this._pendingMsg = msg.id;
475 	var respCallback = new AjxCallback(this, this._handleResponseDoGetMsg, msg);
476 	msg.load({callback:respCallback, noBusyOverlay:true});
477 };
478 
479 ZmDoublePaneController.prototype._handleResponseDoGetMsg =
480 function(msg) {
481 	if (this._pendingMsg && (msg.id != this._pendingMsg)) { return; }
482 	msg._loadPending = false;
483 	this._pendingMsg = null;
484 	this._doublePaneView.setItem(msg);
485 };
486 
487 ZmDoublePaneController.prototype._resetOperations =
488 function(parent, num) {
489 	ZmMailListController.prototype._resetOperations.call(this, parent, num);
490 	var isDraft = this.isDraftsFolder();
491 	if (num == 1) {
492 		var item = this._mailListView.getSelection()[0];
493 		if (item) {
494 			isDraft = item.isDraft;
495 		}
496 		parent.enable(ZmOperation.SHOW_ORIG, true);
497 		if (appCtxt.get(ZmSetting.FILTERS_ENABLED)) {
498 			var isSyncFailuresFolder = this.isSyncFailuresFolder();
499 			parent.enable(ZmOperation.ADD_FILTER_RULE, !isSyncFailuresFolder);
500 		}
501 	}
502 
503 	parent.enable(ZmOperation.DETACH, (appCtxt.get(ZmSetting.DETACH_MAILVIEW_ENABLED) && num == 1 && !isDraft));
504 	parent.enable(ZmOperation.TEXT, true);
505 	parent.enable(ZmOperation.KEEP_READING, this._keepReading(true));
506 
507 	if (appCtxt.isWebClientOffline()) {
508 		parent.enable(
509 			[
510 				ZmOperation.ACTIONS_MENU,
511 				ZmOperation.VIEW_MENU,
512 				ZmOperation.DETACH,
513 				ZmOperation.SHOW_ORIG,
514 				ZmOperation.SHOW_CONV,
515 				ZmOperation.PRINT,
516 				ZmOperation.ADD_FILTER_RULE,
517 				ZmOperation.CREATE_APPT,
518 				ZmOperation.CREATE_TASK,
519 				ZmOperation.CONTACT,
520 				ZmOperation.REDIRECT
521 			],
522 			false
523 		);
524 	}
525 };
526 
527 ZmDoublePaneController.prototype._resetOperation = 
528 function(parent, op, num) {
529 	if (parent && op == ZmOperation.KEEP_READING) {
530 		parent.enable(ZmOperation.KEEP_READING, this._keepReading(true));
531 	}
532 };
533 
534 // top level view means this view is allowed to get shown when user clicks on 
535 // app icon in app toolbar - overload to not allow this.
536 ZmDoublePaneController.prototype._isTopLevelView = 
537 function() {
538 	var sessionId = this.getSessionId();
539 	return (!sessionId || (sessionId == ZmApp.MAIN_SESSION));
540 };
541 
542 // All items in the list view are gone - show "No Results" and clear reading pane
543 ZmDoublePaneController.prototype._handleEmptyList =
544 function(listView) {
545 	this.reset();
546 	ZmMailListController.prototype._handleEmptyList.apply(this, arguments);
547 };
548 
549 // List listeners
550 
551 // Clicking on a message in the message list loads and displays it.
552 ZmDoublePaneController.prototype._listSelectionListener =
553 function(ev) {
554 
555 	var handled = ZmMailListController.prototype._listSelectionListener.call(this, ev);
556 	
557 	var currView = this._listView[this._currentViewId];
558 
559 	if (!handled && ev.detail == DwtListView.ITEM_DBL_CLICKED) {
560 		var item = ev.item;
561 		if (!item) { return; }
562 
563 		var cs = appCtxt.isOffline && appCtxt.getCurrentSearch();
564 		if (cs && cs.isMultiAccount()) {
565 			appCtxt.accountList.setActiveAccount(item.getAccount());
566 		}
567 
568 		var div = this._mailListView.getTargetItemDiv(ev);
569 		this._mailListView._itemSelected(div, ev);
570 
571 		if (appCtxt.get(ZmSetting.SHOW_SELECTION_CHECKBOX)) {
572 			this._mailListView.setSelectionHdrCbox(false);
573 		}
574 
575 		var respCallback = new AjxCallback(this, this._handleResponseListSelectionListener, item);
576 		var folder = appCtxt.getById(item.getFolderId());
577 		if (item.isDraft && folder && folder.id == ZmFolder.ID_DRAFTS && (!folder || !folder.isReadOnly())) {
578 			this._doAction({ev:ev, action:ZmOperation.DRAFT});
579 			return true;
580 		} else if (appCtxt.get(ZmSetting.OPEN_MAIL_IN_NEW_WIN)) {
581 			this._detachListener(null, respCallback);
582 			return true;
583 		} else {
584 			var respCallback =
585 				this._handleResponseListSelectionListener.bind(this, item);
586 			var ctlr =
587 				AjxDispatcher.run(item.type === ZmItem.CONV ?
588 				                  "GetConvController" : "GetMsgController",
589 				                  item.nId);
590 			ctlr.show(item, this, respCallback, true);
591 			return true;
592 		}
593 	} else if (!handled) {
594 		if (this.isReadingPaneOn()) {
595 			// Give the user a chance to zip around the list view via shortcuts without having to
596 			// wait for each successively selected msg to load, by waiting briefly for more list
597 			// selection shortcut actions. An event will have the 'kbNavEvent' property set if it's
598 			// the result of a shortcut.
599 			if (ev.kbNavEvent && ZmDoublePaneController.LIST_SELECTION_SHORTCUT_DELAY) {
600 				if (this._listSelectionShortcutDelayActionId) {
601 					AjxTimedAction.cancelAction(this._listSelectionShortcutDelayActionId);
602 				}
603 				this._listSelectionShortcutDelayActionId = AjxTimedAction.scheduleAction(this._listSelectionShortcutDelayAction,
604 																						 ZmDoublePaneController.LIST_SELECTION_SHORTCUT_DELAY);
605 			} else {
606 				this._setSelectedItem();
607 			}
608 			return true;
609 		} else {
610 			var msg = currView.getSelection()[0];
611 			if (msg) {
612 				this._doublePaneView.resetMsg(msg);
613 			}
614 		}
615 	}
616 
617 	return handled;
618 };
619 
620 ZmDoublePaneController.prototype._handleResponseListSelectionListener =
621 function(item) {
622 	if (item.type == ZmItem.MSG && item._loaded && item.isUnread &&
623 		(appCtxt.get(ZmSetting.MARK_MSG_READ) == ZmSetting.MARK_READ_NOW)) {
624 
625 		this._list.markRead([item], true);
626 	}
627 	// make sure correct msg is displayed in msg pane when user returns
628 	this._setSelectedItem();
629 };
630 
631 ZmDoublePaneController.prototype._listSelectionTimedAction =
632 function() {
633 	if (this._listSelectionShortcutDelayActionId) {
634 		AjxTimedAction.cancelAction(this._listSelectionShortcutDelayActionId);
635 	}
636 	this._setSelectedItem();
637 };
638 
639 /**
640  * Handles selection of a row by loading the item.
641  * 
642  * @param {hash}	params		params for loading the item
643  * 
644  * @private
645  */
646 ZmDoublePaneController.prototype._setSelectedItem =
647 function(params) {
648 	var selCnt = this._listView[this._currentViewId].getSelectionCount();
649 	if (selCnt == 1) {
650 		var respCallback = this._handleResponseSetSelectedItem.bind(this);
651 		this._getLoadedMsg(params, respCallback);
652 	}
653 };
654 
655 ZmDoublePaneController.prototype._handleResponseSetSelectedItem =
656 function(msg) {
657 
658 	if (msg) {
659 		// bug 41196
660 		if (appCtxt.isOffline) {
661 			// clear the new-mail badge every time user reads a msg regardless
662 			// of number of unread messages across all accounts
663 			this._app.clearNewMailBadge();
664 
665 			// offline mode, reset new mail notifier if user reads a msg from that account
666 			var acct = msg.getAccount();
667 
668 			// bug: 46873 - set active account when user clicks on item w/in cross-account search
669 			var cs = appCtxt.getCurrentSearch();
670 			if (cs && cs.isMultiAccount()) {
671 				var active = acct || appCtxt.accountList.defaultAccount
672 				appCtxt.accountList.setActiveAccount(active);
673 			}
674 
675 			if (acct && acct.inNewMailMode) {
676 				acct.inNewMailMode = false;
677 				var allContainers = appCtxt.getOverviewController()._overviewContainer;
678 				for (var i in allContainers) {
679 					allContainers[i].updateAccountInfo(acct, true, true);
680 				}
681 			}
682 		}
683 
684 		if (!this.isReadingPaneOn()) {
685 			return;
686 		}
687 		// make sure list view has this msg
688 		var lv = this._listView[this._currentViewId];
689 		var id = (lv.type == ZmItem.CONV && msg.type == ZmItem.MSG) ? msg.cid : msg.id;
690 		if (lv.hasItem(id)) {
691 			this._displayMsg(msg);
692 		}
693 	}
694 };
695 
696 ZmDoublePaneController.prototype._listActionListener =
697 function(ev) {
698 	ZmMailListController.prototype._listActionListener.call(this, ev);
699 
700 	if (!this.isReadingPaneOn()) {
701 		// reset current message
702 		var msg = this._listView[this._currentViewId].getSelection()[0];
703 		if (msg) {
704 			this._doublePaneView.resetMsg(msg);
705 		}
706 	}
707 };
708 
709 ZmDoublePaneController.prototype._doDelete =
710 function(items, hardDelete, attrs, confirmDelete) {
711 	this._listView[this._currentViewId]._itemToSelect = this._getNextItemToSelect();
712 	ZmMailListController.prototype._doDelete.apply(this, arguments);
713 };
714 
715 ZmDoublePaneController.prototype._doMove =
716 function(items, destinationFolder, attrs, isShiftKey) {
717 
718 	// if user moves a non-selected item via DnD, don't change selection
719 	var dndUnselectedItem = false;
720 	if (items && items.length == 1) {
721 		var lv = this.getListView();
722 		var selection = lv && lv.getSelection();
723 		if (selection && selection.length) {
724 			dndUnselectedItem = true;
725 			var moveItem = items[0];
726 			var id = moveItem.id;
727 			var msgIdMap = {};
728 			var numSelectedInConv = 0;
729 			if ((moveItem.type === ZmId.ITEM_CONV) && moveItem.msgIds) {
730 				// If the moved item is a conversation, we need to check whether the selected items are all messages
731 				// within the conversation.  Create a hash for more efficient testing.
732 				for (var i = 0; i < moveItem.msgIds.length; i++) {
733 					msgIdMap[moveItem.msgIds[i]] = true;
734 				}
735 			}
736 			// AjxUtil.intersection doesn't work with objects, so check IDs
737 			for (var i = 0; i < selection.length; i++) {
738 				if (selection[i].id == id) {
739 					dndUnselectedItem = false;
740 				}
741 				if (msgIdMap[selection[i].id]) {
742 					numSelectedInConv++;
743 				}
744 			}
745 			if (numSelectedInConv == selection.length) {
746 				// All the selected items are messages within a moved conversation, use the normal getNextItemToSelect
747 				dndUnselectedItem = false;
748 			}
749 		}
750 	}		
751 	if (!dndUnselectedItem) {
752 		this._listView[this._currentViewId]._itemToSelect = this._getNextItemToSelect();
753 	}
754 	ZmMailListController.prototype._doMove.apply(this, arguments);
755 };
756 
757 ZmDoublePaneController.prototype._keepReadingListener =
758 function(ev) {
759 	this.handleKeyAction(ZmKeyMap.KEEP_READING, ev);
760 };
761 
762 ZmDoublePaneController.prototype._keepReading = function(ev) {};
763 
764 // Set enabled state of the KEEP_READING button
765 ZmDoublePaneController.prototype._checkKeepReading =
766 function() {
767 	// done on timer so item view has had change to lay out and resize
768 	setTimeout(this._resetOperation.bind(this, this._toolbar[this._currentViewId], ZmOperation.KEEP_READING), 250);
769 };
770 
771 ZmDoublePaneController.handleScroll =
772 function(ev) {
773 	var target = DwtUiEvent.getTarget(ev);
774 	var messagesView = DwtControl.findControl(target);
775 	var controller = messagesView && messagesView._controller;
776 	if (controller && controller._checkKeepReading) {
777 		controller._checkKeepReading();
778 	}
779 };
780 
781 ZmDoublePaneController.prototype._dragListener =
782 function(ev) {
783 	ZmListController.prototype._dragListener.call(this, ev);
784 	if (ev.action == DwtDragEvent.DRAG_END) {
785 		this._resetOperations(this._toolbar[this._currentViewId], this._doublePaneView.getSelection().length);
786 	}
787 };
788 
789 ZmDoublePaneController.prototype._draftSaved =
790 function(msg, resp) {
791 	if (resp) {
792 		if (!msg) {
793 			msg = new ZmMailMsg();
794 		}
795 		msg._loadFromDom(resp);
796 	}
797 	appCtxt.cacheSet(msg.id, msg);
798 	this._redrawDraftItemRows(msg);
799 	var displayedMsg = this._doublePaneView.getMsg();
800 	if (displayedMsg && displayedMsg.id == msg.id) {
801 		this._doublePaneView.reset();
802 		this._doublePaneView.setItem(msg, null, true);
803 	}
804 };
805 
806 ZmDoublePaneController.prototype._redrawDraftItemRows =
807 function(msg) {
808 	this._listView[this._currentViewId].redrawItem(msg);
809 	this._listView[this._currentViewId].setSelection(msg, true);
810 };
811 
812 ZmDoublePaneController.prototype.selectFirstItem =
813 function() {
814 	this._doublePaneView._selectFirstItem();
815 };
816 
817 ZmDoublePaneController.prototype._getDefaultFocusItem =
818 function() {
819 	return this.getListView();
820 };
821 
822 /**
823  * Returns the item that should be selected after a move/delete. Finds
824  * the first non-selected item after the first selected item.
825  *
826  * @param	{hash}		omit		hash of item IDs to exclude from being next selected item
827  */
828 ZmDoublePaneController.prototype._getNextItemToSelect =
829 function(omit) {
830 
831 	omit = omit || {};
832 	var listView = this._listView[this._currentViewId];
833 	var numSelected = listView.getSelectionCount();
834 	if (numSelected) {
835 		var selection = listView.getSelection();
836 		var selIds = {};
837 		for (var i = 0; i < selection.length; i++) {
838 			selIds[selection[i].id] = true;
839 		}
840 		var setting = appCtxt.get(ZmSetting.SELECT_AFTER_DELETE);
841 		var goingUp = (setting == ZmSetting.DELETE_SELECT_PREV || (setting == ZmSetting.DELETE_SELECT_ADAPT &&
842 						(this.lastListAction == DwtKeyMap.SELECT_PREV || this.lastListAction == ZmKeyMap.PREV_UNREAD)));
843 		if (goingUp && (numSelected == 1)) {
844 			var idx = listView._getRowIndex(selection[selection.length - 1]);
845 			var childNodes = listView._parentEl.childNodes;
846 			for (var i = idx - 1; i >= 0; i--) {
847 				var item = listView.getItemFromElement(childNodes[i]);
848 				if (item && !selIds[item.id] && !omit[item.id] && !(item.cid && (selIds[item.cid] || omit[item.cid]))) {
849 					return item;
850 				}
851 			}
852 			return ZmMailListView.FIRST_ITEM;
853 		} else {
854 			var idx = listView._getRowIndex(selection[0]);
855 			var childNodes = listView._parentEl.childNodes;
856 			for (var i = idx + 1; i < childNodes.length; i++) {
857 				var item = listView.getItemFromElement(childNodes[i]);
858 				if (item && !selIds[item.id] && !omit[item.id] && !(item.cid && (selIds[item.cid] || omit[item.cid]))) {
859 					return item;
860 				}
861 			}
862 			return ZmMailListView.LAST_ITEM;
863 		}
864 	}
865 	return ZmMailListView.FIRST_ITEM;	
866 };
867 
868 ZmDoublePaneController.prototype._setItemCountText =
869 function(text) {
870 
871 	text = text || this._getItemCountText();
872 
873 	var rpr = (this._getReadingPanePref() == ZmSetting.RP_RIGHT);
874 	if (this._itemCountText[ZmSetting.RP_RIGHT]) {
875 		this._itemCountText[ZmSetting.RP_RIGHT].setText(rpr ? text : "");
876 	}
877 	if (this._itemCountText[ZmSetting.RP_BOTTOM]) {
878 		this._itemCountText[ZmSetting.RP_BOTTOM].setText(rpr ? "" : text);
879 	}
880 };
881 
882 ZmDoublePaneController.prototype._postShowCallback =
883 function() {
884 
885 	ZmMailListController.prototype._postShowCallback.apply(this, arguments);
886 	var dpv = this._doublePaneView;
887 	if (dpv && dpv.isStale && dpv._staleHandler) {
888 		dpv._staleHandler();
889 	}
890 };
891