diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 28c1739..ef6c18e 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -718,6 +718,9 @@ } } } + }, + "Ничего не найдено" : { + }, "Новый пароль" : { "comment" : "Новый пароль", @@ -944,6 +947,9 @@ } } } + }, + "Поиск" : { + }, "Пока что у вас нет чатов" : { "localizations" : { @@ -977,6 +983,9 @@ } } } + }, + "Попробуйте изменить запрос поиска." : { + }, "Приглашение достигло лимита использования." : { "localizations" : { diff --git a/yobble/Views/Tab/ChatsTab.swift b/yobble/Views/Tab/ChatsTab.swift index ec16447..5812790 100644 --- a/yobble/Views/Tab/ChatsTab.swift +++ b/yobble/Views/Tab/ChatsTab.swift @@ -11,6 +11,7 @@ struct ChatsTab: View { var currentUserId: String? = nil @StateObject private var viewModel = PrivateChatsViewModel() @State private var selectedChatId: String? + @State private var searchText: String = "" var body: some View { content @@ -38,6 +39,14 @@ struct ChatsTab: View { private var chatList: some View { List { + VStack(spacing: 0) { + searchBar + .padding(.horizontal, 16) + .padding(.top, 8) + .padding(.bottom, 8) + } + .background(Color(UIColor.systemBackground)) + if let message = viewModel.errorMessage { Section { HStack(alignment: .top, spacing: 8) { @@ -57,47 +66,126 @@ struct ChatsTab: View { .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) } - ForEach(viewModel.chats) { chat in - Button { - selectedChatId = chat.chatId - } label: { - ChatRowView(chat: chat, currentUserId: currentUserId) - .contentShape(Rectangle()) + if filteredChats.isEmpty && isSearching { + Section { + emptySearchResultView } - .buttonStyle(.plain) - .contextMenu { - Button(action: {}) { - Label(NSLocalizedString("Закрепить (скоро)", comment: ""), systemImage: "pin") + .listRowInsets(EdgeInsets(top: 24, leading: 16, bottom: 24, trailing: 16)) + .listRowSeparator(.hidden) + } else { + ForEach(filteredChats) { chat in + Button { + selectedChatId = chat.chatId + } label: { + ChatRowView(chat: chat, currentUserId: currentUserId) + .contentShape(Rectangle()) } + .buttonStyle(.plain) + .contextMenu { + Button(action: {}) { + Label(NSLocalizedString("Закрепить (скоро)", comment: ""), systemImage: "pin") + } - Button(action: {}) { - Label(NSLocalizedString("Без звука (скоро)", comment: ""), systemImage: "speaker.slash") - } + Button(action: {}) { + Label(NSLocalizedString("Без звука (скоро)", comment: ""), systemImage: "speaker.slash") + } - Button(role: .destructive, action: {}) { - Label(NSLocalizedString("Удалить чат (скоро)", comment: ""), systemImage: "trash") + Button(role: .destructive, action: {}) { + Label(NSLocalizedString("Удалить чат (скоро)", comment: ""), systemImage: "trash") + } } - } - .background( - NavigationLink( - destination: ChatPlaceholderView(chat: chat), - tag: chat.chatId, - selection: $selectedChatId - ) { - EmptyView() + .background( + NavigationLink( + destination: ChatPlaceholderView(chat: chat), + tag: chat.chatId, + selection: $selectedChatId + ) { + EmptyView() + } + .hidden() + ) + .onAppear { + guard !isSearching else { return } + viewModel.loadMoreIfNeeded(currentItem: chat) } - .hidden() - ) - .onAppear { - viewModel.loadMoreIfNeeded(currentItem: chat) } } - if viewModel.isLoadingMore { + if viewModel.isLoadingMore && !isSearching { loadingMoreRow } } .listStyle(.plain) +// .safeAreaInset(edge: .top) { +// VStack(spacing: 0) { +// searchBar +// .padding(.horizontal, 16) +// .padding(.top, 8) +// .padding(.bottom, 8) +// Divider() +// } +// .background(Color(UIColor.systemBackground)) +// } + } + + private var searchBar: some View { + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .foregroundColor(.secondary) + TextField(NSLocalizedString("Поиск", comment: ""), text: $searchText) + .textFieldStyle(.plain) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + if !searchText.isEmpty { + Button(action: { searchText = "" }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(UIColor.secondarySystemBackground)) + ) + } + + private var isSearching: Bool { + !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + private var filteredChats: [PrivateChatListItem] { + let trimmedQuery = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedQuery.isEmpty else { return viewModel.chats } + let lowercasedQuery = trimmedQuery.lowercased() + return viewModel.chats.filter { chat in + let searchableValues = [ + chat.chatData?.customName, + chat.chatData?.fullName, + chat.chatData?.login, + chat.lastMessage?.content + ] + return searchableValues + .compactMap { $0?.lowercased() } + .contains(where: { $0.contains(lowercasedQuery) }) + } + } + + private var emptySearchResultView: some View { + VStack(spacing: 8) { + Image(systemName: "text.magnifyingglass") + .font(.system(size: 40)) + .foregroundColor(.secondary) + Text(NSLocalizedString("Ничего не найдено", comment: "")) + .font(.headline) + Text(NSLocalizedString("Попробуйте изменить запрос поиска.", comment: "")) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) } private var loadingState: some View {