Add reaction feature

This commit is contained in:
Benoit Martins 2024-07-19 16:20:01 +02:00
parent 6742904342
commit 5c82815644
13 changed files with 305 additions and 75 deletions

View file

@ -1570,6 +1570,9 @@
},
"Message" : {
},
"Message copied into clipboard" : {
},
"Messages" : {

View file

@ -625,7 +625,7 @@ struct CallView: View {
)
DispatchQueue.main.async {
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
ToastViewModel.shared.displayToast = true
}
}, label: {

View file

@ -69,7 +69,7 @@ struct ContactListBottomSheet: View {
dismiss()
}
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
ToastViewModel.shared.displayToast.toggle()
} label: {

View file

@ -127,6 +127,7 @@ struct ContentView: View {
self.index = 0
historyViewModel.displayedCall = nil
conversationViewModel.displayedConversation = nil
meetingViewModel.displayedMeeting = nil
}, label: {
VStack {
Image("address-book")
@ -171,6 +172,7 @@ struct ContentView: View {
self.index = 1
contactViewModel.indexDisplayedFriend = nil
conversationViewModel.displayedConversation = nil
meetingViewModel.displayedMeeting = nil
if historyListViewModel.missedCallsCount > 0 {
historyListViewModel.resetMissedCallsCount()
}
@ -219,6 +221,7 @@ struct ContentView: View {
self.index = 2
historyViewModel.displayedCall = nil
contactViewModel.indexDisplayedFriend = nil
meetingViewModel.displayedMeeting = nil
}, label: {
VStack {
Image("chat-teardrop-text")
@ -275,6 +278,11 @@ struct ContentView: View {
}
VStack(spacing: 0) {
Rectangle()
.foregroundColor(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 1)
if searchIsActive == false {
HStack {
Image("profile-image-example")
@ -392,6 +400,7 @@ struct ContentView: View {
.padding(.top, 2.5)
.padding(.bottom, 2.5)
.background(Color.orangeMain500)
.roundedCorner(10, corners: [.bottomRight, .bottomLeft])
} else {
HStack {
Button {
@ -523,6 +532,7 @@ struct ContentView: View {
.padding(.horizontal)
.padding(.bottom, 5)
.background(Color.orangeMain500)
.roundedCorner(10, corners: [.bottomRight, .bottomLeft])
}
if self.index == 0 {
@ -592,6 +602,7 @@ struct ContentView: View {
self.index = 0
historyViewModel.displayedCall = nil
conversationViewModel.displayedConversation = nil
meetingViewModel.displayedMeeting = nil
}, label: {
VStack {
Image("address-book")
@ -638,6 +649,7 @@ struct ContentView: View {
self.index = 1
contactViewModel.indexDisplayedFriend = nil
conversationViewModel.displayedConversation = nil
meetingViewModel.displayedMeeting = nil
if historyListViewModel.missedCallsCount > 0 {
historyListViewModel.resetMissedCallsCount()
}
@ -688,6 +700,7 @@ struct ContentView: View {
self.index = 2
historyViewModel.displayedCall = nil
contactViewModel.indexDisplayedFriend = nil
meetingViewModel.displayedMeeting = nil
}, label: {
VStack {
Image("chat-teardrop-text")

View file

@ -70,52 +70,89 @@ struct ChatBubbleView: View {
}
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(.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..<message.reactions.count, id: \.self) { index in
if message.reactions.firstIndex(of: message.reactions[index]) == index {
Text(message.reactions[index])
.default_text_style(styleSize: 14)
.padding(.horizontal, -2)
}
}
if (
(message.reactions.contains("👍") ? 1 : 0) +
(message.reactions.contains("❤️") ? 1 : 0) +
(message.reactions.contains("😂") ? 1 : 0) +
(message.reactions.contains("😮") ? 1 : 0) +
(message.reactions.contains("😢") ? 1 : 0)
) != message.reactions.count {
Text("\(message.reactions.count)")
.default_text_style(styleSize: 14)
.padding(.horizontal, -2)
}
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(message.isOutgoing ? Color.orangeMain100 : Color.grayMain2c100)
.cornerRadius(20)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(.white, lineWidth: 3)
)
.padding(.top, -20)
.padding(message.isOutgoing ? .trailing : .leading, 5)
}
.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.isOutgoing {
Spacer()
}
}
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
}

View file

@ -18,6 +18,7 @@
*/
import SwiftUI
import UniformTypeIdentifiers
// swiftlint:disable type_body_length
struct ConversationFragment: View {
@ -538,7 +539,7 @@ struct ConversationFragment: View {
.blur(radius: conversationViewModel.selectedMessage != nil ? 8 : 0)
if conversationViewModel.selectedMessage != nil && conversationViewModel.displayedConversation != nil {
let iconSize = ((geometry.size.width - (conversationViewModel.displayedConversation!.isGroup ? 43 : 10) - 10) / 6) - 25
let iconSize = ((geometry.size.width - (conversationViewModel.displayedConversation!.isGroup ? 43 : 10) - 10) / 6) - 30
VStack {
Spacer()
@ -550,39 +551,54 @@ struct ConversationFragment: View {
HStack {
Button {
conversationViewModel.sendReaction(emoji: "👍")
} label: {
Text("👍")
.default_text_style(styleSize: iconSize > 50 ? 50 : iconSize)
}
.padding(.horizontal, 5)
.padding(.horizontal, 8)
.background(conversationViewModel.selectedMessage?.ownReaction == "👍" ? Color.gray200 : .white)
.cornerRadius(10)
Button {
conversationViewModel.sendReaction(emoji: "❤️")
} label: {
Text("❤️")
.default_text_style(styleSize: iconSize > 50 ? 50 : iconSize)
}
.padding(.horizontal, 5)
.padding(.horizontal, 8)
.background(conversationViewModel.selectedMessage?.ownReaction == "❤️" ? Color.gray200 : .white)
.cornerRadius(10)
Button {
conversationViewModel.sendReaction(emoji: "😂")
} label: {
Text("😂")
.default_text_style(styleSize: iconSize > 50 ? 50 : iconSize)
}
.padding(.horizontal, 5)
.padding(.horizontal, 8)
.background(conversationViewModel.selectedMessage?.ownReaction == "😂" ? Color.gray200 : .white)
.cornerRadius(10)
Button {
conversationViewModel.sendReaction(emoji: "😮")
} label: {
Text("😮")
.default_text_style(styleSize: iconSize > 50 ? 50 : iconSize)
}
.padding(.horizontal, 5)
.padding(.horizontal, 8)
.background(conversationViewModel.selectedMessage?.ownReaction == "😮" ? Color.gray200 : .white)
.cornerRadius(10)
Button {
conversationViewModel.sendReaction(emoji: "😢")
} label: {
Text("😢")
.default_text_style(styleSize: iconSize > 50 ? 50 : iconSize)
}
.padding(.horizontal, 5)
.padding(.horizontal, 8)
.background(conversationViewModel.selectedMessage?.ownReaction == "😢" ? Color.gray200 : .white)
.cornerRadius(10)
Button {
} label: {
@ -635,22 +651,33 @@ struct ConversationFragment: View {
Divider()
Button {
} label: {
HStack {
Text("menu_copy_chat_message")
.default_text_style(styleSize: 15)
Spacer()
Image("copy")
.resizable()
.frame(width: 20, height: 20, alignment: .leading)
if !conversationViewModel.selectedMessage!.text.isEmpty {
Button {
UIPasteboard.general.setValue(
conversationViewModel.selectedMessage!.text,
forPasteboardType: UTType.plainText.identifier
)
ToastViewModel.shared.toastMessage = "Success_message_copied_into_clipboard"
ToastViewModel.shared.displayToast = true
conversationViewModel.selectedMessage = nil
} label: {
HStack {
Text("menu_copy_chat_message")
.default_text_style(styleSize: 15)
Spacer()
Image("copy")
.resizable()
.frame(width: 20, height: 20, alignment: .leading)
}
.padding(.vertical, 5)
.padding(.horizontal, 20)
}
.padding(.vertical, 5)
.padding(.horizontal, 20)
Divider()
}
Divider()
Button {
} label: {
HStack {
@ -698,8 +725,6 @@ struct ConversationFragment: View {
.padding(.leading, conversationViewModel.displayedConversation!.isGroup ? 43 : 0)
.shadow(color: .black.opacity(0.1), radius: 10)
}
Spacer()
}
.frame(maxWidth: .infinity)
.background(.gray.opacity(0.1))

View file

@ -73,6 +73,8 @@ public struct Message: Identifiable, Hashable {
public var attachments: [Attachment]
public var recording: Recording?
public var replyMessage: ReplyMessage?
public var ownReaction: String
public var reactions: [String]
public init(
id: String,
@ -85,7 +87,9 @@ public struct Message: Identifiable, Hashable {
text: String = "",
attachments: [Attachment] = [],
recording: Recording? = nil,
replyMessage: ReplyMessage? = nil
replyMessage: ReplyMessage? = nil,
ownReaction: String = "",
reactions: [String] = []
) {
self.id = id
self.status = status
@ -98,6 +102,8 @@ public struct Message: Identifiable, Hashable {
self.attachments = attachments
self.recording = recording
self.replyMessage = replyMessage
self.ownReaction = ownReaction
self.reactions = reactions
}
public static func makeMessage(
@ -131,7 +137,9 @@ public struct Message: Identifiable, Hashable {
text: draft.text,
attachments: attachments,
recording: draft.recording,
replyMessage: draft.replyMessage
replyMessage: draft.replyMessage,
ownReaction: draft.ownReaction,
reactions: draft.reactions
)
}
}
@ -144,7 +152,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.id == rhs.id && lhs.status == rhs.status && lhs.isFirstMessage == rhs.isFirstMessage && lhs.ownReaction == rhs.ownReaction && lhs.reactions == rhs.reactions
}
}
@ -217,6 +225,8 @@ public struct DraftMessage {
public let recording: Recording?
public let replyMessage: ReplyMessage?
public let createdAt: Date
public let ownReaction: String
public let reactions: [String]
public init(id: String? = nil,
isOutgoing: Bool,
@ -227,7 +237,10 @@ public struct DraftMessage {
medias: [Media],
recording: Recording?,
replyMessage: ReplyMessage?,
createdAt: Date) {
createdAt: Date,
ownReaction: String,
reactions: [String]
) {
self.id = id
self.isOutgoing = isOutgoing
self.dateReceived = dateReceived
@ -238,6 +251,8 @@ public struct DraftMessage {
self.recording = recording
self.replyMessage = replyMessage
self.createdAt = createdAt
self.ownReaction = ownReaction
self.reactions = reactions
}
}

View file

@ -35,6 +35,7 @@ class ConversationViewModel: ObservableObject {
@Published var messageText: String = ""
private var chatRoomSuscriptions = Set<AnyCancellable?>()
private var chatMessageSuscriptions = Set<AnyCancellable?>()
@Published var conversationMessagesSection: [MessagesSection] = []
@Published var participantConversationModel: [ContactAvatarModel] = []
@ -60,8 +61,73 @@ class ConversationViewModel: ObservableObject {
}
}
func addChatMessageDelegate(message: ChatMessage) {
coreContext.doOnCoreQueue { _ in
if self.displayedConversation != nil {
/*
self.chatMessageSuscriptions.insert(message.publisher?.onMsgStateChanged?.postOnCoreQueue {(cbValue: (message: ChatMessage, state: ChatMessage.State)) in
var statusTmp: Message.Status? = .sending
switch cbValue.message.state {
case .InProgress:
statusTmp = .sending
case .Delivered:
statusTmp = .sent
case .DeliveredToUser:
statusTmp = .received
case .Displayed:
statusTmp = .read
default:
statusTmp = nil
}
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.id == message.messageId})
DispatchQueue.main.async {
if indexMessage != nil {
self.objectWillChange.send()
self.conversationMessagesSection[0].rows[indexMessage!].status = statusTmp
}
}
})
*/
self.chatMessageSuscriptions.insert(message.publisher?.onNewMessageReaction?.postOnCoreQueue {(cbValue: (message: ChatMessage, reaction: ChatMessageReaction)) in
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.id == message.messageId})
var reactionsTmp: [String] = []
cbValue.message.reactions.forEach({ chatMessageReaction in
reactionsTmp.append(chatMessageReaction.body)
})
DispatchQueue.main.async {
if indexMessage != nil {
self.objectWillChange.send()
self.conversationMessagesSection[0].rows[indexMessage!].reactions = reactionsTmp
}
}
})
self.chatMessageSuscriptions.insert(message.publisher?.onReactionRemoved?.postOnCoreQueue {(cbValue: (message: ChatMessage, address: Address)) in
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.id == message.messageId})
var reactionsTmp: [String] = []
cbValue.message.reactions.forEach({ chatMessageReaction in
reactionsTmp.append(chatMessageReaction.body)
})
DispatchQueue.main.async {
if indexMessage != nil {
self.objectWillChange.send()
self.conversationMessagesSection[0].rows[indexMessage!].reactions = reactionsTmp
}
}
})
}
}
}
func removeConversationDelegate() {
self.chatRoomSuscriptions.removeAll()
self.chatMessageSuscriptions.removeAll()
}
func getHistorySize() {
@ -210,19 +276,28 @@ class ConversationViewModel: ObservableObject {
statusTmp = nil
}
var reactionsTmp: [String] = []
eventLog.chatMessage?.reactions.forEach({ chatMessageReaction in
reactionsTmp.append(chatMessageReaction.body)
})
if eventLog.chatMessage != nil {
conversationMessage.append(
Message(
id: UUID().uuidString,
id: eventLog.chatMessage?.messageId ?? UUID().uuidString,
status: statusTmp,
isOutgoing: eventLog.chatMessage?.isOutgoing ?? false,
dateReceived: eventLog.chatMessage?.time ?? 0,
address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp,
text: contentText,
attachments: attachmentList
attachments: attachmentList,
ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "",
reactions: reactionsTmp
)
)
self.addChatMessageDelegate(message: eventLog.chatMessage!)
}
}
@ -324,19 +399,28 @@ class ConversationViewModel: ObservableObject {
statusTmp = nil
}
var reactionsTmp: [String] = []
eventLog.chatMessage?.reactions.forEach({ chatMessageReaction in
reactionsTmp.append(chatMessageReaction.body)
})
if eventLog.chatMessage != nil {
conversationMessagesTmp.insert(
Message(
id: UUID().uuidString,
id: eventLog.chatMessage?.messageId ?? UUID().uuidString,
status: statusTmp,
isOutgoing: eventLog.chatMessage?.isOutgoing ?? false,
dateReceived: eventLog.chatMessage?.time ?? 0,
address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp,
text: contentText,
attachments: attachmentList
attachments: attachmentList,
ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "",
reactions: reactionsTmp
), at: 0
)
self.addChatMessageDelegate(message: eventLog.chatMessage!)
}
}
@ -451,18 +535,27 @@ class ConversationViewModel: ObservableObject {
statusTmp = nil
}
var reactionsTmp: [String] = []
eventLog.chatMessage?.reactions.forEach({ chatMessageReaction in
reactionsTmp.append(chatMessageReaction.body)
})
if eventLog.chatMessage != nil {
let message = Message(
id: UUID().uuidString,
id: eventLog.chatMessage?.messageId ?? UUID().uuidString,
status: statusTmp,
isOutgoing: eventLog.chatMessage?.isOutgoing ?? false,
dateReceived: eventLog.chatMessage?.time ?? 0,
address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp,
text: contentText,
attachments: attachmentList
attachments: attachmentList,
ownReaction: eventLog.chatMessage?.ownReaction?.body ?? "",
reactions: reactionsTmp
)
self.addChatMessageDelegate(message: eventLog.chatMessage!)
DispatchQueue.main.async {
if !self.conversationMessagesSection.isEmpty
&& !self.conversationMessagesSection[0].rows.isEmpty
@ -729,6 +822,43 @@ class ConversationViewModel: ObservableObject {
}
}
func sendReaction(emoji: String) {
coreContext.doOnCoreQueue { _ in
if self.selectedMessage != nil {
Log.info("[ConversationViewModel] Sending reaction \(emoji) to message with ID \(self.selectedMessage!.id)")
let messageToSendReaction = self.displayedConversation!.chatRoom.findMessage(messageId: self.selectedMessage!.id)
if messageToSendReaction != nil {
do {
let reaction = try messageToSendReaction!.createReaction(utf8Reaction: messageToSendReaction?.ownReaction?.body == emoji ? "" : emoji)
reaction.send()
let indexMessageSelected = self.conversationMessagesSection[0].rows.firstIndex(of: self.selectedMessage!)
DispatchQueue.main.async {
if indexMessageSelected != nil {
self.conversationMessagesSection[0].rows[indexMessageSelected!].ownReaction = messageToSendReaction?.ownReaction?.body == emoji ? "" : emoji
}
self.selectedMessage = nil
}
} catch {
Log.info("[ConversationViewModel] Error: Can't send reaction \(emoji) to message with ID \(self.selectedMessage!.id)")
}
}
}
}
}
func resend() {
coreContext.doOnCoreQueue { _ in
if self.selectedMessage != nil {
Log.info("[ConversationViewModel] Re-sending message with ID \(self.selectedMessage!.id)")
let messageToResend = self.displayedConversation!.chatRoom.findMessage(messageId: self.selectedMessage!.id)
if messageToResend != nil {
messageToResend!.send()
}
}
}
}
}
struct LinphoneCustomEventLog: Hashable {
var id = UUID()

View file

@ -80,13 +80,20 @@ struct ToastView: View {
.default_text_style(styleSize: 15)
.padding(8)
case "Success_copied_into_clipboard":
case "Success_address_copied_into_clipboard":
Text("SIP address copied into clipboard")
.multilineTextAlignment(.center)
.foregroundStyle(Color.greenSuccess500)
.default_text_style(styleSize: 15)
.padding(8)
case "Success_message_copied_into_clipboard":
Text("Message copied into clipboard")
.multilineTextAlignment(.center)
.foregroundStyle(Color.greenSuccess500)
.default_text_style(styleSize: 15)
.padding(8)
case "Info_call_securised":
Text("call_can_be_trusted_toast")
.multilineTextAlignment(.center)

View file

@ -135,7 +135,7 @@ struct HistoryContactFragment: View {
)
}
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
ToastViewModel.shared.displayToast.toggle()
} label: {

View file

@ -155,7 +155,7 @@ struct HistoryListBottomSheet: View {
dismiss()
}
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
ToastViewModel.shared.displayToast.toggle()
} label: {

View file

@ -185,7 +185,7 @@ struct MeetingFragment: View {
)
DispatchQueue.main.async {
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
ToastViewModel.shared.displayToast = true
}
}, label: {

View file

@ -58,8 +58,8 @@ struct Avatar: View {
Image(contactAvatarModel.presenceStatus == .Online ? "presence-online" : "presence-busy")
.resizable()
.frame(width: avatarSize/4, height: avatarSize/4)
.padding(.trailing, avatarSize == 50 ? 1 : 3)
.padding(.bottom, avatarSize == 50 ? 1 : 3)
.padding(.trailing, avatarSize == 50 || avatarSize == 35 ? 1 : 3)
.padding(.bottom, avatarSize == 50 || avatarSize == 35 ? 1 : 3)
}
}
}