Replace ChatRoom class with ConversationModel to update conversation views

This commit is contained in:
Benoit Martins 2024-02-27 17:23:25 +01:00
parent 4196fed865
commit be09968a31
18 changed files with 520 additions and 369 deletions

View file

@ -16,6 +16,7 @@
66C491FF2B24D4AC00CEA16D /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FE2B24D4AC00CEA16D /* FileUtils.swift */; };
66C492012B24DB6900CEA16D /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C492002B24DB6900CEA16D /* Log.swift */; };
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */; };
D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */; };
D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */; };
D70A26F02B7D02E6006CC8FC /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26EF2B7D02E6006CC8FC /* ConversationViewModel.swift */; };
D70A26F22B7F5D95006CC8FC /* ConversationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26F12B7F5D95006CC8FC /* ConversationFragment.swift */; };
@ -118,6 +119,7 @@
66C491FE2B24D4AC00CEA16D /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
66C492002B24DB6900CEA16D /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = "<group>"; };
D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListBottomSheet.swift; sourceTree = "<group>"; };
D70A26EF2B7D02E6006CC8FC /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; };
D70A26F12B7F5D95006CC8FC /* ConversationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationFragment.swift; sourceTree = "<group>"; };
@ -252,6 +254,14 @@
path = Pods;
sourceTree = "<group>";
};
D70959EF2B8DF33B0014AC0B /* Model */ = {
isa = PBXGroup;
children = (
D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */,
);
path = Model;
sourceTree = "<group>";
};
D717071C2AC591EF0037746F /* Utils */ = {
isa = PBXGroup;
children = (
@ -542,6 +552,7 @@
isa = PBXGroup;
children = (
D7CEE0392B7A232200FD79B7 /* Fragments */,
D70959EF2B8DF33B0014AC0B /* Model */,
D7CEE0362B7A212C00FD79B7 /* ViewModel */,
D7CEE0342B7A210300FD79B7 /* ConversationsView.swift */,
);
@ -743,6 +754,7 @@
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
D70A26F22B7F5D95006CC8FC /* ConversationFragment.swift in Sources */,
66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */,
D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */,
D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */,
D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */,
D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */,

View file

@ -380,7 +380,7 @@ struct CallView: View {
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, withPresence: false)
: ContactAvatarModel(friend: nil, name: "", withPresence: false)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {

View file

@ -226,7 +226,7 @@ struct CallsListFragment: View {
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, withPresence: false)
: ContactAvatarModel(friend: nil, name: "", withPresence: false)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {

View file

@ -286,7 +286,7 @@ struct ContactInnerFragment: View {
#Preview {
ContactInnerFragment(
contactAvatarModel: ContactAvatarModel(friend: nil, withPresence: true),
contactAvatarModel: ContactAvatarModel(friend: nil, name: "", withPresence: true),
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
isShowDeletePopup: .constant(false),

View file

@ -154,7 +154,9 @@ struct EditContactFragment: View {
&& editContactViewModel.selectedEditFriend!.photo != nil
&& !editContactViewModel.selectedEditFriend!.photo!.isEmpty && selectedImage == nil && !removedImage {
Avatar(contactAvatarModel: ContactAvatarModel(friend: editContactViewModel.selectedEditFriend!, withPresence: false), avatarSize: 100)
Avatar(contactAvatarModel:
ContactAvatarModel(friend: editContactViewModel.selectedEditFriend!, name: editContactViewModel.selectedEditFriend?.name ?? "", withPresence: false), avatarSize: 100
)
} else if selectedImage == nil {
Image("profil-picture-default")

View file

@ -25,6 +25,8 @@ class ContactAvatarModel: ObservableObject {
let friend: Friend?
let name: String
let withPresence: Bool?
@Published var lastPresenceInfo: String
@ -33,8 +35,9 @@ class ContactAvatarModel: ObservableObject {
private var friendSuscription: AnyCancellable?
init(friend: Friend?, withPresence: Bool?) {
init(friend: Friend?, name: String, withPresence: Bool?) {
self.friend = friend
self.name = name
self.withPresence = withPresence
if friend != nil &&
withPresence == true {

View file

@ -62,6 +62,9 @@ struct ContentView: View {
@State var isShowCallsListFragment = false
var body: some View {
let pub = NotificationCenter.default
.publisher(for: NSNotification.Name("ContactLoaded"))
GeometryReader { geometry in
VStack(spacing: 0) {
if telecomManager.callInProgress && !fullscreenVideo && ((!telecomManager.callDisplayed && callViewModel.calls.count == 1) || callViewModel.calls.count > 1) {
@ -644,7 +647,7 @@ struct ContentView: View {
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, withPresence: false)
: ContactAvatarModel(friend: nil, name: "", withPresence: false)
if contactAvatarModel != nil {
HistoryContactFragment(
@ -866,6 +869,12 @@ struct ContentView: View {
.zIndex(3)
}
}
.onAppear {
MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
}
.onReceive(pub) { _ in
conversationsListViewModel.refreshContactAvatarModel()
}
}
.overlay {
if isMenuOpen {

View file

@ -60,99 +60,14 @@ struct ConversationFragment: View {
}
}
let addressFriend =
(conversationViewModel.displayedConversation!.participants.first != nil && conversationViewModel.displayedConversation!.participants.first!.address != nil)
? contactsManager.getFriendWithAddress(address: conversationViewModel.displayedConversation!.participants.first!.address!)
: nil
let contactAvatarModel = addressFriend != nil
? ContactsManager.shared.avatarListModel.first(where: {
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, withPresence: false)
if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationViewModel.displayedConversation!) {
Image(uiImage: contactsManager.textToImage(
firstName: conversationViewModel.displayedConversation!.subject!,
lastName: conversationViewModel.displayedConversation!.subject!.components(separatedBy: " ").count > 1
? conversationViewModel.displayedConversation!.subject!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
Avatar(contactAvatarModel: conversationViewModel.displayedConversation!.avatarModel, avatarSize: 50)
.padding(.top, 4)
} else if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 50)
.padding(.top, 4)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
.padding(.top, 4)
}
} else {
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: 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)
.clipShape(Circle())
.padding(.top, 4)
} else {
Image(uiImage: contactsManager.textToImage(
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)
.clipShape(Circle())
.padding(.top, 4)
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
.padding(.top, 4)
}
}
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)
.lineLimit(1)
} else if addressFriend != nil {
Text(addressFriend!.name!)
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 4)
.lineLimit(1)
} else {
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)
.lineLimit(1)
}
}
Text(conversationViewModel.displayedConversation!.subject)
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 4)
.lineLimit(1)
Spacer()
@ -238,7 +153,7 @@ struct ConversationFragment: View {
.scaleEffect(x: 1, y: -1, anchor: .center)
.listRowInsets(EdgeInsets(top: 2, leading: 10, bottom: 2, trailing: 10))
.listRowSeparator(.hidden)
.transition(.move(edge: .top))
.transition(.move(edge: .top))
}
}
.scaleEffect(x: 1, y: -1, anchor: .center)

View file

@ -97,7 +97,7 @@ struct ConversationsListBottomSheet: View {
Button {
if conversationsListViewModel.selectedConversation != nil {
conversationsListViewModel.objectWillChange.send()
conversationsListViewModel.selectedConversation!.muted.toggle()
conversationsListViewModel.selectedConversation!.toggleMute()
}
if #available(iOS 16.0, *) {
@ -113,13 +113,13 @@ struct ConversationsListBottomSheet: View {
}
} label: {
HStack {
Image(conversationsListViewModel.selectedConversation!.muted ? "bell" : "bell-slash")
Image(conversationsListViewModel.selectedConversation!.isMuted ? "bell" : "bell-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text(conversationsListViewModel.selectedConversation!.muted ? "Réactiver les notifications" : "Mettre en sourdine")
Text(conversationsListViewModel.selectedConversation!.isMuted ? "Réactiver les notifications" : "Mettre en sourdine")
.default_text_style(styleSize: 16)
Spacer()
}
@ -134,12 +134,10 @@ struct ConversationsListBottomSheet: View {
.frame(maxWidth: .infinity)
if conversationsListViewModel.selectedConversation != nil
&& conversationsListViewModel.selectedConversation!.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue) {
&& !conversationsListViewModel.selectedConversation!.isGroup {
Button {
if conversationsListViewModel.selectedConversation!.participants.first != nil {
TelecomManager.shared.doCallWithCore(
addr: conversationsListViewModel.selectedConversation!.participants.first!.address!, isVideo: false
)
if !conversationsListViewModel.selectedConversation!.isGroup {
conversationsListViewModel.selectedConversation!.call()
}
if #available(iOS 16.0, *) {
@ -178,12 +176,7 @@ struct ConversationsListBottomSheet: View {
}
Button {
if conversationsListViewModel.selectedConversation != nil {
CoreContext.shared.doOnCoreQueue { core in
core.deleteChatRoom(chatRoom: conversationsListViewModel.selectedConversation!)
//conversationsListViewModel.computeChatRoomsList(filter: "")
}
}
conversationsListViewModel.computeChatRoomsList(filter: "")
if #available(iOS 16.0, *) {
if idiom != .pad {

View file

@ -22,8 +22,6 @@ import linphonesw
struct ConversationsListFragment: View {
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject var conversationViewModel: ConversationViewModel
@ObservedObject var conversationsListViewModel: ConversationsListViewModel
@ -34,111 +32,21 @@ struct ConversationsListFragment: View {
List {
ForEach(0..<conversationsListViewModel.conversationsList.count, id: \.self) { index in
HStack {
let addressFriend =
(conversationsListViewModel.conversationsList[index].participants.first != nil && conversationsListViewModel.conversationsList[index].participants.first!.address != nil)
? contactsManager.getFriendWithAddress(address: conversationsListViewModel.conversationsList[index].participants.first!.address!)
: nil
HStack {
let contactAvatarModel = addressFriend != nil
? ContactsManager.shared.avatarListModel.first(where: {
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, withPresence: false)
if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationsListViewModel.conversationsList[index]) {
Image(uiImage: contactsManager.textToImage(
firstName: conversationsListViewModel.conversationsList[index].subject!,
lastName: conversationsListViewModel.conversationsList[index].subject!.components(separatedBy: " ").count > 1
? conversationsListViewModel.conversationsList[index].subject!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
} else if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 50)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
}
} else {
if conversationsListViewModel.conversationsList[index].participants.first != nil
&& conversationsListViewModel.conversationsList[index].participants.first!.address != nil {
if conversationsListViewModel.conversationsList[index].participants.first!.address!.displayName != nil {
Image(uiImage: contactsManager.textToImage(
firstName: conversationsListViewModel.conversationsList[index].participants.first!.address!.displayName!,
lastName: conversationsListViewModel.conversationsList[index].participants.first!.address!.displayName!.components(separatedBy: " ").count > 1
? conversationsListViewModel.conversationsList[index].participants.first!.address!.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
} else {
Image(uiImage: contactsManager.textToImage(
firstName: conversationsListViewModel.conversationsList[index].participants.first!.address!.username ?? "Username Error",
lastName: conversationsListViewModel.conversationsList[index].participants.first!.address!.username!.components(separatedBy: " ").count > 1
? conversationsListViewModel.conversationsList[index].participants.first!.address!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
}
}
Avatar(contactAvatarModel: conversationsListViewModel.conversationsList[index].avatarModel, avatarSize: 50)
VStack(spacing: 0) {
Spacer()
if LinphoneUtils.isChatRoomAGroup(chatRoom: conversationsListViewModel.conversationsList[index]) {
Text(conversationsListViewModel.conversationsList[index].subject ?? "No Subject")
.foregroundStyle(Color.grayMain2c800)
.if(conversationsListViewModel.conversationsList[index].unreadMessagesCount > 0) { view in
view.default_text_style_700(styleSize: 14)
}
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else if addressFriend != nil {
Text(addressFriend!.name!)
.foregroundStyle(Color.grayMain2c800)
.if(conversationsListViewModel.conversationsList[index].unreadMessagesCount > 0) { view in
view.default_text_style_700(styleSize: 14)
}
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else {
if conversationsListViewModel.conversationsList[index].participants.first != nil
&& conversationsListViewModel.conversationsList[index].participants.first!.address != nil {
Text(conversationsListViewModel.conversationsList[index].participants.first!.address!.displayName != nil
? conversationsListViewModel.conversationsList[index].participants.first!.address!.displayName!
: conversationsListViewModel.conversationsList[index].participants.first!.address!.username!)
.foregroundStyle(Color.grayMain2c800)
.if(conversationsListViewModel.conversationsList[index].unreadMessagesCount > 0) { view in
view.default_text_style_700(styleSize: 14)
}
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
Text(conversationsListViewModel.conversationsList[index].subject)
.foregroundStyle(Color.grayMain2c800)
.if(conversationsListViewModel.conversationsList[index].unreadMessagesCount > 0) { view in
view.default_text_style_700(styleSize: 14)
}
}
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
Text(
conversationsListViewModel.conversationsList[index].lastMessageInHistory != nil
? conversationsListViewModel.getContentTextMessage(message: conversationsListViewModel.conversationsList[index].lastMessageInHistory!)
: ""
)
Text(conversationsListViewModel.conversationsList[index].lastMessageText)
.foregroundStyle(Color.grayMain2c400)
.if(conversationsListViewModel.conversationsList[index].unreadMessagesCount > 0) { view in
view.default_text_style_700(styleSize: 14)
@ -156,8 +64,7 @@ struct ConversationsListFragment: View {
Spacer()
HStack {
if conversationsListViewModel.conversationsList[index].currentParams != nil
&& !conversationsListViewModel.conversationsList[index].currentParams!.encryptionEnabled {
if !conversationsListViewModel.conversationsList[index].encryptionEnabled {
Image("warning-circle")
.renderingMode(.template)
.resizable()
@ -174,15 +81,15 @@ struct ConversationsListFragment: View {
Spacer()
HStack {
if conversationsListViewModel.conversationsList[index].muted == false
&& !(conversationsListViewModel.conversationsList[index].lastMessageInHistory != nil
&& conversationsListViewModel.conversationsList[index].lastMessageInHistory!.isOutgoing == true)
if conversationsListViewModel.conversationsList[index].isMuted == false
&& !(!conversationsListViewModel.conversationsList[index].lastMessageText.isEmpty
&& conversationsListViewModel.conversationsList[index].lastMessageIsOutgoing == true)
&& conversationsListViewModel.conversationsList[index].unreadMessagesCount == 0 {
Text("")
.frame(width: 18, height: 18, alignment: .trailing)
}
if conversationsListViewModel.conversationsList[index].muted {
if conversationsListViewModel.conversationsList[index].isMuted {
Image("bell-slash")
.renderingMode(.template)
.resizable()
@ -190,9 +97,9 @@ struct ConversationsListFragment: View {
.frame(width: 18, height: 18, alignment: .trailing)
}
if conversationsListViewModel.conversationsList[index].lastMessageInHistory != nil
&& conversationsListViewModel.conversationsList[index].lastMessageInHistory!.isOutgoing == true {
let imageName = LinphoneUtils.getChatIconState(chatState: conversationsListViewModel.conversationsList[index].lastMessageInHistory!.state)
if !conversationsListViewModel.conversationsList[index].lastMessageText.isEmpty
&& conversationsListViewModel.conversationsList[index].lastMessageIsOutgoing == true {
let imageName = LinphoneUtils.getChatIconState(chatState: conversationsListViewModel.conversationsList[index].lastMessageState)
Image(imageName)
.renderingMode(.template)
.resizable()
@ -220,7 +127,6 @@ struct ConversationsListFragment: View {
Spacer()
}
.padding(.trailing, 10)
}
}
.buttonStyle(.borderless)
.listRowInsets(EdgeInsets(top: 6, leading: 20, bottom: 6, trailing: 20))
@ -228,7 +134,7 @@ struct ConversationsListFragment: View {
.background(.white)
.onTapGesture {
withAnimation {
conversationViewModel.displayedConversation = conversationsListViewModel.conversationsList[index]
conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationsListViewModel.conversationsList[index])
conversationViewModel.getMessage()
}
}

View file

@ -0,0 +1,231 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of Linphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import linphonesw
import Combine
class ConversationModel: ObservableObject {
private var coreContext = CoreContext.shared
private var contactsManager = ContactsManager.shared
let chatRoom: ChatRoom
let isDisabledBecauseNotSecured: Bool = false
static let TAG = "[Conversation Model]"
let id: String
let localSipUri: String
let remoteSipUri: String
let isGroup: Bool
let isReadOnly: Bool
@Published var subject: String
@Published var isComposing: Bool
@Published var lastUpdateTime: time_t
//@Published var composingLabel: String
@Published var isMuted: Bool
@Published var isEphemeral: Bool
@Published var encryptionEnabled: Bool
@Published var lastMessageText: String
@Published var lastMessageIsOutgoing: Bool
@Published var lastMessageState: Int
//@Published var dateTime: String
@Published var unreadMessagesCount: Int
@Published var avatarModel: ContactAvatarModel
//@Published var isBeingDeleted: Bool
//private let lastMessage: ChatMessage? = nil
init(chatRoom: ChatRoom) {
self.chatRoom = chatRoom
self.id = LinphoneUtils.getChatRoomId(room: chatRoom)
self.localSipUri = chatRoom.localAddress?.asStringUriOnly() ?? ""
self.remoteSipUri = chatRoom.peerAddress?.asStringUriOnly() ?? ""
self.isGroup = !chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue) && chatRoom.hasCapability(mask: ChatRoom.Capabilities.Conference.rawValue)
self.isReadOnly = chatRoom.isReadOnly
self.subject = chatRoom.subject ?? ""
self.lastUpdateTime = chatRoom.lastUpdateTime
self.isComposing = chatRoom.isRemoteComposing
//self.composingLabel = chatRoom.compo
self.isMuted = chatRoom.muted
self.isEphemeral = chatRoom.ephemeralEnabled
self.encryptionEnabled = chatRoom.currentParams != nil && chatRoom.currentParams!.encryptionEnabled
self.lastMessageText = ""
self.lastMessageIsOutgoing = false
self.lastMessageState = 0
//self.dateTime = chatRoom.date
self.unreadMessagesCount = chatRoom.unreadMessagesCount
self.avatarModel = ContactAvatarModel(friend: nil, name: "", withPresence: false)
//self.isBeingDeleted = MutableLiveData<Boolean>()
//self.lastMessage: ChatMessage? = null
getContentTextMessage()
getChatRoomSubject()
}
func leave(){
coreContext.doOnCoreQueue { _ in
self.chatRoom.leave()
}
}
func markAsRead() {
coreContext.doOnCoreQueue { _ in
self.chatRoom.markAsRead()
}
}
func toggleMute() {
coreContext.doOnCoreQueue { _ in
self.chatRoom.muted.toggle()
self.isMuted = self.chatRoom.muted
}
}
func call() {
coreContext.doOnCoreQueue { _ in
if self.chatRoom.peerAddress != nil {
TelecomManager.shared.doCallWithCore(
addr: self.chatRoom.peerAddress!, isVideo: false
)
}
}
}
func getContentTextMessage() {
coreContext.doOnCoreQueue { _ in
let lastMessage = self.chatRoom.lastMessageInHistory
if lastMessage != nil {
var fromAddressFriend = lastMessage!.fromAddress != nil
? self.contactsManager.getFriendWithAddress(address: lastMessage!.fromAddress!)?.name ?? nil
: nil
if !lastMessage!.isOutgoing && lastMessage!.chatRoom != nil && !lastMessage!.chatRoom!.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue) {
if fromAddressFriend == nil {
if lastMessage!.fromAddress!.displayName != nil {
fromAddressFriend = lastMessage!.fromAddress!.displayName! + ": "
} else if lastMessage!.fromAddress!.username != nil {
fromAddressFriend = lastMessage!.fromAddress!.username! + ": "
} else {
fromAddressFriend = ""
}
} else {
fromAddressFriend! += ": "
}
} else {
fromAddressFriend = nil
}
let lastMessageTextTmp = (fromAddressFriend ?? "")
+ (lastMessage!.contents.first(where: {$0.isText == true})?.utf8Text ?? (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name ?? ""))
let lastMessageIsOutgoingTmp = lastMessage?.isOutgoing ?? false
let lastMessageStateTmp = lastMessage?.state.rawValue ?? 0
DispatchQueue.main.async {
self.lastMessageText = lastMessageTextTmp
self.lastMessageIsOutgoing = lastMessageIsOutgoingTmp
self.lastMessageState = lastMessageStateTmp
}
}
}
}
func getChatRoomSubject() {
coreContext.doOnCoreQueue { _ in
let addressFriend =
(self.chatRoom.participants.first != nil && self.chatRoom.participants.first!.address != nil)
? self.contactsManager.getFriendWithAddress(address: self.chatRoom.participants.first!.address!)
: nil
if self.isGroup {
self.subject = self.chatRoom.subject!
} else if addressFriend != nil {
self.subject = addressFriend!.name!
} else {
if self.chatRoom.participants.first != nil
&& self.chatRoom.participants.first!.address != nil {
self.subject = self.chatRoom.participants.first!.address!.displayName != nil
? self.chatRoom.participants.first!.address!.displayName!
: self.chatRoom.participants.first!.address!.username!
}
}
let avatarModelTmp = addressFriend != nil && !self.isGroup
? ContactsManager.shared.avatarListModel.first(where: {
$0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
?? ContactAvatarModel(friend: nil, name: self.subject, withPresence: false)
: ContactAvatarModel(friend: nil, name: self.subject, withPresence: false)
DispatchQueue.main.async {
self.avatarModel = avatarModelTmp
}
}
}
func refreshAvatarModel() {
coreContext.doOnCoreQueue { _ in
let addressFriend =
(self.chatRoom.participants.first != nil && self.chatRoom.participants.first!.address != nil)
? self.contactsManager.getFriendWithAddress(address: self.chatRoom.participants.first!.address!)
: nil
if addressFriend != nil && !self.isGroup {
let avatarModelTmp = ContactsManager.shared.avatarListModel.first(where: {
$0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
}) ?? ContactAvatarModel(friend: nil, name: self.subject, withPresence: false)
DispatchQueue.main.async {
self.avatarModel = avatarModelTmp
}
}
}
}
}

View file

@ -26,7 +26,7 @@ class ConversationViewModel: ObservableObject {
private var coreContext = CoreContext.shared
@Published var displayedConversation: ChatRoom?
@Published var displayedConversation: ConversationModel?
@Published var messageText: String = ""
@ -37,14 +37,16 @@ class ConversationViewModel: ObservableObject {
init() {}
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)
})
coreContext.doOnCoreQueue { _ in
if self.displayedConversation != nil {
self.chatRoomSuscriptions.insert(self.displayedConversation?.chatRoom.publisher?.onChatMessageSent?.postOnCoreQueue { (cbValue: (chatRoom: ChatRoom, eventLog: EventLog)) in
self.getNewMessages(eventLogs: [cbValue.eventLog])
})
self.chatRoomSuscriptions.insert(self.displayedConversation?.chatRoom.publisher?.onChatMessagesReceived?.postOnMainQueue { (cbValue: (chatRoom: ChatRoom, eventLogs: [EventLog])) in
self.getNewMessages(eventLogs: cbValue.eventLogs)
})
}
}
}
@ -53,20 +55,36 @@ class ConversationViewModel: ObservableObject {
}
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))
coreContext.doOnCoreQueue { _ in
if self.displayedConversation != nil {
let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: self.conversationMessagesList.count, end: self.conversationMessagesList.count + 30)
historyEvents.reversed().forEach { eventLog in
DispatchQueue.main.async {
self.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))
//let conversationMessagesListTmp = self.conversationMessagesList
//self.conversationMessagesList = []
eventLogs.forEach { eventLog in
DispatchQueue.main.async {
/*
withAnimation {
self.conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog))
}
self.conversationMessagesList.append(contentsOf: conversationMessagesListTmp)
*/
withAnimation(.spring(duration: 2)) {
self.conversationMessagesList.insert(LinphoneCustomEventLog(eventLog: eventLog), at: 0)
}
}
}
}
@ -76,83 +94,93 @@ class ConversationViewModel: ObservableObject {
}
func sendMessage() {
//val messageToReplyTo = chatMessageToReplyTo
//val message = if (messageToReplyTo != null) {
coreContext.doOnCoreQueue { _ in
//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"
//} else {
let message = try? self.displayedConversation!.chatRoom.createEmptyMessage()
//}
let toSend = self.messageText.trimmingCharacters(in: .whitespacesAndNewlines)
if !toSend.isEmpty {
if message != nil {
message!.addUtf8TextContent(text: toSend)
}
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 (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")
DispatchQueue.main.async {
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<FileModel>()
attachments.postValue(attachmentsList)
chatMessageToReplyTo = null
*/
}
*/
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<FileModel>()
attachments.postValue(attachmentsList)
chatMessageToReplyTo = null
*/
}
func changeDisplayedChatRoom(conversationModel: ConversationModel) {
self.displayedConversation = conversationModel
}
}
struct LinphoneCustomEventLog: Hashable {

View file

@ -28,10 +28,10 @@ class ConversationsListViewModel: ObservableObject {
private var mCoreSuscriptions = Set<AnyCancellable?>()
@Published var conversationsList: [ChatRoom] = []
@Published var conversationsList: [ConversationModel] = []
@Published var unreadMessages: Int = 0
var selectedConversation: ChatRoom?
var selectedConversation: ConversationModel?
init() {
computeChatRoomsList(filter: "")
@ -43,47 +43,64 @@ class ConversationsListViewModel: ObservableObject {
let account = core.defaultAccount
let chatRooms = account?.chatRooms != nil ? account!.chatRooms : core.chatRooms
DispatchQueue.main.async {
self.conversationsList = []
chatRooms.forEach { chatRoom in
//let disabledBecauseNotSecured = (account?.isInSecureMode() == true && !chatRoom.hasCapability) ? Capabilities.Encrypted.toInt() : 0
if chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue) {
}
if filter.isEmpty {
//val model = ConversationModel(chatRoom, disabledBecauseNotSecured)
self.conversationsList.append(chatRoom)
}
/*
else {
val participants = chatRoom.participants
val found = participants.find {
// Search in address but also in contact name if exists
val model =
coreContext.contactsManager.getContactAvatarModelForAddress(it.address)
model.contactName?.contains(
filter,
ignoreCase = true
) == true || it.address.asStringUriOnly().contains(
filter,
ignoreCase = true
)
}
if (
found != null ||
chatRoom.peerAddress.asStringUriOnly().contains(filter, ignoreCase = true) ||
chatRoom.subject.orEmpty().contains(filter, ignoreCase = true)
) {
val model = ConversationModel(chatRoom, disabledBecauseNotSecured)
list.add(model)
count += 1
}
}
*/
var conversationsListTmp: [ConversationModel] = []
chatRooms.forEach { chatRoom in
//let disabledBecauseNotSecured = (account?.isInSecureMode() == true && !chatRoom.hasCapability) ? Capabilities.Encrypted.toInt() : 0
if chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue) {
}
self.updateUnreadMessagesCount()
if filter.isEmpty {
let model = ConversationModel(chatRoom: chatRoom)
conversationsListTmp.append(model)
}
/*
else {
val participants = chatRoom.participants
val found = participants.find {
// Search in address but also in contact name if exists
val model =
coreContext.contactsManager.getContactAvatarModelForAddress(it.address)
model.contactName?.contains(
filter,
ignoreCase = true
) == true || it.address.asStringUriOnly().contains(
filter,
ignoreCase = true
)
}
if (
found != null ||
chatRoom.peerAddress.asStringUriOnly().contains(filter, ignoreCase = true) ||
chatRoom.subject.orEmpty().contains(filter, ignoreCase = true)
) {
val model = ConversationModel(chatRoom, disabledBecauseNotSecured)
list.add(model)
count += 1
}
}
*/
}
if !self.conversationsList.isEmpty {
for (index, element) in conversationsListTmp.enumerated() {
if index > 0 && element.id != self.conversationsList[index].id {
DispatchQueue.main.async {
self.conversationsList[index] = element
}
}
}
DispatchQueue.main.async {
self.conversationsList[0] = conversationsListTmp.first!
}
} else {
DispatchQueue.main.async {
self.conversationsList = conversationsListTmp
}
}
self.updateUnreadMessagesCount()
}
}
@ -93,7 +110,8 @@ class ConversationsListViewModel: ObservableObject {
//Log.info("[ConversationsListViewModel] Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]")
switch cbValue.state {
case ChatRoom.State.Created:
self.addChatRoom(cbChatRoom: cbValue.chatRoom)
let model = ConversationModel(chatRoom: cbValue.chatRoom)
self.addChatRoom(cbChatRoom: model)
case ChatRoom.State.Deleted:
self.computeChatRoomsList(filter: "")
//ToastViewModel.shared.toastMessage = "toast_conversation_deleted"
@ -109,14 +127,13 @@ class ConversationsListViewModel: ObservableObject {
self.mCoreSuscriptions.insert(core.publisher?.onMessagesReceived?.postOnMainQueue { _ in
self.computeChatRoomsList(filter: "")
})
}
}
func addChatRoom(cbChatRoom: ChatRoom) {
func addChatRoom(cbChatRoom: ConversationModel) {
Log.info("[ConversationsListViewModel] Re-ordering conversations")
var sortedList: [ChatRoom] = []
var sortedList: [ConversationModel] = []
sortedList.append(cbChatRoom)
sortedList.append(contentsOf: self.conversationsList)
@ -129,7 +146,7 @@ class ConversationsListViewModel: ObservableObject {
func reorderChatRooms() {
Log.info("[ConversationsListViewModel] Re-ordering conversations")
var sortedList: [ChatRoom] = []
var sortedList: [ConversationModel] = []
sortedList.append(contentsOf: conversationsList)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
@ -204,4 +221,10 @@ class ConversationsListViewModel: ObservableObject {
return formatter.string(from: myNSDate)
}
}
func refreshContactAvatarModel() {
conversationsList.forEach { conversationModel in
conversationModel.refreshAvatarModel()
}
}
}

View file

@ -563,7 +563,7 @@ struct HistoryContactFragment: View {
#Preview {
HistoryContactFragment(
contactAvatarModel: ContactAvatarModel(friend: nil, withPresence: false),
contactAvatarModel: ContactAvatarModel(friend: nil, name: "", withPresence: false),
historyViewModel: HistoryViewModel(),
historyListViewModel: HistoryListViewModel(),
contactViewModel: ContactViewModel(),

View file

@ -48,7 +48,7 @@ struct HistoryListFragment: View {
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, withPresence: false)
: ContactAvatarModel(friend: nil, name: "", withPresence: false)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {

View file

@ -22,6 +22,8 @@ import linphonesw
struct Avatar: View {
private var contactsManager = ContactsManager.shared
@ObservedObject var contactAvatarModel: ContactAvatarModel
let avatarSize: CGFloat
@ -71,6 +73,15 @@ struct Avatar: View {
EmptyView()
}
}
} else if !contactAvatarModel.name.isEmpty {
Image(uiImage: contactsManager.textToImage(
firstName: contactAvatarModel.name,
lastName: contactAvatarModel.name.components(separatedBy: " ").count > 1
? contactAvatarModel.name.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
} else {
Image("profil-picture-default")
.resizable()

View file

@ -27,20 +27,36 @@ class LinphoneUtils: NSObject {
return !oneToOne && conference
}
public class func getChatIconState(chatState: ChatMessage.State) -> String {
public class func getChatIconState(chatState: Int) -> String {
return switch chatState {
case ChatMessage.State.Displayed, ChatMessage.State.FileTransferDone:
case ChatMessage.State.Displayed.rawValue, ChatMessage.State.FileTransferDone.rawValue:
"checks"
case ChatMessage.State.DeliveredToUser:
case ChatMessage.State.DeliveredToUser.rawValue:
"check"
case ChatMessage.State.Delivered:
case ChatMessage.State.Delivered.rawValue:
"envelope-simple"
case ChatMessage.State.NotDelivered, ChatMessage.State.FileTransferError:
case ChatMessage.State.NotDelivered.rawValue, ChatMessage.State.FileTransferError.rawValue:
"warning-circle"
case ChatMessage.State.InProgress, ChatMessage.State.FileTransferInProgress:
case ChatMessage.State.InProgress.rawValue, ChatMessage.State.FileTransferInProgress.rawValue:
"animated-in-progress"
default:
"animated-in-progress"
}
}
public class func getChatRoomId(room: ChatRoom) -> String {
return getChatRoomId(localAddress: room.localAddress!, remoteAddress: room.peerAddress!)
}
public class func getChatRoomId(localAddress: Address, remoteAddress: Address) -> String {
let localSipUri = localAddress.clone()
localSipUri!.clean()
let remoteSipUri = remoteAddress.clone()
remoteSipUri!.clean()
return getChatRoomId(localSipUri: localSipUri!.asStringUriOnly(), remoteSipUri: remoteSipUri!.asStringUriOnly())
}
public class func getChatRoomId(localSipUri: String, remoteSipUri: String) -> String {
return "\(localSipUri)#~#\(remoteSipUri)"
}
}

View file

@ -82,9 +82,11 @@ final class MagicSearchSingleton: ObservableObject {
self.contactsManager.lastSearch.forEach { searchResult in
if searchResult.friend != nil {
self.contactsManager.avatarListModel.append(ContactAvatarModel(friend: searchResult.friend!, withPresence: true))
self.contactsManager.avatarListModel.append(ContactAvatarModel(friend: searchResult.friend!, name: searchResult.friend?.name ?? "", withPresence: true))
}
}
NotificationCenter.default.post(name: NSNotification.Name("ContactLoaded"), object: nil)
}
}
}