Add conversations list

This commit is contained in:
Benoit Martins 2024-02-12 17:27:21 +01:00
parent 60d128f4f2
commit dc84803a17
17 changed files with 787 additions and 80 deletions

View file

@ -19,6 +19,7 @@
D70C93DE2AC2D0F60063CA3B /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */; };
D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071D2AC5922E0037746F /* ColorExtension.swift */; };
D71707202AC5989C0037746F /* TextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071F2AC5989C0037746F /* TextExtension.swift */; };
D7173EBE2B7A5C0A00BCC481 /* LinphoneUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */; };
D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */; };
D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABB82ABC67BF00B41C10 /* ContentView.swift */; };
D719ABBB2ABC67BF00B41C10 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D719ABBA2ABC67BF00B41C10 /* Assets.xcassets */; };
@ -80,6 +81,10 @@
D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */; };
D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C48DF32AFA66F900D938CB /* EditContactController.swift */; };
D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C48DF52AFCDF4700D938CB /* ContactInnerActionsFragment.swift */; };
D7CEE0352B7A210300FD79B7 /* ConversationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE0342B7A210300FD79B7 /* ConversationsView.swift */; };
D7CEE0382B7A214F00FD79B7 /* ConversationsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */; };
D7CEE03B2B7A234200FD79B7 /* ConversationsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */; };
D7CEE03D2B7A23B200FD79B7 /* ConversationsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */; };
D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */; };
D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */; };
D7D24D142AC1B4E800C6F35B /* NotoSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */; };
@ -112,6 +117,7 @@
D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
D717071D2AC5922E0037746F /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = "<group>"; };
D717071F2AC5989C0037746F /* TextExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextExtension.swift; sourceTree = "<group>"; };
D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneUtils.swift; sourceTree = "<group>"; };
D719ABB32ABC67BF00B41C10 /* Linphone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Linphone.app; sourceTree = BUILT_PRODUCTS_DIR; };
D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneApp.swift; sourceTree = "<group>"; };
D719ABB82ABC67BF00B41C10 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -176,6 +182,10 @@
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = "<group>"; };
D7C48DF32AFA66F900D938CB /* EditContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactController.swift; sourceTree = "<group>"; };
D7C48DF52AFCDF4700D938CB /* ContactInnerActionsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerActionsFragment.swift; sourceTree = "<group>"; };
D7CEE0342B7A210300FD79B7 /* ConversationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsView.swift; sourceTree = "<group>"; };
D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListViewModel.swift; sourceTree = "<group>"; };
D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsFragment.swift; sourceTree = "<group>"; };
D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListFragment.swift; sourceTree = "<group>"; };
D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = "<group>"; };
D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = "<group>"; };
D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = "<group>"; };
@ -248,6 +258,7 @@
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */,
D732A9082AFD235500DB42BA /* ShareSheetController.swift */,
D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */,
D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -313,6 +324,7 @@
D719ABC62ABC6F0200B41C10 /* Main */ = {
isa = PBXGroup;
children = (
D7CEE0332B7A20A400FD79B7 /* Conversations */,
D7A03FBB2ACC2D850081A588 /* Contacts */,
D74C9CFD2ACAEC150021626A /* Fragments */,
D7A03FBE2ACC2E010081A588 /* History */,
@ -518,6 +530,33 @@
path = ViewModel;
sourceTree = "<group>";
};
D7CEE0332B7A20A400FD79B7 /* Conversations */ = {
isa = PBXGroup;
children = (
D7CEE0392B7A232200FD79B7 /* Fragments */,
D7CEE0362B7A212C00FD79B7 /* ViewModel */,
D7CEE0342B7A210300FD79B7 /* ConversationsView.swift */,
);
path = Conversations;
sourceTree = "<group>";
};
D7CEE0362B7A212C00FD79B7 /* ViewModel */ = {
isa = PBXGroup;
children = (
D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
D7CEE0392B7A232200FD79B7 /* Fragments */ = {
isa = PBXGroup;
children = (
D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */,
D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */,
);
path = Fragments;
sourceTree = "<group>";
};
D7D24D0C2AC1B4C700C6F35B /* Fonts */ = {
isa = PBXGroup;
children = (
@ -676,6 +715,7 @@
files = (
D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */,
D7ADF6002AFE356400212231 /* Avatar.swift in Sources */,
D7CEE03B2B7A234200FD79B7 /* ConversationsFragment.swift in Sources */,
D71707202AC5989C0037746F /* TextExtension.swift in Sources */,
66C491F92B24D25B00CEA16D /* ConfigExtension.swift in Sources */,
D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */,
@ -722,8 +762,10 @@
D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */,
D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */,
D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */,
D7173EBE2B7A5C0A00BCC481 /* LinphoneUtils.swift in Sources */,
66C492012B24DB6900CEA16D /* Log.swift in Sources */,
D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */,
D7CEE0382B7A214F00FD79B7 /* ConversationsListViewModel.swift in Sources */,
D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */,
D7E6D0552AEBFCCE00A57AAF /* ContactsInnerFragment.swift in Sources */,
D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */,
@ -740,9 +782,11 @@
D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */,
D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */,
D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */,
D7CEE0352B7A210300FD79B7 /* ConversationsView.swift in Sources */,
D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */,
D78290B82ADD3910004AA85C /* ContactsFragment.swift in Sources */,
D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */,
D7CEE03D2B7A23B200FD79B7 /* ConversationsListFragment.swift in Sources */,
D74C9CFC2ACACF370021626A /* WelcomePage3Fragment.swift in Sources */,
D719ABCC2ABC769C00B41C10 /* AssistantView.swift in Sources */,
D7C365082AEFAB7F00FE6142 /* ContactListBottomSheet.swift in Sources */,

View file

@ -135,10 +135,12 @@ final class CoreContext: ObservableObject {
self.mCore.removeLinphoneSpec(spec: "conference")
Log.info("Removing spec 'ephemeral' from core for this version")
self.mCore.removeLinphoneSpec(spec: "ephemeral")
/*
Log.info("Removing spec 'groupchat' from core for this version")
self.mCore.removeLinphoneSpec(spec: "groupchat")
Log.info("Removing spec 'lime' from core for this version")
self.mCore.removeLinphoneSpec(spec: "lime")
*/
}
})

View file

@ -45,6 +45,7 @@ struct LinphoneApp: App {
@State private var historyListViewModel: HistoryListViewModel?
@State private var startCallViewModel: StartCallViewModel?
@State private var callViewModel: CallViewModel?
@State private var conversationsListViewModel: ConversationsListViewModel?
var body: some Scene {
WindowGroup {
@ -72,7 +73,8 @@ struct LinphoneApp: App {
historyViewModel: historyViewModel!,
historyListViewModel: historyListViewModel!,
startCallViewModel: startCallViewModel!,
callViewModel: callViewModel!
callViewModel: callViewModel!,
conversationsListViewModel: conversationsListViewModel!
)
} else {
SplashScreen()
@ -86,6 +88,7 @@ struct LinphoneApp: App {
historyListViewModel = HistoryListViewModel()
startCallViewModel = StartCallViewModel()
callViewModel = CallViewModel()
conversationsListViewModel = ConversationsListViewModel()
}
}
}

View file

@ -250,6 +250,9 @@
},
"Continue" : {
},
"Conversations" : {
},
"Copy address" : {
@ -424,6 +427,9 @@
},
"No contacts for the moment..." : {
},
"No conversation for the moment..." : {
},
"Not account yet?" : {

View file

@ -99,10 +99,10 @@ class AccountLoginViewModel: ObservableObject {
accountParams.pushNotificationConfig?.provider = "apns" + pushEnvironment
// Temporary disable these features are they are not used for 6.0 first version
accountParams.conferenceFactoryUri = nil
accountParams.conferenceFactoryAddress = nil
//accountParams.conferenceFactoryUri = nil
//accountParams.conferenceFactoryAddress = nil
accountParams.audioVideoConferenceFactoryAddress = nil
accountParams.limeServerUrl = nil
//accountParams.limeServerUrl = nil
// Now that our AccountParams is configured, we can create the Account object
let account = try core.createAccount(params: accountParams)

View file

@ -230,11 +230,11 @@ struct CallsListFragment: View {
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45)
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 50)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.frame(width: 50, height: 50)
.clipShape(Circle())
}
} else {
@ -245,7 +245,7 @@ struct CallsListFragment: View {
? callViewModel.calls[index].callLog!.remoteAddress!.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 45, height: 45)
.frame(width: 50, height: 50)
.clipShape(Circle())
} else {
@ -255,7 +255,7 @@ struct CallsListFragment: View {
? callViewModel.calls[index].callLog!.remoteAddress!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 45, height: 45)
.frame(width: 50, height: 50)
.clipShape(Circle())
}
}

View file

@ -65,11 +65,11 @@ struct ContactsListFragment: View {
if index < contactsManager.avatarListModel.count
&& contactsManager.avatarListModel[index].friend!.photo != nil
&& !contactsManager.avatarListModel[index].friend!.photo!.isEmpty {
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 45)
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 50)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.frame(width: 50, height: 50)
.clipShape(Circle())
}
Text((contactsManager.lastSearch[index].friend?.name)!)

View file

@ -38,11 +38,11 @@ struct FavoriteContactsListFragment: View {
VStack {
if contactsManager.lastSearch[index].friend!.photo != nil
&& !contactsManager.lastSearch[index].friend!.photo!.isEmpty {
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 45)
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 50)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.frame(width: 50, height: 50)
.clipShape(Circle())
}
Text((contactsManager.lastSearch[index].friend?.name)!)

View file

@ -40,6 +40,7 @@ struct ContentView: View {
@ObservedObject var historyListViewModel: HistoryListViewModel
@ObservedObject var startCallViewModel: StartCallViewModel
@ObservedObject var callViewModel: CallViewModel
@ObservedObject var conversationsListViewModel: ConversationsListViewModel
@State var index = 0
@State private var orientation = UIDevice.current.orientation
@ -128,6 +129,53 @@ struct ContentView: View {
})
Spacer()
ZStack {
if conversationsListViewModel.unreadMessages > 0 {
VStack {
HStack {
Text(
conversationsListViewModel.unreadMessages < 99
? String(conversationsListViewModel.unreadMessages)
: "99+"
)
.foregroundStyle(.white)
.default_text_style(styleSize: 10)
.lineLimit(1)
}
.frame(width: 18, height: 18)
.background(Color.redDanger500)
.cornerRadius(50)
}
.padding(.bottom, 30)
.padding(.leading, 30)
}
Button(action: {
self.index = 2
historyViewModel.displayedCall = nil
contactViewModel.indexDisplayedFriend = nil
}, label: {
VStack {
Image("chat-teardrop-text")
.renderingMode(.template)
.resizable()
.foregroundStyle(self.index == 2 ? Color.orangeMain500 : Color.grayMain2c600)
.frame(width: 25, height: 25)
if self.index == 2 {
Text("Conversations")
.default_text_style_700(styleSize: 10)
} else {
Text("Conversations")
.default_text_style(styleSize: 10)
}
}
})
.padding(.top)
}
Spacer()
}
}
.frame(width: 75)
@ -148,7 +196,7 @@ struct ContentView: View {
openMenu()
}
Text(index == 0 ? "Contacts" : "Calls")
Text(index == 0 ? "Contacts" : (index == 1 ? "Calls" : "Conversations"))
.default_text_style_white_800(styleSize: 20)
.padding(.leading, 10)
@ -166,72 +214,75 @@ struct ContentView: View {
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.padding(.trailing, index == 2 ? 10 : 0)
Menu {
if index == 0 {
Button {
contactViewModel.indexDisplayedFriend = nil
isMenuOpen = false
magicSearch.allContact = true
MagicSearchSingleton.shared.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} label: {
HStack {
Text("See all")
Spacer()
if magicSearch.allContact {
Image("green-check")
if index != 2 {
Menu {
if index == 0 {
Button {
contactViewModel.indexDisplayedFriend = nil
isMenuOpen = false
magicSearch.allContact = true
MagicSearchSingleton.shared.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} label: {
HStack {
Text("See all")
Spacer()
if magicSearch.allContact {
Image("green-check")
.resizable()
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
}
}
Button {
contactViewModel.indexDisplayedFriend = nil
isMenuOpen = false
magicSearch.allContact = false
MagicSearchSingleton.shared.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} label: {
HStack {
Text("See Linphone contact")
Spacer()
if !magicSearch.allContact {
Image("green-check")
.resizable()
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
}
}
} else {
Button(role: .destructive) {
isMenuOpen = false
isShowDeleteAllHistoryPopup.toggle()
} label: {
HStack {
Text("Delete all history")
Spacer()
Image("trash-simple-red")
.resizable()
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
}
}
Button {
contactViewModel.indexDisplayedFriend = nil
isMenuOpen = false
magicSearch.allContact = false
MagicSearchSingleton.shared.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} label: {
HStack {
Text("See Linphone contact")
Spacer()
if !magicSearch.allContact {
Image("green-check")
.resizable()
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
}
}
} else {
Button(role: .destructive) {
isMenuOpen = false
isShowDeleteAllHistoryPopup.toggle()
} label: {
HStack {
Text("Delete all history")
Spacer()
Image("trash-simple-red")
.resizable()
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
}
} label: {
Image(index == 0 ? "funnel" : "dots-three-vertical")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.padding(.trailing, 10)
.onTapGesture {
isMenuOpen = true
}
} label: {
Image(index == 0 ? "funnel" : "dots-three-vertical")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.padding(.trailing, 10)
.onTapGesture {
isMenuOpen = true
}
}
.frame(maxWidth: .infinity)
@ -254,8 +305,10 @@ struct ContentView: View {
magicSearch.currentFilter = ""
MagicSearchSingleton.shared.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} else {
} else if index == 1 {
historyListViewModel.resetFilterCallLogs()
} else {
//TODO Conversations List reset
}
} label: {
Image("caret-left")
@ -293,8 +346,10 @@ struct ContentView: View {
magicSearch.currentFilter = newValue
MagicSearchSingleton.shared.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} else {
} else if index == 1 {
historyListViewModel.filterCallLogs(filter: text)
} else {
//TODO Conversations List Filter
}
}
} else {
@ -317,9 +372,15 @@ struct ContentView: View {
self.focusedField = true
}
.onChange(of: text) { newValue in
magicSearch.currentFilter = newValue
MagicSearchSingleton.shared.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
if index == 0 {
magicSearch.currentFilter = newValue
MagicSearchSingleton.shared.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} else if index == 1 {
historyListViewModel.filterCallLogs(filter: text)
} else {
//TODO Conversations List Filter
}
}
}
@ -360,6 +421,8 @@ struct ContentView: View {
isShowStartCallFragment: $isShowStartCallFragment,
isShowEditContactFragment: $isShowEditContactFragment
)
} else if self.index == 2 {
ConversationsView(conversationsListViewModel: conversationsListViewModel)
}
}
.frame(maxWidth:
@ -408,6 +471,7 @@ struct ContentView: View {
}
})
.padding(.top)
.frame(width: 100)
Spacer()
@ -431,6 +495,56 @@ struct ContentView: View {
}
})
.padding(.top)
.frame(width: 100)
Spacer()
ZStack {
if conversationsListViewModel.unreadMessages > 0 {
VStack {
HStack {
Text(
conversationsListViewModel.unreadMessages < 99
? String(conversationsListViewModel.unreadMessages)
: "99+"
)
.foregroundStyle(.white)
.default_text_style(styleSize: 10)
.lineLimit(1)
}
.frame(width: 18, height: 18)
.background(Color.redDanger500)
.cornerRadius(50)
}
.padding(.bottom, 30)
.padding(.leading, 30)
}
Button(action: {
self.index = 2
historyViewModel.displayedCall = nil
contactViewModel.indexDisplayedFriend = nil
}, label: {
VStack {
Image("chat-teardrop-text")
.renderingMode(.template)
.resizable()
.foregroundStyle(self.index == 2 ? Color.orangeMain500 : Color.grayMain2c600)
.frame(width: 25, height: 25)
if self.index == 2 {
Text("Conversations")
.default_text_style_700(styleSize: 10)
} else {
Text("Conversations")
.default_text_style(styleSize: 10)
}
}
})
.padding(.top)
.frame(width: 100)
}
Spacer()
}
}
@ -491,6 +605,11 @@ struct ContentView: View {
.background(Color.gray100)
.ignoresSafeArea(.keyboard)
}
} else if self.index == 2 {
ConversationsView(conversationsListViewModel: conversationsListViewModel)
.frame(maxWidth: .infinity)
.background(Color.gray100)
.ignoresSafeArea(.keyboard)
}
}
.onAppear {
@ -745,7 +864,8 @@ struct ContentView: View {
historyViewModel: HistoryViewModel(),
historyListViewModel: HistoryListViewModel(),
startCallViewModel: StartCallViewModel(),
callViewModel: CallViewModel()
callViewModel: CallViewModel(),
conversationsListViewModel: ConversationsListViewModel()
)
}
// swiftlint:enable type_body_length

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* 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 SwiftUI
struct ConversationsView: View {
@ObservedObject var conversationsListViewModel: ConversationsListViewModel
var body: some View {
NavigationView {
ZStack(alignment: .bottomTrailing) {
ConversationsFragment(conversationsListViewModel: conversationsListViewModel)
Button {
} label: {
Image("plus-circle")
.renderingMode(.template)
.foregroundStyle(.white)
.padding()
.background(Color.orangeMain500)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 4)
}
.padding()
}
}
.navigationViewStyle(.stack)
}
}
#Preview {
ConversationsListFragment(conversationsListViewModel: ConversationsListViewModel())
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* 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 SwiftUI
struct ConversationsFragment: View {
@ObservedObject var conversationsListViewModel: ConversationsListViewModel
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
var body: some View {
ZStack {
if #available(iOS 16.0, *), idiom != .pad {
ConversationsListFragment(conversationsListViewModel: conversationsListViewModel)
/*
.sheet(isPresented: $showingSheet) {
HistoryListBottomSheet(
historyViewModel: historyViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
historyListViewModel: historyListViewModel,
showingSheet: $showingSheet,
index: $index,
isShowEditContactFragment: $isShowEditContactFragment
)
.presentationDetents([.fraction(0.2)])
}
*/
} else {
ConversationsListFragment(conversationsListViewModel: conversationsListViewModel)
/*
.halfSheet(showSheet: $showingSheet) {
HistoryListBottomSheet(
historyViewModel: historyViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
historyListViewModel: historyListViewModel,
showingSheet: $showingSheet,
index: $index,
isShowEditContactFragment: $isShowEditContactFragment
)
} onDismiss: {}
*/
}
}
}
}
#Preview {
ConversationsFragment(conversationsListViewModel: ConversationsListViewModel())
}

View file

@ -0,0 +1,254 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* 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 SwiftUI
import linphonesw
struct ConversationsListFragment: View {
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject var conversationsListViewModel: ConversationsListViewModel
var body: some View {
VStack {
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())
}
}
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)
}
}
let text = conversationsListViewModel.conversationsList[index].lastMessageInHistory?.contents.first(where: {$0.isText == true})?.utf8Text ?? ""
Text(text)
.foregroundStyle(Color.grayMain2c400)
.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)
Spacer()
}
Spacer()
VStack(alignment: .trailing, spacing: 0) {
Spacer()
HStack {
if conversationsListViewModel.conversationsList[index].currentParams != nil
&& !conversationsListViewModel.conversationsList[index].currentParams!.encryptionEnabled {
Image("warning-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 18, height: 18, alignment: .trailing)
}
if conversationsListViewModel.conversationsList[index].lastMessageInHistory != nil {
Text(conversationsListViewModel.getCallTime(startDate: conversationsListViewModel.conversationsList[index].lastMessageInHistory!.time))
.foregroundStyle(Color.grayMain2c400)
.default_text_style(styleSize: 14)
.lineLimit(1)
} else {
Text(conversationsListViewModel.getCallTime(startDate: conversationsListViewModel.conversationsList[index].lastUpdateTime))
.foregroundStyle(Color.grayMain2c400)
.default_text_style(styleSize: 14)
.lineLimit(1)
}
}
Spacer()
HStack {
if conversationsListViewModel.conversationsList[index].lastMessageInHistory != nil
&& conversationsListViewModel.conversationsList[index].lastMessageInHistory!.isOutgoing == true {
let imageName = LinphoneUtils.getChatIconState(chatState: conversationsListViewModel.conversationsList[index].lastMessageInHistory!.state)
Image(imageName)
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 18, height: 18, alignment: .trailing)
}
if conversationsListViewModel.conversationsList[index].unreadMessagesCount > 0 {
HStack {
Text(
conversationsListViewModel.conversationsList[index].unreadMessagesCount < 99
? String(conversationsListViewModel.conversationsList[index].unreadMessagesCount)
: "99+"
)
.foregroundStyle(.white)
.default_text_style(styleSize: 10)
.lineLimit(1)
}
.frame(width: 18, height: 18)
.background(Color.redDanger500)
.cornerRadius(50)
}
if !(conversationsListViewModel.conversationsList[index].lastMessageInHistory != nil
&& conversationsListViewModel.conversationsList[index].lastMessageInHistory!.isOutgoing == true)
&& conversationsListViewModel.conversationsList[index].unreadMessagesCount == 0 {
Text("")
.frame(width: 18, height: 18, alignment: .trailing)
}
}
Spacer()
}
.padding(.trailing, 10)
}
}
.buttonStyle(.borderless)
.listRowInsets(EdgeInsets(top: 6, leading: 20, bottom: 6, trailing: 20))
.listRowSeparator(.hidden)
.background(.white)
.onTapGesture {
}
.onLongPressGesture(minimumDuration: 0.2) {
}
}
}
.listStyle(.plain)
.overlay(
VStack {
if conversationsListViewModel.conversationsList.isEmpty {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding(.all)
Text("No conversation for the moment...")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
}
}
.padding(.all)
)
}
.navigationTitle("")
.navigationBarHidden(true)
}
}
#Preview {
ConversationsListFragment(conversationsListViewModel: ConversationsListViewModel())
}

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* 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
class ConversationsListViewModel: ObservableObject {
private var coreContext = CoreContext.shared
@Published var conversationsList: [ChatRoom] = []
@Published var unreadMessages: Int = 0
init() {
computeChatRoomsList(filter: "")
updateUnreadMessagesCount()
}
func computeChatRoomsList(filter: String) {
coreContext.doOnCoreQueue { core in
let account = core.defaultAccount
let chatRooms = account?.chatRooms != nil ? account!.chatRooms : core.chatRooms
chatRooms.forEach { chatRoom in
//let disabledBecauseNotSecured = (account?.isInSecureMode() == true && !chatRoom.hasCapability) ? Capabilities.Encrypted.toInt() : 0
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
}
}
*/
}
}
}
func getCallTime(startDate: time_t) -> String {
let timeInterval = TimeInterval(startDate)
let myNSDate = Date(timeIntervalSince1970: timeInterval)
if Calendar.current.isDateInToday(myNSDate) {
let formatter = DateFormatter()
formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a"
return formatter.string(from: myNSDate)
} else if Calendar.current.isDateInYesterday(myNSDate) {
let formatter = DateFormatter()
formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a"
return "Yesterday"
} else if Calendar.current.isDate(myNSDate, equalTo: .now, toGranularity: .year) {
let formatter = DateFormatter()
formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM" : "MM/dd"
return formatter.string(from: myNSDate)
} else {
let formatter = DateFormatter()
formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM/yy" : "MM/dd/yy"
return formatter.string(from: myNSDate)
}
}
func updateUnreadMessagesCount() {
coreContext.doOnCoreQueue { core in
let account = core.defaultAccount
if account != nil {
let count = account?.unreadChatMessageCount != nil ? account!.unreadChatMessageCount : core.unreadChatMessageCount
self.unreadMessages = count
} else {
self.unreadMessages = 0
}
}
}
}

View file

@ -52,7 +52,7 @@ struct HistoryListFragment: View {
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45)
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 50)
} else {
Image("profil-picture-default")
.resizable()

View file

@ -29,6 +29,7 @@ class HistoryListViewModel: ObservableObject {
var callLogsAddressToDelete = ""
var callLogSubscription: AnyCancellable?
init() {
computeCallLogsList()
}

View file

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

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* 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
class LinphoneUtils: NSObject {
public class func isChatRoomAGroup(chatRoom: ChatRoom) -> Bool {
let oneToOne = chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue)
let conference = chatRoom.hasCapability(mask: ChatRoom.Capabilities.Conference.rawValue)
return !oneToOne && conference
}
public class func getChatIconState(chatState: ChatMessage.State) -> String {
return switch chatState {
case ChatMessage.State.Displayed, ChatMessage.State.FileTransferDone:
"checks"
case ChatMessage.State.DeliveredToUser:
"check"
case ChatMessage.State.Delivered:
"envelope-simple"
case ChatMessage.State.NotDelivered, ChatMessage.State.FileTransferError:
"warning-circle"
case ChatMessage.State.InProgress, ChatMessage.State.FileTransferInProgress:
"animated-in-progress"
default:
"animated-in-progress"
}
}
}