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.

refactor: show favorited state in non-interactive story heart

Extract heartIcon and particleBurstOverlay helpers, thread story through
bottomInputBar so the non-interactive variant can reflect favorited state
and trigger the particle burst on change.

Also fix Logger self-capture in StoryTimer.resume and tighten the latest
comment crossfade to 0.2s.

+34 -22
+34 -22
Grain/Views/Stories/StoryViewer.swift
··· 355 355 356 356 Spacer().allowsHitTesting(false) 357 357 358 - bottomInputBar(interactive: false) 358 + bottomInputBar(interactive: false, story: story) 359 359 } 360 360 } 361 361 } ··· 621 621 .transition(.opacity) 622 622 } 623 623 } 624 - .animation(.easeInOut(duration: 0.25), value: commentsViewModel.latestComment?.uri) 624 + .animation(.easeInOut(duration: 0.2), value: commentsViewModel.latestComment?.uri) 625 625 626 - bottomInputBar(interactive: true) 626 + bottomInputBar(interactive: true, story: currentStory) 627 627 } 628 628 } 629 629 } ··· 1078 1078 return story.viewer?.fav != nil || storyFavoriteCache.isLiked(story.uri) 1079 1079 } 1080 1080 1081 - private func bottomInputBar(interactive: Bool) -> some View { 1082 - HStack(spacing: 12) { 1081 + private func bottomInputBar(interactive: Bool, story: GrainStory?) -> some View { 1082 + let isFavorited: Bool = { 1083 + guard let story else { return false } 1084 + return story.viewer?.fav != nil || storyFavoriteCache.isLiked(story.uri) 1085 + }() 1086 + 1087 + return HStack(spacing: 12) { 1083 1088 // Comment bubble — opens comment list 1084 1089 if interactive { 1085 1090 Button { ··· 1123 1128 .glassEffect(.regular, in: .capsule) 1124 1129 } 1125 1130 1126 - // Heart — like/unlike 1131 + // Heart — favorite/unfavorite 1127 1132 if interactive { 1128 1133 Button { 1129 1134 if !isFavorited { addLikeParticleBurst() } 1130 1135 triggerFavoriteToggle() 1131 1136 } label: { 1132 - Image(systemName: isFavorited ? "heart.fill" : "heart") 1133 - .font(.title3) 1134 - .foregroundStyle(isFavorited ? Color("AccentColor") : .white) 1135 - .animation(.spring(response: 0.3, dampingFraction: 0.6), value: isFavorited) 1136 - .frame(width: 36, height: 36) 1137 + heartIcon(isFavorited: isFavorited) 1137 1138 .contentShape(Rectangle()) 1138 1139 } 1139 1140 .buttonStyle(.plain) 1140 - .overlay { 1141 - ForEach(likeParticleBursts, id: \.self) { _ in 1142 - ForEach(0 ..< 5) { i in 1143 - LikeParticleView(index: i) 1144 - } 1145 - } 1146 - } 1141 + .overlay { particleBurstOverlay } 1147 1142 } else { 1148 - Image(systemName: "heart") 1149 - .font(.title3) 1150 - .foregroundStyle(.white) 1151 - .frame(width: 36, height: 36) 1143 + heartIcon(isFavorited: isFavorited) 1144 + .overlay { particleBurstOverlay } 1145 + .onChange(of: isFavorited) { oldValue, newValue in 1146 + if !oldValue, newValue { addLikeParticleBurst() } 1147 + } 1152 1148 } 1153 1149 } 1154 1150 .padding(.horizontal, 16) 1155 1151 .padding(.bottom, 16) 1152 + } 1153 + 1154 + private func heartIcon(isFavorited: Bool) -> some View { 1155 + Image(systemName: isFavorited ? "heart.fill" : "heart") 1156 + .font(.title3) 1157 + .foregroundStyle(isFavorited ? Color("AccentColor") : .white) 1158 + .animation(.spring(response: 0.3, dampingFraction: 0.6), value: isFavorited) 1159 + .frame(width: 36, height: 36) 1160 + } 1161 + 1162 + private var particleBurstOverlay: some View { 1163 + ForEach(likeParticleBursts, id: \.self) { _ in 1164 + ForEach(0 ..< 5) { i in 1165 + LikeParticleView(index: i) 1166 + } 1167 + } 1156 1168 } 1157 1169 1158 1170 private func doubleTapLike(at point: CGPoint) {