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) } } } } }