From 4196fed865fe470d193a4ef7ae021920a6946183 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 22 Feb 2024 17:46:35 +0100 Subject: [PATCH] Add message bubbles --- Linphone/Localizable.xcstrings | 3 - Linphone/UI/Main/ContentView.swift | 14 +- .../Conversations/ConversationsView.swift | 9 +- .../Fragments/ChatBubbleView.swift | 24 ++- .../Fragments/ConversationFragment.swift | 162 +++++++++++++++--- .../Fragments/ConversationsFragment.swift | 7 +- .../Fragments/ConversationsListFragment.swift | 10 +- .../ViewModel/ConversationViewModel.swift | 144 +++++++++++++++- .../ConversationsListViewModel.swift | 1 + 9 files changed, 323 insertions(+), 51 deletions(-) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 068eac355..55e8fd566 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -349,9 +349,6 @@ }, "Headphones" : { - }, - "Hello, World!" : { - }, "History has been deleted" : { diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 696570e98..195f43d48 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -90,7 +90,7 @@ struct ContentView: View { Button(action: { self.index = 0 historyViewModel.displayedCall = nil - conversationsListViewModel.displayedConversation = nil + conversationViewModel.displayedConversation = nil }, label: { VStack { Image("address-book") @@ -134,7 +134,7 @@ struct ContentView: View { Button(action: { self.index = 1 contactViewModel.indexDisplayedFriend = nil - conversationsListViewModel.displayedConversation = nil + conversationViewModel.displayedConversation = nil if historyListViewModel.missedCallsCount > 0 { historyListViewModel.resetMissedCallsCount() } @@ -451,7 +451,7 @@ struct ContentView: View { isShowEditContactFragment: $isShowEditContactFragment ) } else if self.index == 2 { - ConversationsView(conversationsListViewModel: conversationsListViewModel) + ConversationsView(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel) } } .frame(maxWidth: @@ -483,7 +483,7 @@ struct ContentView: View { Button(action: { self.index = 0 historyViewModel.displayedCall = nil - conversationsListViewModel.displayedConversation = nil + conversationViewModel.displayedConversation = nil }, label: { VStack { Image("address-book") @@ -529,7 +529,7 @@ struct ContentView: View { Button(action: { self.index = 1 contactViewModel.indexDisplayedFriend = nil - conversationsListViewModel.displayedConversation = nil + conversationViewModel.displayedConversation = nil if historyListViewModel.missedCallsCount > 0 { historyListViewModel.resetMissedCallsCount() } @@ -613,7 +613,7 @@ struct ContentView: View { } } - if contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil || conversationsListViewModel.displayedConversation != nil { + if contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil || conversationViewModel.displayedConversation != nil { HStack(spacing: 0) { Spacer() .frame(maxWidth: @@ -878,7 +878,7 @@ struct ContentView: View { } } .onRotate { newOrientation in - if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil || conversationsListViewModel.displayedConversation != nil) && searchIsActive { + if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil || conversationViewModel.displayedConversation != nil) && searchIsActive { self.focusedField = false } else if searchIsActive { self.focusedField = true diff --git a/Linphone/UI/Main/Conversations/ConversationsView.swift b/Linphone/UI/Main/Conversations/ConversationsView.swift index 2119f0ab6..161368b2c 100644 --- a/Linphone/UI/Main/Conversations/ConversationsView.swift +++ b/Linphone/UI/Main/Conversations/ConversationsView.swift @@ -21,12 +21,13 @@ import SwiftUI struct ConversationsView: View { + @ObservedObject var conversationViewModel: ConversationViewModel @ObservedObject var conversationsListViewModel: ConversationsListViewModel var body: some View { NavigationView { ZStack(alignment: .bottomTrailing) { - ConversationsFragment(conversationsListViewModel: conversationsListViewModel) + ConversationsFragment(conversationViewModel: conversationViewModel, conversationsListViewModel: conversationsListViewModel) Button { } label: { @@ -47,5 +48,9 @@ struct ConversationsView: View { } #Preview { - ConversationsListFragment(conversationsListViewModel: ConversationsListViewModel(), showingSheet: .constant(false)) + ConversationsListFragment( + conversationViewModel: ConversationViewModel(), + conversationsListViewModel: ConversationsListViewModel(), + showingSheet: .constant(false) + ) } diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index 2f4d7f7fc..7789984ae 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -26,7 +26,29 @@ struct ChatBubbleView: View { let index: Int var body: some View { - Text(conversationViewModel.getMessage(index: index)) + if index < conversationViewModel.conversationMessagesList.count + && conversationViewModel.conversationMessagesList[index].eventLog.chatMessage != nil { + HStack { + if conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing { + Spacer() + } + + VStack { + Text(conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.utf8Text ?? "") + .foregroundStyle(Color.grayMain2c700) + .default_text_style(styleSize: 16) + } + .padding(.all, 15) + .background(conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing ? Color.orangeMain100 : Color.grayMain2c100) + .clipShape(RoundedRectangle(cornerRadius: 16)) + + if !conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing { + Spacer() + } + } + .padding(.leading, conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing ? 40 : 0) + .padding(.trailing, !conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing ? 40 : 0) + } } } diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 02c6e259f..95e1c90b4 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -55,14 +55,14 @@ struct ConversationFragment: View { .padding(.leading, -10) .onTapGesture { withAnimation { - conversationsListViewModel.displayedConversation = nil + conversationViewModel.displayedConversation = nil } } } let addressFriend = - (conversationsListViewModel.displayedConversation!.participants.first != nil && conversationsListViewModel.displayedConversation!.participants.first!.address != nil) - ? contactsManager.getFriendWithAddress(address: conversationsListViewModel.displayedConversation!.participants.first!.address!) + (conversationViewModel.displayedConversation!.participants.first != nil && conversationViewModel.displayedConversation!.participants.first!.address != nil) + ? contactsManager.getFriendWithAddress(address: conversationViewModel.displayedConversation!.participants.first!.address!) : nil let contactAvatarModel = addressFriend != nil @@ -73,11 +73,11 @@ struct ConversationFragment: View { }) : ContactAvatarModel(friend: nil, withPresence: false) - if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationsListViewModel.displayedConversation!) { + if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationViewModel.displayedConversation!) { Image(uiImage: contactsManager.textToImage( - firstName: conversationsListViewModel.displayedConversation!.subject!, - lastName: conversationsListViewModel.displayedConversation!.subject!.components(separatedBy: " ").count > 1 - ? conversationsListViewModel.displayedConversation!.subject!.components(separatedBy: " ")[1] + firstName: conversationViewModel.displayedConversation!.subject!, + lastName: conversationViewModel.displayedConversation!.subject!.components(separatedBy: " ").count > 1 + ? conversationViewModel.displayedConversation!.subject!.components(separatedBy: " ")[1] : "")) .resizable() .frame(width: 50, height: 50) @@ -95,13 +95,13 @@ struct ConversationFragment: View { .padding(.top, 4) } } else { - if conversationsListViewModel.displayedConversation!.participants.first != nil - && conversationsListViewModel.displayedConversation!.participants.first!.address != nil { - if conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName != nil { + if conversationViewModel.displayedConversation!.participants.first != nil + && conversationViewModel.displayedConversation!.participants.first!.address != nil { + if conversationViewModel.displayedConversation!.participants.first!.address!.displayName != nil { Image(uiImage: contactsManager.textToImage( - firstName: conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName!, - lastName: conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName!.components(separatedBy: " ").count > 1 - ? conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName!.components(separatedBy: " ")[1] + firstName: conversationViewModel.displayedConversation!.participants.first!.address!.displayName!, + lastName: conversationViewModel.displayedConversation!.participants.first!.address!.displayName!.components(separatedBy: " ").count > 1 + ? conversationViewModel.displayedConversation!.participants.first!.address!.displayName!.components(separatedBy: " ")[1] : "")) .resizable() .frame(width: 50, height: 50) @@ -110,9 +110,9 @@ struct ConversationFragment: View { } else { Image(uiImage: contactsManager.textToImage( - firstName: conversationsListViewModel.displayedConversation!.participants.first!.address!.username ?? "Username Error", - lastName: conversationsListViewModel.displayedConversation!.participants.first!.address!.username!.components(separatedBy: " ").count > 1 - ? conversationsListViewModel.displayedConversation!.participants.first!.address!.username!.components(separatedBy: " ")[1] + firstName: conversationViewModel.displayedConversation!.participants.first!.address!.username ?? "Username Error", + lastName: conversationViewModel.displayedConversation!.participants.first!.address!.username!.components(separatedBy: " ").count > 1 + ? conversationViewModel.displayedConversation!.participants.first!.address!.username!.components(separatedBy: " ")[1] : "")) .resizable() .frame(width: 50, height: 50) @@ -129,8 +129,8 @@ struct ConversationFragment: View { } } - if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationsListViewModel.displayedConversation!) { - Text(conversationsListViewModel.displayedConversation!.subject ?? "No Subject") + if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationViewModel.displayedConversation!) { + Text(conversationViewModel.displayedConversation!.subject ?? "No Subject") .default_text_style(styleSize: 16) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 4) @@ -142,11 +142,11 @@ struct ConversationFragment: View { .padding(.top, 4) .lineLimit(1) } else { - if conversationsListViewModel.displayedConversation!.participants.first != nil - && conversationsListViewModel.displayedConversation!.participants.first!.address != nil { - Text(conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName != nil - ? conversationsListViewModel.displayedConversation!.participants.first!.address!.displayName! - : conversationsListViewModel.displayedConversation!.participants.first!.address!.username!) + if conversationViewModel.displayedConversation!.participants.first != nil + && conversationViewModel.displayedConversation!.participants.first!.address != nil { + Text(conversationViewModel.displayedConversation!.participants.first!.address!.displayName != nil + ? conversationViewModel.displayedConversation!.participants.first!.address!.displayName! + : conversationViewModel.displayedConversation!.participants.first!.address!.username!) .default_text_style(styleSize: 16) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 4) @@ -225,17 +225,118 @@ struct ConversationFragment: View { .padding(.bottom, 4) .background(.white) + + + + + + List { - if conversationsListViewModel.displayedConversation != nil { - ForEach(0..() + + @Published var conversationMessagesList: [LinphoneCustomEventLog] = [] + init() {} - func getMessage(index: Int) -> String { - if self.displayedConversation != nil { - return displayedConversation!.getHistoryRangeEvents(begin: index, end: index+1).first?.chatMessage?.utf8Text ?? "" - } - else { - return "" + func addConversationDelegate() { + if displayedConversation != nil { + self.chatRoomSuscriptions.insert(displayedConversation!.publisher?.onChatMessageSent?.postOnMainQueue { (cbValue: (chatRoom: ChatRoom, eventLog: EventLog)) in + self.getNewMessages(eventLogs: [cbValue.eventLog]) + }) + + self.chatRoomSuscriptions.insert(displayedConversation!.publisher?.onChatMessagesReceived?.postOnMainQueue { (cbValue: (chatRoom: ChatRoom, eventLogs: [EventLog])) in + self.getNewMessages(eventLogs: cbValue.eventLogs) + }) } } + + func removeConversationDelegate() { + self.chatRoomSuscriptions.removeAll() + } + + func getMessage() { + if self.displayedConversation != nil { + let historyEvents = displayedConversation!.getHistoryRangeEvents(begin: conversationMessagesList.count, end: conversationMessagesList.count + 30) + + historyEvents.reversed().forEach { eventLog in + conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog)) + } + } + } + + func getNewMessages(eventLogs: [EventLog]) { + withAnimation { + eventLogs.forEach { eventLog in + conversationMessagesList.insert(LinphoneCustomEventLog(eventLog: eventLog), at: 0) + //conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog)) + } + } + } + + func resetMessage() { + conversationMessagesList = [] + } + + func sendMessage() { + //val messageToReplyTo = chatMessageToReplyTo + //val message = if (messageToReplyTo != null) { + //Log.i("$TAG Sending message as reply to [${messageToReplyTo.messageId}]") + //chatRoom.createReplyMessage(messageToReplyTo) + //} else { + let message = try? self.displayedConversation!.createEmptyMessage() + //} + + let toSend = self.messageText.trimmingCharacters(in: .whitespacesAndNewlines) + if !toSend.isEmpty { + if message != nil { + message!.addUtf8TextContent(text: toSend) + } + } + + /* + if (isVoiceRecording.value == true && voiceMessageRecorder.file != null) { + stopVoiceRecorder() + val content = voiceMessageRecorder.createContent() + if (content != null) { + Log.i( + "$TAG Voice recording content created, file name is ${content.name} and duration is ${content.fileDuration}" + ) + message.addContent(content) + } else { + Log.e("$TAG Voice recording content couldn't be created!") + } + } else { + for (attachment in attachments.value.orEmpty()) { + val content = Factory.instance().createContent() + + content.type = when (attachment.mimeType) { + FileUtils.MimeType.Image -> "image" + FileUtils.MimeType.Audio -> "audio" + FileUtils.MimeType.Video -> "video" + FileUtils.MimeType.Pdf -> "application" + FileUtils.MimeType.PlainText -> "text" + else -> "file" + } + content.subtype = if (attachment.mimeType == FileUtils.MimeType.PlainText) { + "plain" + } else { + FileUtils.getExtensionFromFileName(attachment.fileName) + } + content.name = attachment.fileName + // Let the file body handler take care of the upload + content.filePath = attachment.file + + message.addFileContent(content) + } + } + */ + + if message != nil && !message!.contents.isEmpty { + Log.info("[ConversationViewModel] Sending message") + message!.send() + } + + Log.info("[ConversationViewModel] Message sent, re-setting defaults") + self.messageText = "" + /* + isReplying.postValue(false) + isFileAttachmentsListOpen.postValue(false) + isParticipantsListOpen.postValue(false) + isEmojiPickerOpen.postValue(false) + + if (::voiceMessageRecorder.isInitialized) { + stopVoiceRecorder() + } + isVoiceRecording.postValue(false) + + // Warning: do not delete files + val attachmentsList = arrayListOf() + attachments.postValue(attachmentsList) + + chatMessageToReplyTo = null + */ + } +} +struct LinphoneCustomEventLog: Hashable { + var id = UUID() + var eventLog: EventLog + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension LinphoneCustomEventLog { + static func ==(lhs: LinphoneCustomEventLog, rhs: LinphoneCustomEventLog) -> Bool { + return lhs.id == rhs.id + } } diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift index 094aa5b04..f3a3c8da7 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -109,6 +109,7 @@ class ConversationsListViewModel: ObservableObject { self.mCoreSuscriptions.insert(core.publisher?.onMessagesReceived?.postOnMainQueue { _ in self.computeChatRoomsList(filter: "") + }) } }