From 2b80c5b78bf4138788e73a1029afa2c9d64be0d6 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 8 Oct 2024 09:12:16 +0200 Subject: [PATCH] Ephemeral message --- .../Fragments/ChatBubbleView.swift | 72 +++++++++++++++++ .../Main/Conversations/Fragments/UIList.swift | 2 +- .../UI/Main/Conversations/Model/Message.swift | 14 +++- .../ViewModel/ConversationViewModel.swift | 79 +++++++++++++++++-- 4 files changed, 156 insertions(+), 11 deletions(-) diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index 4ad63f305..24e1cacc7 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -34,6 +34,9 @@ struct ChatBubbleView: View { @State private var isPressed: Bool = false @State private var timePassed: TimeInterval? + @State private var timer: Timer? + @State private var ephemeralLifetime: String = "" + var body: some View { HStack { if eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage { @@ -165,6 +168,28 @@ struct ChatBubbleView: View { } HStack(alignment: .center) { + if eventLogMessage.message.isEphemeral && eventLogMessage.message.isOutgoing { + Text(ephemeralLifetime) + .foregroundStyle(Color.grayMain2c500) + .default_text_style_300(styleSize: 14) + .padding(.top, 1) + .onAppear { + updateEphemeralTimer() + } + .onChange(of: eventLogMessage.message.ephemeralExpireTime) { ephemeralExpireTimeTmp in + if ephemeralExpireTimeTmp > 0 { + updateEphemeralTimer() + } + } + + Image("clock-countdown") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 15, height: 15) + .padding(.top, 1) + } + Text(conversationViewModel.getMessageTime(startDate: eventLogMessage.message.dateReceived)) .foregroundStyle(Color.grayMain2c500) .default_text_style_300(styleSize: 14) @@ -187,6 +212,29 @@ struct ChatBubbleView: View { .padding(.top, 1) } } + + if eventLogMessage.message.isEphemeral && !eventLogMessage.message.isOutgoing { + Image("clock-countdown") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 15, height: 15) + .padding(.top, 1) + .padding(.trailing, -4) + + Text(ephemeralLifetime) + .foregroundStyle(Color.grayMain2c500) + .default_text_style_300(styleSize: 14) + .padding(.top, 1) + .onAppear { + updateEphemeralTimer() + } + .onChange(of: eventLogMessage.message.ephemeralExpireTime) { ephemeralExpireTimeTmp in + if ephemeralExpireTimeTmp > 0 { + updateEphemeralTimer() + } + } + } } .onTapGesture { conversationViewModel.selectedMessageToDisplayDetails = eventLogMessage @@ -551,6 +599,30 @@ struct ChatBubbleView: View { return "file" } } + + private func updateEphemeralTimer() { + if eventLogMessage.message.isEphemeral { + if eventLogMessage.message.ephemeralExpireTime == 0 { + // Message hasn't been read by all participants yet + self.ephemeralLifetime = eventLogMessage.message.ephemeralLifetime.convertDurationToString() + } else { + let remaining = eventLogMessage.message.ephemeralExpireTime - Int(Date().timeIntervalSince1970) + self.ephemeralLifetime = remaining.convertDurationToString() + + if timer == nil { + timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + let updatedRemaining = eventLogMessage.message.ephemeralExpireTime - Int(Date().timeIntervalSince1970) + if updatedRemaining <= 0 { + timer?.invalidate() + timer = nil + } else { + self.ephemeralLifetime = updatedRemaining.convertDurationToString() + } + } + } + } + } + } } enum URLType { diff --git a/Linphone/UI/Main/Conversations/Fragments/UIList.swift b/Linphone/UI/Main/Conversations/Fragments/UIList.swift index 08d37e850..a98fea944 100644 --- a/Linphone/UI/Main/Conversations/Fragments/UIList.swift +++ b/Linphone/UI/Main/Conversations/Fragments/UIList.swift @@ -233,7 +233,7 @@ struct UIList: UIViewRepresentable { tableView.insertSections([section], with: .top) case .delete(let section, let row): - tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: .top) + tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: .left) case .insert(let section, let row): tableView.insertRows(at: [IndexPath(row: row, section: section)], with: .top) case .edit(let section, let row): diff --git a/Linphone/UI/Main/Conversations/Model/Message.swift b/Linphone/UI/Main/Conversations/Model/Message.swift index 94644eaae..271b23298 100644 --- a/Linphone/UI/Main/Conversations/Model/Message.swift +++ b/Linphone/UI/Main/Conversations/Model/Message.swift @@ -80,6 +80,10 @@ public struct Message: Identifiable, Hashable { public var isForward: Bool public var ownReaction: String public var reactions: [String] + + public var isEphemeral: Bool + public var ephemeralExpireTime: Int + public var ephemeralLifetime: Int public init( id: String, @@ -97,7 +101,10 @@ public struct Message: Identifiable, Hashable { replyMessage: ReplyMessage? = nil, isForward: Bool = false, ownReaction: String = "", - reactions: [String] = [] + reactions: [String] = [], + isEphemeral: Bool = false, + ephemeralExpireTime: Int = 0, + ephemeralLifetime: Int = 0 ) { self.id = id self.appData = appData @@ -115,6 +122,9 @@ public struct Message: Identifiable, Hashable { self.isForward = isForward self.ownReaction = ownReaction self.reactions = reactions + self.isEphemeral = isEphemeral + self.ephemeralExpireTime = ephemeralExpireTime + self.ephemeralLifetime = ephemeralLifetime } public static func makeMessage( @@ -167,7 +177,7 @@ extension Message { extension Message: Equatable { public static func == (lhs: Message, rhs: Message) -> Bool { - lhs.id == rhs.id && lhs.status == rhs.status && lhs.isFirstMessage == rhs.isFirstMessage && lhs.ownReaction == rhs.ownReaction && lhs.reactions == rhs.reactions + lhs.id == rhs.id && lhs.status == rhs.status && lhs.isFirstMessage == rhs.isFirstMessage && lhs.ownReaction == rhs.ownReaction && lhs.reactions == rhs.reactions && lhs.ephemeralExpireTime == rhs.ephemeralExpireTime } } diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 940b0b95e..45e344877 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -114,6 +114,8 @@ class ConversationViewModel: ObservableObject { self.getNewMessages(eventLogs: eventLogs) }, onChatMessageSending: { (_: ChatRoom, eventLog: EventLog) in self.getNewMessages(eventLogs: [eventLog]) + }, onEphemeralMessageDeleted: {(_: ChatRoom, eventLog: EventLog) in + self.removeMessage(eventLog) }) self.chatRoomDelegateHolder = ChatRoomDelegateHolder(chatroom: chatroom, delegate: chatRoomDelegate) } @@ -139,12 +141,22 @@ class ConversationViewModel: ObservableObject { statusTmp = .sending } + let ephemeralExpireTimeTmp = message.ephemeralExpireTime + if !self.conversationMessagesSection.isEmpty && !self.conversationMessagesSection[0].rows.isEmpty { if let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLog.chatMessage?.messageId == message.messageId}) { - if indexMessage < self.conversationMessagesSection[0].rows.count && self.conversationMessagesSection[0].rows[indexMessage].message.status != statusTmp { - DispatchQueue.main.async { - //self.objectWillChange.send() - self.conversationMessagesSection[0].rows[indexMessage].message.status = statusTmp ?? .error + if indexMessage < self.conversationMessagesSection[0].rows.count { + if self.conversationMessagesSection[0].rows[indexMessage].message.status != statusTmp { + DispatchQueue.main.async { + //self.objectWillChange.send() + self.conversationMessagesSection[0].rows[indexMessage].message.status = statusTmp ?? .error + self.conversationMessagesSection[0].rows[indexMessage].message.ephemeralExpireTime = ephemeralExpireTimeTmp + } + } else { + DispatchQueue.main.async { + //self.objectWillChange.send() + self.conversationMessagesSection[0].rows[indexMessage].message.ephemeralExpireTime = ephemeralExpireTimeTmp + } } } } @@ -201,7 +213,18 @@ class ConversationViewModel: ObservableObject { self.conversationMessagesSection[0].rows[indexMessage!].message.reactions = reactionsTmp } } + }, onEphemeralMessageTimerStarted: { (message: ChatMessage) in + let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLog.chatMessage?.messageId == message.messageId}) + let ephemeralExpireTimeTmp = message.ephemeralExpireTime + + DispatchQueue.main.async { + if indexMessage != nil { + self.objectWillChange.send() + self.conversationMessagesSection[0].rows[indexMessage!].message.ephemeralExpireTime = ephemeralExpireTimeTmp + } + } }) + self.chatMessageDelegateHolders.append(ChatMessageDelegateHolder(message: message, delegate: chatMessageDelegate)) } } @@ -480,7 +503,10 @@ class ConversationViewModel: ObservableObject { replyMessage: replyMessageTmp, isForward: eventLog.chatMessage?.isForward ?? false, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", - reactions: reactionsTmp + reactions: reactionsTmp, + isEphemeral: eventLog.chatMessage?.isEphemeral ?? false, + ephemeralExpireTime: eventLog.chatMessage?.ephemeralExpireTime ?? 0, + ephemeralLifetime: eventLog.chatMessage?.ephemeralLifetime ?? 0 ) ) ) @@ -700,7 +726,10 @@ class ConversationViewModel: ObservableObject { replyMessage: replyMessageTmp, isForward: eventLog.chatMessage?.isForward ?? false, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", - reactions: reactionsTmp + reactions: reactionsTmp, + isEphemeral: eventLog.chatMessage?.isEphemeral ?? false, + ephemeralExpireTime: eventLog.chatMessage?.ephemeralExpireTime ?? 0, + ephemeralLifetime: eventLog.chatMessage?.ephemeralLifetime ?? 0 ) ), at: 0 ) @@ -932,7 +961,10 @@ class ConversationViewModel: ObservableObject { replyMessage: replyMessageTmp, isForward: eventLog.chatMessage?.isForward ?? false, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", - reactions: reactionsTmp + reactions: reactionsTmp, + isEphemeral: eventLog.chatMessage?.isEphemeral ?? false, + ephemeralExpireTime: eventLog.chatMessage?.ephemeralExpireTime ?? 0, + ephemeralLifetime: eventLog.chatMessage?.ephemeralLifetime ?? 0 ) ) @@ -1210,7 +1242,10 @@ class ConversationViewModel: ObservableObject { replyMessage: replyMessageTmp, isForward: eventLog.chatMessage?.isForward ?? false, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", - reactions: reactionsTmp + reactions: reactionsTmp, + isEphemeral: eventLog.chatMessage?.isEphemeral ?? false, + ephemeralExpireTime: eventLog.chatMessage?.ephemeralExpireTime ?? 0, + ephemeralLifetime: eventLog.chatMessage?.ephemeralLifetime ?? 0 ) ), at: 0 ) @@ -1257,6 +1292,34 @@ class ConversationViewModel: ObservableObject { } } + func removeMessage(_ eventLog: EventLog) { + /* + if let found = self.conversationMessagesSection[0].rows.first(where: { $0.message.id == eventLog.chatMessage?.messageId }) { + var updatedList = self.conversationMessagesSection[0].rows + + print("Removing message from conversation events list") + if let index = updatedList.firstIndex(where: { $0.message.id == found.message.id }) { + updatedList.remove(at: index) + } + + DispatchQueue.main.async { + self.conversationMessagesSection[0].rows = updatedList + } + } else { + print("Failed to find matching message in conversation events list") + } + */ + + if let index = self.conversationMessagesSection[0].rows.firstIndex(where: { $0.message.id == eventLog.chatMessage?.messageId }) { + DispatchQueue.main.async { + if index > 0 && self.conversationMessagesSection[0].rows[index - 1].message.address == self.conversationMessagesSection[0].rows[index].message.address { + self.conversationMessagesSection[0].rows[index - 1].message.isFirstMessage = self.conversationMessagesSection[0].rows[index].message.isFirstMessage + } + self.conversationMessagesSection[0].rows.remove(at: index) + } + } + } + func sendMessage(audioRecorder: AudioRecorder? = nil) { coreContext.doOnCoreQueue { _ in do {