Add contacts and suggestions to history and conversation views

This commit is contained in:
Benoit Martins 2026-03-02 12:18:52 +01:00
parent 9364e7f196
commit 6c5bf43062
10 changed files with 537 additions and 54 deletions

View file

@ -1,7 +1,7 @@
import Foundation
public enum AppGitInfo {
public static let branch = "master"
public static let commit = "cdde88e32"
public static let branch = "feature/contacts_and_suggestions_list"
public static let commit = "c3f95fe23"
public static let tag = "6.1.0-alpha"
}

View file

@ -48,6 +48,7 @@ struct ContentView: View {
@State private var searchIsActive = false
@State private var text = ""
@FocusState private var focusedField: Bool
@State private var showingDialer = false
@State var isMenuOpen = false
@State var isShowDeleteContactPopup = false
@ -320,6 +321,8 @@ struct ContentView: View {
Spacer()
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 0)
sharedMainViewModel.displayedCall = nil
sharedMainViewModel.displayedConversation = nil
@ -341,7 +344,8 @@ struct ContentView: View {
}
})
.padding(.top)
.frame(height: geometry.size.height/4)
Spacer()
ZStack {
if SharedMainViewModel.shared.missedCallsCount > 0 {
@ -365,6 +369,8 @@ struct ContentView: View {
}
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 1)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedConversation = nil
@ -390,7 +396,8 @@ struct ContentView: View {
})
.padding(.top)
}
.frame(height: geometry.size.height/4)
Spacer()
if !sharedMainViewModel.disableChatFeature {
ZStack {
@ -415,6 +422,8 @@ struct ContentView: View {
}
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 2)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
@ -438,11 +447,14 @@ struct ContentView: View {
})
.padding(.top)
}
.frame(height: geometry.size.height/4)
Spacer()
}
if !sharedMainViewModel.disableMeetingFeature {
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 3)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
@ -464,13 +476,13 @@ struct ContentView: View {
}
})
.padding(.top)
.frame(height: geometry.size.height/4)
Spacer()
}
}
}
.frame(width: 75, height: geometry.size.height)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.padding(.leading,
orientation == .landscapeRight && geometry.safeAreaInsets.bottom > 0
? -geometry.safeAreaInsets.leading
@ -690,10 +702,12 @@ struct ContentView: View {
text = ""
if sharedMainViewModel.indexView == 0 {
if sharedMainViewModel.indexView != 3 {
magicSearch.currentFilter = ""
magicSearch.searchForContacts()
} else if let historyListVM = historyListViewModel, sharedMainViewModel.indexView == 1 {
}
if let historyListVM = historyListViewModel, sharedMainViewModel.indexView == 1 {
historyListVM.resetFilterCallLogs()
} else if let conversationsListVM = conversationsListViewModel, sharedMainViewModel.indexView == 2 {
conversationsListVM.resetFilterConversations()
@ -735,10 +749,12 @@ struct ContentView: View {
self.focusedField = true
}
.onChange(of: text) { newValue in
if sharedMainViewModel.indexView == 0 {
if sharedMainViewModel.indexView != 3 {
magicSearch.currentFilter = newValue
magicSearch.searchForContacts()
} else if let historyListVM = historyListViewModel, sharedMainViewModel.indexView == 1 {
}
if let historyListVM = historyListViewModel, sharedMainViewModel.indexView == 1 {
if text.isEmpty {
historyListVM.resetFilterCallLogs()
} else {
@ -755,6 +771,34 @@ struct ContentView: View {
meetingsListVM.computeMeetingsList()
}
}
.onChange(of: isShowStartCallFragment) { isShowStartCallFragmentNewValue in
if isShowStartCallFragmentNewValue == false && !text.isEmpty {
if let historyListVM = historyListViewModel, sharedMainViewModel.indexView == 1 {
magicSearch.currentFilter = text
magicSearch.searchForContacts()
if text.isEmpty {
historyListVM.resetFilterCallLogs()
} else {
historyListVM.filterCallLogs(filter: text)
}
}
}
}
.onChange(of: isShowStartConversationFragment) { isShowStartConversationFragmentNewValue in
if isShowStartConversationFragmentNewValue == false && !text.isEmpty {
if let conversationsListVM = conversationsListViewModel, sharedMainViewModel.indexView == 2 {
magicSearch.currentFilter = text
magicSearch.searchForContacts()
if text.isEmpty {
conversationsListVM.resetFilterConversations()
} else {
conversationsListVM.filterConversations(filter: text)
}
}
}
}
} else {
TextEditor(text: Binding(
get: {
@ -777,18 +821,56 @@ struct ContentView: View {
self.focusedField = true
}
.onChange(of: text) { newValue in
if sharedMainViewModel.indexView == 0 {
if sharedMainViewModel.indexView != 3 {
magicSearch.currentFilter = newValue
magicSearch.searchForContacts()
} else if let historyListVM = historyListViewModel, sharedMainViewModel.indexView == 1 {
historyListVM.filterCallLogs(filter: text)
}
if let historyListVM = historyListViewModel, sharedMainViewModel.indexView == 1 {
if text.isEmpty {
historyListVM.resetFilterCallLogs()
} else {
historyListVM.filterCallLogs(filter: text)
}
} else if let conversationsListVM = conversationsListViewModel, sharedMainViewModel.indexView == 2 {
conversationsListVM.filterConversations(filter: text)
if text.isEmpty {
conversationsListVM.resetFilterConversations()
} else {
conversationsListVM.filterConversations(filter: text)
}
} else if let meetingsListVM = meetingsListViewModel, sharedMainViewModel.indexView == 3 {
meetingsListVM.currentFilter = text
meetingsListVM.computeMeetingsList()
}
}
.onChange(of: isShowStartCallFragment) { isShowStartCallFragmentNewValue in
if isShowStartCallFragmentNewValue == false && !text.isEmpty {
if let historyListVM = historyListViewModel, sharedMainViewModel.indexView == 1 {
magicSearch.currentFilter = text
magicSearch.searchForContacts()
if text.isEmpty {
historyListVM.resetFilterCallLogs()
} else {
historyListVM.filterCallLogs(filter: text)
}
}
}
}
.onChange(of: isShowStartConversationFragment) { isShowStartConversationFragmentNewValue in
if isShowStartConversationFragmentNewValue == false && !text.isEmpty {
if let conversationsListVM = conversationsListViewModel, sharedMainViewModel.indexView == 2 {
magicSearch.currentFilter = text
magicSearch.searchForContacts()
if text.isEmpty {
conversationsListVM.resetFilterConversations()
} else {
conversationsListVM.filterConversations(filter: text)
}
}
}
}
}
Button {
@ -874,6 +956,8 @@ struct ContentView: View {
Group {
Spacer()
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 0)
sharedMainViewModel.displayedCall = nil
sharedMainViewModel.displayedConversation = nil
@ -921,6 +1005,8 @@ struct ContentView: View {
}
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 1)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedConversation = nil
@ -973,6 +1059,8 @@ struct ContentView: View {
}
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 2)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
@ -1002,6 +1090,8 @@ struct ContentView: View {
if !sharedMainViewModel.disableMeetingFeature {
Spacer()
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 3)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
@ -1299,13 +1389,17 @@ struct ContentView: View {
if sharedMainViewModel.operationInProgress {
PopupLoadingView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad && (orientation == .landscapeLeft || orientation == .landscapeRight || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) ? geometry.safeAreaInsets.bottom : 0)
.background(.black.opacity(0.65))
.zIndex(3)
.onDisappear {
if let contactsListVM = contactsListViewModel, let displayedConversation = contactsListVM.displayedConversation {
if !sharedMainViewModel.disableChatFeature {
sharedMainViewModel.displayedFriend = nil
resetFilter()
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
sharedMainViewModel.changeIndexView(indexViewInt: 2)
@ -1330,6 +1424,8 @@ struct ContentView: View {
} else if let historyListVM = historyListViewModel, let displayedConversation = historyListVM.displayedConversation {
if !sharedMainViewModel.disableChatFeature {
resetFilter()
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
sharedMainViewModel.changeIndexView(indexViewInt: 2)
@ -1352,6 +1448,15 @@ struct ContentView: View {
}
}
}
} else if let conversationsListVM = conversationsListViewModel {
conversationsListVM.currentFilter = ""
self.resetFilter()
if let displayedConversation = conversationsListVM.displayedConversation {
conversationsListVM.changeDisplayedChatRoom(conversationModel: displayedConversation)
conversationsListVM.displayedConversation = nil
}
}
}
}
@ -1404,7 +1509,7 @@ struct ContentView: View {
.transition(.move(edge: .trailing))
}
if let meetingsListVM = meetingsListViewModel, isShowSendCancelMeetingNotificationPopup {
if let meetingsListVM = meetingsListViewModel, isShowSendCancelMeetingNotificationPopup {
PopupView(
isShowPopup: $isShowSendCancelMeetingNotificationPopup,
title: Text("meeting_schedule_cancel_dialog_title"),
@ -1668,6 +1773,7 @@ struct ContentView: View {
.onChange(of: navigationManager.selectedCallId) { newCallId in
if newCallId != nil {
if !sharedMainViewModel.disableChatFeature {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 2)
}
}
@ -1760,6 +1866,17 @@ struct ContentView: View {
self.sideMenuIsOpen.toggle()
}
}
func resetFilter() {
self.text = ""
self.focusedField = false
self.searchIsActive = false
if !magicSearch.currentFilter.isEmpty {
magicSearch.currentFilter = ""
magicSearch.searchForContacts()
}
}
}
struct ContactsContainer: View {

View file

@ -55,7 +55,7 @@ struct ConversationsView: View {
#Preview {
ConversationsListFragment(
showingSheet: .constant(false),
text: .constant("")
text: .constant(""),
showingSheet: .constant(false)
)
}

View file

@ -25,13 +25,14 @@ struct ConversationsFragment: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State var showingSheet: Bool = false
@Binding var text: String
@State var showingSheet: Bool = false
var body: some View {
ZStack {
if #available(iOS 16.0, *), idiom != .pad {
ConversationsListFragment(showingSheet: $showingSheet, text: $text)
ConversationsListFragment(text: $text, showingSheet: $showingSheet)
.sheet(isPresented: $showingSheet) {
ConversationsListBottomSheet(
showingSheet: $showingSheet
@ -43,7 +44,7 @@ struct ConversationsFragment: View {
)
}
} else {
ConversationsListFragment(showingSheet: $showingSheet, text: $text)
ConversationsListFragment(text: $text, showingSheet: $showingSheet)
.halfSheet(showSheet: $showingSheet) {
ConversationsListBottomSheet(
showingSheet: $showingSheet

View file

@ -26,10 +26,12 @@ struct ConversationsListFragment: View {
@EnvironmentObject var navigationManager: NavigationManager
@ObservedObject var contactsManager = ContactsManager.shared
@EnvironmentObject var conversationsListViewModel: ConversationsListViewModel
@Binding var showingSheet: Bool
@Binding var text: String
@Binding var showingSheet: Bool
var body: some View {
VStack {
@ -42,6 +44,34 @@ struct ConversationsListFragment: View {
text: $text
)
}
if !conversationsListViewModel.currentFilter.isEmpty {
if !contactsManager.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
}
ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in
withAnimation {
conversationsListViewModel.createOneToOneChatRoomWith(remote: addr)
}
})
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
suggestionsList
}
}
}
.safeAreaInset(edge: .top, content: {
Spacer()
@ -50,7 +80,13 @@ struct ConversationsListFragment: View {
.listStyle(.plain)
.overlay(
VStack {
if conversationsListViewModel.conversationsList.isEmpty {
if conversationsListViewModel.conversationsList.isEmpty &&
(
conversationsListViewModel.currentFilter.isEmpty ||
(!conversationsListViewModel.currentFilter.isEmpty &&
contactsManager.lastSearch.isEmpty &&
contactsManager.lastSearchSuggestions.isEmpty)
) {
Spacer()
Image("illus-belledonne")
.resizable()
@ -65,6 +101,11 @@ struct ConversationsListFragment: View {
}
.padding(.all)
)
.onDisappear {
if !conversationsListViewModel.currentFilter.isEmpty {
conversationsListViewModel.resetFilterConversations()
}
}
}
.navigationTitle("")
.navigationBarHidden(true)
@ -77,6 +118,69 @@ struct ConversationsListFragment: View {
}
}
}
var suggestionsList: some View {
ForEach(0..<contactsManager.lastSearchSuggestions.count, id: \.self) { index in
Button {
if let address = contactsManager.lastSearchSuggestions[index].address {
withAnimation {
conversationsListViewModel.createOneToOneChatRoomWith(remote: address)
}
}
} label: {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil {
if contactsManager.lastSearchSuggestions[index].address!.domain != AppServices.corePreferences.defaultDomain {
Image(uiImage: contactsManager.textToImage(
firstName: String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)),
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)))
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
} else {
if let address = contactsManager.lastSearchSuggestions[index].address {
let nameTmp = address.displayName
?? address.username
?? String(address.asStringUriOnly().dropFirst(4))
Image(uiImage: contactsManager.textToImage(
firstName: nameTmp,
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(nameTmp)
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text("username_error")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
}
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
}
}
struct ConversationRow: View {
@ -244,7 +348,7 @@ struct ConversationRow: View {
#Preview {
ConversationsListFragment(
showingSheet: .constant(false),
text: .constant("")
text: .constant(""),
showingSheet: .constant(false)
)
}

View file

@ -33,11 +33,13 @@ class ConversationsListViewModel: ObservableObject {
private var coreConversationDelegate: CoreDelegate?
@Published var currentFilter: String = ""
@Published var displayedConversation: ConversationModel?
@Published var conversationsList: [ConversationModel] = []
var selectedConversation: ConversationModel?
var currentFilter: String = ""
private var chatRoomDelegate: ChatRoomDelegate?
init() {
computeChatRoomsList()
@ -428,6 +430,7 @@ class ConversationsListViewModel: ObservableObject {
}
func resetFilterConversations() {
currentFilter = ""
filterConversations(filter: "")
}
@ -479,5 +482,163 @@ class ConversationsListViewModel: ObservableObject {
}
}
}
func createOneToOneChatRoomWith(remote: Address) {
CoreContext.shared.doOnCoreQueue { core in
let account = core.defaultAccount
if account == nil {
Log.error(
"\(ConversationsListViewModel.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())"
)
return
}
DispatchQueue.main.async {
self.sharedMainViewModel.operationInProgress = true
}
do {
let params = try core.createConferenceParams(conference: nil)
params.chatEnabled = true
params.groupEnabled = false
params.subject = NSLocalizedString("conversation_one_to_one_hidden_subject", comment: "")
params.account = account
guard let chatParams = params.chatParams else { return }
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
let sameDomain = remote.domain == AppServices.corePreferences.defaultDomain && remote.domain == account!.params?.domain
if account!.params != nil && (account!.params!.instantMessagingEncryptionMandatory && sameDomain) {
Log.info("\(ConversationsListViewModel.TAG) Account is in secure mode & domain matches, creating an E2E encrypted conversation")
chatParams.backend = ChatRoom.Backend.FlexisipChat
params.securityLevel = Conference.SecurityLevel.EndToEnd
} else if account!.params != nil && (!account!.params!.instantMessagingEncryptionMandatory) {
if LinphoneUtils.isEndToEndEncryptedChatAvailable(core: core) {
Log.info(
"\(ConversationsListViewModel.TAG) Account is in interop mode but LIME is available, creating an E2E encrypted conversation"
)
chatParams.backend = ChatRoom.Backend.FlexisipChat
params.securityLevel = Conference.SecurityLevel.EndToEnd
} else {
Log.info(
"\(ConversationsListViewModel.TAG) Account is in interop mode but LIME isn't available, creating a SIP simple conversation"
)
chatParams.backend = ChatRoom.Backend.Basic
params.securityLevel = Conference.SecurityLevel.None
}
} else {
Log.error(
"\(ConversationsListViewModel.TAG) Account is in secure mode, can't chat with SIP address of different domain \(remote.asStringUriOnly())"
)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.sharedMainViewModel.operationInProgress = false
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
return
}
let participants = [remote]
let localAddress = account?.params?.identityAddress
let existingChatRoom = core.searchChatRoom(params: params, localAddr: localAddress, remoteAddr: nil, participants: participants)
if existingChatRoom == nil {
Log.info(
"\(ConversationsListViewModel.TAG) No existing 1-1 conversation between local account \(localAddress?.asStringUriOnly() ?? "") and remote \(remote.asStringUriOnly()) was found for given parameters, let's create it"
)
do {
let chatRoom = try core.createChatRoom(params: params, participants: participants)
if chatParams.backend == ChatRoom.Backend.FlexisipChat {
let state = chatRoom.state
if state == ChatRoom.State.Created {
let chatRoomId = LinphoneUtils.getConversationId(chatRoom: chatRoom)
Log.info("\(ConversationsListViewModel.TAG) 1-1 conversation \(chatRoomId) has been created")
let model = ConversationModel(chatRoom: chatRoom)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.displayedConversation = model
self.sharedMainViewModel.operationInProgress = false
}
} else {
Log.info("\(ConversationsListViewModel.TAG) Conversation isn't in Created state yet (state is \(state)), wait for it")
self.chatRoomAddDelegate(core: core, chatRoom: chatRoom)
}
} else {
let chatRoomId = LinphoneUtils.getConversationId(chatRoom: chatRoom)
Log.info("\(ConversationsListViewModel.TAG) Conversation successfully created \(chatRoomId)")
let model = ConversationModel(chatRoom: chatRoom)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.displayedConversation = model
self.sharedMainViewModel.operationInProgress = false
}
}
} catch {
Log.error("\(ConversationsListViewModel.TAG) Failed to create 1-1 conversation with \(remote.asStringUriOnly())")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.sharedMainViewModel.operationInProgress = false
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
} else {
Log.warn(
"\(ConversationsListViewModel.TAG) A 1-1 conversation between local account \(localAddress?.asStringUriOnly() ?? "") and remote \(remote.asStringUriOnly()) for given parameters already exists!"
)
let model = ConversationModel(chatRoom: existingChatRoom!)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.displayedConversation = model
self.sharedMainViewModel.operationInProgress = false
}
}
} catch {
}
}
}
func chatRoomAddDelegate(core: Core, chatRoom: ChatRoom) {
self.chatRoomDelegate = ChatRoomDelegateStub(onStateChanged: { (chatRoom: ChatRoom, state: ChatRoom.State) in
let state = chatRoom.state
let chatRoomId = LinphoneUtils.getChatRoomId(room: chatRoom)
if state == ChatRoom.State.CreationFailed {
Log.error("\(StartConversationViewModel.TAG) Conversation \(chatRoomId) creation has failed!")
if let chatRoomDelegate = self.chatRoomDelegate {
chatRoom.removeDelegate(delegate: chatRoomDelegate)
}
self.chatRoomDelegate = nil
DispatchQueue.main.async {
self.sharedMainViewModel.operationInProgress = false
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
}, onConferenceJoined: { (chatRoom: ChatRoom, _: EventLog) in
let state = chatRoom.state
let id = LinphoneUtils.getChatRoomId(room: chatRoom)
Log.info("\(StartConversationViewModel.TAG) Conversation \(id) \(chatRoom.subject ?? "") state changed: \(state)")
if state == ChatRoom.State.Created {
Log.info("\(StartConversationViewModel.TAG) Conversation \(id) successfully created")
if let chatRoomDelegate = self.chatRoomDelegate {
chatRoom.removeDelegate(delegate: chatRoomDelegate)
}
self.chatRoomDelegate = nil
let model = ConversationModel(chatRoom: chatRoom)
DispatchQueue.main.async {
self.displayedConversation = model
self.sharedMainViewModel.operationInProgress = false
}
} else if state == ChatRoom.State.CreationFailed {
Log.error("\(StartConversationViewModel.TAG) Conversation \(id) creation has failed!")
if let chatRoomDelegate = self.chatRoomDelegate {
chatRoom.removeDelegate(delegate: chatRoomDelegate)
}
self.chatRoomDelegate = nil
DispatchQueue.main.async {
self.sharedMainViewModel.operationInProgress = false
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
})
chatRoom.addDelegate(delegate: self.chatRoomDelegate!)
}
}
// swiftlint:enable line_length

View file

@ -36,11 +36,11 @@ class StartConversationViewModel: ObservableObject {
@Published var participants: [SelectedAddressModel] = []
@Published var operationOneToOneInProgress: Bool = false
@Published var hideGroupChatButton: Bool = false
@Published var operationGroupInProgress: Bool = false
@Published var operationOneToOneInProgress: Bool = false
@Published var displayedConversation: ConversationModel?
@Published var hideGroupChatButton: Bool = false
private var chatRoomDelegate: ChatRoomDelegate?

View file

@ -22,32 +22,26 @@ import SwiftUI
struct PopupLoadingView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
ProgressView()
.controlSize(.large)
.progressViewStyle(CircularProgressViewStyle(tint: Color.orangeMain500))
.frame(maxWidth: .infinity)
.padding(.top)
.padding(.bottom)
Text("operation_in_progress_overlay")
.tint(Color.grayMain2c600)
.default_text_style(styleSize: 15)
.frame(maxWidth: .infinity)
}
.padding(.horizontal, 20)
.padding(.vertical, 20)
.background(.white)
.cornerRadius(20)
.padding(.horizontal)
.frame(maxHeight: .infinity)
.frame(maxWidth: .infinity)
.shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2)
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
VStack {
ProgressView()
.controlSize(.large)
.progressViewStyle(CircularProgressViewStyle(tint: Color.orangeMain500))
.frame(maxWidth: .infinity)
.padding(.top)
.padding(.bottom)
Text("operation_in_progress_overlay")
.tint(Color.grayMain2c600)
.default_text_style(styleSize: 15)
.frame(maxWidth: .infinity)
}
.padding(.horizontal, 20)
.padding(.vertical, 20)
.background(.white)
.cornerRadius(20)
.padding(.horizontal)
.shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2)
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
}
}

View file

@ -38,6 +38,34 @@ struct HistoryListFragment: View {
ForEach(historyListViewModel.callLogs) { historyModel in
HistoryRow(historyModel: historyModel, showingSheet: $showingSheet)
}
if !historyListViewModel.callLogsFilter.isEmpty {
if !contactsManager.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
}
ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in
withAnimation {
telecomManager.doCallOrJoinConf(address: addr)
}
})
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
suggestionsList
}
}
}
.safeAreaInset(edge: .top, content: {
Spacer()
@ -46,7 +74,13 @@ struct HistoryListFragment: View {
.listStyle(.plain)
.overlay(
VStack {
if historyListViewModel.callLogs.isEmpty {
if historyListViewModel.callLogs.isEmpty &&
(
historyListViewModel.callLogsFilter.isEmpty ||
(!historyListViewModel.callLogsFilter.isEmpty &&
contactsManager.lastSearch.isEmpty &&
contactsManager.lastSearchSuggestions.isEmpty)
) {
Spacer()
Image("illus-belledonne")
.resizable()
@ -62,10 +96,78 @@ struct HistoryListFragment: View {
}
.padding(.all)
)
.onDisappear {
if !historyListViewModel.callLogsFilter.isEmpty {
historyListViewModel.resetFilterCallLogs()
}
}
}
.navigationTitle("")
.navigationBarHidden(true)
}
var suggestionsList: some View {
ForEach(0..<contactsManager.lastSearchSuggestions.count, id: \.self) { index in
Button {
if let address = contactsManager.lastSearchSuggestions[index].address {
withAnimation {
telecomManager.doCallOrJoinConf(address: address)
}
}
} label: {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil {
if contactsManager.lastSearchSuggestions[index].address!.domain != AppServices.corePreferences.defaultDomain {
Image(uiImage: contactsManager.textToImage(
firstName: String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)),
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)))
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
} else {
if let address = contactsManager.lastSearchSuggestions[index].address {
let nameTmp = address.displayName
?? address.username
?? String(address.asStringUriOnly().dropFirst(4))
Image(uiImage: contactsManager.textToImage(
firstName: nameTmp,
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(nameTmp)
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text("username_error")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
}
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
}
}
struct HistoryRow: View {

View file

@ -31,6 +31,8 @@ class HistoryListViewModel: ObservableObject {
var callLogsAddressToDelete = ""
var callLogCoreDelegate: CoreDelegate?
@Published var callLogsFilter = ""
@Published var selectedCall: HistoryModel?
@Published var displayedConversation: ConversationModel?
@ -171,6 +173,7 @@ class HistoryListViewModel: ObservableObject {
}
func filterCallLogs(filter: String) {
callLogsFilter = filter
callLogs.removeAll()
callLogsTmp.forEach { callLog in
if callLog.addressName.lowercased().contains(filter.lowercased()) {
@ -180,6 +183,7 @@ class HistoryListViewModel: ObservableObject {
}
func resetFilterCallLogs() {
callLogsFilter = ""
callLogs = callLogsTmp
}