Files
Luma/Sources/Shared/UI/GroupInfoView.swift
T
wt cbb820a86e
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
migrate persistence from JSON to SwiftData
Models:
- Convert Conversation and ChatMessage to SwiftData @Model classes with
  a
  one-to-many relationship and cascade delete.
- Rename ChatMessage.id to clientID and drop Conversation.id in favor of
  jid
  (the string id would clash with PersistentIdentifier).
- Keep MessageReaction as a Codable value stored in an array attribute.

Persistence:
- Add ArchiveStore, a per-account ModelContainer/ModelContext that
  replaces
  the JSON ChatArchive with load/save/erase.
- Add ArchiveMetadataRecord for the durable MAM checkpoints, cursor,
  locally
  deleted message IDs and roster contact JIDs.
- Add LegacyArchiveImporter to import the old JSON snapshot once and
  delete
  the file afterwards, preserving existing history.

AppModel:
- Replace ChatArchive with ArchiveStore and persist via context.save();
  insert new models and delete the replaced object on upsert merges so
  SwiftData never keeps an orphaned duplicate.

Tests:
- Replace ChatArchiveTests with ArchiveStoreTests (store round-trip and
  legacy import) and drop the now-obsolete Codable round-trip
  assertions.
2026-08-18 22:58:18 +07:00

122 lines
4.8 KiB
Swift

import Foundation
import SwiftUI
@MainActor
struct GroupInfoView: View {
@ObservedObject var model: AppModel
let conversation: Conversation
@Environment(\.dismiss) private var dismiss
@State private var nickname: String
@State private var invitees = ""
@State private var isJoining = false
@State private var isInviting = false
init(model: AppModel, conversation: Conversation) {
self.model = model
self.conversation = conversation
_nickname = State(initialValue: conversation.groupNickname ?? model.suggestedGroupNickname)
}
var body: some View {
NavigationStack {
Form {
Section("Групповой чат") {
LabeledContent("Комната", value: conversation.jid)
LabeledContent("Состояние", value: liveConversation.isGroupJoined ? "Подключено" : "Не подключено")
LabeledContent("Участников", value: "\(liveConversation.occupantCount)")
TextField("Ваш псевдоним", text: $nickname)
.disabled(liveConversation.isGroupJoined)
}
if !liveConversation.isGroupJoined {
Section {
Button {
join()
} label: {
Label(liveConversation.invitedBy == nil ? "Войти в комнату" : "Принять приглашение", systemImage: "person.2.fill")
}
.disabled(nickname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isJoining)
} footer: {
if let inviter = liveConversation.invitedBy {
Text("Приглашение от \(inviter)")
}
}
} else {
Section("Пригласить участников") {
TextField("user@example.org, ещё@example.org", text: $invitees, axis: .vertical)
.lineLimit(2...4)
Button {
invite()
} label: {
if isInviting {
ProgressView()
} else {
Label("Отправить приглашения", systemImage: "person.badge.plus")
}
}
.disabled(parsedInvitees.isEmpty || isInviting)
}
Section {
Button(role: .destructive) {
model.leaveGroup(jid: conversation.jid)
dismiss()
} label: {
Label("Покинуть комнату", systemImage: "rectangle.portrait.and.arrow.right")
}
}
}
Section {
Label(
model.encryptionEnabled(for: conversation.jid)
? "Для комнаты включено OMEMO. Нужны неанонимный MUC и OMEMO-устройства у всех участников."
: "OMEMO для этой комнаты выключено: сообщения передаются открытым текстом внутри TLS.",
systemImage: model.encryptionEnabled(for: conversation.jid) ? "lock.fill" : "lock.open"
)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.navigationTitle(conversation.displayName)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Готово") { dismiss() }
}
}
}
#if os(macOS)
.frame(minWidth: 480, minHeight: 500)
#endif
}
private var liveConversation: Conversation {
model.conversations.first(where: { $0.jid == conversation.jid }) ?? conversation
}
private var parsedInvitees: [String] {
invitees.components(separatedBy: CharacterSet(charactersIn: ",;\n \t"))
.filter { !$0.isEmpty }
}
private func join() {
isJoining = true
Task {
await model.joinGroup(jid: conversation.jid, nickname: nickname)
isJoining = false
}
}
private func invite() {
let outgoing = parsedInvitees
guard !outgoing.isEmpty else { return }
isInviting = true
Task {
await model.inviteMembers(outgoing, to: conversation.jid)
invitees = ""
isInviting = false
}
}
}