iOS client for Grain grain.social
ios photography atproto
7
fork

Configure Feed

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

fix: dedup location parts in Bluesky cross-post

When Nominatim returned 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 of the name 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>

+176 -4
+22 -4
Grain/Utilities/BlueskyPost.swift
··· 128 128 location: (name: String, address: [String: AnyCodable]?)?, 129 129 description: String? 130 130 ) -> String { 131 - // Build location line (shortened: name, region, country) 131 + // Build location line. `location.name` may be either a POI name 132 + // ("Blue Bottle Coffee") or a Nominatim-formatted fallback that already 133 + // contains locality/state/country ("New York, New York, United States"). 134 + // We take the first comma-separated chunk as the primary label, then 135 + // append locality/region/country while dropping adjacent duplicates — 136 + // this keeps useful context for POIs ("Blue Bottle Coffee, Oakland, California, US") 137 + // while collapsing duplicates for city fallbacks ("New York, US"). 132 138 var locationLine: String? 133 139 if let location { 134 - var parts = [location.name] 140 + let trimmedName = location.name.trimmingCharacters(in: .whitespaces) 141 + let primaryLabel = trimmedName.components(separatedBy: ",").first? 142 + .trimmingCharacters(in: .whitespaces) ?? trimmedName 143 + 144 + var parts: [String] = [] 145 + func appendIfDistinct(_ value: String?) { 146 + guard let value = value?.trimmingCharacters(in: .whitespaces), !value.isEmpty else { return } 147 + if parts.last?.caseInsensitiveCompare(value) == .orderedSame { return } 148 + parts.append(value) 149 + } 150 + 151 + appendIfDistinct(primaryLabel) 135 152 if let address = location.address { 136 - if let region = address["region"]?.stringValue { parts.append(region) } 137 - if let country = address["country"]?.stringValue { parts.append(country) } 153 + appendIfDistinct(address["locality"]?.stringValue) 154 + appendIfDistinct(address["region"]?.stringValue) 155 + appendIfDistinct(address["country"]?.stringValue) 138 156 } 139 157 locationLine = "📍 \(parts.joined(separator: ", "))" 140 158 }
+154
GrainTests/BlueskyPostTests.swift
··· 1 + @testable import Grain 2 + import XCTest 3 + 4 + final class BlueskyPostTests: XCTestCase { 5 + private let url = "https://grain.social/profile/did:plc:abc/gallery/xyz" 6 + 7 + /// POI with distinct name — append locality, region, country 8 + func testBuildPostText_POILocation_IncludesFullContext() { 9 + let text = BlueskyPost.buildPostText( 10 + url: url, 11 + title: "2025-10-12", 12 + location: ( 13 + name: "Overlook Mountain Fire Tower", 14 + address: [ 15 + "locality": AnyCodable("Town of Woodstock"), 16 + "region": AnyCodable("New York"), 17 + "country": AnyCodable("US"), 18 + ] 19 + ), 20 + description: nil 21 + ) 22 + 23 + XCTAssertEqual(text, """ 24 + 2025-10-12 25 + 26 + 📍 Overlook Mountain Fire Tower, Town of Woodstock, New York, US 27 + 28 + #GrainSocial \(url) 29 + """) 30 + } 31 + 32 + /// Coffee shop POI — primary label differs from locality, so locality is kept. 33 + func testBuildPostText_POICafe_IncludesLocality() { 34 + let text = BlueskyPost.buildPostText( 35 + url: url, 36 + title: nil, 37 + location: ( 38 + name: "Blue Bottle Coffee", 39 + address: [ 40 + "locality": AnyCodable("Oakland"), 41 + "region": AnyCodable("California"), 42 + "country": AnyCodable("US"), 43 + ] 44 + ), 45 + description: nil 46 + ) 47 + 48 + XCTAssertTrue(text.contains("📍 Blue Bottle Coffee, Oakland, California, US"), 49 + "Got: \(text)") 50 + } 51 + 52 + /// Nominatim city fallback — `name` already contains locality/region/country. 53 + /// Must NOT duplicate by appending region + country. 54 + func testBuildPostText_CityName_DoesNotDuplicate() { 55 + let text = BlueskyPost.buildPostText( 56 + url: url, 57 + title: "2025-09-14", 58 + location: ( 59 + name: "New York, New York, United States", 60 + address: [ 61 + "locality": AnyCodable("New York"), 62 + "region": AnyCodable("New York"), 63 + "country": AnyCodable("US"), 64 + ] 65 + ), 66 + description: nil 67 + ) 68 + 69 + XCTAssertEqual(text, """ 70 + 2025-09-14 71 + 72 + 📍 New York, US 73 + 74 + #GrainSocial \(url) 75 + """) 76 + } 77 + 78 + /// `name` already includes the state abbreviation. Must not repeat it. 79 + func testBuildPostText_NameWithStateAbbrev_DoesNotDuplicate() { 80 + let text = BlueskyPost.buildPostText( 81 + url: url, 82 + title: nil, 83 + location: ( 84 + name: "Seattle, WA", 85 + address: [ 86 + "locality": AnyCodable("Seattle"), 87 + "region": AnyCodable("WA"), 88 + "country": AnyCodable("US"), 89 + ] 90 + ), 91 + description: nil 92 + ) 93 + 94 + XCTAssertTrue(text.contains("📍 Seattle, WA, US"), "Got: \(text)") 95 + XCTAssertFalse(text.contains("Seattle, WA, Seattle")) 96 + XCTAssertFalse(text.contains("WA, WA")) 97 + } 98 + 99 + /// POI where region differs from the primary label. 100 + func testBuildPostText_CityWithDistinctRegion_IncludesRegion() { 101 + let text = BlueskyPost.buildPostText( 102 + url: url, 103 + title: nil, 104 + location: ( 105 + name: "Albany, New York, United States", 106 + address: [ 107 + "locality": AnyCodable("Albany"), 108 + "region": AnyCodable("New York"), 109 + "country": AnyCodable("US"), 110 + ] 111 + ), 112 + description: nil 113 + ) 114 + 115 + XCTAssertTrue(text.contains("📍 Albany, New York, US"), 116 + "Got: \(text)") 117 + } 118 + 119 + func testBuildPostText_TitleAndDescription_JoinedWithComma() { 120 + let text = BlueskyPost.buildPostText( 121 + url: url, 122 + title: "Sunset at the Beach", 123 + location: nil, 124 + description: "A lovely evening" 125 + ) 126 + 127 + XCTAssertTrue(text.hasPrefix("Sunset at the Beach, A lovely evening")) 128 + } 129 + 130 + func testBuildPostText_NoLocationNoContent_ProducesCleanSuffix() { 131 + let text = BlueskyPost.buildPostText( 132 + url: url, 133 + title: nil, 134 + location: nil, 135 + description: nil 136 + ) 137 + 138 + XCTAssertEqual(text, "\n#GrainSocial \(url)") 139 + } 140 + 141 + func testBuildPostText_AddressOnlyCountry_StillRenders() { 142 + let text = BlueskyPost.buildPostText( 143 + url: url, 144 + title: "Trip", 145 + location: ( 146 + name: "Eiffel Tower", 147 + address: ["country": AnyCodable("FR")] 148 + ), 149 + description: nil 150 + ) 151 + 152 + XCTAssertTrue(text.contains("📍 Eiffel Tower, FR")) 153 + } 154 + }