diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 87d0917df..7fbd4f875 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -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 = ""; }; D717071D2AC5922E0037746F /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; D717071F2AC5989C0037746F /* TextExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextExtension.swift; sourceTree = ""; }; + D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneUtils.swift; sourceTree = ""; }; 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 = ""; }; D719ABB82ABC67BF00B41C10 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -176,6 +182,10 @@ D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; D7C48DF32AFA66F900D938CB /* EditContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactController.swift; sourceTree = ""; }; D7C48DF52AFCDF4700D938CB /* ContactInnerActionsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerActionsFragment.swift; sourceTree = ""; }; + D7CEE0342B7A210300FD79B7 /* ConversationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsView.swift; sourceTree = ""; }; + D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListViewModel.swift; sourceTree = ""; }; + D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsFragment.swift; sourceTree = ""; }; + D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListFragment.swift; sourceTree = ""; }; D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = ""; }; D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = ""; }; D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = ""; }; @@ -248,6 +258,7 @@ D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */, D732A9082AFD235500DB42BA /* ShareSheetController.swift */, D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */, + D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */, ); path = Utils; sourceTree = ""; @@ -313,6 +324,7 @@ D719ABC62ABC6F0200B41C10 /* Main */ = { isa = PBXGroup; children = ( + D7CEE0332B7A20A400FD79B7 /* Conversations */, D7A03FBB2ACC2D850081A588 /* Contacts */, D74C9CFD2ACAEC150021626A /* Fragments */, D7A03FBE2ACC2E010081A588 /* History */, @@ -518,6 +530,33 @@ path = ViewModel; sourceTree = ""; }; + D7CEE0332B7A20A400FD79B7 /* Conversations */ = { + isa = PBXGroup; + children = ( + D7CEE0392B7A232200FD79B7 /* Fragments */, + D7CEE0362B7A212C00FD79B7 /* ViewModel */, + D7CEE0342B7A210300FD79B7 /* ConversationsView.swift */, + ); + path = Conversations; + sourceTree = ""; + }; + D7CEE0362B7A212C00FD79B7 /* ViewModel */ = { + isa = PBXGroup; + children = ( + D7CEE0372B7A214F00FD79B7 /* ConversationsListViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + D7CEE0392B7A232200FD79B7 /* Fragments */ = { + isa = PBXGroup; + children = ( + D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */, + D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */, + ); + path = Fragments; + sourceTree = ""; + }; 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 */, diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 173e36237..bb19caad9 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -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") + */ } }) diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 95378929c..f3a1fc3fa 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -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() } } } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 589a8fa68..279f2ae7b 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -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?" : { diff --git a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift index 98a4f43f9..2d6f9bffb 100644 --- a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift +++ b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift @@ -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) diff --git a/Linphone/UI/Call/Fragments/CallsListFragment.swift b/Linphone/UI/Call/Fragments/CallsListFragment.swift index 60efa482a..1d2d76aea 100644 --- a/Linphone/UI/Call/Fragments/CallsListFragment.swift +++ b/Linphone/UI/Call/Fragments/CallsListFragment.swift @@ -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()) } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 3f594f977..8d9db12e0 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -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)!) diff --git a/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift index 9e2796bfb..7faec743d 100644 --- a/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift @@ -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)!) diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index aef8efb45..2a90ad2cb 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -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 diff --git a/Linphone/UI/Main/Conversations/ConversationsView.swift b/Linphone/UI/Main/Conversations/ConversationsView.swift new file mode 100644 index 000000000..9a79142f3 --- /dev/null +++ b/Linphone/UI/Main/Conversations/ConversationsView.swift @@ -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 . + */ + +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()) +} diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsFragment.swift new file mode 100644 index 000000000..90ae2cd1a --- /dev/null +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsFragment.swift @@ -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 . + */ + +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()) +} diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift new file mode 100644 index 000000000..38b047733 --- /dev/null +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift @@ -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 . + */ + +import SwiftUI +import linphonesw + +struct ConversationsListFragment: View { + + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var conversationsListViewModel: ConversationsListViewModel + + var body: some View { + VStack { + List { + ForEach(0.. 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()) +} diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift new file mode 100644 index 000000000..8b7bac6c6 --- /dev/null +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -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 . + */ + +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 + } + } + } +} diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift index 5858db1b7..9d219ff30 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -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() diff --git a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift index 9bd9c0a05..f82122892 100644 --- a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift @@ -29,6 +29,7 @@ class HistoryListViewModel: ObservableObject { var callLogsAddressToDelete = "" var callLogSubscription: AnyCancellable? + init() { computeCallLogsList() } diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift index 8d44fa061..5143790a5 100644 --- a/Linphone/Utils/Avatar.swift +++ b/Linphone/Utils/Avatar.swift @@ -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) } } } diff --git a/Linphone/Utils/LinphoneUtils.swift b/Linphone/Utils/LinphoneUtils.swift new file mode 100644 index 000000000..579b4cdde --- /dev/null +++ b/Linphone/Utils/LinphoneUtils.swift @@ -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 . + */ + +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" + } + } +}