mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
fix: stop reloading an invitation when the message is marked read
The invitation banner loaded the .ics whenever it was handed a new message object, so marking an invitation read or starring it downloaded and parsed the calendar part again (and fetched its raw text when the method had to be read from it). It now loads once per message, account and calendar part (`calendarInvitationKey`), and reads the current message only when the load runs.
This commit is contained in:
@@ -14,6 +14,7 @@ import { useCalendarStore } from '../../stores/calendar-store';
|
||||
import { useSettingsStore } from '../../stores/settings-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import {
|
||||
calendarInvitationKey,
|
||||
findCalendarAttachment,
|
||||
findParticipantByEmail,
|
||||
getInvitationMethod,
|
||||
@@ -72,6 +73,11 @@ export function CalendarInvitationBanner({ email, jmapAccountId }: Props) {
|
||||
// Login address + identities + aliases, so invitations addressed to an
|
||||
// alias still show the RSVP buttons. Only an invitation looks the aliases up.
|
||||
const currentUserEmails = useUserCalendarAddresses(!!attachment && enabled);
|
||||
// The invitation is loaded once per message, account and calendar part;
|
||||
// a mark-read or star hands a new `email` for the same one.
|
||||
const invitationKey = calendarInvitationKey(email, attachment, jmapAccountId);
|
||||
const latest = React.useRef({ email, attachment });
|
||||
latest.current = { email, attachment };
|
||||
|
||||
const [state, setState] = React.useState<BannerState>('loading');
|
||||
const [event, setEvent] = React.useState<Partial<CalendarEvent> | null>(null);
|
||||
@@ -87,12 +93,13 @@ export function CalendarInvitationBanner({ email, jmapAccountId }: Props) {
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!attachment || !enabled) return;
|
||||
const { email: current, attachment: part } = latest.current;
|
||||
if (!invitationKey || !part || !enabled) return;
|
||||
setState('loading');
|
||||
setServerMatch(null);
|
||||
(async () => {
|
||||
try {
|
||||
const events = await parseCalendarBlob(attachment.blobId, jmapAccountId);
|
||||
const events = await parseCalendarBlob(part.blobId, jmapAccountId);
|
||||
if (cancelled) return;
|
||||
if (events.length === 0) {
|
||||
setState('error');
|
||||
@@ -110,11 +117,11 @@ export function CalendarInvitationBanner({ email, jmapAccountId }: Props) {
|
||||
}
|
||||
// Explicit method (Content-Type params) first; JMAP usually strips
|
||||
// them, so fall back to the raw ICS METHOD line before guessing.
|
||||
let detected = getInvitationMethod(parsed, { email, attachment });
|
||||
let detected = getInvitationMethod(parsed, { email: current, attachment: part });
|
||||
if (detected === 'unknown') {
|
||||
const raw = await fetchCalendarBlobText(attachment.blobId, jmapAccountId);
|
||||
const raw = await fetchCalendarBlobText(part.blobId, jmapAccountId);
|
||||
if (cancelled) return;
|
||||
detected = getInvitationMethod(parsed, { email, attachment, rawIcs: raw });
|
||||
detected = getInvitationMethod(parsed, { email: current, attachment: part, rawIcs: raw });
|
||||
}
|
||||
setMethod(detected);
|
||||
setState('parsed');
|
||||
@@ -123,7 +130,7 @@ export function CalendarInvitationBanner({ email, jmapAccountId }: Props) {
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [attachment, enabled, email, jmapAccountId]);
|
||||
}, [invitationKey, enabled, jmapAccountId]);
|
||||
|
||||
// Import into the account's default calendar; never into a shared calendar,
|
||||
// an iCal subscription (the next feed sync would delete the event) or a
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { Email } from '../../api/types';
|
||||
import {
|
||||
calendarInvitationKey,
|
||||
extractMethodFromContentType,
|
||||
extractMethodFromRawIcs,
|
||||
findCalendarAttachment,
|
||||
@@ -113,3 +114,25 @@ describe('getInvitationTrustAssessment', () => {
|
||||
expect(getInvitationTrustAssessment(request, e, 'unknown').level).toBe('trusted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calendarInvitationKey', () => {
|
||||
const invite = {
|
||||
id: 'e1',
|
||||
keywords: {},
|
||||
attachments: [{ blobId: 'b1', type: 'text/calendar', name: 'invite.ics', size: 10 }],
|
||||
} as unknown as Email;
|
||||
|
||||
it('stays the same when only the keywords change', () => {
|
||||
const read = { ...invite, keywords: { $seen: true } } as Email;
|
||||
const key = calendarInvitationKey(invite, findCalendarAttachment(invite), 'group');
|
||||
expect(key).toBe('group|e1|b1');
|
||||
expect(calendarInvitationKey(read, findCalendarAttachment(read), 'group')).toBe(key);
|
||||
});
|
||||
|
||||
it('differs per account and calendar part, and is null without one', () => {
|
||||
const other = { ...invite, attachments: [{ ...invite.attachments![0], blobId: 'b2' }] } as Email;
|
||||
expect(calendarInvitationKey(invite, findCalendarAttachment(invite))).toBe('|e1|b1');
|
||||
expect(calendarInvitationKey(other, findCalendarAttachment(other))).toBe('|e1|b2');
|
||||
expect(calendarInvitationKey({ id: 'e2' }, null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,6 +250,20 @@ export function findCalendarAttachment(
|
||||
return findCalendarBodyPart(email.textBody) || findCalendarBodyPart(email.htmlBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* What identifies a message's invitation for loading it: the message, its
|
||||
* account and the calendar part's blob. A keyword change (read, star) hands
|
||||
* a new message object with the same key, and must not download and parse
|
||||
* the .ics again. Null when the message has no calendar part.
|
||||
*/
|
||||
export function calendarInvitationKey(
|
||||
email: Pick<Email, 'id'>,
|
||||
attachment: Pick<Attachment, 'blobId'> | null,
|
||||
jmapAccountId?: string,
|
||||
): string | null {
|
||||
return attachment ? `${jmapAccountId ?? ''}|${email.id}|${attachment.blobId}` : null;
|
||||
}
|
||||
|
||||
// ─── Authentication-Results / trust ──────────────────────
|
||||
|
||||
export interface AuthenticationResults {
|
||||
|
||||
Reference in New Issue
Block a user