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: Germ DM button on profiles

- Shows Germ DM button on profiles with messaging enabled
- Respects showButtonTo policy (everyone, usersIFollow, none)
- Uses grain brand color for button tint
- Following button now uses bordered outline style
- Links to Germ landing page with correct URL format

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

+95 -9
+13
Grain/Assets.xcassets/germ-logo.imageset/Contents.json
··· 1 + { 2 + "images" : [ 3 + { 4 + "filename" : "germ-logo.png", 5 + "idiom" : "universal", 6 + "scale" : "1x" 7 + } 8 + ], 9 + "info" : { 10 + "author" : "xcode", 11 + "version" : 1 12 + } 13 + }
+3
Grain/Assets.xcassets/germ-logo.imageset/germ-logo.png
··· 1 + version https://git-lfs.github.com/spec/v1 2 + oid sha256:7ff47365cc88c12f78d57fbec68b315f8b57f9d20b7243c786b76759b6fc4700 3 + size 237598
+7
Grain/Models/Views/ProfileModels.swift
··· 30 30 var createdAt: String? 31 31 var viewer: ActorViewerState? 32 32 var labels: [ATLabel]? 33 + var messageMe: MessageMe? 33 34 34 35 var id: String { did } 35 36 } ··· 39 40 var following: String? 40 41 var followedBy: String? 41 42 } 43 + 44 + /// Germ Network messageMe declaration 45 + struct MessageMe: Codable, Sendable { 46 + let showButtonTo: String 47 + let messageMeUrl: String 48 + }
+72 -9
Grain/Views/Profile/ProfileView.swift
··· 100 100 .frame(maxWidth: .infinity, alignment: .leading) 101 101 .padding(.horizontal) 102 102 103 - // Follow button 103 + // Follow + Germ DM buttons 104 104 if did != auth.userDID { 105 - Button { 106 - Task { await viewModel.toggleFollow(auth: auth.authContext()) } 107 - } label: { 108 - Text(profile.viewer?.following != nil ? "Following" : "Follow") 109 - .font(.subheadline.weight(.semibold)) 110 - .frame(maxWidth: .infinity) 105 + HStack(spacing: 8) { 106 + followButton(profile: profile) 107 + 108 + if let germUrl = germDMUrl(profile: profile) { 109 + Link(destination: germUrl) { 110 + HStack(spacing: 4) { 111 + Image("germ-logo") 112 + .resizable() 113 + .frame(width: 14, height: 14) 114 + Text("Germ DM") 115 + .font(.subheadline.weight(.semibold)) 116 + } 117 + .frame(maxWidth: .infinity) 118 + } 119 + .buttonStyle(.bordered) 120 + .tint(Color(red: 0.52, green: 0.63, blue: 1.0)) 121 + } 122 + } 123 + .padding(.horizontal) 124 + } else if let germUrl = germDMUrl(profile: profile) { 125 + Link(destination: germUrl) { 126 + HStack(spacing: 4) { 127 + Image("germ-logo") 128 + .resizable() 129 + .frame(width: 14, height: 14) 130 + Text("Germ DM") 131 + .font(.subheadline.weight(.semibold)) 132 + } 133 + .frame(maxWidth: .infinity) 111 134 } 112 - .buttonStyle(.borderedProminent) 113 - .tint(profile.viewer?.following != nil ? .secondary : Color("AccentColor")) 135 + .buttonStyle(.bordered) 136 + .tint(Color(red: 0.52, green: 0.63, blue: 1.0)) 114 137 .padding(.horizontal) 115 138 } 116 139 ··· 242 265 deletedGalleryUri = nil 243 266 } 244 267 } 268 + } 269 + 270 + @ViewBuilder 271 + private func followButton(profile: GrainProfileDetailed) -> some View { 272 + if profile.viewer?.following != nil { 273 + Button { 274 + Task { await viewModel.toggleFollow(auth: auth.authContext()) } 275 + } label: { 276 + Text("Following") 277 + .font(.subheadline.weight(.semibold)) 278 + .frame(maxWidth: .infinity) 279 + } 280 + .buttonStyle(.bordered) 281 + .tint(.primary) 282 + } else { 283 + Button { 284 + Task { await viewModel.toggleFollow(auth: auth.authContext()) } 285 + } label: { 286 + Text("Follow") 287 + .font(.subheadline.weight(.semibold)) 288 + .frame(maxWidth: .infinity) 289 + } 290 + .buttonStyle(.borderedProminent) 291 + .tint(Color("AccentColor")) 292 + } 293 + } 294 + 295 + private func germDMUrl(profile: GrainProfileDetailed) -> URL? { 296 + guard let messageMe = profile.messageMe, 297 + let viewerDid = auth.userDID else { return nil } 298 + let isOwn = did == viewerDid 299 + if !isOwn { 300 + switch messageMe.showButtonTo { 301 + case "everyone": break 302 + case "usersIFollow": 303 + guard profile.viewer?.followedBy != nil else { return nil } 304 + default: return nil 305 + } 306 + } 307 + return URL(string: "\(messageMe.messageMeUrl)/web#\(did)+\(viewerDid)") 245 308 } 246 309 } 247 310