ios_client
This commit is contained in:
301
ios_client 0.8.5/Kecalek/Views/Groups/GroupInfoView.swift
Normal file
301
ios_client 0.8.5/Kecalek/Views/Groups/GroupInfoView.swift
Normal file
@@ -0,0 +1,301 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
struct GroupInfoView: View {
|
||||
@Binding var conversation: Conversation
|
||||
var appState: AppState
|
||||
var conversationListVM: ConversationListVM?
|
||||
@State private var showRenameSheet = false
|
||||
@State private var showLeaveConfirm = false
|
||||
@State private var showAddMember = false
|
||||
@State private var showRemoveConfirm = false
|
||||
@State private var showAvatarPicker = false
|
||||
@State private var newName = ""
|
||||
@State private var addMemberEmail = ""
|
||||
@State private var memberToRemove: ConversationMember?
|
||||
@State private var errorMessage: String?
|
||||
@State private var showError = false
|
||||
@State private var isUploadingAvatar = false
|
||||
@State private var groupAvatarData: Data?
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
private var isCreator: Bool {
|
||||
conversation.createdBy == appState.currentUser?.id
|
||||
}
|
||||
|
||||
private func refreshConversation() async {
|
||||
let convs = await appState.chatClient.listConversations()
|
||||
if let updated = convs.first(where: { $0.id == conversation.id }) {
|
||||
conversation = updated
|
||||
}
|
||||
await conversationListVM?.forceRefresh(chatClient: appState.chatClient)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
// Avatar section
|
||||
Section {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack(spacing: 8) {
|
||||
CircularAvatarView(
|
||||
name: conversation.name ?? "Group",
|
||||
imageData: groupAvatarData ?? conversationListVM?.avatarCache[conversation.id],
|
||||
size: 64,
|
||||
isGroup: true
|
||||
)
|
||||
|
||||
Text(conversation.name ?? "Group")
|
||||
.font(.title2.bold())
|
||||
|
||||
Text("\(conversation.members.count) members")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
// Actions
|
||||
if isCreator {
|
||||
Section {
|
||||
Button("Add Member") {
|
||||
addMemberEmail = ""
|
||||
showAddMember = true
|
||||
}
|
||||
|
||||
Button("Rename Group") {
|
||||
newName = conversation.name ?? ""
|
||||
showRenameSheet = true
|
||||
}
|
||||
|
||||
Button {
|
||||
showAvatarPicker = true
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Change Avatar")
|
||||
if isUploadingAvatar {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(isUploadingAvatar)
|
||||
}
|
||||
}
|
||||
|
||||
// Members
|
||||
Section("Members") {
|
||||
ForEach(conversation.members) { member in
|
||||
HStack {
|
||||
CircularAvatarView(name: member.username, size: 32, isGroup: false)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(member.username)
|
||||
.font(.body)
|
||||
Text(member.email)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if member.userId == conversation.createdBy {
|
||||
Text("Admin")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
if isCreator && member.userId != appState.currentUser?.id {
|
||||
Button("Remove from Group", role: .destructive) {
|
||||
memberToRemove = member
|
||||
showRemoveConfirm = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Leave / Delete
|
||||
Section {
|
||||
Button("Leave Group", role: .destructive) {
|
||||
showLeaveConfirm = true
|
||||
}
|
||||
|
||||
if isCreator {
|
||||
Button("Delete Group", role: .destructive) {
|
||||
Task {
|
||||
_ = await appState.chatClient.deleteConversation(convId: conversation.id)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Group Info")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
}
|
||||
.alert("Leave Group?", isPresented: $showLeaveConfirm) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Leave", role: .destructive) {
|
||||
Task {
|
||||
_ = await appState.chatClient.leaveGroup(convId: conversation.id)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Remove Member?", isPresented: $showRemoveConfirm) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Remove", role: .destructive) {
|
||||
if let member = memberToRemove {
|
||||
Task {
|
||||
let (success, msg) = await appState.chatClient.removeMember(
|
||||
convId: conversation.id, userId: member.userId
|
||||
)
|
||||
if success {
|
||||
await refreshConversation()
|
||||
} else {
|
||||
errorMessage = msg
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
if let member = memberToRemove {
|
||||
Text("Remove \(member.username) from the group?")
|
||||
}
|
||||
}
|
||||
.alert("Add Member", isPresented: $showAddMember) {
|
||||
TextField("Email", text: $addMemberEmail)
|
||||
.textInputAutocapitalization(.never)
|
||||
.keyboardType(.emailAddress)
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Add") {
|
||||
let email = addMemberEmail.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !email.isEmpty else { return }
|
||||
Task {
|
||||
let (success, msg) = await appState.chatClient.addMember(
|
||||
convId: conversation.id, email: email
|
||||
)
|
||||
if success {
|
||||
await refreshConversation()
|
||||
} else {
|
||||
errorMessage = msg
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Rename Group", isPresented: $showRenameSheet) {
|
||||
TextField("Group Name", text: $newName)
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Rename") {
|
||||
let trimmedName = newName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedName.isEmpty else { return }
|
||||
// Optimistic update - immediately reflect in UI
|
||||
conversation.name = trimmedName
|
||||
Task {
|
||||
let (success, _) = await appState.chatClient.renameConversation(convId: conversation.id, name: trimmedName)
|
||||
if success {
|
||||
await refreshConversation()
|
||||
} else {
|
||||
// Revert on failure
|
||||
await refreshConversation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Error", isPresented: $showError) {
|
||||
Button("OK") {}
|
||||
} message: {
|
||||
Text(errorMessage ?? "")
|
||||
}
|
||||
.sheet(isPresented: $showAvatarPicker) {
|
||||
AvatarPickerView { imageData in
|
||||
isUploadingAvatar = true
|
||||
Task {
|
||||
let success = await appState.chatClient.updateGroupAvatar(
|
||||
convId: conversation.id, imageData: imageData
|
||||
)
|
||||
isUploadingAvatar = false
|
||||
if success {
|
||||
// Update local avatar cache (memory + disk)
|
||||
groupAvatarData = imageData
|
||||
conversationListVM?.updateAvatar(convId: conversation.id, data: imageData)
|
||||
await refreshConversation()
|
||||
} else {
|
||||
errorMessage = "Failed to update avatar"
|
||||
showError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
// Load current group avatar
|
||||
if groupAvatarData == nil, let cached = conversationListVM?.avatarCache[conversation.id] {
|
||||
groupAvatarData = cached
|
||||
} else if groupAvatarData == nil {
|
||||
groupAvatarData = await appState.chatClient.getGroupAvatar(convId: conversation.id)
|
||||
}
|
||||
await refreshConversation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Avatar Picker (PHPicker wrapper for avatar selection)
|
||||
|
||||
private struct AvatarPickerView: UIViewControllerRepresentable {
|
||||
let onImagePicked: (Data) -> Void
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(onImagePicked: onImagePicked)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var config = PHPickerConfiguration()
|
||||
config.filter = .images
|
||||
config.selectionLimit = 1
|
||||
let picker = PHPickerViewController(configuration: config)
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||
|
||||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||
let onImagePicked: (Data) -> Void
|
||||
|
||||
init(onImagePicked: @escaping (Data) -> Void) {
|
||||
self.onImagePicked = onImagePicked
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
guard let provider = results.first?.itemProvider,
|
||||
provider.canLoadObject(ofClass: UIImage.self) else {
|
||||
picker.dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
provider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in
|
||||
guard let uiImage = image as? UIImage,
|
||||
let data = uiImage.jpegData(compressionQuality: 0.8) else {
|
||||
DispatchQueue.main.async { picker.dismiss(animated: true) }
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self?.onImagePicked(data)
|
||||
picker.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user