From 3ba5fd5f3847e162f91abbb0db1c395822735146 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 14 Aug 2024 14:37:46 +0200 Subject: [PATCH] Add swipe action in message list --- .../reply-reversed.imageset/Contents.json | 21 ++ .../reply-reversed.svg | 5 + Linphone/Localizable.xcstrings | 20 ++ .../Fragments/ChatBubbleView.swift | 271 +++++++++--------- .../Fragments/ConversationFragment.swift | 59 +++- .../Main/Conversations/Fragments/UIList.swift | 14 + .../Main/Conversations/Model/Attachment.swift | 8 +- .../UI/Main/Conversations/Model/Message.swift | 7 +- .../ViewModel/ConversationViewModel.swift | 61 ++++ Linphone/Utils/PhotoPicker.swift | 4 +- 10 files changed, 325 insertions(+), 145 deletions(-) create mode 100644 Linphone/Assets.xcassets/reply-reversed.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/reply-reversed.imageset/reply-reversed.svg diff --git a/Linphone/Assets.xcassets/reply-reversed.imageset/Contents.json b/Linphone/Assets.xcassets/reply-reversed.imageset/Contents.json new file mode 100644 index 000000000..6d84f517c --- /dev/null +++ b/Linphone/Assets.xcassets/reply-reversed.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reply-reversed.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/reply-reversed.imageset/reply-reversed.svg b/Linphone/Assets.xcassets/reply-reversed.imageset/reply-reversed.svg new file mode 100644 index 000000000..a65d6cf6b --- /dev/null +++ b/Linphone/Assets.xcassets/reply-reversed.imageset/reply-reversed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 74082046b..c4ead7d27 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -27,6 +27,9 @@ }, "*" : { + }, + "**%@**" : { + }, "**Camera** : Pour capturer votre vidéo lors des appels vidéo et conférence." : { @@ -969,6 +972,23 @@ } } }, + "conversation_reply_to_message_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Replying to: " + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En réponse à : " + } + } + } + }, "Conversations" : { }, diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index bef6589ca..75070b266 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -33,159 +33,168 @@ struct ChatBubbleView: View { @State private var timePassed: TimeInterval? var body: some View { - VStack { - if !message.text.isEmpty || !message.attachments.isEmpty { - HStack(alignment: .top, content: { - if message.isOutgoing { - Spacer() - } - - if conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup && !message.isOutgoing && message.isFirstMessage { - VStack { - Avatar( - contactAvatarModel: conversationViewModel.participantConversationModel.first(where: {$0.address == message.address}) ?? - ContactAvatarModel(friend: nil, name: "??", address: "", withPresence: false), - avatarSize: 35 - ) - .padding(.top, 30) + HStack { + VStack { + if !message.text.isEmpty || !message.attachments.isEmpty { + HStack(alignment: .top, content: { + if message.isOutgoing { + Spacer() } - } else if conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup && !message.isOutgoing { - VStack { - } - .padding(.leading, 43) - } - - VStack(alignment: .leading, spacing: 0) { + if conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup && !message.isOutgoing && message.isFirstMessage { - Text(conversationViewModel.participantConversationModel.first(where: {$0.address == message.address})?.name ?? "") - .default_text_style(styleSize: 12) - .padding(.top, 10) - .padding(.bottom, 2) + VStack { + Avatar( + contactAvatarModel: conversationViewModel.participantConversationModel.first(where: {$0.address == message.address}) ?? + ContactAvatarModel(friend: nil, name: "??", address: "", withPresence: false), + avatarSize: 35 + ) + .padding(.top, 30) + } + } else if conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup && !message.isOutgoing { + VStack { + } + .padding(.leading, 43) } - ZStack { - - HStack { - if message.isOutgoing { - Spacer() - } + + VStack(alignment: .leading, spacing: 0) { + if conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup && !message.isOutgoing && message.isFirstMessage { + Text(conversationViewModel.participantConversationModel.first(where: {$0.address == message.address})?.name ?? "") + .default_text_style(styleSize: 12) + .padding(.top, 10) + .padding(.bottom, 2) + } + ZStack { - VStack(alignment: message.isOutgoing ? .trailing : .leading) { + HStack { + if message.isOutgoing { + Spacer() + } + VStack(alignment: message.isOutgoing ? .trailing : .leading) { - if !message.attachments.isEmpty { - messageAttachments() - } - - if !message.text.isEmpty { - Text(message.text) - .foregroundStyle(Color.grayMain2c700) - .default_text_style(styleSize: 16) - } - - HStack(alignment: .center) { - Text(conversationViewModel.getMessageTime(startDate: message.dateReceived)) - .foregroundStyle(Color.grayMain2c500) - .default_text_style_300(styleSize: 14) - .padding(.top, 1) + VStack(alignment: message.isOutgoing ? .trailing : .leading) { + if !message.attachments.isEmpty { + messageAttachments() + } - if (conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup) || message.isOutgoing { - if message.status == .sending { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500)) - .frame(width: 15, height: 15) - .padding(.top, 1) - } else if message.status != nil { - Image(conversationViewModel.getImageIMDN(status: message.status!)) - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 15, height: 15) - .padding(.top, 1) + if !message.text.isEmpty { + Text(message.text) + .foregroundStyle(Color.grayMain2c700) + .default_text_style(styleSize: 16) + } + + HStack(alignment: .center) { + Text(conversationViewModel.getMessageTime(startDate: message.dateReceived)) + .foregroundStyle(Color.grayMain2c500) + .default_text_style_300(styleSize: 14) + .padding(.top, 1) + + if (conversationViewModel.displayedConversation != nil && conversationViewModel.displayedConversation!.isGroup) || message.isOutgoing { + if message.status == .sending { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500)) + .frame(width: 15, height: 15) + .padding(.top, 1) + } else if message.status != nil { + Image(conversationViewModel.getImageIMDN(status: message.status!)) + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 15, height: 15) + .padding(.top, 1) + } } } + .padding(.top, -4) } - .padding(.top, -4) - } - .padding(.all, 15) - .background(message.isOutgoing ? Color.orangeMain100 : Color.grayMain2c100) - .clipShape(RoundedRectangle(cornerRadius: 3)) - .roundedCorner( - 16, - corners: message.isOutgoing && message.isFirstMessage ? [.topLeft, .topRight, .bottomLeft] : - (!message.isOutgoing && message.isFirstMessage ? [.topRight, .bottomRight, .bottomLeft] : [.allCorners])) - - if !message.reactions.isEmpty { - HStack { - ForEach(0..= scrollView.contentSize.height - scrollView.frame.height - 200 } + + func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + + let archiveAction = UIContextualAction(style: .normal, title: "") { action, view, completionHandler in + self.conversationViewModel.replyToMessage(index: indexPath.row) + completionHandler(true) + } + + archiveAction.image = UIImage(named: "reply-reversed")! + + let configuration = UISwipeActionsConfiguration(actions: [archiveAction]) + + return configuration + } } } diff --git a/Linphone/UI/Main/Conversations/Model/Attachment.swift b/Linphone/UI/Main/Conversations/Model/Attachment.swift index 39d456e79..0e84668f1 100644 --- a/Linphone/UI/Main/Conversations/Model/Attachment.swift +++ b/Linphone/UI/Main/Conversations/Model/Attachment.swift @@ -45,18 +45,20 @@ public enum AttachmentType: String, Codable { public struct Attachment: Codable, Identifiable, Hashable { public let id: String + public let name: String public let thumbnail: URL public let full: URL public let type: AttachmentType - public init(id: String, thumbnail: URL, full: URL, type: AttachmentType) { + public init(id: String, name: String, thumbnail: URL, full: URL, type: AttachmentType) { self.id = id + self.name = name self.thumbnail = thumbnail self.full = full self.type = type } - public init(id: String, url: URL, type: AttachmentType) { - self.init(id: id, thumbnail: url, full: url, type: type) + public init(id: String, name: String, url: URL, type: AttachmentType) { + self.init(id: id, name: name, thumbnail: url, full: url, type: type) } } diff --git a/Linphone/UI/Main/Conversations/Model/Message.swift b/Linphone/UI/Main/Conversations/Model/Message.swift index 97ea3f44d..495848fde 100644 --- a/Linphone/UI/Main/Conversations/Model/Message.swift +++ b/Linphone/UI/Main/Conversations/Model/Message.swift @@ -70,6 +70,7 @@ public struct Message: Identifiable, Hashable { public var address: String public var isFirstMessage: Bool public var text: String + public var attachmentsNames: String public var attachments: [Attachment] public var recording: Recording? public var replyMessage: ReplyMessage? @@ -85,6 +86,7 @@ public struct Message: Identifiable, Hashable { address: String, isFirstMessage: Bool = false, text: String = "", + attachmentsNames: String = "", attachments: [Attachment] = [], recording: Recording? = nil, replyMessage: ReplyMessage? = nil, @@ -99,6 +101,7 @@ public struct Message: Identifiable, Hashable { self.isFirstMessage = isFirstMessage self.address = address self.text = text + self.attachmentsNames = attachmentsNames self.attachments = attachments self.recording = recording self.replyMessage = replyMessage @@ -117,12 +120,12 @@ public struct Message: Identifiable, Hashable { switch media.type { case .image: - return Attachment(id: UUID().uuidString, url: thumbnailURL, type: .image) + return Attachment(id: UUID().uuidString, name: "", url: thumbnailURL, type: .image) case .video: guard let fullURL = await media.getURL() else { return nil } - return Attachment(id: UUID().uuidString, thumbnail: thumbnailURL, full: fullURL, type: .video) + return Attachment(id: UUID().uuidString, name: "", thumbnail: thumbnailURL, full: fullURL, type: .video) } } diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index c41d22749..6d2ff4472 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -46,6 +46,7 @@ class ConversationViewModel: ObservableObject { var oldMessageReceived = false @Published var selectedMessage: Message? + @Published var messageToReply: Message? init() {} @@ -180,6 +181,15 @@ 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) + } + } + } } } } @@ -188,6 +198,10 @@ class ConversationViewModel: ObservableObject { self.getHistorySize() self.getUnreadMessagesCount() self.getParticipantConversationModel() + + self.mediasToSend.removeAll() + self.messageToReply = nil + coreContext.doOnCoreQueue { _ in if self.displayedConversation != nil { let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: 0, end: 30) @@ -195,6 +209,7 @@ class ConversationViewModel: ObservableObject { var conversationMessage: [Message] = [] historyEvents.enumerated().forEach { index, eventLog in + var attachmentNameList: String = "" var attachmentList: [Attachment] = [] var contentText = "" @@ -211,9 +226,11 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, url: path!, type: .image ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } else { @@ -224,9 +241,11 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, url: path!, type: (content.name?.lowercased().hasSuffix("gif"))! ? .gif : .image ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } else if content.type == "video" { @@ -237,10 +256,12 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, thumbnail: pathThumbnail!, full: path!, type: .video ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } @@ -282,6 +303,10 @@ class ConversationViewModel: ObservableObject { reactionsTmp.append(chatMessageReaction.body) }) + if !attachmentNameList.isEmpty { + attachmentNameList = String(attachmentNameList.dropFirst(2)) + } + if eventLog.chatMessage != nil { conversationMessage.append( Message( @@ -292,6 +317,7 @@ class ConversationViewModel: ObservableObject { address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, text: contentText, + attachmentsNames: attachmentNameList, attachments: attachmentList, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", reactions: reactionsTmp @@ -334,6 +360,7 @@ class ConversationViewModel: ObservableObject { var conversationMessagesTmp: [Message] = [] historyEvents.enumerated().reversed().forEach { index, eventLog in + var attachmentNameList: String = "" var attachmentList: [Attachment] = [] var contentText = "" @@ -350,9 +377,11 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, url: path!, type: .image ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } else { @@ -363,9 +392,11 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, url: path!, type: (content.name?.lowercased().hasSuffix("gif"))! ? .gif : .image ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } else if content.type == "video" { @@ -376,10 +407,12 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, thumbnail: pathThumbnail!, full: path!, type: .video ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } @@ -421,6 +454,10 @@ class ConversationViewModel: ObservableObject { reactionsTmp.append(chatMessageReaction.body) }) + if !attachmentNameList.isEmpty { + attachmentNameList = String(attachmentNameList.dropFirst(2)) + } + if eventLog.chatMessage != nil { conversationMessagesTmp.insert( Message( @@ -431,6 +468,7 @@ class ConversationViewModel: ObservableObject { address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, text: contentText, + attachmentsNames: attachmentNameList, attachments: attachmentList, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", reactions: reactionsTmp @@ -471,6 +509,7 @@ class ConversationViewModel: ObservableObject { func getNewMessages(eventLogs: [EventLog]) { eventLogs.enumerated().forEach { index, eventLog in + var attachmentNameList: String = "" var attachmentList: [Attachment] = [] var contentText = "" @@ -487,9 +526,11 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, url: path!, type: .image ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } else if content.name != nil && !content.name!.isEmpty { @@ -500,9 +541,11 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, url: path!, type: (content.name?.lowercased().hasSuffix("gif"))! ? .gif : .image ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } else if content.type == "video" { @@ -513,10 +556,12 @@ class ConversationViewModel: ObservableObject { let attachment = Attachment( id: UUID().uuidString, + name: content.name!, thumbnail: pathThumbnail!, full: path!, type: .video ) + attachmentNameList += ", \(content.name!)" attachmentList.append(attachment) } } @@ -573,6 +618,10 @@ class ConversationViewModel: ObservableObject { reactionsTmp.append(chatMessageReaction.body) }) + if !attachmentNameList.isEmpty { + attachmentNameList = String(attachmentNameList.dropFirst(2)) + } + if eventLog.chatMessage != nil { let message = Message( id: eventLog.chatMessage?.messageId ?? UUID().uuidString, @@ -582,6 +631,7 @@ class ConversationViewModel: ObservableObject { address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, text: contentText, + attachmentsNames: attachmentNameList, attachments: attachmentList, ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "", reactions: reactionsTmp @@ -636,6 +686,17 @@ class ConversationViewModel: ObservableObject { conversationMessagesSection = [] } + func replyToMessage(index: Int) { + coreContext.doOnCoreQueue { _ in + let messageToReplyTmp = self.conversationMessagesSection[0].rows[index] + DispatchQueue.main.async { + withAnimation(.linear(duration: 0.15)) { + self.messageToReply = messageToReplyTmp + } + } + } + } + func sendMessage() { coreContext.doOnCoreQueue { _ in //val messageToReplyTo = chatMessageToReplyTo diff --git a/Linphone/Utils/PhotoPicker.swift b/Linphone/Utils/PhotoPicker.swift index 6811d85b7..5bc4befb9 100644 --- a/Linphone/Utils/PhotoPicker.swift +++ b/Linphone/Utils/PhotoPicker.swift @@ -79,7 +79,7 @@ struct PhotoPicker: UIViewControllerRepresentable { let dataResult = try Data(contentsOf: urlFile!) let urlImage = self.saveMedia(name: urlFile!.lastPathComponent, data: dataResult, type: .image) if urlImage != nil { - let attachment = Attachment(id: UUID().uuidString, url: urlImage!, type: .image) + let attachment = Attachment(id: UUID().uuidString, name: urlFile!.lastPathComponent, url: urlImage!, type: .image) medias.append(attachment) } } catch { @@ -98,7 +98,7 @@ struct PhotoPicker: UIViewControllerRepresentable { let urlThumbnail = getURLThumbnail(name: urlFile!.lastPathComponent) if urlImage != nil { - let attachment = Attachment(id: UUID().uuidString, thumbnail: urlThumbnail, full: urlImage!, type: .video) + let attachment = Attachment(id: UUID().uuidString, name: urlFile!.lastPathComponent, thumbnail: urlThumbnail, full: urlImage!, type: .video) medias.append(attachment) } } catch {