From d84f10de3909ec54c76fa79e971de837826d3d76 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Tue, 5 Nov 2024 18:11:04 +0100 Subject: [PATCH 01/18] Update build to (58) --- Linphone.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index e1c59af06..d837e61ab 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -1258,7 +1258,7 @@ CODE_SIGN_ENTITLEMENTS = msgNotificationService/msgNotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 58; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1301,7 +1301,7 @@ CODE_SIGN_ENTITLEMENTS = msgNotificationService/msgNotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 58; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1458,7 +1458,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Linphone/Linphone.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 58; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\""; @@ -1515,7 +1515,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Linphone/Linphone.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 58; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\""; DEVELOPMENT_TEAM = Z2V957B3D6; From 34d125eff64b330163ac44cc6e8993be02dda287 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Thu, 7 Nov 2024 17:23:48 +0100 Subject: [PATCH 02/18] Fix videopreview image stretching in simple calls --- Linphone/UI/Call/CallView.swift | 34 ++++++++++++++------- Linphone/UI/Call/Model/CallStatsModel.swift | 18 +++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 786b561d7..f8fdbab45 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -59,6 +59,7 @@ struct CallView: View { @State private var pointingUp: CGFloat = 0.0 @State private var currentOffset: CGFloat = 0.0 @State var displayVideo = false + @State private var previewVideoLocation = CGPoint(x: 0, y: 0) @Binding var fullscreenVideo: Bool @State var isShowCallsListFragment: Bool = false @@ -554,23 +555,28 @@ struct CallView: View { Spacer() VStack { Spacer() - LinphoneVideoViewHolder { view in - coreContext.doOnCoreQueue { core in - core.nativePreviewWindow = view + HStack { + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + core.nativePreviewWindow = view + } } + .aspectRatio(callViewModel.callStatsModel.sentVideoWindow.widthFactor/callViewModel.callStatsModel.sentVideoWindow.heightFactor, contentMode: .fill) + .frame(maxWidth: callViewModel.callStatsModel.sentVideoWindow.widthFactor * 256, + maxHeight: callViewModel.callStatsModel.sentVideoWindow.heightFactor * 256) + .clipped() } .frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2) .cornerRadius(20) - .padding(10) - .padding(.trailing, abs(angleDegree/2)) } + .padding(10) + .padding(.trailing, abs(angleDegree/2)) } .frame( maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom ) } - if telecomManager.outgoingCallStarted { VStack { ActivityIndicator(color: .white) @@ -662,16 +668,22 @@ struct CallView: View { Spacer() VStack { Spacer() - LinphoneVideoViewHolder { view in - coreContext.doOnCoreQueue { core in - core.nativePreviewWindow = view + HStack { + LinphoneVideoViewHolder { view in + coreContext.doOnCoreQueue { core in + core.nativePreviewWindow = view + } } + .aspectRatio(callViewModel.callStatsModel.sentVideoWindow.widthFactor/callViewModel.callStatsModel.sentVideoWindow.heightFactor, contentMode: .fill) + .frame(maxWidth: callViewModel.callStatsModel.sentVideoWindow.widthFactor * 256, + maxHeight: callViewModel.callStatsModel.sentVideoWindow.heightFactor * 256) + .clipped() } .frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2) .cornerRadius(20) - .padding(10) - .padding(.trailing, abs(angleDegree/2)) } + .padding(10) + .padding(.trailing, abs(angleDegree/2)) } .frame( maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8, diff --git a/Linphone/UI/Call/Model/CallStatsModel.swift b/Linphone/UI/Call/Model/CallStatsModel.swift index 7e19cfe62..cdc8950ad 100644 --- a/Linphone/UI/Call/Model/CallStatsModel.swift +++ b/Linphone/UI/Call/Model/CallStatsModel.swift @@ -20,6 +20,20 @@ import Foundation import linphonesw +class SentVideoWindow: ObservableObject { + var widthFactor: CGFloat = 1 + var heightFactor: CGFloat = 1 + var isVertical = true + init(videoWidth: UInt, videoHeight: UInt) { + self.isVertical = videoWidth < videoHeight + if isVertical { + self.widthFactor = videoHeight == 0 ? 1 : CGFloat(videoWidth) / CGFloat(videoHeight) + } else { + self.heightFactor = videoWidth == 0 ? 1 : CGFloat(videoHeight) / CGFloat(videoWidth) + } + } +} + class CallStatsModel: ObservableObject { var coreContext = CoreContext.shared @@ -32,6 +46,7 @@ class CallStatsModel: ObservableObject { @Published var videoCodec = "" @Published var videoBandwidth = "" @Published var videoLossRate = "" + @Published var sentVideoWindow = SentVideoWindow(videoWidth: 480, videoHeight: 640) @Published var videoResolution = "" @Published var videoFps = "" @@ -88,6 +103,8 @@ class CallStatsModel: ObservableObject { let lossRateLabel = "Lossrate: ↑ \(senderLossRate)% ↓ \(receiverLossRate)%" let sentResolution = call.currentParams!.sentVideoDefinition!.name + let sentVideoWindow = SentVideoWindow(videoWidth: call.currentParams!.sentVideoDefinition!.width + , videoHeight: call.currentParams!.sentVideoDefinition!.height) let receivedResolution = call.currentParams!.receivedVideoDefinition!.name let resolutionLabel = "Resolution: " + "↑ \(sentResolution!) ↓ \(receivedResolution!)" @@ -99,6 +116,7 @@ class CallStatsModel: ObservableObject { self.videoCodec = codecLabel self.videoBandwidth = bandwidthLabel self.videoLossRate = lossRateLabel + self.sentVideoWindow = sentVideoWindow self.videoResolution = resolutionLabel self.videoFps = fpsLabel } From aa10440bc91befbb8a51e6858e722d240278dd00 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 8 Nov 2024 11:33:02 +0100 Subject: [PATCH 03/18] Make preview video draggable in simple calls --- Linphone/UI/Call/CallView.swift | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index f8fdbab45..bc90b7ace 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -59,7 +59,8 @@ struct CallView: View { @State private var pointingUp: CGFloat = 0.0 @State private var currentOffset: CGFloat = 0.0 @State var displayVideo = false - @State private var previewVideoLocation = CGPoint(x: 0, y: 0) + @State private var previewVideoOffset = CGSize.zero + @State private var previewVideoOffsetPreviousDrag = CGSize.zero @Binding var fullscreenVideo: Bool @State var isShowCallsListFragment: Bool = false @@ -566,8 +567,20 @@ struct CallView: View { maxHeight: callViewModel.callStatsModel.sentVideoWindow.heightFactor * 256) .clipped() } - .frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2) + .frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2) // 144*192 .cornerRadius(20) + .gesture( + DragGesture(coordinateSpace: .global) + .onChanged { value in + previewVideoOffset = CGSize(width: previewVideoOffsetPreviousDrag.width + value.translation.width, + height: previewVideoOffsetPreviousDrag.height + value.translation.height) + } + .onEnded { _ in + previewVideoOffsetPreviousDrag = previewVideoOffset + } + ) + + .offset(x: previewVideoOffset.width, y: previewVideoOffset.height) } .padding(10) .padding(.trailing, abs(angleDegree/2)) @@ -679,8 +692,20 @@ struct CallView: View { maxHeight: callViewModel.callStatsModel.sentVideoWindow.heightFactor * 256) .clipped() } - .frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2) + .frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2) // 144*192 .cornerRadius(20) + .gesture( + DragGesture(coordinateSpace: .global) + .onChanged { value in + previewVideoOffset = CGSize(width: previewVideoOffsetPreviousDrag.width + value.translation.width, + height: previewVideoOffsetPreviousDrag.height + value.translation.height) + } + .onEnded { _ in + previewVideoOffsetPreviousDrag = previewVideoOffset + } + ) + + .offset(x: previewVideoOffset.width, y: previewVideoOffset.height) } .padding(10) .padding(.trailing, abs(angleDegree/2)) From 03f1ca2a4ac66605547413c90b61c71d1395331c Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 8 Nov 2024 16:33:15 +0100 Subject: [PATCH 04/18] Make canceled meetings unclicable in meetings list --- Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift b/Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift index 4c8f55955..7ee93ec3d 100644 --- a/Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/MeetingsFragment.swift @@ -85,7 +85,7 @@ struct MeetingsFragment: View { .shadow(color: .black.opacity(0.2), radius: 4) .onTapGesture { withAnimation { - if let meetingModel = model.model { + if let meetingModel = model.model, meetingModel.confInfo.state != ConferenceInfo.State.Cancelled { meetingViewModel.loadExistingMeeting(meeting: meetingModel) } } From 70405b0f1be8daf87c2eb3c0d7af8648356a4763 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 6 Nov 2024 13:34:47 +0100 Subject: [PATCH 05/18] Fix updating of chat room list --- .../Model/ConversationModel.swift | 5 ++++- .../ViewModel/ConversationViewModel.swift | 14 ++++++++++++ .../ConversationsListViewModel.swift | 22 +++++-------------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift index 1f3c2ffd3..6e681a91f 100644 --- a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift +++ b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift @@ -306,7 +306,10 @@ class ConversationModel: ObservableObject, Identifiable { func getUnreadMessagesCount() { coreContext.doOnCoreQueue { _ in - self.unreadMessagesCount = self.chatRoom.unreadMessagesCount + let unreadMessagesCountTmp = self.chatRoom.unreadMessagesCount + DispatchQueue.main.async { + self.unreadMessagesCount = unreadMessagesCountTmp + } } } diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 6723d3b4f..055e358d6 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -126,6 +126,20 @@ class ConversationViewModel: ObservableObject { self.getNewMessages(eventLogs: [eventLogs]) }, onSubjectChanged: { (_: ChatRoom, eventLogs: EventLog) in self.getNewMessages(eventLogs: [eventLogs]) + }, onConferenceJoined: {(_: ChatRoom, eventLog: EventLog) in + self.getNewMessages(eventLogs: [eventLog]) + if self.displayedConversation != nil { + DispatchQueue.main.async { + self.displayedConversation!.isReadOnly = false + } + } + }, onConferenceLeft: {(_: ChatRoom, eventLog: EventLog) in + self.getNewMessages(eventLogs: [eventLog]) + if self.displayedConversation != nil { + DispatchQueue.main.async { + self.displayedConversation!.isReadOnly = true + } + } }, onEphemeralEvent: {(_: ChatRoom, eventLogs: EventLog) in self.getNewMessages(eventLogs: [eventLogs]) }, onEphemeralMessageDeleted: {(_: ChatRoom, eventLog: EventLog) in diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift index bab467a99..21fc88826 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -68,7 +68,6 @@ class ConversationsListViewModel: ObservableObject { coreContext.doOnCoreQueue { core in let account = core.defaultAccount let chatRoomsCounter = account?.chatRooms != nil ? account!.chatRooms.count : core.chatRooms.count - var counter = 0 self.coreConversationDelegate = CoreDelegateStub(onMessagesReceived: { (_: Core, _: ChatRoom, _: [ChatMessage]) in self.computeChatRoomsList(filter: "") @@ -76,24 +75,15 @@ class ConversationsListViewModel: ObservableObject { self.computeChatRoomsList(filter: "") }, onChatRoomRead: { (_: Core, _: ChatRoom) in self.computeChatRoomsList(filter: "") - }, onChatRoomStateChanged: { (_: Core, chatRoom: ChatRoom, state: ChatRoom.State) in + }, onChatRoomStateChanged: { (core: Core, chatRoom: ChatRoom, state: ChatRoom.State) in // Log.info("[ConversationsListViewModel] Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]") - switch state { - case ChatRoom.State.Created: - if !(chatRoom.isEmpty && chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue)) { - counter += 1 - } - - if counter >= chatRoomsCounter { + if core.globalState == .On { + switch state { + case .Created, .Deleted, .Terminated: self.computeChatRoomsList(filter: "") - counter = 0 + default: + break } - case ChatRoom.State.Deleted: - self.computeChatRoomsList(filter: "") - // ToastViewModel.shared.toastMessage = "toast_conversation_deleted" - // ToastViewModel.shared.displayToast = true - default: - break } }) core.addDelegate(delegate: self.coreConversationDelegate!) From 75588af0e28deeb225358e1ac01c9b917e1a0ef6 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 6 Nov 2024 15:57:44 +0100 Subject: [PATCH 06/18] Add nil check for self.displayedConversation in ComputeComposingLabel --- .../ViewModel/ConversationViewModel.swift | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 055e358d6..4b71fad8d 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -1862,48 +1862,50 @@ class ConversationViewModel: ObservableObject { } func computeComposingLabel() { - let composing = self.displayedConversation!.chatRoom.isRemoteComposing - - if !composing { - DispatchQueue.main.async { - withAnimation { - self.composingLabel = "" + if self.displayedConversation != nil { + let composing = self.displayedConversation!.chatRoom.isRemoteComposing + + if !composing { + DispatchQueue.main.async { + withAnimation { + self.composingLabel = "" + } + } + return + } + + var composingFriends: [String] = [] + var label = "" + + for address in self.displayedConversation!.chatRoom.composingAddresses { + if let addressCleaned = address.clone() { + addressCleaned.clean() + + if let avatar = self.participantConversationModel.first(where: {$0.address == addressCleaned.asStringUriOnly()}) { + let name = avatar.name + composingFriends.append(name) + label += "\(name), " + } } } - return - } - - var composingFriends: [String] = [] - var label = "" - - for address in self.displayedConversation!.chatRoom.composingAddresses { - if let addressCleaned = address.clone() { - addressCleaned.clean() + + if !composingFriends.isEmpty { + label = String(label.dropLast(2)) - if let avatar = self.participantConversationModel.first(where: {$0.address == addressCleaned.asStringUriOnly()}) { - let name = avatar.name - composingFriends.append(name) - label += "\(name), " + let format = composingFriends.count > 1 + ? String(format: NSLocalizedString("conversation_composing_label_multiple", comment: ""), label) + : String(format: NSLocalizedString("conversation_composing_label_single", comment: ""), label) + + DispatchQueue.main.async { + withAnimation { + self.composingLabel = format + } } - } - } - - if !composingFriends.isEmpty { - label = String(label.dropLast(2)) - - let format = composingFriends.count > 1 - ? String(format: NSLocalizedString("conversation_composing_label_multiple", comment: ""), label) - : String(format: NSLocalizedString("conversation_composing_label_single", comment: ""), label) - - DispatchQueue.main.async { - withAnimation { - self.composingLabel = format - } - } - } else { - DispatchQueue.main.async { - withAnimation { - self.composingLabel = "" + } else { + DispatchQueue.main.async { + withAnimation { + self.composingLabel = "" + } } } } From a839c7d6434ecc1755b5485e5acf9d9008edd40a Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 7 Nov 2024 18:09:23 +0100 Subject: [PATCH 07/18] Add participant list in conversation info fragment --- Linphone/Localizable.xcstrings | 68 +++++++ Linphone/UI/Call/CallView.swift | 17 +- .../Contacts/Model/ContactAvatarModel.swift | 2 +- Linphone/UI/Main/ContentView.swift | 12 +- .../Fragments/ConversationFragment.swift | 11 +- .../Fragments/ConversationInfoFragment.swift | 189 ++++++++++++++++-- .../ViewModel/ConversationViewModel.swift | 34 +++- .../Fragments/HistoryContactFragment.swift | 1 - 8 files changed, 304 insertions(+), 30 deletions(-) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 5490add1f..4267eeb33 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -1466,6 +1466,23 @@ } } }, + "conversation_info_add_participants_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add participants" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter des membres" + } + } + } + }, "conversation_info_confirm_start_group_call_dialog_message" : { "extractionState" : "manual", "localizations" : { @@ -1517,6 +1534,23 @@ } } }, + "conversation_info_menu_add_to_contacts" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add to contacts" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter aux contacts" + } + } + } + }, "conversation_info_menu_go_to_contact" : { "extractionState" : "manual", "localizations" : { @@ -1534,6 +1568,40 @@ } } }, + "conversation_info_participant_is_admin_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Admin" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrateur" + } + } + } + }, + "conversation_info_participants_list_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Group members" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Membres du groupe" + } + } + } + }, "conversation_invalid_participant_due_to_security_mode_toast" : { "extractionState" : "manual", "localizations" : { diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index bc90b7ace..52dac2093 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -37,6 +37,8 @@ struct CallView: View { @ObservedObject var conversationViewModel: ConversationViewModel @ObservedObject var conversationsListViewModel: ConversationsListViewModel @ObservedObject var conversationForwardMessageViewModel: ConversationForwardMessageViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel @State private var addParticipantsViewModel: AddParticipantsViewModel? @@ -71,6 +73,9 @@ struct CallView: View { @State var buttonSize = 60.0 + @Binding var isShowEditContactFragment: Bool + @Binding var indexPage: Int + var body: some View { GeometryReader { geo in ZStack { @@ -200,8 +205,12 @@ struct CallView: View { conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, conversationForwardMessageViewModel: conversationForwardMessageViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, isShowConversationFragment: $isShowConversationFragment, - isShowStartCallGroupPopup: $isShowStartCallGroupPopup + isShowStartCallGroupPopup: $isShowStartCallGroupPopup, + isShowEditContactFragment: $isShowEditContactFragment, + indexPage: $indexPage ) .frame(maxWidth: .infinity) .background(Color.gray100) @@ -2825,10 +2834,14 @@ struct PressedButtonStyle: ButtonStyle { conversationViewModel: ConversationViewModel(), conversationsListViewModel: ConversationsListViewModel(), conversationForwardMessageViewModel: ConversationForwardMessageViewModel(), + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), fullscreenVideo: .constant(false), isShowStartCallFragment: .constant(false), isShowConversationFragment: .constant(false), - isShowStartCallGroupPopup: .constant(false) + isShowStartCallGroupPopup: .constant(false), + isShowEditContactFragment: .constant(false), + indexPage: .constant(0) ) } // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift b/Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift index 4938ae238..818083113 100644 --- a/Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift +++ b/Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift @@ -21,7 +21,7 @@ import Foundation import linphonesw import Combine -class ContactAvatarModel: ObservableObject { +class ContactAvatarModel: ObservableObject, Identifiable { let friend: Friend? diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 64c4dd3b7..e460256e7 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -862,8 +862,12 @@ struct ContentView: View { conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, conversationForwardMessageViewModel: conversationForwardMessageViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, isShowConversationFragment: $isShowConversationFragment, - isShowStartCallGroupPopup: $isShowStartCallGroupPopup + isShowStartCallGroupPopup: $isShowStartCallGroupPopup, + isShowEditContactFragment: $isShowEditContactFragment, + indexPage: $index ) .frame(maxWidth: .infinity) .background(Color.gray100) @@ -1200,10 +1204,14 @@ struct ContentView: View { conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, conversationForwardMessageViewModel: conversationForwardMessageViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, fullscreenVideo: $fullscreenVideo, isShowStartCallFragment: $isShowStartCallFragment, isShowConversationFragment: $isShowConversationFragment, - isShowStartCallGroupPopup: $isShowStartCallGroupPopup + isShowStartCallGroupPopup: $isShowStartCallGroupPopup, + isShowEditContactFragment: $isShowEditContactFragment, + indexPage: $index ) .zIndex(5) .transition(.scale.combined(with: .move(edge: .top))) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 611b9a48c..cae7ab69d 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -33,6 +33,8 @@ struct ConversationFragment: View { @ObservedObject var conversationViewModel: ConversationViewModel @ObservedObject var conversationsListViewModel: ConversationsListViewModel @ObservedObject var conversationForwardMessageViewModel: ConversationForwardMessageViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel @State var isMenuOpen = false @State private var isMuted: Bool = false @@ -63,6 +65,9 @@ struct ConversationFragment: View { @State private var selectedCategoryIndex = 0 + @Binding var isShowEditContactFragment: Bool + @Binding var indexPage: Int + var body: some View { NavigationView { GeometryReader { geometry in @@ -977,10 +982,14 @@ struct ConversationFragment: View { ConversationInfoFragment( conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, isMuted: $isMuted, isShowEphemeralFragment: $isShowEphemeralFragment, isShowStartCallGroupPopup: $isShowStartCallGroupPopup, - isShowInfoConversationFragment: $isShowInfoConversationFragment + isShowInfoConversationFragment: $isShowInfoConversationFragment, + isShowEditContactFragment: $isShowEditContactFragment, + indexPage: $indexPage ) .zIndex(5) .transition(.move(edge: .trailing)) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift index 28b092c53..8975ade93 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift @@ -22,15 +22,22 @@ import SwiftUI struct ConversationInfoFragment: View { @State private var orientation = UIDevice.current.orientation + @ObservedObject var contactsManager = ContactsManager.shared @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject var conversationViewModel: ConversationViewModel @ObservedObject var conversationsListViewModel: ConversationsListViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel @Binding var isMuted: Bool @Binding var isShowEphemeralFragment: Bool @Binding var isShowStartCallGroupPopup: Bool @Binding var isShowInfoConversationFragment: Bool + @Binding var isShowEditContactFragment: Bool + @Binding var indexPage: Int + + @State private var participantListIsOpen = true var body: some View { NavigationView { @@ -87,7 +94,7 @@ struct ConversationInfoFragment: View { .frame(maxWidth: .infinity) .padding(.top, 10) - Text(conversationViewModel.displayedConversation!.avatarModel.address) + Text(conversationViewModel.participantConversationModel.first?.address ?? "") .foregroundStyle(Color.grayMain2c700) .multilineTextAlignment(.center) .default_text_style(styleSize: 14) @@ -115,12 +122,29 @@ struct ConversationInfoFragment: View { Avatar(contactAvatarModel: conversationViewModel.displayedConversation!.avatarModel, avatarSize: 100) .padding(.top, 4) - Text(conversationViewModel.displayedConversation!.avatarModel.name) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 10) + HStack { + Text(conversationViewModel.displayedConversation!.avatarModel.name) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .padding(.top, 10) + + if conversationViewModel.isUserAdmin { + Button( + action: { + }, + label: { + Image("pencil-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 20, height: 20) + } + ) + .padding(.top, 10) + } + } + .padding(.leading, conversationViewModel.isUserAdmin ? 20 : 0) } } .frame(minHeight: 150) @@ -219,6 +243,91 @@ struct ConversationInfoFragment: View { .background(Color.gray100) } + if conversationViewModel.displayedConversation!.isGroup { + HStack(alignment: .center) { + Text("conversation_info_participants_list_title") + .default_text_style_800(styleSize: 18) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image(participantListIsOpen ? "caret-up" : "caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + .padding(.top, 30) + .padding(.bottom, 10) + .padding(.horizontal, 20) + .background(Color.gray100) + .onTapGesture { + withAnimation { + participantListIsOpen.toggle() + } + } + + if participantListIsOpen { + VStack(spacing: 0) { + ForEach(conversationViewModel.participantConversationModel) { participantConversationModel in + HStack { + Avatar(contactAvatarModel: participantConversationModel, avatarSize: 50) + + VStack { + Text(participantConversationModel.name) + .foregroundStyle(Color.grayMain2c700) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + + if conversationViewModel.participantConversationModelAdmin != nil && participantConversationModel.address == conversationViewModel.participantConversationModelAdmin!.address { + Text("conversation_info_participant_is_admin_label") + .foregroundStyle(Color.grayMain2c400) + .default_text_style(styleSize: 12) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + } + } + .padding(.vertical, 15) + .padding(.horizontal, 20) + } + + if conversationViewModel.isUserAdmin { + Button( + action: { + }, + 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) + } + } + .background(.white) + .cornerRadius(15) + .padding(.horizontal) + .zIndex(-1) + .transition(.move(edge: .top)) + } + } + Text("contact_details_actions_title") .default_text_style_800(styleSize: 18) .frame(maxWidth: .infinity, alignment: .leading) @@ -230,20 +339,60 @@ struct ConversationInfoFragment: View { if !conversationViewModel.displayedConversation!.isGroup { Button( action: { + if conversationViewModel.displayedConversation != nil { + + let addressConv = conversationViewModel.participantConversationModel.first?.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(conversationViewModel.participantConversationModel.first?.address.dropFirst(4) ?? "")) + editContactViewModel.sipAddresses.append("") + } + } + } }, label: { HStack { - 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) + let addressConv = conversationViewModel.participantConversationModel.first?.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) + } } } ) @@ -361,9 +510,13 @@ struct ConversationInfoFragment: View { ConversationInfoFragment( conversationViewModel: ConversationViewModel(), conversationsListViewModel: ConversationsListViewModel(), + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), isMuted: .constant(false), isShowEphemeralFragment: .constant(false), isShowStartCallGroupPopup: .constant(false), - isShowInfoConversationFragment: .constant(true) + isShowInfoConversationFragment: .constant(true), + isShowEditContactFragment: .constant(false), + indexPage: .constant(0) ) } diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 4b71fad8d..6759c6331 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -76,6 +76,8 @@ class ConversationViewModel: ObservableObject { @Published var conversationMessagesSection: [MessagesSection] = [] @Published var participantConversationModel: [ContactAvatarModel] = [] + @Published var participantConversationModelAdmin: ContactAvatarModel? + @Published var isUserAdmin: Bool = false @Published var mediasToSend: [Attachment] = [] var maxMediaCount = 12 @@ -310,12 +312,24 @@ class ConversationViewModel: ObservableObject { func getParticipantConversationModel() { coreContext.doOnCoreQueue { _ in if self.displayedConversation != nil { + DispatchQueue.main.async { + self.isUserAdmin = false + self.participantConversationModelAdmin = nil + self.participantConversationModel.removeAll() + } self.displayedConversation!.chatRoom.participants.forEach { participant in if participant.address != nil { ContactAvatarModel.getAvatarModelFromAddress(address: participant.address!) { avatarResult in let avatarModelTmp = avatarResult - DispatchQueue.main.async { - self.participantConversationModel.append(avatarModelTmp) + if participant.isAdmin { + DispatchQueue.main.async { + self.participantConversationModelAdmin = avatarModelTmp + self.participantConversationModel.append(avatarModelTmp) + } + } else { + DispatchQueue.main.async { + self.participantConversationModel.append(avatarModelTmp) + } } } } @@ -324,8 +338,16 @@ class ConversationViewModel: ObservableObject { if self.displayedConversation!.chatRoom.me != nil { ContactAvatarModel.getAvatarModelFromAddress(address: self.displayedConversation!.chatRoom.me!.address!) { avatarResult in let avatarModelTmp = avatarResult - DispatchQueue.main.async { - self.participantConversationModel.append(avatarModelTmp) + if self.displayedConversation!.chatRoom.me!.isAdmin { + DispatchQueue.main.async { + self.isUserAdmin = true + self.participantConversationModelAdmin = avatarModelTmp + self.participantConversationModel.append(avatarModelTmp) + } + } else { + DispatchQueue.main.async { + self.participantConversationModel.append(avatarModelTmp) + } } } } @@ -338,7 +360,9 @@ class ConversationViewModel: ObservableObject { ContactAvatarModel.getAvatarModelFromAddress(address: address) { avatarResult in let avatarModelTmp = avatarResult DispatchQueue.main.async { - self.participantConversationModel.append(avatarModelTmp) + if self.participantConversationModel.first(where: {$0.address == avatarModelTmp.address}) == nil { + self.participantConversationModel.append(avatarModelTmp) + } } } } diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index ab49963ef..2356af002 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -86,7 +86,6 @@ struct HistoryContactFragment: View { let friendIndex = contactsManager.lastSearch.firstIndex( where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall!.asStringUriOnly()})}) if friendIndex != nil { - withAnimation { historyViewModel.displayedCall = nil indexPage = 0 From bff25fc3f228a7ffc9fb5bda71335ea253a7dfea Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 12 Nov 2024 15:08:39 +0100 Subject: [PATCH 08/18] Allow admins to update conversation subject --- Linphone.xcodeproj/project.pbxproj | 24 +++-- Linphone/Localizable.xcstrings | 17 ++++ Linphone/UI/Main/ContentView.swift | 33 +++++++ .../Fragments/ConversationInfoFragment.swift | 1 + .../ViewModel/ConversationViewModel.swift | 23 +++++ .../Fragments/PopupViewWithTextField.swift | 87 +++++++++++++++++++ 6 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 Linphone/UI/Main/Fragments/PopupViewWithTextField.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index d837e61ab..0bf43c8c5 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -162,6 +162,7 @@ D7D24D182AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D122AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf */; }; D7DA67622ACCB2FA00E95002 /* LoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */; }; D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */; }; + D7E2E69F2CE356C90080DA0D /* PopupViewWithTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */; }; D7E6ADF32B9875C20009A2BC /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF22B9875C20009A2BC /* Message.swift */; }; D7E6ADF52B9876ED0009A2BC /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF42B9876ED0009A2BC /* Attachment.swift */; }; D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */; }; @@ -353,6 +354,7 @@ D7D24D122AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-ExtraBold.ttf"; sourceTree = ""; }; D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFragment.swift; sourceTree = ""; }; D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModeFragment.swift; sourceTree = ""; }; + D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupViewWithTextField.swift; sourceTree = ""; }; D7E6ADF22B9875C20009A2BC /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; D7E6ADF42B9876ED0009A2BC /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListFragment.swift; sourceTree = ""; }; @@ -687,6 +689,7 @@ C6DC4E3E2C19C289009096FD /* SideMenuEntry.swift */, C62817272C1B389700DBA646 /* SideMenuAccountRow.swift */, D7E6D04C2AEBD77600A57AAF /* CustomBottomSheet.swift */, + D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */, ); path = Fragments; sourceTree = ""; @@ -1136,6 +1139,7 @@ C62817282C1B389700DBA646 /* SideMenuAccountRow.swift in Sources */, C60E8F192C0F649200A06DB8 /* UIApplicationExtension.swift in Sources */, D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */, + D7E2E69F2CE356C90080DA0D /* PopupViewWithTextField.swift in Sources */, C67586B52C09F617002E77BF /* SingleSignOnManager.swift in Sources */, D7F4D9CB2B5FD27200CDCD76 /* CallsListFragment.swift in Sources */, C6A5A9482C10B6A30070FEA4 /* AuthState.swift in Sources */, @@ -1265,7 +1269,6 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", - "USE_CRASHLYTICS=1", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = msgNotificationService/Info.plist; @@ -1279,7 +1282,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 6.0.0; - OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.msgNotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1305,10 +1308,7 @@ DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_PREPROCESSOR_DEFINITIONS = ( - "$(inherited)", - "USE_CRASHLYTICS=1", - ); + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = msgNotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = msgNotificationService; @@ -1321,7 +1321,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 6.0.0; - OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.msgNotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1468,7 +1468,6 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", - "USE_CRASHLYTICS=1", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; @@ -1495,7 +1494,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; MARKETING_VERSION = 6.0.0; - OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1521,10 +1520,7 @@ DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; - GCC_PREPROCESSOR_DEFINITIONS = ( - "$(inherited)", - "USE_CRASHLYTICS=1", - ); + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls"; @@ -1550,7 +1546,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; MARKETING_VERSION = 6.0.0; - OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 4267eeb33..a7ee0c03c 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -1064,6 +1064,23 @@ } } }, + "conversation_dialog_edit_subject" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit conversation subject" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renommer la conversation" + } + } + } + }, "conversation_dialog_set_subject" : { "extractionState" : "manual", "localizations" : { diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index e460256e7..dd5b34494 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -1189,6 +1189,39 @@ struct ContentView: View { } } + if isShowStartCallGroupPopup { + PopupView( + isShowPopup: $isShowStartCallGroupPopup, + title: Text("conversation_info_confirm_start_group_call_dialog_title"), + content: Text("conversation_info_confirm_start_group_call_dialog_message"), + titleFirstButton: Text("Cancel"), + actionFirstButton: { + self.isShowStartCallGroupPopup.toggle() + }, + titleSecondButton: Text("Confirm"), + actionSecondButton: { + if conversationViewModel.displayedConversation != nil { + conversationViewModel.displayedConversation!.createGroupCall() + } + self.isShowStartCallGroupPopup.toggle() + } + ) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + self.isShowStartCallGroupPopup.toggle() + } + } + + if conversationViewModel.isShowConversationInfoPopup { + PopupViewWithTextField(conversationViewModel: conversationViewModel) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + conversationViewModel.isShowConversationInfoPopup = false + } + } + if telecomManager.meetingWaitingRoomDisplayed { MeetingWaitingRoomFragment(meetingWaitingRoomViewModel: meetingWaitingRoomViewModel) .zIndex(3) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift index 8975ade93..ba5a3a4c9 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift @@ -132,6 +132,7 @@ struct ConversationInfoFragment: View { if conversationViewModel.isUserAdmin { Button( action: { + conversationViewModel.isShowConversationInfoPopup = true }, label: { Image("pencil-simple") diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 6759c6331..ed9c1bea9 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -41,6 +41,9 @@ class ConversationViewModel: ObservableObject { @Published var isEphemeral: Bool = false @Published var ephemeralTime: String = NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") + @Published var isShowConversationInfoPopup: Bool = false + @Published var conversationInfoPopupText: String = "" + // Used to keep track of a ChatRoom callback without having to worry about life cycle // Init will add the delegate, deinit will remove it class ChatRoomDelegateHolder { @@ -378,6 +381,8 @@ class ConversationViewModel: ObservableObject { self.mediasToSend.removeAll() self.messageToReply = nil + self.conversationInfoPopupText = displayedConversation?.subject ?? "" + coreContext.doOnCoreQueue { _ in if self.displayedConversation != nil { let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: 0, end: 30) @@ -2071,6 +2076,24 @@ 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 { diff --git a/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift b/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift new file mode 100644 index 000000000..c2495bec0 --- /dev/null +++ b/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift @@ -0,0 +1,87 @@ +// +// PopupViewWithTextField.swift +// Linphone +// +// Created by Benoît Martins on 12/11/2024. +// + +import SwiftUI + +struct PopupViewWithTextField: View { + + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + + @ObservedObject var conversationViewModel: ConversationViewModel + + @FocusState var isMessageTextFocused: Bool + + var body: some View { + GeometryReader { geometry in + VStack(alignment: .leading) { + Text("conversation_dialog_edit_subject") + .default_text_style_800(styleSize: 16) + .frame(alignment: .leading) + .padding(.bottom, 2) + + TextField("conversation_dialog_subject_hint", text: $conversationViewModel.conversationInfoPopupText) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isMessageTextFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .padding(.bottom) + .focused($isMessageTextFocused) + + Button(action: { + conversationViewModel.isShowConversationInfoPopup = false + }, label: { + Text("Cancel") + .default_text_style_orange_600(styleSize: 20) + .frame(height: 35) + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(Color.orangeMain500, lineWidth: 1) + ) + .padding(.bottom, 10) + + Button(action: { + conversationViewModel.setNewChatRoomSubject() + }, label: { + Text("Confirm") + .default_text_style_white_600(styleSize: 20) + .frame(height: 35) + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(conversationViewModel.conversationInfoPopupText.isEmpty ? Color.orangeMain100 : Color.orangeMain500) + .cornerRadius(60) + .disabled(conversationViewModel.conversationInfoPopupText.isEmpty) + } + .padding(.horizontal, 20) + .padding(.vertical, 20) + .background(.white) + .cornerRadius(20) + .padding(.horizontal) + .frame(maxHeight: .infinity) + .shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2) + .frame(maxWidth: sharedMainViewModel.maxWidth) + .position(x: geometry.size.width / 2, y: geometry.size.height / 2) + } + } +} + +#Preview { + PopupViewWithTextField(conversationViewModel: ConversationViewModel()) +} From d4b6fe6d8edeeac04fc03838413a54061206d089 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 12 Nov 2024 18:29:10 +0100 Subject: [PATCH 09/18] 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 From ecea020da3348b1f7f2ab85399d1bdf9c2e1ec03 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 14 Nov 2024 10:15:57 +0100 Subject: [PATCH 10/18] Enable meeting creation from a conversation with auto-filled details --- Linphone/UI/Call/CallView.swift | 11 ++++++++-- Linphone/UI/Main/ContentView.swift | 8 ++++++-- .../Fragments/ConversationFragment.swift | 7 ++++++- .../Fragments/ConversationInfoFragment.swift | 20 +++++++++++++++++-- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 52dac2093..caf4be1fe 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -39,6 +39,7 @@ struct CallView: View { @ObservedObject var conversationForwardMessageViewModel: ConversationForwardMessageViewModel @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var meetingViewModel: MeetingViewModel @State private var addParticipantsViewModel: AddParticipantsViewModel? @@ -76,6 +77,8 @@ struct CallView: View { @Binding var isShowEditContactFragment: Bool @Binding var indexPage: Int + @Binding var isShowScheduleMeetingFragment: Bool + var body: some View { GeometryReader { geo in ZStack { @@ -207,10 +210,12 @@ struct CallView: View { conversationForwardMessageViewModel: conversationForwardMessageViewModel, contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, + meetingViewModel: meetingViewModel, isShowConversationFragment: $isShowConversationFragment, isShowStartCallGroupPopup: $isShowStartCallGroupPopup, isShowEditContactFragment: $isShowEditContactFragment, - indexPage: $indexPage + indexPage: $indexPage, + isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment ) .frame(maxWidth: .infinity) .background(Color.gray100) @@ -2836,12 +2841,14 @@ struct PressedButtonStyle: ButtonStyle { conversationForwardMessageViewModel: ConversationForwardMessageViewModel(), contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), + meetingViewModel: MeetingViewModel(), fullscreenVideo: .constant(false), isShowStartCallFragment: .constant(false), isShowConversationFragment: .constant(false), isShowStartCallGroupPopup: .constant(false), isShowEditContactFragment: .constant(false), - indexPage: .constant(0) + indexPage: .constant(0), + isShowScheduleMeetingFragment: .constant(false) ) } // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index dd5b34494..6dd48d923 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -864,10 +864,12 @@ struct ContentView: View { conversationForwardMessageViewModel: conversationForwardMessageViewModel, contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, + meetingViewModel: meetingViewModel, isShowConversationFragment: $isShowConversationFragment, isShowStartCallGroupPopup: $isShowStartCallGroupPopup, isShowEditContactFragment: $isShowEditContactFragment, - indexPage: $index + indexPage: $index, + isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment ) .frame(maxWidth: .infinity) .background(Color.gray100) @@ -1239,12 +1241,14 @@ struct ContentView: View { conversationForwardMessageViewModel: conversationForwardMessageViewModel, contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, + meetingViewModel: meetingViewModel, fullscreenVideo: $fullscreenVideo, isShowStartCallFragment: $isShowStartCallFragment, isShowConversationFragment: $isShowConversationFragment, isShowStartCallGroupPopup: $isShowStartCallGroupPopup, isShowEditContactFragment: $isShowEditContactFragment, - indexPage: $index + indexPage: $index, + isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment ) .zIndex(5) .transition(.scale.combined(with: .move(edge: .top))) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index cae7ab69d..8b475f0a4 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -35,6 +35,7 @@ struct ConversationFragment: View { @ObservedObject var conversationForwardMessageViewModel: ConversationForwardMessageViewModel @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var meetingViewModel: MeetingViewModel @State var isMenuOpen = false @State private var isMuted: Bool = false @@ -68,6 +69,8 @@ struct ConversationFragment: View { @Binding var isShowEditContactFragment: Bool @Binding var indexPage: Int + @Binding var isShowScheduleMeetingFragment: Bool + var body: some View { NavigationView { GeometryReader { geometry in @@ -984,12 +987,14 @@ struct ConversationFragment: View { conversationsListViewModel: conversationsListViewModel, contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, + meetingViewModel: meetingViewModel, isMuted: $isMuted, isShowEphemeralFragment: $isShowEphemeralFragment, isShowStartCallGroupPopup: $isShowStartCallGroupPopup, isShowInfoConversationFragment: $isShowInfoConversationFragment, isShowEditContactFragment: $isShowEditContactFragment, - indexPage: $indexPage + indexPage: $indexPage, + isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment ) .zIndex(5) .transition(.move(edge: .trailing)) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift index c5f5e7581..4e1127cfb 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationInfoFragment.swift @@ -30,6 +30,7 @@ struct ConversationInfoFragment: View { @ObservedObject var conversationsListViewModel: ConversationsListViewModel @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var meetingViewModel: MeetingViewModel @State var addParticipantsViewModel = AddParticipantsViewModel() @@ -40,6 +41,8 @@ struct ConversationInfoFragment: View { @Binding var isShowEditContactFragment: Bool @Binding var indexPage: Int + @Binding var isShowScheduleMeetingFragment: Bool + @State private var participantListIsOpen = true var body: some View { @@ -217,7 +220,15 @@ struct ConversationInfoFragment: View { Spacer() Button(action: { - // TODO Create Meeting + if conversationViewModel.displayedConversation != nil { + meetingViewModel.subject = conversationViewModel.displayedConversation!.subject + meetingViewModel.participants = conversationViewModel.participants + conversationViewModel.displayedConversation = nil + indexPage = 3 + withAnimation { + isShowScheduleMeetingFragment = true + } + } }, label: { VStack { HStack(alignment: .center) { @@ -636,6 +647,9 @@ struct ConversationInfoFragment: View { } .background(.white) .navigationBarHidden(true) + .onAppear { + conversationViewModel.getParticipants() + } .onRotate { newOrientation in orientation = newOrientation } @@ -652,13 +666,15 @@ struct ConversationInfoFragment: View { conversationsListViewModel: ConversationsListViewModel(), contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), + meetingViewModel: MeetingViewModel(), addParticipantsViewModel: AddParticipantsViewModel(), isMuted: .constant(false), isShowEphemeralFragment: .constant(false), isShowStartCallGroupPopup: .constant(false), isShowInfoConversationFragment: .constant(true), isShowEditContactFragment: .constant(false), - indexPage: .constant(0) + indexPage: .constant(0), + isShowScheduleMeetingFragment: .constant(false) ) } // swiftlint:enable type_body_length From 8491ce02ed001ae280124866cbd2fd6dc16d1db1 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 14 Nov 2024 17:44:24 +0100 Subject: [PATCH 11/18] Remove message refresh when conversation appears --- .../UI/Main/Conversations/Fragments/ConversationFragment.swift | 2 ++ Linphone/Utils/QuickLookPreview.swift | 0 2 files changed, 2 insertions(+) create mode 100644 Linphone/Utils/QuickLookPreview.swift diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 8b475f0a4..5719c50f6 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -357,9 +357,11 @@ struct ConversationFragment: View { sections: conversationViewModel.conversationMessagesSection ) } + /* .onAppear { conversationViewModel.getMessages() } + */ .onDisappear { conversationViewModel.resetMessage() } diff --git a/Linphone/Utils/QuickLookPreview.swift b/Linphone/Utils/QuickLookPreview.swift new file mode 100644 index 000000000..e69de29bb From bde4d0622eb55b1a1896991449c17be14dcfeca2 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 14 Nov 2024 17:46:25 +0100 Subject: [PATCH 12/18] Add file player --- Linphone.xcodeproj/project.pbxproj | 4 + .../Fragments/ChatBubbleView.swift | 36 +++++++- .../ViewModel/ConversationViewModel.swift | 40 +++++++++ Linphone/Utils/QuickLookPreview.swift | 83 +++++++++++++++++++ 4 files changed, 162 insertions(+), 1 deletion(-) diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 0bf43c8c5..b7b73212a 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -163,6 +163,7 @@ D7DA67622ACCB2FA00E95002 /* LoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */; }; D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */; }; D7E2E69F2CE356C90080DA0D /* PopupViewWithTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */; }; + D7E2E6A12CE5F8850080DA0D /* QuickLookPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */; }; D7E6ADF32B9875C20009A2BC /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF22B9875C20009A2BC /* Message.swift */; }; D7E6ADF52B9876ED0009A2BC /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF42B9876ED0009A2BC /* Attachment.swift */; }; D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */; }; @@ -355,6 +356,7 @@ D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFragment.swift; sourceTree = ""; }; D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModeFragment.swift; sourceTree = ""; }; D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupViewWithTextField.swift; sourceTree = ""; }; + D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreview.swift; sourceTree = ""; }; D7E6ADF22B9875C20009A2BC /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; D7E6ADF42B9876ED0009A2BC /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListFragment.swift; sourceTree = ""; }; @@ -517,6 +519,7 @@ C67586AF2C09F247002E77BF /* URIHandler.swift */, C6A5A9462C10B64A0070FEA4 /* SingleSignOn */, D79F2D092C47F4BF0038FA07 /* TouchFeedback.swift */, + D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */, ); path = Utils; sourceTree = ""; @@ -1216,6 +1219,7 @@ 66F08C892C2AFEF700D9AE2F /* MeetingsListBottomSheet.swift in Sources */, D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */, D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */, + D7E2E6A12CE5F8850080DA0D /* QuickLookPreview.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, D714DE602C1B3B34006C1F1D /* RegisterViewModel.swift in Sources */, D70C82A72C85F5910087F43F /* ConversationForwardMessageViewModel.swift in Sources */, diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index ab3ea8e76..920f71124 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -39,6 +39,9 @@ struct ChatBubbleView: View { @State private var timer: Timer? @State private var ephemeralLifetime: String = "" + @State private var selectedAttachment: Bool = false + @State private var selectedAttachmentIndex: Int = 0 + var body: some View { HStack { if eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage { @@ -488,6 +491,9 @@ struct ChatBubbleView: View { } UIApplication.shared.endEditing() } + .fullScreenCover(isPresented: $selectedAttachment) { + QuickLookFullScreenView(conversationViewModel: conversationViewModel, currentIndex: $selectedAttachmentIndex) + } } func containsDuplicates(strings: [String]) -> Bool { @@ -554,6 +560,10 @@ struct ChatBubbleView: View { } .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } else { AsyncImage(url: eventLogMessage.message.attachments.first!.thumbnail) { phase in switch phase { @@ -583,17 +593,29 @@ struct ChatBubbleView: View { .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) .id(UUID()) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } } else if eventLogMessage.message.attachments.first!.type == .gif { if #available(iOS 16.0, *) { GifImageView(eventLogMessage.message.attachments.first!.thumbnail) .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } else { GifImageView(eventLogMessage.message.attachments.first!.thumbnail) .id(UUID()) .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } } } @@ -636,13 +658,17 @@ struct ChatBubbleView: View { .frame(width: geometryProxy.size.width - 110, height: 100) .background(.white) .clipShape(RoundedRectangle(cornerRadius: 10)) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) + selectedAttachment.toggle() + } } } else if eventLogMessage.message.attachments.count > 1 { let isGroup = conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup LazyVGrid(columns: [ GridItem(.adaptive(minimum: 120), spacing: 1) ], spacing: 3) { - ForEach(eventLogMessage.message.attachments) { attachment in + ForEach(eventLogMessage.message.attachments, id: \.id) { attachment in ZStack { Rectangle() .fill(Color(.white)) @@ -668,6 +694,10 @@ struct ChatBubbleView: View { ProgressView() } .layoutPriority(-1) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: attachment) + selectedAttachment.toggle() + } } else { AsyncImage(url: attachment.thumbnail) { image in ZStack { @@ -689,6 +719,10 @@ struct ChatBubbleView: View { } .id(UUID()) .layoutPriority(-1) + .onTapGesture { + selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: attachment) + selectedAttachment.toggle() + } } } .clipShape(RoundedRectangle(cornerRadius: 4)) diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index b74b31390..69405473a 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -103,6 +103,8 @@ class ConversationViewModel: ObservableObject { @Published var isPlaying = false @Published var progress: Double = 0.0 + @Published var attachments: [Attachment] = [] + struct SheetCategory: Identifiable { let id = UUID() let name: String @@ -392,6 +394,8 @@ class ConversationViewModel: ObservableObject { self.conversationInfoPopupText = displayedConversation?.subject ?? "" + self.attachments.removeAll() + coreContext.doOnCoreQueue { _ in if self.displayedConversation != nil { let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: 0, end: 30) @@ -454,6 +458,11 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + self.attachments.append(attachment) + } + } } } else if content.type == "video" { let path = URL(string: self.getNewFilePath(name: content.name ?? "")) @@ -471,6 +480,9 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + DispatchQueue.main.async { + self.attachments.append(attachment) + } } } } @@ -679,6 +691,11 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + self.attachments.append(attachment) + } + } } } else if content.type == "video" { let path = URL(string: self.getNewFilePath(name: content.name ?? "")) @@ -696,6 +713,9 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + DispatchQueue.main.async { + self.attachments.append(attachment) + } } } } @@ -902,6 +922,11 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + self.attachments.append(attachment) + } + } } } else if content.type == "video" { let path = URL(string: self.getNewFilePath(name: content.name ?? "")) @@ -919,6 +944,9 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + DispatchQueue.main.async { + self.attachments.append(attachment) + } } } } @@ -1203,6 +1231,11 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + self.attachments.append(attachment) + } + } } } else if content.type == "video" { let path = URL(string: self.getNewFilePath(name: content.name ?? "")) @@ -1220,6 +1253,9 @@ class ConversationViewModel: ObservableObject { ) attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) + DispatchQueue.main.async { + self.attachments.append(attachment) + } } } } @@ -2220,6 +2256,10 @@ class ConversationViewModel: ObservableObject { } } } + + func getAttachmentIndex(attachment: Attachment) -> Int { + return self.attachments.firstIndex(where: {$0.id == attachment.id}) ?? 0 + } } // swiftlint:enable line_length // swiftlint:enable type_body_length diff --git a/Linphone/Utils/QuickLookPreview.swift b/Linphone/Utils/QuickLookPreview.swift index e69de29bb..d0c081b46 100644 --- a/Linphone/Utils/QuickLookPreview.swift +++ b/Linphone/Utils/QuickLookPreview.swift @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI +import QuickLook + +struct QuickLookFullScreenView: View { + @Environment(\.presentationMode) var presentationMode + + @ObservedObject var conversationViewModel: ConversationViewModel + + @Binding var currentIndex: Int + + var body: some View { + NavigationView { + QuickLookPreview(fileURLs: conversationViewModel.attachments.map { $0.full }, startIndex: currentIndex) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text(conversationViewModel.attachments.first?.name ?? "File error") + .lineLimit(1) + .truncationMode(.middle) + .font(.headline) + } + } + .navigationBarItems(trailing: Button("Close") { + presentationMode.wrappedValue.dismiss() + }) + } + } +} + +struct QuickLookPreview: UIViewControllerRepresentable { + let fileURLs: [URL] + let startIndex: Int + + func makeUIViewController(context: Context) -> QLPreviewController { + let previewController = QLPreviewController() + previewController.dataSource = context.coordinator + previewController.currentPreviewItemIndex = startIndex // Définir l’index de départ + return previewController + } + + func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) { + // No update needed + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, QLPreviewControllerDataSource { + var parent: QuickLookPreview + + init(_ parent: QuickLookPreview) { + self.parent = parent + } + + func numberOfPreviewItems(in controller: QLPreviewController) -> Int { + return parent.fileURLs.count + } + + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { + return parent.fileURLs[index] as QLPreviewItem + } + } +} From d1489d2287e7041727760f055d0a18d43654cfac Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 14 Nov 2024 18:09:27 +0100 Subject: [PATCH 13/18] Fix message bubble for text files --- .../UI/Main/Conversations/ViewModel/ConversationViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 69405473a..cc8ac2a67 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -409,7 +409,7 @@ class ConversationViewModel: ObservableObject { if eventLog.chatMessage != nil && !eventLog.chatMessage!.contents.isEmpty { eventLog.chatMessage!.contents.forEach { content in - if content.isText { + if content.isText && content.name == nil { contentText = content.utf8Text ?? "" } else if content.name != nil && !content.name!.isEmpty { if content.filePath == nil || content.filePath!.isEmpty { From 94e387dac2ba28a2b58a3717d8c2f1fcd6d159b6 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 15 Nov 2024 09:51:00 +0100 Subject: [PATCH 14/18] Message can be deleted when selected --- .../Fragments/ConversationFragment.swift | 1 + .../ViewModel/ConversationViewModel.swift | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 5719c50f6..5de44778d 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -923,6 +923,7 @@ struct ConversationFragment: View { Divider() Button { + conversationViewModel.deleteMessage() } label: { HStack { Text("menu_delete_selected_item") diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index cc8ac2a67..efb321009 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -2260,6 +2260,18 @@ class ConversationViewModel: ObservableObject { func getAttachmentIndex(attachment: Attachment) -> Int { return self.attachments.firstIndex(where: {$0.id == attachment.id}) ?? 0 } + + func deleteMessage() { + if self.displayedConversation != nil && selectedMessage != nil && selectedMessage!.eventModel.eventLog.chatMessage != nil { + coreContext.doOnCoreQueue { _ in + self.displayedConversation!.chatRoom.deleteMessage(message: self.selectedMessage!.eventModel.eventLog.chatMessage!) + DispatchQueue.main.async { + self.conversationMessagesSection[0].rows.remove(at: self.conversationMessagesSection[0].rows.firstIndex(of: self.selectedMessage!)!) + self.selectedMessage = nil + } + } + } + } } // swiftlint:enable line_length // swiftlint:enable type_body_length From 1c9210ceb2c7e9ec51877c02d48baef261085900 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 15 Nov 2024 16:13:42 +0100 Subject: [PATCH 15/18] Fix QuickLookPreview --- Linphone.xcodeproj/project.pbxproj | 8 -- .../Fragments/ChatBubbleView.swift | 35 +++---- Linphone/Utils/QuickLookPreview.swift | 83 ----------------- Linphone/Utils/ShareSheetController.swift | 91 ------------------- 4 files changed, 18 insertions(+), 199 deletions(-) delete mode 100644 Linphone/Utils/QuickLookPreview.swift delete mode 100644 Linphone/Utils/ShareSheetController.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index b7b73212a..604966c47 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -94,7 +94,6 @@ D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */; }; D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */; }; D72A9A052B9750A1000DC093 /* UIList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9A042B9750A1000DC093 /* UIList.swift */; }; - D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9082AFD235500DB42BA /* ShareSheetController.swift */; }; D732A90C2B0376F500DB42BA /* linphonerc-default in Resources */ = {isa = PBXBuildFile; fileRef = D732A90A2B0376F500DB42BA /* linphonerc-default */; }; D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */ = {isa = PBXBuildFile; fileRef = D732A90B2B0376F500DB42BA /* linphonerc-factory */; }; D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */; }; @@ -163,7 +162,6 @@ D7DA67622ACCB2FA00E95002 /* LoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */; }; D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */; }; D7E2E69F2CE356C90080DA0D /* PopupViewWithTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */; }; - D7E2E6A12CE5F8850080DA0D /* QuickLookPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */; }; D7E6ADF32B9875C20009A2BC /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF22B9875C20009A2BC /* Message.swift */; }; D7E6ADF52B9876ED0009A2BC /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF42B9876ED0009A2BC /* Attachment.swift */; }; D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */; }; @@ -286,7 +284,6 @@ D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallViewModel.swift; sourceTree = ""; }; D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = ""; }; D72A9A042B9750A1000DC093 /* UIList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIList.swift; sourceTree = ""; }; - D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = ""; }; D732A90A2B0376F500DB42BA /* linphonerc-default */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-default"; sourceTree = ""; }; D732A90B2B0376F500DB42BA /* linphonerc-factory */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-factory"; sourceTree = ""; }; D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryFragment.swift; sourceTree = ""; }; @@ -356,7 +353,6 @@ D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFragment.swift; sourceTree = ""; }; D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModeFragment.swift; sourceTree = ""; }; D7E2E69E2CE356C90080DA0D /* PopupViewWithTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupViewWithTextField.swift; sourceTree = ""; }; - D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreview.swift; sourceTree = ""; }; D7E6ADF22B9875C20009A2BC /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; D7E6ADF42B9876ED0009A2BC /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListFragment.swift; sourceTree = ""; }; @@ -513,13 +509,11 @@ D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */, D74C9D002ACB098C0021626A /* PermissionManager.swift */, D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */, - D732A9082AFD235500DB42BA /* ShareSheetController.swift */, D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */, D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */, C67586AF2C09F247002E77BF /* URIHandler.swift */, C6A5A9462C10B64A0070FEA4 /* SingleSignOn */, D79F2D092C47F4BF0038FA07 /* TouchFeedback.swift */, - D7E2E6A02CE5F87D0080DA0D /* QuickLookPreview.swift */, ); path = Utils; sourceTree = ""; @@ -1199,7 +1193,6 @@ D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */, 66E56BCE2BA9A1F8006CE56F /* MeetingModel.swift in Sources */, D7E6D0552AEBFCCE00A57AAF /* ContactsInnerFragment.swift in Sources */, - D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */, D72343362AD037AF009AA24E /* ToastView.swift in Sources */, D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */, D7E6ADF32B9875C20009A2BC /* Message.swift in Sources */, @@ -1219,7 +1212,6 @@ 66F08C892C2AFEF700D9AE2F /* MeetingsListBottomSheet.swift in Sources */, D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */, D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */, - D7E2E6A12CE5F8850080DA0D /* QuickLookPreview.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, D714DE602C1B3B34006C1F1D /* RegisterViewModel.swift in Sources */, D70C82A72C85F5910087F43F /* ConversationForwardMessageViewModel.swift in Sources */, diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index 920f71124..e4ccc2acf 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -19,11 +19,14 @@ import SwiftUI import WebKit +import QuickLook // swiftlint:disable type_body_length // swiftlint:disable cyclomatic_complexity struct ChatBubbleView: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject var conversationViewModel: ConversationViewModel @@ -42,6 +45,10 @@ struct ChatBubbleView: View { @State private var selectedAttachment: Bool = false @State private var selectedAttachmentIndex: Int = 0 + @State private var selectedURLAttachment: URL? + + @State private var showShareSheet = false + var body: some View { HStack { if eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage { @@ -491,9 +498,7 @@ struct ChatBubbleView: View { } UIApplication.shared.endEditing() } - .fullScreenCover(isPresented: $selectedAttachment) { - QuickLookFullScreenView(conversationViewModel: conversationViewModel, currentIndex: $selectedAttachmentIndex) - } + .quickLookPreview($selectedURLAttachment, in: conversationViewModel.attachments.map { $0.full }) } func containsDuplicates(strings: [String]) -> Bool { @@ -561,8 +566,7 @@ struct ChatBubbleView: View { .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) .onTapGesture { - selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) - selectedAttachment.toggle() + selectedURLAttachment = eventLogMessage.message.attachments.first!.full } } else { AsyncImage(url: eventLogMessage.message.attachments.first!.thumbnail) { phase in @@ -594,8 +598,7 @@ struct ChatBubbleView: View { .clipShape(RoundedRectangle(cornerRadius: 4)) .id(UUID()) .onTapGesture { - selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) - selectedAttachment.toggle() + selectedURLAttachment = eventLogMessage.message.attachments.first!.full } } } else if eventLogMessage.message.attachments.first!.type == .gif { @@ -603,18 +606,18 @@ struct ChatBubbleView: View { GifImageView(eventLogMessage.message.attachments.first!.thumbnail) .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) + .contentShape(Rectangle()) .onTapGesture { - selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) - selectedAttachment.toggle() + selectedURLAttachment = eventLogMessage.message.attachments.first!.full } } else { GifImageView(eventLogMessage.message.attachments.first!.thumbnail) .id(UUID()) .layoutPriority(-1) .clipShape(RoundedRectangle(cornerRadius: 4)) + .contentShape(Rectangle()) .onTapGesture { - selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) - selectedAttachment.toggle() + selectedURLAttachment = eventLogMessage.message.attachments.first!.full } } } @@ -659,8 +662,7 @@ struct ChatBubbleView: View { .background(.white) .clipShape(RoundedRectangle(cornerRadius: 10)) .onTapGesture { - selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: eventLogMessage.message.attachments.first!) - selectedAttachment.toggle() + selectedURLAttachment = eventLogMessage.message.attachments.first!.full } } } else if eventLogMessage.message.attachments.count > 1 { @@ -695,8 +697,7 @@ struct ChatBubbleView: View { } .layoutPriority(-1) .onTapGesture { - selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: attachment) - selectedAttachment.toggle() + selectedURLAttachment = attachment.full } } else { AsyncImage(url: attachment.thumbnail) { image in @@ -720,8 +721,7 @@ struct ChatBubbleView: View { .id(UUID()) .layoutPriority(-1) .onTapGesture { - selectedAttachmentIndex = conversationViewModel.getAttachmentIndex(attachment: attachment) - selectedAttachment.toggle() + selectedURLAttachment = attachment.full } } } @@ -853,6 +853,7 @@ struct GifImageView: UIViewRepresentable { if data != nil { webview.load(data!, mimeType: "image/gif", characterEncodingName: "UTF-8", baseURL: url.deletingLastPathComponent()) webview.scrollView.isScrollEnabled = false + webview.isUserInteractionEnabled = false } return webview } diff --git a/Linphone/Utils/QuickLookPreview.swift b/Linphone/Utils/QuickLookPreview.swift deleted file mode 100644 index d0c081b46..000000000 --- a/Linphone/Utils/QuickLookPreview.swift +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2010-2023 Belledonne Communications SARL. - * - * This file is part of linphone-iphone - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import SwiftUI -import QuickLook - -struct QuickLookFullScreenView: View { - @Environment(\.presentationMode) var presentationMode - - @ObservedObject var conversationViewModel: ConversationViewModel - - @Binding var currentIndex: Int - - var body: some View { - NavigationView { - QuickLookPreview(fileURLs: conversationViewModel.attachments.map { $0.full }, startIndex: currentIndex) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text(conversationViewModel.attachments.first?.name ?? "File error") - .lineLimit(1) - .truncationMode(.middle) - .font(.headline) - } - } - .navigationBarItems(trailing: Button("Close") { - presentationMode.wrappedValue.dismiss() - }) - } - } -} - -struct QuickLookPreview: UIViewControllerRepresentable { - let fileURLs: [URL] - let startIndex: Int - - func makeUIViewController(context: Context) -> QLPreviewController { - let previewController = QLPreviewController() - previewController.dataSource = context.coordinator - previewController.currentPreviewItemIndex = startIndex // Définir l’index de départ - return previewController - } - - func updateUIViewController(_ uiViewController: QLPreviewController, context: Context) { - // No update needed - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, QLPreviewControllerDataSource { - var parent: QuickLookPreview - - init(_ parent: QuickLookPreview) { - self.parent = parent - } - - func numberOfPreviewItems(in controller: QLPreviewController) -> Int { - return parent.fileURLs.count - } - - func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { - return parent.fileURLs[index] as QLPreviewItem - } - } -} diff --git a/Linphone/Utils/ShareSheetController.swift b/Linphone/Utils/ShareSheetController.swift deleted file mode 100644 index 8a512713c..000000000 --- a/Linphone/Utils/ShareSheetController.swift +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2010-2023 Belledonne Communications SARL. - * - * This file is part of linphone-iphone - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import Foundation -import SwiftUI -import linphonesw - -struct ShareSheet: UIViewControllerRepresentable { - typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void - - let friendToShare: Friend - var activityItems: [Any] = [] - let applicationActivities: [UIActivity]? = nil - let excludedActivityTypes: [UIActivity.ActivityType]? = nil - let callback: Callback? = nil - - func makeUIViewController(context: Context) -> UIActivityViewController { - let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first - - if directoryURL != nil { - if friendToShare.name != nil { - let filename = friendToShare.name!.replacingOccurrences(of: " ", with: "") - - let fileURL = directoryURL! - .appendingPathComponent(filename) - .appendingPathExtension("vcf") - - if friendToShare.vcard != nil { - try? friendToShare.vcard!.asVcard4String().write(to: fileURL, atomically: false, encoding: String.Encoding.utf8) - - let controller = UIActivityViewController( - activityItems: [fileURL], - applicationActivities: applicationActivities - ) - controller.excludedActivityTypes = excludedActivityTypes - controller.completionWithItemsHandler = callback - return controller - } - } - } - - let controller = UIActivityViewController( - activityItems: activityItems, - applicationActivities: applicationActivities) - controller.excludedActivityTypes = excludedActivityTypes - controller.completionWithItemsHandler = callback - return controller - } - - func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { - // nothing to do here - } - - func shareContacts(friend: String) { - - let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first - - if directoryURL != nil { - let filename = NSUUID().uuidString - - let fileURL = directoryURL! - .appendingPathComponent(filename) - .appendingPathExtension("vcf") - - try? friend.write(to: fileURL, atomically: false, encoding: String.Encoding.utf8) - } - - /* - let activityViewController = UIActivityViewController( - activityItems: [fileURL], - applicationActivities: nil - ) - */ - } -} From 4b17ecb173126d4395f52d775f62095b253c6f75 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 18 Nov 2024 14:27:10 +0100 Subject: [PATCH 16/18] Revert ShareSheetController deletion --- Linphone.xcodeproj/project.pbxproj | 4 + Linphone/Utils/ShareSheetController.swift | 91 +++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 Linphone/Utils/ShareSheetController.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 604966c47..b894ff1be 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ D71707202AC5989C0037746F /* TextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071F2AC5989C0037746F /* TextExtension.swift */; }; D7173EBE2B7A5C0A00BCC481 /* LinphoneUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */; }; D717630D2BD7BD0E00464097 /* ParticipantsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717630C2BD7BD0E00464097 /* ParticipantsListFragment.swift */; }; + D717A10E2CEB772300849D92 /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717A10D2CEB770D00849D92 /* ShareSheetController.swift */; }; D71968922B86369D00DF4459 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71968912B86369D00DF4459 /* ChatBubbleView.swift */; }; D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */; }; D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABB82ABC67BF00B41C10 /* ContentView.swift */; }; @@ -258,6 +259,7 @@ D717071F2AC5989C0037746F /* TextExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextExtension.swift; sourceTree = ""; }; D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneUtils.swift; sourceTree = ""; }; D717630C2BD7BD0E00464097 /* ParticipantsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsListFragment.swift; sourceTree = ""; }; + D717A10D2CEB770D00849D92 /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = ""; }; D71968912B86369D00DF4459 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = ""; }; D719ABB32ABC67BF00B41C10 /* Linphone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Linphone.app; sourceTree = BUILT_PRODUCTS_DIR; }; D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneApp.swift; sourceTree = ""; }; @@ -500,6 +502,7 @@ D717071C2AC591EF0037746F /* Utils */ = { isa = PBXGroup; children = ( + D717A10D2CEB770D00849D92 /* ShareSheetController.swift */, 66C491F72B24D25A00CEA16D /* Extensions */, 66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */, D7ADF5FF2AFE356400212231 /* Avatar.swift */, @@ -1114,6 +1117,7 @@ D7C2DA1D2CA44DE400A2441B /* EventModel.swift in Sources */, D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */, D70A26F22B7F5D95006CC8FC /* ConversationFragment.swift in Sources */, + D717A10E2CEB772300849D92 /* ShareSheetController.swift in Sources */, 66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */, D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */, C6DC4E3D2C199C4E009096FD /* BundleExtenion.swift in Sources */, diff --git a/Linphone/Utils/ShareSheetController.swift b/Linphone/Utils/ShareSheetController.swift new file mode 100644 index 000000000..fded1c065 --- /dev/null +++ b/Linphone/Utils/ShareSheetController.swift @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import Foundation +import SwiftUI +import linphonesw + +struct ShareSheet: UIViewControllerRepresentable { + typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void + + let friendToShare: Friend + var activityItems: [Any] = [] + let applicationActivities: [UIActivity]? = nil + let excludedActivityTypes: [UIActivity.ActivityType]? = nil + let callback: Callback? = nil + + func makeUIViewController(context: Context) -> UIActivityViewController { + let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + + if directoryURL != nil { + if friendToShare.name != nil { + let filename = friendToShare.name!.replacingOccurrences(of: " ", with: "") + + let fileURL = directoryURL! + .appendingPathComponent(filename) + .appendingPathExtension("vcf") + + if friendToShare.vcard != nil { + try? friendToShare.vcard!.asVcard4String().write(to: fileURL, atomically: false, encoding: String.Encoding.utf8) + + let controller = UIActivityViewController( + activityItems: [fileURL], + applicationActivities: applicationActivities + ) + controller.excludedActivityTypes = excludedActivityTypes + controller.completionWithItemsHandler = callback + return controller + } + } + } + + let controller = UIActivityViewController( + activityItems: activityItems, + applicationActivities: applicationActivities) + controller.excludedActivityTypes = excludedActivityTypes + controller.completionWithItemsHandler = callback + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { + // nothing to do here + } + + func shareContacts(friend: String) { + + let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + + if directoryURL != nil { + let filename = NSUUID().uuidString + + let fileURL = directoryURL! + .appendingPathComponent(filename) + .appendingPathExtension("vcf") + + try? friend.write(to: fileURL, atomically: false, encoding: String.Encoding.utf8) + } + + /* + let activityViewController = UIActivityViewController( + activityItems: [fileURL], + applicationActivities: nil + ) + */ + } +} From d47cd3b06c3a0a872a16883334e32c8e09a4dc8e Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 19 Nov 2024 15:59:49 +0100 Subject: [PATCH 17/18] Fix content list in message bubble --- .../Fragments/ChatBubbleView.swift | 185 ++++++++++++------ .../UI/Main/Conversations/Model/Message.swift | 6 +- .../ViewModel/ConversationViewModel.swift | 45 +++-- 3 files changed, 157 insertions(+), 79 deletions(-) diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index e4ccc2acf..efaf4fe1d 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -641,6 +641,16 @@ struct ChatBubbleView: View { } .frame(width: 100, height: 100) .background(Color.grayMain2c200) + .onTapGesture { + if eventLogMessage.message.attachments.first!.type == .fileTransfer && !eventLogMessage.message.isFileTransferInProgress { + CoreContext.shared.doOnCoreQueue { _ in + conversationViewModel.downloadContent( + chatMessage: eventLogMessage.eventModel.eventLog.chatMessage!, + content: eventLogMessage.eventModel.eventLog.chatMessage!.contents.first! + ) + } + } + } VStack { Text(eventLogMessage.message.attachments.first!.name) @@ -658,7 +668,6 @@ struct ChatBubbleView: View { .padding(.horizontal, 10) .frame(maxWidth: .infinity, alignment: .leading) } - .frame(width: geometryProxy.size.width - 110, height: 100) .background(.white) .clipShape(RoundedRectangle(cornerRadius: 10)) .onTapGesture { @@ -666,74 +675,126 @@ struct ChatBubbleView: View { } } } else if eventLogMessage.message.attachments.count > 1 { - let isGroup = conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup - LazyVGrid(columns: [ - GridItem(.adaptive(minimum: 120), spacing: 1) - ], spacing: 3) { - ForEach(eventLogMessage.message.attachments, id: \.id) { attachment in - ZStack { - Rectangle() - .fill(Color(.white)) - .frame(width: 120, height: 120) - - if #available(iOS 16.0, *) { - AsyncImage(url: attachment.thumbnail) { image in - ZStack { - image - .resizable() - .interpolation(.medium) - .aspectRatio(contentMode: .fill) - - if attachment.type == .video { - Image("play-fill") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 40, height: 40, alignment: .leading) + let sizeCard = ((geometryProxy.size.width - 150)/2)-2 + let columns = [ + GridItem(.adaptive(minimum: sizeCard), spacing: 1)] + + LazyVStack { + LazyVGrid(columns: columns) { + ForEach(eventLogMessage.message.attachments, id: \.id) { attachment in + if attachment.type == .image || attachment.type == .gif + || attachment.type == .video { + ZStack { + Rectangle() + .fill(Color(.white)) + .frame(width: sizeCard, height: sizeCard) + + if #available(iOS 16.0, *) { + AsyncImage(url: attachment.thumbnail) { image in + ZStack { + image + .resizable() + .interpolation(.medium) + .aspectRatio(contentMode: .fill) + + if attachment.type == .video { + Image("play-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 40, height: 40, alignment: .leading) + } + } + } placeholder: { + ProgressView() + } + .layoutPriority(-1) + .onTapGesture { + selectedURLAttachment = attachment.full + } + } else { + AsyncImage(url: attachment.thumbnail) { image in + ZStack { + image + .resizable() + .interpolation(.medium) + .aspectRatio(contentMode: .fill) + + if attachment.type == .video { + Image("play-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 40, height: 40, alignment: .leading) + } + } + } placeholder: { + ProgressView() + } + .id(UUID()) + .layoutPriority(-1) + .onTapGesture { + selectedURLAttachment = attachment.full } } - } placeholder: { - ProgressView() - } - .layoutPriority(-1) - .onTapGesture { - selectedURLAttachment = attachment.full - } - } else { - AsyncImage(url: attachment.thumbnail) { image in - ZStack { - image - .resizable() - .interpolation(.medium) - .aspectRatio(contentMode: .fill) - - if attachment.type == .video { - Image("play-fill") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 40, height: 40, alignment: .leading) - } - } - } placeholder: { - ProgressView() - } - .id(UUID()) - .layoutPriority(-1) - .onTapGesture { - selectedURLAttachment = attachment.full } + .clipShape(RoundedRectangle(cornerRadius: 4)) + .contentShape(Rectangle()) + } + } + } + + ForEach(eventLogMessage.message.attachments, id: \.id) { attachment in + if !(attachment.type == .image || attachment.type == .gif + || attachment.type == .video) { + HStack { + VStack { + Image(getImageOfType(type: attachment.type)) + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c700) + .frame(width: 60, height: 60, alignment: .leading) + } + .frame(width: 100, height: 100) + .background(Color.grayMain2c200) + .onTapGesture { + if attachment.type == .fileTransfer && !eventLogMessage.message.isFileTransferInProgress { + if let content = eventLogMessage.eventModel.eventLog.chatMessage!.contents.first(where: {$0.filePath == attachment.full.absoluteString}) { + CoreContext.shared.doOnCoreQueue { _ in + conversationViewModel.downloadContent( + chatMessage: eventLogMessage.eventModel.eventLog.chatMessage!, + content: content + ) + } + } + } + } + + VStack { + Text(attachment.name) + .foregroundStyle(Color.grayMain2c700) + .default_text_style_600(styleSize: 14) + .truncationMode(.middle) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + + Text(attachment.size.formatBytes()) + .default_text_style_300(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + .padding(.horizontal, 10) + .frame(maxWidth: .infinity, alignment: .leading) + } + .background(.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .onTapGesture { + selectedURLAttachment = attachment.full } } - .clipShape(RoundedRectangle(cornerRadius: 4)) - .contentShape(Rectangle()) } } - .frame( width: geometryProxy.size.width > 0 - && CGFloat(122 * eventLogMessage.message.attachments.count) > geometryProxy.size.width - 110 - (isGroup ? 40 : 0) - ? 122 * floor(CGFloat(geometryProxy.size.width - 110 - (isGroup ? 40 : 0)) / 122) - : CGFloat(122 * eventLogMessage.message.attachments.count) - ) + .frame(width: geometryProxy.size.width - 150) } } diff --git a/Linphone/UI/Main/Conversations/Model/Message.swift b/Linphone/UI/Main/Conversations/Model/Message.swift index ec035c713..9f43634df 100644 --- a/Linphone/UI/Main/Conversations/Model/Message.swift +++ b/Linphone/UI/Main/Conversations/Model/Message.swift @@ -88,6 +88,8 @@ public struct Message: Identifiable, Hashable { public var isIcalendar: Bool public var messageConferenceInfo: MessageConferenceInfo? + public var isFileTransferInProgress: Bool + public init( id: String, appData: String = "", @@ -109,7 +111,8 @@ public struct Message: Identifiable, Hashable { ephemeralExpireTime: Int = 0, ephemeralLifetime: Int = 0, isIcalendar: Bool = false, - messageConferenceInfo: MessageConferenceInfo? = nil + messageConferenceInfo: MessageConferenceInfo? = nil, + isFileTransferInProgress: Bool = false ) { self.id = id self.appData = appData @@ -132,6 +135,7 @@ public struct Message: Identifiable, Hashable { self.ephemeralLifetime = ephemeralLifetime self.isIcalendar = isIcalendar self.messageConferenceInfo = messageConferenceInfo + self.isFileTransferInProgress = isFileTransferInProgress } public static func makeMessage( diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index efb321009..6bcdb0982 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -590,7 +590,8 @@ class ConversationViewModel: ObservableObject { ephemeralExpireTime: eventLog.chatMessage?.ephemeralExpireTime ?? 0, ephemeralLifetime: eventLog.chatMessage?.ephemeralLifetime ?? 0, isIcalendar: eventLog.chatMessage?.contents.first?.isIcalendar ?? false, - messageConferenceInfo: eventLog.chatMessage != nil && eventLog.chatMessage!.contents.first != nil && eventLog.chatMessage!.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: eventLog.chatMessage!.contents.first!) : nil + messageConferenceInfo: eventLog.chatMessage != nil && eventLog.chatMessage!.contents.first != nil && eventLog.chatMessage!.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: eventLog.chatMessage!.contents.first!) : nil, + isFileTransferInProgress: eventLog.chatMessage!.isFileTransferInProgress ) ) ) @@ -823,7 +824,8 @@ class ConversationViewModel: ObservableObject { ephemeralExpireTime: eventLog.chatMessage?.ephemeralExpireTime ?? 0, ephemeralLifetime: eventLog.chatMessage?.ephemeralLifetime ?? 0, isIcalendar: eventLog.chatMessage?.contents.first?.isIcalendar ?? false, - messageConferenceInfo: eventLog.chatMessage != nil && eventLog.chatMessage!.contents.first != nil && eventLog.chatMessage!.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: eventLog.chatMessage!.contents.first!) : nil + messageConferenceInfo: eventLog.chatMessage != nil && eventLog.chatMessage!.contents.first != nil && eventLog.chatMessage!.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: eventLog.chatMessage!.contents.first!) : nil, + isFileTransferInProgress: eventLog.chatMessage!.isFileTransferInProgress ) ), at: 0 ) @@ -1069,7 +1071,8 @@ class ConversationViewModel: ObservableObject { ephemeralExpireTime: eventLog.chatMessage?.ephemeralExpireTime ?? 0, ephemeralLifetime: eventLog.chatMessage?.ephemeralLifetime ?? 0, isIcalendar: eventLog.chatMessage?.contents.first?.isIcalendar ?? false, - messageConferenceInfo: eventLog.chatMessage != nil && eventLog.chatMessage!.contents.first != nil && eventLog.chatMessage!.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: eventLog.chatMessage!.contents.first!) : nil + messageConferenceInfo: eventLog.chatMessage != nil && eventLog.chatMessage!.contents.first != nil && eventLog.chatMessage!.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: eventLog.chatMessage!.contents.first!) : nil, + isFileTransferInProgress: eventLog.chatMessage!.isFileTransferInProgress ) ) @@ -1363,7 +1366,8 @@ class ConversationViewModel: ObservableObject { ephemeralExpireTime: eventLog.chatMessage?.ephemeralExpireTime ?? 0, ephemeralLifetime: eventLog.chatMessage?.ephemeralLifetime ?? 0, isIcalendar: eventLog.chatMessage?.contents.first?.isIcalendar ?? false, - messageConferenceInfo: eventLog.chatMessage != nil && eventLog.chatMessage!.contents.first != nil && eventLog.chatMessage!.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: eventLog.chatMessage!.contents.first!) : nil + messageConferenceInfo: eventLog.chatMessage != nil && eventLog.chatMessage!.contents.first != nil && eventLog.chatMessage!.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: eventLog.chatMessage!.contents.first!) : nil, + isFileTransferInProgress: eventLog.chatMessage!.isFileTransferInProgress ) ), at: 0 ) @@ -1608,18 +1612,27 @@ class ConversationViewModel: ObservableObject { func downloadContent(chatMessage: ChatMessage, content: Content) { // Log.debug("[ConversationViewModel] Starting downloading content for file \(model.fileName)") - if !chatMessage.isFileTransferInProgress && (content.filePath == nil || content.filePath!.isEmpty) { - if let contentName = content.name { - // let isImage = FileUtil.isExtensionImage(path: contentName) - let file = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Images").absoluteString + (contentName.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "") - // let file = FileUtil.getFileStoragePath(fileName: contentName ?? "", isImage: isImage) - content.filePath = String(file.dropFirst(7)) - Log.info( - "[ConversationViewModel] File \(contentName) will be downloaded at \(content.filePath ?? "NIL")" - ) - self.displayedConversation?.downloadContent(chatMessage: chatMessage, content: content) - } else { - Log.error("[ConversationViewModel] Content name is null, can't download it!") + if self.displayedConversation != nil { + if !chatMessage.isFileTransferInProgress && (content.filePath == nil || content.filePath!.isEmpty) { + if let contentName = content.name { + // let isImage = FileUtil.isExtensionImage(path: contentName) + var file = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Images").absoluteString + (contentName.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "") + // let file = FileUtil.getFileStoragePath(fileName: contentName ?? "", isImage: isImage) + + var counter = 1 + while FileManager.default.fileExists(atPath: file) { + file = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Images").absoluteString + "\(counter)_" + (contentName.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "") + counter += 1 + } + + content.filePath = String(file.dropFirst(7)) + Log.info( + "[ConversationViewModel] File \(contentName) will be downloaded at \(content.filePath ?? "NIL")" + ) + self.displayedConversation!.downloadContent(chatMessage: chatMessage, content: content) + } else { + Log.error("[ConversationViewModel] Content name is null, can't download it!") + } } } } From a07f69e1df3b8ecb31ab9fbbf6d7a4d37c9ef63b Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 19 Nov 2024 16:31:30 +0100 Subject: [PATCH 18/18] Update the ephemeralExpireTime only if the index is within bounds of the message rows --- .../Main/Conversations/ViewModel/ConversationViewModel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 6bcdb0982..9cc73a6e4 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -199,7 +199,9 @@ class ConversationViewModel: ObservableObject { } } else { DispatchQueue.main.async { - self.conversationMessagesSection[0].rows[indexMessage].message.ephemeralExpireTime = ephemeralExpireTimeTmp + if indexMessage < self.conversationMessagesSection[0].rows.count { + self.conversationMessagesSection[0].rows[indexMessage].message.ephemeralExpireTime = ephemeralExpireTimeTmp + } } } }