feat: enhance EventModal with alert and recurrence labels, add view/edit mode toggle
This commit is contained in:
@@ -4,10 +4,10 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { X, Trash2, Check, Users, CalendarDays, Copy } from "lucide-react";
|
import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Video, Repeat, Bell, AlignLeft } from "lucide-react";
|
||||||
import { format, parseISO, addHours, addDays } from "date-fns";
|
import { format, parseISO, addHours, addDays } from "date-fns";
|
||||||
import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types";
|
import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types";
|
||||||
import { parseDuration } from "./event-card";
|
import { parseDuration, getEventColor } from "./event-card";
|
||||||
import { buildAllDayDuration, getEventDisplayEndDate } from "@/lib/calendar-utils";
|
import { buildAllDayDuration, getEventDisplayEndDate } from "@/lib/calendar-utils";
|
||||||
import { ParticipantInput } from "./participant-input";
|
import { ParticipantInput } from "./participant-input";
|
||||||
import {
|
import {
|
||||||
@@ -59,6 +59,41 @@ function buildDuration(startDate: Date, endDate: Date): string {
|
|||||||
type RecurrenceOption = "none" | "daily" | "weekly" | "monthly" | "yearly";
|
type RecurrenceOption = "none" | "daily" | "weekly" | "monthly" | "yearly";
|
||||||
type AlertOption = "none" | "at_time" | "5" | "15" | "30" | "60" | "1440";
|
type AlertOption = "none" | "at_time" | "5" | "15" | "30" | "60" | "1440";
|
||||||
|
|
||||||
|
function formatDurationDisplay(minutes: number): string {
|
||||||
|
if (minutes < 60) return `${minutes}min`;
|
||||||
|
const h = Math.floor(minutes / 60);
|
||||||
|
const m = minutes % 60;
|
||||||
|
if (m === 0) return `${h}h`;
|
||||||
|
return `${h}h${m}min`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlertLabel(event: CalendarEvent, t: ReturnType<typeof useTranslations>): string | null {
|
||||||
|
if (!event.alerts) return null;
|
||||||
|
const first = Object.values(event.alerts)[0];
|
||||||
|
if (!first || first.trigger["@type"] !== "OffsetTrigger") return null;
|
||||||
|
const offset = first.trigger.offset;
|
||||||
|
if (offset === "PT0S") return t("alerts.at_time");
|
||||||
|
const minMatch = offset.match(/-?PT?(\d+)M$/);
|
||||||
|
if (minMatch) return t("alerts.minutes_before", { count: parseInt(minMatch[1]) });
|
||||||
|
const hourMatch = offset.match(/-?PT?(\d+)H$/);
|
||||||
|
if (hourMatch) return t("alerts.hours_before", { count: parseInt(hourMatch[1]) });
|
||||||
|
const dayMatch = offset.match(/-?P(\d+)D/);
|
||||||
|
if (dayMatch) return t("alerts.days_before", { count: parseInt(dayMatch[1]) });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecurrenceLabel(event: CalendarEvent, t: ReturnType<typeof useTranslations>): string | null {
|
||||||
|
if (!event.recurrenceRules?.length) return null;
|
||||||
|
const freq = event.recurrenceRules[0].frequency;
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
daily: t("recurrence.daily"),
|
||||||
|
weekly: t("recurrence.weekly"),
|
||||||
|
monthly: t("recurrence.monthly"),
|
||||||
|
yearly: t("recurrence.yearly"),
|
||||||
|
};
|
||||||
|
return labels[freq] || null;
|
||||||
|
}
|
||||||
|
|
||||||
export function EventModal({
|
export function EventModal({
|
||||||
event,
|
event,
|
||||||
calendars,
|
calendars,
|
||||||
@@ -74,6 +109,7 @@ export function EventModal({
|
|||||||
}: EventModalProps) {
|
}: EventModalProps) {
|
||||||
const t = useTranslations("calendar");
|
const t = useTranslations("calendar");
|
||||||
const isEdit = !!event;
|
const isEdit = !!event;
|
||||||
|
const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit");
|
||||||
|
|
||||||
const userIsOrganizer = useMemo(() => {
|
const userIsOrganizer = useMemo(() => {
|
||||||
if (!event) return true;
|
if (!event) return true;
|
||||||
@@ -340,7 +376,13 @@ export function EventModal({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKey = (e: KeyboardEvent) => {
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") onClose();
|
if (e.key === "Escape") {
|
||||||
|
if (mode === "edit" && isEdit) {
|
||||||
|
setMode("view");
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!isAttendeeMode) handleSave();
|
if (!isAttendeeMode) handleSave();
|
||||||
@@ -348,7 +390,7 @@ export function EventModal({
|
|||||||
};
|
};
|
||||||
window.addEventListener("keydown", handleKey);
|
window.addEventListener("keydown", handleKey);
|
||||||
return () => window.removeEventListener("keydown", handleKey);
|
return () => window.removeEventListener("keydown", handleKey);
|
||||||
}, [onClose, handleSave, isAttendeeMode]);
|
}, [onClose, handleSave, isAttendeeMode, mode, isEdit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const modal = modalRef.current;
|
const modal = modalRef.current;
|
||||||
@@ -486,6 +528,175 @@ export function EventModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// View mode: read-only display of event details with Edit button
|
||||||
|
if (mode === "view" && event) {
|
||||||
|
const startD = parseISO(event.start);
|
||||||
|
const durMin = parseDuration(event.duration);
|
||||||
|
const endD = new Date(startD.getTime() + durMin * 60000);
|
||||||
|
const locationName = event.locations ? Object.values(event.locations)[0]?.name || null : null;
|
||||||
|
const virtualLoc = event.virtualLocations ? Object.values(event.virtualLocations)[0]?.uri || null : null;
|
||||||
|
const viewParticipants = getParticipantList(event);
|
||||||
|
const recurrenceLabel = getRecurrenceLabel(event, t);
|
||||||
|
const alertLabel = getAlertLabel(event, t);
|
||||||
|
const eventCalendar = calendars.find(c => event.calendarIds[c.id]);
|
||||||
|
const color = getEventColor(event, eventCalendar);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={modalRef} role="dialog" aria-modal={isMobile || undefined} aria-label={event.title || t("events.no_title")} className={isMobile ? "fixed inset-0 z-50 flex flex-col bg-background" : "flex flex-col h-full bg-background"}>
|
||||||
|
{/* Color accent bar */}
|
||||||
|
<div className="h-1 w-full flex-shrink-0" style={{ backgroundColor: color }} />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between gap-2 px-6 py-4 border-b border-border flex-shrink-0">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} />
|
||||||
|
<h2 className="text-lg font-semibold truncate">{event.title || t("events.no_title")}</h2>
|
||||||
|
</div>
|
||||||
|
{eventCalendar && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5 pl-[18px]">{eventCalendar.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="p-1.5 rounded-md hover:bg-muted transition-colors duration-150 flex-shrink-0 mt-0.5 text-muted-foreground hover:text-foreground" aria-label={t("form.cancel")}>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="px-6 py-4 space-y-3">
|
||||||
|
{/* Date & Time */}
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<Clock className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{format(startD, "EEE, MMM d, yyyy")}
|
||||||
|
</span>
|
||||||
|
{event.showWithoutTime ? (
|
||||||
|
<span className="text-muted-foreground ml-1.5">{t("events.all_day")}</span>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{format(startD, "HH:mm")} – {format(endD, "HH:mm")}
|
||||||
|
<span className="ml-1.5 text-xs">({formatDurationDisplay(durMin)})</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
{locationName && (
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<MapPin className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
{/^https?:\/\//i.test(locationName) ? (
|
||||||
|
<a href={locationName} target="_blank" rel="noreferrer" className="text-sm text-primary hover:underline truncate" title={locationName}>
|
||||||
|
{(() => { try { return new URL(locationName).hostname; } catch { return locationName; } })()}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-foreground">{locationName}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Virtual Location */}
|
||||||
|
{virtualLoc && (
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<Video className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
<a href={virtualLoc} target="_blank" rel="noreferrer" className="text-sm text-primary hover:underline truncate" title={virtualLoc}>
|
||||||
|
{(() => { try { return new URL(virtualLoc).hostname; } catch { return virtualLoc; } })()}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Participants */}
|
||||||
|
{viewParticipants.length > 0 && (
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<Users className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="text-sm min-w-0">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("participants.count", { count: viewParticipants.length })}
|
||||||
|
</span>
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
{viewParticipants.map((p) => (
|
||||||
|
<div key={p.id} className="flex items-center justify-between gap-2 text-xs">
|
||||||
|
<span className="truncate text-foreground">
|
||||||
|
{p.name || p.email}
|
||||||
|
{p.isOrganizer && (
|
||||||
|
<span className="text-muted-foreground ml-1">({t("participants.organizer").toLowerCase()})</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<StatusBadge status={p.status} isOrganizer={p.isOrganizer} t={t} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recurrence */}
|
||||||
|
{recurrenceLabel && (
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<Repeat className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="text-sm text-foreground">{recurrenceLabel}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reminder */}
|
||||||
|
{alertLabel && (
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<Bell className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="text-sm text-foreground">{alertLabel}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{event.description && (
|
||||||
|
<div className="flex items-start gap-2.5">
|
||||||
|
<AlignLeft className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-muted-foreground whitespace-pre-line">{event.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Bar */}
|
||||||
|
<div className="px-6 py-3 border-t border-border flex-shrink-0 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{onDelete && (
|
||||||
|
showDeleteConfirm ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">{t("form.delete_confirm")}</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => { onDelete(event.id, hasParticipants || undefined); onClose(); }} className="text-red-600 dark:text-red-400 border-red-300 dark:border-red-700">
|
||||||
|
{t("events.delete")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setShowDeleteConfirm(false)}>
|
||||||
|
{t("form.cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setShowDeleteConfirm(true)} className="text-red-600 dark:text-red-400">
|
||||||
|
<Trash2 className="w-4 h-4 mr-1" />
|
||||||
|
{t("events.delete")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{onDuplicate && !showDeleteConfirm && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleDuplicate} aria-label={t("events.duplicate")}>
|
||||||
|
<Copy className="w-4 h-4 mr-1" />
|
||||||
|
{t("events.duplicate")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!showDeleteConfirm && (
|
||||||
|
<Button onClick={() => setMode("edit")}>
|
||||||
|
<Pencil className="w-4 h-4 mr-1" />
|
||||||
|
{t("events.edit")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={modalRef} role="dialog" aria-modal={isMobile || undefined} aria-label={isEdit ? t("events.edit") : t("events.create")} className={isMobile ? "fixed inset-0 z-50 flex flex-col bg-background" : "flex flex-col h-full bg-background"}>
|
<div ref={modalRef} role="dialog" aria-modal={isMobile || undefined} aria-label={isEdit ? t("events.edit") : t("events.create")} className={isMobile ? "fixed inset-0 z-50 flex flex-col bg-background" : "flex flex-col h-full bg-background"}>
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border flex-shrink-0">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-border flex-shrink-0">
|
||||||
@@ -728,7 +939,7 @@ export function EventModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="outline" onClick={isEdit ? () => setMode("view") : onClose}>
|
||||||
{t("form.cancel")}
|
{t("form.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave} disabled={!title.trim()}>
|
<Button onClick={handleSave} disabled={!title.trim()}>
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ export function ParticipantInput({ participants, onAdd, onRemove, disabled }: Pa
|
|||||||
|
|
||||||
{participants.length > 0 && (
|
{participants.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{participants.map((p) => (
|
{participants.map((p, i) => (
|
||||||
<span
|
<span
|
||||||
key={p.email}
|
key={`${p.email}-${i}`}
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded-full bg-muted text-foreground max-w-[200px]"
|
className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded-full bg-muted text-foreground max-w-[200px]"
|
||||||
>
|
>
|
||||||
<span className="truncate">{p.name || p.email}</span>
|
<span className="truncate">{p.name || p.email}</span>
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ vi.mock('next-intl', () => ({
|
|||||||
actor_declined_counter_info: '{name} declined the counter proposal.',
|
actor_declined_counter_info: '{name} declined the counter proposal.',
|
||||||
actor_note: 'Note: {comment}',
|
actor_note: 'Note: {comment}',
|
||||||
actor_unknown: 'Someone',
|
actor_unknown: 'Someone',
|
||||||
|
sender_mismatch_unverified_info: 'This invitation was sent from {sender}, while the organizer listed in the calendar data is {organizer}, and the message could not be verified.',
|
||||||
action_failed: 'Could not complete that calendar action.',
|
action_failed: 'Could not complete that calendar action.',
|
||||||
proposal_applied: 'Proposed changes applied.',
|
proposal_applied: 'Proposed changes applied.',
|
||||||
proposed_changes: 'Proposed changes',
|
proposed_changes: 'Proposed changes',
|
||||||
@@ -172,6 +173,48 @@ describe('CalendarInvitationBanner', () => {
|
|||||||
expect(mocks.clientMock.parseCalendarEvents).not.toHaveBeenCalled();
|
expect(mocks.clientMock.parseCalendarEvents).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not show trust warnings on sent invitations from the current user', async () => {
|
||||||
|
mocks.clientMock.parseCalendarEvents.mockResolvedValue([
|
||||||
|
{
|
||||||
|
uid: 'uid-sent',
|
||||||
|
title: 'My Sent Invite',
|
||||||
|
start: '2026-03-22T14:00:00Z',
|
||||||
|
organizerCalendarAddress: 'mailto:other-organizer@example.com',
|
||||||
|
participants: {
|
||||||
|
organizer: {
|
||||||
|
'@type': 'Participant',
|
||||||
|
calendarAddress: 'mailto:other-organizer@example.com',
|
||||||
|
name: 'Other Organizer',
|
||||||
|
roles: { owner: true },
|
||||||
|
participationStatus: 'accepted',
|
||||||
|
},
|
||||||
|
attendee: {
|
||||||
|
'@type': 'Participant',
|
||||||
|
email: 'guest@example.com',
|
||||||
|
name: 'Guest',
|
||||||
|
roles: { attendee: true },
|
||||||
|
participationStatus: 'needs-action',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CalendarInvitationBanner
|
||||||
|
email={makeEmail({
|
||||||
|
id: 'email-sent',
|
||||||
|
from: [{ email: 'user@example.com', name: 'User' }],
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('heading', { name: 'My Sent Invite' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText(/sent from/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('shows existing event status, current response, and view-in-calendar action', async () => {
|
it('shows existing event status, current response, and view-in-calendar action', async () => {
|
||||||
mocks.calendarState.events = [
|
mocks.calendarState.events = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -347,39 +347,6 @@ function getMethodAccentClass(method: InvitationMethod, actorStatus?: string | n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMethodBadgeClass(method: InvitationMethod): string {
|
|
||||||
switch (method) {
|
|
||||||
case 'cancel':
|
|
||||||
case 'declinecounter':
|
|
||||||
return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400';
|
|
||||||
case 'request':
|
|
||||||
case 'add':
|
|
||||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400';
|
|
||||||
case 'counter':
|
|
||||||
return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400';
|
|
||||||
case 'reply':
|
|
||||||
return 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400';
|
|
||||||
case 'publish':
|
|
||||||
case 'refresh':
|
|
||||||
default:
|
|
||||||
return 'bg-muted text-muted-foreground';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMethodBadgeLabel(method: InvitationMethod): string | null {
|
|
||||||
switch (method) {
|
|
||||||
case 'publish': return 'PUBLISH';
|
|
||||||
case 'request': return 'REQUEST';
|
|
||||||
case 'reply': return 'REPLY';
|
|
||||||
case 'add': return 'ADD';
|
|
||||||
case 'cancel': return 'CANCEL';
|
|
||||||
case 'refresh': return 'REFRESH';
|
|
||||||
case 'counter': return 'COUNTER';
|
|
||||||
case 'declinecounter': return 'DECLINE-COUNTER';
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CalendarInvitationBannerProps {
|
interface CalendarInvitationBannerProps {
|
||||||
email: Email;
|
email: Email;
|
||||||
}
|
}
|
||||||
@@ -502,9 +469,12 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
|||||||
|| myParticipant?.participant.participationStatus
|
|| myParticipant?.participant.participationStatus
|
||||||
|| null;
|
|| null;
|
||||||
const userIsOrganizer = Boolean(isOnlyOrganizer);
|
const userIsOrganizer = Boolean(isOnlyOrganizer);
|
||||||
|
const userIsSender = Boolean(
|
||||||
|
currentUserEmail && email.from?.[0]?.email?.toLowerCase() === currentUserEmail.toLowerCase()
|
||||||
|
);
|
||||||
const bannerTitle = getBannerTitle(t, method);
|
const bannerTitle = getBannerTitle(t, method);
|
||||||
const bannerInfo = getBannerInfo(t, method, userIsOrganizer, supportsCalendar);
|
const bannerInfo = getBannerInfo(t, method, userIsOrganizer, supportsCalendar);
|
||||||
const trustAssessment = parsedEvent ? getInvitationTrustAssessment(parsedEvent, email, method) : null;
|
const trustAssessment = parsedEvent && !userIsSender ? getInvitationTrustAssessment(parsedEvent, email, method) : null;
|
||||||
const trustMessage = trustAssessment ? getTrustMessage(t, trustAssessment) : null;
|
const trustMessage = trustAssessment ? getTrustMessage(t, trustAssessment) : null;
|
||||||
const participationLabel = getParticipationLabel(t, currentRsvp);
|
const participationLabel = getParticipationLabel(t, currentRsvp);
|
||||||
const actorSummary = parsedEvent ? getInvitationActorSummary(parsedEvent, method) : null;
|
const actorSummary = parsedEvent ? getInvitationActorSummary(parsedEvent, method) : null;
|
||||||
@@ -735,9 +705,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const methodBadgeLabel = getMethodBadgeLabel(method);
|
|
||||||
const accentClass = getMethodAccentClass(method, actorSummary?.participationStatus);
|
const accentClass = getMethodAccentClass(method, actorSummary?.participationStatus);
|
||||||
const badgeClass = getMethodBadgeClass(method);
|
|
||||||
|
|
||||||
if (state === 'loading') {
|
if (state === 'loading') {
|
||||||
return (
|
return (
|
||||||
@@ -770,11 +738,6 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
|||||||
)}
|
)}
|
||||||
<span className="text-sm font-medium text-foreground truncate">{bannerTitle}</span>
|
<span className="text-sm font-medium text-foreground truncate">{bannerTitle}</span>
|
||||||
</div>
|
</div>
|
||||||
{methodBadgeLabel && (
|
|
||||||
<span className={cn("rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide flex-shrink-0 ml-2", badgeClass)}>
|
|
||||||
iTIP: {methodBadgeLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
|||||||
Reference in New Issue
Block a user