feat(sharing): label shared personal calendars by owner + drop group title prefix

Two group/sharing display fixes, both server-side so every client benefits:

1. A personal calendar shared WITH a user showed only its raw name
   ("Persönlich"), indistinguishable from the user's own. The merge read now
   relabels a shared *personal* calendar under the owner's display name (so
   Guido's "Persönlich" reads as "Guido" for recipients) and adds read_only:true
   when the share isn't read_write. Group calendars are excluded — they keep
   their own name and stay writable for members.

2. The combined group view prefixed every foreign event with the owner's first
   name ("Guido: …"). Each member already has a distinct display_color, so the
   prefix was redundant. _decorate_title now returns the raw title; display_title
   stays non-empty so clients' legacy prefix fallback never triggers. The change
   takes effect on already-installed clients with no app update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-04 18:42:26 +02:00
parent 444772c959
commit e3844294ae
2 changed files with 32 additions and 11 deletions

View File

@@ -352,6 +352,21 @@ def get_events(
.all()
) if readable_ids else []
name_cache = {u.id: (u.display_name or u.username) for u in db.query(models.User).all()}
# A personal calendar shared WITH the current user is relabelled under the
# owner's name (so Guido's "Persönlich" reads as "Guido" for his mum) and
# flagged read_only unless the share grants write. Group calendars are
# excluded — they keep their own name and stay writable for members.
shares_map = {
s.calendar_id: s.permission
for s in db.query(models.CalendarShare).filter(
models.CalendarShare.user_id == current_user.id
)
}
group_cal_ids = {
r[0] for r in db.query(models.GroupCalendar.calendar_id).filter(
models.GroupCalendar.calendar_id.in_(readable_ids)
)
} if readable_ids else set()
# Cache each owner's private-event visibility (one lookup per owner, not per event).
vis_cache: dict = {}
@@ -361,6 +376,13 @@ def get_events(
return vis_cache[uid]
for local_cal in local_calendars:
# Decoration for a personal calendar shared with (not owned by) me.
is_shared_personal = (
local_cal.user_id != current_user.id
and local_cal.id not in group_cal_ids
)
shared_owner_name = name_cache.get(local_cal.user_id) if is_shared_personal else None
shared_read_only = is_shared_personal and shares_map.get(local_cal.id) != "read_write"
local_events = (
db.query(models.LocalEvent)
.filter(
@@ -391,6 +413,10 @@ def get_events(
else:
built = [build_local_event_dict(ev, local_cal, rrule=None, creator=creator)]
for b in built:
if shared_owner_name:
b["calendar_name"] = shared_owner_name
if shared_read_only:
b["read_only"] = True
b = apply_event_privacy(
b, owner_id=owner_id, is_private=is_priv,
requester_id=current_user.id, visibility=visibility,

View File

@@ -317,17 +317,12 @@ def _first_name(name: Optional[str]) -> str:
def _decorate_title(title: str, *, is_group: bool, creator: Optional[dict],
owner: Optional[dict], me_id: int) -> str:
"""Server-side display title for the combined view so every client (web,
iOS, Android) renders identically: another member's / creator's first name
is prefixed. No icon glyph is embedded — group icons are semantic keys the
clients render as native vector icons, and group-calendar events are
distinguished by their (group) colour. The raw `title` stays for editing."""
if is_group:
if creator and creator.get("id") is not None and creator.get("id") != me_id:
return f"{_first_name(creator.get('display_name'))}: {title}"
return title
if owner and owner.get("id") is not None and owner.get("id") != me_id:
return f"{_first_name(owner.get('display_name'))}: {title}"
"""Server-side display title for the combined view. The former owner/creator
first-name prefix ("Guido: …") was dropped: each member already has a
distinct colour, so the prefix was redundant noise. We still return a
non-empty `display_title` (== raw title) so the clients' legacy fallback —
which rebuilds a prefix when `display_title` is empty — never kicks in.
`display_color` continues to carry the per-person colour."""
return title