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 double-pane view, with a list of conversations in the top pane,
 26  * and a message in the bottom pane.
 27  * @constructor
 28  * @class
 29  * This variation of a double pane view combines a conv list view with a reading
 30  * pane in which the first msg of a conv is shown. Any conv with more than one
 31  * message is expandable, and gets an expansion icon in the left column. Clicking on that
 32  * will display the conv's first page of messages. The icon then becomes a collapse icon and
 33  * clicking it will collapse the conv (hide the messages).
 34  * <p>
 35  * If a conv has more than one page of messages, the last message on the first page
 36  * will get a + icon, and that message is expandable.</p>
 37  *
 38  * @author Conrad Damon
 39  * 
 40  * @private
 41  */
 42 ZmConvDoublePaneView = function(params) {
 43 
 44 	this._invitereplylisteners = [];
 45 	this._sharelisteners = [];
 46 	this._subscribelisteners = [];
 47 
 48 	params.className = params.className || "ZmConvDoublePaneView";
 49 	params.mode = ZmId.VIEW_CONVLIST;
 50 	ZmDoublePaneView.call(this, params);
 51 };
 52 
 53 ZmConvDoublePaneView.prototype = new ZmDoublePaneView;
 54 ZmConvDoublePaneView.prototype.constructor = ZmConvDoublePaneView;
 55 
 56 ZmConvDoublePaneView.prototype.isZmConvDoublePaneView = true;
 57 ZmConvDoublePaneView.prototype.toString = function() { return "ZmConvDoublePaneView"; };
 58 
 59 ZmConvDoublePaneView.prototype._createMailListView =
 60 function(params) {
 61 	return new ZmConvListView(params);
 62 };
 63 
 64 // default to conv item view
 65 ZmConvDoublePaneView.prototype._createMailItemView =
 66 function(params) {
 67 	this._itemViewParams = params;
 68 	return this._getItemView(ZmItem.CONV);
 69 };
 70 
 71 // get the item view based on the given type
 72 ZmConvDoublePaneView.prototype._getItemView =
 73 function(type) {
 74 	var newview;
 75 	
 76 	this._itemViewParams.className = null;
 77 	if (type == ZmItem.CONV) {
 78 		if (!this._convView) {
 79 			this._itemViewParams.id = ZmId.getViewId(ZmId.VIEW_CONV, null, this._itemViewParams.view);
 80 			newview = this._convView = new ZmConvView2(this._itemViewParams);
 81 		}
 82 	}
 83 	else if (type == ZmItem.MSG) {
 84 		if (!this._mailMsgView) {
 85 			this._itemViewParams.id = ZmId.getViewId(ZmId.VIEW_MSG, null, this._itemViewParams.view);
 86 			newview = this._mailMsgView = new ZmMailMsgView(this._itemViewParams);
 87 		}
 88 	}
 89 
 90 	if (newview) {
 91 		AjxUtil.foreach(this._invitereplylisteners,
 92 		                function(listener) {
 93 		                	newview.addInviteReplyListener(listener);
 94 		                });
 95 		AjxUtil.foreach(this._sharelisteners,
 96 		                function(listener) {
 97 		                	newview.addShareListener(listener);
 98 		                });
 99 		AjxUtil.foreach(this._subscribelisteners,
100 		                function(listener) {
101 		                	newview.addSubscribeListener(listener);
102 		                });
103 	}
104 
105 	return (type == ZmItem.CONV) ? this._convView : this._mailMsgView;
106 };
107 
108 // set up to display either a conv or a msg in the item view
109 ZmConvDoublePaneView.prototype.setItem =
110 function(item, force) {
111 
112 	if (!force && !this._controller.popShield(null, this.setItem.bind(this, item, true))) {
113 		return;
114 	}
115 
116 	var changed = ((item.type == ZmItem.CONV) != (this._itemView && this._itemView == this._convView));
117 	var itemView = this._itemView = this._getItemView(item.type);
118 	var otherView = (item.type == ZmItem.CONV) ? this._mailMsgView : this._convView;
119 	if (otherView) {
120 		otherView.setVisible(false);
121 	}
122 	// Clear quick reply if going from msg view to conv view in reading pane
123 	if (changed && itemView && itemView._replyView) {
124 		itemView._replyView.reset();
125 	}
126 	this._itemView.setVisible(true,null,item);
127 	if (changed) {
128 		this.setReadingPane(true);	// so that second view gets positioned
129 	}
130 
131 	return ZmDoublePaneView.prototype.setItem.apply(this, arguments);
132 };
133 
134 ZmConvDoublePaneView.prototype.addInviteReplyListener =
135 function(listener) {
136 	this._invitereplylisteners.push(listener);
137 	ZmDoublePaneView.prototype.addInviteReplyListener.call(this, listener);
138 };
139 
140 ZmConvDoublePaneView.prototype.addShareListener =
141 function(listener) {
142 	this._sharelisteners.push(listener);
143 	ZmDoublePaneView.prototype.addShareListener.call(this, listener);
144 };
145 
146 ZmConvDoublePaneView.prototype.addSubscribeListener =
147 function(listener) {
148 	this._subscribelisteners.push(listener);
149 	ZmDoublePaneView.prototype.addSubscribeListener.call(this, listener);
150 };
151 
152 /**
153  * This class is a ZmMailListView which can display both convs and msgs.
154  * It handles expanding convs as well as paging additional messages in. Message rows are
155  * inserted after the row of the owning conv.
156  * 
157  * @private
158  */
159 ZmConvListView = function(params) {
160 
161 	params.type = ZmItem.CONV;
162 	this._controller = params.controller;
163 	this._mode = this.view = ZmId.VIEW_CONVLIST;
164 	params.headerList = this._getHeaderList();
165 	ZmMailListView.call(this, params);
166 
167 	// change listener needs to handle both types of events
168 	this._handleEventType[ZmItem.CONV] = true;
169 	this._handleEventType[ZmItem.MSG] = true;
170 
171 	this.setAttribute("aria-label", ZmMsg.conversationList);
172 
173 	this._hasHiddenRows = true;	// so that up and down arrow keys work
174 	this._resetExpansion();
175 };
176 
177 ZmConvListView.prototype = new ZmMailListView;
178 ZmConvListView.prototype.constructor = ZmConvListView;
179 
180 ZmConvListView.prototype.isZmConvListView = true;
181 ZmConvListView.prototype.toString = function() { return "ZmConvListView"; };
182 
183 ZmConvListView.prototype.role = 'tree';
184 ZmConvListView.prototype.itemRole = 'treeitem';
185 
186 // Constants
187 
188 ZmListView.FIELD_CLASS[ZmItem.F_EXPAND] = "Expand";
189 
190 ZmConvListView.MSG_STYLE = "ZmConvExpanded";	// for differentiating msg rows
191 
192 
193 ZmConvListView.prototype.set =
194 function(list, sortField) {
195 	if (this.offset == 0) {
196 		this._resetExpansion();
197 	}
198 	ZmMailListView.prototype.set.apply(this, arguments);
199 };
200 
201 /**
202  * check whether all conversations are checked
203  * overrides ZmListView.prototype._isAllChecked since the list here contains both conversations and messages, and we care only about messages
204  * @return {Boolean} true if all conversations are checked
205  */
206 ZmConvListView.prototype._isAllChecked =
207 function() {
208 	var selection = this.getSelection();
209 	//let's see how many conversations are checked.
210 	//ignore checked messages. Sure, if the user selects manually all messages in a conversation, the
211 	//conversation is not selected automatically too, but that's fine I think.
212 	//This method returns true if and only if all the conversations (in the conversation layer of the tree) are selected
213 	var convsSelected = 0;
214 	for (var i = 0; i < selection.length; i++) {
215 		if (selection[i].type == ZmItem.CONV) {
216 			convsSelected++;
217 		}
218 	}
219 
220 	var list = this.getList();
221 	return (list && convsSelected == list.size());
222 };
223 
224 
225 ZmConvListView.prototype.markUIAsMute =
226 function(item) {
227 	ZmMailListView.prototype.markUIAsMute.apply(this, arguments);
228 };
229 
230 ZmConvListView.prototype.markUIAsRead =
231 function(item) {
232 	ZmMailListView.prototype.markUIAsRead.apply(this, arguments);
233 	if (item.type == ZmItem.MSG) {
234 		var classes = this._getClasses(ZmItem.F_STATUS, !this.isMultiColumn() ? ["ZmMsgListBottomRowIcon"]:null);
235 		this._setImage(item, ZmItem.F_STATUS, item.getStatusIcon(), classes);
236 	}
237 };
238 
239 /**
240  * Overrides DwtListView.getList to optionally include any visible msgs.
241  *
242  * @param {Boolean}	allItems	if <code>true</code>, include visible msgs
243  */
244 ZmConvListView.prototype.getList =
245 function(allItems) {
246 	if (!allItems) {
247 		return ZmMailListView.prototype.getList.call(this);
248 	} else {
249 		var list = [];
250 		var childNodes = this._parentEl.childNodes;
251 		for (var i = 0; i < childNodes.length; i++) {
252 			var el = childNodes[i];
253 			if (Dwt.getVisible(el)) {
254 				var item = this.getItemFromElement(el);
255 				if (item) {
256 					list.push(item);
257 				}
258 			}
259 		}
260 		return AjxVector.fromArray(list);
261 	}
262 };
263 
264 // See if we've been rigged to return a particular msg
265 ZmConvListView.prototype.getSelection =
266 function() {
267 	return this._selectedMsg ? [this._selectedMsg] : ZmMailListView.prototype.getSelection.apply(this, arguments);
268 };
269 
270 ZmConvListView.prototype.getItemIndex =
271 function(item, allItems) {
272 	var list = this.getList(allItems);
273 	if (item && list) {
274 		var len = list.size();
275 		for (var i = 0; i < len; ++i) {
276 			var test = list.get(i);
277 			if (test && test.id == item.id) {
278 				return i;
279 			}
280 		}
281 	}
282 	return null;
283 };
284 
285 ZmConvListView.prototype._initHeaders =
286 function() {
287 	if (!this._headerInit) {
288 		ZmMailListView.prototype._initHeaders.call(this);
289 		this._headerInit[ZmItem.F_EXPAND]	= {icon:"NodeCollapsed", width:ZmListView.COL_WIDTH_ICON, name:ZmMsg.expand, tooltip: ZmMsg.expandCollapse, cssClass:"ZmMsgListColExpand"};
290         //bug:45171 removed sorted from converstaion for FROM field
291         this._headerInit[ZmItem.F_FROM]		= {text:ZmMsg.from, width:ZmMsg.COLUMN_WIDTH_FROM_CLV, resizeable:true, cssClass:"ZmMsgListColFrom"};
292         this._headerInit[ZmItem.F_FOLDER]		= {text:ZmMsg.folder, width:ZmMsg.COLUMN_WIDTH_FOLDER, resizeable:true, cssClass:"ZmMsgListColFolder",visible:false};
293 	}
294 };
295 
296 ZmConvListView.prototype._getLabelFieldList =
297 function() {
298 	var headers = ZmMailListView.prototype._getLabelFieldList.call(this);
299 	var selectionidx = AjxUtil.indexOf(headers, ZmItem.F_SELECTION);
300 
301 	if (selectionidx >= 0) {
302 		headers.splice(selectionidx + 1, 0, ZmItem.F_EXPAND);
303 	}
304 
305 	return headers;
306 }
307 
308 ZmConvListView.prototype._getDivClass =
309 function(base, item, params) {
310 	if (item.type == ZmItem.MSG) {
311 		if (params.isDragProxy || params.isMatched) {
312 			return ZmMailMsgListView.prototype._getDivClass.apply(this, arguments);
313 		} else {
314 			return [base, ZmConvListView.MSG_STYLE].join(" ");
315 		}
316 	} else {
317 		return ZmMailListView.prototype._getDivClass.apply(this, arguments);
318 	}
319 };
320 
321 ZmConvListView.prototype._getRowClass =
322 function(item) {
323 	return (item.type == ZmItem.MSG) ?
324 		ZmMailMsgListView.prototype._getRowClass.apply(this, arguments) :
325 		ZmMailListView.prototype._getRowClass.apply(this, arguments);
326 };
327 
328 // set isMatched for msgs	
329 ZmConvListView.prototype._addParams =
330 function(item, params) {
331 	if (item.type == ZmItem.MSG) {
332 		ZmMailMsgListView.prototype._addParams.apply(this, arguments);
333 	}
334 };
335 
336 
337 ZmConvListView.prototype._getCellId =
338 function(item, field) {
339 	return ((field == ZmItem.F_FROM || field == ZmItem.F_SUBJECT) && item.type == ZmItem.CONV)
340 		? this._getFieldId(item, field)
341 		: ZmMailListView.prototype._getCellId.apply(this, arguments);
342 };
343 
344 ZmConvListView.prototype._getCellClass =
345 function(item, field, params) {
346 	var cls = ZmMailListView.prototype._getCellClass.apply(this, arguments);
347 	return item.type === ZmItem.CONV && field === ZmItem.F_SIZE ? "Count " + cls : cls;
348 };
349 
350 
351 ZmConvListView.prototype._getCellCollapseExpandImage =
352 function(item) {
353 	if (!this._isExpandable(item)) {
354 		return null;
355 	}
356 	return this._expanded[item.id] ? "NodeExpanded" : "NodeCollapsed";
357 };
358 
359 
360 ZmConvListView.prototype._getCellContents =
361 function(htmlArr, idx, item, field, colIdx, params, classes) {
362 
363 	var classes = classes || [];
364 	var zimletStyle = this._getStyleViaZimlet(field, item) || "";
365 	
366 	if (field === ZmItem.F_SELECTION) {
367 		if (this.isMultiColumn()) {
368 			//add the checkbox only for multicolumn layout. The checkbox for single column layout is added in _getAbridgedContent
369 			idx = ZmMailListView.prototype._getCellContents.apply(this, arguments);
370 		}
371 	}
372 	else if (field === ZmItem.F_EXPAND) {
373 		idx = this._getImageHtml(htmlArr, idx, this._getCellCollapseExpandImage(item), this._getFieldId(item, field), classes);
374 	}
375     else if (field === ZmItem.F_READ) {
376 		idx = this._getImageHtml(htmlArr, idx, item.getReadIcon(), this._getFieldId(item, field), classes);
377 	}
378 	else if (item.type === ZmItem.MSG) {
379 		idx = ZmMailMsgListView.prototype._getCellContents.apply(this, arguments);
380 	}
381 	else {
382 		var visibleMsgCount = this._getDisplayedMsgCount(item);
383 		if (field === ZmItem.F_STATUS) {
384 			if (item.type == ZmItem.CONV && item.numMsgs == 1 && item.isScheduled) {
385 				idx = this._getImageHtml(htmlArr, idx, "SendLater", this._getFieldId(item, field), classes);
386 			} else {
387 				htmlArr[idx++] = "<div " + AjxUtil.getClassAttr(classes) + "></div>";
388 			}
389 		}
390 		else if (field === ZmItem.F_FROM) {
391 			htmlArr[idx++] = "<div id='" + this._getFieldId(item, field) + "' " + AjxUtil.getClassAttr(classes) +  zimletStyle + ">";
392 			htmlArr[idx++] = this._getParticipantHtml(item, this._getFieldId(item, ZmItem.F_PARTICIPANT));
393 			if (item.type === ZmItem.CONV && (visibleMsgCount > 1) && !this.isMultiColumn()) {
394 				htmlArr[idx++] = " - <span class='ZmConvListNumMsgs'>";
395 				htmlArr[idx++] = visibleMsgCount;
396 				htmlArr[idx++] = "</span>";
397 			}
398 			htmlArr[idx++] = "</div>";
399 		}
400 		else if (field === ZmItem.F_SUBJECT) {
401 			var subj = item.subject || ZmMsg.noSubject;
402 			if (item.numMsgs > 1) {
403 				subj = ZmMailMsg.stripSubjectPrefixes(subj);
404 			}
405 			htmlArr[idx++] = "<div id='" + this._getFieldId(item, field) + "' " + AjxUtil.getClassAttr(classes) + zimletStyle + ">";
406 			htmlArr[idx++] = "<span>";
407 			htmlArr[idx++] = AjxStringUtil.htmlEncode(subj, true) + "</span>";
408 			if (appCtxt.get(ZmSetting.SHOW_FRAGMENTS) && item.fragment) {
409 				htmlArr[idx++] = this._getFragmentSpan(item);
410 			}
411 			htmlArr[idx++] = "</div>";
412 		}
413 		else if (field === ZmItem.F_FOLDER) {
414 				htmlArr[idx++] = "<div " + AjxUtil.getClassAttr(classes) + " id='";
415 				htmlArr[idx++] = this._getFieldId(item, field);
416 				htmlArr[idx++] = "'>"; // required for IE bug
417 				if (item.folderId) {
418 					var folder = appCtxt.getById(item.folderId);
419 					if (folder) {
420 						htmlArr[idx++] = folder.getName();
421 					}
422 				}
423 				htmlArr[idx++] = "</div>";
424 		}
425 		else if (field === ZmItem.F_SIZE) {
426 			htmlArr[idx++] = "<div id='" + this._getFieldId(item, field) + "' " + AjxUtil.getClassAttr(classes) + ">";
427 			if (item.size) {
428 				htmlArr[idx++] = AjxUtil.formatSize(item.size);
429 			}
430 			else {
431 				htmlArr[idx++] = "(";
432 				htmlArr[idx++] = visibleMsgCount;
433 				htmlArr[idx++] = ")";
434 			}
435 			htmlArr[idx++] = "</div>";
436 		}
437 		else if (field === ZmItem.F_SORTED_BY) {
438 			htmlArr[idx++] = this._getAbridgedContent(item, colIdx);
439 		}
440 		else {
441 			idx = ZmMailListView.prototype._getCellContents.apply(this, arguments);
442 		}
443 	}
444 	
445 	return idx;
446 };
447 
448 ZmConvListView.prototype._getAbridgedContent =
449 function(item, colIdx) {
450 
451 	var htmlArr = [];
452 	var idx = 0;
453 	var width = (AjxEnv.isIE || AjxEnv.isSafari) ? 22 : 16;
454 
455 	var isMsg = (item.type === ZmItem.MSG);
456 	var isConv = (item.type === ZmItem.CONV && this._getDisplayedMsgCount(item) > 1);
457 
458 	var selectionCssClass = '';
459 	for (var i = 0; i < this._headerList.length; i++) {
460 		if (this._headerList[i]._field == ZmItem.F_SELECTION) {
461 			selectionCssClass = "ZmMsgListSelection";
462 			break;
463 		}
464 	}
465 	htmlArr[idx++] = "<div class='TopRow " + selectionCssClass + "' ";
466 	htmlArr[idx++] = "id='";
467 	htmlArr[idx++] = DwtId.getListViewItemId(DwtId.WIDGET_ITEM_FIELD, this._view, item.id, ZmItem.F_ITEM_ROW_3PANE);
468 	htmlArr[idx++] = "'>";
469 	if (selectionCssClass) {
470 		idx = ZmMailListView.prototype._getCellContents.apply(this, [htmlArr, idx, item, ZmItem.F_SELECTION, colIdx]);
471 	}
472 	if (isMsg) {
473 		idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_EXPAND, colIdx);
474 	}
475 	idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_READ, colIdx, width);
476 	if (isConv) {
477 		idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_EXPAND, colIdx, "16", null, ["ZmMsgListExpand"]);
478 	}
479 	
480 	// for multi-account, show the account icon for cross mbox search results
481 	if (appCtxt.multiAccounts && !isMsg && appCtxt.getSearchController().searchAllAccounts) {
482 		idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_ACCOUNT, colIdx, "16");
483 	}
484 	idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_FROM, colIdx);
485 	idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_DATE, colIdx, ZmMsg.COLUMN_WIDTH_DATE, null, ["ZmMsgListDate"]);
486 	htmlArr[idx++] = "</div>";
487 
488 	// second row
489 	htmlArr[idx++] = "<div class='BottomRow " + selectionCssClass + "'>";
490 	var bottomRowMargin = ["ZmMsgListBottomRowIcon"];
491 	if (isMsg) {
492 		idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_STATUS, colIdx, width, null, bottomRowMargin);
493 		bottomRowMargin = null;
494 	}
495 	if (item.isHighPriority || item.isLowPriority) {
496 		idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_PRIORITY, colIdx, "10", null, bottomRowMargin);
497 		bottomRowMargin = null;
498 	}
499 	idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_SUBJECT, colIdx, null, null, bottomRowMargin);
500 
501 	//add the attach, flag and tags in a wrapping div
502 	idx = this._getListFlagsWrapper(htmlArr, idx, item);
503 
504 	if (item.hasAttach) {
505 		idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_ATTACHMENT, colIdx, width);
506 	}
507 	var tags = item.getVisibleTags();
508 	if (tags && tags.length) {
509 		idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_TAG, colIdx, width, null, ["ZmMsgListColTag"]);
510 	}
511 	if (appCtxt.get(ZmSetting.FLAGGING_ENABLED)) {
512 		idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_FLAG, colIdx, width);
513 	}
514 	htmlArr[idx++] = "</div></div>";
515 	
516 	return htmlArr.join("");
517 };
518 
519 ZmConvListView.prototype._getParticipantHtml =
520 function(conv, fieldId) {
521 
522 	var html = [];
523 	var idx = 0;
524 
525 	var part = conv.participants ? conv.participants.getArray() : [],
526 		isOutbound = this._isOutboundFolder(),
527 		part1 = [];
528 
529 	for (var i = 0; i < part.length; i++) {
530 		var p = part[i];
531 		if ((isOutbound && p.type === AjxEmailAddress.TO) || (!isOutbound && p.type === AjxEmailAddress.FROM)) {
532 			part1.push(p);
533 		}
534 	}
535 	// Workaround for bug 87597: for "sent" folder, when no "to" fields were reported after notification,
536 	// push all participants to part1 to trick origLen > 0
537 	// then get recipients from msg.getAddresses below and overwrite part1
538 	if (part1.length === 0 && isOutbound) {
539 		part1 = part;
540 	}
541 	var origLen = part1 ? part1.length : 0;
542 	if (origLen > 0) {
543 
544 		// bug 23832 - create notif for conv in sent gives us sender as participant, we want recip
545 		if (origLen == 1 && (part1[0].type === AjxEmailAddress.FROM) && conv.isZmConv && isOutbound) {
546 			var msg = conv.getFirstHotMsg();
547 			if (msg) {
548 				var addrs = msg.getAddresses(AjxEmailAddress.TO).getArray();
549 	            if (addrs && addrs.length) {
550 					part1 = addrs;
551 				} else {
552 					return " "
553 				}
554 			}
555 		}
556 
557 		var headerCol = this._headerHash[ZmItem.F_FROM];
558 		var partColWidth = headerCol ? headerCol._width : ZmMsg.COLUMN_WIDTH_FROM_CLV;
559 		var part2 = this._fitParticipants(part1, conv, partColWidth);
560 		for (var j = 0; j < part2.length; j++) {
561 			if (j === 0 && (conv.participantsElided || part2.length < origLen)) {
562 				html[idx++] = AjxStringUtil.ELLIPSIS;
563 			}
564 			else if (part2.length > 1 && j > 0) {
565 				html[idx++] = AjxStringUtil.LIST_SEP;
566 			}
567 			var p2 = (part2 && part2[j] && (part2[j].index != null)) ? part2[j].index : "";
568 			var spanId = [fieldId, p2].join(DwtId.SEP);
569 			html[idx++] = "<span id='";
570 			html[idx++] = spanId;
571 			html[idx++] = "'>";
572 			html[idx++] = (part2 && part2[j]) ? AjxStringUtil.htmlEncode(part2[j].name) : "";
573 			html[idx++] = "</span>";
574 		}
575 	} else {
576 		html[idx++] = isOutbound ? " " : ZmMsg.noRecipients;
577 	}
578 
579 	return html.join("");
580 };
581 
582 // Returns the actual number of msgs that will be shown on expansion or in
583 // the reading pane (msgs in Trash/Junk/Drafts are omitted)
584 ZmConvListView.prototype._getDisplayedMsgCount =
585 function(conv) {
586 
587 	var omit = ZmMailApp.getFoldersToOmit(),
588 		num = 0, id;
589 
590 	if (AjxUtil.arraySize(conv.msgFolder) < conv.numMsgs) {
591 		//if msgFolder is empty, or does not include folders for all numMsgs message, for some reason (there are complicated cases like that), assume all messages are displayed.
592 		// This should not cause too big of a problem, as when the user expands, it will load the conv with the correct msgFolder and display only the relevant messages.
593 		return conv.numMsgs;
594 	}
595 	for (id in conv.msgFolder) {
596 		if (!omit[conv.msgFolder[id]]) {
597 			num++;
598 		}
599 	}
600 
601 	return num;
602 };
603 
604 ZmConvListView.prototype._getLabelForField =
605 function(item, field) {
606 	switch (field) {
607 	case ZmItem.F_EXPAND:
608 		if (this._isExpandable(item)) {
609 			return this.isExpanded(item) ? ZmMsg.expanded : ZmMsg.collapsed;
610 		}
611 
612 		break;
613 
614 	case ZmItem.F_SIZE:
615 		if (item.numMsgs > 1) {
616 			var messages =
617 				AjxMessageFormat.format(ZmMsg.typeMessage, item.numMsgs);
618 			return AjxMessageFormat.format(ZmMsg.itemCount,
619 			                               [item.numMsgs, messages]);
620 		}
621 
622 		break;
623 	}
624 
625 	return ZmMailListView.prototype._getLabelForField.apply(this, arguments);
626 };
627 
628 ZmConvListView.prototype._getHeaderToolTip =
629 function(field, itemIdx) {
630 
631 	if (field == ZmItem.F_EXPAND) {
632 		return "";
633 	}
634 	if (field == ZmItem.F_FROM) {
635 		return ZmMsg.from;
636 	}
637 	return ZmMailListView.prototype._getHeaderToolTip.call(this, field, itemIdx);
638 };
639 
640 ZmConvListView.prototype._getToolTip =
641 function(params) {
642 
643 	if (!params.item) { return; }
644 
645 	if (appCtxt.get(ZmSetting.CONTACTS_ENABLED) && (params.field == ZmItem.F_PARTICIPANT)) { 
646 		var parts = params.item.participants;
647 		var matchedPart = params.match && params.match.participant;
648 		var addr = parts && parts.get(matchedPart || 0);
649 		if (!addr) { return ""; }
650 
651 		var ttParams = {address:addr, ev:params.ev};
652 		var ttCallback = new AjxCallback(this,
653 			function(callback) {
654 				appCtxt.getToolTipMgr().getToolTip(ZmToolTipMgr.PERSON, ttParams, callback);
655 			});
656 		return {callback:ttCallback};
657 	} else if (params.item.type == ZmItem.MSG) {
658 		return ZmMailMsgListView.prototype._getToolTip.apply(this, arguments);
659 	} else if (params.field == ZmItem.F_FROM) {
660 		// do nothing - this is white space in the TD not taken up by participants
661 	} else {
662 		return ZmMailListView.prototype._getToolTip.apply(this, arguments);
663 	}
664 };
665 
666 /**
667  * @param {ZmConv}		conv	conv that owns the messages we will display
668  * @param {ZmMailMsg}	msg		msg that is the anchor for paging in more msgs (optional)
669  * @param {boolean}		force	if true, render msg rows		
670  * 
671  * @private
672  */
673 ZmConvListView.prototype._expand =
674 function(conv, msg, force) {
675 	var item = msg || conv;
676 	var isConv = (item.type == ZmItem.CONV);
677 	var rowIds = this._msgRowIdList[item.id];
678 	var lastRow;
679 	if (rowIds && rowIds.length && this._rowsArePresent(item) && !force) {
680 		this._showMsgs(rowIds, true);
681 		lastRow = document.getElementById(rowIds[rowIds.length - 1]);
682 	} else {
683 		this._msgRowIdList[item.id] = [];
684 		var msgList = conv.msgs;
685 		if (!msgList) { return; }
686 		if (isConv) {
687 			// should be here only when the conv is first expanded
688 			msgList.addChangeListener(this._listChangeListener);
689 		}
690 
691 		var ascending = (appCtxt.get(ZmSetting.CONVERSATION_ORDER) == ZmSearch.DATE_ASC);
692 		var index = this._getRowIndex(item);	// row after which to add rows
693 		if (ascending && msg) {
694 			index--;	// for ascending, we want to expand upward (add above expandable msg row)
695 		}
696 		var offset = this._msgOffset[item.id] || 0;
697 		var a = conv.getMsgList(offset, ascending, ZmMailApp.getFoldersToOmit());
698 		for (var i = 0; i < a.length; i++) {
699 			var msg = a[i];
700 			var div = this._createItemHtml(msg);
701 			this._addRow(div, index + i + 1);
702 			rowIds = this._msgRowIdList[item.id];
703 			if (rowIds) {
704 				rowIds.push(div.id);
705 			}
706 			// TODO: we may need to use a group for nested conversations;
707 			// either as proper DOM nesting or with aria-owns.
708 			div.setAttribute('aria-level', 2);
709 			rowIds = this._msgRowIdList[item.id];
710 			if (i == a.length - 1) {
711 				lastRow = div;
712 			}
713 		}
714 	}
715 
716 	this._setImage(item, ZmItem.F_EXPAND, "NodeExpanded", this._getClasses(ZmItem.F_EXPAND));
717 	this._expanded[item.id] = true;
718 	
719 	var cid = isConv ? item.id : item.cid;
720 	if (!this._expandedItems[cid]) {
721 		this._expandedItems[cid] = [];
722 	}
723 	this._expandedItems[cid].push(item);
724 
725 	this._resetColWidth();
726 	if (lastRow) {
727 		this._scrollList(lastRow);
728 		if (rowIds) {
729 			var convHeight = rowIds.length * Dwt.getSize(lastRow).y;
730 			if (convHeight > Dwt.getSize(lastRow.parentNode).y) {
731 				this._scrollList(this._getElFromItem(item));
732 			}
733 		}
734 	}
735 
736 	this._updateLabelForItem(item);
737 };
738 
739 ZmConvListView.prototype._collapse =
740 function(item) {
741 	var isConv = (item.type == ZmItem.CONV);
742 	var cid = isConv ? item.id : item.cid;
743 	var expItems = this._expandedItems[cid];
744 	// also collapse any expanded sections below us within same conv
745 	if (expItems && expItems.length) {
746 		var done = false;
747 		while (!done) {
748 			var nextItem = expItems.pop();
749 			this._doCollapse(nextItem);
750 			done = ((nextItem.id == item.id) || (expItems.length == 0));
751 		}
752 	}
753 
754 	if (isConv) {
755 		this._expanded[item.id] = false;
756 		this._expandedItems[cid] = [];
757 	}
758 
759 	this._resetColWidth();
760 	this._updateLabelForItem(item);
761 };
762 
763 ZmConvListView.prototype._updateLabelForItem =
764 function(item) {
765 	ZmMailListView.prototype._updateLabelForItem.apply(this, arguments);
766 
767 	if (item && this._isExpandable(item)) {
768 		var el = this._getElFromItem(item);
769 		if (el && el.setAttribute) {
770 			el.setAttribute('aria-expanded', this.isExpanded(item));
771 		}
772 	}
773 }
774 
775 ZmConvListView.prototype._doCollapse =
776 function(item) {
777 	var rowIds = this._msgRowIdList[item.id];
778 	if (rowIds && rowIds.length) {
779 		this._showMsgs(rowIds, false);
780 	}
781 	this._setImage(item, ZmItem.F_EXPAND, "NodeCollapsed", this._getClasses(ZmItem.F_EXPAND));
782 	this._expanded[item.id] = false;
783 	this._updateLabelForItem(item);
784 };
785 
786 ZmConvListView.prototype._showMsgs =
787 function(ids, show) {
788 	if (!(ids && ids.length)) { return; }
789 
790 	for (var i = 0; i < ids.length; i++) {
791 		var row = document.getElementById(ids[i]);
792 		if (row) {
793 			Dwt.setVisible(row, show);
794 		}
795 	}
796 };
797 
798 /**
799  * Make sure that the given item has a set of expanded rows. If you expand an item
800  * and then page away and back, the DOM is reset and your rows are gone.
801  * 
802  * @private
803  */
804 ZmConvListView.prototype._rowsArePresent =
805 function(item) {
806 	var rowIds = this._msgRowIdList[item.id];
807 	if (rowIds && rowIds.length) {
808 		for (var i = 0; i < rowIds.length; i++) {
809 			if (document.getElementById(rowIds[i])) {
810 				return true;
811 			}
812 		}
813 	}
814 	this._msgRowIdList[item.id] = [];	// start over
815 	this._expanded[item.id] = false;
816 	if (item.type == ZmItem.CONV) {
817 		this._expandedItems[item.id] = [];
818 	}
819 	else {
820 		AjxUtil.arrayRemove(this._expandedItems[item.cid], item);
821 	}
822 	return false;
823 };
824 
825 /**
826  * Returns true if the given conv or msg should have an expansion icon. A conv is
827  * expandable if it has 2 or more msgs. A msg is expandable if it's the last on a
828  * page and there are more msgs.
829  *
830  * @param item		[ZmMailItem]	conv or msg to check
831  * 
832  * @private
833  */
834 ZmConvListView.prototype._isExpandable =
835 function(item) {
836 	var expandable = false;
837 	if (item.type == ZmItem.CONV) {
838 		expandable = (this._getDisplayedMsgCount(item) > 1);
839 	} else {
840 		var conv = appCtxt.getById(item.cid);
841 		if (!conv) { return false; }
842 		
843 		var a = conv.msgs ? conv.msgs.getArray() : null;
844 		if (a && a.length) {
845 			var limit = appCtxt.get(ZmSetting.CONVERSATION_PAGE_SIZE);
846 			var idx = null;
847 			for (var i = 0; i < a.length; i++) {
848 				if (a[i].id == item.id) {
849 					idx = i + 1;	// start with 1
850 					break;
851 				}
852 			}
853 			if (idx && (idx % limit == 0) && (idx < a.length || conv.msgs._hasMore)) {
854 				this._msgOffset[item.id] = idx;
855 				expandable = true;
856 			}
857 		}
858 	}
859 
860 	return expandable;
861 };
862 
863 ZmConvListView.prototype._resetExpansion =
864 function() {
865 
866 	// remove change listeners on conv msg lists
867 	for (var id in this._expandedItems) {
868 		var item = this._expandedItems[id];
869 		if (item && item.msgs) {
870 			item.msgs.removeChangeListener(this._listChangeListener);
871 		}
872 	}
873 
874 	this._expanded		= {};	// current expansion state, by ID
875 	this._msgRowIdList	= {};	// list of row IDs for a conv ID
876 	this._msgOffset		= {};	// the offset for a msg ID
877 	this._expandedItems	= {};	// list of expanded items for a conv ID (inc conv)
878 };
879 
880 ZmConvListView.prototype.isExpanded =
881 function(item) {
882 	return Boolean(item && this._expanded[item.id]);
883 };
884 
885 ZmConvListView.prototype._expandItem =
886 function(item) {
887 	if (item && this._isExpandable(item)) {
888 		this._controller._toggle(item);
889 	} else if (item.type == ZmItem.MSG && this._expanded[item.cid]) {
890 		var conv = appCtxt.getById(item.cid);
891 		this._controller._toggle(conv);
892 		this.setSelection(conv, true);
893 	}
894 };
895 
896 ZmConvListView.prototype._expandAll = function(expand) {
897 
898     if (!this._list) {
899         return;
900     }
901 
902 	var a = this._list.getArray();
903 	for (var i = 0, count = a.length; i < count; i++) {
904 		var conv = a[i];
905 		if (!this._isExpandable(conv) || expand === this.isExpanded(conv)) {
906             continue;
907         }
908 		if (expand)	{
909             if (conv._loaded) {
910 			    this._expandItem(conv);
911             }
912 		}
913         else if (!expand) {
914 			this._collapse(conv);
915 		}
916 	}
917 };
918 
919 ZmConvListView.prototype._sortColumn =
920 function(columnItem, bSortAsc, callback) {
921 
922 	// call base class to save the new sorting pref
923 	ZmMailListView.prototype._sortColumn.apply(this, arguments);
924 
925 	var query;
926 	var list = this.getList();
927 	if (this._columnHasCustomQuery(columnItem)) {
928 		query = this._getSearchForSort(columnItem._sortable);
929 	}
930 	else if (list && list.size() > 1 && this._sortByString) {
931 		query = this._controller.getSearchString();
932 	}
933 
934 	var queryHint = this._controller.getSearchStringHint();
935 
936 	if (query || queryHint) {
937 		var params = {
938 			query:			query,
939 			queryHint:		queryHint,
940 			types:			[ZmItem.CONV],
941 			sortBy:			this._sortByString,
942 			limit:			this.getLimit(),
943 			callback:		callback,
944 			userInitiated:	this._controller._currentSearch.userInitiated,
945 			sessionId:		this._controller._currentSearch.sessionId,
946 			isViewSwitch:	true
947 		};
948 		appCtxt.getSearchController().search(params);
949 	}
950 };
951 
952 ZmConvListView.prototype._changeListener =
953 function(ev) {
954 
955 	var item = this._getItemFromEvent(ev);
956 	if (!item || ev.handled || !this._handleEventType[item.type]) {
957 		if (ev && ev.event == ZmEvent.E_CREATE) {
958 			AjxDebug.println(AjxDebug.NOTIFY, "ZmConvListView: initial check failed");
959 		}
960 		return;
961 	}
962 
963 	var fields = ev.getDetail("fields");
964 	var isConv = (item.type == ZmItem.CONV);
965     var isMute = item.isMute ? item.isMute : false;
966 	var sortBy = this._sortByString || ZmSearch.DATE_DESC;
967 	var handled = false;
968 	var forceUpdateConvSize = false; //in case of soft delete we don't get notification of size change from server so take care of this case outselves.
969 	var convToUpdate = null; //in case this is a msg but we want to update the size field for a conv - this is the conv to use.
970 	
971 	// msg moved or deleted
972 	if (!isConv && (ev.event == ZmEvent.E_MOVE || ev.event == ZmEvent.E_DELETE)) {
973 		var items = ev.batchMode ? this._getItemsFromBatchEvent(ev) : [item];
974 		for (var i = 0, len = items.length; i < len; i++) {
975 			var item = items[i];
976 			var conv = appCtxt.getById(item.cid);
977 			handled = true;
978 			if (conv) {
979 				if (item.folderId == ZmFolder.ID_SPAM || item.folderId == ZmFolder.ID_TRASH || ev.event == ZmEvent.E_DELETE) {
980 					if (item.folderId == ZmFolder.ID_TRASH) {
981 						//only in this case we don't get size notification from server.
982 						forceUpdateConvSize = true;
983 						convToUpdate = conv;
984 					}
985 					// msg marked as Junk, or hard-deleted
986 					conv.removeMsg(item);
987 					this.removeItem(item, true, ev.batchMode);	// remove msg row
988 					this._controller._app._checkReplenishListView = this;
989 					this._setNextSelection();
990 				} else {
991 					if (!conv.containsMsg(item)) {
992 						//the message was moved to this conv, most likely by "undo". (not sure if any other ways, probably not).
993 						sortIndex = conv.msgs && conv.msgs._getSortIndex(item, ZmSearch.DATE_DESC);
994 						conv.addMsg(item, sortIndex);
995 						forceUpdateConvSize = true;
996 						convToUpdate = conv;
997 						var expanded = this._expanded[conv.id];
998 						//remove rows so will have to redraw them, reflecting the new item.
999 						this._removeMsgRows(conv.id);
1000 						if (expanded) {
1001 							//expand if it was expanded before this undo.
1002 							this._expand(conv, null, true);
1003 						}
1004 					}
1005 					else if (!conv.hasMatchingMsg(this._controller._currentSearch, true)) {
1006 						this._list.remove(conv);				// view has sublist of controller list
1007 						this._controller._list.remove(conv);	// complete list
1008 						ev.item = item = conv;
1009 						isConv = true;
1010 						handled = false;
1011 					} else {
1012 						// normal case: just change folder name for msg
1013 						this._changeFolderName(item, ev.getDetail("oldFolderId"));
1014 					}
1015 				}
1016 			}
1017 		}
1018 	}
1019 
1020 	// conv moved or deleted	
1021 	if (isConv && (ev.event == ZmEvent.E_MOVE || ev.event == ZmEvent.E_DELETE)) {
1022 		var items = ev.batchMode ? this._getItemsFromBatchEvent(ev) : [item];
1023 		for (var i = 0, len = items.length; i < len; i++) {
1024 			var conv = items[i];
1025 			if (this._itemToSelect && (this._itemToSelect.cid == conv.id  //the item to select is in this conv.
1026 										|| this._itemToSelect.id == conv.id)) { //the item to select IS this conv
1027 				var omit = {};
1028 				if (conv.msgs) { //for some reason, msgs might not be set for the conv.
1029 					var a = conv.msgs.getArray();
1030 					for (var j = 0, len1 = a.length; j < len1; j++) {
1031 						omit[a[j].id] = true;
1032 					}
1033 				}
1034 				//omit the conv too, since if we have ZmSetting.DELETE_SELECT_PREV, going up will get back to this conv, but the conv is gone
1035 				omit[conv.id] = true;
1036 				this._itemToSelect = this._controller._getNextItemToSelect(omit);
1037 			}
1038 			this._removeMsgRows(conv.id);	// conv move: remove msg rows
1039 			this._expanded[conv.id] = false;
1040 			this._expandedItems[conv.id] = [];
1041 			delete this._msgRowIdList[conv.id];
1042 		}
1043 	}
1044 
1045 	// if we get a new msg that's part of an expanded conv, insert it into the
1046 	// expanded conv, and don't move that conv
1047 	if (!isConv && (ev.event == ZmEvent.E_CREATE)) {
1048 		AjxDebug.println(AjxDebug.NOTIFY, "ZmConvListView: handle msg create " + item.id);
1049 		var rowIds = this._msgRowIdList[item.cid];
1050 		var conv = appCtxt.getById(item.cid);
1051 		if (rowIds && rowIds.length && this._rowsArePresent(conv)) {
1052 			var div = this._createItemHtml(item);
1053 			if (!this._expanded[item.cid]) {
1054 				Dwt.setVisible(div, false);
1055 			}
1056 			var convIndex = this._getRowIndex(conv);
1057 			var sortIndex = ev.getDetail("sortIndex");
1058 			var msgIndex = sortIndex || 0;
1059 			AjxDebug.println(AjxDebug.NOTIFY, "ZmConvListView: add msg row to conv " + item.id + " within " + conv.id);
1060 			this._addRow(div, convIndex + msgIndex + 1);
1061 			rowIds.push(div.id);
1062 		}
1063 		if (conv) { //see bug 91083 for change prior to this "if" wrapper I add here just in case.
1064 			forceUpdateConvSize = true;
1065 			convToUpdate = conv;
1066 			handled = ev.handled = true;
1067 		}
1068 	}
1069 
1070 	// The sort index we're given is relative to a list of convs. We want one relative to a list view which may
1071 	// have some msg rows from expanded convs in there.
1072 	if (isConv && (ev.event == ZmEvent.E_CREATE)) {
1073 		ev.setDetail("sortIndex", this._getSortIndex(item, sortBy));
1074 	}
1075 	
1076 	// virtual conv promoted to real conv, got new ID
1077 	if (isConv && (ev.event == ZmEvent.E_MODIFY) && (fields && fields[ZmItem.F_ID])) {
1078 		// a virtual conv has become real, and changed its ID
1079 		var div = document.getElementById(this._getItemId({id:item._oldId}));
1080 		if (div) {
1081 			this._createItemHtml(item, {div:div});
1082 			this.associateItemWithElement(item, div);
1083 			DBG.println(AjxDebug.DBG1, "conv updated from ID " + item._oldId + " to ID " + item.id);
1084 		}
1085 		this._expanded[item.id] = this._expanded[item._oldId];
1086 		this._expandedItems[item.id] = this._expandedItems[item._oldId];
1087 		this._msgRowIdList[item.id] = this._msgRowIdList[item._oldId] || [];
1088 	}
1089 
1090 	// when adding a conv (or changing its position within the list), we need to look at its sort order
1091 	// within the list of rows (which may include msg rows) rather than in the ZmList of convs, since
1092 	// those two don't necessarily map to each other
1093 	if (isConv && ((ev.event == ZmEvent.E_MODIFY) && (fields && fields[ZmItem.F_INDEX]))) {
1094 		// INDEX change: a conv has gotten a new msg and may need to be moved within the list of convs
1095 		// if an expanded conv gets a new msg, don't move it to top
1096 		AjxDebug.println(AjxDebug.NOTIFY, "ZmConvListView: handle conv create " + item.id);
1097 		var sortIndex = this._getSortIndex(item, sortBy);
1098 		var curIndex = this.getItemIndex(item, true);
1099 
1100 		if ((sortIndex != null) && (curIndex != null) && (sortIndex != curIndex) &&	!this._expanded[item.id]) {
1101             AjxDebug.println(AjxDebug.NOTIFY, "ZmConvListView: change position of conv " + item.id + " to " + sortIndex);
1102             this._removeMsgRows(item.id);
1103             this.removeItem(item);
1104             this.addItem(item, sortIndex);
1105             // TODO: mark create notif handled?
1106 		}
1107 	}
1108 
1109 	// only a conv can change its fragment
1110 	if ((ev.event == ZmEvent.E_MODIFY || ev.event == ZmEvent.E_MOVE) && (fields && fields[ZmItem.F_FRAGMENT])) {
1111 		this._updateField(isConv ? item : appCtxt.getById(item.cid), ZmItem.F_SUBJECT);
1112 	}
1113 
1114 	if (ev.event == ZmEvent.E_MODIFY && (fields && (fields[ZmItem.F_PARTICIPANT] || fields[ZmItem.F_FROM] ||
1115 													(fields[ZmItem.F_SIZE] && !this.isMultiColumn())))) {
1116 		this._updateField(item, ZmItem.F_FROM);
1117 	}
1118 
1119 	// remember if a conv's unread state changed since it affects how the conv is loaded when displayed
1120 	if (ev.event == ZmEvent.E_FLAGS) {
1121 		var flags = ev.getDetail("flags");
1122 		if (AjxUtil.isArray(flags) && AjxUtil.indexOf(flags, ZmItem.FLAG_UNREAD) != -1) {
1123 			item = item || (items && items[i]);
1124 			var conv = isConv ? item : item && appCtxt.getById(item.cid);
1125 			if (conv) {
1126 				conv.unreadHasChanged = true;
1127 			}
1128 		}
1129 	}
1130 
1131 	// msg count in a conv changed - see if we need to add or remove an expand icon
1132 	if (forceUpdateConvSize || (isConv && (ev.event === ZmEvent.E_MODIFY && fields && fields[ZmItem.F_SIZE]))) {
1133 		conv = convToUpdate || item;
1134 		var numDispMsgs = this._getDisplayedMsgCount(conv);
1135 		//redraw the item when redraw is requested or when the new msg count is set to 1(msg deleted) or 2(msg added)
1136 		//redrawConvRow is from bug 75301 - not sure this case is still needed after my fix but keeping it to be safe for now.
1137 		if (conv.redrawConvRow || numDispMsgs === 1 || numDispMsgs === 2) {
1138 			if (numDispMsgs === 1) {
1139 				this._collapse(conv); //collapse since it's only one message.
1140 			}
1141 			//must redraw the line since the ZmItem.F_EXPAND field might not be there when switching from 1 message conv, so updateField does not work. And also we
1142 			//don't want it after deleting message(s) resulting in 1.
1143 			this.redrawItem(conv);
1144 		}
1145 		this._updateField(conv, this.isMultiColumn() ? ZmItem.F_SIZE : ZmItem.F_FROM); //in reading pane on the right, the count appears in the "from".
1146 	}
1147 
1148 	if (ev.event == ZmEvent.E_MODIFY && (fields && fields[ZmItem.F_DATE])) {
1149 		this._updateField(item, ZmItem.F_DATE);
1150 	}
1151 
1152 	if (!handled) {
1153 		if (isConv) {
1154 			if (ev.event == ZmEvent.E_MODIFY && item.msgs) {
1155 				//bug 79256 - in some cases the listeners gets removed when Conv is moved around.
1156 				//so add the listeners again. If they are already present than this will be a no-op.
1157 				var cv = this.getController()._convView;
1158 				if (cv) {
1159 					item.msgs.addChangeListener(cv._listChangeListener);
1160 				}
1161 				item.msgs.addChangeListener(this._listChangeListener);
1162 			}
1163 			ZmMailListView.prototype._changeListener.apply(this, arguments);
1164 		} else {
1165 			ZmMailMsgListView.prototype._changeListener.apply(this, arguments);
1166 		}
1167 	}
1168 };
1169 
1170 ZmConvListView.prototype.handleUnmuteConv =
1171 function(items) {
1172     for(var i=0; i<items.length; i++) {
1173         var item = items[i];
1174         var isConv = (item.type == ZmItem.CONV);
1175         if (!isConv) { continue; }
1176         var sortBy = this._sortByString || ZmSearch.DATE_DESC;
1177         var sortIndex = this._getSortIndex(item, sortBy);
1178         var curIndex = this.getItemIndex(item, true);
1179 
1180         if ((sortIndex != null) && (curIndex != null) && (sortIndex != curIndex) &&	!this._expanded[item.id]) {
1181             AjxDebug.println(AjxDebug.NOTIFY, "ZmConvListView: change position of conv " + item.id + " to " + sortIndex);
1182             this._removeMsgRows(item.id);
1183             this.removeItem(item);
1184             this.addItem(item, sortIndex);
1185         }
1186     }
1187 };
1188 
1189 ZmConvListView.prototype._getSortIndex =
1190 function(conv, sortBy) {
1191 
1192 	var itemDate = parseInt(conv.date);
1193 	var list = this.getList(true);
1194 	var a = list && list.getArray();
1195 	if (a && a.length) {
1196 		for (var i = 0; i < a.length; i++) {
1197 			var item = a[i];
1198 			if (!item || (item && item.type == ZmItem.MSG)) { continue; }
1199 			var date = parseInt(item.date);
1200 			if ((sortBy && sortBy.toLowerCase() === ZmSearch.DATE_DESC.toLowerCase() && (itemDate >= date)) ||
1201 				(sortBy && sortBy.toLowerCase() === ZmSearch.DATE_ASC.toLowerCase() && (itemDate <= date))) {
1202 				return i;
1203 			}
1204 		}
1205 		return i;
1206 	}
1207 	else {
1208 		return null;
1209 	}
1210 };
1211 
1212 ZmConvListView.prototype._removeMsgRows =
1213 function(convId) {
1214 	var msgRows = this._msgRowIdList[convId];
1215 	if (msgRows && msgRows.length) {
1216 		for (var i = 0; i < msgRows.length; i++) {
1217 			var row = document.getElementById(msgRows[i]);
1218 			if (row) {
1219 				this._selectedItems.remove(row);
1220 				this._parentEl.removeChild(row);
1221 			}
1222 		}
1223 	}
1224 };
1225 
1226 /**
1227  * Override so we can clean up lists of cached rows.
1228  */
1229 ZmConvListView.prototype.removeItem =
1230 function(item, skipNotify) {
1231 	if (item.type == ZmItem.MSG) {
1232 		AjxUtil.arrayRemove(this._msgRowIdList[item.cid], this._getItemId(item));
1233 	}
1234 	DwtListView.prototype.removeItem.apply(this, arguments);
1235 };
1236 
1237 ZmConvListView.prototype._allowFieldSelection =
1238 function(id, field) {
1239 	// allow left selection if clicking on blank icon
1240 	if (field == ZmItem.F_EXPAND) {
1241 		var item = appCtxt.getById(id);
1242 		return (item && !this._isExpandable(item));
1243 	} else {
1244 		return ZmListView.prototype._allowFieldSelection.apply(this, arguments);
1245 	}
1246 };
1247 
1248 ZmConvListView.prototype.redoExpansion =
1249 function() {
1250 	var list = [];
1251 	var offsets = {};
1252 	for (var cid in this._expandedItems) {
1253 		var items = this._expandedItems[cid];
1254 		if (items && items.length) {
1255 			for (var i = 0; i < items.length; i++) {
1256 				var id = items[i];
1257 				list.push(id);
1258 				offsets[id] = this._msgOffset[id];
1259 			}
1260 		}
1261 	}
1262 	this._expandAll(false);
1263 	this._resetExpansion();
1264 	for (var i = 0; i < list.length; i++) {
1265 		var id = list[i];
1266 		this._expand(id, offsets[id]);
1267 	}
1268 };
1269 
1270 ZmConvListView.prototype._getLastItem =
1271 function() {
1272 	var list = this.getList();
1273 	var a = list && list.getArray();
1274 	if (a && a.length > 1) {
1275 		return a[a.length - 1];
1276 	}
1277 	return null;
1278 };
1279 
1280 ZmConvListView.prototype._getActionMenuForColHeader =
1281 function(force) {
1282 
1283 	var menu = ZmMailListView.prototype._getActionMenuForColHeader.apply(this, arguments);
1284 	if (!this.isMultiColumn()) {
1285 		var mi = this._colHeaderActionMenu.getMenuItem(ZmItem.F_FROM);
1286 		if (mi) {
1287 			mi.setVisible(false);
1288 		}
1289 		mi = this._colHeaderActionMenu.getMenuItem(ZmItem.F_TO);
1290 		if (mi) {
1291 			mi.setVisible(false);
1292 		}
1293 	}
1294 	return menu;
1295 };
1296 
1297 /**
1298  * @private
1299  * @param {hash}		params			hash of parameters:
1300  * @param {boolean}		expansion		if true, preserve expansion
1301  */
1302 ZmConvListView.prototype._saveState =
1303 function(params) {
1304 	ZmMailListView.prototype._saveState.apply(this, arguments);
1305 	this._state.expanded = params && params.expansion && this._expanded;
1306 };
1307 
1308 ZmConvListView.prototype._restoreState =
1309 function(state) {
1310 
1311 	var s = state || this._state;
1312 	if (s.expanded) {
1313 		for (var id in s.expanded) {
1314 			if (s.expanded[id]) {
1315 				this._expandItem(s.expanded[id]);
1316 			}
1317 		}
1318 	}
1319 	ZmMailListView.prototype._restoreState.call(this);
1320 };
1321