Conversation
|
Claude finished @onefovrth's task —— View job PR Review: Unify photo/video media bubble layoutI've completed a thorough code review. Here are my findings: Tasks:
Code Quality & ArchitectureStrengths:
Areas for Improvement: 1. Potential State Race Condition (MessagesGroupItemView.swift:615-618).onChange(of: shouldBlur) {
if shouldBlur, isPlaying {
inlinePlayer?.pause()
isPlaying = false
} else if !shouldBlur, pendingPlayAfterReveal {
pendingPlayAfterReveal = false
handleVideoPlayTap()
}
}Issue: When a blurred video is tapped, Recommendation: Add a generation counter or timestamp to the pending play request to ensure the most recent intent is preserved: @State private var pendingPlayGeneration: Int = 0
// In singleTapAction:
pendingPlayGeneration += 1
pendingPlayAfterReveal = true
// In onChange(of: shouldBlur):
else if !shouldBlur, pendingPlayAfterReveal {
let generation = pendingPlayGeneration
pendingPlayAfterReveal = false
DispatchQueue.main.async {
if generation == pendingPlayGeneration {
handleVideoPlayTap()
}
}
}2. Memory Leak Risk in VideoURLCache (MessagesGroupItemView.swift:491-502)private actor VideoURLCache {
static let shared: VideoURLCache = VideoURLCache()
private var cache: [String: URL] = [:]
func url(for key: String) -> URL? {
cache[key]
}
func set(_ url: URL, for key: String) {
cache[key] = url
}
}Issue: The cache grows unbounded and stores temporary file URLs that are never cleaned up. Temporary files may accumulate in the file system, consuming disk space. Recommendation:
private actor VideoURLCache {
static let shared = VideoURLCache()
private let cache = NSCache<NSString, NSURL>()
private let fileManager = FileManager.default
func url(for key: String) -> URL? {
cache.object(forKey: key as NSString) as URL?
}
func set(_ url: URL, for key: String) {
cache.setObject(url as NSURL, forKey: key as NSString)
}
func cleanup() async {
// Periodically clean temp directory
}
}3. Inconsistent Error Handling (MessagesGroupItemView.swift:772-778)} catch {
loadError = error
isLoading = false
isLoadingVideo = false
videoLoadFailed = true
Log.error("Failed to load video: \(error)")
}Issue: Video load failures are logged but not surfaced to the user in a clear way. The Recommendation: Add an error overlay with retry option: .overlay {
if videoLoadFailed {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
Text("Video failed to load")
.font(.caption)
Button("Retry") { /* retry logic */ }
}
.padding()
.background(.ultraThinMaterial)
.cornerRadius(12)
}
}4. Gesture Exclusion Frame Complexity (MessageContextMenuWrapper.swift:501-504)func isExcluded(point: CGPoint, in overlay: UIView) -> Bool {
let pointInGlobal = overlay.convert(point, to: nil)
return excludedFrames.contains { $0.contains(pointInGlobal) }
}Observation: The gesture exclusion system works correctly but is complex. Each overlay (avatar, info badge, reactions) reports its global frame via Recommendation: Consider adding documentation explaining this system, as it's not immediately obvious how the parts connect. Also verify that rapid layout changes (e.g., during rotation) don't cause frame mismatches. 5. SwiftLint ViolationsBased on CLAUDE.md rules, potential violations: Line Length: Several lines exceed 200 characters (e.g., MessagesGroupItemView.swift:148): private func messagesGroupView(for group: MessagesGroup) -> MessagesGroupView {Function Body Length: Type Body Length: Performance ConsiderationsGood:
Concerns:
Security ConcernsNo critical security issues identified. Minor observations:
Test CoverageMissing Test Coverage:
Recommended Test Cases: // Test: Tapping blurred video reveals and plays
func testTapBlurredVideoRevealsAndPlays() async {
// Given: blurred video
// When: single tap
// Then: onPhotoRevealed called, pendingPlayAfterReveal set
// When: shouldBlur updates to false
// Then: video starts playing
}
// Test: Double-tapping playing video shows reaction peek
func testDoubleTapPlayingVideoShowsReactionPeek() async {
// Given: playing video
// When: double-tap
// Then: reactionsPeekVisible = true
// Then: after 1100ms, peek auto-hides
}
// Test: Gesture exclusion prevents overlay taps from swiping
func testOverlayTapsDoNotTriggerSwipe() {
// Given: media bubble with avatar overlay
// When: tap on avatar frame
// Then: onTapAvatar called, swipe gesture ignored
}Bugs & Edge CasesPotential Bug: Seek Completion Handler DispatchMessagesGroupItemView.swift:675-686: player.seek(to: .zero) { [weak player] finished in
guard finished else {
DispatchQueue.main.async {
isPlaying = false
}
return
}
DispatchQueue.main.async {
player?.play()
isPlaying = true
NotificationCenter.default.post(name: Self.videoPlaybackStarted, object: id)
}
}Issue: The Recommendation: Add user-facing feedback if seek fails (e.g., show an alert or reset the play button state). Best Practices AlignmentAdheres to CLAUDE.md guidelines:
Minor Deviations:
SummaryThis is a well-architected PR that successfully unifies the media bubble layout with a clean overlay system. The code quality is high, with proper state management and gesture handling. Critical issues: None Recommendation: This PR is ready to merge after addressing the memory leak in |
ApprovabilityVerdict: Needs human review This PR unifies photo/video media bubble layouts but introduces significant user-facing behavior changes including new reaction handling on media, modified video playback interaction (double-tap and reveal-to-play flow), gesture exclusion zones, and layout adjustments between adjacent full-bleed items. The scope goes beyond cosmetic unification into new feature territory. You can customize Macroscope's approvability policy. Learn more. |
Profile info (avatar + name) is now always top-left, reactions are inset bottom-left inside the bubble, and a new top-right info badge shows "Tap to reveal" and/or video duration. Layout is consistent regardless of sent vs received. - Replace PhotoSenderLabel with MediaContainerID (always shows name) - Add MediaContainerInfo (top-right badge) and MediaContainerReax (frosted glass inset reaction pill) - Add MediaTopGradient (always top-aligned) - Update video play button to frosted glass circle design - Skip external reactionRow for full-bleed attachments - Delete PhotoBlurOverlayView.swift (unused) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The interactive containers (ID, Info, Reax) were inside AttachmentPlaceholder which is wrapped by .messageGesture(), causing the UIKit gesture recognizer to swallow all taps. Move them to VideoTapAttachmentView after .messageGesture() so SwiftUI Buttons receive taps first. - Reaction pill tap now opens reactions sheet - Avatar/name tap now opens profile view - Expose isPlaying as @binding for overlay visibility - Thread onTapAvatar through to MediaContainerID - Remove debug red backgrounds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Media container overlays (identity, info, reactions) now follow the swipe-to-reply offset. Context menu photo preview no longer shows gradient, sender label, or info badge overlays. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Corners animate from sharp to 20pt as the user swipes, applied directly to the image clip shape for a natural rounding effect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sync external swipe offset inside the animation block so container overlays animate back with the same spring as the image. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Play button now appears on blurred video previews. Tapping a blurred video reveals and plays in one action. Video duration is extracted from the AVAsset for received videos where it isn't stored in the model. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Video thumbnails loaded from attachment data were only stored in local state, so the context menu preview couldn't find them. Now cached in ImageCache so they're available for the long-press preview. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reaction pill uses .clear glass when not reacted and .regular glass with white tint (0.6 opacity) when the current user has reacted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Consecutive full-bleed attachment groups (images/videos) now render with a single-pixel gap instead of the standard 16pt spacing. Adjacency flags are computed in MessagesListProcessor and read directly by MessagesGroupView. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes SwiftLint explicit_type_interface violation in MessagesGroupView. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When tapping a blurred video, onPhotoRevealed runs async so shouldBlur is still true when handleVideoPlayTap fires, silently dropping the play intent. Add a pendingPlayAfterReveal flag that defers playback until the onChange(of: shouldBlur) observer sees the blur clear. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract loadVideoAttachment, loadPhotoAttachment, and resolveImageData from loadAttachment to reduce cyclomatic complexity from 16 to under 14. Remove explicit nil initialization on resolvedDuration optional. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set pendingPlayAfterReveal directly in singleTapAction instead of toggling videoPlayTrigger, which would check stale blur state. Also move isPlaying assignment inside the seek completion handler so UI state stays consistent if the seek fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Received videos lack thumbnailData because the XMTP protocol does not carry sender-side custom metadata (thumbnail, dimensions, duration). The AVPlayer displayed the first frame directly but no UIImage was ever cached, so reply bar previews and inline reply references showed a placeholder. Generate a thumbnail from the video asset after loading and cache it in ImageCache, matching the behavior photos already have. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Media container overlays (avatar, duration, reactions) were layered above the GestureOverlayView, intercepting touches before the UIKit double-tap recognizer could fire. Move messageGesture to apply after the overlays and mark overlays as non-hit-testable so the gesture overlay sits on top and handles all interactions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
a8119de to
71b5698
Compare
Summary
.clearvs.regular.tint(.white.opacity(0.6)))colorFillMinimaltocolorFillSubtleTest plan
🤖 Generated with Claude Code
Note
Unify photo and video media bubble layout with shared overlays and reactions support
MediaTopGradient,MediaContainerID(avatar/name),MediaContainerInfo(tap-to-reveal/duration), andMediaContainerReax(reaction chips) in MessagesGroupItemView.swiftPhotoBlurOverlayView,PhotoEdgeGradient, andPhotoSenderLabelin favor of the new shared componentsonReactioncallback wired fromMessagesGroupViewthrough the data source up toMessagesViewController, enabling reaction toggle events to propagate out of the collection viewMessagesGroupgainsadjacentToFullBleedAbove/Belowflags computed inMessagesListProcessor, used to tighten spacing and suppress inline reaction indicators between consecutive full-bleed attachment groupsMessageContextMenuOverlay) no longer shows a sender label and now overlays a play icon for videosMacroscope summarized 023261b.