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 }