1// VideoMessageView.swift
2// SophaxChat
3//
4// Thumbnail bubble for video messages. Tap to play fullscreen.
5
6import SwiftUI
7import AVKit
8import SophaxChatCore
9
10struct VideoMessageView: View {
11 let data: Data
12 let isSent: Bool
13
14 @State private var thumbnail: UIImage? = nil
15 @State private var showingPlayer = false
16
17 var body: some View {
18 ZStack {
19 if let thumb = thumbnail {
20 Image(uiImage: thumb)
21 .resizable()
22 .scaledToFill()
23 .frame(maxWidth: 220, maxHeight: 200)
24 .clipped()
25 } else {
26 RoundedRectangle(cornerRadius: 14)
27 .fill(Color(.systemGray5))
28 .frame(width: 220, height: 160)
29 }
30
31 // Play button overlay
32 ZStack {
33 Circle()
34 .fill(.black.opacity(0.5))
35 .frame(width: 48, height: 48)
36 Image(systemName: "play.fill")
37 .font(.system(size: 20))
38 .foregroundStyle(.white)
39 .offset(x: 2)
40 }
41 }
42 .clipShape(RoundedRectangle(cornerRadius: 14))
43 .overlay(
44 RoundedRectangle(cornerRadius: 14)
45 .stroke(isSent ? Color.accentColor.opacity(0.4) : Color.secondary.opacity(0.2), lineWidth: 1)
46 )
47 .onTapGesture { showingPlayer = true }
48 .task { await generateThumbnail() }
49 .fullScreenCover(isPresented: $showingPlayer) {
50 VideoPlayerSheet(data: data)
51 }
52 }
53
54 private func generateThumbnail() async {
55 let tempURL = FileManager.default.temporaryDirectory
56 .appendingPathComponent(UUID().uuidString + ".mp4")
57 defer { try? FileManager.default.removeItem(at: tempURL) }
58 do {
59 try data.write(to: tempURL)
60 let asset = AVURLAsset(url: tempURL)
61 let gen = AVAssetImageGenerator(asset: asset)
62 gen.appliesPreferredTrackTransform = true
63 let time = CMTime(seconds: 0.5, preferredTimescale: 600)
64 let cgImage = try await gen.image(at: time).image
65 thumbnail = UIImage(cgImage: cgImage)
66 } catch {
67 // No thumbnail — player overlay still available
68 }
69 }
70}
71
72// MARK: - Fullscreen player
73
74private struct VideoPlayerSheet: View {
75 @Environment(\.dismiss) private var dismiss
76 let data: Data
77
78 @State private var player: AVPlayer? = nil
79 @State private var videoURL: URL? = nil
80
81 var body: some View {
82 ZStack(alignment: .topTrailing) {
83 Color.black.ignoresSafeArea()
84 if let player {
85 VideoPlayer(player: player)
86 .ignoresSafeArea()
87 }
88 Button {
89 player?.pause()
90 dismiss()
91 } label: {
92 Image(systemName: "xmark.circle.fill")
93 .font(.system(size: 30))
94 .foregroundStyle(.white.opacity(0.8))
95 .padding(16)
96 }
97 }
98 .onAppear {
99 let url = FileManager.default.temporaryDirectory
100 .appendingPathComponent(UUID().uuidString + ".mp4")
101 try? data.write(to: url)
102 videoURL = url
103 player = AVPlayer(url: url)
104 player?.play()
105 }
106 .onDisappear {
107 if let url = videoURL {
108 try? FileManager.default.removeItem(at: url)
109 videoURL = nil
110 }
111 }
112 }
113}