summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/imip-bar.js
blob: f40e4cce22a526dee37b98ba9a428304903887aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* import-globals-from ../../../mail/base/content/msgHdrView.js */
/* import-globals-from item-editing/calendar-item-editing.js */

var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");

/**
 * Provides shortcuts to set label and collapsed attribute of imip-bar node.
 */
const imipBar = {
  get bar() {
    return document.querySelector(".calendar-notification-bar");
  },
  get label() {
    return this.bar.querySelector(".msgNotificationBarText").textContent;
  },
  set label(val) {
    this.bar.querySelector(".msgNotificationBarText").textContent = val;
  },
  get collapsed() {
    return this.bar.collapsed;
  },
  set collapsed(val) {
    this.bar.collapsed = val;
  },
};

/**
 * This bar lives inside the message window.
 * Its lifetime is the lifetime of the main thunderbird message window.
 */
var calImipBar = {
  actionFunc: null,
  itipItem: null,
  foundItems: null,
  loadingItipItem: null,

  /**
   * Thunderbird Message listener interface, hide the bar before we begin
   */
  onStartHeaders() {
    calImipBar.resetBar();
  },

  /**
   * Thunderbird Message listener interface
   */
  onEndHeaders() {},

  /**
   * Load Handler called to initialize the imip bar
   * NOTE: This function is called without a valid this-context!
   */
  load() {
    // Add a listener to gMessageListeners defined in msgHdrView.js
    gMessageListeners.push(calImipBar);

    // Hook into this event to hide the message header pane otherwise, the imip
    // bar will still be shown when changing folders.
    document.getElementById("msgHeaderView").addEventListener("message-header-pane-hidden", () => {
      calImipBar.resetBar();
    });

    // Set up our observers
    Services.obs.addObserver(calImipBar, "onItipItemCreation");
  },

  /**
   * Unload handler to clean up after the imip bar
   * NOTE: This function is called without a valid this-context!
   */
  unload() {
    removeEventListener("messagepane-loaded", calImipBar.load, true);
    removeEventListener("messagepane-unloaded", calImipBar.unload, true);

    calImipBar.resetBar();
    Services.obs.removeObserver(calImipBar, "onItipItemCreation");
  },

  showImipBar(itipItem, imipMethod) {
    if (!Services.prefs.getBoolPref("calendar.itip.showImipBar", true)) {
      // Do not show the imip bar if the user has opted out of seeing it.
      return;
    }

    // How we get here:
    //
    // 1. `mime_find_class` finds the `CalMimeConverter` class matches the
    //      content-type of an attachment.
    // 2. `mime_find_class` extracts the method from the attachments headers
    //      and sets `imipMethod` on the message's mail channel.
    // 3. `CalMimeConverter` is called to generate the HTML in the message.
    //      It initialises `itipItem` and sets it on the channel.
    // 4. msgHdrView.js gathers `itipItem` and `imipMethod` from the channel.

    cal.itip.initItemFromMsgData(itipItem, imipMethod, gMessage);

    if (Services.prefs.getBoolPref("calendar.itip.newInvitationDisplay")) {
      window.dispatchEvent(new CustomEvent("onItipItemCreation", { detail: itipItem }));
    }

    imipBar.collapsed = false;
    imipBar.label = cal.itip.getMethodText(itipItem.receivedMethod);

    // This is triggered by CalMimeConverter.convertToHTML, so we know that
    // the message is not yet loaded with the invite. Keep track of this for
    // displayModifications.
    calImipBar.overlayLoaded = false;

    if (!Services.prefs.getBoolPref("calendar.itip.newInvitationDisplay")) {
      calImipBar.overlayLoaded = true;

      let doc = document.getElementById("messagepane").contentDocument;
      let details = doc.getElementById("imipHTMLDetails");
      let msgbody = doc.querySelector("div.moz-text-html");
      if (!msgbody) {
        details.setAttribute("open", "open");
      } else {
        // The HTML representation can contain important notes.

        // For consistent appearance, move the generated meeting details first.
        msgbody.prepend(details);

        if (Services.prefs.getBoolPref("calendar.itip.imipDetailsOpen", true)) {
          // Expand the iMIP details if pref says so.
          details.setAttribute("open", "open");
        }
      }
    }
    // NOTE: processItipItem may call setupOptions asynchronously because the
    // getItem method it triggers is async for *some* calendars. In theory,
    // this could complete after a different item has been loaded, so we
    // record the loading item now, and early exit setupOptions if the loading
    // item has since changed.
    // NOTE: loadingItipItem is reset on changing messages in resetBar.
    calImipBar.loadingItipItem = itipItem;
    cal.itip.processItipItem(itipItem, calImipBar.setupOptions);

    // NOTE: At this point we essentially have two parallel async operations:
    // 1. Load the CalMimeConverter.convertToHTML into the #messagepane and
    //    then set overlayLoaded to true.
    // 2. Find a corresponding event through processItipItem and then call
    //    setupOptions. Note that processItipItem may be instantaneous for
    //    some calendars.
    //
    // In the mean time, if we switch messages, then loadingItipItem will be
    // set to some other value: either another item, or null by resetBar.
    //
    // Once setupOptions is called, if the message has since changed we do
    // nothing and exit. Otherwise, if we found a corresponding item in the
    // calendar, we proceed to displayModifications. If overlayLoaded is true
    // we update the #messagepane immediately, otherwise we update it on
    // DOMContentLoaded, which has not yet happened.
  },

  /**
   * Hide the imip bar and reset the itip item.
   */
  resetBar() {
    imipBar.collapsed = true;
    calImipBar.resetButtons();

    // Clear our iMIP/iTIP stuff so it doesn't contain stale information.
    cal.itip.cleanupItipItem(calImipBar.itipItem);
    calImipBar.itipItem = null;
    calImipBar.loadingItipItem = null;
  },

  /**
   * Resets all buttons and its menuitems, all buttons are hidden thereafter
   */
  resetButtons() {
    let buttons = calImipBar.getButtons();
    for (let button of buttons) {
      button.setAttribute("hidden", "true");
      for (let item of calImipBar.getMenuItems(button)) {
        item.removeAttribute("hidden");
      }
    }
  },

  /**
   * Provides a list of all available buttons
   */
  getButtons() {
    let toolbarbuttons = document
      .getElementById("imip-view-toolbar")
      .getElementsByTagName("toolbarbutton");
    return Array.from(toolbarbuttons);
  },

  /**
   * Provides a list of available menuitems of a button
   *
   * @param aButton        button node
   */
  getMenuItems(aButton) {
    let items = [];
    let mitems = aButton.getElementsByTagName("menuitem");
    if (mitems != null && mitems.length > 0) {
      for (let mitem of mitems) {
        items.push(mitem);
      }
    }
    return items;
  },

  /**
   * Checks and converts button types based on available menuitems of the buttons
   * to avoid dropdowns which are empty or only replicating the default button action
   * Should be called once the buttons are set up
   */
  conformButtonType() {
    // check only needed on visible and not simple buttons
    let buttons = calImipBar
      .getButtons()
      .filter(aElement => aElement.hasAttribute("type") && !aElement.hidden);
    // change button if appropriate
    for (let button of buttons) {
      let items = calImipBar.getMenuItems(button).filter(aItem => !aItem.hidden);
      if (button.type == "menu" && items.length == 0) {
        // hide non functional buttons
        button.hidden = true;
      } else if (button.type == "menu") {
        if (
          items.length == 0 ||
          (items.length == 1 &&
            button.hasAttribute("oncommand") &&
            items[0].hasAttribute("oncommand") &&
            button.getAttribute("oncommand").endsWith(items[0].getAttribute("oncommand")))
        ) {
          // convert to simple button
          button.removeAttribute("type");
        }
      }
    }
  },

  /**
   * This is our callback function that is called each time the itip bar UI needs updating.
   * NOTE: This function is called without a valid this-context!
   *
   * @param itipItem      The iTIP item to set up for
   * @param rc            The status code from processing
   * @param actionFunc    The action function called for execution
   * @param foundItems    An array of items found while searching for the item
   *                      in subscribed calendars
   */
  setupOptions(itipItem, rc, actionFunc, foundItems) {
    if (itipItem !== calImipBar.loadingItipItem) {
      // The given itipItem refers to an earlier displayed message.
      return;
    }

    let data = cal.itip.getOptionsText(itipItem, rc, actionFunc, foundItems);

    if (Components.isSuccessCode(rc)) {
      calImipBar.itipItem = itipItem;
      calImipBar.actionFunc = actionFunc;
      calImipBar.foundItems = foundItems;
    }

    // We need this to determine whether this is an outgoing or incoming message because
    // Thunderbird doesn't provide a distinct flag on message level to do so. Relying on
    // folder flags only may lead to false positives.
    let isOutgoing = function (aMsgHdr) {
      if (!aMsgHdr) {
        return false;
      }
      let author = aMsgHdr.mime2DecodedAuthor;
      let isSentFolder = aMsgHdr.folder && aMsgHdr.folder.flags & Ci.nsMsgFolderFlags.SentMail;
      if (author && isSentFolder) {
        for (let identity of MailServices.accounts.allIdentities) {
          if (author.includes(identity.email) && !identity.fccReplyFollowsParent) {
            return true;
          }
        }
      }
      return false;
    };

    // We override the bar label for sent out invitations and in case the event does not exist
    // anymore, we also clear the buttons if any to avoid e.g. accept/decline buttons
    if (isOutgoing(gMessage)) {
      if (calImipBar.foundItems && calImipBar.foundItems[0]) {
        data.label = cal.l10n.getLtnString("imipBarSentText");
      } else {
        data = {
          label: cal.l10n.getLtnString("imipBarSentButRemovedText"),
          buttons: [],
          hideMenuItems: [],
          hideItems: [],
          showItems: [],
        };
      }
    }

    imipBar.label = data.label;
    // let's reset all buttons first
    calImipBar.resetButtons();
    // now we update the visible items - buttons are hidden by default
    // apart from that, we need this to adapt the accept button depending on
    // whether three or four button style is present
    for (let item of data.hideItems) {
      document.getElementById(item).setAttribute("hidden", "true");
    }
    for (let item of data.showItems) {
      document.getElementById(item).removeAttribute("hidden");
    }
    // adjust button style if necessary
    calImipBar.conformButtonType();

    calImipBar.displayModifications();
  },

  /**
   * Displays changes in case of invitation updates in invitation overlay.
   *
   * NOTE: This should only be called if the invitation is already loaded in the
   * #messagepane, in which case calImipBar.overlayLoaded should be set to true,
   * or is guaranteed to be loaded next in #messagepane.
   */
  displayModifications() {
    if (
      !calImipBar.foundItems ||
      !calImipBar.foundItems[0] ||
      !calImipBar.itipItem ||
      !Services.prefs.getBoolPref("calendar.itip.displayInvitationChanges", false)
    ) {
      return;
    }

    let itipItem = calImipBar.itipItem;
    let foundEvent = calImipBar.foundItems[0];
    let currentEvent = itipItem.getItemList()[0];
    let diff = cal.itip.compare(currentEvent, foundEvent);
    if (diff != 0) {
      let newEvent;
      let oldEvent;

      if (diff == 1) {
        // This is an update to previously accepted invitation.
        oldEvent = foundEvent;
        newEvent = currentEvent;
      } else {
        // This is a copy of a previously sent out invitation or a previous
        // revision of a meanwhile accepted invitation, so we flip the order.
        oldEvent = currentEvent;
        newEvent = foundEvent;
      }

      let browser = document.getElementById("messagepane");
      let doUpdate = () => {
        if (Services.prefs.getBoolPref("calendar.itip.newInvitationDisplay")) {
          return;
        }
        cal.invitation.updateInvitationOverlay(
          browser.contentDocument,
          newEvent,
          itipItem,
          oldEvent
        );
      };
      if (calImipBar.overlayLoaded) {
        // Document is already loaded.
        doUpdate();
      } else {
        // The event is not yet shown. This can happen if setupOptions is called
        // before CalMimeConverter.convertToHTML has finished, or the
        // corresponding HTML string has not yet been loaded.
        // Wait until the event is shown, then immediately update it.
        browser.addEventListener("DOMContentLoaded", doUpdate, { once: true });
      }
    }
  },

  /**
   * Executes an action triggered by an imip bar button
   *
   * @param   {string}  aParticipantStatus  A partstat string as per RfC 5545
   * @param   {string}  aResponse           Either 'AUTO', 'NONE' or 'USER',
   *                                          see calItipItem interface
   * @returns {boolean} true, if the action succeeded
   */
  executeAction(aParticipantStatus, aResponse) {
    return cal.itip.executeAction(
      window,
      aParticipantStatus,
      aResponse,
      calImipBar.actionFunc,
      calImipBar.itipItem,
      calImipBar.foundItems,
      ({ resetButtons, label }) => {
        if (label != undefined) {
          calImipBar.label = label;
        }
        if (resetButtons) {
          calImipBar.resetButtons();
        }
      }
    );
  },

  /**
   * Hide the imip bar in all windows and set a pref to prevent it from being
   * shown again. Called when clicking the imip bar's "do not show..." menu item.
   */
  doNotShowImipBar() {
    Services.prefs.setBoolPref("calendar.itip.showImipBar", false);
    for (let window of Services.ww.getWindowEnumerator()) {
      if (window.calImipBar) {
        window.calImipBar.resetBar();
      }
    }
  },
};

{
  let msgHeaderView = document.getElementById("msgHeaderView");
  if (msgHeaderView && msgHeaderView.loaded) {
    calImipBar.load();
  } else {
    addEventListener("messagepane-loaded", calImipBar.load, true);
  }
}
addEventListener("messagepane-unloaded", calImipBar.unload, true);