grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: include gallery title and dedup location in Bluesky cross-post

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>

+26 -5
+25 -5
app/lib/utils/bsky-post.ts
··· 26 26 27 27 const graphemeLength = (s: string) => [...new Intl.Segmenter().segment(s)].length; 28 28 29 - // Build location line (shortened: name, region, country) 29 + // Build location line. `location.name` may be either a POI name 30 + // ("Blue Bottle Coffee") or a Nominatim-formatted fallback that already 31 + // contains locality/state/country ("New York, New York, United States"). 32 + // We take the first comma-separated chunk as the primary label, then 33 + // append locality/region/country while dropping adjacent duplicates — 34 + // this keeps useful context for POIs ("Blue Bottle Coffee, Oakland, California, US") 35 + // while collapsing duplicates for city fallbacks ("New York, US"). 30 36 let locationLine: string | null = null; 31 37 if (location) { 32 - const parts = [location.name]; 33 - if (location.address?.region) parts.push(location.address.region); 34 - if (location.address?.country) parts.push(location.address.country); 38 + const trimmedName = location.name.trim(); 39 + const primaryLabel = trimmedName.split(",")[0].trim() || trimmedName; 40 + const parts: string[] = []; 41 + const appendIfDistinct = (value?: string) => { 42 + const v = value?.trim(); 43 + if (!v) return; 44 + if (parts[parts.length - 1]?.toLowerCase() === v.toLowerCase()) return; 45 + parts.push(v); 46 + }; 47 + appendIfDistinct(primaryLabel); 48 + appendIfDistinct(location.address?.locality); 49 + appendIfDistinct(location.address?.region); 50 + appendIfDistinct(location.address?.country); 35 51 locationLine = `📍 ${parts.join(", ")}`; 36 52 } 37 53 ··· 57 73 58 74 if (content && graphemeLength(content) > maxContent) { 59 75 const segments = [...new Intl.Segmenter().segment(content)]; 60 - content = segments.slice(0, Math.max(0, maxContent - 1)).map((s) => s.segment).join("") + "…"; 76 + content = 77 + segments 78 + .slice(0, Math.max(0, maxContent - 1)) 79 + .map((s) => s.segment) 80 + .join("") + "…"; 61 81 } 62 82 63 83 const lines: string[] = [];
+1
app/routes/create/+page.svelte
··· 294 294 const galleryUrl = `${window.location.origin}/profile/${$viewer.did}/gallery/${galleryRkey}` 295 295 await createBskyPost({ 296 296 url: galleryUrl, 297 + title: title.trim() || undefined, 297 298 location: location ? { name: location.name, address: location.address } : null, 298 299 description: description.trim() || undefined, 299 300 images: photos,