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