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 controller. 26 * @constructor 27 * @class 28 * This class manages the two-pane conversation view. The top pane contains a list 29 * view of the messages in the conversation, and the bottom pane contains the current 30 * message. 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 * 39 * @extends ZmConvListController 40 */ 41 ZmConvController = function(container, mailApp, type, sessionId) { 42 43 ZmConvListController.apply(this, arguments); 44 this._elementsToHide = ZmAppViewMgr.LEFT_NAV; 45 }; 46 47 ZmConvController.prototype = new ZmConvListController; 48 ZmConvController.prototype.constructor = ZmConvController; 49 50 ZmConvController.prototype.isZmConvController = true; 51 ZmConvController.prototype.toString = function() { return "ZmConvController"; }; 52 53 ZmMailListController.GROUP_BY_ICON[ZmId.VIEW_CONV] = "ConversationView"; 54 55 ZmConvController.viewToTab = {}; 56 57 ZmConvController.DEFAULT_TAB_TEXT = ZmMsg.conversation; 58 59 /** 60 * Displays the given conversation in a two-pane view. 61 * 62 * @param {ZmConv} conv a conversation 63 * @param {ZmListController} parentController the controller that called this method 64 * @param {AjxCallback} callback the client callback 65 * @param {Boolean} markRead if <code>true</code>, mark msg read 66 * @param {ZmMailMsg} msg msg that launched this conv view (via "Show Conversation") 67 */ 68 ZmConvController.prototype.show = 69 function(conv, parentController, callback, markRead, msg) { 70 71 this._conv = conv; 72 conv.refCount++; 73 conv.isInUse = true; 74 75 this._relatedMsg = msg; 76 this._parentController = parentController; 77 78 this._setup(this._currentViewId); 79 this._convView = this._view[this._currentViewId]; 80 81 if (!conv._loaded) { 82 var respCallback = this._handleResponseLoadConv.bind(this, conv, callback); 83 markRead = !msg && (markRead || (appCtxt.get(ZmSetting.MARK_MSG_READ) == ZmSetting.MARK_READ_NOW)); 84 conv.load({fetch:ZmSetting.CONV_FETCH_UNREAD_OR_FIRST, markRead:markRead}, respCallback); 85 } else { 86 this._handleResponseLoadConv(conv, callback, conv._createResult()); 87 } 88 }; 89 90 ZmConvController.prototype.supportsDnD = 91 function() { 92 return false; 93 }; 94 95 // No headers, can't sort 96 ZmConvController.prototype.supportsSorting = function() { 97 return false; 98 }; 99 100 // Cannot choose to group a conv by either msg or conv, it's always a msg list 101 ZmConvController.prototype.supportsGrouping = function() { 102 return false; 103 }; 104 105 ZmConvController.prototype._handleResponseLoadConv = 106 function(conv, callback, result) { 107 108 var searchResult = result.getResponse(); 109 var list = searchResult.getResults(ZmItem.MSG); 110 this._currentSearch = searchResult.search; 111 if (list && list.isZmList) { 112 this.setList(list); 113 this._activeSearch = searchResult; 114 } 115 116 this._showConv(); 117 118 if (callback) { 119 callback.run(); 120 } 121 }; 122 123 ZmConvController.prototype._tabCallback = 124 function(oldView, newView) { 125 return (appCtxt.getViewTypeFromId(oldView) == ZmId.VIEW_CONV); 126 }; 127 128 129 ZmConvController.prototype._showConv = 130 function() { 131 //for now it's straight forward but I keep this layer, if only for clarity of purpose by the name _showConv. 132 this._showMailItem(); 133 }; 134 135 ZmConvController.prototype._resetNavToolBarButtons = 136 function(view) { 137 //overide to do nothing. 138 }; 139 140 ZmConvController.prototype._getTabParams = 141 function(tabId, tabCallback) { 142 return { 143 id: tabId, 144 image: "CloseGray", 145 hoverImage: "Close", 146 style: DwtLabel.IMAGE_RIGHT, 147 textPrecedence: 85, 148 tooltip: ZmDoublePaneController.DEFAULT_TAB_TEXT, 149 tabCallback: tabCallback 150 }; 151 }; 152 153 ZmConvController.prototype._getActionMenuOps = 154 function() { 155 return ZmDoublePaneController.prototype._getActionMenuOps.call(this); 156 }; 157 158 159 ZmConvController.prototype._setViewContents = 160 function(view) { 161 var convView = this._view[view]; 162 convView.reset(); 163 convView.set(this._conv); 164 }; 165 166 ZmConvController.prototype.getConv = 167 function() { 168 return this._conv; 169 }; 170 171 172 // Private and protected methods 173 174 ZmConvController.prototype._getReadingPanePref = 175 function() { 176 return (this._readingPaneLoc || appCtxt.get(ZmSetting.READING_PANE_LOCATION_CV)); 177 }; 178 179 ZmConvController.prototype._setReadingPanePref = 180 function(value) { 181 if (this.isSearchResults || appCtxt.isExternalAccount()) { 182 this._readingPaneLoc = value; 183 } 184 else { 185 appCtxt.set(ZmSetting.READING_PANE_LOCATION_CV, value); 186 } 187 }; 188 189 ZmConvController.prototype._initializeView = 190 function(view) { 191 if (!this._view[view]) { 192 var params = { 193 parent: this._container, 194 id: ZmId.getViewId(ZmId.VIEW_CONV2, null, view), 195 posStyle: Dwt.ABSOLUTE_STYLE, 196 mode: view, 197 standalone: true, //double-clicked stand-alone view of the conv (not within the double pane) 198 controller: this 199 }; 200 this._view[view] = new ZmConvView2(params); 201 this._view[view].addInviteReplyListener(this._inviteReplyListener); 202 this._view[view].addShareListener(this._shareListener); 203 this._view[view].addSubscribeListener(this._subscribeListener); 204 } 205 }; 206 207 ZmConvController.prototype._getToolBarOps = 208 function() { 209 var list = [ZmOperation.CLOSE, ZmOperation.SEP]; 210 list = list.concat(ZmConvListController.prototype._getToolBarOps.call(this)); 211 return list; 212 }; 213 214 // conv view has arrows to go to prev/next conv, so needs regular nav toolbar 215 ZmConvController.prototype._initializeNavToolBar = 216 function(view) { 217 // ZmMailListController.prototype._initializeNavToolBar.apply(this, arguments); 218 // this._itemCountText[ZmSetting.RP_BOTTOM] = this._navToolBar[view]._textButton; 219 }; 220 221 ZmConvController.prototype._backListener = 222 function(ev) { 223 if (!this.popShield(null, this._close.bind(this))) { 224 return; 225 } 226 this._close(); 227 }; 228 229 ZmConvController.prototype._close = 230 function(ev) { 231 this._app.popView(); 232 }; 233 234 ZmConvController.prototype._postRemoveCallback = 235 function() { 236 this._conv.isInUse = false; 237 }; 238 239 ZmConvController.prototype._navBarListener = 240 function(ev) { 241 var op = ev.item.getData(ZmOperation.KEY_ID); 242 if (op == ZmOperation.PAGE_BACK || op == ZmOperation.PAGE_FORWARD) { 243 this._goToConv(op == ZmOperation.PAGE_FORWARD); 244 } 245 }; 246 247 ZmConvController.getDefaultViewType = 248 function() { 249 return ZmId.VIEW_CONV; 250 }; 251 ZmConvController.prototype.getDefaultViewType = ZmConvController.getDefaultViewType; 252 253 ZmConvController.prototype._setActiveSearch = 254 function(view) { 255 // bug fix #7389 - do nothing! 256 }; 257 258 259 ZmConvController.prototype.getItemView = 260 function() { 261 return this._view[this._currentViewId]; 262 }; 263 264 // if going from msg to conv view, don't have server mark stuff read (we'll just expand the one msg view) 265 ZmConvController.prototype._handleMarkRead = 266 function(item, check) { 267 return this._relatedMsg ? false : ZmConvListController.prototype._handleMarkRead.apply(this, arguments); 268 }; 269 270 // Operation listeners 271 272 273 // Handle DnD tagging (can only add a tag to a single item) - if a tag got dropped onto 274 // a msg, we need to update its conv 275 ZmConvController.prototype._dropListener = 276 function(ev) { 277 ZmListController.prototype._dropListener.call(this, ev); 278 // need to check to make sure tagging actually happened 279 if (ev.action == DwtDropEvent.DRAG_DROP) { 280 var div = this._listView[this._currentViewId].getTargetItemDiv(ev.uiEvent); 281 if (div) { 282 var tag = ev.srcData; 283 if (!this._conv.hasTag(tag.id)) { 284 // this._doublePaneView._setTags(this._conv); // update tag summary 285 } 286 } 287 } 288 }; 289 290 291 // Miscellaneous 292 293 // Called after a delete/move notification has been received. 294 // Return value indicates whether view was popped as a result of a delete. 295 ZmConvController.prototype.handleDelete = 296 function() { 297 298 var popView = true; 299 300 if (this._conv.numMsgs > 1) { 301 popView = !this._conv.hasMatchingMsg(AjxDispatcher.run("GetConvListController").getList().search, true); 302 } 303 304 // Don't pop unless we're currently visible! 305 var currViewId = appCtxt.getCurrentViewId(); 306 307 // bug fix #4356 - if currViewId is compose (among other restrictions) then still pop 308 var popAnyway = false; 309 if (currViewId == ZmId.VIEW_COMPOSE && this._conv.numMsgs == 1 && this._conv.msgs) { 310 var msg = this._conv.msgs.getArray()[0]; 311 popAnyway = (msg.isInvite() && msg.folderId == ZmFolder.ID_TRASH); 312 } 313 314 popView = popView && ((currViewId == this._currentViewId) || popAnyway); 315 316 if (popView) { 317 this._app.popView(); 318 } else { 319 var delButton = this._toolbar[this._currentViewId].getButton(ZmOperation.DELETE_MENU); 320 var delMenu = delButton ? delButton.getMenu() : null; 321 if (delMenu) { 322 delMenu.enable(ZmOperation.DELETE_MSG, false); 323 } 324 } 325 326 return popView; 327 }; 328 329 ZmConvController.prototype.getKeyMapName = function() { 330 // if user is quick replying, don't use the mapping of conv/mail list - so Ctrl+Z works 331 return this._convView && this._convView.isActiveQuickReply() ? ZmKeyMap.MAP_QUICK_REPLY : ZmKeyMap.MAP_CONVERSATION; 332 }; 333 334 ZmConvController.prototype.handleKeyAction = 335 function(actionCode) { 336 DBG.println(AjxDebug.DBG3, "ZmConvController.handleKeyAction"); 337 338 var navToolbar = this._navToolBar[this._currentViewId], 339 button; 340 341 switch (actionCode) { 342 case ZmKeyMap.CANCEL: 343 this._backListener(); 344 break; 345 346 case ZmKeyMap.NEXT_CONV: 347 button = navToolbar && navToolbar.getButton(ZmOperation.PAGE_FORWARD); 348 if (!button || button.getEnabled()) { 349 this._goToConv(true); 350 } 351 break; 352 353 case ZmKeyMap.PREV_CONV: 354 button = navToolbar && navToolbar.getButton(ZmOperation.PAGE_BACK); 355 if (!button || button.getEnabled()) { 356 this._goToConv(false); 357 } 358 break; 359 360 // switching view not supported here 361 case ZmKeyMap.VIEW_BY_CONV: 362 case ZmKeyMap.VIEW_BY_MSG: 363 break; 364 365 default: 366 return ZmConvListController.prototype.handleKeyAction.call(this, actionCode); 367 } 368 return true; 369 }; 370 371 372 ZmConvController.prototype._getNumTotal = 373 function() { 374 return this._conv.numMsgs; 375 }; 376 377 /** 378 * Gets the selected message. 379 * 380 * @param {Hash} params a hash of parameters 381 * @return {ZmMailMsg} the selected message 382 */ 383 ZmConvController.prototype.getMsg = 384 function(params) { 385 return ZmConvListController.prototype.getMsg.call(this, params); //we need to get the first hot message from the conv. 386 }; 387 388 ZmConvController.prototype._getLoadedMsg = 389 function(params, callback) { 390 callback.run(this.getMsg()); 391 }; 392 393 // overloaded... 394 ZmConvController.prototype._search = 395 function(view, offset, limit, callback) { 396 var params = { 397 sortBy: appCtxt.get(ZmSetting.SORTING_PREF, view), 398 offset: offset, 399 limit: limit 400 }; 401 this._conv.load(params, callback); 402 }; 403 404 ZmConvController.prototype._goToConv = 405 function(next) { 406 var ctlr = this._getConvListController(); 407 if (ctlr) { 408 ctlr.pageItemSilently(this._conv, next); 409 } 410 }; 411 412 ZmConvController.prototype._getSearchFolderId = 413 function() { 414 return this._conv.list && this._conv.list.search && this._conv.list.search.folderId; 415 }; 416 417 // top level view means this view is allowed to get shown when user clicks on 418 // app icon in app toolbar - we dont want conv view to be top level (always show CLV) 419 ZmConvController.prototype._isTopLevelView = 420 function() { 421 return false; 422 }; 423 424 // don't preserve selection in CV, just select first hot msg as usual 425 ZmConvController.prototype._resetSelection = function() {}; 426 427 ZmConvController.prototype._selectNextItemInParentListView = 428 function() { 429 var controller = this._getConvListController(); 430 if (controller && controller._listView[controller._currentViewId]) { 431 controller._listView[controller._currentViewId]._itemToSelect = controller._getNextItemToSelect(); 432 } 433 }; 434 435 ZmConvController.prototype._checkItemCount = 436 function() { 437 if (this._view[this._currentViewId]._selectedMsg) { 438 return; //just a message was deleted, not the entire conv. Do nothing else. 439 } 440 this._backListener(); 441 }; 442 443 /** 444 * have to do this since we don't want what is from ZmDoublePaneController, and ZmConvListController extends ZmDoublePaneController... (unlike 445 * ZmMailListController - ZmDoublePaneController extends ZmMailLitController actually). 446 * @returns {Array} 447 * @private 448 */ 449 ZmConvController.prototype._getRightSideToolBarOps = 450 function() { 451 return [ZmOperation.VIEW_MENU]; 452 }; 453 454 /** 455 * have to do this since otherwise we get the one from ZmDoublePaneController and that's not good. 456 * @private 457 */ 458 ZmConvController.prototype._setupReadingPaneMenu = function() { 459 }; 460 461 /** 462 * this is called sometimes as a result of stuf in ZmConvListController - but this view has no next item so override to just return null 463 * @returns {null} 464 * @private 465 */ 466 ZmConvController.prototype._getNextItemToSelect = 467 function() { 468 return null; 469 }; 470 471 ZmConvController.prototype._doMove = 472 function() { 473 this._selectNextItemInParentListView(); 474 ZmConvListController.prototype._doMove.apply(this, arguments); 475 }; 476 477 ZmConvController.prototype._doSpam = 478 function() { 479 this._selectNextItemInParentListView(); 480 ZmConvListController.prototype._doSpam.apply(this, arguments); 481 }; 482 483 ZmConvController.prototype._msgViewCurrent = 484 function() { 485 return true; 486 }; 487 488 ZmConvController.prototype._getTagMenuMsg = 489 function(num) { 490 return AjxMessageFormat.format(ZmMsg.tagMessages, num); 491 }; 492 493 ZmConvController.prototype._getConvListController = 494 function(num) { 495 return this._parentController || AjxDispatcher.run("GetConvListController"); 496 }; 497 498 ZmConvController.prototype.popShield = 499 function(viewId, callback, newViewId) { 500 var ctlr = this._getConvListController(); 501 if (ctlr && ctlr.popShield) { 502 return ctlr.popShield.apply(this, arguments); 503 } else { 504 return true; 505 } 506 }; 507 508 ZmConvController.prototype._popShieldYesCallback = 509 function(switchingView, callback) { 510 var ctlr = this._getConvListController(); 511 return ctlr && ctlr._popShieldYesCallback.apply(this, arguments); 512 }; 513 514 ZmConvController.prototype._popShieldNoCallback = 515 function(switchingView, callback) { 516 var ctlr = this._getConvListController(); 517 return ctlr && ctlr._popShieldNoCallback.apply(this, arguments); 518 }; 519 520 ZmConvController.prototype._postRemoveCallback = function() { 521 this._conv.refCount--; 522 }; 523