1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a view that will later display one conversation at a time.
 26  * @constructor
 27  * @class
 28  * This class displays and manages a conversation.
 29  *
 30  * @author Conrad Damon
 31  * 
 32  * @param {string}						id				ID for HTML element
 33  * @param {ZmConvListController}		controller		containing controller
 34  * 
 35  * @extends		ZmMailItemView
 36  */
 37 ZmConvView2 = function(params) {
 38 
 39 	params.className = params.className || "ZmConvView2";
 40 	ZmMailItemView.call(this, params);
 41 
 42 	this._mode = params.mode;
 43 	this._controller = params.controller;
 44 	this._convChangeHandler = this._convChangeListener.bind(this);
 45 	this._listChangeListener = this._msgListChangeListener.bind(this);
 46 	this._standalone = params.standalone;
 47 	this._hasBeenExpanded = {};	// track which msgs have been expanded at least once
 48 	this.inviteMsgsExpanded = 0; //track how many invite messages have been expanded.
 49 
 50 	this.addControlListener(this._scheduleResize.bind(this));
 51 	this._setAllowSelection();
 52 	this._setEventHdlrs([DwtEvent.ONMOUSEOUT, DwtEvent.ONMOUSEOVER, DwtEvent.ONMOUSEENTER, DwtEvent.ONMOUSELEAVE]); // needed by object manager
 53 	this._objectManager = true;
 54 };
 55 
 56 ZmConvView2.prototype = new ZmMailItemView;
 57 ZmConvView2.prototype.constructor = ZmConvView2;
 58 
 59 ZmConvView2.prototype.isZmConvView2 = true;
 60 ZmConvView2.prototype.toString = function() { return "ZmConvView2"; };
 61 ZmConvView2.MAX_INVITE_MSG_EXPANDED = 10;
 62 
 63 ZmConvView2.prototype.role = 'region';
 64 
 65 /**
 66  * Displays the given conversation.
 67  * 
 68  * @param {ZmConv}		conv		the conversation to display
 69  */
 70 ZmConvView2.prototype.set = function(conv) {
 71 
 72     if (conv && this._item && conv.id === this._item.id && !this._convDirty) {
 73         return;
 74     }
 75 
 76 	var gotConv = (conv != null);
 77 	this.reset(gotConv);
 78 	this._item = conv;
 79 
 80 	this._cleared = this.noTab = !gotConv;
 81 	if (gotConv) {
 82 		this._initialize();
 83 		conv.addChangeListener(this._convChangeHandler);
 84 	
 85 		this._renderConv(conv);
 86 		if (conv.msgs) {
 87 			conv.msgs.addChangeListener(this._listChangeListener);
 88 			var clv = this._controller.getListView();
 89 			if (clv && clv.isZmConvListView) {
 90 				conv.msgs.addChangeListener(clv._listChangeListener);
 91 				if (clv.isExpanded(conv)) {
 92 					// bug 74730 - rerender expanded conv's msg rows
 93 					clv._removeMsgRows(conv.id);
 94 					clv._expand(conv, null, true);
 95 				}
 96 			}
 97 		}
 98 	}
 99 	else {
100 		this._initializeClear();
101 		this._clearDiv.innerHTML = (this._controller.getList().size()) ? this._viewConvHtml : "";
102 	}
103 
104     Dwt.setVisible(this._mainDiv, gotConv);
105     Dwt.setVisible(this._clearDiv, !gotConv);
106 };
107 
108 ZmConvView2.prototype._initialize =
109 function() {
110 
111 	if (this._initialized) { return; }
112 	
113 	// Create HTML structure
114 	this._mainDivId			= this._htmlElId + "_main";
115 	var headerDivId			= this._htmlElId + "_header";
116 	this._messagesDivId		= this._htmlElId + "_messages";
117 	
118 	var subs = {
119 		mainDivId:			this._mainDivId,
120 		headerDivId:		headerDivId,
121 		messagesDivId:		this._messagesDivId
122 	}
123 
124 	var html = AjxTemplate.expand("mail.Message#Conv2View", subs);
125 	this.getHtmlElement().appendChild(Dwt.parseHtmlFragment(html));
126 	
127 	this._mainDiv			= document.getElementById(this._mainDivId);
128 	this._messagesDiv		= document.getElementById(this._messagesDivId);
129 	
130 	this._header = new ZmConvView2Header({
131 		parent: this,
132 		id:		[this._htmlElId, ZmId.MV_MSG_HEADER].join("_")
133 	});
134 	this._header.replaceElement(headerDivId);
135 
136 	 // label our control after the subject element
137 	this.setAttribute('aria-labelledby', this._header._convSubjectId);
138 
139 	if (this._controller && this._controller._checkKeepReading) {
140 		Dwt.setHandler(this._messagesDiv, DwtEvent.ONSCROLL, ZmDoublePaneController.handleScroll);
141 	}
142 
143 	this._initialized = true;
144 };
145 
146 ZmConvView2.prototype._initializeClear =
147 function() {
148 
149 	if (this._initializedClear) { return; }
150 	
151 	this._viewConvHtml = AjxTemplate.expand("mail.Message#viewMessage", {isConv:true});
152 	var div = this._clearDiv = document.createElement("div");
153 	div.id = this._htmlElId + "_clear";
154 	this.getHtmlElement().appendChild(div);
155 	
156 	this._initializedClear = true;
157 };
158 
159 ZmConvView2.prototype._renderConv =
160 function(conv) {
161 
162 	this._now = new Date();
163 	this._header.set(this._item);
164 	var firstExpanded = this._renderMessages(conv, this._messagesDiv);
165 	DBG.println("cv2", "Conv render time: " + ((new Date()).getTime() - this._now.getTime()));
166 
167 	this._header._setExpandIcon();
168 	this._scheduleResize(firstExpanded || true);
169 	this.inviteMsgsExpanded = 0; //reset the inviteMsgExpanded count.
170 	Dwt.setLoadedTime("ZmConv");
171     this._convDirty = false;
172 };
173 
174 // Only invoked by doing a saveDraft, from editing a reply in an individual Conversation display
175 ZmConvView2.prototype.redrawItem = function(item) {
176 	this._renderConv(this._item);
177 }
178 ZmConvView2.prototype.setSelection = function(item, skipNotify, forceSelection) { }
179 
180 /**
181  * Renders this conversation's messages. Each message may be expanded (shows header, body, and footer)
182  * or collapsed (shows just the header).
183  * 
184  * So far the messages are not contained within a ZmListView. Instead, they rely on the controller for
185  * the parent CLV to handle actions.
186  * 
187  * @param conv
188  * @param container
189  */
190 ZmConvView2.prototype._renderMessages =
191 function(conv, container) {
192 
193 	// clear messages from tabgroup; we'll re-add them later
194 	this.getTabGroupMember().removeAllMembers();
195 
196 	this.getTabGroupMember().addMember(this._header);
197 
198 	this._msgViews = {};
199 	this._msgViewList = [];
200 	var msgs = conv.getMsgList(0, false, ZmMailApp.getFoldersToOmit());
201 	
202 	// base the ordering off a list of msg IDs
203 	var idList = [], idHash = {};
204 	for (var i = 0, len = msgs.length; i < len; i++) {
205 		idList.push(msgs[i].id);
206 		idHash[msgs[i].id] = msgs[i];
207 	}
208 	
209 	// figure out which msg views should be expanded; if the msg is loaded and we're viewing it
210 	// for the first time, it was unread so we expand it; expand the first if there are none to expand
211 	var toExpand = {}, toCollapse = {};
212 	// check if conv was opened by selecting "Show Conversation" for a msg
213 	var launchMsgId = this._controller._relatedMsg && this._controller._relatedMsg.id;
214 	var gotOne = false;
215 	for (var i = 0, len = idList.length; i < len; i++) {
216 		var id = idList[i];
217 		var msg = idHash[id];
218 		if (launchMsgId) {
219 			toExpand[id] = (id == launchMsgId);
220 			toCollapse[id] = (id != launchMsgId);
221 		}
222 		else if (msg && msg.isLoaded() && !this._hasBeenExpanded[id]) {
223 			toExpand[id] = gotOne = true;
224 		}
225 	}
226 	if (!gotOne && !launchMsgId) {
227 		toExpand[idList[0]] = true;
228 	}
229 	
230 	// flip the list for display based on user pref
231 	var oldToNew = (appCtxt.get(ZmSetting.CONVERSATION_ORDER) == ZmSearch.DATE_ASC);
232 	if (oldToNew) {
233 		idList.reverse();
234 	}
235 
236 	var idx;
237 	var oldestIndex = oldToNew ? 0 : msgs.length - 1;
238 	for (var i = 0, len = idList.length; i < len; i++) {
239 		var id = idList[i];
240 		var msg = idHash[id];
241 		var params = {
242 			parent:			this,
243 			parentElement:	container,
244 			controller:		this._controller,
245 			index:          i
246 		}
247 		params.forceExpand = toExpand[id];
248 		params.forceCollapse = toCollapse[id];
249 		// don't look for quoted text in oldest msg - it is considered wholly original
250 		params.forceOriginal = (i == oldestIndex);
251 		this._renderMessage(msg, params);
252 		var msgView = this._msgViews[id];
253 		if (idx == null) {
254 			idx = msgView._expanded ? i : null;
255 		}
256 	}
257 	
258 	return idx && this._msgViews[this._msgViewList[idx]];
259 };
260 
261 ZmConvView2.prototype._renderMessage =
262 function(msg, params) {
263 	
264 	params = AjxUtil.hashCopy(params) || {};
265 	params.mode = this._mode;
266 	params.msgId = msg.id;
267 	params.sessionId = this._controller.getSessionId();
268 	params.isDraft = msg.isDraft;
269 
270 	var container = params.parentElement;
271 	if (container) {
272 		// wrap the message element in a DIV with role listitem
273 		var listitem = params.parentElement = document.createElement('DIV');
274 		listitem.setAttribute('role', 'listitem');
275 		if ((params.index != null) && container.childNodes[params.index]) {
276 			container.insertBefore(listitem, container.childNodes[params.index]);
277 		}
278 		else {
279 			container.appendChild(listitem);
280 		}
281 
282 		// this method is called when iterating over messages; hence,
283 		// the current index is the number of messages processed
284 		var msgCount = this._item.msgs ? this._item.msgs.size() : 0;
285 		var msgIdx = (params.index != null) ? params.index + 1 : container.childNodes.length;
286 		var messages = AjxMessageFormat.format(ZmMsg.typeMessage, [msgCount]);
287 		var label = AjxMessageFormat.format(ZmMsg.itemCount1, [msgIdx, msgCount, messages]);
288 
289 		listitem.setAttribute('aria-label', label);
290 
291 		// TODO: hidden header support
292 		/* listitem.appendChild(util.createHiddenHeader(label, 2)); */
293 	}
294 
295 	AjxUtil.arrayAdd(this._msgViewList, msg.id, params.index);
296 	var msgView = this._msgViews[msg.id] = new ZmMailMsgCapsuleView(params);
297 
298 	// add to tabgroup
299 	this.getTabGroupMember().addMember(msgView.getTabGroupMember());
300 	msgView.set(msg);
301 };
302 
303 ZmConvView2.prototype.clearChangeListeners =
304 function() {
305 
306 	if (!this._item) {
307 		return;
308 	}
309 	this._item.removeChangeListener(this._convChangeHandler);
310 	if (this._item.msgs) {
311 		this._item.msgs.removeChangeListener(this._listChangeListener);
312 	}
313 	this._item = null;
314 };
315 
316 ZmConvView2.prototype.reset =
317 function(noClear) {
318 	
319 	this._setSelectedMsg(null);
320 	this.clearChangeListeners();
321 
322 	for (var id in this._msgViews) {
323 		var msgView = this._msgViews[id];
324 		msgView.reset();
325 		msgView.dispose();
326 		msgView = null;
327 		delete this._msgViews[id];
328 	}
329 	this._msgViewList = null;
330 	this._currentMsgView = null;
331 
332 	// remove the listitem wrappers around the msg views (see _renderMessage)
333 	var msgsDiv = this._messagesDiv;
334 	while (msgsDiv && msgsDiv.lastChild) {
335 		msgsDiv.removeChild(msgsDiv.lastChild);
336 	}
337 
338 	if (this._initialized) {
339 		this._header.reset();
340 		Dwt.setVisible(this._headerDiv, noClear);
341 	}
342 	
343 	if (this._replyView) {
344 		this._replyView.reset();
345 	}
346 };
347 
348 ZmConvView2.prototype.dispose =
349 function() {
350 	this.clearChangeListeners();
351 	ZmMailItemView.prototype.dispose.apply(this, arguments);
352 };
353 
354 ZmConvView2.prototype._removeMessageView =
355 function(msgId) {
356 	AjxUtil.arrayRemove(this._msgViewList, msgId);
357 	this._msgViews[msgId] = null;
358 	delete this._msgViews[msgId];
359 };
360 
361 ZmConvView2.prototype._resize =
362 function(scrollMsgView) {
363 
364 	this._resizePending = false;
365 	if (this.isDisposed()) { return; }
366 
367 	if (this._cleared) { return; }
368 	if (!this._messagesDiv) { return; }
369 	
370 	var ctlr = this._controller, container;
371 	if (this._isStandalone()) {
372 		container = this;
373 	}
374 	else {
375 		// height of list view more reliable for reading pane on right
376 		var rpRight = ctlr.isReadingPaneOnRight();
377 		container = rpRight ? ctlr.getListView() : ctlr.getItemView();
378 	}
379 	var header = this._header;
380 	if (!container || !header || !this._messagesDiv) { return; }
381 	
382 	var mySize = container.getSize(AjxEnv.isIE);
383 	var scrollbarSizes = Dwt.getScrollbarSizes(this.getHtmlElement());
384 	var myHeight = mySize ? mySize.y : 0;
385 	var headerSize = header.getSize();
386 	var headerHeight = headerSize ? headerSize.y : 0;
387 	var messagesHeight = myHeight - headerHeight - 1 - scrollbarSizes.y;
388 	var messagesWidth = this.getSize().x - scrollbarSizes.x;
389 	Dwt.setSize(this._messagesDiv, messagesWidth, messagesHeight);
390 
391 	// widen msg views if needed
392 	if (this._msgViewList && this._msgViewList.length) {
393 		for (var i = 0; i < this._msgViewList.length; i++) {
394 			var msgView = this._msgViews[this._msgViewList[i]];
395 			if (msgView) {
396 				ZmMailMsgView._resetIframeHeight(msgView);
397 
398 				var iframe = msgView._usingIframe && msgView.getIframe();
399 				var width = iframe ? Dwt.getOuterSize(iframe).x : msgView._contentWidth;
400 				width += msgView.getInsets().left + msgView.getInsets().right;
401 				if (width && width > Dwt.getSize(this._messagesDiv).x) {
402 					msgView.setSize(width, Dwt.DEFAULT);
403 				}
404 				if (msgView._isCalendarInvite && msgView._inviteMsgView) {
405 				    msgView._inviteMsgView.convResize();
406 				    msgView._inviteMsgView.scrollToInvite();
407 
408 				}
409 			}
410 		}
411 	}
412 	window.setTimeout(this._resizeMessages.bind(this, scrollMsgView), 0);
413 };
414 
415 ZmConvView2.prototype._resizeMessages =
416 function(scrollMsgView) {
417 	
418 	if (this._msgViewList) {
419 		for (var i = 0; i < this._msgViewList.length; i++) {
420 			this._msgViews[this._msgViewList[i]]._resetIframeHeightOnTimer();
421 		}
422 	}
423 
424 	// see if we need to scroll to top or a particular msg view
425 	if (scrollMsgView) {
426 		if (scrollMsgView === true) {
427 			this._messagesDiv.scrollTop = 0;
428 		}
429 		else if (scrollMsgView.isZmMailMsgCapsuleView) {
430 			this._scrollToTop(scrollMsgView);
431 		}
432 	}
433 };
434 
435 ZmConvView2.prototype._scrollToTop =
436 function(msgView) {
437 	var msgViewTop = Dwt.toWindow(msgView.getHtmlElement(), 0, 0, null, null, DwtPoint.tmp).y;
438 	var containerTop = Dwt.toWindow(this._messagesDiv, 0, 0, null, null, DwtPoint.tmp).y;
439 	var diff = msgViewTop - containerTop;
440 	this._messagesDiv.scrollTop = (diff > 0) ? diff : 0;
441 	this._currentMsgView = msgView;
442 };
443 
444 // since we may get multiple calls to _resize
445 ZmConvView2.prototype._scheduleResize =
446 function(scrollMsgView) {
447 	if (!this._resizePending) {
448 		window.setTimeout(this._resize.bind(this, scrollMsgView), 100);
449 		this._resizePending = true;
450 	}
451 };
452 
453 ZmConvView2.prototype.getTabGroupMember = function() {
454 	if (!this._tabGroupMember) {
455 		this._tabGroupMember = new DwtTabGroup(this.toString());
456 	}
457 	return this._tabGroupMember;
458 };
459 
460 // re-render if reading pane moved between right and bottom
461 ZmConvView2.prototype.setReadingPane =
462 function() {
463 	var rpLoc = this._controller._getReadingPanePref();
464 	if (this._rpLoc && this._item) {
465 		if (this._rpLoc != ZmSetting.RP_OFF && rpLoc != ZmSetting.RP_OFF && this._rpLoc != rpLoc) {
466 			this.set(this._item);
467 		}
468 	}
469 	this._rpLoc = rpLoc;
470 };
471 
472 /**
473  * Returns a list of IDs for msg views whose expanded state matches the given one.
474  * 
475  * @param {boolean}		expanded		if true, look for expanded msg views
476  */
477 ZmConvView2.prototype.getExpanded =
478 function(expanded) {
479 
480 	var list = [];
481 	if (this._msgViewList && this._msgViewList.length) {
482 		for (var i = 0; i < this._msgViewList.length; i++) {
483 			var id = this._msgViewList[i];
484 			var msgView = this._msgViews[id];
485 			if (msgView.isExpanded() == expanded) {
486 				list.push(id);
487 			}
488 		}
489 	}
490 	return list;
491 };
492 
493 /**
494  * Returns a list of IDs for msg views whose msg's loaded state matches the given one.
495  * 
496  * @param {boolean}		loaded		if true, look for msg views whose msg has been loaded
497  */
498 ZmConvView2.prototype.getLoaded =
499 function(loaded) {
500 
501 	var list = [];
502 	if (this._msgViewList && this._msgViewList.length) {
503 		for (var i = 0; i < this._msgViewList.length; i++) {
504 			var id = this._msgViewList[i];
505 			var msg = this._msgViews[id] && this._msgViews[id]._msg;
506 			if (msg && (msg.isLoaded() == loaded)) {
507 				list.push(id);
508 			}
509 		}
510 	}
511 	return list;
512 };
513 
514 /**
515  * Expands or collapses the conv view as a whole by expanding or collapsing each of its message views. If
516  * at least one message view is collapsed, then expansion is done.
517  * 
518  * @param {boolean}		expanded		if true, expand message views; otherwise, collapse them
519  * @param {boolean}		force			if true, do not check for unsent quick reply content
520  */
521 ZmConvView2.prototype.setExpanded =
522 function(expanded, force) {
523 	
524 	var list = this.getExpanded(!expanded);
525 	if (list.length && !expanded) {
526 		if (!force && !this._controller.popShield(null, this.setExpanded.bind(this, expanded, true))) {
527 			return;
528 		}
529 		for (var i = 0; i < this._msgViewList.length; i++) {
530 			var msgView = this._msgViews[this._msgViewList[i]];
531 			msgView._setExpansion(false);
532 		}
533 		this._header._setExpandIcon();
534 	}
535 	else if (list.length && expanded) {
536 		var unloaded = this.getLoaded(false);
537 		if (unloaded.length) {
538 			var respCallback = this._handleResponseSetExpanded.bind(this, list);
539 			this._item.loadMsgs({fetchAll:true}, respCallback);
540 		}
541 		else {
542 			// no need to load the msgs if we already have them all
543 			this._handleResponseSetExpanded(list);
544 		}
545 	}
546 };
547 
548 ZmConvView2.prototype._handleResponseSetExpanded =
549 function(ids) {
550 	for (var i = 0; i < ids.length; i++) {
551 		var id = ids[i];
552 		var msgView = this._msgViews[id];
553 		// the msgs that were fetched by GetConvRequest have more info than the ones we got
554 		// from SearchConvRequest, so update our cached versions
555 		var newMsg = appCtxt.getById(id);
556 		if (newMsg) {
557 			msgView._msg = newMsg;
558 			if (msgView._header) {
559 				msgView._header._msg = newMsg;
560 			}
561 		}
562 		msgView._setExpansion(true);
563 	}
564 	this._header._setExpandIcon();
565 };
566 
567 ZmConvView2.prototype.isDirty =
568 function() {
569 	return (this._replyView && (this._replyView.getValue() != ""));
570 };
571 
572 // Scrolls to show the user something new. If the current msg view isn't completely visible,
573 // scroll to show the next page. Otherwise, scroll the next expanded msg view to the top.
574 // Returns true if scrolling was done.
575 ZmConvView2.prototype._keepReading =
576 function(check) {
577 
578 	if (!(this._msgViewList && this._msgViewList.length)) { return false; }
579 	
580 	var firstMsgView = this._msgViews[this._msgViewList[0]];
581 	if (!firstMsgView) { return false; }
582 	var startMsgView = this._currentMsgView || firstMsgView;
583 	var el = startMsgView.getHtmlElement();
584 
585 	// offsetTop is supposed to be relative to parent, but msgView seems to be relative to conv view rather than
586 	// messages container, so we figure out an adjustment that also includes margin.
587 	if (!this._offsetAdjustment) {
588 		var firstEl = firstMsgView.getHtmlElement();
589 		this._offsetAdjustment = firstEl.offsetTop - parseInt(DwtCssStyle.getComputedStyleObject(firstEl).marginTop);
590 	}
591 	
592 	var cont = this._messagesDiv;
593 	var contHeight = Dwt.getSize(cont).y;
594 	var canScroll = (cont.scrollHeight > contHeight && (cont.scrollTop + contHeight < cont.scrollHeight));
595 
596 	// first, see if the current msg view could be scrolled
597 	if (el && canScroll) {
598 		// if bottom of current msg view is not visible, scroll down a page
599 		var elHeight = Dwt.getSize(el).y;
600 		// is bottom of msg view below bottom of container?
601 		if (((el.offsetTop - this._offsetAdjustment) + elHeight) > (cont.scrollTop + contHeight)) {
602 			if (!check) {
603 				cont.scrollTop = cont.scrollTop + contHeight;
604 			}
605 			return true;
606 		}
607 	}
608 	
609 	// next, see if there's an expanded msg view we could bring to the top
610 	var msgView = this._getSiblingMsgView(startMsgView, true),
611 		done = false;
612 
613 	while (msgView && !done) {
614 		if (msgView && msgView._expanded) {
615 			done = true;
616 		}
617 		else {
618 			msgView = this._getSiblingMsgView(msgView, true);
619 		}
620 	}
621 	if (msgView && done && canScroll) {
622 		if (!check) {
623 			this._scrollToTop(msgView);
624 			// following also works to bring msg view to top
625 			// cont.scrollTop = el.offsetTop - this._offsetAdjustment;
626 		}
627 		return true;
628 	}
629 	
630 	return false;
631 };
632 
633 // Returns the next or previous msg view based on the given msg view.
634 ZmConvView2.prototype._getSiblingMsgView = function(curMsgView, next) {
635 
636 	var idList = this._msgViewList,
637 		index = AjxUtil.indexOf(idList, curMsgView._msgId),
638 		msgView = null;
639 
640 	if (index !== -1) {
641 		var id = next ? idList[index + 1] : idList[index - 1];
642 		if (id) {
643 			msgView = this._msgViews[id];
644 		}
645 	}
646 
647 	return msgView;
648 };
649 
650 /**
651  * returns true if we are under the standalone conv view (double-clicked from conv list view)
652  */
653 ZmConvView2.prototype._isStandalone =
654 function() {
655 	return this._standalone;
656 };
657 
658 ZmConvView2.prototype._setSelectedMsg =
659 function(msg) {
660 	if (this._isStandalone()) {
661 		this._selectedMsg = msg;
662 	}
663 	var mlv = this._controller._mailListView;
664 	if (mlv) {
665 		mlv._selectedMsg = msg;
666 	}
667 };
668 
669 ZmConvView2.prototype._sendListener =
670 function() {
671 	
672 	var val = this._replyView.getValue();
673 	if (val) {
674 		var params = {
675 			action:			this._replyView.action,
676 			sendNow:		true,
677 			inNewWindow:	false
678 		};
679 		this._compose(params);
680 	}
681     this._renderCurMsgFooter();
682 };
683 
684 ZmConvView2.prototype._cancelListener =
685 function() {
686 	if (this._replyView && this._controller.popShield()) {
687 		this._replyView.reset();
688 	}
689     this._renderCurMsgFooter();
690 };
691 
692 // re-renders the message footer for the current msg view (one that's being replied to) so it has all its links
693 ZmConvView2.prototype._renderCurMsgFooter = function() {
694 
695     var msgView = this._replyView && this._replyView._msgView;
696     if (msgView) {
697         msgView._renderMessageFooter();
698     }
699 };
700 
701 // Hands off to a compose view, or takes what's in the quick reply and sends it
702 ZmConvView2.prototype._compose =
703 function(params) {
704 	
705 	if (!this._item) { return; }
706 	params = params || {};
707 
708 	params.action = params.action || ZmOperation.REPLY_ALL;
709 	var msg = params.msg = params.msg || (this._replyView && this._replyView.getMsg());
710 	if (!msg) { return; }
711 	
712 	params.hideView = params.sendNow;
713 	var composeCtlr = AjxDispatcher.run("GetComposeController", params.hideView ? ZmApp.HIDDEN_SESSION : null);
714 	params.composeMode = composeCtlr._getComposeMode(msg, composeCtlr._getIdentity(msg));
715 	var htmlMode = (params.composeMode == Dwt.HTML);
716 	params.toOverride = this._replyView.getAddresses(AjxEmailAddress.TO);
717 	params.ccOverride = this._replyView.getAddresses(AjxEmailAddress.CC);
718 	var value = this._replyView.getValue();
719 	if (value) {
720 		params.extraBodyText = htmlMode ? AjxStringUtil.convertToHtml(value) : value;
721 	}
722 
723 	var what = appCtxt.get(ZmSetting.REPLY_INCLUDE_WHAT);
724 	if (msg && (what == ZmSetting.INC_BODY || what == ZmSetting.INC_SMART)) {
725 		// make sure we've loaded the part with the type we want to reply in, if it's available
726 		var desiredPartType = htmlMode ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN;
727 		msg.getBodyPart(desiredPartType, this._sendMsg.bind(this, params, composeCtlr));
728 	}
729 	else {
730 		this._sendMsg(params, composeCtlr);
731 	}
732 };
733 
734 ZmConvView2.prototype._sendMsg =
735 function(params, composeCtlr) {
736 	composeCtlr.doAction(params);
737 	if (params.sendNow) {
738 		composeCtlr.sendMsg(null, null, this._handleResponseSendMsg.bind(this));
739 	}
740 };
741 
742 ZmConvView2.prototype._handleResponseSendMsg =
743 function() {
744 	this._replyView.reset();
745 };
746 
747 ZmConvView2.prototype.addInviteReplyListener =
748 function(listener) {
749 	this._inviteReplyListener = listener;
750 };
751 
752 ZmConvView2.prototype.addShareListener =
753 function(listener) {
754 	this._shareListener = listener;
755 };
756 
757 ZmConvView2.prototype.addSubscribeListener =
758 function(listener) {
759 	this._subscribeListener = listener;
760 };
761 
762 ZmConvView2.prototype._convChangeListener =
763 function(ev) {
764 
765 	if (ev.type != ZmEvent.S_CONV) { return; }
766 	var fields = ev.getDetail("fields");
767 	if ((ev.event == ZmEvent.E_MODIFY) && (fields && fields[ZmItem.F_SIZE])) {
768 		this._header._setInfo();
769 	}
770     this._convDirty = true;
771 };
772 
773 ZmConvView2.prototype._msgListChangeListener =
774 function(ev) {
775 	
776 	if (ev.type != ZmEvent.S_MSG) { return; }
777 	
778 	var msg = ev.item;
779 	if (!msg) { return; }
780 
781 	if (ev.event == ZmEvent.E_CREATE && this._item && (msg.cid == this._item.id) && !msg.isDraft) {
782 		var index = ev.getDetail("sortIndex");
783 		var replyViewIndex = this.getReplyViewIndex();
784 		// bump index by one if reply view comes before it
785 		index = (replyViewIndex != -1 && index > replyViewIndex) ? index + 1 : index; 
786 		var params = {
787 			parent:			this,
788 			parentElement:	document.getElementById(this._messagesDivId),
789 			controller:		this._controller,
790 			forceCollapse:	true,
791 			forceExpand:	msg.isSent,	// trumps forceCollapse
792 			index:			index
793 		}
794 		this._renderMessage(msg, params);
795 		var msgView = this._msgViews && this._msgViews[msg.id];
796 		if (msgView) {
797 			msgView._resetIframeHeightOnTimer();
798 		}
799 	}
800 	else {
801 		var msgView = this._msgViews && this._msgViews[msg.id];
802 		if (msgView) {
803 			return msgView._handleChange(ev);
804 		}
805 	}
806     this._convDirty = true;
807 };
808 
809 ZmConvView2.prototype.resetMsg =
810 function(newMsg) {
811 };
812 
813 
814 ZmConvView2.prototype.isWaitOnMarkRead =
815 function() {
816 	return this._item && this._item.waitOnMarkRead;
817 };
818 
819 // Following two overrides are a hack to allow this view to pretend it's a list view
820 ZmConvView2.prototype.getSelection =
821 function() {
822 	if (this._selectedMsg) {
823 		return [this._selectedMsg];
824 	}
825 	return [this._item];
826 };
827 
828 ZmConvView2.prototype.getSelectionCount =
829 function() {
830 	return 1;
831 };
832 
833 ZmConvView2.prototype.setReply =
834 function(msg, msgView, op) {
835 	
836 	if (!this._replyView) {
837 		this._replyView = new ZmConvReplyView({parent: this});
838 		this.getTabGroupMember().addMember(this._replyView.getTabGroupMember());
839 	}
840 	this._replyView.set(msg, msgView, op);
841 };
842 
843 /**
844  * Returns the index of the given msg view within the other msg views.
845  * 
846  * @param {ZmMailMsgCapsuleView}	msgView
847  * @return {int}
848  */
849 ZmConvView2.prototype.getMsgViewIndex =
850 function(msgView) {
851 
852 	var el = msgView && msgView.getHtmlElement();
853 	if (msgView && this._messagesDiv) {
854 		for (var i = 0; i < this._messagesDiv.childNodes.length; i++) {
855 			if (this._messagesDiv.childNodes[i] == el) {
856 				return i;
857 			}
858 		}
859 	}
860 	return -1;
861 };
862 
863 ZmConvView2.prototype.getReplyViewIndex =
864 function(msgView) {
865 
866 	if (this._messagesDiv && this._replyView) {
867 		var children = this._messagesDiv.childNodes;
868 		for (var i = 0; i < children.length; i++) {
869 			if (children[i].id == this._replyView._htmlElId) {
870 				return i;
871 			}
872 		}
873 	}
874 	return -1;
875 };
876 
877 ZmConvView2.prototype.getController = function() {
878     return this._controller;
879 };
880 
881 /**
882  * is the user actively focused on the quick reply? This is used from ZmConvListController.prototype.getKeyMapName to determine what key mapping we should use
883  */
884 ZmConvView2.prototype.isActiveQuickReply = function() {
885 	return this._replyView && this._replyView._input == document.activeElement;
886 };
887 
888 /**
889  * Creates an object manager and returns findObjects content
890  * @param view    {Object} the view used by ZmObjectManager to set mouse events
891  * @param content {String} content to scan
892  * @param htmlEncode {boolean}
893  */
894 ZmConvView2.prototype.renderObjects =
895 function(view, content, htmlEncode) {
896 	if (this._objectManager) {
897 		this._lazyCreateObjectManager(view || this);
898 		return this._objectManager.findObjects(content, htmlEncode);
899 	}
900 	return content;
901 };
902 
903 ZmConvView2.prototype._getItemCountType = function() {
904 	return ZmId.ITEM_MSG;
905 };
906 
907 
908 ZmConvView2Header = function(params) {
909 
910 	params.className = params.className || "Conv2Header";
911 	DwtComposite.call(this, params);
912 
913 	this._setEventHdlrs([DwtEvent.ONMOUSEDOWN, DwtEvent.ONMOUSEUP, DwtEvent.ONDBLCLICK]);
914 	//the following allows selection. See also comment in DwtComposite.prototype._mouseDownListener
915 	this.setEventPropagation(true, [DwtEvent.ONMOUSEDOWN, DwtEvent.ONSELECTSTART, DwtEvent.ONMOUSEUP, DwtEvent.ONMOUSEMOVE]);
916 
917 	this._convView = this.parent;
918 	this._conv = this.parent._item;
919 	this._controller = this.parent._controller;
920 	
921 	if (!this._convView._isStandalone()) {
922 		this._dblClickIsolation = true;	// ignore single click that is part of dbl click
923 		this.addListener(DwtEvent.ONDBLCLICK, this._dblClickListener.bind(this));
924 	}
925 	this.addListener(DwtEvent.ONMOUSEUP, this._mouseUpListener.bind(this));
926 	this._createHtml();
927 	this._setAllowSelection();
928 };
929 
930 ZmConvView2Header.prototype = new DwtComposite;
931 ZmConvView2Header.prototype.constructor = ZmConvView2Header;
932 
933 ZmConvView2Header.prototype.isZmConvView2Header = true;
934 ZmConvView2Header.prototype.toString = function() { return "ZmConvView2Header"; };
935 
936 ZmConvView2Header.prototype.isFocusable = true;
937 
938 ZmConvView2Header.prototype.set =
939 function(conv) {
940 
941 	this._item = conv;
942 	this._setSubject();
943 	this._setInfo();
944 	this.setVisible(true);
945 
946 	// Clean up minor WebKit-only issue where bottom edge of overflowed subject text is visible in info div
947 	if (AjxEnv.isWebKitBased && !this._headerSet) {
948 		Dwt.setSize(this._infoDiv, Dwt.DEFAULT, Dwt.getSize(this._subjectSpan).y);
949 		this._headerSet = true;
950 	}
951 };
952 
953 ZmConvView2Header.prototype.reset =
954 function() {
955 	this.setVisible(false);
956 	if (this._subjectSpan && this._infoDiv) {
957 		this._subjectSpan.innerHTML = this._infoDiv.innerHTML = "";
958         this._subjectSpan.title = "";
959 	}
960 };
961 
962 ZmConvView2Header.prototype._createHtml =
963 function() {
964 
965 	this._convExpandId		= this._htmlElId + "_expand";
966 	this._convSubjectId		= this._htmlElId + "_subject";
967 	this._convInfoId		= this._htmlElId + "_info";
968 
969 	var subs = {
970 		convExpandId:		this._convExpandId,
971 		convSubjectId:		this._convSubjectId,
972 		convInfoId:			this._convInfoId
973 	}
974 	this.getHtmlElement().innerHTML = AjxTemplate.expand("mail.Message#Conv2Header", subs);
975 
976 	this._expandDiv			= document.getElementById(this._convExpandId);
977 	this._subjectSpan		= document.getElementById(this._convSubjectId);
978 	this._infoDiv			= document.getElementById(this._convInfoId);
979 
980 	var convviewel = this._convView.getHtmlElement();
981 	convviewel.setAttribute('aria-labelledby', this._convSubjectId);
982 };
983 
984 ZmConvView2Header.prototype._setExpandIcon =
985 function() {
986 	var collapsed = this._convView.getExpanded(false);
987 	var doExpand = this._doExpand = (collapsed.length > 0);
988 	var attrs = "title='" + (doExpand ? ZmMsg.expandAllMessages : ZmMsg.collapseAllMessages) + "'";
989 	this._expandDiv.innerHTML = AjxImg.getImageHtml(doExpand ? "ConvExpand" : "ConvCollapse", "display:inline-block", attrs);
990 };
991 
992 ZmConvView2Header.prototype._setSubject =
993 function() {
994 	var subject = this._item.subject || ZmMsg.noSubject;
995 	if (this._item.numMsgs > 1) {
996 		subject = ZmMailMsg.stripSubjectPrefixes(subject);
997 	}
998 	this._subjectSpan.innerHTML = AjxStringUtil.htmlEncode(subject);
999 	this._subjectSpan.title = this._convView.subject = subject;
1000 };
1001 
1002 ZmConvView2Header.prototype._setInfo =
1003 function() {
1004 	var conv = this._item;
1005 	if (!conv) { return; }
1006 	var numMsgs = conv.numMsgs || (conv.msgs && conv.msgs.size());
1007 	if (!numMsgs) { return; }
1008 	var info = AjxMessageFormat.format(ZmMsg.messageCount, numMsgs);
1009 	var numUnread = conv.getNumUnreadMsgs();
1010 	if (numUnread) {
1011 		info = info + ", " + AjxMessageFormat.format(ZmMsg.unreadCount, numUnread).toLowerCase();
1012 	}
1013 	this._infoDiv.innerHTML = info;
1014 };
1015 
1016 ZmConvView2Header.prototype._mouseUpListener =
1017 function(ev) {
1018 	var selectedText = false, selectionObj = false, selectedId = false;
1019 	if (typeof window.getSelection != "undefined") {
1020 		selectionObj = window.getSelection();
1021 		selectedText = selectionObj.toString();
1022 		selectedId =  selectionObj.focusNode && selectionObj.focusNode.parentNode && selectionObj.focusNode.parentNode.id
1023 	} else if (typeof document.selection != "undefined" && document.selection.type == "Text") {
1024 		selectionObj = document.selection.createRange();
1025 		selectedText = selectionObj.text;
1026 		selectedId = selectionObj.parentElement().id;
1027 	}
1028 
1029 	if (selectedText && selectedId == this._convSubjectId) {
1030 		return;  //prevent expand/collapse when subject is selected
1031 	}
1032 	if (ev.button == DwtMouseEvent.LEFT) {
1033 		this._convView.setExpanded(this._doExpand);
1034 		this._setExpandIcon();
1035 	}
1036 };
1037 
1038 // Open a msg into a tabbed view
1039 ZmConvView2Header.prototype._dblClickListener =
1040 function(ev) {
1041 	if (this._convView._isStandalone()) { return; }
1042 	var conv = ev.dwtObj && ev.dwtObj.parent && ev.dwtObj.parent._item;
1043 	if (conv && ev.target !== this._subjectSpan) {
1044 		AjxDispatcher.run("GetConvController", conv.id).show(conv, this._controller);
1045 	}
1046 };
1047 
1048 
1049 
1050 
1051 ZmConvReplyView = function(params) {
1052 
1053 	params.className = params.className || "Conv2Reply";
1054 	params.id = params.parent._htmlElId + "_reply";
1055 	DwtComposite.call(this, params);
1056 	
1057 	this._convView = params.parent;
1058 	this._objectManager = new ZmObjectManager(this);
1059 
1060 	this.addControlListener(this._resized.bind(this));
1061 };
1062 
1063 ZmConvReplyView.prototype = new DwtComposite;
1064 ZmConvReplyView.prototype.constructor = ZmConvReplyView;
1065 
1066 ZmConvReplyView.prototype.isZmConvReplyView = true;
1067 ZmConvReplyView.prototype.toString = function() { return "ZmConvReplyView"; };
1068 
1069 
1070 ZmConvReplyView.prototype.TEMPLATE = "mail.Message#Conv2Reply";
1071 ZmConvReplyView.prototype.TABLE_TEMPLATE = "mail.Message#Conv2ReplyTable";
1072 
1073 ZmConvReplyView.ADDR_TYPES = [AjxEmailAddress.TO, AjxEmailAddress.CC];
1074 
1075 /**
1076  * Opens up the quick reply area below the given msg view. Addresses are set as
1077  * appropriate.
1078  * 
1079  * @param {ZmMailMsg}				msg			original msg
1080  * @param {ZmMailMsgCapsuleView} 	msgView		msg view from which reply was invoked
1081  * @param {string}					op			REPLY or REPLY_ALL
1082  */
1083 ZmConvReplyView.prototype.set =
1084 function(msg, msgView, op) {
1085 
1086 	this.action = op;
1087 	AjxDispatcher.require("Mail");
1088 	
1089 	var ai = this._addressInfo = this._getReplyAddressInfo(msg, msgView, op);
1090 
1091 	if (!this._initialized) {
1092 		var subs = ai;
1093 		subs.replyToDivId = this._htmlElId + "_replyToDiv";
1094 		subs.replyCcDivId = this._htmlElId + "_replyCcDiv";
1095 		subs.replyInputId = this._htmlElId + "_replyInput";
1096 		this._createHtmlFromTemplate(this.TEMPLATE, subs);
1097 		this._initializeToolbar();
1098 		this._replyToDiv = document.getElementById(subs.replyToDivId);
1099 		this._replyCcDiv = document.getElementById(subs.replyCcDivId);
1100 		this._input = document.getElementById(subs.replyInputId);
1101 		this._initialized = true;
1102 	}
1103 	else {
1104 		this.reset();
1105 	}
1106 	this._msg = msg;
1107 	var gotCc = (op == ZmOperation.REPLY_ALL && ai.participants[AjxEmailAddress.CC]);
1108 	this._replyToDiv.innerHTML = AjxTemplate.expand(this.TABLE_TEMPLATE, ai.participants[AjxEmailAddress.TO]);
1109 	this._replyCcDiv.innerHTML = gotCc ? AjxTemplate.expand(this.TABLE_TEMPLATE, ai.participants[AjxEmailAddress.CC]) : "";
1110 	Dwt.setVisible(this._replyCcDiv, gotCc);
1111 	setTimeout(this._resized.bind(this), 0);
1112 
1113 	var index = this._convView.getMsgViewIndex(msgView);
1114 	index = this._index = (index != -1) ? index + 1 : null;
1115 	this.reparentHtmlElement(this._convView._messagesDiv, index);
1116 	msgView.addClassName("Reply");
1117 	msgView._createBubbles();
1118     this._msgView = msgView;
1119 
1120 	this.setVisible(true);
1121 	Dwt.scrollIntoView(this.getHtmlElement(), this._convView._messagesDiv);
1122 	appCtxt.getKeyboardMgr().grabFocus(this._input);
1123 };
1124 
1125 ZmConvReplyView.prototype.getAddresses =
1126 function(type) {
1127 	return this._addressInfo && this._addressInfo.participants[type] && this._addressInfo.participants[type].addresses;
1128 };
1129 
1130 /**
1131  * Returns the value of the quick reply input box.
1132  * @return {string}
1133  */
1134 ZmConvReplyView.prototype.getValue =
1135 function() {
1136 	return this._input ? this._input.value : "";
1137 };
1138 
1139 /**
1140  * Returns the msg associated with this quick reply.
1141  * @return {ZmMailMsg}
1142  */
1143 ZmConvReplyView.prototype.getMsg =
1144 function() {
1145 	return this._msg;
1146 };
1147 
1148 /**
1149  * Sets the value of the quick reply input box.
1150  * 
1151  * @param {string}	value	new value for input
1152  */
1153 ZmConvReplyView.prototype.setValue =
1154 function(value) {
1155 	if (this._input) {
1156 		this._input.value = value;
1157 	}
1158 };
1159 
1160 /**
1161  * Clears the quick reply input box and hides the view.
1162  */
1163 ZmConvReplyView.prototype.reset =
1164 function() {
1165 	var msgView = this._msg && this._convView._msgViews[this._msg.id];
1166 	if (msgView) {
1167 		msgView.delClassName("Reply");
1168 	}
1169 	this.setValue("");
1170 	this.setVisible(false);
1171 	this._msg = null;
1172 };
1173 
1174 ZmConvReplyView.prototype._initializeToolbar = function() {
1175 	
1176 	if (!this._replyToolbar) {
1177 		var buttons = [
1178 			ZmOperation.SEND,
1179             ZmOperation.CANCEL,
1180 			ZmOperation.FILLER
1181 		];
1182 		var overrides = {};
1183 		overrides[ZmOperation.CANCEL] = {
1184             tooltipKey: "cancel",
1185             shortcut:   null
1186         };
1187 		var tbParams = {
1188 			parent:				this,
1189 			buttons:			buttons,
1190 			overrides:			overrides,
1191 			posStyle:			DwtControl.STATIC_STYLE,
1192 			buttonClassName:	"DwtToolbarButton",
1193 			context:			ZmId.VIEW_CONV2,
1194 			toolbarType:		ZmId.TB_REPLY
1195 		};
1196 		var tb = this._replyToolbar = new ZmButtonToolBar(tbParams);
1197 		tb.addSelectionListener(ZmOperation.SEND, this._convView._sendListener.bind(this._convView));
1198 		tb.addSelectionListener(ZmOperation.CANCEL, this._convView._cancelListener.bind(this._convView));
1199 	}
1200 };
1201 
1202 // Returns lists of To: and Cc: addresses to reply to, based on the msg
1203 ZmConvReplyView.prototype._getReplyAddressInfo =
1204 function(msg, msgView, op) {
1205 	
1206 	var addresses = ZmComposeView.getReplyAddresses(op, msg, msg);
1207 	
1208 	var options = {};
1209 	options.shortAddress = appCtxt.get(ZmSetting.SHORT_ADDRESS);
1210 
1211 	var showMoreIds = {};
1212 	var addressTypes = [], participants = {};
1213 	for (var i = 0; i < ZmConvReplyView.ADDR_TYPES.length; i++) {
1214 		var type = ZmConvReplyView.ADDR_TYPES[i];
1215 		var addrs = addresses[type];
1216 		if (addrs && addrs.length > 0) {
1217 			addressTypes.push(type);
1218 			var prefix = AjxStringUtil.htmlEncode(ZmMsg[AjxEmailAddress.TYPE_STRING[type]]);
1219 			var addressInfo = msgView.getAddressesFieldInfo(addrs, options, type, this._htmlElId);
1220 			participants[type] = {
1221 				addresses:	addrs,
1222 				prefix:		prefix,
1223 				partStr:	addressInfo.html
1224 			};
1225 			if (addressInfo.showMoreLinkId) {
1226 			    showMoreIds[addressInfo.showMoreLinkId] = true;
1227 			}
1228 		}
1229 	}
1230 	
1231 	return {
1232 		addressTypes:	addressTypes,
1233 		participants:	participants,
1234         showMoreIds:    showMoreIds
1235 	}
1236 };
1237 
1238 ZmConvReplyView.prototype._addAddresses =
1239 function(addresses, type, addrs, used) {
1240 	var a = addrs.getArray();
1241 	for (var i = 0; i < a.length; i++) {
1242 		var addr = a[i];
1243 		if (addr && addr.address) {
1244 			if (!used || !used[addr.address]) {
1245 				addresses[type].push(addr);
1246 			}
1247 			if (used) {
1248 				used[addr.address] = true;
1249 			}
1250 		}
1251 	}
1252 };
1253 
1254 ZmConvReplyView.prototype._resized =
1255 function() {
1256 	var bounds = this.boundsForChild(this._input);
1257 
1258 	Dwt.setSize(this._input, bounds.width, Dwt.CLEAR);
1259 };
1260 
1261 
1262 
1263 
1264 /**
1265  * The capsule view of a message is intended to be brief so that all the messages in a conv
1266  * can be shown together in a natural way. Quoted content is stripped.
1267  * 
1268  * @param {hash}			params			hash of params:
1269  * @param {string}			className		(optional) defaults to "ZmMailMsgCapsuleView"
1270  * @param {ZmConvView2}		parent			parent conv view
1271  * @param {string}			msgId			ID of msg
1272  * @param {string}			sessionId		ID of containing session (used with above param to create DOM IDs)
1273  * @param {ZmController}	controller		owning conv list controller
1274  * @param {ZmActionMenu}	actionsMenu		shared action menu
1275  * @param {boolean}			forceExpand		if true, show header, body, and footer
1276  * @param {boolean}			forceCollapse	if true, just show header
1277  * @param {boolean}			isDraft			is this message a draft
1278  */
1279 ZmMailMsgCapsuleView = function(params) {
1280 
1281 	this._normalClass = "ZmMailMsgCapsuleView";
1282 	params.className = params.className || this._normalClass;
1283 	this._msgId = params.msgId;
1284 	params.id = this._getViewId(params.sessionId);
1285 	ZmMailMsgView.call(this, params);
1286 
1287 	this._convView = this.parent;
1288 	this._controller = params.controller;
1289 	//the Boolean is to make sure undefined changes to false as otherwise this leaks down (_expanded becomes undefined) and causes problems (undefined != false in ZmConvView2.prototype.getExpanded)
1290 	this._forceExpand = Boolean(params.forceExpand);
1291 	this._forceCollapse = Boolean(params.forceCollapse);
1292 	this._forceOriginal = params.forceOriginal && !(DBG && DBG.getDebugLevel() == "orig");
1293 	this._isDraft = params.isDraft;
1294 	this._index = params.index;
1295 	this._showingCalendar = false;
1296 	this._infoBarId = this._htmlElId;
1297 	
1298 	this._browserToolTip = appCtxt.get(ZmSetting.BROWSER_TOOLTIPS_ENABLED);
1299 	
1300 	this._linkClass = "Link";
1301 
1302 	this.setScrollStyle(Dwt.VISIBLE);
1303 	
1304 	// cache text and HTML versions of original content
1305 	this._origContent = {};
1306 
1307 	this.addListener(ZmInviteMsgView.REPLY_INVITE_EVENT, this._convView._inviteReplyListener);
1308 	this.addListener(ZmMailMsgView.SHARE_EVENT, this._convView._shareListener);
1309 	this.addListener(ZmMailMsgView.SUBSCRIBE_EVENT, this._convView._subscribeListener);
1310 
1311 	this.addListener(DwtEvent.ONFOCUS,
1312 	                 ZmMailMsgCapsuleView.prototype.__onFocus.bind(this));
1313 	this.addListener(DwtEvent.ONBLUR,
1314 	                 ZmMailMsgCapsuleView.prototype.__onBlur.bind(this));
1315 };
1316 
1317 ZmMailMsgCapsuleView.prototype = new ZmMailMsgView;
1318 ZmMailMsgCapsuleView.prototype.constructor = ZmMailMsgCapsuleView;
1319 
1320 ZmMailMsgCapsuleView.prototype.isZmMailMsgCapsuleView = true;
1321 ZmMailMsgCapsuleView.prototype.toString = function() { return "ZmMailMsgCapsuleView"; };
1322 
1323 ZmMailMsgCapsuleView.prototype._getViewId =
1324 function(sessionId) {
1325 	var prefix = !sessionId ? "" : this._standalone ? ZmId.VIEW_CONV + sessionId + "_" : sessionId + "_";
1326 	return prefix + ZmId.VIEW_MSG_CAPSULE + this._msgId;
1327 };
1328 
1329 ZmMailMsgCapsuleView.prototype._getContainer =
1330 function() {
1331 	return this._container;
1332 };
1333 
1334 ZmMailMsgCapsuleView.prototype._setHeaderClass =
1335 function() {
1336 	var classes = [this._normalClass];
1337 	classes.push(this._expanded ? "Expanded" : "Collapsed");
1338 	if (this._isDraft) {
1339 		classes.push("draft");
1340 	}
1341 	if (this._lastCollapsed) {
1342 		classes.push("Last");
1343 	}
1344 	this.setClassName(classes.join(" "));
1345 };
1346 
1347 ZmMailMsgCapsuleView.prototype.set =
1348 function(msg, force) {
1349 	if (this._controller.isSearchResults) {
1350 		this._expanded = this._isMatchingMsg = msg.inHitList;
1351 	}
1352 	else {
1353 		this._expanded = !this._forceCollapse && msg.isUnread;
1354 	}
1355 	this._isCalendarInvite = appCtxt.get(ZmSetting.CALENDAR_ENABLED) && msg.invite && !msg.invite.isEmpty();
1356 	this._expanded = this._expanded || this._forceExpand;
1357 	if (this._expanded && this._isCalendarInvite && this._convView.inviteMsgsExpanded >= ZmConvView2.MAX_INVITE_MSG_EXPANDED) {
1358 		//do not expand more than MAX_INVITE_MSG_EXPANDED messages.
1359 		this._expanded = false;
1360 		this._forceExpand = false;
1361 	}
1362 	if (this._expanded) {
1363 		this._convView._hasBeenExpanded[msg.id] = true;
1364 	}
1365 	this.setAttribute('aria-expanded', Boolean(this._expanded));
1366 	this._setHeaderClass();
1367 
1368 	var dayViewCallback = null;
1369 	var showCalInConv = appCtxt.get(ZmSetting.CONV_SHOW_CALENDAR);
1370     if (this._expanded) {
1371 		if (this._isCalendarInvite) {
1372 			this._convView.inviteMsgsExpanded++;
1373 		}
1374 		dayViewCallback = this._handleShowCalendarLink.bind(this, ZmOperation.SHOW_ORIG, showCalInConv);
1375 	}
1376 	ZmMailMsgView.prototype.set.apply(this, [msg, force, dayViewCallback]);
1377 };
1378 
1379 ZmMailMsgCapsuleView.prototype.reset =
1380 function() {
1381 	ZmMailMsgView.prototype.reset.call(this);
1382 	if (this._header) {
1383 		this._header.dispose();
1384 		this._header = null;
1385 	}
1386 };
1387 
1388 /**
1389  * Resize IFRAME to match its content. IFRAMEs have a default height of 150, so we need to
1390  * explicitly set the correct height if the content is smaller. The easiest way would be
1391  * to measure the height of the HTML or BODY element, but some browsers (mainly IE) report
1392  * that to be 150. So as a backup we sum the height of the BODY element's child nodes. To
1393  * get the true height of an element we use its computed style object to add together the
1394  * vertical measurements of height, padding, and margins.
1395  */
1396 ZmMailMsgCapsuleView.prototype._resize =
1397 function() {
1398 
1399 	this._resizePending = false;
1400 	if (!this._expanded || !this._usingIframe) {
1401 		return;
1402 	}
1403 	
1404 	var contentContainer = this.getContentContainer();
1405 	if (!contentContainer) {
1406 		return;
1407 	}
1408 
1409 	//todo - combine this with _resetIframeOnTimer that is from the MSG view and also used here somehow in cases the Iframe is taller than 150 pixels.
1410 
1411 	// Get height from computed style, which seems to be the most reliable source.
1412 	var height = this._getHeightFromComputedStyle(contentContainer);
1413 	height += 20;	// account for 10px of top and bottom padding for class MsgBody-html - todo adjustment should probably be calculated, not hardcoded?
1414 
1415 	// resize the IFRAME to fit content.
1416 	DBG.println(AjxDebug.DBG1, "resizing capsule msg view IFRAME height to " + height);
1417 	Dwt.setSize(this.getIframeElement(), Dwt.DEFAULT, height);
1418 };
1419 
1420 
1421 // Look in the computed style object for height, padding, and margins.
1422 ZmMailMsgCapsuleView.prototype._getHeightFromComputedStyle =
1423 function(el) {
1424 	// Set the container overflow.  This is insures the proper height calculation.  See W3C Visual
1425 	// Formatting model details, section 10.6.6 and 10.6.7
1426 	el.style.overflow = "hidden";
1427 	var styleObj = DwtCssStyle.getComputedStyleObject(el),
1428 		height = 0;
1429 
1430 	if (styleObj && styleObj.height) {
1431 		var props = [ 'height', 'marginTop', 'marginBottom', 'paddingTop', 'paddingBottom' ];
1432 		for (var i = 0; i < props.length; i++) {
1433 			var prop = props[i];
1434 			var h = parseInt(styleObj[prop]);
1435 			if (prop === "height" && isNaN(h)) {
1436 				//default to offsetHeight if height is NaN (i.e. "auto" - this would happen for IE8 since getComputedStyleObject returns htmlElement.currentStyle, which has "auto" type stuff (i.e. not computed)
1437 				height = el.offsetHeight;
1438 				break;
1439 			}
1440 			height += isNaN(h) ? 0 : h;
1441 		}
1442 	}
1443 	el.style.overflow = "";
1444 	return height;
1445 };
1446 
1447 /**
1448  * override from ZmMailMsgView
1449  */
1450 ZmMailMsgCapsuleView.prototype._resetIframeHeightOnTimer =
1451 function() {
1452 	if (!this._resizePending) {
1453 		window.setTimeout(this._resize.bind(this), 100);
1454 		this._resizePending = true;
1455 	}
1456 };
1457 
1458 ZmMailMsgCapsuleView.prototype._renderMessage =
1459 function(msg, container, callback) {
1460 	
1461 	msg = this._msg;
1462 	this._clearBubbles();
1463 	this._createMessageHeader();
1464 	if (this._expanded) {
1465 		this._renderMessageBodyAndFooter(msg, container, callback);
1466 	}
1467 	else {
1468 		this._header.set(ZmMailMsgCapsuleViewHeader.COLLAPSED);
1469 	}
1470 };
1471 
1472 /**
1473  * Renders the header bar for this message. It's a control so that we can drag it to move the message.
1474  * 
1475  * @param msg
1476  * @param container
1477  */
1478 ZmMailMsgCapsuleView.prototype._createMessageHeader =
1479 function() {
1480 	
1481 	if (this._header) { return; }
1482 
1483 	this._header = new ZmMailMsgCapsuleViewHeader({
1484 		parent: this,
1485 		id:		[this._viewId, ZmId.MV_MSG_HEADER].join("_")
1486 	});
1487 	this._headerTabGroup.addMember(this._header);
1488 };
1489 
1490 ZmMailMsgCapsuleView.prototype._renderMessageBodyAndFooter =
1491 function(msg, container, callback) {
1492 
1493 	if (!msg.isLoaded() || this._showEntireMsg) {
1494 		var params = {
1495 			getHtml:		appCtxt.get(ZmSetting.VIEW_AS_HTML),
1496 			callback:		this._handleResponseLoadMessage.bind(this, msg, container, callback),
1497 			needExp:		true,
1498 			noTruncate:		this._showEntireMsg,
1499 			forceLoad:		this._showEntireMsg,
1500 			markRead:		this._controller._handleMarkRead(msg, true)
1501 		}
1502 		msg.load(params);
1503 		this._showEntireMsg = false;
1504 	}
1505 	else {
1506 		this._handleResponseLoadMessage(msg, container, callback);
1507 	}
1508 };
1509 
1510 ZmMailMsgCapsuleView.prototype._handleResponseLoadMessage =
1511 function(msg, container, callback) {
1512 
1513 	// Take care of a race condition, where this view may be deleted while
1514 	// a ZmMailMsg.fetch (that references this function via a callback) is
1515 	// still in progress
1516 	if (this.isDisposed()) { return; }
1517 
1518 	this._header.set(this._expanded ? ZmMailMsgCapsuleViewHeader.EXPANDED : ZmMailMsgCapsuleViewHeader.COLLAPSED);
1519 	var respCallback = this._handleResponseLoadMessage1.bind(this, msg, container, callback);
1520 	this._renderMessageBody(msg, container, respCallback);
1521 };
1522 
1523 // use a callback in case we needed to load an alternative part
1524 ZmMailMsgCapsuleView.prototype._handleResponseLoadMessage1 = function(msg, container, callback) {
1525 
1526 	this._renderMessageFooter(msg);
1527 	if (appCtxt.get(ZmSetting.MARK_MSG_READ) !== ZmSetting.MARK_READ_NOW) {
1528 		this._controller._handleMarkRead(msg);	// in case we need to mark read after a delay  bug 73711
1529 	}
1530 	if (callback) {
1531 		callback.run();
1532 	}
1533 };
1534 
1535 // Display all text messages and some HTML messages in a DIV rather than in an IFRAME.
1536 ZmMailMsgCapsuleView.prototype._useIframe =
1537 function(isTextMsg, html, isTruncated) {
1538 
1539 	this._cleanedHtml = null;
1540 
1541 	if (isTruncated)	{ return true; }
1542 	if (isTextMsg)		{ return false; }
1543 	
1544 	// Code below attempts to determine if we can display an HTML msg in a DIV. If there are
1545 	// issues with the msg DOM being part of the window DOM, we may want to just always return
1546 	// true from this function.
1547 	var result = AjxStringUtil.checkForCleanHtml(html, ZmMailMsgView.TRUSTED_TAGS, ZmMailMsgView.UNTRUSTED_ATTRS);
1548 	if (!result.useIframe) {
1549 		this._cleanedHtml = result.html;
1550 		this._contentWidth = result.width;
1551 		return false;
1552 	}
1553 	else {
1554         this._cleanedHtml = result.html;
1555 		return true;
1556 	}
1557 };
1558 
1559 ZmMailMsgCapsuleView.prototype._renderMessageBody =
1560 function(msg, container, callback, index) {
1561 
1562 	this._addLine(); //separator between header and message body
1563 
1564 	this._msgBodyDivId = [this._htmlElId, ZmId.MV_MSG_BODY].join("_");
1565 	var autoSendTime = AjxUtil.isDate(msg.autoSendTime) ? AjxDateFormat.getDateTimeInstance(AjxDateFormat.FULL, AjxDateFormat.MEDIUM).format(msg.autoSendTime) : null;
1566 	if (autoSendTime) {
1567 		var div = document.createElement("DIV");
1568 		div.id = this._autoSendHeaderId = this._msgBodyDivId + "_autoSend";
1569 		div.innerHTML = AjxTemplate.expand("mail.Message#AutoSend", {autoSendTime: autoSendTime});
1570 		this.getHtmlElement().appendChild(div);
1571 	}
1572 
1573 	if (msg.attrs) {
1574 		var additionalHdrs = [];
1575 		for (var hdrName in ZmMailMsgView.displayAdditionalHdrsInMsgView) {
1576 			if (msg.attrs[hdrName]) {
1577 				additionalHdrs.push({hdrName: ZmMailMsgView.displayAdditionalHdrsInMsgView[hdrName], hdrVal: msg.attrs[hdrName]});
1578 			}
1579 		}
1580 		if (additionalHdrs.length) {
1581 			var div = document.createElement("DIV");
1582 			div.id = this._addedHeadersId = this._msgBodyDivId + "_addedHeaders";
1583 			div.innerHTML = AjxTemplate.expand("mail.Message#AddedHeaders", {additionalHdrs: additionalHdrs});
1584 			this.getHtmlElement().appendChild(div);
1585 		}
1586 	}
1587 
1588 	var isCalendarInvite = this._isCalendarInvite;
1589 	var isShareInvite = this._isShareInvite = (appCtxt.get(ZmSetting.SHARING_ENABLED) &&
1590 												msg.share && msg.folderId != ZmFolder.ID_TRASH &&
1591 												appCtxt.getActiveAccount().id != msg.share.grantor.id &&
1592 												(msg.share.action == ZmShare.NEW ||
1593 													(msg.share.action == ZmShare.EDIT &&
1594 														!this.__hasMountpoint(msg.share))));
1595     var isSharePermNone = isShareInvite && msg.share && msg.share.link && !msg.share.link.perm;
1596 	var isSubscribeReq = msg.subscribeReq && msg.folderId != ZmFolder.ID_TRASH;
1597 
1598     if (!isCalendarInvite) {
1599         var attachmentsCount = this._msg.getAttachmentLinks(true, !appCtxt.get(ZmSetting.VIEW_AS_HTML), true).length;
1600         if (attachmentsCount > 0) {
1601             var div = document.createElement("DIV");
1602             div.id = this._attLinksId;
1603             div.className = "attachments";
1604             this.getHtmlElement().appendChild(div);
1605         }
1606     }
1607 
1608 	if (isCalendarInvite || isSubscribeReq) {
1609 		ZmMailMsgView.prototype._renderMessageHeader.call(this, msg, container, true);
1610 	}
1611 	
1612 	var params = {
1613 		getHtml:		appCtxt.get(ZmSetting.VIEW_AS_HTML),
1614 		callback:		ZmMailMsgView.prototype._renderMessageBody.bind(this, msg, container, callback, index),
1615 		needExp:		true
1616 	}
1617 	msg.load(params);
1618 
1619 	if (isCalendarInvite) {
1620 		if (AjxEnv.isIE) {
1621 			// for some reason width=100% on inv header table makes it too wide (bug 65696)
1622 			Dwt.setSize(this._headerElement, this._header.getSize().x, Dwt.DEFAULT);
1623 		}
1624 	}
1625 	
1626 	if ((isShareInvite && !isSharePermNone) || isSubscribeReq) {
1627 		var bodyEl = this.getMsgBodyElement();
1628 		var toolbar = isShareInvite ? this._getShareToolbar() : this._getSubscribeToolbar(msg.subscribeReq);
1629 		if (toolbar) {
1630 			toolbar.reparentHtmlElement(bodyEl, 0);
1631 		}
1632 		// invite header
1633 		if (this._headerElement) {
1634 			bodyEl.insertBefore(this._headerElement.parentNode, bodyEl.firstChild);
1635 		}
1636 	}
1637 	
1638 	this._beenHere = true;
1639 };
1640 
1641 ZmMailMsgCapsuleView.prototype._getInviteSubs =
1642 function(subs) {
1643 	ZmMailMsgView.prototype._getInviteSubs.apply(this, arguments);
1644 
1645     subs.noTopHeader = true;
1646 };
1647 
1648 ZmMailMsgCapsuleView.prototype._addLine =
1649 function() {
1650 	var div = document.createElement("div");
1651 	div.className = "separator";
1652 	this.getHtmlElement().appendChild(div);
1653 };
1654 
1655 ZmMailMsgCapsuleView.prototype._getBodyContent =
1656 function(bodyPart) {
1657 
1658 	if (!bodyPart || !bodyPart.getContent()) { return ""; }
1659 	
1660 	var isHtml = (bodyPart.ct == ZmMimeTable.TEXT_HTML);
1661 	var cacheKey = [bodyPart.part, bodyPart.contentType].join("|");
1662 	var origContent = this._origContent[cacheKey];
1663 	if (!origContent && !this._forceOriginal && !this._isMatchingMsg) {
1664 		origContent = AjxStringUtil.getOriginalContent(bodyPart.getContent(), isHtml);
1665 		if (origContent.length != bodyPart.getContent().length) {
1666 			this._origContent[cacheKey] = origContent;
1667 			this._hasOrigContent = true;
1668 		}
1669 	}
1670 
1671 	var content = (this._showingQuotedText || this._forceOriginal || this._isMatchingMsg || !origContent) ? bodyPart.getContent() : origContent;
1672 	content = content || "";
1673 	// remove trailing blank lines
1674 	content = isHtml ? AjxStringUtil.trimHtml(content) : AjxStringUtil.trim(content);
1675 	return content;
1676 };
1677 
1678 /**
1679  * Renders the row of links at the bottom of the msg.
1680  *
1681  * @param {ZmMailMsg}   msg     msg being displayed
1682  * @param {string}      op      (optional) operation being performed
1683  *
1684  * @private
1685  */
1686 ZmMailMsgCapsuleView.prototype._renderMessageFooter = function(msg, op) {
1687 
1688     msg = msg || this._msg;
1689     var div = this._footerId && Dwt.byId(this._footerId);
1690     if (!div) {
1691         div = document.createElement("div");
1692         div.className = "footer";
1693         div.id = this._footerId = [this.getHTMLElId(), ZmId.MV_MSG_FOOTER].join("_");
1694     }
1695 	
1696 	var showTextKey, showTextHandler;
1697 	if (this._isCalendarInvite) {
1698 		showTextKey = "showCalendar";
1699 		showTextHandler = this._handleShowCalendarLink;
1700 	}
1701 	else if (this._hasOrigContent) {
1702 		showTextKey = this._showingQuotedText ? "hideQuotedText" : "showQuotedText";
1703 		showTextHandler = this._handleShowTextLink;
1704 	}
1705 	
1706 	var linkInfo = this._linkInfo = {};
1707     var isExternalAccount = appCtxt.isExternalAccount();
1708 	linkInfo[ZmOperation.SHOW_ORIG] 	    = {key: showTextKey,	        handler: showTextHandler,                                           disabled: isExternalAccount};
1709 	linkInfo[ZmOperation.DRAFT]			    = {key: "editDraft",	        handler: this._handleEditDraftLink,     op: ZmOperation.DRAFT,	    disabled: isExternalAccount};
1710 	linkInfo[ZmOperation.REPLY]			    = {key: "reply",		        handler: this._handleReplyLink, 	    op: ZmOperation.REPLY,	    disabled: isExternalAccount};
1711 	linkInfo[ZmOperation.REPLY_ALL]		    = {key: "replyAll",		        handler: this._handleReplyLink, 	    op: ZmOperation.REPLY_ALL,	disabled: isExternalAccount};
1712 	linkInfo[ZmOperation.FORWARD]		    = {key: "forward",		        handler: this._handleForwardLink,	    op: ZmOperation.FORWARD,    disabled: isExternalAccount};
1713 	linkInfo[ZmOperation.ACTIONS_MENU]	    = {key: "moreActions",	        handler: this._handleMoreActionsLink};
1714 	linkInfo[ZmOperation.COMPOSE_OPTIONS]	= {key: "moreComposeOptions",	handler: this._handleMoreOptionsLink,                               disabled: isExternalAccount};
1715 
1716 	var links;
1717 	var folder = appCtxt.getById(msg.folderId);
1718 
1719 	if (folder && folder.isFeed()) {
1720 		links = [
1721 			ZmOperation.SHOW_ORIG,
1722 			ZmOperation.FORWARD,
1723 			ZmOperation.ACTIONS_MENU
1724 		];
1725 	}
1726 	else if (msg.isDraft) {
1727         links = [
1728 			ZmOperation.SHOW_ORIG,
1729 			ZmOperation.ACTIONS_MENU
1730 		];
1731         if (!folder.isReadOnly()){
1732             links = [].concat(ZmOperation.DRAFT,links);
1733         }
1734 	}
1735 	else {
1736         links = [ ZmOperation.REPLY, ZmOperation.REPLY_ALL ];
1737         if (op) {
1738             // if user is doing Reply or Reply All, show the other one
1739             links = (op === ZmOperation.REPLY) ? [ ZmOperation.REPLY_ALL ] : [ ZmOperation.REPLY ];
1740         }
1741         links.unshift(ZmOperation.SHOW_ORIG);
1742         links.push(ZmOperation.FORWARD, ZmOperation.ACTIONS_MENU);
1743 	}
1744 
1745     if (op) {
1746         links.push(ZmOperation.COMPOSE_OPTIONS);
1747     }
1748 
1749 	var linkHtml = [];
1750 	for (var i = 0; i < links.length; i++) {
1751 		var html = this._makeLink(links[i]);
1752 		if (html) {
1753 			linkHtml.push(html);
1754 		}
1755 	}
1756 	div.innerHTML = linkHtml.join(" - ");
1757 	this.getHtmlElement().appendChild(div);
1758 
1759     this._links = [];
1760     var linkFound = false;
1761 	for (var i = 0; i < links.length; i++) {
1762 		var info = this._linkInfo[links[i]];
1763 		var link = info && document.getElementById(info.linkId);
1764 		if (link) {
1765 			this._makeFocusable(link);
1766 			Dwt.setHandler(link, DwtEvent.ONCLICK, this._linkClicked.bind(this, links[i], info.op));
1767 			this._footerTabGroup.addMember(link);
1768             // Bit of a hack so we can treat the row of links like a toolbar and move focus
1769             // between links using left/right arrow buttons.
1770             link.noTab = linkFound;
1771             this._links.push(link);
1772             linkFound = true;
1773 		}
1774 	}
1775 
1776     // Attempt to display the calendar if the preference is to auto-open it
1777     this._handleShowCalendarLink(ZmOperation.SHOW_ORIG, appCtxt.get(ZmSetting.CONV_SHOW_CALENDAR)); //this is called from here since the _linkInfo is now ready and needed in _handleShowCalendarLink. Might be other reason too.
1778 };
1779 
1780 ZmMailMsgCapsuleView.prototype._makeLink =
1781 function(id) {
1782 	var info = this._linkInfo && id && this._linkInfo[id];
1783 	if (!(info && info.key && info.handler)) { return ""; } 
1784 	
1785 	var linkId = info.linkId = [this._footerId, info.key].join("_");
1786     if (info.disabled) {
1787         return "<span id='" + linkId + "'>" + ZmMsg[info.key] + "</span>";
1788     }
1789 	return "<a class='ConvLink Link' id='" + linkId + "'>" + ZmMsg[info.key] + "</a>";
1790 };
1791 
1792 ZmMailMsgCapsuleView.prototype._linkClicked =
1793 function(id, op, ev) {
1794 
1795 	var info = this._linkInfo && id && this._linkInfo[id];
1796 	var handler = (info && !info.disabled) ? info.handler : null;
1797 	if (handler) {
1798 		handler.apply(this, [id, info.op, ev]);
1799 	}
1800 };
1801 
1802 // Moves focus between links in the footer
1803 ZmMailMsgCapsuleView.prototype._focusLink = function(prev, refLink) {
1804 
1805     var link;
1806     for (var i = 0; i < this._links.length; i++) {
1807         if (this._links[i] === refLink) {
1808             link = this._links[prev ? i - 1 : i + 1];
1809             break;
1810         }
1811     }
1812 
1813     if (link) {
1814         appCtxt.getKeyboardMgr().grabFocus(link);
1815     }
1816 };
1817 
1818 ZmMailMsgCapsuleView.prototype.__onFocus =
1819 function(ev) {
1820 	if (!ev || !ev.target) {
1821 		return;
1822 	}
1823 
1824 	Dwt.setOpacity(this._footerId, 100);
1825 };
1826 
1827 ZmMailMsgCapsuleView.prototype.__onBlur =
1828 function(ev) {
1829 	if (!ev || !ev.target) {
1830 		return;
1831 	}
1832 
1833 	var footer = Dwt.byId(this._footerId);
1834 
1835 	if (footer) {
1836 		footer.style.opacity = null;
1837 	}
1838 };
1839 
1840 // TODO: something more efficient than a re-render
1841 ZmMailMsgCapsuleView.prototype._handleShowTextLink =
1842 function(id, op, ev) {
1843 	var msg = this._msg;
1844 	this.reset();
1845 	this._showingQuotedText = !this._showingQuotedText;
1846 	this._forceExpand = true;
1847 	this.set(msg, true);
1848 };
1849 
1850 ZmMailMsgCapsuleView.prototype._handleShowCalendarLink =
1851 function(id, show) {
1852     // Allow one of two possible paths to auto display the calendar view
1853     if (!this._isCalendarInvite) {
1854 		return;
1855 	}
1856 
1857     var showCalendarLink = this._linkInfo && document.getElementById(this._linkInfo[ZmOperation.SHOW_ORIG].linkId);
1858 
1859 	if (show !== undefined) {
1860 		this._showingCalendar = show; //force a value
1861 	}
1862 	else {
1863 		this._showingCalendar = !this._showingCalendar; //toggle
1864 		// Track the last show/hide and apply to other invites that are opened.
1865 		appCtxt.set(ZmSetting.CONV_SHOW_CALENDAR, this._showingCalendar);
1866 	}
1867 
1868 	var imv = this._inviteMsgView;
1869 	if (!this._inviteCalendarContainer && imv) {
1870         var dayView = imv && imv._dayView;
1871         if (dayView && showCalendarLink) {
1872             // Both components (dayView and footer) have been rendered - can go ahead and
1873             // attach the dayView.  This is only an issue for the initial auto display
1874 
1875             // Shove it in a relative-positioned container DIV so it can use absolute positioning
1876             var div = this._inviteCalendarContainer = document.createElement("div");
1877             var elRef = this.getHtmlElement();
1878             if (elRef) {
1879                 elRef.appendChild(div);
1880                 Dwt.setSize(div, Dwt.DEFAULT, 220);
1881                 Dwt.setPosition(div, Dwt.RELATIVE_STYLE);
1882                 dayView.reparentHtmlElement(div);
1883                 dayView.setVisible(true);
1884                 imv.convResize();
1885             }
1886         }
1887     }
1888 	if (this._inviteCalendarContainer) {
1889 		Dwt.setVisible(this._inviteCalendarContainer, this._showingCalendar);
1890 	}
1891 
1892 
1893     if (imv && this._showingCalendar) {
1894         imv.scrollToInvite();
1895     }
1896     if (showCalendarLink) {
1897         showCalendarLink.innerHTML = this._showingCalendar ? ZmMsg.hideCalendar : ZmMsg.showCalendar;
1898     }
1899 	this._resetIframeHeightOnTimer();
1900 };
1901 
1902 ZmMailMsgCapsuleView.prototype._handleForwardLink =
1903 function(id, op, ev) {
1904 	var text = "", replyView = this._convView._replyView;
1905 	if (replyView) {
1906 		text = replyView.getValue();
1907 		replyView.reset();
1908 	}
1909 	this._controller._doAction({action:op, msg:this._msg, extraBodyText:text});
1910 };
1911 
1912 ZmMailMsgCapsuleView.prototype._handleMoreActionsLink = function(id, op, ev) {
1913 
1914 	ev = DwtUiEvent.getEvent(ev);
1915 
1916     // User can focus on link and hit Enter - create a location to pop up the menu
1917     var x = ev.clientX, y = ev.clientY;
1918     if (!x || !y) {
1919         var loc = Dwt.getLocation(DwtUiEvent.getTarget(ev));
1920         if (loc) {
1921             x = loc.x;
1922             y = loc.y;
1923         }
1924     }
1925 	ev.docX = x;
1926 	ev.docY = y;
1927 	this._actionListener(ev, true);
1928 };
1929 
1930 ZmMailMsgCapsuleView.prototype._handleReplyLink = function(id, op, ev, force) {
1931 
1932 	if (!force && !this._controller.popShield(null, this._handleReplyLink.bind(this, id, op, ev, true))) {
1933 		return;
1934 	}
1935 	this._convView.setReply(this._msg, this, op);
1936     this._renderMessageFooter(this._msg, op);
1937 };
1938 
1939 ZmMailMsgCapsuleView.prototype._handleMoreOptionsLink = function(ev) {
1940 
1941     this._convView._compose({
1942         msg:    this._msg,
1943         action: this.action
1944     });
1945     this._convView._replyView.reset();
1946     this._renderMessageFooter(this._msg);
1947 };
1948 
1949 ZmMailMsgCapsuleView.prototype._handleEditDraftLink =
1950 function(id, op, ev) {
1951 	this._controller._doAction({action:op, msg:this._msg});
1952 };
1953 
1954 ZmMailMsgCapsuleView.prototype.isExpanded =
1955 function() {
1956 	return this._expanded;
1957 };
1958 
1959 /**
1960  * Expand the msg view by hiding/showing the body and footer. If the msg hasn't
1961  * been rendered, we need to render it to expand it.
1962  */
1963 ZmMailMsgCapsuleView.prototype._toggleExpansion =
1964 function() {
1965 	
1966 	var expanded = !this._expanded;
1967 	if (!expanded && !this._controller.popShield(null, this._setExpansion.bind(this, false))) {
1968 		return;
1969 	}
1970 	this._setExpansion(expanded);
1971 	this._resetIframeHeightOnTimer();
1972 };
1973 
1974 ZmMailMsgCapsuleView.prototype._setExpansion =
1975 function(expanded) {
1976 
1977 	if (this.isDisposed()) { return; }
1978 
1979 	if (Dwt.isAncestor(this.getHtmlElement(), document.activeElement)) {
1980 		this.getHtmlElement().focus();
1981 	}
1982 
1983 	var showCalInConv = appCtxt.get(ZmSetting.CONV_SHOW_CALENDAR);
1984 	this._expanded = expanded;
1985 	this.setAttribute('aria-expanded', Boolean(this._expanded));
1986 	if (this._expanded && !this._msgBodyCreated) {
1987 		// Provide a callback to ensure address bubbles are properly set up
1988 		var dayViewCallback = null;
1989 		if (this._isCalendarInvite) {
1990 			dayViewCallback = this._handleShowCalendarLink.bind(this, ZmOperation.SHOW_ORIG, showCalInConv);
1991 		}
1992 		var respCallback = this._handleResponseSetExpansion.bind(this, this._msg, dayViewCallback);
1993 		this._renderMessage(this._msg, null, respCallback);
1994 	}
1995 	else {
1996 		// hide or show everything below the header
1997         var children = this.getHtmlElement().childNodes;
1998         for (var i = 0; i < children.length; i++) {
1999 			var child = children[i];
2000 			if (child === this._header.getHtmlElement()) {
2001 				//do not collapse the header! (p.s. it might not be the first child - see bug 82989
2002 				continue;
2003 			}
2004 			var show = (child && child.id === this._displayImagesId) ? this._expanded && this._needToShowInfoBar : this._expanded;
2005 			Dwt.setVisible(child, show);
2006 		}
2007 		this._header.set(this._expanded ? ZmMailMsgCapsuleViewHeader.EXPANDED : ZmMailMsgCapsuleViewHeader.COLLAPSED);
2008 		if (this._expanded) {
2009 			this._setTags(this._msg);
2010 			this._controller._handleMarkRead(this._msg);
2011 			appCtxt.notifyZimlets("onMsgExpansion", [this._msg, this]);
2012 		}
2013 		else {
2014 			var replyView = this._convView._replyView;
2015 			if (replyView && replyView._msg == this._msg) {
2016 				replyView.reset();
2017 			}
2018 		}
2019 		this._convView._header._setExpandIcon();
2020 		if (this._expanded && this._isCalendarInvite) {
2021 			this._handleShowCalendarLink(ZmOperation.SHOW_ORIG, showCalInConv);
2022 		}
2023 	}
2024 
2025 	if (this._expanded) {
2026 		this._createBubbles();
2027 		if (this._controller._checkKeepReading) {
2028 			this._controller._checkKeepReading();
2029 		}
2030 		this._lastCollapsed = false;
2031 	}
2032 
2033 	this._setHeaderClass();
2034 	this._resetIframeHeightOnTimer();
2035 };
2036 
2037 ZmMailMsgCapsuleView.prototype._handleResponseSetExpansion =
2038 function(msg, callback) {
2039 	this._handleResponseSet(msg, null, callback);
2040 	this._convView._header._setExpandIcon();
2041 };
2042 
2043 ZmMailMsgCapsuleView.prototype._insertTagRow =
2044 function(table, tagCellId) {
2045 	
2046 	if (!table) { return; }
2047 	
2048 	var tagRow = table.insertRow(-1);
2049 	var cell;
2050 	tagRow.id = this._tagRowId;
2051 	cell = tagRow.insertCell(-1);
2052 	cell.className = "LabelColName";
2053 	cell.innerHTML = ZmMsg.tags + ":";
2054 	cell.style.verticalAlign = "middle";
2055 	var tagCell = tagRow.insertCell(-1);
2056 	tagCell.className = "LabelColValue";
2057 	tagCell.id = tagCellId;
2058 	cell = tagRow.insertCell(-1);
2059 	cell.style.align = "right";
2060 	cell.innerHTML = " ";
2061 	
2062 	return tagCell;
2063 };
2064 
2065 // Msg view header has been left-clicked
2066 ZmMailMsgCapsuleView.prototype._selectionListener =
2067 function(ev) {
2068 
2069 	this._toggleExpansion();
2070 
2071 	// Remember the last msg view that the user collapsed. Expanding any msg view clears that.
2072 	var convView = this._convView,
2073 		lastCollapsedMsgView = convView._lastCollapsedId && convView._msgViews[convView._lastCollapsedId];
2074 
2075 	if (lastCollapsedMsgView) {
2076 		lastCollapsedMsgView._lastCollapsed = false;
2077 		lastCollapsedMsgView._setHeaderClass();
2078 	}
2079 	var isCollapsed = !this.isExpanded();
2080 	this._lastCollapsed = isCollapsed;
2081 	convView._lastCollapsedId = isCollapsed && this._msgId;
2082 	this._setHeaderClass();
2083 
2084 	return true;
2085 };
2086 
2087 // Msg view header has been right-clicked
2088 ZmMailMsgCapsuleView.prototype._actionListener =
2089 function(ev, force) {
2090 
2091 	var hdr = this._header;
2092 	var el = DwtUiEvent.getTargetWithProp(ev, "id", false, hdr._htmlElId);
2093 	if (force || (el == hdr.getHtmlElement())) {
2094 		var target = DwtUiEvent.getTarget(ev);
2095 		var objMgr = this._objectManager;
2096 		if (objMgr && !AjxUtil.isBoolean(objMgr) && objMgr._findObjectSpan(target)) {
2097 			// let zimlet framework handle this; we don't want to popup our action menu
2098 			return;
2099 		}
2100 		this._convView._setSelectedMsg(this._msg);
2101 		this._controller._listActionListener.call(this._controller, ev);
2102 		return true;
2103 	}
2104 	return false;
2105 };
2106 
2107 /**
2108  * returns true if we are under the standalone conv view (double-clicked from conv list view)
2109  */
2110 ZmMailMsgCapsuleView.prototype._isStandalone =
2111 function() {
2112 	return this.parent._isStandalone();
2113 };
2114 
2115 // No-op parent change listener. We rely on list change listener.
2116 ZmMailMsgCapsuleView.prototype._msgChangeListener = function(ev) {};
2117 
2118 // Handle changes internally, without using ZmMailMsgView's change listener (it assumes a single
2119 // msg displayed in reading pane).
2120 ZmMailMsgCapsuleView.prototype._handleChange =
2121 function(ev) {
2122 
2123 	if (ev.type != ZmEvent.S_MSG) { return; }
2124 	if (this.isDisposed()) { return; }
2125 
2126 	if (ev.event == ZmEvent.E_FLAGS) {
2127 		var flags = ev.getDetail("flags");
2128 		for (var j = 0; j < flags.length; j++) {
2129 			var flag = flags[j];
2130 			if (flag == ZmItem.FLAG_UNREAD) {
2131 				this._header._setReadIcon();
2132 				this._header._setHeaderClass();
2133 				this._convView._header._setInfo();
2134 			}
2135 		}
2136 	}
2137 	else if (ev.event == ZmEvent.E_DELETE) {
2138 		this.dispose();
2139 		this._convView._removeMessageView(this._msg.id);
2140 		this._convView._header._setInfo();
2141 	}
2142 	else if (ev.event == ZmEvent.E_MOVE) {
2143 		this._changeFolderName(ev.getDetail("oldFolderId"));
2144 	}
2145 	else if (ev.event == ZmEvent.E_TAGS || ev.event == ZmEvent.E_REMOVE_ALL) {
2146 		this._setTags(this._msg);
2147 	}
2148 };
2149 
2150 ZmMailMsgCapsuleView.prototype._changeFolderName = 
2151 function(oldFolderId) {
2152 
2153 	var msg = this._msg;
2154 	var folder = appCtxt.getById(msg.folderId);
2155 	if (folder && (folder.nId == ZmFolder.ID_TRASH || oldFolderId == ZmFolder.ID_TRASH)) {
2156 		this._header._setHeaderClass(msg);
2157 	}
2158 };
2159 
2160 ZmMailMsgCapsuleView.prototype._handleMsgTruncated =
2161 function() {
2162 	this._msg.viewEntireMessage = true;	// remember so we reply to entire msg
2163 	this._showEntireMsg = true;			// set flag to load non-truncated msg
2164 	if (this._inviteMsgView) {
2165 		this._inviteMsgView._dayView = null; // for some reason the DOM of it gets lost so we have to null it so we don't try to access it later - instead of would be re-created.
2166 		this._inviteCalendarContainer = null;
2167 	}
2168 	this._forceExpand = true;
2169 	// redo loading and display of entire msg
2170 	this.set(this._msg, true);
2171 	
2172 	Dwt.setVisible(this._msgTruncatedId, false);
2173 };
2174 
2175 ZmMailMsgCapsuleView.prototype.isTruncated =
2176 function(part) {
2177 	/*
2178 	 There are 3 possible cases here
2179 	 1. Message does not have a quoted content - In this case use truncation information from the message part.
2180 	 2. Message has a quoted content which is visible - In this case use the truncation information from message part.
2181 	 3. Message has a quoted content which is hidden - In this case if the truncation happens it will always be in the quoted content which is hidden so return false.
2182 	 */
2183 	return (!this._hasOrigContent || this._showingQuotedText) ? part.isTruncated : false;
2184 };
2185 
2186 ZmMailMsgCapsuleView.prototype._getIframeTitle = function() {
2187 	return AjxMessageFormat.format(ZmMsg.messageTitleInConv, this._index + 1);
2188 };
2189 
2190 /**
2191  * The header bar of a capsule message view:
2192  * 	- shows minimal header info (from, date)
2193  * 	- has an expansion icon
2194  * 	- is used to drag the message
2195  * 	- is the drop target for tags
2196  * 	
2197  * @param params
2198  */
2199 ZmMailMsgCapsuleViewHeader = function(params) {
2200 
2201 	this._normalClass = "Conv2MsgHeader";
2202 	params.posStyle = DwtControl.RELATIVE_STYLE;
2203 	params.className = params.className || this._normalClass;
2204 	DwtControl.call(this, params);
2205 
2206 	this._setEventHdlrs([DwtEvent.ONMOUSEDOWN, DwtEvent.ONMOUSEMOVE, DwtEvent.ONMOUSEUP, DwtEvent.ONDBLCLICK]);
2207 	
2208 	this._msgView = this.parent;
2209 	this._convView = this.parent._convView;
2210 	this._msg = this.parent._msg;
2211 	this._controller = this.parent._controller;
2212 	this._browserToolTip = this.parent._browserToolTip;
2213 	
2214 	if (this._controller.supportsDnD()) {
2215 		var dragSrc = new DwtDragSource(Dwt.DND_DROP_MOVE);
2216 		dragSrc.addDragListener(this._dragListener.bind(this));
2217 		this.setDragSource(dragSrc);
2218 		var dropTgt = this._dropTgt = new DwtDropTarget("ZmTag");
2219 		dropTgt.addDropListener(this._dropListener.bind(this));
2220 		this.setDropTarget(dropTgt);
2221 	}
2222 	
2223 	this.addListener(DwtEvent.ONDBLCLICK, this._dblClickListener);
2224 	this.addListener(DwtEvent.ONMOUSEUP, this._mouseUpListener.bind(this));
2225 	
2226 	this.setScrollStyle(DwtControl.CLIP);
2227 };
2228 
2229 ZmMailMsgCapsuleViewHeader.prototype = new DwtControl;
2230 ZmMailMsgCapsuleViewHeader.prototype.constructor = ZmMailMsgCapsuleViewHeader;
2231 
2232 ZmMailMsgCapsuleViewHeader.prototype.isZmMailMsgCapsuleViewHeader = true;
2233 ZmMailMsgCapsuleViewHeader.prototype.toString = function() { return "ZmMailMsgCapsuleViewHeader"; };
2234 
2235 ZmMailMsgCapsuleViewHeader.prototype.isFocusable = true;
2236 ZmMailMsgCapsuleViewHeader.prototype.role = 'header';
2237 
2238 ZmMailMsgCapsuleViewHeader.COLLAPSED	= "Collapsed";
2239 ZmMailMsgCapsuleViewHeader.EXPANDED		= "Expanded";
2240 
2241 /**
2242  * Renders a header in one of two ways:
2243  * 
2244  *		collapsed:	from address (full name), fragment, date
2245  *		expanded:	address headers with bubbles, date, icons for folder, tags, etc
2246  *	
2247  * We can't cache the header content because the email zimlet fills in the bubbles after the
2248  * HTML has been generated (expanded view).
2249  * 
2250  * @param {constant}	state	collapsed or expanded
2251  * @param {boolean}		force	if true, render even if not changing state
2252  */
2253 ZmMailMsgCapsuleViewHeader.prototype.set =
2254 function(state, force) {
2255 
2256 	if (!force && state == this._state) { return; }
2257 	var beenHere = !!this._state;
2258 	state = this._state = state || this._state;
2259 	var isExpanded = (state == ZmMailMsgCapsuleViewHeader.EXPANDED);
2260 	
2261 	var id = this._htmlElId;
2262 	var msg = this._msg;
2263 	var ai = this._msgView._getAddrInfo(msg);
2264 	this._showMoreIds = ai.showMoreIds;
2265 
2266 	var folder = appCtxt.getById(msg.folderId);
2267 	msg.showImages = msg.showImages || (folder && folder.isFeed());
2268 	this._idToAddr = {};
2269 
2270 	this._dateCellId = id + "_dateCell";
2271 	var date = msg.sentDate || msg.date;
2272 	var dateFormatter = AjxDateFormat.getDateTimeInstance(AjxDateFormat.LONG, AjxDateFormat.SHORT);
2273 	var dateString = dateFormatter.format(new Date(date));
2274 
2275 	this._readIconId = id + "_read";
2276 	this._readCellId = id + "_readCell";
2277 
2278 	var html;
2279 
2280 	var subs = {
2281 		readCellId:		this._readCellId,
2282 		date:			dateString,
2283 		dateCellId:		this._dateCellId,
2284 	};
2285 
2286 	var imageSize = isExpanded ? 48 : 32,
2287 		imageURL  = ai.sentByContact && ai.sentByContact.getImageUrl(imageSize, imageSize),
2288 		imageAltText = imageURL && ai.sentByContact && ai.sentByContact.getFullName();
2289 
2290 	if (!isExpanded) {
2291 		var fromId = id + "_0";
2292 		this._idToAddr[fromId] = ai.fromAddr;
2293 
2294 		AjxUtil.hashUpdate(subs, {
2295 			imageURL:	    imageURL || ZmZimbraMail.DEFAULT_CONTACT_ICON_SMALL,
2296 			defaultImageUrl:	ZmZimbraMail.DEFAULT_CONTACT_ICON_SMALL,
2297 			imageAltText:   imageAltText || ZmMsg.unknownPerson,
2298 			from:		    ai.from,
2299 			fromId:		    fromId,
2300 			fragment:	    AjxStringUtil.htmlEncode(msg.fragment),
2301 			isInvite:       this.parent._isCalendarInvite
2302 		});
2303 		html = AjxTemplate.expand("mail.Message#Conv2MsgHeader-collapsed", subs);
2304 	}
2305 	else {
2306 
2307 		AjxUtil.hashUpdate(subs, {
2308 			hdrTableId:		this._msgView._hdrTableId = id + "_hdrTable",
2309 			imageURL:		imageURL || ZmZimbraMail.DEFAULT_CONTACT_ICON,
2310 			defaultImageUrl:	ZmZimbraMail.DEFAULT_CONTACT_ICON,
2311 			imageAltText:   imageAltText || ZmMsg.unknownPerson,
2312 			sentBy:			ai.sentBy,
2313 			sentByAddr:		ai.sentByAddr,
2314 			obo:			ai.obo,
2315 			oboAddr:		ai.oboAddr,
2316 			oboId:			id +  ZmId.CMP_OBO_SPAN,
2317 			bwo:			ai.bwo,
2318 			bwoAddr:		ai.bwoAddr,
2319 			bwoId:			id +  ZmId.CMP_BWO_SPAN,
2320 			addressTypes:	ai.addressTypes,
2321 			participants:	ai.participants,
2322 			isOutDated:		msg.invite && msg.invite.isEmpty()
2323 		});
2324 		html = AjxTemplate.expand("mail.Message#Conv2MsgHeader-expanded", subs);
2325 	}
2326 
2327 	this.setContent(html);
2328 	this._setHeaderClass();
2329 	
2330 	this._setReadIcon();
2331 	
2332 	for (var id in this._showMoreIds) {
2333 		var showMoreLink = document.getElementById(id);
2334 		if (showMoreLink) {
2335 			showMoreLink.notoggle = 1;
2336 		}
2337 	}
2338 };
2339 
2340 /**
2341  * Gets the tool tip content.
2342  * 
2343  * @param	{Object}	ev		the hover event
2344  * @return	{String}	the tool tip content
2345  */
2346 ZmMailMsgCapsuleViewHeader.prototype.getToolTipContent =
2347 function(ev) {
2348 	var el = DwtUiEvent.getTargetWithProp(ev, "id");
2349 	if (el && el.id) {
2350 		var id = el.id;
2351 		if (!id) { return ""; }
2352 		var addr = this._idToAddr[id];
2353 		if (addr) {
2354 			var ttParams = {address:addr, ev:ev, noRightClick:true};
2355 			var ttCallback = new AjxCallback(this,
2356 				function(callback) {
2357 					appCtxt.getToolTipMgr().getToolTip(ZmToolTipMgr.PERSON, ttParams, callback);
2358 				});
2359 			return {callback:ttCallback};
2360 		}
2361 	}
2362 };
2363 
2364 ZmMailMsgCapsuleViewHeader.prototype.getTooltipBase =
2365 function(hoverEv) {
2366 	return hoverEv ? DwtUiEvent.getTargetWithProp(hoverEv.object, "id") : DwtControl.prototype.getTooltipBase.apply(this, arguments);
2367 };
2368 
2369 // Indicate unread and/or in Trash
2370 ZmMailMsgCapsuleViewHeader.prototype._setHeaderClass =
2371 function() {
2372 
2373 	var msg = this._msg;
2374 	var classes = [this._normalClass];
2375 	var folder = appCtxt.getById(msg.folderId);
2376 	if (folder && folder.isInTrash()) {
2377 		classes.push("Trash");
2378 	}
2379 	if (msg.isUnread && !msg.isMute) {
2380 		classes.push("Unread");
2381 	}
2382 	this.setClassName(classes.join(" "));
2383 };
2384 
2385 // Set the ball icon to show read or unread
2386 ZmMailMsgCapsuleViewHeader.prototype._setReadIcon =
2387 function() {
2388 	var readCell = document.getElementById(this._readCellId);
2389 	if (readCell) {
2390 		var isExpanded = (this._state == ZmMailMsgCapsuleViewHeader.EXPANDED);
2391 		var tooltip = this._msg.isUnread ? ZmMsg.markAsRead : ZmMsg.markAsUnread;
2392 		var attrs = "id='" + this._readIconId + "' noToggle=1 title='" + tooltip + "'";
2393 		var iePos = AjxEnv.isIE ? "position:static" : null;
2394 		readCell.innerHTML = AjxImg.getImageHtml(this._msg.getReadIcon(), isExpanded ? iePos : "display:inline-block", attrs);
2395 	}
2396 };
2397 
2398 ZmMailMsgCapsuleViewHeader.prototype._mouseUpListener =
2399 function(ev) {
2400 	
2401 	var msgView = this._msgView;
2402 	var convView = msgView._convView;
2403 
2404 	var target = DwtUiEvent.getTarget(ev);
2405 	if (target && target.id == this._readIconId) {
2406 		var folder = appCtxt.getById(this._msg.folderId);
2407 		if (!(folder && folder.isReadOnly())) {
2408 			this._controller._doMarkRead([this._msg], this._msg.isUnread);
2409 		}
2410 		return true;
2411 	}
2412 	else if (DwtUiEvent.getTargetWithProp(ev, "notoggle")) {
2413 		// ignore event if an internal control should handle it
2414 		return false;
2415 	}
2416 	
2417 	if (ev.button == DwtMouseEvent.LEFT) {
2418 		return msgView._selectionListener(ev);
2419 	}
2420 	else if (ev.button == DwtMouseEvent.RIGHT) {
2421 		return msgView._actionListener(ev);
2422 	}
2423 };
2424 	
2425 ZmMailMsgCapsuleViewHeader.prototype._dragListener =
2426 function(ev) {
2427 	if (ev.action == DwtDragEvent.SET_DATA) {
2428 		ev.srcData = {data: this._msg, controller: this._controller};
2429 	}
2430 };
2431 
2432 ZmMailMsgCapsuleViewHeader.prototype._getDragProxy =
2433 function(dragOp) {
2434 	var view = this._msgView;
2435 	var icon = ZmMailMsgListView.prototype._createItemHtml.call(this._controller._mailListView, view._msg, {now:new Date(), isDragProxy:true});
2436 	Dwt.setPosition(icon, Dwt.ABSOLUTE_STYLE);
2437 	appCtxt.getShell().getHtmlElement().appendChild(icon);
2438 	Dwt.setZIndex(icon, Dwt.Z_DND);
2439 	return icon;
2440 };
2441 
2442 // TODO: should we highlight msg header (dragSelect it)?
2443 ZmMailMsgCapsuleViewHeader.prototype._dropListener =
2444 function(ev) {
2445 
2446 	var item = this._msg;
2447 
2448 	// only tags can be dropped on us
2449 	var data = ev.srcData.data;
2450 	if (ev.action == DwtDropEvent.DRAG_ENTER) {
2451 		ev.doIt = (item && item.isZmItem && !item.isReadOnly() && this._dropTgt.isValidTarget(data));
2452         // Bug: 44488 - Don't allow dropping tag of one account to other account's item
2453         if (appCtxt.multiAccounts) {
2454            var listAcctId = item ? item.getAccount().id : null;
2455            var tagAcctId = (data.account && data.account.id) || data[0].account.id;
2456            if (listAcctId != tagAcctId) {
2457                ev.doIt = false;
2458            }
2459         }
2460 		DBG.println(AjxDebug.DBG3, "DRAG_ENTER: doIt = " + ev.doIt);
2461 	} else if (ev.action == DwtDropEvent.DRAG_DROP) {
2462 		this._controller._doTag([item], data, true);
2463 	}
2464 };
2465 
2466 // Open a msg into a tabbed view
2467 ZmMailMsgCapsuleViewHeader.prototype._dblClickListener =
2468 function(ev) {
2469 	var msg = ev.dwtObj && ev.dwtObj.parent && ev.dwtObj.parent._msg;
2470 	if (msg) {
2471 		AjxDispatcher.run("GetMsgController", msg.nId).show(msg, this._controller, null, true);
2472 	}
2473 };
2474