added preview for views
This commit is contained in:
1 parent
1650145d4d
commit
1e2670903f
36 files changed
+744
-16
No files matched your search
@@ -0,0 +1,300 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
/// Shared fixtures for SwiftUI previews.
|
||||
///
|
||||
/// Building an `AppModel` wires up the XMPP service, the WebRTC call engine
|
||||
/// and the keychain-backed stores, so previews share one instance instead of
|
||||
/// rebuilding it per canvas (the model skips account bootstrap inside the
|
||||
/// preview host). SwiftData screens share one stable in-memory container and
|
||||
/// seeded context so `@Query`-backed lists render sample data without
|
||||
/// touching the real archive or re-fetching on every body evaluation.
|
||||
enum PreviewSupport {
|
||||
/// Single shared model: no account, no network traffic, connection is
|
||||
/// "disconnected" — every screen renders in its resting state.
|
||||
static let model: AppModel = MainActor.assumeIsolated { AppModel() }
|
||||
|
||||
// MARK: - SwiftData
|
||||
|
||||
/// Shared in-memory container for `@Query`-backed previews. One stable
|
||||
/// container/context pair keeps SwiftUI list diffing from seeing a
|
||||
/// brand-new fetch result on every body evaluation.
|
||||
static let container: ModelContainer = {
|
||||
do {
|
||||
return try ModelContainer(
|
||||
for: Conversation.self, ChatMessage.self,
|
||||
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
)
|
||||
} catch {
|
||||
fatalError("Failed to create the preview container: \(error)")
|
||||
}
|
||||
}()
|
||||
|
||||
/// Seeded context: two direct chats, one group and a short Alice thread.
|
||||
@MainActor
|
||||
static let previewContext: ModelContext = {
|
||||
let context = container.mainContext
|
||||
|
||||
let alice = conversation(
|
||||
jid: "alice@example.org",
|
||||
displayName: "Алиса",
|
||||
lastMessage: "Договорились 👌",
|
||||
lastActivity: Date().addingTimeInterval(-120),
|
||||
unreadCount: 2,
|
||||
isOnline: true
|
||||
)
|
||||
let bob = conversation(
|
||||
jid: "bob@example.org",
|
||||
displayName: "Боб",
|
||||
lastMessage: "Файл: отчёт.pdf",
|
||||
lastActivity: Date().addingTimeInterval(-3_600)
|
||||
)
|
||||
let team = conversation(
|
||||
jid: "team@conference.example.org",
|
||||
displayName: "Команда Luma",
|
||||
lastMessage: "Иван: созвон в 15:00",
|
||||
lastActivity: Date().addingTimeInterval(-86_400),
|
||||
unreadCount: 5,
|
||||
kind: .group,
|
||||
isGroupJoined: true,
|
||||
occupantCount: 7
|
||||
)
|
||||
context.insert(alice)
|
||||
context.insert(bob)
|
||||
context.insert(team)
|
||||
|
||||
let aliceJID = alice.jid
|
||||
context.insert(
|
||||
message(
|
||||
conversationID: aliceJID,
|
||||
senderJID: aliceJID,
|
||||
body: "Привет! Как дела?",
|
||||
timestamp: Date().addingTimeInterval(-600),
|
||||
direction: .incoming,
|
||||
delivery: .delivered
|
||||
)
|
||||
)
|
||||
context.insert(
|
||||
message(
|
||||
conversationID: aliceJID,
|
||||
senderJID: "me@example.org",
|
||||
body: "Отлично! Созвонимся вечером?",
|
||||
timestamp: Date().addingTimeInterval(-420),
|
||||
direction: .outgoing,
|
||||
delivery: .sent
|
||||
)
|
||||
)
|
||||
context.insert(
|
||||
message(
|
||||
conversationID: aliceJID,
|
||||
senderJID: aliceJID,
|
||||
body: "Договорились 👌",
|
||||
timestamp: Date().addingTimeInterval(-60),
|
||||
direction: .incoming,
|
||||
delivery: .delivered
|
||||
)
|
||||
)
|
||||
return context
|
||||
}()
|
||||
|
||||
// MARK: - Models
|
||||
|
||||
static func conversation(
|
||||
jid: String = "alice@example.org",
|
||||
displayName: String? = nil,
|
||||
lastMessage: String = "",
|
||||
lastActivity: Date = .distantPast,
|
||||
unreadCount: Int = 0,
|
||||
isOnline: Bool = false,
|
||||
isPinned: Bool = false,
|
||||
encryptionPreference: EncryptionPreference = .inheritGlobal,
|
||||
kind: Conversation.Kind = .direct,
|
||||
groupNickname: String? = nil,
|
||||
isGroupJoined: Bool = false,
|
||||
shouldAutojoin: Bool = false,
|
||||
occupantCount: Int = 0,
|
||||
invitedBy: String? = nil
|
||||
) -> Conversation {
|
||||
Conversation(
|
||||
jid: jid,
|
||||
displayName: displayName,
|
||||
lastMessage: lastMessage,
|
||||
lastActivity: lastActivity,
|
||||
unreadCount: unreadCount,
|
||||
isOnline: isOnline,
|
||||
isPinned: isPinned,
|
||||
encryptionPreference: encryptionPreference,
|
||||
kind: kind,
|
||||
groupNickname: groupNickname,
|
||||
isGroupJoined: isGroupJoined,
|
||||
shouldAutojoin: shouldAutojoin,
|
||||
occupantCount: occupantCount,
|
||||
invitedBy: invitedBy
|
||||
)
|
||||
}
|
||||
|
||||
static func message(
|
||||
id: String = UUID().uuidString,
|
||||
conversationID: String = "alice@example.org",
|
||||
senderJID: String = "alice@example.org",
|
||||
body: String = "Привет! Как дела?",
|
||||
timestamp: Date = Date(),
|
||||
direction: ChatMessage.Direction = .incoming,
|
||||
delivery: ChatMessage.Delivery = .delivered,
|
||||
security: ChatMessage.Security = .omemo,
|
||||
kind: ChatMessage.Kind = .text,
|
||||
remoteAttachmentURL: String? = nil,
|
||||
localFilename: String? = nil,
|
||||
mimeType: String? = nil,
|
||||
duration: TimeInterval? = nil,
|
||||
byteCount: Int? = nil,
|
||||
senderDisplayName: String? = nil,
|
||||
isGroupMessage: Bool = false
|
||||
) -> ChatMessage {
|
||||
ChatMessage(
|
||||
id: id,
|
||||
conversationID: conversationID,
|
||||
senderJID: senderJID,
|
||||
body: body,
|
||||
timestamp: timestamp,
|
||||
direction: direction,
|
||||
delivery: delivery,
|
||||
security: security,
|
||||
kind: kind,
|
||||
remoteAttachmentURL: remoteAttachmentURL,
|
||||
localFilename: localFilename,
|
||||
mimeType: mimeType,
|
||||
duration: duration,
|
||||
byteCount: byteCount,
|
||||
senderDisplayName: senderDisplayName,
|
||||
isGroupMessage: isGroupMessage
|
||||
)
|
||||
}
|
||||
|
||||
static func photoMessage(
|
||||
body: String = "Фото с прогулки",
|
||||
filename: String = "IMG_1234.jpg",
|
||||
byteCount: Int = 2_420_112
|
||||
) -> ChatMessage {
|
||||
message(
|
||||
body: body,
|
||||
kind: .photo,
|
||||
localFilename: filename,
|
||||
mimeType: "image/jpeg",
|
||||
byteCount: byteCount
|
||||
)
|
||||
}
|
||||
|
||||
static func videoMessage(
|
||||
body: String = "Видео с концерта",
|
||||
filename: String = "IMG_5678.mov",
|
||||
duration: TimeInterval = 42,
|
||||
byteCount: Int = 18_330_240
|
||||
) -> ChatMessage {
|
||||
message(
|
||||
body: body,
|
||||
kind: .video,
|
||||
localFilename: filename,
|
||||
mimeType: "video/quicktime",
|
||||
duration: duration,
|
||||
byteCount: byteCount
|
||||
)
|
||||
}
|
||||
|
||||
static func videoNoteMessage(
|
||||
body: String = "",
|
||||
duration: TimeInterval = 12
|
||||
) -> ChatMessage {
|
||||
message(
|
||||
body: body,
|
||||
kind: .videoNote,
|
||||
mimeType: "video/mp4",
|
||||
duration: duration,
|
||||
byteCount: 1_840_000
|
||||
)
|
||||
}
|
||||
|
||||
static func voiceMessage(
|
||||
body: String = "",
|
||||
duration: TimeInterval = 9
|
||||
) -> ChatMessage {
|
||||
message(
|
||||
body: body,
|
||||
kind: .voice,
|
||||
mimeType: "audio/m4a",
|
||||
duration: duration,
|
||||
byteCount: 180_000
|
||||
)
|
||||
}
|
||||
|
||||
static func callSnapshot(
|
||||
peerJID: String = "alice@example.org",
|
||||
direction: CallDirection = .incoming,
|
||||
media: Set<CallMedia> = [.video],
|
||||
phase: CallPhase = .ringing,
|
||||
isMuted: Bool = false,
|
||||
isCameraEnabled: Bool = true,
|
||||
isSpeakerEnabled: Bool = true
|
||||
) -> CallSnapshot {
|
||||
CallSnapshot(
|
||||
id: UUID(),
|
||||
peerJID: peerJID,
|
||||
direction: direction,
|
||||
media: media,
|
||||
phase: phase,
|
||||
connectedAt: phase == .connected ? Date().addingTimeInterval(-75) : nil,
|
||||
isMuted: isMuted,
|
||||
isCameraEnabled: isCameraEnabled,
|
||||
isSpeakerEnabled: isSpeakerEnabled,
|
||||
hasLocalVideo: media.contains(.video),
|
||||
hasRemoteVideo: media.contains(.video)
|
||||
)
|
||||
}
|
||||
|
||||
static func drafts() -> [AttachmentDraft] {
|
||||
[
|
||||
AttachmentDraft(
|
||||
url: URL(fileURLWithPath: "/tmp/preview-photo.jpg"),
|
||||
filename: "IMG_1234.jpg",
|
||||
mimeType: "image/jpeg",
|
||||
kind: .photo,
|
||||
byteCount: 2_420_112
|
||||
),
|
||||
AttachmentDraft(
|
||||
url: URL(fileURLWithPath: "/tmp/preview-video.mov"),
|
||||
filename: "IMG_5678.mov",
|
||||
mimeType: "video/quicktime",
|
||||
kind: .video,
|
||||
duration: 21,
|
||||
byteCount: 18_330_240
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Composed scenes
|
||||
|
||||
/// Populated chat screen: a conversation plus the seeded Alice thread so
|
||||
/// the `@Query`-driven timeline renders bubbles instead of an empty
|
||||
/// state.
|
||||
@MainActor
|
||||
static func chatPreview() -> some View {
|
||||
let conversation = conversation(
|
||||
jid: "alice@example.org",
|
||||
displayName: "Алиса",
|
||||
lastMessage: "Договорились 👌",
|
||||
lastActivity: Date().addingTimeInterval(-60),
|
||||
isOnline: true
|
||||
)
|
||||
return ChatView(model: model, conversation: conversation)
|
||||
.environment(\.modelContext, previewContext)
|
||||
}
|
||||
|
||||
/// Main screen backed by the seeded in-memory context so the chat list
|
||||
/// shows conversations on both platforms.
|
||||
@MainActor
|
||||
static func mainPreview() -> some View {
|
||||
MainTabView(model: model)
|
||||
.environment(\.modelContext, previewContext)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user