Show shared-invitee icon on owner's calendar for events with active guests

Adds has_active_invitees flag to the events GET response. The Users icon
now appears on the owner's calendar view when an event has accepted or
tentative invitees, giving visual feedback that the event is actively
shared. Single batch query with set lookup — no N+1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-17 01:14:44 +08:00
parent c66fd159ea
commit 2f45220c5d
3 changed files with 22 additions and 1 deletions

View File

@ -35,6 +35,7 @@ def _event_to_dict(
display_calendar_name: str | None = None,
display_calendar_color: str | None = None,
can_modify: bool = False,
has_active_invitees: bool = False,
) -> dict:
"""Serialize a CalendarEvent ORM object to a response dict including calendar info."""
# For invited events: use display calendar if set, otherwise fallback to "Invited"/gray
@ -74,6 +75,7 @@ def _event_to_dict(
"invitation_id": invitation_id,
"display_calendar_id": display_calendar_id,
"can_modify": can_modify,
"has_active_invitees": has_active_invitees,
}
return d
@ -243,6 +245,21 @@ async def get_events(
all_event_ids = [e.id for e in events]
override_map = await get_invitation_overrides_for_user(db, current_user.id, all_event_ids)
# Batch-fetch event IDs that have accepted/tentative invitees (for owner's shared icon)
active_invitee_set: set[int] = set()
if all_event_ids:
active_inv_result = await db.execute(
select(EventInvitation.event_id).where(
EventInvitation.event_id.in_(all_event_ids),
EventInvitation.status.in_(["accepted", "tentative"]),
).distinct()
)
active_invitee_set = {r[0] for r in active_inv_result.all()}
# Also mark parent events: if a parent has active invitees, all its children should show the icon
parent_ids = {e.parent_event_id for e in events if e.parent_event_id and e.parent_event_id in active_invitee_set}
if parent_ids:
active_invitee_set.update(e.id for e in events if e.parent_event_id in active_invitee_set)
response: List[dict] = []
for e in events:
# Determine if this event is from an invitation
@ -272,6 +289,7 @@ async def get_events(
display_calendar_name=disp_cal_name,
display_calendar_color=disp_cal_color,
can_modify=inv_can_modify,
has_active_invitees=(parent_id in active_invitee_set or e.id in active_invitee_set),
))
# Fetch the user's Birthdays system calendar; only generate virtual events if visible

View File

@ -389,6 +389,7 @@ export default function CalendarPage() {
calendarColor: event.calendar_color || 'hsl(var(--accent-color))',
is_invited: event.is_invited,
can_modify: event.can_modify,
has_active_invitees: event.has_active_invitees,
},
}));
@ -531,6 +532,7 @@ export default function CalendarPage() {
const isAllDay = arg.event.allDay;
const isRecurring = arg.event.extendedProps.is_recurring || arg.event.extendedProps.parent_event_id;
const isInvited = arg.event.extendedProps.is_invited;
const hasActiveInvitees = arg.event.extendedProps.has_active_invitees;
const calColor = arg.event.extendedProps.calendarColor as string;
// Sync --event-color on the parent FC element so CSS rules (background, hover)
@ -544,7 +546,7 @@ export default function CalendarPage() {
const icons = (
<>
{isInvited && <Users className="h-2.5 w-2.5 shrink-0 opacity-60" />}
{(isInvited || hasActiveInvitees) && <Users className="h-2.5 w-2.5 shrink-0 opacity-60" />}
{isRecurring && <Repeat className="h-2.5 w-2.5 shrink-0 opacity-50" />}
</>
);

View File

@ -117,6 +117,7 @@ export interface CalendarEvent {
invitation_id?: number | null;
display_calendar_id?: number | null;
can_modify?: boolean;
has_active_invitees?: boolean;
created_at: string;
updated_at: string;
}