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