From d4b6fe6d8edeeac04fc03838413a54061206d089 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 12 Nov 2024 18:29:10 +0100 Subject: [PATCH] Allow admins to update conversation participants list --- Linphone/Localizable.xcstrings | 51 +++++++ .../Fragments/ConversationInfoFragment.swift | 143 +++++++++++++++++- .../ViewModel/ConversationViewModel.swift | 140 ++++++++++++++--- 3 files changed, 311 insertions(+), 23 deletions(-) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index a7ee0c03c..732930291 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -1500,6 +1500,57 @@ } } }, + "conversation_info_admin_menu_remove_participant" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove from the group" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retirer du groupe" + } + } + } + }, + "conversation_info_admin_menu_set_participant_admin" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Give admin rights" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donner les privilèges administrateur" + } + } + } + }, + "conversation_info_admin_menu_unset_participant_admin" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove admin rights" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retirer les privilèges administrateur" + } + } + } + }, "conversation_info_confirm_start_group_call_dialog_message" : { "extractionState" : "manual", "localizations" : { diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift index ba5a3a4c9..c5f5e7581 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift @@ -19,6 +19,7 @@ import SwiftUI +// swiftlint:disable type_body_length struct ConversationInfoFragment: View { @State private var orientation = UIDevice.current.orientation @@ -30,6 +31,8 @@ struct ConversationInfoFragment: View { @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel + @State var addParticipantsViewModel = AddParticipantsViewModel() + @Binding var isMuted: Bool @Binding var isShowEphemeralFragment: Bool @Binding var isShowStartCallGroupPopup: Bool @@ -282,7 +285,10 @@ struct ConversationInfoFragment: View { .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(1) - if conversationViewModel.participantConversationModelAdmin != nil && participantConversationModel.address == conversationViewModel.participantConversationModelAdmin!.address { + let participantConversationModelIsAdmin = conversationViewModel.participantConversationModelAdmin.first( + where: {$0.address == participantConversationModel.address}) + + if participantConversationModelIsAdmin != nil { Text("conversation_info_participant_is_admin_label") .foregroundStyle(Color.grayMain2c400) .default_text_style(styleSize: 12) @@ -290,12 +296,144 @@ struct ConversationInfoFragment: View { .lineLimit(1) } } + + if conversationViewModel.myParticipantConversationModel != nil && conversationViewModel.myParticipantConversationModel!.address != participantConversationModel.address { + Menu { + Button( + action: { + let addressConv = participantConversationModel.address + + let friendIndex = contactsManager.lastSearch.firstIndex( + where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressConv})}) + if friendIndex != nil { + withAnimation { + conversationViewModel.displayedConversation = nil + indexPage = 0 + contactViewModel.indexDisplayedFriend = friendIndex + } + } else { + withAnimation { + conversationViewModel.displayedConversation = nil + indexPage = 0 + + isShowEditContactFragment.toggle() + editContactViewModel.sipAddresses.removeAll() + editContactViewModel.sipAddresses.append(String(participantConversationModel.address.dropFirst(4) ?? "")) + editContactViewModel.sipAddresses.append("") + } + } + }, + label: { + HStack { + let addressConv = participantConversationModel.address + + let friendIndex = contactsManager.lastSearch.firstIndex( + where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressConv})}) + if friendIndex != nil { + Image("address-book") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + + Text("conversation_info_menu_go_to_contact") + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + Image("user-plus") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + + Text("conversation_info_menu_add_to_contacts") + .default_text_style(styleSize: 16) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + } + } + ) + + if conversationViewModel.isUserAdmin { + let participantConversationModelIsAdmin = conversationViewModel.participantConversationModelAdmin.first( + where: {$0.address == participantConversationModel.address}) + + Button { + conversationViewModel.toggleAdminRights(address: participantConversationModel.address) + } label: { + HStack { + Text(participantConversationModelIsAdmin != nil ? "conversation_info_admin_menu_unset_participant_admin" : "conversation_info_admin_menu_set_participant_admin") + Spacer() + Image("user-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + } + + Button(role: .destructive) { + conversationViewModel.removeParticipant(address: participantConversationModel.address) + } label: { + HStack { + Text("conversation_info_admin_menu_remove_participant") + Spacer() + Image("trash-simple-red") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + } + } + } label: { + Image("dots-three-vertical") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 4) + } + } } .padding(.vertical, 15) .padding(.horizontal, 20) } if conversationViewModel.isUserAdmin { + NavigationLink(destination: { + AddParticipantsFragment(addParticipantsViewModel: addParticipantsViewModel, confirmAddParticipantsFunc: conversationViewModel.addParticipants) + .onAppear { + conversationViewModel.getParticipants() + addParticipantsViewModel.participantsToAdd = conversationViewModel.participants + } + }, label: { + HStack { + Image("plus-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 20, height: 20) + + Text("conversation_info_add_participants_label") + .default_text_style_orange_500(styleSize: 14) + .frame(height: 35) + } + + }) + .padding(.horizontal, 20) + .padding(.vertical, 5) + .background(Color.orangeMain100) + .cornerRadius(60) + .padding(.top, 10) + .padding(.bottom, 20) + + /* Button( action: { }, @@ -319,6 +457,7 @@ struct ConversationInfoFragment: View { .cornerRadius(60) .padding(.top, 10) .padding(.bottom, 20) + */ } } .background(.white) @@ -513,6 +652,7 @@ struct ConversationInfoFragment: View { conversationsListViewModel: ConversationsListViewModel(), contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), + addParticipantsViewModel: AddParticipantsViewModel(), isMuted: .constant(false), isShowEphemeralFragment: .constant(false), isShowStartCallGroupPopup: .constant(false), @@ -521,3 +661,4 @@ struct ConversationInfoFragment: View { indexPage: .constant(0) ) } +// swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index ed9c1bea9..b74b31390 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -29,6 +29,8 @@ import AVFoundation class ConversationViewModel: ObservableObject { + static let TAG = "[ConversationViewModel]" + private var coreContext = CoreContext.shared @Published var displayedConversation: ConversationModel? @@ -79,8 +81,10 @@ class ConversationViewModel: ObservableObject { @Published var conversationMessagesSection: [MessagesSection] = [] @Published var participantConversationModel: [ContactAvatarModel] = [] - @Published var participantConversationModelAdmin: ContactAvatarModel? + @Published var participantConversationModelAdmin: [ContactAvatarModel] = [] + @Published var myParticipantConversationModel: ContactAvatarModel? = nil @Published var isUserAdmin: Bool = false + @Published var participants: [SelectedAddressModel] = [] @Published var mediasToSend: [Attachment] = [] var maxMediaCount = 12 @@ -125,10 +129,13 @@ class ConversationViewModel: ObservableObject { self.getNewMessages(eventLogs: [eventLog]) }, onParticipantAdded: { (_: ChatRoom, eventLogs: EventLog) in self.getNewMessages(eventLogs: [eventLogs]) + self.getParticipantConversationModel() }, onParticipantRemoved: { (_: ChatRoom, eventLogs: EventLog) in self.getNewMessages(eventLogs: [eventLogs]) + self.getParticipantConversationModel() }, onParticipantAdminStatusChanged: { (_: ChatRoom, eventLogs: EventLog) in self.getNewMessages(eventLogs: [eventLogs]) + self.getParticipantConversationModel() }, onSubjectChanged: { (_: ChatRoom, eventLogs: EventLog) in self.getNewMessages(eventLogs: [eventLogs]) }, onConferenceJoined: {(_: ChatRoom, eventLog: EventLog) in @@ -317,7 +324,7 @@ class ConversationViewModel: ObservableObject { if self.displayedConversation != nil { DispatchQueue.main.async { self.isUserAdmin = false - self.participantConversationModelAdmin = nil + self.participantConversationModelAdmin.removeAll() self.participantConversationModel.removeAll() } self.displayedConversation!.chatRoom.participants.forEach { participant in @@ -326,7 +333,7 @@ class ConversationViewModel: ObservableObject { let avatarModelTmp = avatarResult if participant.isAdmin { DispatchQueue.main.async { - self.participantConversationModelAdmin = avatarModelTmp + self.participantConversationModelAdmin.append(avatarModelTmp) self.participantConversationModel.append(avatarModelTmp) } } else { @@ -344,12 +351,14 @@ class ConversationViewModel: ObservableObject { if self.displayedConversation!.chatRoom.me!.isAdmin { DispatchQueue.main.async { self.isUserAdmin = true - self.participantConversationModelAdmin = avatarModelTmp + self.participantConversationModelAdmin.append(avatarModelTmp) self.participantConversationModel.append(avatarModelTmp) + self.myParticipantConversationModel = avatarModelTmp } } else { DispatchQueue.main.async { self.participantConversationModel.append(avatarModelTmp) + self.myParticipantConversationModel = avatarModelTmp } } } @@ -2076,24 +2085,6 @@ class ConversationViewModel: ObservableObject { } } - func setNewChatRoomSubject() { - if self.displayedConversation != nil && self.conversationInfoPopupText != self.displayedConversation!.subject { - - coreContext.doOnCoreQueue { _ in - self.displayedConversation!.chatRoom.subject = self.conversationInfoPopupText - } - - self.displayedConversation!.subject = self.conversationInfoPopupText - self.displayedConversation!.avatarModel = ContactAvatarModel( - friend: self.displayedConversation!.avatarModel.friend, - name: self.conversationInfoPopupText, - address: self.displayedConversation!.avatarModel.address, - withPresence: false - ) - self.isShowConversationInfoPopup = false - } - } - func getEphemeralTime() { coreContext.doOnCoreQueue { _ in if self.displayedConversation != nil { @@ -2124,6 +2115,111 @@ class ConversationViewModel: ObservableObject { } } } + + func setNewChatRoomSubject() { + if self.displayedConversation != nil && self.conversationInfoPopupText != self.displayedConversation!.subject { + + coreContext.doOnCoreQueue { _ in + self.displayedConversation!.chatRoom.subject = self.conversationInfoPopupText + } + + self.displayedConversation!.subject = self.conversationInfoPopupText + self.displayedConversation!.avatarModel = ContactAvatarModel( + friend: self.displayedConversation!.avatarModel.friend, + name: self.conversationInfoPopupText, + address: self.displayedConversation!.avatarModel.address, + withPresence: false + ) + self.isShowConversationInfoPopup = false + } + } + + func getParticipants() { + self.participants = [] + var list: [SelectedAddressModel] = [] + for participant in participantConversationModel { + let addr = try? Factory.Instance.createAddress(addr: participant.address) + if addr != nil { + if let found = list.first(where: { $0.address.weakEqual(address2: addr!) }) { + Log.info("\(ConversationViewModel.TAG) Participant \(found.address.asStringUriOnly()) already in list, skipping") + continue + } + + if self.displayedConversation!.chatRoom.me != nil && self.displayedConversation!.chatRoom.me!.address != nil && !self.displayedConversation!.chatRoom.me!.address!.weakEqual(address2: addr!) { + list.append(SelectedAddressModel(addr: addr!, avModel: participant)) + Log.info("\(ConversationViewModel.TAG) Added participant \(addr!.asStringUriOnly())") + } + } + } + + self.participants = list + + Log.info("\(ConversationViewModel.TAG) \(list.count) participants added to chat room") + } + + func addParticipants(participantsToAdd: [SelectedAddressModel]) { + var list: [SelectedAddressModel] = [] + for selectedAddr in participantsToAdd { + if let found = list.first(where: { $0.address.weakEqual(address2: selectedAddr.address) }) { + Log.info("\(ConversationViewModel.TAG) Participant \(found.address.asStringUriOnly()) already in list, skipping") + continue + } + + list.append(selectedAddr) + Log.info("\(ConversationViewModel.TAG) Added participant \(selectedAddr.address.asStringUriOnly())") + } + + let participantsAddress = self.displayedConversation!.chatRoom.participants.map { $0.address?.asStringUriOnly() } + let listAddress = list.map { $0.address.asStringUriOnly() } + + let differences = participantsAddress.difference(from: listAddress) + + if !differences.isEmpty { + let differenceAddresses = differences.compactMap { change -> String? in + switch change { + case .insert(_, let element, _), .remove(_, let element, _): + return element + } + } + + let filteredParticipants = self.displayedConversation!.chatRoom.participants.filter { participant in + differenceAddresses.contains(participant.address!.asStringUriOnly()) + } + + coreContext.doOnCoreQueue { _ in + _ = self.displayedConversation!.chatRoom.addParticipants(addresses: list.map { $0.address }) + self.displayedConversation!.chatRoom.removeParticipants(participants: filteredParticipants) + } + } else { + coreContext.doOnCoreQueue { _ in + _ = self.displayedConversation!.chatRoom.addParticipants(addresses: list.map { $0.address }) + } + } + + Log.info("\(ConversationViewModel.TAG) \(list.count) participants added to chat room") + } + + func toggleAdminRights(address: String) { + if self.displayedConversation != nil { + coreContext.doOnCoreQueue { _ in + if let participant = self.displayedConversation!.chatRoom.participants.first(where: {$0.address?.asStringUriOnly() == address}) { + self.displayedConversation!.chatRoom.setParticipantAdminStatus(participant: participant, isAdmin: !participant.isAdmin) + } + + } + } + } + + func removeParticipant(address: String) { + if self.displayedConversation != nil { + coreContext.doOnCoreQueue { _ in + if let participant = self.displayedConversation!.chatRoom.participants.first(where: {$0.address?.asStringUriOnly() == address}) { + self.displayedConversation!.chatRoom.removeParticipant(participant: participant) + } + + } + } + } } // swiftlint:enable line_length // swiftlint:enable type_body_length