Skip to content

Commit d7a12b3

Browse files
committed
fix(calendar): floor event-link priority so bundled emails never take primacy
Google Calendar's non-organizer/non-attendee event links, and Outlook's event links (which have no reliable self/organizer signal to split 100/50), now carry priority >= 1. Bundled email links default to priority 0, so the calendar event link keeps rendering as the thread's primary link even after email replies bundle onto it.
1 parent 127e6af commit d7a12b3

4 files changed

Lines changed: 193 additions & 2 deletions

File tree

‎connectors/google-calendar/src/sync.test.ts‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,3 +1535,116 @@ describe("processCalendarEventsFn — message-model note audiences", () => {
15351535
expect(emails).toEqual(["bob@x.com", "org@x.com"]);
15361536
});
15371537
});
1538+
1539+
// ---------------------------------------------------------------------------
1540+
// Event link priority — the calendar event link must always outrank a
1541+
// bundled email link (which defaults to priority 0), so the thread keeps
1542+
// rendering as an event even after email replies bundle onto it.
1543+
// ---------------------------------------------------------------------------
1544+
1545+
describe("processCalendarEventsFn — event link priority", () => {
1546+
afterEach(() => {
1547+
vi.unstubAllGlobals();
1548+
});
1549+
1550+
const calendarId = "user@example.com";
1551+
const isoDaysFromNow = (n: number) =>
1552+
new Date(Date.now() + n * 24 * 60 * 60 * 1000).toISOString();
1553+
1554+
it("floors priority at 1 for an event where the user is neither organizer nor attendee", async () => {
1555+
const host = makeFakeHost({ calendarId });
1556+
vi.stubGlobal("fetch", vi.fn(async () => makeEventsResponse([])));
1557+
1558+
const event = {
1559+
id: "e1",
1560+
iCalUID: "uid-1",
1561+
status: "confirmed" as const,
1562+
summary: "X",
1563+
organizer: { email: "o@x" },
1564+
attendees: [],
1565+
start: { dateTime: isoDaysFromNow(1) },
1566+
end: { dateTime: isoDaysFromNow(1) },
1567+
};
1568+
1569+
await processCalendarEventsFn(host, [event], calendarId, false);
1570+
1571+
const link = host.savedLinks
1572+
.flat()
1573+
.find((l) => l.source === "google-calendar:uid-1");
1574+
expect(link?.priority).toBe(1);
1575+
});
1576+
1577+
it("gives organizer events priority 100", async () => {
1578+
const host = makeFakeHost({ calendarId });
1579+
vi.stubGlobal("fetch", vi.fn(async () => makeEventsResponse([])));
1580+
1581+
const event = {
1582+
id: "e2",
1583+
iCalUID: "uid-organizer",
1584+
status: "confirmed" as const,
1585+
summary: "Organized by me",
1586+
organizer: { email: "me@x.com", self: true },
1587+
attendees: [{ email: "me@x.com", self: true, organizer: true }],
1588+
start: { dateTime: isoDaysFromNow(1) },
1589+
end: { dateTime: isoDaysFromNow(1) },
1590+
};
1591+
1592+
await processCalendarEventsFn(host, [event], calendarId, false);
1593+
1594+
const link = host.savedLinks
1595+
.flat()
1596+
.find((l) => l.source === "google-calendar:uid-organizer");
1597+
expect(link?.priority).toBe(100);
1598+
});
1599+
1600+
it("gives attendee (non-organizer) events priority 50", async () => {
1601+
const host = makeFakeHost({ calendarId });
1602+
vi.stubGlobal("fetch", vi.fn(async () => makeEventsResponse([])));
1603+
1604+
const event = {
1605+
id: "e3",
1606+
iCalUID: "uid-attendee",
1607+
status: "confirmed" as const,
1608+
summary: "Invited",
1609+
organizer: { email: "boss@x.com", self: false },
1610+
attendees: [
1611+
{ email: "boss@x.com", organizer: true },
1612+
{ email: "me@x.com", self: true },
1613+
],
1614+
start: { dateTime: isoDaysFromNow(1) },
1615+
end: { dateTime: isoDaysFromNow(1) },
1616+
};
1617+
1618+
await processCalendarEventsFn(host, [event], calendarId, false);
1619+
1620+
const link = host.savedLinks
1621+
.flat()
1622+
.find((l) => l.source === "google-calendar:uid-attendee");
1623+
expect(link?.priority).toBe(50);
1624+
});
1625+
1626+
it("floors priority at 1 on the cancellation-path link too", async () => {
1627+
const host = makeFakeHost({ calendarId });
1628+
host.store.set(`first_sync_at_${calendarId}`, isoDaysFromNow(-30));
1629+
vi.stubGlobal("fetch", vi.fn(async () => makeEventsResponse([])));
1630+
1631+
const event = {
1632+
id: "e4",
1633+
iCalUID: "uid-cancelled",
1634+
status: "cancelled" as const,
1635+
summary: "Cancelled, not mine",
1636+
organizer: { email: "o@x" },
1637+
attendees: [],
1638+
updated: isoDaysFromNow(0),
1639+
start: { dateTime: isoDaysFromNow(1) },
1640+
end: { dateTime: isoDaysFromNow(1) },
1641+
};
1642+
1643+
await processCalendarEventsFn(host, [event], calendarId, false);
1644+
1645+
const link = host.savedLinks
1646+
.flat()
1647+
.find((l) => l.source === "google-calendar:uid-cancelled");
1648+
expect(link?.priority).toBe(1);
1649+
});
1650+
});

‎connectors/google-calendar/src/sync.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ export async function processCalendarEventsFn(
777777
? 100
778778
: event.attendees?.some((a) => a.self)
779779
? 50
780-
: 0,
780+
: 1,
781781
access: "private",
782782
accessContacts: cancelMentions,
783783
author: authorContact,
@@ -968,7 +968,7 @@ export async function processCalendarEventsFn(
968968
? 100
969969
: event.attendees?.some((a) => a.self)
970970
? 50
971-
: 0,
971+
: 1,
972972
access: "private",
973973
accessContacts: attendeeMentions,
974974
author: authorContact,

‎connectors/outlook-calendar/src/sync.test.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,71 @@ describe("processOutlookEventsFn — message-model note/link audiences", () => {
264264
).toBeUndefined();
265265
});
266266
});
267+
268+
// ---------------------------------------------------------------------------
269+
// Event link priority — the calendar event link must always outrank a
270+
// bundled email link (which defaults to priority 0), so the thread keeps
271+
// rendering as an event even after email replies bundle onto it. Outlook's
272+
// Graph event payload carries no reliable self/organizer signal (no
273+
// `isOrganizer` boolean, no per-attendee `self` flag), so both the main
274+
// event link and the cancellation-path link use a constant floor.
275+
// ---------------------------------------------------------------------------
276+
277+
describe("processOutlookEventsFn — event link priority", () => {
278+
afterEach(() => {
279+
vi.unstubAllGlobals();
280+
});
281+
282+
const calendarId = "cal-1";
283+
284+
it("gives the main event link priority >= 1", async () => {
285+
const host = makeFakeHost();
286+
287+
await processOutlookEventsFn(
288+
host,
289+
[
290+
outlookEventFixture({
291+
id: "e1",
292+
iCalUId: "uid-1",
293+
subject: "Sync",
294+
organizer: { emailAddress: { address: "org@x.com", name: "Org" } },
295+
attendees: [
296+
{ emailAddress: { address: "bob@x.com" }, type: "required" },
297+
],
298+
}),
299+
],
300+
calendarId,
301+
false
302+
);
303+
304+
const link = host.savedLinks
305+
.flat()
306+
.find((l) => l.source === `outlook-calendar:${calendarId}:e1`);
307+
expect(link?.priority).toBeGreaterThanOrEqual(1);
308+
});
309+
310+
it("gives the cancellation-path link priority >= 1", async () => {
311+
const host = makeFakeHost();
312+
313+
await processOutlookEventsFn(
314+
host,
315+
[
316+
outlookEventFixture({
317+
id: "e2",
318+
"@removed": { reason: "deleted" },
319+
organizer: { emailAddress: { address: "org@x.com", name: "Org" } },
320+
attendees: [
321+
{ emailAddress: { address: "bob@x.com" }, type: "required" },
322+
],
323+
}),
324+
],
325+
calendarId,
326+
false
327+
);
328+
329+
const link = host.savedLinks
330+
.flat()
331+
.find((l) => l.source === `outlook-calendar:${calendarId}:e2`);
332+
expect(link?.priority).toBeGreaterThanOrEqual(1);
333+
});
334+
});

‎connectors/outlook-calendar/src/sync.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,11 @@ export async function processOutlookEventsFn(
784784
iCalUId: outlookEvent.iCalUId,
785785
}),
786786
channelId: calendarId,
787+
// Floor above a bundled email link's default priority (0) so the
788+
// event link stays primary. Graph's event payload has no reliable
789+
// self/organizer signal (no `isOrganizer`, no per-attendee `self`),
790+
// so this is a constant rather than a 100/50 split like Google.
791+
priority: 1,
787792
meta: { syncProvider: "microsoft", syncableId: calendarId },
788793
notes: [cancelNote],
789794
...(cancelMentions.length > 0
@@ -960,6 +965,11 @@ export async function processOutlookEventsFn(
960965
}),
961966
type: "event",
962967
title: threadData.title || "",
968+
// Floor above a bundled email link's default priority (0) so the
969+
// event link stays primary. Graph's event payload has no reliable
970+
// self/organizer signal (no `isOrganizer`, no per-attendee `self`),
971+
// so this is a constant rather than a 100/50 split like Google.
972+
priority: 1,
963973
access: "private",
964974
accessContacts: attendeeMentions,
965975
created: threadData.created,

0 commit comments

Comments
 (0)