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: gallery location links fall back to cell when name lookup empty

- 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.

+40 -4
+1 -1
app/lib/components/molecules/GalleryCard.svelte
··· 169 169 </span> 170 170 {#if gallery.location} 171 171 <!-- svelte-ignore node_invalid_placement_ssr --> 172 - <a class="location-link" href="/location/{encodeURIComponent(gallery.location.value)}?name={encodeURIComponent(gallery.location.name ?? gallery.location.value)}" onclick={(e) => e.stopPropagation()}> 172 + <a class="location-link" href="/location/{encodeURIComponent(gallery.location.value)}?name={encodeURIComponent(gallery.locationDisplay ?? gallery.location.name ?? gallery.location.value)}" onclick={(e) => e.stopPropagation()}> 173 173 {gallery.locationDisplay ?? gallery.location.name ?? gallery.location.value} 174 174 </a> 175 175 {/if}
+39 -3
server/feeds/location.ts
··· 53 53 .map((s) => s.trim()) 54 54 .filter(Boolean); 55 55 56 - type Interp = { locality?: string; region?: string; country?: string }; 56 + type Interp = { 57 + locality?: string; 58 + region?: string; 59 + country?: string; 60 + // When true, require the matched record to have region IS NULL. 61 + // Used for the [POI, locality, country] 3-part fallback so that 62 + // "Seattle, Washington, US" doesn't pull in Washington DC records 63 + // (locality=Washington, region=District of Columbia) while still 64 + // catching records with no region like "Tokyo Midtown, Minato, JP". 65 + regionMustBeNull?: boolean; 66 + }; 57 67 const interps: Interp[] = []; 58 68 if (parts.length >= 3) { 59 - interps.push({ locality: parts[0], region: parts[1], country: parts[2] }); 69 + // Take the LAST three parts as [locality, region, country] so displays 70 + // with a POI prefix ("The Space Needle, Seattle, Washington, US" or 71 + // "Northeast 33rd Drive, Portland, Oregon, US") parse correctly. 72 + const [locality, region, country] = parts.slice(-3); 73 + interps.push({ locality, region, country }); 74 + // Also try [POI, locality, country] for records that legitimately 75 + // have no region (common for non-US places and POIs). 76 + interps.push({ 77 + locality: parts[parts.length - 2], 78 + country: parts[parts.length - 1], 79 + regionMustBeNull: true, 80 + }); 60 81 } else if (parts.length === 2) { 82 + // Ambiguous: could be [locality, country] ("Paris, FR") or 83 + // [region, country] ("Oregon, US"). Try both; only the right one 84 + // matches records. 61 85 interps.push({ locality: parts[0], country: parts[1] }); 62 86 interps.push({ region: parts[0], country: parts[1] }); 63 87 } else if (parts.length === 1) { 88 + // Ambiguous: could be a country ("Greece") or a locality 89 + // ("Seattle"). Try both. 64 90 interps.push({ country: parts[0] }); 91 + interps.push({ locality: parts[0] }); 65 92 } 66 93 67 94 const viewer = ctx.viewer?.did; ··· 86 113 matches.push(`UPPER(json_extract(t.address, '$.country')) IN (${placeholders})`); 87 114 params.push(...aliases); 88 115 } 116 + if (interp.regionMustBeNull) { 117 + matches.push(`json_extract(t.address, '$.region') IS NULL`); 118 + } 89 119 if (matches.length) interpClauses.push(`(${matches.join(" AND ")})`); 90 120 } 91 121 ··· 107 137 ${bmFilter}`, 108 138 { orderBy: "t.created_at", params }, 109 139 ); 110 - return ctx.ok({ uris: rows.map((r) => r.uri), cursor }); 140 + 141 + // If the name-based query found matches, or we have no H3 to fall back to, 142 + // return. Otherwise fall through to the legacy H3 path — at minimum the 143 + // user sees the clicked gallery's own cell instead of an empty page. 144 + if (rows.length > 0 || !location) { 145 + return ctx.ok({ uris: rows.map((r) => r.uri), cursor }); 146 + } 111 147 } 112 148 113 149 if (!location) return ctx.ok({ uris: [] });