Campus Tab & Chapter Events#
This page covers the user-facing surfaces introduced in Stage 18 of the
Phase 2 plan: the Campus tab on /app/feed, chapter events surfaced
from the existing calendar onto each chapter’s group page, and the
per-category announcement subscription model that lets you mute one
announcement bucket without losing the rest.
The Campus tab is one of the default landing surfaces for every authenticated stakeholder — students, alumni, faculty, parents, and staff. It’s where time-sensitive announcements (recess windows, chapter meetups, placement results) and chapter activity surface for the people in scope.
Where to find it#
- Campus tab —
/app/feed→ switch to the Campus tab. - Chapter events —
/app/groups/<slug>→ Events tab. Available for any group ofkind=chapteryou’re a member of. - Subscriptions —
/app/groups/<slug>→ Events tab → toggle Subscribe to chapter announcements. Per-category granularity for every announcement bucket is also surfaced via the notification preferences page. - Admin authoring —
/admin/cms/announcements→ category dropdown- pin until + cover image fields on the create / edit drawer.
What the Campus tab shows#
Two stacks, top-down:
- Pinned announcements (
feed-campus-pinned). Any announcement whosepin_untilis in the future ranks here, sorted bypin_until DESCso the soonest-to-expire pin is at the top. Pins are first-class — pinning is the only signal the campus tab uses to promote a row above the regular stream, so use it sparingly (the admin authoring flow gates the field behind a deliberate “set a pin date” action). - All other announcements (
feed-campus-list). Published announcements visible to the caller, ordered bypublished_at DESC.
Each row uses the AnnouncementCard component:
- Title + body excerpt at the top.
- Category chip (academic / campus / chapter / alumni / placements / admin) on the upper-right.
- Pin ribbon when the row is pinned.
- Cover image when the author supplied one.
When the user has no chapter memberships and there are no active
announcements visible to them, the tab renders the
feed-campus-empty placeholder explaining why and pointing at the
groups page.
Categories#
Six buckets defined by Stage 18:
| Category | Typical use |
|---|---|
academic | Recess windows, exam-week procedures, semester rollover. |
campus | Fest schedules, library hours, dining-hall changes. |
chapter | Alumni chapter meetups, regional events, chapter newsletters. |
alumni | Cross-chapter alumni announcements (reunions, BITSian Day). |
placements | Drive results, internship program calls, on-campus interviews. |
admin | Catch-all for ops messages that don’t fit a sharper bucket. |
Categories are independent of audience_scope. A chapter category
announcement targeted at audience_scope=alumni reaches every alumni
viewer; the category drives the notification topic name
(cms.announcement.chapter) and the per-category opt-out, while the
audience scope drives who can see it at all.
Existing announcements (created before Stage 18) inherit category
admin and pin_until=NULL — they continue to surface on the Campus
tab unchanged.
Per-category subscription opt-out#
The system has three layers gating an announcement → notification delivery:
- Audience scope — you must be in the audience tier the author
chose (
institute,alumni,batch:<year>, etc.). If you aren’t, you don’t see the announcement at all and no notification is considered. - Per-category subscription — a row in
cms_announcement_subscriptionkeyed on(user_id, category). Three states:- Row absent — fall through to the global notification preference. The default for everyone.
opted_in=true— same effect as absent (mostly here for the UI to render the toggle in the on position deterministically).opted_in=false— explicit mute. Fan-out drops you before the global preference is ever consulted.
- Global notification preferences —
/profile/notifications. The per-channel matrix (push / email / WhatsApp / in-app). The announcement’s category-derived topic (cms.announcement.<category>) is what the matrix matches on, so a row in this table can disable a specific bucket on a specific channel.
The chapter Events tab surfaces a one-click toggle for the chapter
category (since that’s the most common bucket alumni want to silence
on a per-chapter basis without giving up the other categories). The
notification preferences page is the source of truth for the
remaining categories + channel matrix.
Chapter events#
Chapter events are calendar events with kind=chapter whose
scope_ref (a UUID) points at the owning groups.Group row. Stage 18
added:
ListChapterEvents(chapter_id, from, to)— convenience RPC overListEventswith the membership facade enforcing the chapter scope on the read side. Non-members getPermissionDenied; members get a paginated, time-windowed list ordered bystarts_at.- The Events tab on
/app/groups/<slug>— read-only listing of the chapter’s events surfaced via that RPC. Event creation still goes through the standard calendar flow (admin calendar, or the chapter admin’s create drawer once that ships); the Events tab is for members to see what’s on, not author it.
The membership gate is the same code path the unified calendar uses
for chapter scoping, so a non-member listing
/api/v1/calendar/events?scopes=chapter and a non-member listing
/api/v1/calendar/chapters/{id}/events see consistent behaviour
(empty / forbidden) — there’s no surface where one path returns rows
the other hides.
Authoring a pinned chapter announcement#
Admin walkthrough:
- Open
/admin/cms/announcements→ New announcement. - Fill Title, Body, Audience scope as before.
- Pick Category = Chapter (or another category as appropriate).
- Optional: set Pin until to a future datetime. Empty / cleared = unpinned. Clearing a pin on an already-published announcement demotes it on the next Campus-tab read.
- Optional: paste a Cover image URL. The image renders inside
AnnouncementCardon the Campus tab. - Click Save → row lands as
draft. - From the announcements grid, click Publish on the row. Fan-out
targets every audience-scoped recipient minus anyone with an
explicit
opted_in=falserow for the announcement’s category.recipients_notifiedon the response is the post-filter count.
Pinning is a soft signal — there is no row-locking or contention
between concurrent pinned announcements. If two announcements pin to
the same future timestamp, the campus tab orders them by pin_until DESC and tie-breaks on published_at DESC, deterministic across all
viewers.
Operations checklist#
- A chapter’s slug is its
groups_group.slugand stable across renames (the slug column is unique). Chapter events are bound by group id, not slug, so a slug rename doesn’t break the membership gate. - The membership facade table is
memberships_membership; the scope_type for a chapter isgroup(chapters and interest groups share the same scope type because membership rows are keyed on the group’s UUID regardless of kind). - The notification template table needs one row per
cms.announcement.<category>topic. Stage 18 seeds these viaadminctl seed reference; new categories should add a template row at the same time as the enum value.