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