Bug 1695039 - Show changes to an invitation email by editing the Document directly rather than replacing it. r=darktrojan

Differential Revision: https://phabricator.services.mozilla.com/D120712

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Henry Wilkes 2021-07-29 10:01:26 +00:00
Родитель e5dd228abb
Коммит 4ad004e95f
5 изменённых файлов: 1404 добавлений и 1030 удалений

Просмотреть файл

@ -39,7 +39,7 @@ var calImipBar = {
actionFunc: null,
itipItem: null,
foundItems: null,
msgOverlay: null,
loadingItipItem: null,
/**
* Thunderbird Message listener interface, hide the bar before we begin
@ -94,22 +94,21 @@ var calImipBar = {
// Do not show the imip bar if the user has opted out of seeing it.
return;
}
// NOTE: Itip item is *about* to be loaded into the #messagepane when this
// callback is triggered by CalMimeConverter.convertToHTML.
let itipItem = null;
let msgOverlay = null;
try {
if (!subject) {
let sinkProps = msgWindow.msgHeaderSink.properties;
// This property was set by CalMimeConverter.jsm.
itipItem = sinkProps.getPropertyAsInterface("itipItem", Ci.calIItipItem);
msgOverlay = sinkProps.getPropertyAsAUTF8String("msgOverlay");
}
} catch (e) {
// This will throw on every message viewed that doesn't have the
// itipItem property set on it. So we eat the errors and move on.
// XXX TODO: Only swallow the errors we need to. Throw all others.
}
if (!itipItem || !msgOverlay || !gMessageDisplay.displayedMessage) {
if (!itipItem || !gMessageDisplay.displayedMessage) {
return;
}
@ -119,9 +118,39 @@ var calImipBar = {
imipBar.collapsed = false;
imipBar.label = cal.itip.getMethodText(itipItem.receivedMethod);
calImipBar.msgOverlay = msgOverlay;
// 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;
document
.getElementById("messagepane")
.addEventListener("DOMContentLoaded", () => (calImipBar.overlayLoaded = true), {
once: true,
});
// 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.
}
},
@ -135,6 +164,7 @@ var calImipBar = {
// Clear our iMIP/iTIP stuff so it doesn't contain stale information.
cal.itip.cleanupItipItem(calImipBar.itipItem);
calImipBar.itipItem = null;
calImipBar.loadingItipItem = null;
},
/**
@ -218,6 +248,11 @@ var calImipBar = {
* 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)) {
@ -276,43 +311,65 @@ var calImipBar = {
}
// adjust button style if necessary
calImipBar.conformButtonType();
calImipBar.displayModifications();
},
/**
* Displays changes in case of invitation updates in invitation overlay
* 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.msgOverlay ||
!msgWindow ||
!calImipBar.foundItems ||
!calImipBar.foundItems[0] ||
!calImipBar.itipItem
!calImipBar.itipItem ||
!Services.prefs.getBoolPref("calendar.itip.displayInvitationChanges", false)
) {
return;
}
let msgOverlay = calImipBar.msgOverlay;
let diff = cal.itip.compare(calImipBar.itipItem.getItemList()[0], calImipBar.foundItems[0]);
// displaying changes is only needed if that is enabled, an item already exists and there are
// differences
if (diff != 0 && Services.prefs.getBoolPref("calendar.itip.displayInvitationChanges", false)) {
let foundOverlay = cal.invitation.createInvitationOverlay(
calImipBar.foundItems[0],
calImipBar.itipItem
);
let serializedOverlay = cal.xml.serializeDOM(foundOverlay);
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
msgOverlay = cal.invitation.compareInvitationOverlay(serializedOverlay, msgOverlay);
// 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 comparison order
msgOverlay = cal.invitation.compareInvitationOverlay(msgOverlay, serializedOverlay);
// 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 = () =>
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 });
}
}
msgWindow.displayHTMLInMessagePane("", msgOverlay, false);
},
/**

Просмотреть файл

@ -66,22 +66,49 @@ var calinvitation = {
return header;
},
_createAddedElement(doc) {
let el = doc.createElement("ins");
el.classList.add("added");
return el;
},
_createRemovedElement(doc) {
let el = doc.createElement("del");
el.classList.add("removed");
return el;
},
/**
* Creates new icon and text label for the given event attendee.
*
* @param {Document} doc - The document the new label will belong to.
* @param {calIAttendee} attendee - The attendee to create the label for.
* @param {calIAttendee[]} attendees - The full list of attendees for the
* @param {calIAttendee[]} attendeeList - The full list of attendees for the
* event.
* @param {calIAttendee} [oldAttendee] - The previous version of this attendee
* for this event.
* @param {calIAttendee[]} [attendeeList] - The previous list of attendees for
* this event. This is not optional if oldAttendee is given.
*
* @return {HTMLDivElement} - The new attendee label.
*/
createAttendeeLabel(doc, attendee, attendees) {
createAttendeeLabel(doc, attendee, attendeeList, oldAttendee, oldAttendeeList) {
let userType = attendee.userType || "INDIVIDUAL";
let role = attendee.role || "REQ-PARTICIPANT";
let partstat = attendee.participationStatus || "NEEDS-ACTION";
let modified =
oldAttendee &&
((oldAttendee.userType || "INDIVIDUAL") != userType ||
(oldAttendee.role || "REQ-PARTICIPANT") != role ||
(oldAttendee.participationStatus || "NEEDS-ACTION") != partstat);
// resolve delegatees/delegators to display also the CN
let del = cal.itip.resolveDelegation(attendee, attendees);
let del = cal.itip.resolveDelegation(attendee, attendeeList);
if (oldAttendee && !modified) {
let oldDel = cal.itip.resolveDelegation(oldAttendee, oldAttendeeList);
modified = oldDel.delegatees !== del.delegatees || oldDel.delegator !== del.delegator;
}
let userTypeString = cal.l10n.getLtnString("imipHtml.attendeeUserType2." + userType, [
attendee.toString(),
@ -98,24 +125,28 @@ var calinvitation = {
name += " " + cal.l10n.getLtnString("imipHtml.attendeeDelegatedFrom", [del.delegators]);
}
let attendeeLabel = doc.createElementNS("http://www.w3.org/1999/xhtml", "div");
let attendeeLabel = doc.createElement("div");
attendeeLabel.classList.add("attendee-label");
// NOTE: tooltip will not appear when the top level is XUL.
attendeeLabel.setAttribute("title", tooltip);
attendeeLabel.setAttribute("attendeeid", attendee.id);
if (modified) {
attendeeLabel.classList.add("modified");
}
// FIXME: Replace icon with an img element with src and alt. The current
// problem is that the icon image is set in CSS on the itip-icon class
// with a background image that changes with the role attribute. This is
// generally inaccessible (see Bug 1702560).
let icon = doc.createElementNS("http://www.w3.org/1999/xhtml", "div");
let icon = doc.createElement("div");
icon.classList.add("itip-icon");
icon.setAttribute("partstat", partstat);
icon.setAttribute("usertype", userType);
icon.setAttribute("role", role);
attendeeLabel.appendChild(icon);
let text = doc.createElementNS("http://www.w3.org/1999/xhtml", "div");
let text = doc.createElement("div");
text.classList.add("attendee-name");
text.appendChild(doc.createTextNode(name));
attendeeLabel.appendChild(text);
@ -133,7 +164,7 @@ var calinvitation = {
* return {HTMLLIElement} - The attendee list item.
*/
createAttendeeListItem(doc, attendeeLabel) {
let listItem = doc.createElementNS("http://www.w3.org/1999/xhtml", "li");
let listItem = doc.createElement("li");
listItem.classList.add("attendee-list-item");
listItem.appendChild(attendeeLabel);
return listItem;
@ -144,17 +175,96 @@ var calinvitation = {
*
* @param {Document} doc - The document the new list will belong to.
* @param {calIAttendee[]} attendees - The attendees to create the list for.
* @param {calIAttendee[]} [oldAttendees] - A list of attendees for a
* previous version of the event.
*
* @return {HTMLUListElement} - The list of attendees.
*/
createAttendeesList(doc, attendees) {
let list = doc.createElementNS("http://www.w3.org/1999/xhtml", "ul");
createAttendeesList(doc, attendees, oldAttendees) {
let list = doc.createElement("ul");
list.classList.add("attendee-list");
let oldAttendeeData;
if (oldAttendees) {
oldAttendeeData = [];
for (let attendee of oldAttendees) {
let data = { attendee, item: null };
oldAttendeeData.push(data);
}
}
for (let attendee of attendees) {
list.appendChild(
this.createAttendeeListItem(doc, this.createAttendeeLabel(doc, attendee, attendees))
);
let attendeeLabel;
let oldData;
if (oldAttendeeData) {
oldData = oldAttendeeData.find(old => old.attendee.id == attendee.id);
if (oldData) {
// Same attendee.
attendeeLabel = this.createAttendeeLabel(
doc,
attendee,
attendees,
oldData.attendee,
oldAttendees
);
} else {
// Added attendee.
attendeeLabel = this._createAddedElement(doc);
attendeeLabel.appendChild(this.createAttendeeLabel(doc, attendee, attendees));
}
} else {
attendeeLabel = this.createAttendeeLabel(doc, attendee, attendees);
}
let listItem = this.createAttendeeListItem(doc, attendeeLabel);
if (oldData) {
oldData.item = listItem;
}
list.appendChild(listItem);
}
if (oldAttendeeData) {
let next = null;
// Traverse from the end of the list to the start.
for (let i = oldAttendeeData.length - 1; i >= 0; i--) {
let data = oldAttendeeData[i];
if (!data.item) {
// Removed attendee.
let attendeeLabel = this._createRemovedElement(doc);
attendeeLabel.appendChild(this.createAttendeeLabel(doc, data.attendee, attendees));
let listItem = this.createAttendeeListItem(doc, attendeeLabel);
data.item = listItem;
// Insert the removed attendee list item *before* the list item that
// corresponds to the attendee that follows this attendee in the
// oldAttendees list.
//
// NOTE: by traversing from the end of the list to the start, we are
// prioritising being next to the attendee that follows us, rather
// than being next to the attendee that precedes us in the oldAttendee
// list.
//
// Specifically, if a new attendee is added between these two old
// neighbours, the added attendee will be shown earlier than the
// removed attendee in the list.
//
// E.g., going from the list
// [first@person, removed@person, second@person]
// to
// [first@person, added@person, second@person]
// will be shown as
// first@person
// + added@person
// - removed@person
// second@person
// because the removed@person's uses second@person as their reference
// point.
//
// NOTE: next.item is always non-null because next.item is always set
// by the end of the last loop.
list.insertBefore(listItem, next ? next.item : null);
}
next = data;
}
}
return list;
@ -163,88 +273,199 @@ var calinvitation = {
/**
* Returns the html representation of the event as a DOM document.
*
* @param {calIItemBase} aEvent The event to parse into html.
* @param {calItipItem} aItipItem The itip item, which contains aEvent.
* @return {DOM} The html representation of aEvent.
* @param {calIItemBase} event - The event to parse into html.
* @param {calItipItem} itipItem - The itip item, which contains the event.
* @return {Document} The html representation of the event.
*/
createInvitationOverlay(aEvent, aItipItem) {
createInvitationOverlay(event, itipItem) {
// Creates HTML using the Node strings in the properties file
let doc = cal.xml.parseString(calinvitation.htmlTemplate);
const parser = new DOMParser();
let doc = parser.parseFromString(calinvitation.htmlTemplate, "text/html");
this.updateInvitationOverlay(doc, event, itipItem);
return doc;
},
/**
* Update the document created by createInvitationOverlay to show the new
* event details, and optionally show changes in the event against an older
* version of it.
*
* For example, this can be used for email invitations to update the invite to
* show the most recent version of the event found in the calendar, whilst
* also showing the event details that were removed since the original email
* invitation. I.e. contrasting the event found in the calendar with the event
* found within the email. Alternatively, if the email invitation is newer
* than the event found in the calendar, you can switch the comparison around.
* (As used in imip-bar.js.)
*
* @param {Document} doc - The document to update, previously created through
* createInvitationOverlay.
* @param {calIItemBase} event - The newest version of the event.
* @param {calItipItem} itipItem - The itip item, which contains the event.
* @param {calIItemBase} [oldEvent] - A previous version of the event to
* show as updated.
*/
updateInvitationOverlay(doc, event, itipItem, oldEvent) {
doc.body.toggleAttribute(
"systemcolors",
Services.prefs.getBoolPref("calendar.view.useSystemColors", false)
);
let headerDescr = doc.getElementById("imipHtml-header");
if (headerDescr) {
headerDescr.textContent = calinvitation.getItipHeader(itipItem);
}
let formatter = cal.dtz.formatter;
let field = function(aField, aContentText, aConvert, aContentHTML) {
let descr = doc.getElementById("imipHtml-" + aField + "-descr");
if (descr) {
let labelText = cal.l10n.getLtnString("imipHtml." + aField);
descr.textContent = labelText;
/**
* Set whether the given field should be shown.
*
* @param {string} fieldName - The name of the field.
* @param {boolean} show - Whether the field should be shown.
*/
let showField = (fieldName, show) => {
let row = doc.getElementById("imipHtml-" + fieldName + "-row");
if (row.hidden && show) {
// Make sure the field name is set.
doc.getElementById("imipHtml-" + fieldName + "-descr").textContent = cal.l10n.getLtnString(
"imipHtml." + fieldName
);
}
if (aContentText) {
let content = doc.getElementById("imipHtml-" + aField + "-content");
doc.getElementById("imipHtml-" + aField + "-row").hidden = false;
if (aConvert) {
let docFragment = cal.view.textToHtmlDocumentFragment(aContentText, doc, aContentHTML);
content.appendChild(docFragment);
} else {
content.textContent = aContentText;
}
row.hidden = !show;
};
/**
* Set the given element to display the given value.
*
* @param {Element} element - The element to display the value within.
* @param {string} value - The value to show.
* @param {boolean} [convert=false] - Whether the value will need converting
* to a sanitised document fragment.
* @param {string} [html] - The html to use as the value. This is only used
* if convert is set to true.
*/
let setElementValue = (element, value, convert = false, html) => {
if (convert) {
element.appendChild(cal.view.textToHtmlDocumentFragment(value, doc, html));
} else {
element.textContent = value;
}
};
// Simple fields
let headerDescr = doc.getElementById("imipHtml-header");
if (headerDescr) {
headerDescr.textContent = calinvitation.getItipHeader(aItipItem);
}
field("summary", aEvent.title, true);
field("location", aEvent.getProperty("LOCATION"), true);
let dateString = formatter.formatItemInterval(aEvent);
if (aEvent.recurrenceInfo) {
let kDefaultTimezone = cal.dtz.defaultTimezone;
let startDate = aEvent.startDate;
let endDate = aEvent.endDate;
startDate = startDate ? startDate.getInTimezone(kDefaultTimezone) : null;
endDate = endDate ? endDate.getInTimezone(kDefaultTimezone) : null;
let repeatString = recurrenceRule2String(
aEvent.recurrenceInfo,
startDate,
endDate,
startDate.isDate
);
if (repeatString) {
dateString = repeatString;
/**
* Set the given field.
*
* If oldEvent is set, and the new value differs from the old one, it will
* be shown as added and/or removed content.
*
* If neither events have a value, the field will be hidden.
*
* @param {string} fieldName - The name of the field to set.
* @param {Function} getValue - A method to retrieve the field value from an
* event. Should return a string, or a falsey value if the event has no
* value for this field.
* @param {boolean} [convert=false] - Whether the value will need converting
* to a sanitised document fragment.
* @param {Function} [getHtml] - A method to retrieve the value as a html.
*/
let setField = (fieldName, getValue, convert = false, getHtml) => {
let cell = doc.getElementById("imipHtml-" + fieldName + "-content");
while (cell.lastChild) {
cell.lastChild.remove();
}
let value = getValue(event);
let oldValue = oldEvent && getValue(oldEvent);
let html = getHtml && getHtml(event);
let oldHtml = oldEvent && getHtml && getHtml(event);
if (oldEvent && (oldValue || value) && oldValue !== value) {
// Different values, with at least one being truthy.
showField(fieldName, true);
if (!oldValue) {
let added = this._createAddedElement(doc);
setElementValue(added, value, convert, html);
cell.appendChild(added);
} else if (!value) {
let removed = this._createRemovedElement(doc);
setElementValue(removed, oldValue, convert, oldHtml);
cell.appendChild(removed);
} else {
let added = this._createAddedElement(doc);
setElementValue(added, value, convert, html);
let removed = this._createRemovedElement(doc);
setElementValue(removed, oldValue, convert, oldHtml);
cell.appendChild(added);
cell.appendChild(doc.createElement("br"));
cell.appendChild(removed);
}
} else if (value) {
// Same truthy value.
showField(fieldName, true);
setElementValue(cell, value, convert, html);
} else {
showField(fieldName, false);
}
};
setField("summary", ev => ev.title, true);
setField("location", ev => ev.getProperty("LOCATION"), true);
let kDefaultTimezone = cal.dtz.defaultTimezone;
setField("when", ev => {
if (ev.recurrenceInfo) {
let startDate = ev.startDate?.getInTimezone(kDefaultTimezone) ?? null;
let endDate = ev.endDate?.getInTimezone(kDefaultTimezone) ?? null;
let repeatString = recurrenceRule2String(
ev.recurrenceInfo,
startDate,
endDate,
startDate.isDate
);
if (repeatString) {
return repeatString;
}
}
return formatter.formatItemInterval(ev);
});
setField("canceledOccurrences", ev => {
if (!ev.recurrenceInfo) {
return null;
}
let formattedExDates = [];
let modifiedOccurrences = [];
let dateComptor = function(a, b) {
return a.startDate.compare(b.startDate);
};
// Show removed instances
for (let exc of aEvent.recurrenceInfo.getRecurrenceItems()) {
if (exc instanceof Ci.calIRecurrenceDate) {
if (exc.isNegative) {
// This is an EXDATE
let excDate = exc.date.getInTimezone(kDefaultTimezone);
formattedExDates.push(formatter.formatDateTime(excDate));
} else {
// This is an RDATE, close enough to a modified occurrence
let excItem = aEvent.recurrenceInfo.getOccurrenceFor(exc.date);
cal.data.binaryInsert(modifiedOccurrences, excItem, dateComptor, true);
}
for (let exc of ev.recurrenceInfo.getRecurrenceItems()) {
if (exc instanceof Ci.calIRecurrenceDate && exc.isNegative) {
// This is an EXDATE
let excDate = exc.date.getInTimezone(kDefaultTimezone);
formattedExDates.push(formatter.formatDateTime(excDate));
}
}
if (formattedExDates.length > 0) {
field("canceledOccurrences", formattedExDates.join("\n"));
return formattedExDates.join("\n");
}
return null;
});
// Show modified occurrences
for (let recurrenceId of aEvent.recurrenceInfo.getExceptionIds()) {
let exc = aEvent.recurrenceInfo.getExceptionFor(recurrenceId);
let dateComptor = (a, b) => a.startDate.compare(b.startDate);
setField("modifiedOccurrences", ev => {
if (!ev.recurrenceInfo) {
return null;
}
let modifiedOccurrences = [];
for (let exc of ev.recurrenceInfo.getRecurrenceItems()) {
if (exc instanceof Ci.calIRecurrenceDate && !exc.isNegative) {
// This is an RDATE, close enough to a modified occurrence
let excItem = ev.recurrenceInfo.getOccurrenceFor(exc.date);
cal.data.binaryInsert(modifiedOccurrences, excItem, dateComptor, true);
}
}
for (let recurrenceId of ev.recurrenceInfo.getExceptionIds()) {
let exc = ev.recurrenceInfo.getExceptionFor(recurrenceId);
let excLocation = exc.getProperty("LOCATION");
// Only show modified occurrence if start, duration or location
@ -252,336 +473,118 @@ var calinvitation = {
exc.QueryInterface(Ci.calIEvent);
if (
exc.startDate.compare(exc.recurrenceId) != 0 ||
exc.duration.compare(aEvent.duration) != 0 ||
excLocation != aEvent.getProperty("LOCATION")
exc.duration.compare(ev.duration) != 0 ||
excLocation != ev.getProperty("LOCATION")
) {
cal.data.binaryInsert(modifiedOccurrences, exc, dateComptor, true);
}
}
let stringifyOcc = function(occ) {
let formattedExc = formatter.formatItemInterval(occ);
let occLocation = occ.getProperty("LOCATION");
if (occLocation != aEvent.getProperty("LOCATION")) {
let location = cal.l10n.getLtnString("imipHtml.newLocation", [occLocation]);
formattedExc += " (" + location + ")";
}
return formattedExc;
};
if (modifiedOccurrences.length > 0) {
field("modifiedOccurrences", modifiedOccurrences.map(stringifyOcc).join("\n"));
let evLocation = ev.getProperty("LOCATION");
return modifiedOccurrences
.map(occ => {
let formattedExc = formatter.formatItemInterval(occ);
let occLocation = occ.getProperty("LOCATION");
if (occLocation != evLocation) {
formattedExc +=
" (" + cal.l10n.getLtnString("imipHtml.newLocation", [occLocation]) + ")";
}
return formattedExc;
})
.join("\n");
}
}
return null;
});
field("when", dateString);
field("comment", aEvent.getProperty("COMMENT"), true);
setField(
"description",
// We remove the useless "Outlookism" squiggle.
ev => ev.descriptionText?.replace("*~*~*~*~*~*~*~*~*~*", ""),
true,
ev => ev.descriptionHTML
);
// DESCRIPTION field
let eventDescription = (aEvent.descriptionText || "")
/* Remove the useless "Outlookism" squiggle. */
.replace("*~*~*~*~*~*~*~*~*~*", "");
field("description", eventDescription, true, aEvent.descriptionHTML);
// URL
field("url", aEvent.getProperty("URL"), true);
// ATTACH - we only display URI but no BINARY type attachments here
let links = [];
let attachments = aEvent.getAttachments();
for (let attachment of attachments) {
if (attachment.uri) {
links.push(attachment.uri.spec);
}
}
field("attachments", links.join("\n"), true);
setField("url", ev => ev.getProperty("URL"), true);
setField(
"attachments",
ev => {
// ATTACH - we only display URI but no BINARY type attachments here
let links = [];
for (let attachment of ev.getAttachments()) {
if (attachment.uri) {
links.push(attachment.uri.spec);
}
}
return links.join("\n");
},
true
);
// ATTENDEE and ORGANIZER fields
let attendees = event.getAttendees();
let oldAttendees = oldEvent?.getAttendees();
let organizerCell = doc.getElementById("imipHtml-organizer-cell");
let attendeeCell = doc.getElementById("imipHtml-attendees-cell");
let attendees = aEvent.getAttendees();
doc.getElementById("imipHtml-attendees-row").hidden = attendees.length < 1;
doc.getElementById("imipHtml-organizer-row").hidden = !aEvent.organizer;
field("organizer");
if (aEvent.organizer) {
organizerCell.appendChild(this.createAttendeeLabel(doc, aEvent.organizer, attendees));
while (organizerCell.lastChild) {
organizerCell.lastChild.remove();
}
// Fill rows for attendees and organizer
field("attendees");
attendeeCell.appendChild(this.createAttendeesList(doc, attendees));
return doc;
},
/**
* Expects and return a serialized DOM - use cal.xml.serializeDOM(aDOM)
* @param {String} aOldDoc serialized DOM of the the old document
* @param {String} aNewDoc serialized DOM of the the new document
* @return {String} updated serialized DOM of the new document
*/
compareInvitationOverlay(aOldDoc, aNewDoc) {
let systemColors = Services.prefs.getBoolPref("calendar.view.useSystemColors", false);
/**
* Add a styling class to the given element.
*
* @param {Element} el - The element to add the class to.
* @param {string} className - The name of the styling class to add.
*/
function _addStyleClass(el, className) {
el.classList.add(className);
el.toggleAttribute("systemcolors", systemColors);
}
/**
* Extract the elements from an element and place them within a new element
* that represents a change in content.
*
* @param {Element} el - The element to extract content from. This will be
* empty after the method returns.
* @param {string} change - The change that the returned element should
* represent.
*
* @return {HTMLModElement} - A new container for the previous content of
* the element. It will be styled and semantically tagged according to the
* given change.
*/
function _extractChangedContent(el, change) {
// Static list of children, including text nodes.
let nodeDoc = el.ownerDocument;
let children = Array.from(el.childNodes);
let wrapper;
if (change === "removed") {
wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "del");
let organizer = event.organizer;
if (oldEvent) {
let oldOrganizer = oldEvent.organizer;
if (!organizer && !oldOrganizer) {
showField("organizer", false);
} else {
wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "ins");
showField("organizer", true);
let removed = false;
let added = false;
if (!organizer) {
removed = true;
} else if (!oldOrganizer) {
added = true;
} else if (organizer.id !== oldOrganizer.id) {
removed = true;
added = true;
} else {
// Same organizer, potentially modified.
organizerCell.appendChild(
this.createAttendeeLabel(doc, organizer, attendees, oldOrganizer, oldAttendees)
);
}
// Append added first.
if (added) {
let addedEl = this._createAddedElement(doc);
addedEl.appendChild(this.createAttendeeLabel(doc, organizer, attendees));
organizerCell.appendChild(addedEl);
}
if (removed) {
let removedEl = this._createRemovedElement(doc);
removedEl.appendChild(this.createAttendeeLabel(doc, oldOrganizer, oldAttendees));
organizerCell.appendChild(removedEl);
}
}
_addStyleClass(wrapper, change);
for (let child of children) {
el.removeChild(child);
wrapper.appendChild(child);
}
return wrapper;
} else if (!organizer) {
showField("organizer", false);
} else {
showField("organizer", true);
organizerCell.appendChild(this.createAttendeeLabel(doc, organizer, attendees));
}
/**
* Compares a row across the two documents. The row in the new document will
* be shown if the row was shown in either document. Otherwise, it will
* remain hidden.
*
* @param {Document} doc - The current document.
* @param {Document} oldDoc - The old document to compare against.
* @param {String} rowId - The id for the row to compare.
* @param {Function} removedCallback - Method to call if the row is hidden
* in the current document, but shown in the old document.
* @param {Function} addedCallback - Method to call if the row is shown
* in the current document, but hidden in the old document.
* @param {Function} modifiedCallback - Method to call if the row is shown
* in both documents.
*/
function _compareRows(doc, oldDoc, rowId, removedCallback, addedCallback, modifiedCallback) {
let oldRow = oldDoc.getElementById(rowId);
let row = doc.getElementById(rowId);
if (row.hidden && !oldRow.hidden) {
removedCallback();
row.hidden = false;
} else if (!row.hidden && oldRow.hidden) {
addedCallback();
} else if (!row.hidden && !oldRow.hidden) {
modifiedCallback();
}
let attendeesCell = doc.getElementById("imipHtml-attendees-cell");
while (attendeesCell.lastChild) {
attendeesCell.lastChild.remove();
}
/**
* Compares content across the two documents. The content of the new
* document will be modified to reflect the changes.
*
* @param {Document} doc - The current document (which will be modified).
* @param {Document} oldDoc - The old document to compare against.
* @param {String} rowId - The id for the row that contains the content.
* @param {String} contentId - The id for the content element.
*/
function _compareContent(doc, oldDoc, rowId, contentId) {
let content = doc.getElementById(contentId);
let oldContent = oldDoc.getElementById(contentId);
_compareRows(
doc,
oldDoc,
rowId,
// Removed row.
() => {
let removed = _extractChangedContent(oldContent, "removed");
while (content.lastChild) {
content.lastChild.remove();
}
content.appendChild(removed);
},
// Added row.
() => {
let added = _extractChangedContent(content, "added");
content.appendChild(added);
},
// Modified row.
() => {
if (content.textContent !== oldContent.textContent) {
let added = _extractChangedContent(content, "added");
let removed = _extractChangedContent(oldContent, "removed");
content.appendChild(added);
content.appendChild(doc.createElementNS("http://www.w3.org/1999/xhtml", "br"));
content.appendChild(removed);
}
}
);
// Hide if we have no attendees, and neither does the old event.
if (attendees.length == 0 && (!oldEvent || oldAttendees.length == 0)) {
showField("attendees", false);
} else {
// oldAttendees is undefined if oldEvent is undefined.
showField("attendees", true);
attendeesCell.appendChild(this.createAttendeesList(doc, attendees, oldAttendees));
}
let oldDoc = cal.xml.parseString(aOldDoc);
let doc = cal.xml.parseString(aNewDoc);
// elements to consider for comparison
[
["imipHtml-summary-row", "imipHtml-summary-content"],
["imipHtml-location-row", "imipHtml-location-content"],
["imipHtml-when-row", "imipHtml-when-content"],
["imipHtml-canceledOccurrences-row", "imipHtml-canceledOccurrences-content"],
["imipHtml-modifiedOccurrences-row", "imipHtml-modifiedOccurrences-content"],
].forEach(ids => _compareContent(doc, oldDoc, ids[0], ids[1]));
/**
* Relate two attendee labels.
*
* @param {Element} attendeeLabel - An attendee label.
* @param {Element} otherAttendeeLabel - Another attendee label to compare
* against.
*
* @return {string} - The relation between the two labels:
* "different" if the attendee names differ,
* "modified" if the attendance details differ,
* "same" otherwise.
*/
function _attendeeDiff(attendeeLabel, otherAttendeeLabel) {
if (attendeeLabel.textContent !== otherAttendeeLabel.textContent) {
return "different";
}
let otherIcon = otherAttendeeLabel.querySelector(".itip-icon");
let icon = attendeeLabel.querySelector(".itip-icon");
for (let attr of ["role", "partstat", "usertype"]) {
if (icon.getAttribute(attr) !== otherIcon.getAttribute(attr)) {
return "modified";
}
}
return "same";
}
/**
* Wrap the given element in-place to describe the given change.
* The wrapper will semantically and/or stylistically describe the change.
*
* @param {Element} - The element to wrap. The new wrapper will take its
* place in the parent container.
* @param {string} - The change that the wrapper should represent.
*/
function _wrapChanged(el, change) {
let nodeDoc = el.ownerDocument;
let wrapper;
switch (change) {
case "removed":
wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "del");
break;
case "added":
wrapper = nodeDoc.createElementNS("http://www.w3.org/1999/xhtml", "ins");
break;
}
if (wrapper) {
el.replaceWith(wrapper);
wrapper.appendChild(el);
el = wrapper;
}
_addStyleClass(el, change);
}
let organizerCell = doc.querySelector("#imipHtml-organizer-cell");
let organizerLabel = organizerCell.querySelector(".attendee-label");
let oldOrganizerLabel = oldDoc.querySelector("#imipHtml-organizer-cell .attendee-label");
_compareRows(
doc,
oldDoc,
"imipHtml-organizer-row",
// Removed row.
() => {
oldOrganizerLabel.remove();
if (organizerLabel) {
organizerLabel.remove();
}
organizerCell.appendChild(oldOrganizerLabel);
_wrapChanged(oldOrganizerLabel, "removed");
},
// Added row.
() => _wrapChanged(organizerLabel, "added"),
// Modified row.
() => {
switch (_attendeeDiff(organizerLabel, oldOrganizerLabel)) {
case "different":
_wrapChanged(organizerLabel, "added");
oldOrganizerLabel.remove();
organizerCell.appendChild(oldOrganizerLabel);
_wrapChanged(oldOrganizerLabel, "removed");
break;
case "modified":
_wrapChanged(organizerLabel, "modified");
break;
}
}
);
let attendeeCell = doc.querySelector("#imipHtml-attendees-cell");
let attendeeList = attendeeCell.querySelector(".attendee-list");
let oldAttendeeList = oldDoc.querySelector("#imipHtml-attendees-cell .attendee-list");
_compareRows(
doc,
oldDoc,
"imipHtml-attendees-row",
// Removed row.
() => {
oldAttendeeList.remove();
if (attendeeList) {
attendeeList.remove();
}
attendeeCell.appendChild(oldAttendeeList);
_wrapChanged(oldAttendeeList, "removed");
},
// Added row.
() => _wrapChanged(attendeeList, "added"),
// Modified row.
() => {
let oldAttendees = Array.from(oldAttendeeList.querySelectorAll(".attendee-label"));
for (let attendeeLabel of attendeeList.querySelectorAll(".attendee-label")) {
let added = true;
for (let i = 0; added && i < oldAttendees.length; i++) {
switch (_attendeeDiff(attendeeLabel, oldAttendees[i])) {
case "different":
break;
case "modified":
_wrapChanged(attendeeLabel, "modified");
// Fallthrough.
case "same":
oldAttendees.splice(i, 1);
added = false;
break;
}
}
if (added) {
_wrapChanged(attendeeLabel, "added");
}
}
for (let oldAttendeeLabel of oldAttendees) {
oldAttendeeLabel.remove();
attendeeList.appendChild(this.createAttendeeListItem(doc, oldAttendeeLabel));
_wrapChanged(oldAttendeeLabel, "removed");
}
}
);
return cal.xml.serializeDOM(doc);
},
/**

Просмотреть файл

@ -39,11 +39,9 @@ CalMimeConverter.prototype = {
return "";
}
let itipItem = null;
let msgOverlay = "";
let msgWindow = null;
itipItem = Cc["@mozilla.org/calendar/itip-item;1"].createInstance(Ci.calIItipItem);
let itipItem = Cc["@mozilla.org/calendar/itip-item;1"].createInstance(Ci.calIItipItem);
itipItem.init(data);
// this.uri is the message URL that we are processing.
@ -62,12 +60,11 @@ CalMimeConverter.prototype = {
// msgOverlay needs to be defined irrespectively of the existence of msgWindow to not break
// printing of invitation emails
let dom = cal.invitation.createInvitationOverlay(event, itipItem);
msgOverlay = cal.xml.serializeDOM(dom);
let msgOverlay = cal.xml.serializeDOM(dom);
if (msgWindow) {
let sinkProps = msgWindow.msgHeaderSink.properties;
sinkProps.setPropertyAsInterface("itipItem", itipItem);
sinkProps.setPropertyAsAUTF8String("msgOverlay", msgOverlay);
// Notify the observer that the itipItem is available
Services.obs.notifyObservers(null, "onItipItemCreation");

Просмотреть файл

@ -49,7 +49,7 @@
text-decoration-line: none;
}
.invitation-table .added[systemcolors] {
body[systemcolors] .invitation-table .added {
color: currentColor;
font-weight: bold;
}
@ -59,7 +59,7 @@
font-style: italic;
}
.invitation-table .modified[systemcolors] {
body[systemcolors] .invitation-table .modified {
color: currentColor;
}
@ -67,6 +67,6 @@
color: rgb(125, 125, 125);
}
.invitation-table .removed[systemcolors] {
body[systemcolors] .invitation-table .removed {
color: currentColor;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу