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.

feat: include EXIF preference toggle in settings and gallery creation

Add "Include camera data (EXIF) when uploading" toggle to Settings,
synced with the server via dev.hatk.putPreference (same key as web).
Gallery creation now respects this preference, skipping EXIF record
creation when disabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+55 -2
+8
Grain/API/Endpoints/FeedEndpoints.swift
··· 108 108 try await procedure("dev.hatk.putPreference", input: Input(key: "pinnedFeeds", value: feeds), auth: auth) 109 109 } 110 110 111 + func putIncludeExif(_ value: Bool, auth: AuthContext? = nil) async throws { 112 + struct Input: Encodable { 113 + let key: String 114 + let value: Bool 115 + } 116 + try await procedure("dev.hatk.putPreference", input: Input(key: "includeExif", value: value), auth: auth) 117 + } 118 + 111 119 func searchGalleries( 112 120 query q: String, 113 121 limit: Int = 30,
+14
Grain/ViewModels/FeedPreferencesViewModel.swift
··· 5 5 final class FeedPreferencesViewModel { 6 6 var pinnedFeeds: [PinnedFeed] = PinnedFeed.defaults 7 7 var selectedFeedId: String = "recent" 8 + var includeExif: Bool = true 8 9 private var hasLoaded = false 9 10 10 11 private let client: XRPCClient ··· 32 33 selectedFeedId = feeds.first?.id ?? "recent" 33 34 } 34 35 } 36 + if let exif = response.preferences.includeExif { 37 + includeExif = exif 38 + } 35 39 } catch { 36 40 // Fall back to defaults, already set 37 41 } ··· 49 53 try await client.putPinnedFeeds(updated, auth: auth) 50 54 } catch { 51 55 pinnedFeeds.removeAll { $0.id == feed.id } 56 + } 57 + } 58 + 59 + func setIncludeExif(_ value: Bool, auth: AuthContext?) async { 60 + let previous = includeExif 61 + includeExif = value 62 + do { 63 + try await client.putIncludeExif(value, auth: auth) 64 + } catch { 65 + includeExif = previous 52 66 } 53 67 } 54 68
+12 -2
Grain/Views/Create/CreateGalleryView.swift
··· 23 23 @State private var photoItems: [PhotoItem] = [] 24 24 @State private var mentionState = MentionAutocompleteState() 25 25 @State private var postToBluesky = false 26 + @State private var includeExif = true 26 27 27 28 let client: XRPCClient 28 29 var onCreated: (() -> Void)? ··· 177 178 } 178 179 .ignoresSafeArea() 179 180 } 181 + .task { 182 + if let authContext = auth.authContext() { 183 + if let prefs = try? await client.getPreferences(auth: authContext).preferences { 184 + if let exif = prefs.includeExif { 185 + includeExif = exif 186 + } 187 + } 188 + } 189 + } 180 190 .navigationTitle("New Gallery") 181 191 .toolbar { 182 192 ToolbarItem(placement: .cancellationAction) { ··· 329 339 guard let uri = result.uri else { continue } 330 340 photoUris.append(uri) 331 341 332 - // Create EXIF record if we extracted metadata 333 - if var exif = photo.exif { 342 + // Create EXIF record if we extracted metadata and user has it enabled 343 + if includeExif, var exif = photo.exif { 334 344 exif["photo"] = AnyCodable(uri) 335 345 exif["createdAt"] = AnyCodable(now) 336 346 _ = try await client.createRecord(
+21
Grain/Views/Settings/SettingsView.swift
··· 5 5 @Environment(\.dismiss) private var dismiss 6 6 let client: XRPCClient 7 7 var onProfileEdited: (() -> Void)? 8 + @State private var includeExif = true 9 + @State private var hasLoadedExifPref = false 8 10 9 11 var body: some View { 10 12 List { ··· 24 26 } 25 27 } 26 28 29 + Section("Photos") { 30 + Toggle("Include camera data (EXIF) when uploading", isOn: $includeExif) 31 + .onChange(of: includeExif) { 32 + guard hasLoadedExifPref else { return } 33 + Task { 34 + guard let authContext = auth.authContext() else { return } 35 + try? await client.putIncludeExif(includeExif, auth: authContext) 36 + } 37 + } 38 + } 39 + 27 40 Section("Legal") { 28 41 Link("Privacy Policy", destination: URL(string: "https://grain.social/support/privacy")!) 29 42 Link("Terms of Service", destination: URL(string: "https://grain.social/support/terms")!) ··· 42 55 } 43 56 } 44 57 .navigationTitle("Settings") 58 + .task { 59 + if let authContext = auth.authContext(), 60 + let prefs = try? await client.getPreferences(auth: authContext).preferences, 61 + let exif = prefs.includeExif { 62 + includeExif = exif 63 + } 64 + hasLoadedExifPref = true 65 + } 45 66 } 46 67 }