Add history call list

This commit is contained in:
Benoit Martins 2023-11-15 16:18:06 +01:00
parent ae1ea15558
commit ce9f6c454c
17 changed files with 978 additions and 100 deletions

View file

@ -29,6 +29,10 @@
D72343362AD037AF009AA24E /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343352AD037AF009AA24E /* ToastView.swift */; };
D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */; };
D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9082AFD235500DB42BA /* ShareSheetController.swift */; };
D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */; };
D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */; };
D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */; };
D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */; };
D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; };
D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; };
D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */; };
@ -95,6 +99,10 @@
D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = "<group>"; };
D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = "<group>"; };
D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryFragment.swift; sourceTree = "<group>"; };
D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListFragment.swift; sourceTree = "<group>"; };
D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListViewModel.swift; sourceTree = "<group>"; };
D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListBottomSheet.swift; sourceTree = "<group>"; };
D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = "<group>"; };
D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = "<group>"; };
D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = "<group>"; };
@ -267,6 +275,7 @@
isa = PBXGroup;
children = (
D72250622ADE9615008FB426 /* HistoryViewModel.swift */,
D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -275,6 +284,9 @@
isa = PBXGroup;
children = (
D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */,
D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */,
D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */,
D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */,
);
path = Fragments;
sourceTree = "<group>";
@ -510,12 +522,14 @@
D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */,
D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */,
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */,
D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */,
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */,
D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */,
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */,
D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */,
D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */,
D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */,
D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */,
D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */,
D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */,
@ -523,9 +537,11 @@
D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */,
D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */,
D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */,
D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */,
D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */,
D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */,
D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */,
D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */,
D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */,
D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */,
D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */,

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "trash-simple-red.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.3333 5.76923H4.66667C4.48986 5.76923 4.32029 5.84217 4.19526 5.972C4.07024 6.10184 4 6.27793 4 6.46154C4 6.64515 4.07024 6.82124 4.19526 6.95107C4.32029 7.08091 4.48986 7.15385 4.66667 7.15385H5.33333V19.6154C5.33333 19.9826 5.47381 20.3348 5.72386 20.5945C5.97391 20.8541 6.31304 21 6.66667 21H17.3333C17.687 21 18.0261 20.8541 18.2761 20.5945C18.5262 20.3348 18.6667 19.9826 18.6667 19.6154V7.15385H19.3333C19.5101 7.15385 19.6797 7.08091 19.8047 6.95107C19.9298 6.82124 20 6.64515 20 6.46154C20 6.27793 19.9298 6.10184 19.8047 5.972C19.6797 5.84217 19.5101 5.76923 19.3333 5.76923ZM17.3333 19.6154H6.66667V7.15385H17.3333V19.6154ZM8 3.69231C8 3.5087 8.07024 3.33261 8.19526 3.20277C8.32029 3.07294 8.48986 3 8.66667 3H15.3333C15.5101 3 15.6797 3.07294 15.8047 3.20277C15.9298 3.33261 16 3.5087 16 3.69231C16 3.87592 15.9298 4.05201 15.8047 4.18184C15.6797 4.31168 15.5101 4.38462 15.3333 4.38462H8.66667C8.48986 4.38462 8.32029 4.31168 8.19526 4.18184C8.07024 4.05201 8 3.87592 8 3.69231Z" fill="#DD5F5F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -271,7 +271,7 @@ final class ContactsManager {
}
}
func getFriend(contact: Contact) -> Friend? {
func getFriendWithContact(contact: Contact) -> Friend? {
if friendList != nil {
let friend = friendList!.friends.first(where: {$0.nativeUri == contact.identifier})
return friend
@ -279,6 +279,18 @@ final class ContactsManager {
return nil
}
}
func getFriendWithAddress(address: Address) -> Friend? {
if friendList != nil {
var friend = friendList!.friends.first(where: {$0.addresses.contains(where: {$0.asStringUriOnly() == address.asStringUriOnly()})})
if friend == nil {
friend = linphoneFriendList!.friends.first(where: {$0.addresses.contains(where: {$0.asStringUriOnly() == address.asStringUriOnly()})})
}
return friend
} else {
return nil
}
}
}
struct PhoneNumber {

View file

@ -36,7 +36,12 @@ struct LinphoneApp: App {
AssistantView(sharedMainViewModel: sharedMainViewModel)
.toast(isShowing: $coreContext.toastMessage)
} else if coreContext.defaultAccount != nil {
ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel())
ContentView(
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
historyViewModel: HistoryViewModel(),
historyListViewModel: HistoryListViewModel()
)
.toast(isShowing: $coreContext.toastMessage)
}
} else {

View file

@ -104,9 +104,15 @@
},
"Add a picture" : {
},
"Add the contact" : {
},
"Add to favourites" : {
},
"All calls will be removed from the history." : {
},
"All contacts" : {
@ -172,6 +178,9 @@
},
"Copy number" : {
},
"Copy SIP address" : {
},
"D'accord" : {
@ -187,6 +196,9 @@
},
"Delete %@?" : {
},
"Delete all history" : {
},
"Delete this contact" : {
@ -199,6 +211,9 @@
},
"Display Name" : {
},
"Do you really want to delete all calls history?" : {
},
"Domain" : {
@ -293,7 +308,7 @@
"Next" : {
},
"No calls for the moment..." : {
"No call for the moment..." : {
},
"No contacts for the moment..." : {
@ -372,6 +387,9 @@
},
"See all" : {
},
"See contact" : {
},
"See Linphone contact" : {

View file

@ -1,9 +1,21 @@
//
// ContactInnerActionsFragment.swift
// Linphone
//
// Created by Benoît Martins on 09/11/2023.
//
/*
* 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

View file

@ -21,9 +21,6 @@ import SwiftUI
import linphonesw
struct ContactsInnerFragment: View {
@Environment(\.scenePhase) var scenePhase
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@ -76,16 +73,6 @@ struct ContactsInnerFragment: View {
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet)
}
.navigationBarHidden(true)
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
ContactsManager.shared.fetchContacts()
print("Active")
} else if newPhase == .inactive {
print("Inactive")
} else if newPhase == .background {
print("Background")
}
}
}
}

View file

@ -17,18 +17,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// swiftlint:disable type_body_length
import SwiftUI
import linphonesw
struct ContentView: View {
@Environment(\.scenePhase) var scenePhase
@ObservedObject private var coreContext = CoreContext.shared
var contactManager = ContactsManager.shared
var magicSearch = MagicSearchSingleton.shared
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@ObservedObject private var coreContext = CoreContext.shared
@ObservedObject var historyListViewModel: HistoryListViewModel
@State var index = 0
@State private var orientation = UIDevice.current.orientation
@ -38,7 +43,8 @@ struct ContentView: View {
@State private var text = ""
@FocusState private var focusedField: Bool
@State var isMenuOpen = false
@State var isShowDeletePopup = false
@State var isShowDeleteContactPopup = false
@State var isShowDeleteAllHistoryPopup = false
@State var isShowEditContactFragment = false
@State var isShowDismissPopup = false
@ -134,6 +140,7 @@ struct ContentView: View {
}
Menu {
if index == 0 {
Button {
isMenuOpen = false
magicSearch.allContact = true
@ -167,6 +174,21 @@ struct ContentView: View {
}
}
}
} else {
Button(role: .destructive) {
isMenuOpen = false
isShowDeleteAllHistoryPopup.toggle()
//historyListViewModel.removeCallLogs()
} label: {
HStack {
Text("Delete all history")
Spacer()
Image("trash-simple-red")
.resizable()
.frame(width: 25, height: 25, alignment: .leading)
}
}
}
} label: {
Image(index == 0 ? "funnel" : "dots-three-vertical")
.renderingMode(.template)
@ -193,9 +215,14 @@ struct ContentView: View {
}
text = ""
if index == 0 {
magicSearch.currentFilter = ""
magicSearch.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} else {
historyListViewModel.resetFilterCallLogs()
}
} label: {
Image("caret-left")
.renderingMode(.template)
@ -226,9 +253,13 @@ struct ContentView: View {
self.focusedField = true
}
.onChange(of: text) { newValue in
if index == 0 {
magicSearch.currentFilter = newValue
magicSearch.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
} else {
historyListViewModel.filterCallLogs(filter: text)
}
}
} else {
TextEditor(text: Binding(
@ -281,10 +312,17 @@ struct ContentView: View {
historyViewModel: historyViewModel,
editContactViewModel: editContactViewModel,
isShowEditContactFragment: $isShowEditContactFragment,
isShowDeletePopup: $isShowDeletePopup
isShowDeletePopup: $isShowDeleteContactPopup
)
} else if self.index == 1 {
HistoryView()
HistoryView(
historyListViewModel: historyListViewModel,
historyViewModel: historyViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
index: $index,
isShowEditContactFragment: $isShowEditContactFragment
)
}
}
.frame(maxWidth:
@ -367,7 +405,7 @@ struct ContentView: View {
}
}
if contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty {
if contactViewModel.indexDisplayedFriend != nil || historyViewModel.indexDisplayedCall != nil {
HStack(spacing: 0) {
Spacer()
.frame(maxWidth:
@ -381,7 +419,7 @@ struct ContentView: View {
ContactFragment(
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
isShowDeletePopup: $isShowDeletePopup,
isShowDeletePopup: $isShowDeleteContactPopup,
isShowDismissPopup: $isShowDismissPopup
)
.frame(maxWidth: .infinity)
@ -440,8 +478,8 @@ struct ContentView: View {
}
}
if isShowDeletePopup {
PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeletePopup,
if isShowDeleteContactPopup {
PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeleteContactPopup,
title: Text(
contactViewModel.selectedFriend != nil
? "Delete \(contactViewModel.selectedFriend!.name!)?"
@ -451,7 +489,7 @@ struct ContentView: View {
content: Text("This contact will be deleted definitively."),
titleFirstButton: Text("Cancel"),
actionFirstButton: {
self.isShowDeletePopup.toggle()},
self.isShowDeleteContactPopup.toggle()},
titleSecondButton: Text("Ok"),
actionSecondButton: {
if contactViewModel.selectedFriendToDelete != nil {
@ -470,18 +508,37 @@ struct ContentView: View {
}
magicSearch.searchForContacts(
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
self.isShowDeletePopup.toggle()
self.isShowDeleteContactPopup.toggle()
})
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
self.isShowDeletePopup.toggle()
self.isShowDeleteContactPopup.toggle()
}
.onAppear {
contactViewModel.selectedFriendToDelete = contactViewModel.selectedFriend
}
}
if isShowDeleteAllHistoryPopup {
PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeleteContactPopup,
title: Text("Do you really want to delete all calls history?"),
content: Text("All calls will be removed from the history."),
titleFirstButton: Text("Cancel"),
actionFirstButton: {
self.isShowDeleteAllHistoryPopup.toggle()},
titleSecondButton: Text("Ok"),
actionSecondButton: {
historyListViewModel.removeCallLogs()
self.isShowDeleteAllHistoryPopup.toggle()
})
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
self.isShowDeleteAllHistoryPopup.toggle()
}
}
if isShowDismissPopup {
PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDismissPopup,
title: Text("Dont save modifications?"),
@ -524,13 +581,23 @@ struct ContentView: View {
}
}
.onRotate { newOrientation in
if (contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty) && searchIsActive {
if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.indexDisplayedCall != nil) && searchIsActive {
self.focusedField = false
} else if searchIsActive {
self.focusedField = true
}
orientation = newOrientation
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
ContactsManager.shared.fetchContacts()
print("Active")
} else if newPhase == .inactive {
print("Inactive")
} else if newPhase == .background {
print("Background")
}
}
}
func openMenu() {
@ -541,5 +608,11 @@ struct ContentView: View {
}
#Preview {
ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel())
ContentView(
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
historyViewModel: HistoryViewModel(),
historyListViewModel: HistoryListViewModel()
)
}
// swiftlint:enable type_body_length

View file

@ -0,0 +1,92 @@
/*
* 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 HistoryFragment: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var historyListViewModel: HistoryListViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@State private var showingSheet = false
@Binding var index: Int
@Binding var isShowEditContactFragment: Bool
var body: some View {
ZStack {
if #available(iOS 16.0, *) {
if idiom != .pad {
HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet)
.sheet(isPresented: $showingSheet) {
HistoryListBottomSheet(
historyViewModel: historyViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
historyListViewModel: historyListViewModel,
showingSheet: $showingSheet,
index: $index,
isShowEditContactFragment: $isShowEditContactFragment
)
.presentationDetents([.fraction(0.2)])
}
} else {
HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet)
.halfSheet(showSheet: $showingSheet) {
HistoryListBottomSheet(
historyViewModel: historyViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
historyListViewModel: historyListViewModel,
showingSheet: $showingSheet,
index: $index,
isShowEditContactFragment: $isShowEditContactFragment
)
} onDismiss: {}
}
} else {
HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet)
.halfSheet(showSheet: $showingSheet) {
HistoryListBottomSheet(
historyViewModel: historyViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
historyListViewModel: historyListViewModel,
showingSheet: $showingSheet,
index: $index,
isShowEditContactFragment: $isShowEditContactFragment
)
} onDismiss: {}
}
}
}
}
#Preview {
HistoryFragment(
historyListViewModel: HistoryListViewModel(),
historyViewModel: HistoryViewModel(),
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
index: .constant(1),
isShowEditContactFragment: .constant(false)
)
}

View file

@ -0,0 +1,240 @@
/*
* 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 UniformTypeIdentifiers
struct HistoryListBottomSheet: View {
@Environment(\.dismiss) var dismiss
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var historyViewModel: HistoryViewModel
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@ObservedObject var historyListViewModel: HistoryListViewModel
@State private var orientation = UIDevice.current.orientation
@Binding var showingSheet: Bool
@Binding var index: Int
@Binding var isShowEditContactFragment: Bool
var body: some View {
VStack(alignment: .leading) {
if idiom != .pad && (orientation == .landscapeLeft
|| orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Spacer()
HStack {
Spacer()
Button("Close") {
if #available(iOS 16.0, *) {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
}
}
.padding(.trailing)
}
Spacer()
Button {
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
index = 0
if ContactsManager.shared.getFriendWithAddress(
address: historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing
? historyViewModel.selectedCall!.toAddress!
: historyViewModel.selectedCall!.fromAddress!
) != nil {
let addressCall = historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing
? historyViewModel.selectedCall!.toAddress!
: historyViewModel.selectedCall!.fromAddress!
let friendIndex = MagicSearchSingleton.shared.lastSearch.firstIndex(where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})})
if friendIndex != nil {
withAnimation {
contactViewModel.indexDisplayedFriend = friendIndex
}
}
} else {
let addressCall = historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing
? historyViewModel.selectedCall!.toAddress!
: historyViewModel.selectedCall!.fromAddress!
withAnimation {
isShowEditContactFragment.toggle()
editContactViewModel.sipAddresses.removeAll()
editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4)))
editContactViewModel.sipAddresses.append("")
}
}
} label: {
HStack {
if ContactsManager.shared.getFriendWithAddress(
address: historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing
? historyViewModel.selectedCall!.toAddress!
: historyViewModel.selectedCall!.fromAddress!
) != nil {
Image("user-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text("See contact")
.default_text_style(styleSize: 16)
Spacer()
} else {
Image("plus-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text("Add the contact")
.default_text_style(styleSize: 16)
Spacer()
}
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
if historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing {
UIPasteboard.general.setValue(
historyViewModel.selectedCall!.toAddress!.asStringUriOnly().dropFirst(4),
forPasteboardType: UTType.plainText.identifier
)
} else {
UIPasteboard.general.setValue(
historyViewModel.selectedCall!.fromAddress!.asStringUriOnly().dropFirst(4),
forPasteboardType: UTType.plainText.identifier
)
}
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("copy")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text("Copy SIP address")
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
CoreContext.shared.doOnCoreQueue { core in
if historyViewModel.selectedCall != nil {
core.removeCallLog(callLog: historyViewModel.selectedCall!)
historyListViewModel.removeCallLog(callLog: historyViewModel.selectedCall!)
}
}
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25, alignment: .leading)
Text("Delete")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
}
.background(Color.gray100)
.frame(maxWidth: .infinity)
.onRotate { newOrientation in
orientation = newOrientation
}
}
}
#Preview {
HistoryListBottomSheet(
historyViewModel: HistoryViewModel(),
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
historyListViewModel: HistoryListViewModel(),
showingSheet: .constant(false),
index: .constant(1),
isShowEditContactFragment: .constant(false)
)
}

View file

@ -0,0 +1,232 @@
/*
* 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 HistoryListFragment: View {
@ObservedObject var historyListViewModel: HistoryListViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@Binding var showingSheet: Bool
var body: some View {
VStack {
List {
ForEach(0..<historyListViewModel.callLogs.count, id: \.self) { index in
Button {
} label: {
HStack {
let fromAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!)
let toAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!)
if historyListViewModel.callLogs[index].dir == .Incoming && fromAddressFriend != nil && fromAddressFriend!.photo != nil && !fromAddressFriend!.photo!.isEmpty {
AsyncImage(url:
ContactsManager.shared.getImagePath(
friendPhotoPath: fromAddressFriend!.photo!)) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 45, height: 45)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 45, height: 45)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
} else if historyListViewModel.callLogs[index].dir == .Outgoing && toAddressFriend != nil && toAddressFriend!.photo != nil && !toAddressFriend!.photo!.isEmpty {
AsyncImage(url:
ContactsManager.shared.getImagePath(
friendPhotoPath: toAddressFriend!.photo!)) { image in
switch image {
case .empty:
ProgressView()
.frame(width: 45, height: 45)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 45, height: 45)
.clipShape(Circle())
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
@unknown default:
EmptyView()
}
}
} else {
if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil {
if historyListViewModel.callLogs[index].toAddress!.displayName != nil {
Image(uiImage: ContactsManager.shared.textToImage(
firstName: historyListViewModel.callLogs[index].toAddress!.displayName!,
lastName: historyListViewModel.callLogs[index].toAddress!.displayName!.components(separatedBy: " ").count > 1
? historyListViewModel.callLogs[index].toAddress!.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
} else {
Image(uiImage: ContactsManager.shared.textToImage(
firstName: historyListViewModel.callLogs[index].toAddress!.username ?? "Username Error",
lastName: historyListViewModel.callLogs[index].toAddress!.username!.components(separatedBy: " ").count > 1
? historyListViewModel.callLogs[index].toAddress!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
} else if historyListViewModel.callLogs[index].fromAddress != nil {
if historyListViewModel.callLogs[index].fromAddress!.displayName != nil {
Image(uiImage: ContactsManager.shared.textToImage(
firstName: historyListViewModel.callLogs[index].fromAddress!.displayName!,
lastName: historyListViewModel.callLogs[index].fromAddress!.displayName!.components(separatedBy: " ").count > 1
? historyListViewModel.callLogs[index].fromAddress!.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
} else {
Image(uiImage: ContactsManager.shared.textToImage(
firstName: historyListViewModel.callLogs[index].fromAddress!.username ?? "Username Error",
lastName: historyListViewModel.callLogs[index].fromAddress!.username!.components(separatedBy: " ").count > 1
? historyListViewModel.callLogs[index].fromAddress!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
}
}
VStack(spacing: 0) {
Spacer()
let fromAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!)
let toAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!)
if historyListViewModel.callLogs[index].dir == .Incoming && fromAddressFriend != nil {
Text(fromAddressFriend!.name!)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else if historyListViewModel.callLogs[index].dir == .Outgoing && toAddressFriend != nil {
Text(toAddressFriend!.name!)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else {
if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil {
Text(historyListViewModel.callLogs[index].toAddress!.displayName != nil
? historyListViewModel.callLogs[index].toAddress!.displayName!
: historyListViewModel.callLogs[index].toAddress!.username!)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else if historyListViewModel.callLogs[index].fromAddress != nil {
Text(historyListViewModel.callLogs[index].fromAddress!.displayName != nil
? historyListViewModel.callLogs[index].fromAddress!.displayName!
: historyListViewModel.callLogs[index].fromAddress!.username!)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
}
HStack {
Image(historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir))
.resizable()
.frame(
width: historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir).contains("rejected") ? 12 : 8,
height: historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir).contains("rejected") ? 6 : 8)
Text(historyListViewModel.getCallTime(startDate: historyListViewModel.callLogs[index].startDate))
.default_text_style_300(styleSize: 12)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
}
Spacer()
}
Image("phone")
.resizable()
.frame(width: 25, height: 25)
.padding(.trailing, 5)
}
}
.buttonStyle(.borderless)
.listRowInsets(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20))
.listRowSeparator(.hidden)
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
historyViewModel.selectedCall = historyListViewModel.callLogs[index]
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
//historyViewModel.indexDisplayedCall = index
}
}
)
}
}
.listStyle(.plain)
.overlay(
VStack {
if historyListViewModel.callLogs.isEmpty {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding(.all)
Text("No call for the moment...")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
}
}
.padding(.all)
)
}
}
}
#Preview {
HistoryListFragment(historyListViewModel: HistoryListViewModel(), historyViewModel: HistoryViewModel(), showingSheet: .constant(false))
}

View file

@ -21,22 +21,37 @@ import SwiftUI
struct HistoryView: View {
@ObservedObject var historyListViewModel: HistoryListViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@Binding var index: Int
@Binding var isShowEditContactFragment: Bool
var body: some View {
NavigationView {
VStack(spacing: 0) {
VStack {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding(.all)
Text("No calls for the moment...")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
ZStack(alignment: .bottomTrailing) {
HistoryFragment(
historyListViewModel: historyListViewModel,
historyViewModel: historyViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
index: $index,
isShowEditContactFragment: $isShowEditContactFragment
)
Button {
} label: {
Image("phone-plus")
.padding()
.background(.white)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 4)
}
.padding(.all)
.padding()
}
}
.navigationViewStyle(.stack)
@ -44,5 +59,12 @@ struct HistoryView: View {
}
#Preview {
HistoryView()
HistoryFragment(
historyListViewModel: HistoryListViewModel(),
historyViewModel: HistoryViewModel(),
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
index: .constant(1),
isShowEditContactFragment: .constant(false)
)
}

View file

@ -0,0 +1,142 @@
/*
* 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 linphonesw
class HistoryListViewModel: ObservableObject {
private var coreContext = CoreContext.shared
@Published var callLogs: [CallLog] = []
var callLogsTmp: [CallLog] = []
init() {
computeCallLogsList()
}
func computeCallLogsList() {
coreContext.doOnCoreQueue { core in
let account = core.defaultAccount
let logs = account?.callLogs != nil ? account!.callLogs : core.callLogs
self.callLogs.removeAll()
self.callLogsTmp.removeAll()
DispatchQueue.main.async {
logs.forEach { log in
self.callLogs.append(log)
self.callLogsTmp.append(log)
}
}
}
}
func getCallIconResId(callStatus: Call.Status, callDir: Call.Dir) -> String {
switch callStatus {
case Call.Status.Missed:
if callDir == .Outgoing {
"outgoing-call-missed"
} else {
"incoming-call-missed"
}
case Call.Status.Success:
if callDir == .Outgoing {
"outgoing-call"
} else {
"incoming-call"
}
default:
if callDir == .Outgoing {
"outgoing-call-rejected"
} else {
"incoming-call-rejected"
}
}
}
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 " + formatter.string(from: myNSDate)
} else if Calendar.current.isDate(myNSDate, equalTo: .now, toGranularity: .year) {
let formatter = DateFormatter()
formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM | HH:mm" : "MM/dd | h:mm a"
return formatter.string(from: myNSDate)
} else {
let formatter = DateFormatter()
formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM/yy | HH:mm" : "MM/dd/yy | h:mm a"
return formatter.string(from: myNSDate)
}
}
func filterCallLogs(filter: String) {
callLogs.removeAll()
callLogsTmp.forEach { callLog in
if callLog.dir == .Outgoing && callLog.toAddress != nil {
if callLog.toAddress!.username != nil && callLog.toAddress!.username!.contains(filter) {
callLogs.append(callLog)
} else if callLog.toAddress!.displayName != nil && callLog.toAddress!.displayName!.contains(filter) {
callLogs.append(callLog)
}
} else if callLog.fromAddress != nil {
if callLog.fromAddress!.username != nil && callLog.fromAddress!.username!.contains(filter) {
callLogs.append(callLog)
} else if callLog.fromAddress!.displayName != nil && callLog.fromAddress!.displayName!.contains(filter) {
callLogs.append(callLog)
}
}
}
}
func resetFilterCallLogs() {
callLogs = callLogsTmp
}
func removeCallLogs() {
coreContext.doOnCoreQueue { core in
let account = core.defaultAccount
if account != nil {
account!.clearCallLogs()
} else {
core.clearCallLogs()
}
self.callLogs.removeAll()
self.callLogsTmp.removeAll()
}
}
func removeCallLog(callLog: CallLog) {
let index = self.callLogs.firstIndex(where: {$0.callId == callLog.callId})
self.callLogs.remove(at: index!)
let indexTmp = self.callLogsTmp.firstIndex(where: {$0.callId == callLog.callId})
self.callLogsTmp.remove(at: index!)
}
}

View file

@ -18,10 +18,13 @@
*/
import Foundation
import linphonesw
class HistoryViewModel: ObservableObject {
@Published var historyTitle: String = ""
@Published var indexDisplayedCall: Int?
var selectedCall: CallLog?
init() {}
}

View file

@ -53,7 +53,7 @@ struct EditContactView: UIViewControllerRepresentable {
name: cnc.givenName + cnc.familyName + String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""),
contact: newContact,
linphoneFriend: false,
existingFriend: ContactsManager.shared.getFriend(contact: newContact))
existingFriend: ContactsManager.shared.getFriendWithContact(contact: newContact))
MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
}