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 ZmMailMsgListView = function(params) { 25 26 this._mode = params.mode; 27 this.view = params.view; 28 params.type = ZmItem.MSG; 29 this._controller = params.controller; 30 params.headerList = this._getHeaderList(); 31 ZmMailListView.call(this, params); 32 this.setAttribute("aria-label", ZmMsg.messageList); 33 }; 34 35 ZmMailMsgListView.prototype = new ZmMailListView; 36 ZmMailMsgListView.prototype.constructor = ZmMailMsgListView; 37 38 ZmMailMsgListView.prototype.isZmMailMsgListView = true; 39 ZmMailMsgListView.prototype.toString = function() { return "ZmMailMsgListView"; }; 40 41 // Consts 42 43 // TODO: move to CV 44 ZmMailMsgListView.SINGLE_COLUMN_SORT_CV = [ 45 {field:ZmItem.F_FROM, msg:"from" }, 46 {field:ZmItem.F_SIZE, msg:"size" }, 47 {field:ZmItem.F_DATE, msg:"date" } 48 ]; 49 50 // Public methods 51 52 53 ZmMailMsgListView.prototype.markUIAsRead = 54 function(msg) { 55 ZmMailListView.prototype.markUIAsRead.apply(this, arguments); 56 var classes = this._getClasses(ZmItem.F_STATUS, !this.isMultiColumn() ? ["ZmMsgListBottomRowIcon"]:null); 57 this._setImage(msg, ZmItem.F_STATUS, msg.getStatusIcon(), classes); 58 }; 59 60 // Private / protected methods 61 62 // following _createItemHtml support methods are also used for creating msg 63 // rows in ZmConvListView 64 65 // support for showing which msgs in a conv matched the search 66 // TODO: move to CV 67 ZmMailMsgListView.prototype._addParams = 68 function(msg, params) { 69 if (this._mode == ZmId.VIEW_TRAD) { 70 return ZmMailListView.prototype._addParams.apply(this, arguments); 71 } else { 72 var conv = appCtxt.getById(msg.cid); 73 var s = this._controller._activeSearch && this._controller._activeSearch.search; 74 params.isMatched = (s && s.hasContentTerm() && msg.inHitList); 75 } 76 }; 77 78 ZmMailMsgListView.prototype._getDivClass = 79 function(base, item, params) { 80 if (params.isMatched && !params.isDragProxy) { 81 return base + " " + [base, DwtCssStyle.MATCHED].join("-"); // Row Row-matched 82 } else { 83 return ZmMailListView.prototype._getDivClass.apply(this, arguments); 84 } 85 }; 86 87 ZmMailMsgListView.prototype._getRowClass = 88 function(msg) { 89 var classes = this._isMultiColumn ? ["DwtMsgListMultiCol"]:["ZmRowDoubleHeader"]; 90 if (this._mode != ZmId.VIEW_TRAD) { 91 var folder = appCtxt.getById(msg.folderId); 92 if (folder && folder.isInTrash()) { 93 classes.push("Trash"); 94 } 95 } 96 if (msg.isUnread) { classes.push("Unread"); } 97 if (msg.isSent) { classes.push("Sent"); } 98 99 return classes.join(" "); 100 }; 101 102 ZmMailMsgListView.prototype._getCellId = 103 function(item, field) { 104 if (field == ZmItem.F_SUBJECT && (this._mode == ZmId.VIEW_CONV || 105 this._mode == ZmId.VIEW_CONVLIST)) { 106 return this._getFieldId(item, field); 107 } else { 108 return ZmMailListView.prototype._getCellId.apply(this, arguments); 109 } 110 }; 111 112 ZmMailMsgListView.prototype._getCellContents = 113 function(htmlArr, idx, msg, field, colIdx, params, classes) { 114 var zimletStyle = this._getStyleViaZimlet(field, msg) || ""; 115 if (field == ZmItem.F_READ) { 116 idx = this._getImageHtml(htmlArr, idx, msg.getReadIcon(), this._getFieldId(msg, field), classes); 117 } 118 else if (field == ZmItem.F_STATUS) { 119 idx = this._getImageHtml(htmlArr, idx, msg.getStatusIcon(), this._getFieldId(msg, field), classes); 120 } else if (field == ZmItem.F_FROM || field == ZmItem.F_PARTICIPANT) { 121 htmlArr[idx++] = "<div " + AjxUtil.getClassAttr(classes) + zimletStyle + ">"; 122 // setup participants list for Sent/Drafts/Outbox folders 123 if (this._isOutboundFolder()) { 124 var addrs = msg.getAddresses(AjxEmailAddress.TO).getArray(); 125 126 if (addrs && addrs.length) { 127 var fieldId = this._getFieldId(msg, ZmItem.F_FROM); 128 var origLen = addrs.length; 129 var headerCol = this._headerHash[field]; 130 var partColWidth = headerCol ? headerCol._width : ZmMsg.COLUMN_WIDTH_FROM_CLV; 131 var parts = this._fitParticipants(addrs, msg, partColWidth); 132 for (var j = 0; j < parts.length; j++) { 133 if (j == 0 && (parts.length < origLen)) { 134 htmlArr[idx++] = AjxStringUtil.ELLIPSIS; 135 } else if (parts.length > 1 && j > 0) { 136 htmlArr[idx++] = AjxStringUtil.LIST_SEP; 137 } 138 htmlArr[idx++] = "<span id='"; 139 htmlArr[idx++] = [fieldId, parts[j].index].join(DwtId.SEP); 140 htmlArr[idx++] = "'>"; 141 htmlArr[idx++] = AjxStringUtil.htmlEncode(parts[j].name); 142 htmlArr[idx++] = "</span>"; 143 } 144 } else { 145 htmlArr[idx++] = " "; 146 } 147 } else { 148 var fromAddr = msg.getAddress(AjxEmailAddress.FROM); 149 var fromText = fromAddr && fromAddr.getText(); 150 if (fromText) { 151 htmlArr[idx++] = "<span id='"; 152 htmlArr[idx++] = this._getFieldId(msg, ZmItem.F_FROM); 153 htmlArr[idx++] = "'>"; 154 htmlArr[idx++] = AjxStringUtil.htmlEncode(fromText); 155 htmlArr[idx++] = "</span>"; 156 } 157 else { 158 htmlArr[idx++] = "<span>" + ZmMsg.unknown + "</span>"; 159 } 160 } 161 htmlArr[idx++] = "</div>"; 162 163 } else if (field == ZmItem.F_SUBJECT) { 164 htmlArr[idx++] = "<div " + AjxUtil.getClassAttr(classes) + zimletStyle + ">"; 165 if (this._mode == ZmId.VIEW_CONV || this._mode == ZmId.VIEW_CONVLIST) { 166 // msg within a conv shows just the fragment 167 //originally bug 97510 - need a span so I can target it via CSS rule, so the margin is within the column content, and doesn't push the other columns 168 htmlArr[idx++] = "<span " + (this._isMultiColumn ? "" : "class='ZmConvListFragment'") + " id='" + this._getFieldId(msg, ZmItem.F_FRAGMENT) + "'>"; 169 htmlArr[idx++] = AjxStringUtil.htmlEncode(msg.fragment, true); 170 htmlArr[idx++] = "</span>"; 171 } else { 172 // msg on its own (TV) shows subject and fragment 173 var subj = msg.subject || ZmMsg.noSubject; 174 htmlArr[idx++] = "<span id='"; 175 htmlArr[idx++] = this._getFieldId(msg, field); 176 htmlArr[idx++] = "'>" + AjxStringUtil.htmlEncode(subj) + "</span>"; 177 if (appCtxt.get(ZmSetting.SHOW_FRAGMENTS) && msg.fragment) { 178 htmlArr[idx++] = this._getFragmentSpan(msg); 179 } 180 } 181 htmlArr[idx++] = "</div>"; 182 183 } else if (field == ZmItem.F_FOLDER) { 184 htmlArr[idx++] = "<div " + AjxUtil.getClassAttr(classes) + " id='"; 185 htmlArr[idx++] = this._getFieldId(msg, field); 186 htmlArr[idx++] = "'>"; // required for IE bug 187 var folder = appCtxt.getById(msg.folderId); 188 if (folder) { 189 htmlArr[idx++] = folder.getName(); 190 } 191 htmlArr[idx++] = "</div>"; 192 193 } else if (field == ZmItem.F_SIZE) { 194 htmlArr[idx++] = "<div " + AjxUtil.getClassAttr(classes) + ">"; 195 htmlArr[idx++] = AjxUtil.formatSize(msg.size); 196 htmlArr[idx++] = "</div>"; 197 } else if (field == ZmItem.F_SORTED_BY) { 198 htmlArr[idx++] = this._getAbridgedContent(msg, colIdx); 199 } else { 200 if (this.isMultiColumn() || field !== ZmItem.F_SELECTION) { 201 //do not call this for checkbox in single column layout 202 idx = ZmMailListView.prototype._getCellContents.apply(this, arguments); 203 } 204 } 205 206 return idx; 207 }; 208 209 ZmMailMsgListView.prototype._getAbridgedContent = 210 function(item, colIdx) { 211 var htmlArr = []; 212 var idx = 0; 213 var width = (AjxEnv.isIE || AjxEnv.isSafari) ? "22" : "16"; 214 215 var selectionCssClass = ''; 216 for (var i = 0; i < this._headerList.length; i++) { 217 if (this._headerList[i]._field == ZmItem.F_SELECTION) { 218 selectionCssClass = "ZmMsgListSelection"; 219 break; 220 } 221 } 222 // first row 223 htmlArr[idx++] = "<div class='TopRow " + selectionCssClass + "' "; 224 htmlArr[idx++] = "id='"; 225 htmlArr[idx++] = DwtId.getListViewItemId(DwtId.WIDGET_ITEM_FIELD, this._view, item.id, ZmItem.F_ITEM_ROW_3PANE); 226 htmlArr[idx++] = "'>"; 227 if (selectionCssClass) { 228 idx = ZmMailListView.prototype._getCellContents.apply(this, [htmlArr, idx, item, ZmItem.F_SELECTION, colIdx]); 229 } 230 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_READ, colIdx, width); 231 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_FROM, colIdx); 232 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_DATE, colIdx, ZmMsg.COLUMN_WIDTH_DATE, "align=right", ["ZmMsgListDate"]); 233 htmlArr[idx++] = "</div>"; 234 235 // second row 236 htmlArr[idx++] = "<div class='BottomRow " + selectionCssClass + "'>"; 237 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_STATUS, colIdx, width, null, ["ZmMsgListBottomRowIcon"]); 238 239 // for multi-account, show the account icon for cross mbox search results 240 if (appCtxt.multiAccounts && appCtxt.getSearchController().searchAllAccounts) { 241 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_ACCOUNT, colIdx, "16", "align=right"); 242 } 243 if (item.isHighPriority || item.isLowPriority) { 244 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_PRIORITY, colIdx, "10", "align=right"); 245 } 246 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_SUBJECT, colIdx); 247 //add the attach, flag and tags in a wrapping div 248 idx = this._getListFlagsWrapper(htmlArr, idx, item); 249 if (item.hasAttach) { 250 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_ATTACHMENT, colIdx, width); 251 } 252 var tags = item.getVisibleTags(); 253 if (tags && tags.length) { 254 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_TAG, colIdx, width, null, ["ZmMsgListColTag"]); 255 } 256 if (appCtxt.get("FLAGGING_ENABLED")) { 257 idx = this._getAbridgedCell(htmlArr, idx, item, ZmItem.F_FLAG, colIdx, width); 258 } 259 htmlArr[idx++] = "</div></div>"; 260 261 return htmlArr.join(""); 262 }; 263 264 ZmMailMsgListView.prototype._getToolTip = 265 function(params) { 266 267 var tooltip, field = params.field, item = params.item; 268 if (!item) { return; } 269 270 if (!this._isMultiColumn && (field == ZmItem.F_SUBJECT || field == ZmItem.F_FRAGMENT)) { 271 var invite = (item.type == ZmItem.MSG) && item.isInvite() && item.invite; 272 if (invite && (item.needsRsvp() || !invite.isEmpty())) { 273 tooltip = ZmMailListView.prototype._getToolTip.apply(this, arguments); 274 } 275 else { 276 tooltip = AjxStringUtil.htmlEncode(item.fragment || (item.hasAttach ? "" : ZmMsg.fragmentIsEmpty)); 277 var folderTip = null; 278 var folder = appCtxt.getById(item.folderId); 279 if (folder && folder.parent) { 280 folderTip = AjxMessageFormat.format(ZmMsg.accountDownloadToFolder, folder.getPath()); 281 } 282 tooltip = (tooltip && folderTip) ? [tooltip, folderTip].join("<br>") : tooltip || folderTip; 283 } 284 } 285 else { 286 tooltip = ZmMailListView.prototype._getToolTip.apply(this, arguments); 287 } 288 289 return tooltip; 290 }; 291 292 // Listeners 293 294 ZmMailMsgListView.prototype._changeListener = 295 function(ev) { 296 297 var msg = this._getItemFromEvent(ev); 298 if (!msg || ev.handled || !this._handleEventType[msg.type]) { return; } 299 300 if ((ev.event == ZmEvent.E_DELETE || ev.event == ZmEvent.E_MOVE) && this._mode == ZmId.VIEW_CONV) { 301 if (!this._controller.handleDelete()) { 302 if (ev.event == ZmEvent.E_DELETE) { 303 ZmMailListView.prototype._changeListener.call(this, ev); 304 } else { 305 // if spam, remove it from listview 306 if (msg.folderId == ZmFolder.ID_SPAM) { 307 this._controller._list.remove(msg, true); 308 ZmMailListView.prototype._changeListener.call(this, ev); 309 } else { 310 this._changeFolderName(msg, ev.getDetail("oldFolderId")); 311 this._checkReplenishOnTimer(); 312 } 313 } 314 } 315 } else if (this._mode == ZmId.VIEW_CONV && ev.event == ZmEvent.E_CREATE) { 316 var conv = AjxDispatcher.run("GetConvController").getConv(); 317 if (conv && (msg.cid == conv.id)) { 318 ZmMailListView.prototype._changeListener.call(this, ev); 319 } 320 } else if (ev.event == ZmEvent.E_FLAGS) { // handle "replied" and "forwarded" flags 321 var flags = ev.getDetail("flags"); 322 for (var j = 0; j < flags.length; j++) { 323 var flag = flags[j]; 324 var on = msg[ZmItem.FLAG_PROP[flag]]; 325 if (flag == ZmItem.FLAG_REPLIED && on) { 326 this._setImage(msg, ZmItem.F_STATUS, "MsgStatusReply", this._getClasses(ZmItem.F_STATUS)); 327 } else if (flag == ZmItem.FLAG_FORWARDED && on) { 328 this._setImage(msg, ZmItem.F_STATUS, "MsgStatusForward", this._getClasses(ZmItem.F_STATUS)); 329 } 330 } 331 ZmMailListView.prototype._changeListener.call(this, ev); // handle other flags 332 } else { 333 ZmMailListView.prototype._changeListener.call(this, ev); 334 if (ev.event == ZmEvent.E_CREATE || ev.event == ZmEvent.E_DELETE || ev.event == ZmEvent.E_MOVE) { 335 this._resetColWidth(); 336 } 337 } 338 }; 339 340 ZmMailMsgListView.prototype._initHeaders = 341 function() { 342 343 ZmMailListView.prototype._initHeaders.apply(this, arguments); 344 if (this._mode == ZmId.VIEW_CONV) { 345 this._headerInit[ZmItem.F_SUBJECT] = {text:ZmMsg.message, noRemove:true, resizeable:true}; 346 } 347 }; 348 349 ZmMailMsgListView.prototype._getHeaderToolTip = 350 function(field, itemIdx) { 351 if (field == ZmItem.F_SUBJECT && this._mode == ZmId.VIEW_CONV) { 352 return ZmMsg.message; 353 } 354 else { 355 return ZmMailListView.prototype._getHeaderToolTip.apply(this, arguments); 356 } 357 }; 358 359 ZmMailMsgListView.prototype._getSingleColumnSortFields = 360 function() { 361 return (this._mode == ZmId.VIEW_CONV) ? ZmMailMsgListView.SINGLE_COLUMN_SORT_CV : ZmMailListView.SINGLE_COLUMN_SORT; 362 }; 363 364 ZmMailMsgListView.prototype._sortColumn = 365 function(columnItem, bSortAsc, callback) { 366 367 // call base class to save new sorting pref 368 ZmMailListView.prototype._sortColumn.call(this, columnItem, bSortAsc); 369 370 var query; 371 var list = this.getList(); 372 if (this._columnHasCustomQuery(columnItem)) { 373 query = this._getSearchForSort(columnItem._sortable, this._controller); 374 } 375 else if (list && list.size() > 1 && this._sortByString) { 376 query = this._controller.getSearchString(); 377 } 378 379 var queryHint = this._controller.getSearchStringHint(); 380 381 if (query || queryHint) { 382 var params = { 383 query: query, 384 queryHint: queryHint, 385 sortBy: this._sortByString, 386 userInitiated: this._controller._currentSearch.userInitiated, 387 sessionId: this._controller._currentSearch.sessionId 388 } 389 if (this._mode == ZmId.VIEW_CONV) { 390 var conv = this._controller.getConv(); 391 if (conv) { 392 var respCallback = new AjxCallback(this, this._handleResponseSortColumn, [conv, columnItem, this._controller, callback]); 393 conv.load(params, respCallback); 394 } 395 } else { 396 params.types = [ZmItem.MSG]; 397 params.limit = this.getLimit(); 398 params.callback = callback; 399 appCtxt.getSearchController().search(params); 400 } 401 } 402 }; 403 404 ZmMailMsgListView.prototype._handleResponseSortColumn = 405 function(conv, columnItem, controller, callback, result) { 406 var searchResult = result.getResponse(); 407 var list = searchResult.getResults(ZmItem.MSG); 408 controller.setList(list); // set the new list returned 409 controller._activeSearch = searchResult; 410 this.offset = 0; 411 this.set(conv.msgs, columnItem); 412 this.setSelection(conv.getFirstHotMsg({offset:this.offset, limit:this.getLimit(this.offset)})); 413 if (callback instanceof AjxCallback) 414 callback.run(); 415 }; 416 417 ZmMailMsgListView.prototype._getParentForColResize = 418 function() { 419 return this.parent; 420 }; 421