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: resolve header drop, tap bugginess, and add button ease-in

- Add two Color.clear 36pt placeholders to pendingFaceView header so
its HStack height matches storyContent's (prevents layout jump at commit)
- Wrap flag/trash and close buttons in Group with opacity animation;
reset on currentAuthorIndex change for a subtle ease-in after transition
- Bypass transitionToAuthor for tap-triggered author navigation — set
currentAuthorIndex directly and call switchToCurrentAuthor inside
withAnimation, removing the 400ms canNavigate() block window

+55 -30
+55 -30
Grain/Views/Stories/StoryViewer.swift
··· 88 88 @State private var authorHistory: [(authorIndex: Int, storyIndex: Int)] = [] 89 89 @State private var imagePrefetcher = ImagePrefetcher() 90 90 @State private var nextStoryFromTrailing = true 91 + @State private var headerButtonsVisible = true 91 92 92 93 init(authors: [GrainStoryAuthor], startAuthorDid: String? = nil, client: XRPCClient, onProfileTap: ((String) -> Void)? = nil, onDismiss: (() -> Void)? = nil) { 93 94 self.authors = authors ··· 170 171 timer.onQuarter = { [self] in markCurrentStoryViewed() } 171 172 await loadStoriesForCurrentAuthor() 172 173 } 174 + .onChange(of: currentAuthorIndex) { 175 + headerButtonsVisible = false 176 + withAnimation(.easeIn(duration: 0.2).delay(0.05)) { 177 + headerButtonsVisible = true 178 + } 179 + } 173 180 } 174 181 175 182 /// Mirrors the logic in presentStories so pendingTransition.storyIndex matches what will be committed. ··· 228 235 } 229 236 } 230 237 Spacer() 238 + // Placeholder slots to match storyContent's button layout height 239 + Color.clear.frame(width: 36, height: 36) 240 + Color.clear.frame(width: 36, height: 36) 231 241 } 232 242 .padding(.horizontal, 16) 233 243 .padding(.vertical, 8) ··· 365 375 } 366 376 Spacer() 367 377 368 - if let story { 369 - if story.creator.did == auth.userDID { 370 - Button { 371 - timer.stop() 372 - showDeleteConfirm = true 373 - } label: { 374 - Image(systemName: "trash") 375 - .foregroundStyle(.white) 376 - .frame(width: 36, height: 36) 378 + Group { 379 + if let story { 380 + if story.creator.did == auth.userDID { 381 + Button { 382 + timer.stop() 383 + showDeleteConfirm = true 384 + } label: { 385 + Image(systemName: "trash") 386 + .foregroundStyle(.white) 387 + .frame(width: 36, height: 36) 388 + } 389 + } else { 390 + Button { 391 + timer.stop() 392 + reportStoryUri = story.uri 393 + reportStoryCid = story.cid 394 + showReportSheet = true 395 + } label: { 396 + Image(systemName: "flag") 397 + .foregroundStyle(.white) 398 + .frame(width: 36, height: 36) 399 + } 377 400 } 378 - } else { 379 - Button { 380 - timer.stop() 381 - reportStoryUri = story.uri 382 - reportStoryCid = story.cid 383 - showReportSheet = true 384 - } label: { 385 - Image(systemName: "flag") 386 - .foregroundStyle(.white) 387 - .frame(width: 36, height: 36) 388 - } 401 + } 402 + Button { close() } label: { 403 + Image(systemName: "xmark") 404 + .foregroundStyle(.white) 405 + .font(.body.weight(.semibold)) 406 + .frame(width: 36, height: 36) 389 407 } 390 408 } 391 - 392 - Button { close() } label: { 393 - Image(systemName: "xmark") 394 - .foregroundStyle(.white) 395 - .font(.body.weight(.semibold)) 396 - .frame(width: 36, height: 36) 397 - } 409 + .opacity(headerButtonsVisible ? 1 : 0) 410 + .animation(.easeIn(duration: 0.2), value: headerButtonsVisible) 398 411 } 399 412 .padding(.horizontal, 16) 400 413 .padding(.vertical, 8) ··· 504 517 close(); return 505 518 } 506 519 authorHistory.append((authorIndex: currentAuthorIndex, storyIndex: currentStoryIndex)) 507 - transitionToAuthor(next, forward: true) 520 + nextStoryFromTrailing = true 521 + currentAuthorIndex = next 522 + withAnimation(.spring(response: 0.28, dampingFraction: 0.88)) { 523 + switchToCurrentAuthor() 524 + } 508 525 } 509 526 510 527 private func goToPreviousAuthor() { ··· 518 535 return 519 536 } 520 537 if let prev = authorHistory.popLast() { 521 - transitionToAuthor(prev.authorIndex, forward: false, resumeIndex: prev.storyIndex) 538 + nextStoryFromTrailing = false 539 + currentAuthorIndex = prev.authorIndex 540 + withAnimation(.spring(response: 0.28, dampingFraction: 0.88)) { 541 + switchToCurrentAuthor(resumeIndex: prev.storyIndex) 542 + } 522 543 return 523 544 } 524 545 // No history — walk backward ignoring the reads filter 525 546 var i = currentAuthorIndex - 1 526 547 while i >= 0 { 527 548 if authors[i].profile.did != auth.userDID { 528 - transitionToAuthor(i, forward: false) 549 + nextStoryFromTrailing = false 550 + currentAuthorIndex = i 551 + withAnimation(.spring(response: 0.28, dampingFraction: 0.88)) { 552 + switchToCurrentAuthor() 553 + } 529 554 return 530 555 } 531 556 i -= 1