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: UI polish pass

- Glass post button and iMessage-style comment input
- Solid pink-red heart color across gallery cards, stories, notifications
- Replace custom image cache with NukeUI LazyImage for reliable loading
- Avatar border rings removed, notification avatar sizes matched
- Story + icon: white on indigo accent
- Profile buttons: glass style with rounded rectangle shape
- Context menu on logged-in user's story avatar
- Collapsible exif section when no metadata present
- Scrollable alt text overlay for panoramic images
- Location search disambiguation with Nominatim display_name
- AccentColor asset catalog properly wired

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

+131 -141
+12
Grain/Assets.xcassets/AccentColor.colorset/Contents.json
··· 10 10 "alpha" : "1.000" 11 11 } 12 12 }, 13 + "idiom" : "universal" 14 + }, 15 + { 16 + "color" : { 17 + "color-space" : "srgb", 18 + "components" : { 19 + "red" : "0.180", 20 + "green" : "0.230", 21 + "blue" : "0.780", 22 + "alpha" : "1.000" 23 + } 24 + }, 13 25 "idiom" : "universal", 14 26 "appearances" : [ 15 27 {
+2 -2
Grain/GrainApp.swift
··· 58 58 .environment(storyStatusCache) 59 59 .environment(viewedStoryStorage) 60 60 .environment(labelDefsCache) 61 - .tint(Color("AccentColor")) 61 + .tint(Color.accentColor) 62 62 .onAppear { 63 63 appSignposter.emitEvent("WindowOnAppear") 64 64 Task { ··· 78 78 } else { 79 79 LoginView() 80 80 .environment(authManager) 81 - .tint(Color("AccentColor")) 81 + .tint(Color.accentColor) 82 82 } 83 83 } 84 84 .onOpenURL { url in
+11 -1
Grain/Utilities/LocationServices.swift
··· 29 29 let addr = json["address"] as? [String: Any] 30 30 let city = addr?["city"] as? String ?? addr?["town"] as? String ?? addr?["village"] as? String 31 31 32 + let county = addr?["county"] as? String 32 33 var locationParts: [String] = [] 33 34 if let city { locationParts.append(city) } 35 + if let county { locationParts.append(county) } 34 36 if let state = addr?["state"] as? String { locationParts.append(state) } 35 37 if let country = addr?["country"] as? String { locationParts.append(country) } 36 38 ··· 42 44 : locationParts.joined(separator: ", ") 43 45 } 44 46 45 - context = locationParts.isEmpty ? nil : locationParts.joined(separator: ", ") 47 + // Use display_name for context, stripping the place name prefix for more detail 48 + if let displayName = json["display_name"] as? String { 49 + let stripped = displayName.drop(while: { $0 != "," }) 50 + .dropFirst() // drop the comma 51 + .trimmingCharacters(in: .whitespaces) 52 + context = stripped.isEmpty ? (locationParts.isEmpty ? nil : locationParts.joined(separator: ", ")) : stripped 53 + } else { 54 + context = locationParts.isEmpty ? nil : locationParts.joined(separator: ", ") 55 + } 46 56 47 57 if let countryCode = (addr?["country_code"] as? String)?.uppercased() { 48 58 var addressFields: [String: AnyCodable] = ["country": AnyCodable(countryCode)]
+5 -4
Grain/Utilities/PreviewHelpers.swift
··· 11 11 12 12 extension XRPCClient { 13 13 /// Shared preview client — avoids repeating `XRPCClient(baseURL: AuthManager.serverURL)`. 14 - static var preview: XRPCClient { XRPCClient(baseURL: AuthManager.serverURL) } 14 + static var preview: XRPCClient { 15 + XRPCClient(baseURL: AuthManager.serverURL) 16 + } 15 17 } 16 18 17 19 extension View { 18 20 /// Applies the standard Grain preview styling: dark mode + accent color. 19 21 /// Use on every #Preview so the canvas matches the real app. 20 22 func grainPreview() -> some View { 21 - self 22 - .preferredColorScheme(.dark) 23 - .tint(Color("AccentColor")) 23 + preferredColorScheme(.dark) 24 + .tint(Color.accentColor) 24 25 } 25 26 26 27 /// Injects the standard Grain environment objects required by most previews.
+6
Grain/Utilities/Theme.swift
··· 1 + import SwiftUI 2 + 3 + extension Color { 4 + /// Solid pink-red used for filled heart / favorite states. 5 + static let heart = Color(red: 1.0, green: 0.25, blue: 0.4) 6 + }
+17 -13
Grain/Views/Comments/CommentSheetContent.swift
··· 150 150 } label: { 151 151 Image(systemName: "xmark") 152 152 .font(.caption.weight(.semibold)) 153 - .foregroundStyle(Color("AccentColor")) 153 + .foregroundStyle(Color.accentColor) 154 154 .frame(width: 32, height: 32) 155 155 .contentShape(Rectangle()) 156 156 } ··· 164 164 } 165 165 166 166 GlassEffectContainer(spacing: 8) { 167 - HStack(alignment: .bottom, spacing: 10) { 167 + let isEmpty = commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 168 + HStack(alignment: .bottom, spacing: 0) { 168 169 TextField(replyingTo != nil ? "Reply..." : "Add a comment...", text: $commentText, axis: .vertical) 169 170 .textFieldStyle(.plain) 170 171 .font(.body) 171 172 .focused($commentFocused) 172 173 .lineLimit(1 ... 5) 173 174 .onChange(of: commentText) { mentionState.update(text: commentText) } 174 - .padding(.horizontal, 18) 175 + .padding(.leading, 18) 176 + .padding(.trailing, 8) 175 177 .padding(.vertical, 12) 176 - .glassEffect(.regular.tint(.primary.opacity(0.1)), in: .capsule) 177 178 178 - let isEmpty = commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 179 179 if !isEmpty { 180 180 Button { 181 181 Task { await postComment() } 182 182 } label: { 183 - Image(systemName: "arrow.up.circle.fill") 184 - .font(.system(size: 28)) 185 - .foregroundStyle(Color("AccentColor")) 186 - .frame(width: 44, height: 44) 183 + Image(systemName: "arrow.up") 184 + .font(.system(size: 14, weight: .bold)) 185 + .foregroundStyle(.white) 186 + .frame(width: 32, height: 32) 187 + .background(Color.accentColor, in: .circle) 187 188 } 188 189 .accessibilityLabel("Send comment") 189 - .glassEffect(.regular.interactive(), in: .circle) 190 + .buttonStyle(.plain) 190 191 .disabled(isPostingComment) 192 + .padding(.trailing, 8) 193 + .padding(.bottom, 7) 191 194 .transition( 192 195 .asymmetric( 193 - insertion: .offset(x: 60).combined(with: .opacity), 194 - removal: .offset(x: 60).combined(with: .opacity) 196 + insertion: .scale.combined(with: .opacity), 197 + removal: .scale.combined(with: .opacity) 195 198 ) 196 199 ) 197 200 } 198 201 } 202 + .glassEffect(.regular.tint(.primary.opacity(0.1)), in: .capsule) 199 203 .clipped() 200 - .animation(.spring(response: 0.3, dampingFraction: 0.8), value: commentText.isEmpty) 204 + .animation(.spring(response: 0.3, dampingFraction: 0.8), value: isEmpty) 201 205 .padding(.horizontal, 12) 202 206 .padding(.vertical, 10) 203 207 }
+8 -36
Grain/Views/Components/AvatarView.swift
··· 1 - import Nuke 2 1 import NukeUI 3 2 import SwiftUI 4 3 ··· 9 8 /// an animated parent (e.g. story parallax pane) so it snaps atomically. 10 9 var animated: Bool = true 11 10 12 - /// Only set for async cache misses — cache hits are read synchronously in body. 13 - @State private var asyncImage: UIImage? 14 - 15 11 private var imageURL: URL? { 16 12 guard let url else { return nil } 17 13 return URL(string: url) 18 14 } 19 - 20 - private static let placeholder = UIImage() 21 15 22 16 var body: some View { 23 - Image(uiImage: resolvedImage ?? Self.placeholder) 24 - .resizable() 25 - .frame(width: size, height: size) 26 - .background { 17 + LazyImage(url: imageURL) { state in 18 + if let image = state.image { 19 + image 20 + .resizable() 21 + } else { 27 22 fallback 28 23 } 29 - .clipShape(Circle()) 30 - .overlay(Circle().strokeBorder(Color.primary.opacity(0.08), lineWidth: 0.5)) 31 - .onAppear { loadIfNeeded() } 32 - } 33 - 34 - /// Synchronous image resolution — checks memory cache first, then falls back to async-loaded image. 35 - private var resolvedImage: UIImage? { 36 - if let imageURL { 37 - if let cached = ImagePipeline.shared.cache.cachedImage(for: ImageRequest(url: imageURL))?.image { 38 - return cached 39 - } 40 24 } 41 - return asyncImage 42 - } 43 - 44 - private func loadIfNeeded() { 45 - guard let imageURL else { return } 46 - let request = ImageRequest(url: imageURL) 47 - // If in memory cache, no state change needed — resolvedImage picks it up 48 - if ImagePipeline.shared.cache.cachedImage(for: request) != nil { return } 49 - // Only go async for true cache misses 50 - guard asyncImage == nil else { return } 51 - Task { 52 - if let image = try? await ImagePipeline.shared.image(for: request) { 53 - asyncImage = image 54 - } 55 - } 25 + .animation(animated ? .default : nil, value: imageURL) 26 + .frame(width: size, height: size) 27 + .clipShape(Circle()) 56 28 } 57 29 58 30 private var fallback: some View {
+1 -1
Grain/Views/Components/ContentLabelPicker.swift
··· 72 72 } 73 73 .previewEnvironments() 74 74 .preferredColorScheme(.dark) 75 - .tint(Color("AccentColor")) 75 + .tint(Color.accentColor) 76 76 }
+1 -1
Grain/Views/Components/ContentWarningOverlay.swift
··· 93 93 .frame(maxWidth: .infinity, maxHeight: .infinity) 94 94 .background(Color.black) 95 95 .preferredColorScheme(.dark) 96 - .tint(Color("AccentColor")) 96 + .tint(Color.accentColor) 97 97 }
+1 -1
Grain/Views/Components/CustomFullScreenCover.swift
··· 94 94 } 95 95 } 96 96 .preferredColorScheme(.dark) 97 - .tint(Color("AccentColor")) 97 + .tint(Color.accentColor) 98 98 }
+1 -1
Grain/Views/Components/ExpandableDescriptionView.swift
··· 98 98 } 99 99 .padding() 100 100 .preferredColorScheme(.dark) 101 - .tint(Color("AccentColor")) 101 + .tint(Color.accentColor) 102 102 }
+8 -8
Grain/Views/Components/GalleryCardView.swift
··· 59 59 ZStack { 60 60 Image(systemName: "heart") 61 61 .font(.system(size: 80, weight: .light)) 62 - .foregroundStyle(Color("AccentColor")) 62 + .foregroundStyle(Color.heart) 63 63 .scaleEffect(state.ripple3Scale) 64 64 .opacity(state.ripple3Opacity) 65 65 .rotationEffect(.degrees(state.rotation * 0.6)) 66 66 67 67 Image(systemName: "heart") 68 68 .font(.system(size: 80, weight: .ultraLight)) 69 - .foregroundStyle(Color("AccentColor")) 69 + .foregroundStyle(Color.heart) 70 70 .scaleEffect(state.ripple2Scale) 71 71 .opacity(state.ripple2Opacity) 72 72 .rotationEffect(.degrees(state.rotation * 0.8)) 73 73 74 74 Image(systemName: "heart") 75 75 .font(.system(size: 80, weight: .thin)) 76 - .foregroundStyle(Color("AccentColor")) 76 + .foregroundStyle(Color.heart) 77 77 .scaleEffect(state.ripple1Scale) 78 78 .opacity(state.ripple1Opacity) 79 79 .rotationEffect(.degrees(state.rotation * 0.9)) 80 80 81 81 Image(systemName: "heart.fill") 82 82 .font(.system(size: 80)) 83 - .foregroundStyle(Color("AccentColor")) 84 - .shadow(color: Color("AccentColor").opacity(0.4), radius: 12) 83 + .foregroundStyle(Color.heart) 84 + .shadow(color: .pink.opacity(0.4), radius: 12) 85 85 .scaleEffect(state.heartScale) 86 86 .opacity(state.heartScale > 1.2 ? 0 : 1) 87 87 .rotationEffect(.degrees(state.rotation)) ··· 115 115 let cfg = Self.configs[index] 116 116 Image(systemName: "heart.fill") 117 117 .font(.system(size: 10)) 118 - .foregroundStyle(Color("AccentColor").opacity(0.9)) 118 + .foregroundStyle(Color.heart.opacity(0.9)) 119 119 .scaleEffect(scale) 120 120 .offset(offset) 121 121 .opacity(opacity) ··· 469 469 } 470 470 } 471 471 } 472 - .foregroundStyle(isFavorited ? Color("AccentColor") : .secondary) 472 + .foregroundStyle(isFavorited ? AnyShapeStyle(Color.heart) : AnyShapeStyle(.secondary)) 473 473 .accessibilityLabel(isFavorited ? "Unlike" : "Like") 474 474 .accessibilityValue("\(gallery.favCount ?? 0) likes") 475 475 .overlay(alignment: .leading) { ··· 657 657 } 658 658 .previewEnvironments() 659 659 .preferredColorScheme(.dark) 660 - .tint(Color("AccentColor")) 660 + .tint(Color.accentColor) 661 661 .frame(maxHeight: .infinity, alignment: .top) 662 662 } 663 663
+5 -5
Grain/Views/Components/RichTextView.swift
··· 29 29 if let linkURL = URL(string: url) { 30 30 part.link = linkURL 31 31 } 32 - part.foregroundColor = Color("AccentColor") 32 + part.foregroundColor = Color.accentColor 33 33 case let .mention(str, did): 34 34 part = AttributedString(str) 35 35 let encoded = did.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? did 36 36 part.link = URL(string: "grain-mention://\(encoded)") 37 - part.foregroundColor = Color("AccentColor") 37 + part.foregroundColor = Color.accentColor 38 38 case let .hashtag(str, tag): 39 39 part = AttributedString(str) 40 40 let encoded = tag.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? tag 41 41 part.link = URL(string: "grain-hashtag://\(encoded)") 42 - part.foregroundColor = Color("AccentColor") 42 + part.foregroundColor = Color.accentColor 43 43 } 44 44 part.font = font 45 45 result.append(part) ··· 49 49 50 50 var body: some View { 51 51 Text(attributedString) 52 - .tint(Color("AccentColor")) 52 + .tint(Color.accentColor) 53 53 .environment(\.openURL, OpenURLAction { url in 54 54 if url.scheme == "grain-mention" { 55 55 let did = url.host()?.removingPercentEncoding ?? "" ··· 210 210 } 211 211 .padding() 212 212 .preferredColorScheme(.dark) 213 - .tint(Color("AccentColor")) 213 + .tint(Color.accentColor) 214 214 }
+1 -1
Grain/Views/Components/SuggestedFollowsView.swift
··· 114 114 ) 115 115 .previewEnvironments() 116 116 .preferredColorScheme(.dark) 117 - .tint(Color("AccentColor")) 117 + .tint(Color.accentColor) 118 118 }
+1 -1
Grain/Views/Components/ZoomableImage.swift
··· 372 372 .frame(maxWidth: .infinity) 373 373 .background(Color.black) 374 374 .preferredColorScheme(.dark) 375 - .tint(Color("AccentColor")) 375 + .tint(Color.accentColor) 376 376 }
+1 -1
Grain/Views/Create/CaptionsListPrototype.swift
··· 71 71 } 72 72 return CaptionsListPrototype(items: items) 73 73 .preferredColorScheme(.dark) 74 - .tint(Color("AccentColor")) 74 + .tint(Color.accentColor) 75 75 .frame(maxHeight: .infinity, alignment: .top) 76 76 }
+1
Grain/Views/Create/CreateGalleryView.swift
··· 204 204 .bold() 205 205 } 206 206 } 207 + .buttonStyle(.glassProminent) 207 208 .disabled(title.isEmpty || photoItems.isEmpty || isUploading || title.count > maxTitle || description.count > maxDescription) 208 209 } 209 210 }
+1 -1
Grain/Views/Create/DragToReorder.swift
··· 54 54 .padding() 55 55 .background(Color(.systemBackground)) 56 56 .preferredColorScheme(.dark) 57 - .tint(Color("AccentColor")) 57 + .tint(Color.accentColor) 58 58 }
+1
Grain/Views/Create/GalleryEditor.swift
··· 204 204 .padding(.top, 8) 205 205 .padding(.bottom, 12) 206 206 .frame(maxWidth: .infinity, alignment: .leading) 207 + .background(Color(.secondarySystemGroupedBackground)) 207 208 } 208 209 } 209 210 .background(Color(.secondarySystemBackground))
+1 -1
Grain/Views/Feed/FeedView.swift
··· 126 126 feedRefreshID = UUID() 127 127 } 128 128 } 129 - .tint(Color("AccentColor")) 129 + .tint(Color.accentColor) 130 130 } 131 131 .fullScreenCover(item: $deepLinkStoryAuthor) { author in 132 132 StoryViewer(
+1 -1
Grain/Views/MainTabView.swift
··· 83 83 } 84 84 } 85 85 } 86 - .tint(Color("AccentColor")) 86 + .tint(Color.accentColor) 87 87 .environment(commentPresenter) 88 88 } else { 89 89 Color.clear
+15 -37
Grain/Views/Notifications/NotificationsView.swift
··· 1 1 import Nuke 2 + import NukeUI 2 3 import os 3 4 import SwiftUI 4 5 ··· 216 217 } 217 218 } 218 219 220 + private var iconColor: Color { 221 + switch reason { 222 + case .galleryFavorite, .storyFavorite: .heart 223 + default: .accentColor 224 + } 225 + } 226 + 219 227 var body: some View { 220 228 Image(systemName: iconName) 221 - .foregroundStyle(Color("AccentColor")) 229 + .foregroundStyle(iconColor) 222 230 .font(.system(size: 18)) 223 231 .frame(width: 20) 224 232 .accessibilityLabel(label) ··· 335 343 var body: some View { 336 344 HStack(alignment: .top, spacing: 10) { 337 345 ReasonIcon(reason: notification.reasonType) 338 - .frame(height: 34) 346 + .frame(height: 38) 339 347 340 348 VStack(alignment: .leading, spacing: 6) { 341 - AvatarView(url: notification.author.avatar, size: 34, animated: false) 349 + AvatarView(url: notification.author.avatar, size: 38, animated: false) 342 350 .onTapGesture { 343 351 onProfileTap?(notification.author.did) 344 352 } ··· 410 418 onProfileTap?(author.did) 411 419 } label: { 412 420 AvatarView(url: author.avatar, size: size, animated: false) 413 - .overlay(Circle().strokeBorder(Color(.systemBackground), lineWidth: 2)) 414 421 } 415 422 .buttonStyle(.plain) 416 423 .zIndex(Double(authors.count - i)) ··· 435 442 portrait ? size * 4 / 3 : size 436 443 } 437 444 438 - @State private var asyncImage: UIImage? 439 - 440 445 private var imageURL: URL? { 441 446 URL(string: url) 442 447 } 443 448 444 - private var thumbRequest: ImageRequest? { 445 - guard let imageURL else { return nil } 446 - let scale = UIScreen.main.scale 447 - let targetSize = CGSize(width: width * scale, height: height * scale) 448 - return ImageRequest(url: imageURL, processors: [.resize(size: targetSize, contentMode: .aspectFill)]) 449 - } 450 - 451 - private var resolvedImage: UIImage? { 452 - if let request = thumbRequest, 453 - let cached = ImagePipeline.shared.cache.cachedImage(for: request)?.image 454 - { 455 - return cached 456 - } 457 - return asyncImage 458 - } 459 - 460 449 var body: some View { 461 - Group { 462 - if let image = resolvedImage { 463 - Image(uiImage: image) 450 + LazyImage(url: imageURL) { state in 451 + if let image = state.image { 452 + image 464 453 .resizable() 465 454 .aspectRatio(contentMode: .fill) 466 455 } else { 467 456 Rectangle().fill(.quaternary) 468 457 } 469 458 } 459 + .processors([ImageProcessors.Resize(size: CGSize(width: width * UIScreen.main.scale, height: height * UIScreen.main.scale), contentMode: .aspectFill)]) 470 460 .frame(width: width, height: height) 471 461 .clipShape(.rect(cornerRadius: 6)) 472 - .onAppear { loadIfNeeded() } 473 - } 474 - 475 - private func loadIfNeeded() { 476 - guard let request = thumbRequest else { return } 477 - if ImagePipeline.shared.cache.cachedImage(for: request) != nil { return } 478 - guard asyncImage == nil else { return } 479 - Task { 480 - if let image = try? await ImagePipeline.shared.image(for: request) { 481 - asyncImage = image 482 - } 483 - } 484 462 } 485 463 } 486 464
+1 -1
Grain/Views/Profile/FollowListView.swift
··· 300 300 } 301 301 .previewEnvironments() 302 302 .preferredColorScheme(.dark) 303 - .tint(Color("AccentColor")) 303 + .tint(Color.accentColor) 304 304 .frame(maxHeight: .infinity, alignment: .top) 305 305 }
+10 -12
Grain/Views/Profile/ProfileView.swift
··· 79 79 if did == auth.userDID { 80 80 Image(systemName: "plus.circle.fill") 81 81 .font(.system(size: 22)) 82 - .foregroundStyle(.white, Color("AccentColor")) 82 + .foregroundStyle(.white, Color.accentColor) 83 + .background(Circle().fill(Color(.systemBackground)).padding(-1)) 83 84 .offset(x: 4, y: 4) 84 85 .accessibilityHidden(true) 85 86 } ··· 239 240 } 240 241 .frame(maxWidth: .infinity) 241 242 } 242 - .buttonStyle(.bordered) 243 - .tint(.primary) 243 + .buttonStyle(.glass) 244 244 } 245 245 } 246 + .buttonBorderShape(.roundedRectangle(radius: 10)) 246 247 .padding(.horizontal) 247 248 } else { 248 249 HStack(spacing: 8) { ··· 255 256 .font(.subheadline.weight(.semibold)) 256 257 .frame(maxWidth: .infinity) 257 258 } 258 - .buttonStyle(.bordered) 259 - .tint(.primary) 259 + .buttonStyle(.glass) 260 260 261 261 if let germUrl = germDMUrl(profile: profile) { 262 262 Link(destination: germUrl) { ··· 269 269 } 270 270 .frame(maxWidth: .infinity) 271 271 } 272 - .buttonStyle(.bordered) 273 - .tint(.primary) 272 + .buttonStyle(.glass) 274 273 } 275 274 } 275 + .buttonBorderShape(.roundedRectangle(radius: 10)) 276 276 .padding(.horizontal) 277 277 } 278 278 } ··· 631 631 let clamped = max(0, min(fraction, CGFloat(modes.count - 1))) 632 632 let xOffset = clamped * tabWidth + (tabWidth - indicatorWidth) / 2 633 633 Rectangle() 634 - .fill(Color("AccentColor")) 634 + .fill(Color.accentColor) 635 635 .frame(width: indicatorWidth, height: 2.5) 636 636 .offset(x: xOffset, y: -6) 637 637 } ··· 984 984 .font(.subheadline.weight(.semibold)) 985 985 .frame(maxWidth: .infinity) 986 986 } 987 - .buttonStyle(.bordered) 988 - .tint(.primary) 987 + .buttonStyle(.glass) 989 988 } else { 990 989 Button { 991 990 Task { await viewModel.toggleFollow(auth: auth.authContext()) } ··· 994 993 .font(.subheadline.weight(.semibold)) 995 994 .frame(maxWidth: .infinity) 996 995 } 997 - .buttonStyle(.borderedProminent) 998 - .tint(Color("AccentColor")) 996 + .buttonStyle(.glassProminent) 999 997 } 1000 998 } 1001 999
+1 -1
Grain/Views/Search/SearchView.swift
··· 227 227 } label: { 228 228 Image(systemName: "xmark.circle.fill") 229 229 .font(.system(size: 18)) 230 - .foregroundStyle(.white, Color("AccentColor")) 230 + .foregroundStyle(.white, Color.accentColor) 231 231 } 232 232 .accessibilityLabel("Remove \(profile.displayName ?? profile.handle ?? "") from recent") 233 233 }
+1 -1
Grain/Views/Settings/EditProfileView.swift
··· 55 55 .font(.system(size: 14)) 56 56 .foregroundStyle(.white) 57 57 .frame(width: 32, height: 32) 58 - .background(Color("AccentColor"), in: Circle()) 58 + .background(Color.accentColor, in: Circle()) 59 59 } 60 60 .accessibilityLabel("Change profile photo") 61 61 }
+1 -3
Grain/Views/Settings/SettingsView.swift
··· 50 50 } label: { 51 51 HStack { 52 52 Text("Clear cache") 53 - .foregroundStyle(Color("AccentColor")) 53 + .foregroundStyle(Color.accentColor) 54 54 Spacer() 55 55 Text(cacheSizeText) 56 56 .foregroundStyle(.secondary) ··· 175 175 } 176 176 } 177 177 .navigationTitle("Feeds") 178 - .tint(Color("AccentColor")) 179 178 } 180 179 } 181 180 ··· 222 221 } 223 222 } 224 223 .navigationTitle("Privacy") 225 - .tint(Color("AccentColor")) 226 224 .task { 227 225 if let authContext = await auth.authContext(), 228 226 let prefs = try? await client.getPreferences(auth: authContext).preferences
+1 -1
Grain/Views/Stories/StoryCreateView.swift
··· 241 241 StoryCreateView(client: .preview) 242 242 .previewEnvironments() 243 243 .preferredColorScheme(.dark) 244 - .tint(Color("AccentColor")) 244 + .tint(Color.accentColor) 245 245 }
+1 -1
Grain/Views/Stories/StoryRingView.swift
··· 57 57 } 58 58 .padding() 59 59 .preferredColorScheme(.dark) 60 - .tint(Color("AccentColor")) 60 + .tint(Color.accentColor) 61 61 }
+13 -4
Grain/Views/Stories/StoryStripView.swift
··· 38 38 .padding(4) 39 39 Image(systemName: "plus.circle.fill") 40 40 .font(.system(size: 18)) 41 - .foregroundStyle(.white, Color("AccentColor")) 41 + .foregroundStyle(.white, Color.accentColor) 42 + .background(Circle().fill(Color(.systemBackground)).padding(-1)) 42 43 .offset(x: -1, y: -1) 43 44 .accessibilityHidden(true) 44 45 } ··· 57 58 onCreateTap() 58 59 } 59 60 } 60 - .onLongPressGesture { 61 - UIImpactFeedbackGenerator(style: .medium).impactOccurred() 62 - onCreateTap() 61 + .profileContextMenu( 62 + handle: nil, 63 + hasStory: ownAuthor != nil, 64 + onViewStory: ownAuthor.map { own in { onAuthorTap(own, 0) } }, 65 + onAddStory: { onCreateTap() }, 66 + showSharingActions: false 67 + ) { 68 + StoryRingView(hasStory: ownAuthor != nil, viewed: false, size: 96) { 69 + AvatarView(url: userAvatar, size: 96) 70 + } 71 + .padding(6) 63 72 } 64 73 65 74 ForEach(sorted, id: \.id) { author in
+1 -1
Grain/Views/Stories/StoryViewer.swift
··· 1117 1117 private func heartIcon(isFavorited: Bool) -> some View { 1118 1118 Image(systemName: isFavorited ? "heart.fill" : "heart") 1119 1119 .font(.title) 1120 - .foregroundStyle(isFavorited ? Color("AccentColor") : .white) 1120 + .foregroundStyle(isFavorited ? AnyShapeStyle(Color.heart) : AnyShapeStyle(.white)) 1121 1121 .keyframeAnimator(initialValue: 1.0, trigger: heartBeatTrigger) { content, scale in 1122 1122 content.scaleEffect(scale) 1123 1123 } keyframes: { _ in