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 {