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