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