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 of kind=chapter you’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:

  1. Pinned announcements (feed-campus-pinned). Any announcement whose pin_until is in the future ranks here, sorted by pin_until DESC so 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).
  2. All other announcements (feed-campus-list). Published announcements visible to the caller, ordered by published_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:

CategoryTypical use
academicRecess windows, exam-week procedures, semester rollover.
campusFest schedules, library hours, dining-hall changes.
chapterAlumni chapter meetups, regional events, chapter newsletters.
alumniCross-chapter alumni announcements (reunions, BITSian Day).
placementsDrive results, internship program calls, on-campus interviews.
adminCatch-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:

  1. 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.
  2. Per-category subscription — a row in cms_announcement_subscription keyed 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.
  3. 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 over ListEvents with the membership facade enforcing the chapter scope on the read side. Non-members get PermissionDenied; members get a paginated, time-windowed list ordered by starts_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:

  1. Open /admin/cms/announcementsNew announcement.
  2. Fill Title, Body, Audience scope as before.
  3. Pick Category = Chapter (or another category as appropriate).
  4. 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.
  5. Optional: paste a Cover image URL. The image renders inside AnnouncementCard on the Campus tab.
  6. Click Save → row lands as draft.
  7. From the announcements grid, click Publish on the row. Fan-out targets every audience-scoped recipient minus anyone with an explicit opted_in=false row for the announcement’s category. recipients_notified on 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.slug and 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 is group (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 via adminctl seed reference; new categories should add a template row at the same time as the enum value.