Add iOS media placeholders

This commit is contained in:
Mikhail Kilin
2026-05-21 15:50:14 +03:00
parent 508db79c34
commit ec74961677
5 changed files with 183 additions and 0 deletions

View File

@@ -61,6 +61,7 @@ public actor FakeSessionBridge: SessionBridge {
senderName: "Alice",
text: "Hello from fake TDLib",
date: Self.baseMessageDate,
media: .photo(PhotoMedia(fileId: 100, width: 1280, height: 720)),
isOutgoing: false,
isRead: false
)
@@ -72,6 +73,7 @@ public actor FakeSessionBridge: SessionBridge {
senderName: "Mikhail",
text: "Bridge smoke is green",
date: Self.baseMessageDate + 60,
media: .voice(VoiceMedia(fileId: 200, duration: 12, mimeType: "audio/ogg")),
isOutgoing: true
)
],

View File

@@ -91,12 +91,67 @@ public struct Reaction: Equatable, Sendable {
}
}
public enum MediaDownloadState: Equatable, Sendable {
case notDownloaded
case downloading
case downloaded(path: String)
case error(String)
}
public struct PhotoMedia: Equatable, Sendable {
public var fileId: Int32
public var width: Int32
public var height: Int32
public var downloadState: MediaDownloadState
public init(
fileId: Int32,
width: Int32,
height: Int32,
downloadState: MediaDownloadState = .notDownloaded
) {
self.fileId = fileId
self.width = width
self.height = height
self.downloadState = downloadState
}
}
public struct VoiceMedia: Equatable, Sendable {
public var fileId: Int32
public var duration: Int32
public var mimeType: String?
public var waveform: String?
public var downloadState: MediaDownloadState
public init(
fileId: Int32,
duration: Int32,
mimeType: String? = nil,
waveform: String? = nil,
downloadState: MediaDownloadState = .notDownloaded
) {
self.fileId = fileId
self.duration = duration
self.mimeType = mimeType
self.waveform = waveform
self.downloadState = downloadState
}
}
public enum MessageMedia: Equatable, Sendable {
case photo(PhotoMedia)
case voice(VoiceMedia)
}
public struct Message: Identifiable, Equatable, Sendable {
public var id: Int64
public var chatId: Int64
public var senderName: String
public var text: String
public var date: Int32
public var mediaAlbumId: Int64?
public var media: MessageMedia?
public var isOutgoing: Bool
public var isRead: Bool
public var editDate: Int32?
@@ -110,6 +165,8 @@ public struct Message: Identifiable, Equatable, Sendable {
senderName: String,
text: String,
date: Int32 = 0,
mediaAlbumId: Int64? = nil,
media: MessageMedia? = nil,
isOutgoing: Bool,
isRead: Bool = true,
editDate: Int32? = nil,
@@ -122,6 +179,8 @@ public struct Message: Identifiable, Equatable, Sendable {
self.senderName = senderName
self.text = text
self.date = date
self.mediaAlbumId = mediaAlbumId
self.media = media
self.isOutgoing = isOutgoing
self.isRead = isRead
self.editDate = editDate

View File

@@ -199,6 +199,41 @@ public actor UniFfiSessionBridge: SessionBridge {
Reaction(emoji: reaction.emoji, count: reaction.count, isChosen: reaction.isChosen)
}
private static func mapDownloadState(_ state: IosDownloadState) -> MediaDownloadState {
switch state {
case .notDownloaded:
.notDownloaded
case .downloading:
.downloading
case let .downloaded(path):
.downloaded(path: path)
case let .error(message):
.error(message)
}
}
private static func mapMedia(_ media: IosMedia) -> MessageMedia? {
switch media.kind {
case "photo":
.photo(PhotoMedia(
fileId: media.fileId,
width: media.width ?? 0,
height: media.height ?? 0,
downloadState: mapDownloadState(media.downloadState)
))
case "voice":
.voice(VoiceMedia(
fileId: media.fileId,
duration: media.duration ?? 0,
mimeType: media.mimeType,
waveform: media.waveform,
downloadState: mapDownloadState(media.downloadState)
))
default:
nil
}
}
private static func mapMessage(_ message: IosMessage, chatId: Int64) -> Message {
Message(
id: message.id,
@@ -206,6 +241,8 @@ public actor UniFfiSessionBridge: SessionBridge {
senderName: message.senderName,
text: message.text,
date: message.date,
mediaAlbumId: message.mediaAlbumId,
media: message.media.flatMap(mapMedia),
isOutgoing: message.isOutgoing,
isRead: message.isRead,
editDate: message.editDate,

View File

@@ -657,6 +657,9 @@ public struct MessageRow: View {
.font(.caption)
.foregroundStyle(.secondary)
}
if let media = message.media {
MediaPlaceholderView(media: media, mediaAlbumId: message.mediaAlbumId)
}
Text(renderedText)
.textSelection(.enabled)
if !message.reactions.isEmpty {
@@ -706,6 +709,81 @@ public struct MessageRow: View {
}()
}
public struct MediaPlaceholderView: View {
public var media: MessageMedia
public var mediaAlbumId: Int64?
public init(media: MessageMedia, mediaAlbumId: Int64? = nil) {
self.media = media
self.mediaAlbumId = mediaAlbumId
}
public var body: some View {
HStack(spacing: 8) {
Image(systemName: iconName)
.frame(width: 22, height: 22)
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(title)
.font(.subheadline)
if mediaAlbumId != nil {
Image(systemName: "square.stack.3d.up.fill")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
Text(detail)
.font(.caption2)
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
}
.padding(8)
.background(Color.gray.opacity(0.10), in: RoundedRectangle(cornerRadius: 8))
}
private var iconName: String {
switch media {
case .photo:
"photo"
case .voice:
"waveform"
}
}
private var title: String {
switch media {
case .photo:
"Photo"
case .voice:
"Voice"
}
}
private var detail: String {
switch media {
case let .photo(photo):
"\(photo.width)x\(photo.height) · \(downloadLabel(photo.downloadState))"
case let .voice(voice):
"\(voice.duration)s · \(downloadLabel(voice.downloadState))"
}
}
private func downloadLabel(_ state: MediaDownloadState) -> String {
switch state {
case .notDownloaded:
"not downloaded"
case .downloading:
"downloading"
case .downloaded:
"downloaded"
case .error:
"error"
}
}
}
public struct ComposeBar: View {
@Binding public var text: String
public var replyTo: Message?

View File

@@ -70,6 +70,13 @@ struct TeleTuiIOSSmokeTests {
precondition(viewModel.messages.count == 1)
precondition(viewModel.messages[0].date == 1_700_000_000)
precondition(viewModel.pinnedMessages.map(\.id) == [1])
if case let .photo(photo) = viewModel.messages[0].media {
precondition(photo.fileId == 100)
precondition(photo.width == 1280)
precondition(photo.height == 720)
} else {
preconditionFailure("fake saved message should contain photo media")
}
viewModel.composeText = "Hi from SwiftUI"
await viewModel.send()