1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 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 reminder controller to manage the reminder dialog and status area.
 26  * @class
 27  *
 28  * This controller uses the following timed actions:
 29  * <ol>
 30  * <li>one for refreshing our "cache" of upcoming appts to notify on</li>
 31  * <li>one for when to next popup the reminder dialog. 
 32  *    by default, next appt start time minus lead time pref (i..e, 5 minutes before).
 33  *    but, also could be controlled by snooze prefs.</li>
 34  * </ol>
 35  * 
 36  * @param	{ZmCalViewController}		calController		the controller
 37  * 
 38  */
 39 ZmReminderController = function(calController, apptType) {
 40 	this._calController = calController;
 41     this._apptType = apptType;
 42 	this._apptState = {};	// keyed on appt.getUniqueId(true)
 43     this._cacheMap = {};    
 44 	this._cachedAppts = new AjxVector(); // set of appts in cache from refresh
 45 	this._activeAppts = new AjxVector(); // set of appts we are actively reminding on
 46 	this._oldAppts = new AjxVector(); // set of appts which are olde and needs silent dismiss    
 47 	this._housekeepingTimedAction = new AjxTimedAction(this, this._housekeepingAction);
 48 	this._refreshTimedAction = new AjxTimedAction(this, this.refresh);
 49 };
 50 
 51 ZmReminderController.prototype.constructor = ZmReminderController;
 52 
 53 /**
 54  * Defines the "active" reminder state.
 55  */
 56 ZmReminderController._STATE_ACTIVE = 1; // appt was in reminder, never dismissed
 57 /**
 58  * Defines the "dismissed" reminder state.
 59  */
 60 ZmReminderController._STATE_DISMISSED = 2; // appt was in reminder, and was dismissed
 61 /**
 62  * Defines the "snoozed" reminder state.
 63  */
 64 ZmReminderController._STATE_SNOOZED = 3; // appt was in reminder, and was snoozed
 65 
 66 ZmReminderController._CACHE_RANGE = 24; // range of appts to grab 24 hours (-1, +23)
 67 ZmReminderController._CACHE_REFRESH = 16; // when to grab another range
 68 
 69 ZmReminderController.prototype.toString =
 70 function() {
 71 	return "ZmReminderController";
 72 };
 73 
 74 /**
 75  * called when: (1) app first loads, (2) on refresh blocks, (3) after appt cache is cleared. Our
 76  * _apptState info will keep us from popping up the same appt again if we aren't supposed to
 77  * (at least for the duration of the app)
 78  * 
 79  * @private
 80  */
 81 ZmReminderController.prototype.refresh =
 82 function(retryCount) {
 83 	this._searchTimeRange = this.getSearchTimeRange();
 84     DBG.println(AjxDebug.DBG1, "reminder search time range: " + this._searchTimeRange.start + " to " + this._searchTimeRange.end);
 85 
 86 	try {
 87 		var params = this.getRefreshParams();
 88 	} catch(e) {
 89 		if (retryCount == null && retryCount != 0) {
 90 			retryCount = 3; //retry 3 times before giving up.
 91 		}
 92 		//bug 76771 if there is a exception retry after 1 sec
 93 		if (retryCount) {
 94 			setTimeout(this.refresh.bind(this, --retryCount), 1000);
 95 			return;
 96 		}
 97 		DBG.println(AjxDebug.DBG1, "Too many failures to get refresh params. Giving up.");
 98 		return;
 99 	}
100 	this._calController.getApptSummaries(params);
101 
102 	// cancel outstanding refresh, since we are doing one now, and re-schedule a new one
103 	if (this._refreshActionId) {
104 		AjxTimedAction.cancelAction(this._refreshActionId);
105 	}
106 	DBG.println(AjxDebug.DBG1, "reminder refresh");
107 	this._refreshActionId = AjxTimedAction.scheduleAction(this._refreshTimedAction, (AjxDateUtil.MSEC_PER_HOUR * ZmReminderController._CACHE_REFRESH));
108 };
109 
110 /**
111  * Gets the search time range.
112  * 
113  * @return	{Hash}	a hash of parameters
114  */
115 ZmReminderController.prototype.getSearchTimeRange =
116 function() {
117 	var endOfDay = new Date();
118 	endOfDay.setHours(23,59,59,999);
119 
120 	//grab a week's appt backwards
121 	var end = new Date(endOfDay.getTime());
122 	endOfDay.setDate(endOfDay.getDate()-7);
123 
124 	var start = endOfDay;
125 	start.setHours(0,0,0, 0);
126 
127 	return { start: start.getTime(), end: end.getTime() };
128 };
129 
130 ZmReminderController.prototype.getRefreshParams =
131 function() {
132 	
133 	var timeRange = this.getSearchTimeRange();
134 	return {
135 		start: timeRange.start,
136 		end: timeRange.end,
137 		fanoutAllDay: false,
138 		folderIds: this._apptType ==
139             "appt" ? this._calController.getReminderCalendarFolderIds() :
140                      this._calController.getCheckedCalendarFolderIds(true),
141 		callback: (new AjxCallback(this, this._refreshCallback)),
142 		includeReminders: true
143 	};
144 };
145 
146 ZmReminderController.prototype._cancelRefreshAction =
147 function() {
148 	if (this._refreshActionId) {
149 		AjxTimedAction.cancelAction(this._refreshActionId);
150 		delete this._refreshActionId;
151 	}
152 };
153 
154 ZmReminderController.prototype._cancelHousekeepingAction =
155 function() {
156 	if (this._houseKeepingActionId) {
157 		AjxTimedAction.cancelAction(this._housekeepingActionId);
158 		delete this._houseKeepingActionId;
159 	}
160 };
161 
162 ZmReminderController.prototype._scheduleHouseKeepingAction =
163 function() {
164 	this._cancelHousekeepingAction(); //cancel to be on safe side against race condition when 2 will be runing instead of one.
165 	this._housekeepingActionId = AjxTimedAction.scheduleAction(this._housekeepingTimedAction, 60 * 1000);
166 };
167 
168 /**
169  * called after we get upcoming appts from server. Save list,
170  * and call housekeeping.
171  *
172  * @private
173  */
174 ZmReminderController.prototype._refreshCallback =
175 function(list) {
176 	if (this._refreshDelay > 0) {
177 		AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._refreshCallback, [list]), this._refreshDelay);
178 		this._refreshDelay = 0;
179 		return;
180 	}
181 
182 	if (list instanceof ZmCsfeException) {
183 		this._calController._handleError(list, new AjxCallback(this, this._maintErrorHandler));
184 		return;
185 	}
186 
187 	var newList = new AjxVector();
188 	this._cacheMap = {};
189 
190 	// filter recurring appt instances, the alarmData is common for all the instances
191 	var size = list.size();
192 	for (var i = 0; i < size; i++) {
193 		var appt = list.get(i);
194 		var id = appt.id;
195         var hasAlarm = appt.recurring ? appt.isAlarmInstance() : appt.hasAlarmData();
196 		if (hasAlarm) {
197             var alarmData = appt.getAlarmData();
198             alarmData = (alarmData && alarmData.length > 0) ? alarmData[0] : {};
199             AjxDebug.println(AjxDebug.REMINDER, appt.name + " :: " + appt.startDate + " :: " + appt.endDate + " :: " + appt.recurring + " :: " + appt.isException + " :: " + alarmData.nextAlarm + " :: " + alarmData.alarmInstStart);
200 			if (!this._cacheMap[id]) {
201 				this._cacheMap[id] = true;
202 				newList.add(appt);
203 			}
204 		}
205 	}
206 
207 	this._cachedAppts = newList.clone();
208 	this._cachedAppts.sort(ZmCalBaseItem.compareByTimeAndDuration);
209 	this._activeAppts.removeAll();
210 
211 	// cancel outstanding timed action and update now...
212 	this._cancelHousekeepingAction();
213 	this._housekeepingAction();
214 };
215 
216 ZmReminderController.prototype.updateCache =
217 function(list) {
218 	if (!list) { return; }
219 
220 	if (!this._cachedAppts) {
221 		this._cachedAppts = new AjxVector();
222 	}
223 
224     AjxDebug.println(AjxDebug.REMINDER, "updating reminder cache...");
225 	var srchRange = this.getSearchTimeRange();
226 	var count = 0;
227 
228 	// filter recurring appt instances, the alarmData is common for all the instances
229 	var size = list.size();
230 	for (var i = 0; i < size; i++) {
231 		var appt = list.get(i);
232 		var id = appt.id;
233 		if(appt.hasAlarmData() && !this._cacheMap[id] && appt.isStartInRange(srchRange.start, srchRange.end)) {
234 			this._cacheMap[id] = true;
235 			this._cachedAppts.add(appt);
236 			count++;
237 		}
238 	}
239 
240     AjxDebug.println(AjxDebug.REMINDER, "new appts added to reminder cache :" + count);
241 };
242 
243 ZmReminderController.prototype.isApptSnoozed =
244 function(uid) {
245 	return (this._apptState[uid] == ZmReminderController._STATE_SNOOZED);
246 };
247 
248 /**
249  * go through list to see if we should add any cachedAppts to activeAppts and
250  * popup the dialog or not.
251  * 
252  * @private
253  */
254 ZmReminderController.prototype._housekeepingAction =
255 function() {
256     AjxDebug.println(AjxDebug.REMINDER, "reminder house keeping action...");
257 	var rd = this.getReminderDialog();
258 	if (ZmCsfeCommand.noAuth) {
259         AjxDebug.println(AjxDebug.REMINDER, "reminder check: no auth token, bailing");
260 		if (rd && rd.isPoppedUp()) {
261 			rd.popdown();
262 		}
263 		return;
264 	}
265 
266 	if (this._searchTimeRange) {
267 		var newTimeRange = this.getSearchTimeRange();
268 		var diff = newTimeRange.end - this._searchTimeRange.end;
269 		if (diff > AjxDateUtil.MSEC_PER_HOUR) {
270             AjxDebug.println(AjxDebug.REMINDER, "time elapsed - refreshing reminder cache");
271 			this._searchTimeRange = null;
272 			this.refresh();
273 			return;
274 		}
275 	}
276 
277 	var cachedSize = this._cachedAppts.size();
278 	var activeSize = this._activeAppts.size();
279 	if (cachedSize == 0 && activeSize == 0) {
280         AjxDebug.println(AjxDebug.REMINDER, "no appts - empty cached and active list");
281 		this._scheduleHouseKeepingAction();
282 		return;
283 	}
284 
285 	var numNotify = 0;
286 	var toRemove = [];
287 
288 	for (var i=0; i < cachedSize; i++) {
289 		var appt = this._cachedAppts.get(i);
290 
291 		if (!appt || appt.ptst == ZmCalBaseItem.PSTATUS_DECLINED) {
292 			toRemove.push(appt);
293 		} else if (appt.isAlarmInRange()) {
294 			var uid = appt.getUniqueId(true);
295 			var state = this._apptState[uid];
296 			var addToActiveList = false;
297 			if (state == ZmReminderController._STATE_DISMISSED) {
298 				// just remove themn
299 			} else if (state == ZmReminderController._STATE_ACTIVE) {
300 				addToActiveList = true;
301 			} else {
302 				// we need to notify on this one
303 				numNotify++;
304 				addToActiveList = true;
305 				this._apptState[uid] = ZmReminderController._STATE_ACTIVE;
306 			}
307 
308 			if (addToActiveList) {
309 				toRemove.push(appt);
310 				if (!appCtxt.get(ZmSetting.CAL_SHOW_PAST_DUE_REMINDERS) && appt.isAlarmOld()) {
311 					numNotify--;
312 					this._oldAppts.add(appt);
313 				} else {
314 					this._activeAppts.add(appt);
315 				}
316 			}
317 		}
318 	}
319 
320 	// remove any appts in cachedAppts that are no longer supposed to be in there	
321 	// need to do this here so we don't screw up iteration above
322 	for (var i = 0; i < toRemove.length; i++) {
323 		this._cachedAppts.remove(toRemove[i]);
324 	}
325 
326 	// if we have any to notify on, do it
327 	if (numNotify || rd.isPoppedUp()) {
328 		if (this._activeAppts.size() == 0 && rd.isPoppedUp()) {
329             AjxDebug.println(AjxDebug.REMINDER, "popping down reminder dialog");
330             rd.popdown();
331 		} else {
332             AjxDebug.println(AjxDebug.REMINDER, "initializing reminder dialog");
333 			rd.initialize(this._activeAppts);
334 			if (!rd.isPoppedUp()) rd.popup();
335 		}
336 	}
337 
338     AjxDebug.println(AjxDebug.REMINDER, "no of appts active:" + this._activeAppts.size() + ", no of appts cached:" + cachedSize);
339 
340 	if (this._oldAppts.size() > 0) {
341 		this.dismissAppt(this._oldAppts, new AjxCallback(this, this._silentDismissCallback));
342 	}
343 
344 	// need to schedule housekeeping callback, ideally right before next _cachedAppt start time - lead,
345 	// for now just check once a minute...
346 	this._scheduleHouseKeepingAction();
347 };
348 
349 ZmReminderController.prototype._silentDismissCallback =
350 function(list) {
351 	var size = list.size();
352 	for (var i = 0; i < size; i++) {
353 		var appt = list.get(i);
354 		if (appt && appt.hasAlarmData()) {
355 			if(appt.isAlarmInRange()) {
356 				this._activeAppts.add(appt);
357 			}
358 		}
359 	}
360 	this._oldAppts.removeAll();
361 
362 	// cancel outstanding timed action and update now...
363 	this._cancelHousekeepingAction();
364 	this._housekeepingAction();
365 };
366 
367 /**
368  * Dismisses an appointment. This method is called when
369  * an appointment (individually or as part of "dismiss all") is removed from reminders.
370  * 
371  * @param	{AjxVector|Array}	list	a list of {@link ZmAppt} objects
372  * @param	{AjxCallback}		callback		a callback
373  */
374 ZmReminderController.prototype.dismissAppt =
375 function(list, callback) {
376 	if (!(list instanceof AjxVector)) {
377 		list = AjxVector.fromArray((list instanceof Array)? list: [list]);
378 	}
379 
380 	for (var i=0; i<list.size(); i++) {
381 		var appt = list.get(i);
382 		this._apptState[appt.getUniqueId(true)] = ZmReminderController._STATE_DISMISSED;
383 		this._activeAppts.remove(appt);
384 	}
385 
386 	this.dismissApptRequest(list, callback);
387 };
388 
389 /**
390  * Snoozes the appointments.
391  * 
392  * @param	{AjxVector}	appts	a list of {@link ZmAppt} objects
393  * @return	{Array}	an array of snoozed apt ids
394  */
395 ZmReminderController.prototype.snoozeAppt =
396 function(appts) {
397 	appts = AjxUtil.toArray(appts);
398 
399 	var snoozedIds = [];
400 	var appt;
401 	var uid;
402 	for (var i = 0; i < appts.length; i++) {
403 		appt = appts[i];
404 		uid = appt.getUniqueId(true);
405 		this._apptState[uid] = ZmReminderController._STATE_SNOOZED;
406 		snoozedIds.push(uid);
407 		this._activeAppts.remove(appt);
408 		this._cachedAppts.add(appt);
409 	}
410 	return snoozedIds;
411 };
412 
413 ZmReminderController.prototype.dismissApptRequest = 
414 function(list, callback) {
415 
416 
417     //<DismissCalendarItemAlarmRequest>
418     //    <appt|task id="cal item id" dismissedAt="time alarm was dismissed, in millis"/>+
419     //</DismissCalendarItemAlarmRequest>
420     var jsonObj = {DismissCalendarItemAlarmRequest:{_jsns:"urn:zimbraMail"}};
421     var request = jsonObj.DismissCalendarItemAlarmRequest;
422 
423     var appts = [];
424     var dismissedAt = (new Date()).getTime();
425     for (var i = 0; i < list.size(); i++) {
426         var appt = list.get(i);
427         var apptInfo = { id: appt.id, dismissedAt: dismissedAt};
428         appts.push(apptInfo)
429     }
430     request[this._apptType] = appts;
431 
432     var respCallback    = this._handleDismissAppt.bind(this, list, callback);
433     var offlineCallback = this._handleOfflineReminderAction.bind(this,  jsonObj, list, true);
434     var errorCallback   = this._handleErrorDismissAppt.bind(this, list, callback);
435     var params =
436         {jsonObj:         jsonObj,
437          asyncMode:       true,
438          callback:        respCallback,
439          offlineCallback: offlineCallback,
440          errorCallback:   errorCallback
441         };
442     appCtxt.getAppController().sendRequest(params);
443 
444 	return true;
445 };
446 
447 ZmReminderController.prototype.setAlarmData =
448 function (soapDoc, request, params) {
449 	var alarmData = soapDoc.set("alarmData", null, request);
450 	alarmData.setAttribute("");
451 };
452 
453 ZmReminderController.prototype._handleDismissAppt =
454 function(list, callback, result) {
455 	if (result.isException()) { return; }
456 
457 	var response = result.getResponse();
458 	var dismissResponse = response.DismissCalendarItemAlarmResponse;
459 	var appts = dismissResponse ? dismissResponse.appt : null;
460 	if (!appts) { return; }
461 
462     this._updateApptAlarmData(list, appts);
463 
464 	if (callback) {
465 		callback.run(list);
466 	}
467 };
468 
469 ZmReminderController.prototype._handleErrorDismissAppt =
470 function(list, callback, response) {
471 };
472 
473 
474 ZmReminderController.prototype._updateApptAlarmData =
475 function(apptList, responseAppts) {
476     var updateData = {};
477     for (var i = 0; i < responseAppts.length; i++) {
478         var appt = responseAppts[i];
479         if (appt && appt.calItemId) {
480             updateData[appt.calItemId] = appt.alarmData ? appt.alarmData : {};
481         }
482     }
483 
484     var size = apptList.size();
485     for (var i = 0; i < size; i++) {
486         var appt = apptList.get(i);
487         if (appt) {
488             if (updateData[appt.id]) {
489                 appt.alarmData = (updateData[appt.id] != {}) ? updateData[appt.id] : null;
490             }
491         }
492     }
493 };
494 
495 /**
496  * Gets the reminder dialog.
497  * 
498  * @return	{ZmReminderDialog}	the dialog
499  */
500 ZmReminderController.prototype.getReminderDialog =
501 function() {
502 	if (this._reminderDialog == null) {
503 		this._reminderDialog = new ZmReminderDialog(appCtxt.getShell(), this, this._calController, this._apptType);
504 	}
505 	return this._reminderDialog;
506 };
507 
508 
509 ZmReminderController.prototype._snoozeApptAction =
510 function(apptArray, snoozeMinutes, beforeAppt) {
511 
512 	var apptList = AjxVector.fromArray(apptArray);
513 
514     var chosenSnoozeMilliseconds = snoozeMinutes * 60 * 1000;
515     var added = false;
516 
517     //     <SnoozeCalendarItemAlarmRequest xmlns="urn:zimbraMail">
518     //        <appt id="573" until="1387833974851"/>
519     //        <appt id="601" until="1387833974851"/>
520     //    </SnoozeCalendarItemAlarmRequest>
521 
522     var jsonObj = {SnoozeCalendarItemAlarmRequest:{_jsns:"urn:zimbraMail"}};
523     var request = jsonObj.SnoozeCalendarItemAlarmRequest;
524 
525     var appts = [];
526     if (beforeAppt) {
527         // Using a before time, relative to the start of each appointment
528         if (!this._beforeProcessor) {
529             this._beforeProcessor = new ZmSnoozeBeforeProcessor(this._apptType);
530         }
531         added = this._beforeProcessor.execute(apptList, chosenSnoozeMilliseconds, appts);
532     } else {
533         // using a fixed untilTime for all appts
534         added = apptList.size() > 0;
535         // untilTime determines next alarm time, based on the option user has chosen in snooze reminder pop up .
536         var untilTime = (new Date()).getTime() + chosenSnoozeMilliseconds;
537         for (var i = 0; i < apptList.size(); i++) {
538             var appt = apptList.get(i);
539             if (chosenSnoozeMilliseconds === 0) { // at time of event, making it to appt start time .
540                 untilTime = appt.getStartTime();
541             }
542             var apptInfo = { id: appt.id, until: untilTime};
543             appts.push(apptInfo)
544         }
545     }
546     request[this._apptType] = appts;
547 
548     var respCallback    = this._handleResponseSnoozeAction.bind(this, apptList, snoozeMinutes);
549     var offlineCallback = this._handleOfflineReminderAction.bind(this,  jsonObj, apptList, false);
550     var errorCallback   = this._handleErrorResponseSnoozeAction.bind(this);
551     var ac = window.parentAppCtxt || window.appCtxt;
552     ac.getRequestMgr().sendRequest(
553         {jsonObj:         jsonObj,
554          asyncMode:       true,
555          callback:        respCallback,
556          offlineCallback: offlineCallback,
557          errorCallback:   errorCallback});
558 
559 };
560 
561 
562 ZmReminderController.prototype._handleResponseSnoozeAction =
563 function(apptList, snoozeMinutes, result) {
564     if (result.isException()) { return; }
565 
566 	var response = result.getResponse();
567 	var snoozeResponse = response.SnoozeCalendarItemAlarmResponse;
568 	var appts = snoozeResponse ? snoozeResponse[this._apptType] : null;
569 	if (!appts) { return; }
570 
571     this._updateApptAlarmData(apptList, appts);
572 
573     if (snoozeMinutes == 1) {
574 	    // cancel outstanding timed action and update now...
575 		// I'm not sure why this is here but I suspect to prevent some race condition.
576 		this._cancelHousekeepingAction();
577 		//however calling _housekeepingAction immediately caused some other race condition issues. so I just schedule it again.
578 		this._scheduleHouseKeepingAction();
579     }
580 };
581 ZmReminderController.prototype._handleErrorResponseSnoozeAction =
582 function(result) {
583     //appCtxt.getAppController().popupErrorDialog(ZmMsg.reminderSnoozeError, result.msg, null, true);
584 };
585 
586 ZmReminderController.prototype._handleOfflineReminderAction =
587 function(jsonObj, apptList, dismiss) {
588     var jsonObjCopy = $.extend(true, {}, jsonObj);  //Always clone the object.  ?? Needed here ??
589     var methodName = dismiss ? "DismissCalendarItemAlarmRequest" : "SnoozeCalendarItemAlarmRequest";
590     jsonObjCopy.methodName = methodName;
591     // Modify the id to thwart ZmOffline._handleResponseSendOfflineRequest, which sends a DELETE
592     // notification for the id (which impacts here if there is a single id).
593     jsonObjCopy.id = "C" + this._createSendRequestKey(apptList);
594 
595     var value = {
596         update:          true,
597         methodName:      methodName,
598         id:              jsonObjCopy.id,
599         value:           jsonObjCopy
600     };
601 
602     var callback = this._handleOfflineReminderDBCallback.bind(this, jsonObjCopy, apptList, dismiss);
603     ZmOfflineDB.setItemInRequestQueue(value, callback);
604 };
605 
606 ZmReminderController.prototype._createSendRequestKey =
607 function(apptList) {
608     var keyPart = [];
609     var appt;
610     for (var i = 0; i < apptList.size(); i++) {
611         appt = apptList.get(i);
612         if (appt) {
613             keyPart.push(apptList.get(i).invId);
614         }
615     }
616     return keyPart.join(":");
617 }
618 
619 ZmReminderController.prototype._handleOfflineReminderDBCallback =
620 function(jsonObj, apptList, dismiss) {
621     // Successfully stored the snooze request in the SendRequest queue, update the db items and flush the apptCache
622 
623     var request = jsonObj[jsonObj.methodName];
624     var appts   = request[this._apptType];
625 
626     var callback;
627     var appt;
628     var apptCache = this._calController.getApptCache();
629     for (var i = 0; i < apptList.size(); i++) {
630         appt = apptList.get(i);
631         if (appt) {
632             // AWKWARD, but with indexedDB there's no way to specify a set of ids to read. So for the moment
633             // (hopefully not too many appts triggered at once) - read one, modify, write it to the Calendar Obj store.
634             // When done with each one, invoke a callback to update the reminder appt in memory.
635             var apptInfo = appts[i];
636             callback = this._updateOfflineAlarmCallback.bind(this, appt, dismiss, apptInfo.until);
637             // Set up null data and replacement data for snooze.  apptInfo.until will be undefined for dismiss,
638             // but we remove the alarm data for dismiss anyway
639             var nullData = [];
640             nullData.push({ nextAlarm: apptInfo.until});
641             apptCache.updateOfflineAppt(appt.invId, "alarmData.0.nextAlarm", apptInfo.until, nullData, callback);
642         }
643     }
644 }
645 
646 // Final step in the Reminder Snooze: update in memory.  I believe alarmData[0].nextAlarm is all that needs to
647 // be modified, try for now.   The online _updateApptAlarmData replaces the entire alarmData with the Snooze response,
648 // but all we have is the nextAlarm value.
649 ZmReminderController.prototype._updateOfflineAlarmCallback =
650 function(appt, dismiss, origValue, field, value) {
651     if (dismiss) {
652         appt.alarmData = null;
653     } else {
654         appt.alarmData[0].nextAlarm = origValue;
655     }
656 }