commits
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up handle re-resolution fixes: #identity firehose events without
a handle field now re-resolve from plc.directory, and backfill no
longer caches stale PDS resolutions across the process lifetime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two HTML templates + rendered PNGs for marketing/social use:
- hero.html / hero-16x9.png — 16:9 hero image (3840x2160) with gallery
card collage, "grain" wordmark, and "Share your photography" tagline
- banner.html / banner.png — 3:1 Bluesky banner (3000x1000) with
card collage only, no branding
Both use real galleries from the recent feed with proper attribution
(avatar + title + handle on each card), matching the OG card aesthetic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A gallery published with a future-dated createdAt (e.g. client clock
skew) was pinning itself to the top of /recent — and inflating the
freshness term in /foryou's time-decay scoring — until wall-clock time
caught up. Order chronological feeds by min(created_at, indexed_at)
instead, so future-dated records slot in at their actual ingest time.
Backdated values (e.g. /settings/import preserving the original
Bluesky post date) still sort by created_at since min picks the
smaller value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The POI+locality+country fallback was guarded with region IS NULL to
avoid pulling Washington DC records into Seattle clicks, but that also
rejected NYC POI records (region="New York"). Loosen to
(region IS NULL OR region = parts[-2]) so a click on "India Street,
New York, US" matches NYC records (region="New York" = parts[-2])
while still keeping DC out (region="District of Columbia", neither
null nor "Washington").
Verified against 500 sampled prod records: 458/500 now self-match via
the name-based query (up from 406), zero over-match from Seattle
clicks to DC. The remaining 42 are POI+region or POI+country-only
cases that fall through to the exact H3-cell match.
- GalleryCard: pass the server-computed locationDisplay (full formatted
address) as the ?name= param instead of the raw location.name, so the
feed parser receives structured data to work with.
- Location feed: take the last 3 parts as [locality, region, country] so
displays with a POI prefix parse correctly. Add a [POI, locality, country]
fallback restricted to records where region IS NULL — fixes POI-in-city
records without address region without over-matching cases like "Seattle,
Washington, US" into Washington DC galleries. For 1-part names, also try
matching as locality so "Seattle" finds Seattle records.
- If the name-based query returns zero rows and the URL carries an H3
cell, fall through to the legacy H3 path. Ensures a user clicking a
gallery's location link always sees at least that cell's galleries
instead of an empty page.
- getLocations: use normalised ISO-2 code in multi-part display names so
all rows in a group render identically (fixes "Portland, Oregon, USA"
appearing alongside "...US" entries). Country-only groups expand to the
full country name via Intl.DisplayNames ("GR" → "Greece").
- country helper: add name→code reverse lookup so clicking "Greece" in the
sidebar still matches records stored as "GR". Brute-forces 2-letter codes
since Intl.supportedValuesOf doesn't accept "region". Prefers earlier
alphabetical codes when names collide (GB wins over UK).
- formatStoredLocation: normalise the country tail on gallery cards and
suppress it when the primary label already names the country (fixes
"Greece, GR" on the gallery card).
Inline comments near each relevant file capturing work discussed but not
shipped in ca49c1b: /place/[slug] URL scheme, capturing osm_type/osm_id
from Nominatim, source of the "USA" country variant, limit param for the
index pages, and native parity with the multi-cell union behavior.
- Flatten sidebar cards into a single continuous minimal layout; search as
bordered input, uppercase section labels, compact footer with dot separators.
- Dedupe locations server-side: getLocations groups by (locality, region,
country) with USA→US country alias. getFeed?feed=location accepts a name
param and unions all H3 cells sharing the display label via case-insensitive
address matching.
- Return h3Cells on LocationItem so LocationMapBanner can render a centroid +
dynamic zoom across all cells in a place (was showing only one cell).
- Normalize camera names server-side in getCameras: strip manufacturer
legalese, dedup adjacent tokens, title-case all-caps brands. Rows that
collide after normalization merge. Camera feed matches by
normalization-equivalence so old raw URLs and new cleaned URLs both work.
- Add /cameras and /locations index pages with "See all →" links in the
sidebar when the section has more than seven items.
Adds a Delete Account row in Settings, Account that calls the new
deleteAccount xrpc, signs the user out, and returns them to the home
page. Styled red to match the Sign Out button. Two confirmation dialogs
guard the action and any server error is surfaced inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds social.grain.unspecced.deleteAccount procedure so clients can
satisfy App Store guideline 5.1.1(v) with a single call. Deletes every
Grain record owned by the viewer across all ten grain.* collections
(photos, exif, gallery.items, galleries, stories, favorites, comments,
follows, blocks, actor profile), then clears appview-side state the user
created (mutes, preferences, push tokens) and revokes their OAuth
session. Reports and labels are moderation records and are preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a formatted locationDisplay string to galleryView and storyView so
clients render one field instead of re-implementing dedup and formatting
logic per platform. Handles POI names ("Blue Bottle Coffee, Oakland,
California, US"), Nominatim city fallbacks ("New York, US"), county-in-
name leakage from older clients ("Kansas City, Missouri, US"), and legacy
hthree records without structured address (preserved as-is).
- lexicons: add locationDisplay to gallery and story view defs
- server/helpers/formatLocation.ts: canonical implementation
- test/formatLocation.test.ts: 11 vitest cases across real-world shapes
- server/hydrate: include locationDisplay in hydrated views
- app/lib/utils/formatLocation.ts: re-export so $lib consumers don't
reach into server/
- app/lib/utils/bsky-post.ts: use shared helper for cross-post line
- UI components: render locationDisplay ?? location.name ?? fallback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The gallery create page was calling createBskyPost without a title, so
every cross-posted gallery appeared on Bluesky without its title. Pass
title alongside description.
Also mirror the native app's location dedup: when Nominatim returns a
formatted fallback name ("New York, New York, United States"), the old
logic appended region and country on top, producing "New York, New York,
United States, New York, US". Now we take the first comma-separated
chunk as the primary label and append locality/region/country while
skipping case-insensitive adjacent duplicates — preserving POI context
("Blue Bottle Coffee, Oakland, California, US") while collapsing
city-fallback redundancy ("New York, US").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract compactCount util (1K, 1.2M) and apply to gallery fav counts,
comment fav counts, and profile popover stats.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ability to favorite comments using the existing social.grain.favorite
lexicon. Adds favCount and viewer state to the comment view, hydrates
favorites in getCommentThread, sends push notifications for comment
favorites, and includes comment-favorite in the notification feed with
grouping support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Upgrade hatk to alpha.58 and configure CDN with imgproxy signing
in production. Images served via cdn.grain.social through Bunny CDN.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add select mode to gallery grid with SelectCheck atom, floating action
bar with confirmation dialog, progress tracking, and partial failure
handling. Refactor import page to share the same SelectCheck component.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch import from individual createRecord calls to batched applyWrites,
reducing round trips from N+1 to 2 per post. Bump hatk to alpha.57 which
includes the applyWrites proxy and a fix for firehose commit indexing
that matched blocks by $type instead of by CID.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each post in the import review now has a ContentWarningPicker for
self-labeling. Settings link shows a Beta badge. Review header uses
glassmorphism background matching the detail header.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parse Instagram JSON export zip in-browser, preview posts with
thumbnails, and batch-import as galleries. Titles use the post date,
captions map to descriptions (truncated to 1000 chars), and original
timestamps are preserved as createdAt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the profile OpenGraph image grid with the same collage layout
used by gallery OG images, showing the first photo from the latest 10
galleries. Extract collage algorithm into shared server/og/collage.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Show gradient story ring on gallery card and popover avatars when
author has an active story, tapping opens story viewer
- Redesign "Your story" button to show viewer avatar with plus badge
and create/view menu when user has existing stories
- Filter own user from story author list to avoid duplication
- Extract SettingsGroup and SettingsToggleRow reusable atoms
- Refactor upload-defaults page to use new atoms
- Remove icons from settings hub rows
- Fix Avatar ring padding and hover clipping
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show a filled stacked squares icon (SF Symbols style) in the top-right
corner of gallery thumbnails when a gallery contains more than one photo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add standard line-clamp property in NotificationItem
- Fix a11y click handler warnings in StoryViewer
- Remove unused .comment-author CSS selector in StoryViewer
- Replace deprecated svelte:component with dynamic component in SidebarRight
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add share button to profile overflow menu with native share/clipboard fallback
- Replace login modal subtitle with atmosphere app logos marquee
- Change active feed tab border color to grain accent color
- Add ring around grouped notification avatars matching bg-root/bg-hover
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Account page with handle, DID, and manage data link
- Add Upload Defaults page (location, camera data toggles)
- Move privacy settings out of Edit Profile into Upload Defaults
- Add legal links and sign out to main settings page
- Update privacy policy: add location data section with H3/reverse
geocoding details, correct EXIF metadata description
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Query unseen notification count at push time and include it in the
APNs payload so the app icon badge updates on every push.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Restore gallery/story thumbnails (44px, right-aligned)
- Use 38px grouped avatars with -8px overlap to match native
- Stack avatar above text body for single notifications
- Inline timestamp with action text
- Vertically center icons with avatars
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- FeedTabs appends current page's feed as a tab even when not pinned
- Remove redundant "Pinned" section from My Feeds that duplicated custom feeds
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- `/` now renders whatever feed is first in the pinned list
- Add dedicated `/feeds/recent` route so recent is accessible when not first
- Update FeedTabs, SidebarRight, MobileDrawer to link first pin to `/`
- Migrate old `path: "/"` to `/feeds/recent` for existing users
- Redirect to custom feed path if non-core feed is pinned first
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This reverts commit 2526728d53430dbb95771ee8f548b0be97485291.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Redirect / to first pinned feed if recent isn't in the list
- Prevent unpinning the last remaining feed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix $effect resetting paginated state by only syncing from query once
- Increase notification fetch limit to 100 to match native
- Remove gallery/story thumbnail images from notification items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add notification settings page under /settings/notifications
- Filter notifications by category (favorites, follows, comments, mentions)
- Support push and inApp toggles per category
- Respect "from follows only" preference for push notifications
- Add settings link in main settings page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add notification grouping (favorites, follows) with overlapping avatars
- Show author name + @handle + timestamp on first line, action on second
- Add gallery title context line for gallery-related notifications
- Use filled icons for heart, follow, and comment
- Include author description in API response for notification profiles
- Use avatar preset for gallery/story thumbnails
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Moved the Bluesky cross-post icon from an absolute-positioned overlay
to inline between the comment input and favorite button in the bottom bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add resolveHandle helper to convert handles to DIDs via _repos table.
Apply AT URI handle resolution in getGallery and getStory so URLs like
/profile/chadtmiller.com/gallery/... work without frontend resolution.
Bump hatk to alpha.54 for feed-layer handle resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Capture click position before the setTimeout since e.currentTarget
is null by the time the delayed callback fires.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two HTML templates + rendered PNGs for marketing/social use:
- hero.html / hero-16x9.png — 16:9 hero image (3840x2160) with gallery
card collage, "grain" wordmark, and "Share your photography" tagline
- banner.html / banner.png — 3:1 Bluesky banner (3000x1000) with
card collage only, no branding
Both use real galleries from the recent feed with proper attribution
(avatar + title + handle on each card), matching the OG card aesthetic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A gallery published with a future-dated createdAt (e.g. client clock
skew) was pinning itself to the top of /recent — and inflating the
freshness term in /foryou's time-decay scoring — until wall-clock time
caught up. Order chronological feeds by min(created_at, indexed_at)
instead, so future-dated records slot in at their actual ingest time.
Backdated values (e.g. /settings/import preserving the original
Bluesky post date) still sort by created_at since min picks the
smaller value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The POI+locality+country fallback was guarded with region IS NULL to
avoid pulling Washington DC records into Seattle clicks, but that also
rejected NYC POI records (region="New York"). Loosen to
(region IS NULL OR region = parts[-2]) so a click on "India Street,
New York, US" matches NYC records (region="New York" = parts[-2])
while still keeping DC out (region="District of Columbia", neither
null nor "Washington").
Verified against 500 sampled prod records: 458/500 now self-match via
the name-based query (up from 406), zero over-match from Seattle
clicks to DC. The remaining 42 are POI+region or POI+country-only
cases that fall through to the exact H3-cell match.
- GalleryCard: pass the server-computed locationDisplay (full formatted
address) as the ?name= param instead of the raw location.name, so the
feed parser receives structured data to work with.
- Location feed: take the last 3 parts as [locality, region, country] so
displays with a POI prefix parse correctly. Add a [POI, locality, country]
fallback restricted to records where region IS NULL — fixes POI-in-city
records without address region without over-matching cases like "Seattle,
Washington, US" into Washington DC galleries. For 1-part names, also try
matching as locality so "Seattle" finds Seattle records.
- If the name-based query returns zero rows and the URL carries an H3
cell, fall through to the legacy H3 path. Ensures a user clicking a
gallery's location link always sees at least that cell's galleries
instead of an empty page.
- getLocations: use normalised ISO-2 code in multi-part display names so
all rows in a group render identically (fixes "Portland, Oregon, USA"
appearing alongside "...US" entries). Country-only groups expand to the
full country name via Intl.DisplayNames ("GR" → "Greece").
- country helper: add name→code reverse lookup so clicking "Greece" in the
sidebar still matches records stored as "GR". Brute-forces 2-letter codes
since Intl.supportedValuesOf doesn't accept "region". Prefers earlier
alphabetical codes when names collide (GB wins over UK).
- formatStoredLocation: normalise the country tail on gallery cards and
suppress it when the primary label already names the country (fixes
"Greece, GR" on the gallery card).
- Flatten sidebar cards into a single continuous minimal layout; search as
bordered input, uppercase section labels, compact footer with dot separators.
- Dedupe locations server-side: getLocations groups by (locality, region,
country) with USA→US country alias. getFeed?feed=location accepts a name
param and unions all H3 cells sharing the display label via case-insensitive
address matching.
- Return h3Cells on LocationItem so LocationMapBanner can render a centroid +
dynamic zoom across all cells in a place (was showing only one cell).
- Normalize camera names server-side in getCameras: strip manufacturer
legalese, dedup adjacent tokens, title-case all-caps brands. Rows that
collide after normalization merge. Camera feed matches by
normalization-equivalence so old raw URLs and new cleaned URLs both work.
- Add /cameras and /locations index pages with "See all →" links in the
sidebar when the section has more than seven items.
Adds a Delete Account row in Settings, Account that calls the new
deleteAccount xrpc, signs the user out, and returns them to the home
page. Styled red to match the Sign Out button. Two confirmation dialogs
guard the action and any server error is surfaced inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds social.grain.unspecced.deleteAccount procedure so clients can
satisfy App Store guideline 5.1.1(v) with a single call. Deletes every
Grain record owned by the viewer across all ten grain.* collections
(photos, exif, gallery.items, galleries, stories, favorites, comments,
follows, blocks, actor profile), then clears appview-side state the user
created (mutes, preferences, push tokens) and revokes their OAuth
session. Reports and labels are moderation records and are preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a formatted locationDisplay string to galleryView and storyView so
clients render one field instead of re-implementing dedup and formatting
logic per platform. Handles POI names ("Blue Bottle Coffee, Oakland,
California, US"), Nominatim city fallbacks ("New York, US"), county-in-
name leakage from older clients ("Kansas City, Missouri, US"), and legacy
hthree records without structured address (preserved as-is).
- lexicons: add locationDisplay to gallery and story view defs
- server/helpers/formatLocation.ts: canonical implementation
- test/formatLocation.test.ts: 11 vitest cases across real-world shapes
- server/hydrate: include locationDisplay in hydrated views
- app/lib/utils/formatLocation.ts: re-export so $lib consumers don't
reach into server/
- app/lib/utils/bsky-post.ts: use shared helper for cross-post line
- UI components: render locationDisplay ?? location.name ?? fallback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The gallery create page was calling createBskyPost without a title, so
every cross-posted gallery appeared on Bluesky without its title. Pass
title alongside description.
Also mirror the native app's location dedup: when Nominatim returns a
formatted fallback name ("New York, New York, United States"), the old
logic appended region and country on top, producing "New York, New York,
United States, New York, US". Now we take the first comma-separated
chunk as the primary label and append locality/region/country while
skipping case-insensitive adjacent duplicates — preserving POI context
("Blue Bottle Coffee, Oakland, California, US") while collapsing
city-fallback redundancy ("New York, US").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add ability to favorite comments using the existing social.grain.favorite
lexicon. Adds favCount and viewer state to the comment view, hydrates
favorites in getCommentThread, sends push notifications for comment
favorites, and includes comment-favorite in the notification feed with
grouping support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch import from individual createRecord calls to batched applyWrites,
reducing round trips from N+1 to 2 per post. Bump hatk to alpha.57 which
includes the applyWrites proxy and a fix for firehose commit indexing
that matched blocks by $type instead of by CID.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Show gradient story ring on gallery card and popover avatars when
author has an active story, tapping opens story viewer
- Redesign "Your story" button to show viewer avatar with plus badge
and create/view menu when user has existing stories
- Filter own user from story author list to avoid duplication
- Extract SettingsGroup and SettingsToggleRow reusable atoms
- Refactor upload-defaults page to use new atoms
- Remove icons from settings hub rows
- Fix Avatar ring padding and hover clipping
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add share button to profile overflow menu with native share/clipboard fallback
- Replace login modal subtitle with atmosphere app logos marquee
- Change active feed tab border color to grain accent color
- Add ring around grouped notification avatars matching bg-root/bg-hover
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Account page with handle, DID, and manage data link
- Add Upload Defaults page (location, camera data toggles)
- Move privacy settings out of Edit Profile into Upload Defaults
- Add legal links and sign out to main settings page
- Update privacy policy: add location data section with H3/reverse
geocoding details, correct EXIF metadata description
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- `/` now renders whatever feed is first in the pinned list
- Add dedicated `/feeds/recent` route so recent is accessible when not first
- Update FeedTabs, SidebarRight, MobileDrawer to link first pin to `/`
- Migrate old `path: "/"` to `/feeds/recent` for existing users
- Redirect to custom feed path if non-core feed is pinned first
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add notification settings page under /settings/notifications
- Filter notifications by category (favorites, follows, comments, mentions)
- Support push and inApp toggles per category
- Respect "from follows only" preference for push notifications
- Add settings link in main settings page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add notification grouping (favorites, follows) with overlapping avatars
- Show author name + @handle + timestamp on first line, action on second
- Add gallery title context line for gallery-related notifications
- Use filled icons for heart, follow, and comment
- Include author description in API response for notification profiles
- Use avatar preset for gallery/story thumbnails
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add resolveHandle helper to convert handles to DIDs via _repos table.
Apply AT URI handle resolution in getGallery and getStory so URLs like
/profile/chadtmiller.com/gallery/... work without frontend resolution.
Bump hatk to alpha.54 for feed-layer handle resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>