diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 1279fcc32..71f3fce2e 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ D74C9CFF2ACAEC5E0021626A /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CFE2ACAEC5E0021626A /* PopupView.swift */; }; D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9D002ACB098C0021626A /* PermissionManager.swift */; }; D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */; }; + D76005F62B0798B00054B79A /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76005F52B0798B00054B79A /* IntExtension.swift */; }; D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7702EF12AC7205000557C00 /* WelcomeView.swift */; }; D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D777DBB22AE12C5900565A99 /* ContactsManager.swift */; }; D78290B82ADD3910004AA85C /* ContactsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290B72ADD3910004AA85C /* ContactsFragment.swift */; }; @@ -111,6 +112,7 @@ D74C9CFE2ACAEC5E0021626A /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = ""; }; D74C9D002ACB098C0021626A /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = ""; }; D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupLoadingView.swift; sourceTree = ""; }; + D76005F52B0798B00054B79A /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; }; D7702EF12AC7205000557C00 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; D78290B72ADD3910004AA85C /* ContactsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsFragment.swift; sourceTree = ""; }; @@ -168,6 +170,7 @@ children = ( D717071D2AC5922E0037746F /* ColorExtension.swift */, D717071F2AC5989C0037746F /* TextExtension.swift */, + D76005F52B0798B00054B79A /* IntExtension.swift */, D74C9D002ACB098C0021626A /* PermissionManager.swift */, D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */, D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */, @@ -543,6 +546,7 @@ D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */, D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */, D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */, + D76005F62B0798B00054B79A /* IntExtension.swift in Sources */, D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */, diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index dda26ce71..7965caa6a 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -107,6 +107,9 @@ }, "Add the contact" : { + }, + "Add to contacts" : { + }, "Add to favourites" : { @@ -202,6 +205,9 @@ }, "Delete all history" : { + }, + "Delete history" : { + }, "Delete this contact" : { @@ -262,6 +268,9 @@ }, "I understand" : { + }, + "Incoming Call" : { + }, "Information" : { @@ -328,6 +337,9 @@ }, "Other actions" : { + }, + "Outgoing Call" : { + }, "password" : { "extractionState" : "manual", diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 62f76caca..ad1d37600 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -427,10 +427,18 @@ struct ContentView: View { .background(Color.gray100) .ignoresSafeArea(.keyboard) } else if self.index == 1 { - HistoryContactFragment(historyViewModel: historyViewModel, isShowDeleteAllHistoryPopup: $isShowDeleteAllHistoryPopup) - .frame(maxWidth: .infinity) - .background(Color.gray100) - .ignoresSafeArea(.keyboard) + HistoryContactFragment( + historyViewModel: historyViewModel, + historyListViewModel: historyListViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + isShowDeleteAllHistoryPopup: $isShowDeleteAllHistoryPopup, + isShowEditContactFragment: $isShowEditContactFragment, + indexPage: $index + ) + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) } } .onAppear { @@ -527,11 +535,14 @@ struct ContentView: View { content: Text("All calls will be removed from the history."), titleFirstButton: Text("Cancel"), actionFirstButton: { - self.isShowDeleteAllHistoryPopup.toggle()}, + self.isShowDeleteAllHistoryPopup.toggle() + historyListViewModel.callLogsAddressToDelete = "" + }, titleSecondButton: Text("Ok"), actionSecondButton: { historyListViewModel.removeCallLogs() self.isShowDeleteAllHistoryPopup.toggle() + historyViewModel.displayedCall = nil }) .background(.black.opacity(0.65)) .zIndex(3) diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index 97489fb0b..8062d5913 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -18,6 +18,7 @@ */ import SwiftUI +import UniformTypeIdentifiers struct HistoryContactFragment: View { @@ -25,10 +26,15 @@ struct HistoryContactFragment: View { @ObservedObject var sharedMainViewModel = SharedMainViewModel() @ObservedObject var historyViewModel: HistoryViewModel + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel @State var isMenuOpen = false @Binding var isShowDeleteAllHistoryPopup: Bool + @Binding var isShowEditContactFragment: Bool + @Binding var indexPage: Int var body: some View { NavigationView { @@ -60,13 +66,52 @@ struct HistoryContactFragment: View { Spacer() Menu { + let fromAddressFriend = historyViewModel.displayedCall != nil ? ContactsManager.shared.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil + let toAddressFriend = historyViewModel.displayedCall != nil ? ContactsManager.shared.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil + let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil + Button { isMenuOpen = false + + indexPage = 0 + + if ContactsManager.shared.getFriendWithAddress( + address: historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + ) != nil { + let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + + let friendIndex = MagicSearchSingleton.shared.lastSearch.firstIndex( + where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) + if friendIndex != nil { + + withAnimation { + historyViewModel.displayedCall = nil + contactViewModel.indexDisplayedFriend = friendIndex + } + } + } else { + let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + + withAnimation { + historyViewModel.displayedCall = nil + isShowEditContactFragment.toggle() + editContactViewModel.sipAddresses.removeAll() + editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4))) + editContactViewModel.sipAddresses.append("") + } + } + } label: { HStack { - Text("See all") + Text(addressFriend != nil ? "See contact" : "Add to contacts") Spacer() - Image("green-check") + Image(addressFriend != nil ? "user-circle" : "plus-circle") .resizable() .frame(width: 25, height: 25, alignment: .leading) } @@ -74,11 +119,23 @@ struct HistoryContactFragment: View { Button { isMenuOpen = false + + if historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing { + UIPasteboard.general.setValue( + historyViewModel.displayedCall!.toAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } else { + UIPasteboard.general.setValue( + historyViewModel.displayedCall!.fromAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } } label: { HStack { - Text("See Linphone contact") + Text("Copy SIP address") Spacer() - Image("green-check") + Image("copy") .resizable() .frame(width: 25, height: 25, alignment: .leading) } @@ -86,9 +143,18 @@ struct HistoryContactFragment: View { Button(role: .destructive) { isMenuOpen = false + + if historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing { + historyListViewModel.callLogsAddressToDelete = historyViewModel.displayedCall!.toAddress!.asStringUriOnly() + } else { + historyListViewModel.callLogsAddressToDelete = historyViewModel.displayedCall!.fromAddress!.asStringUriOnly() + } + + isShowDeleteAllHistoryPopup.toggle() + } label: { HStack { - Text("Delete all history") + Text("Delete history") Spacer() Image("trash-simple-red") .resizable() @@ -166,6 +232,13 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) + + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) Text("") .multilineTextAlignment(.center) @@ -188,6 +261,13 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) + + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) Text("") .multilineTextAlignment(.center) @@ -213,7 +293,14 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) - + + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + Text("") .multilineTextAlignment(.center) .default_text_style_300(styleSize: 12) @@ -235,7 +322,14 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) - + + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + Text("") .multilineTextAlignment(.center) .default_text_style_300(styleSize: 12) @@ -253,6 +347,22 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) + + if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + } else if historyViewModel.displayedCall!.fromAddress != nil { + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + } Text("En ligne") .foregroundStyle(Color.greenSuccess500) @@ -260,6 +370,7 @@ struct HistoryContactFragment: View { .default_text_style_300(styleSize: 12) .frame(maxWidth: .infinity) .frame(height: 20) + .padding(.top, 5) } } .frame(minHeight: 150) @@ -355,58 +466,61 @@ struct HistoryContactFragment: View { .background(Color.gray100) VStack(spacing: 0) { - - let fromAddressFriend = historyViewModel.displayedCall != nil ? ContactsManager.shared.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil - let toAddressFriend = historyViewModel.displayedCall != nil ? ContactsManager.shared.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil - let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil - - if historyViewModel.displayedCall != nil && addressFriend != nil && addressFriend != nil { - ForEach(0.. String { + switch callStatus { + case Call.Status.Missed: + if callDir == .Outgoing { + "Outgoing Call" + } else { + "Missed Call" + } + + case Call.Status.Success: + if callDir == .Outgoing { + "Outgoing Call" + } else { + "Incoming Call" + } + + default: + if callDir == .Outgoing { + "Outgoing Call" + } else { + "Incoming Call" + } + } + } + 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) + return "Today | " + 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) + 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" @@ -120,15 +147,26 @@ class HistoryListViewModel: ObservableObject { } func removeCallLogs() { - coreContext.doOnCoreQueue { core in - let account = core.defaultAccount - if account != nil { - account!.clearCallLogs() - } else { - core.clearCallLogs() + if callLogsAddressToDelete.isEmpty { + coreContext.doOnCoreQueue { core in + let account = core.defaultAccount + if account != nil { + account!.clearCallLogs() + } else { + core.clearCallLogs() + } + self.callLogs.removeAll() + self.callLogsTmp.removeAll() } - self.callLogs.removeAll() - self.callLogsTmp.removeAll() + } else { + removeCallLogsWithAddress() + callLogsAddressToDelete = "" + } + } + + func removeCallLogsWithAddress() { + self.callLogs.filter { $0.toAddress!.asStringUriOnly() == callLogsAddressToDelete || $0.fromAddress!.asStringUriOnly() == callLogsAddressToDelete }.forEach { callLog in + removeCallLog(callLog: callLog) } } @@ -137,6 +175,6 @@ class HistoryListViewModel: ObservableObject { self.callLogs.remove(at: index!) let indexTmp = self.callLogsTmp.firstIndex(where: {$0.callId == callLog.callId}) - self.callLogsTmp.remove(at: index!) + self.callLogsTmp.remove(at: indexTmp!) } } diff --git a/Linphone/Utils/IntExtension.swift b/Linphone/Utils/IntExtension.swift new file mode 100644 index 000000000..fca8ba9d1 --- /dev/null +++ b/Linphone/Utils/IntExtension.swift @@ -0,0 +1,67 @@ +/* + * 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 . + */ + +import Foundation + +extension Int { + + public func hmsFrom() -> (Int, Int, Int) { + return (self / 3600, (self % 3600) / 60, (self % 3600) % 60) + } + + public func convertDurationToString() -> String { + var duration = "" + let (hour, minute, second) = self.hmsFrom() + if (hour > 0) { + duration = self.getHour(hour: hour) + } + return "\(duration)\(self.getMinute(minute: minute))\(self.getSecond(second: second))" + } + + private func getHour(hour: Int) -> String { + var duration = "\(hour):" + if (hour < 10) { + duration = "0\(hour):" + } + return duration + } + + private func getMinute(minute: Int) -> String { + if (minute == 0) { + return "00:" + } + + if (minute < 10) { + return "0\(minute):" + } + + return "\(minute):" + } + + private func getSecond(second: Int) -> String { + if (second == 0){ + return "00" + } + + if (second < 10) { + return "0\(second)" + } + return "\(second)" + } +}