Overview
Major feature release adding collaborative shared albums and automatic event albums. Albums can now be shared with other members via open (anyone can join) or closed (invite/request only) membership. Events automatically get an album where any logged-in member can contribute photos. Also includes a Chrome-specific BlurImage compositing fix, React Compiler compatibility improvements, and various ESLint/TypeScript fixes.
Shared Albums
Concept
Albums can now be "shared" — multiple members can contribute photos to a single album. Two join policies:
- Open: any member can join instantly
- Closed: members must request to join or be invited by the owner
The album owner can set a per-user photo limit (max_photos_per_user) and manage members (invite, remove, accept/decline requests).
Database (supabase/migrations/00000000000000_baseline.sql)
New tables and columns consolidated into baseline:
shared_album_members— tracks who's a member of which shared albumshared_album_requests— join requests and invites (status: pending/accepted/declined)albums.is_shared,albums.join_policy,albums.max_photos_per_user,albums.event_idalbum_photos.added_by— tracks who added each photo
New RPCs:
add_photos_to_shared_album— adds photos withadded_bytrackingremove_shared_album_photo— removes a photo from a shared albumjoin_shared_album/leave_shared_album— membership managementinvite_to_shared_album/resolve_album_request— invite/request flowremove_album_member— owner removes a memberadd_shared_album_owner— adds album owner as first member when enabling sharing
Components (src/components/albums/)
Seven new components for the shared album UI:
| Component | Purpose |
|-----------|---------|
| JoinAlbumButton | Join open/closed albums, shows pending request status |
| SubmitToSharedAlbumButton | Opens modal to add photos, respects per-user limits |
| SubmitToSharedAlbumContent | Modal content: select from library or upload, shows quota |
| SharedAlbumMemberList | Member list with avatars, owner badge, remove action |
| AlbumRequestsPanel | Pending join requests and invites with accept/decline |
| InviteMembersModal | Search and invite members (excludes existing/pending) |
| AlbumSharedActions | Combines join + submit buttons for public album pages |
Hooks
src/hooks/useSharedAlbumMembers.ts— queries for members, requests, membership status; mutations for join/leave/invite/resolve/removesrc/hooks/useSharedAlbumSubmissions.ts— queries for album photo IDs and per-user count; mutation for adding photos
Management UI Changes
SingleAlbumEditForm— toggle to enable sharing, join policy selector, max photos per userSharedAlbumEditForm— full edit form for shared albums with member managementAlbumEditSidebar— routes to correct form based on album typeAlbumSwitcher— sections for "Your albums", "Your shared albums", "Shared with you", "Event albums"AddPhotosToAlbumModal— shows shared-with-me albums, uses separate RPC for shared albums- Albums page (
/account/albums) — new collapsible sections for shared albums, shared-with-me, event albums, pending invites - Album detail page — read-only sidebar for shared-with-me albums, owner info display
Notifications
src/app/api/albums/requests/notify/route.ts— POST endpoint handling: request received, invite received, request accepted/declined, invite acceptedNotificationContent— renders shared album notification types
Event Albums
Concept
Every event automatically gets a photo album. Any logged-in member can add photos (no join required). The album is "ownerless" — it belongs to the event, not a specific user.
Database
- Trigger on
eventsINSERT creates an album withevent_idset andjoin_policy = null - Backfill creates albums for existing events
- Event soft-delete cascades to album soft-delete
Components
EventPhotosSection(src/components/events/EventPhotosSection.tsx) — displays event album photos in a justified grid with attribution, "Add photos" button for logged-in users- Event page (
src/app/events/[eventSlug]/page.tsx) — fetches event album viagetEventAlbum(), renders photo section
Data Layer
src/lib/eventAlbums.ts— types and helpers (EventAlbum,hasEventPhotos(),getEventPhotoCount())src/lib/data/albums.ts—getEventAlbum()function for server-side fetching
Photo Attribution in Shared Albums
Problem
When viewing a photo in a shared album, the sidebar always showed the album owner's profile instead of the actual photo owner.
Solution
getAlbumPhotoByShortId(src/lib/data/profiles.ts) now fetches the photo owner's profile whenphoto.user_iddiffers from the album owner, and returnsalbumOwnerNicknameseparatelyPhotoPageContentusesalbumOwnerNicknamefor filmstrip/navigation URLs (album lives under the owner's profile) while showing the photo owner in the author rowAlbumContentfetches owner profiles for all unique photo owners viagetProfilesByUserIds()and passes them to the grid
BlurImage Chrome Compositing Fix
Problem
In Chrome, the blurhash placeholder background was bleeding through the edges of the main image during the fade-in animation. This is a Chrome compositor bug where a semi-transparent element composited over its parent's backgroundImage causes edge artifacts.
Solution (src/components/shared/BlurImage.tsx)
Instead of using backgroundImage on the parent wrapper (which Chrome composites as a single layer), the blurhash is now rendered as a separate absolutely-positioned <span> element behind the image:
<span className="block relative overflow-hidden">
{/* Blurhash as separate layer */}
<span
className="absolute inset-0 z-0"
style={{ backgroundImage: `url(${blurhashDataUrl})`, backgroundSize: '100% 100%' }}
/>
{/* Image on top */}
<Image className={`${opacityClass} relative z-10`} onAnimationEnd={handleAnimationEnd} />
</span>
Also added onAnimationEnd to transition from fade-in to visible state, allowing cleanup after the animation completes.
Code Quality Fixes
React Compiler Compatibility
React Hook Form's watch() method returns a mutable object that the React Compiler can't safely memoize. Replaced with useWatch() (a proper hook) across all form components:
// Before — causes "Compilation Skipped: Use of incompatible library"
const { watch } = useForm();
const slug = watch('slug');
// After — React Compiler can track this
const { control } = useForm();
const slug = useWatch({ control, name: 'slug' });
Files affected: SingleAlbumEditForm, SharedAlbumEditForm, SinglePhotoEditForm, BulkPhotoEditForm, BulkAlbumEditForm
ESLint Fixes
- Moved async functions inside
useEffectto fix missing dependency warnings (AnnounceChallengeModal,AnnounceEventModal,EmailAttendeesModal) - Removed unnecessary
onClosefromuseCallbackdeps (using ref pattern instead) - Added missing deps (
supabase,modalContext,updatePosition,reset) - Moved form default values to module-level constants to avoid dependency warnings
- Extracted
sectionIds.join(',')to a variable for stableuseEffectdeps
TypeScript Fixes
- Wrapped
supabase.rpc().then()inPromise.resolve()for strictPromisetyping - Fixed
string | nullvsstring | undefinedmismatches with nullish coalescing - Made
previousAlbumsoptional in mutation type - Fixed duplicate
role/tabIndexprops inSortableGridItemby reordering spread
Other
- Replaced
<img>withnext/imageinInviteMembersModalfor optimization - Refactored
setState-in-effect to render-phase state adjustments inSingleAlbumEditForm
Migration Consolidation
16 individual migrations (from 2026-01-23 through 2026-02-07) were consolidated into the baseline migration. These covered: album photo normalization, stats RPCs, global search, view tracking, signup bypass tokens, photo challenges (full feature), comment replies, and reports.
4 new migrations added:
20260216300000_admin_event_album_permissions.sql— admin permissions for event albums20260216400000_fix_soft_delete_stats.sql— fix stats RPCs to exclude soft-deleted items20260217000000_add_shared_album_owner_rpc.sql— RPC to add album owner as member20260217100000_add_remove_album_member_rpc.sql— RPC to remove album members
All Modified Files (92 total)
New Files (19)
docs/event-albums.md— Event albums documentationdocs/shared-albums.md— Shared albums documentationdocs/shared-albums-revalidation.md— Cache invalidation strategypublic/icons/lock-micro.svg— Lock icon for closed albumspublic/icons/users-micro.svg— Users icon for shared albumssrc/app/api/albums/requests/notify/route.ts— Album notification endpointsrc/components/albums/AlbumRequestsPanel.tsx— Join request managementsrc/components/albums/AlbumSharedActions.tsx— Combined shared album actionssrc/components/albums/InviteMembersModal.tsx— Member invite modalsrc/components/albums/JoinAlbumButton.tsx— Join/request buttonsrc/components/albums/SharedAlbumMemberList.tsx— Member list displaysrc/components/albums/SubmitToSharedAlbumButton.tsx— Submit photos buttonsrc/components/albums/SubmitToSharedAlbumContent.tsx— Photo submission modalsrc/components/events/EventPhotosSection.tsx— Event album photo gridsrc/components/manage/SharedAlbumEditForm.tsx— Shared album edit formsrc/hooks/useSharedAlbumMembers.ts— Membership hookssrc/hooks/useSharedAlbumSubmissions.ts— Submission hookssrc/lib/eventAlbums.ts— Event album types and helperssupabase/migrations/20260216300000_admin_event_album_permissions.sql
Modified Files (73)
README.md,docs/README.md— Updated features and docssrc/app/[nickname]/album/[albumSlug]/AlbumContent.tsx— Shared album attributionsrc/app/[nickname]/album/[albumSlug]/photo/[photoId]/page.tsx— Album owner nickname passthroughsrc/app/account/(manage)/albums/[slug]/AlbumDetailClient.tsx— Shared album detail viewsrc/app/account/(manage)/albums/page.tsx— Shared/event album sectionssrc/app/account/(manage)/photos/page.tsx— Minor updatessrc/app/account/events/page.tsx— Dependency fixsrc/app/api/admin/albums/delete/route.ts— Shared album supportsrc/app/api/admin/albums/suspend/route.ts— Shared album supportsrc/app/api/admin/albums/unsuspend/route.ts— Shared album supportsrc/app/api/comments/route.ts— Shared album contextsrc/app/challenges/[slug]/page.tsx— Minor updatessrc/app/events/[eventSlug]/page.tsx— Event album integrationsrc/app/globals.css— Minor CSS updatessrc/app/help/page.tsx,src/app/page.tsx— Minor updatessrc/app/signup/SignupClient.tsx— Minor updatessrc/components/admin/AnnounceChallengeModal.tsx— ESLint fixessrc/components/admin/AnnounceEventModal.tsx— ESLint fixessrc/components/admin/EmailAttendeesModal.tsx— ESLint fixessrc/components/album/AlbumMiniCard.tsx— Owner nickname displaysrc/components/challenges/SubmitToChallengeContent.tsx— Minor updatessrc/components/events/EventRsvpStatus.tsx,EventSignupBar.tsx— Minor updatessrc/components/layout/Layout.tsx,PageContainer.tsx— Minor updatessrc/components/manage/AddPhotosToAlbumModal.tsx— Shared album supportsrc/components/manage/AddToAlbumContent.tsx— Shared album supportsrc/components/manage/AlbumCard.tsx— Shared album badgessrc/components/manage/AlbumEditSidebar.tsx— Form routingsrc/components/manage/AlbumGrid.tsx— Minor updatessrc/components/manage/AlbumPicker.tsx— Minor updatessrc/components/manage/AlbumSwitcher.tsx— Shared/event sectionssrc/components/manage/BulkAlbumEditForm.tsx— useWatch fixsrc/components/manage/BulkPhotoEditForm.tsx— useWatch fixsrc/components/manage/ManageLayout.tsx— Minor updatessrc/components/manage/MobileActionBar.tsx— Shared album actionssrc/components/manage/PhotoCard.tsx— Owner attributionsrc/components/manage/PhotoEditSidebar.tsx— Minor updatessrc/components/manage/PhotoGrid.tsx— Attribution supportsrc/components/manage/SelectableGrid.tsx— Minor updatessrc/components/manage/SingleAlbumEditForm.tsx— Sharing toggle, useWatch fixsrc/components/manage/SinglePhotoEditForm.tsx— useWatch fixsrc/components/manage/SortableGridItem.tsx— Prop spreading fixsrc/components/notifications/NotificationContent.tsx— Album notification typessrc/components/notifications/ToastProvider.tsx— Minor updatessrc/components/photo/JustifiedPhotoGrid.tsx— Attribution supportsrc/components/photo/PhotoPageContent.tsx— Album owner nicknamesrc/components/shared/AlbumActionsMenu.tsx,AlbumActionsPopover.tsx— Minor updatessrc/components/shared/BlurImage.tsx— Chrome compositing fixsrc/components/shared/Button.tsx— Minor updatessrc/components/shared/ReportModal.tsx— Minor updatessrc/components/shared/SignUpCTA.tsx— Minor updatessrc/components/shared/Tooltip.tsx— Dependency fixsrc/content/help/photos.tsx— Shared album FAQ entriessrc/context/ManageDataContext.tsx— Minor updatessrc/database.types.ts— New tables, RPCs, and fieldssrc/hooks/useAccountForm.ts— Dependency fixsrc/hooks/useActiveHelpSection.ts— Dependency fixsrc/hooks/useAlbumMutations.ts— Shared album mutationssrc/hooks/useAlbumPhotoMutations.ts— Shared album supportsrc/hooks/useAlbumPhotos.ts— Null handling fixessrc/hooks/useAlbums.ts— Shared/event album hookssrc/lib/actions/likes.ts— Minor updatessrc/lib/data/albums.ts— getProfilesByUserIds, getEventAlbumsrc/lib/data/likes.ts— Minor updatessrc/lib/data/profiles.ts— Photo owner attributionsrc/types/albums.ts— Shared album typessrc/types/events.ts— Event album fieldssrc/types/notifications.ts— Album notification typessrc/types/photos.ts— Attribution fieldssrc/utils/confirmHelpers.tsx— Shared album confirmationssupabase/migrations/00000000000000_baseline.sql— Consolidated + shared albums