Fix chat send button behavior
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import Lottie
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Chat List Search Content
|
||||
@@ -9,6 +8,7 @@ struct ChatListSearchContent: View {
|
||||
let searchText: String
|
||||
@ObservedObject var viewModel: ChatListViewModel
|
||||
var onSelectRecent: (String) -> Void
|
||||
var onOpenDialog: (ChatRoute) -> Void
|
||||
|
||||
var body: some View {
|
||||
let trimmed = searchText.trimmingCharacters(in: .whitespaces)
|
||||
@@ -46,8 +46,11 @@ private extension ChatListSearchContent {
|
||||
var noResultsState: some View {
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
LottieView(animationName: "search", loopMode: .playOnce, animationSpeed: 1.0)
|
||||
.frame(width: 120, height: 120)
|
||||
LottieView(
|
||||
animationName: "search",
|
||||
animationSpeed: 1.0
|
||||
)
|
||||
.frame(width: 120, height: 120)
|
||||
Text("Search for users")
|
||||
.font(.system(size: 17, weight: .medium))
|
||||
.foregroundStyle(RosettaColors.Adaptive.text)
|
||||
@@ -64,7 +67,12 @@ private extension ChatListSearchContent {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(localResults) { dialog in
|
||||
ChatRowView(dialog: dialog)
|
||||
Button {
|
||||
onOpenDialog(ChatRoute(dialog: dialog))
|
||||
} label: {
|
||||
ChatRowView(dialog: dialog)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
ForEach(serverOnly, id: \.publicKey) { user in
|
||||
@@ -79,7 +87,7 @@ private extension ChatListSearchContent {
|
||||
Spacer().frame(height: 80)
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,15 +121,18 @@ private extension ChatListSearchContent {
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
}
|
||||
}
|
||||
|
||||
var searchPlaceholder: some View {
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
LottieView(animationName: "search", loopMode: .loop, animationSpeed: 1.0)
|
||||
.frame(width: 120, height: 120)
|
||||
LottieView(
|
||||
animationName: "search",
|
||||
animationSpeed: 1.0
|
||||
)
|
||||
.frame(width: 120, height: 120)
|
||||
Text("Search for users")
|
||||
.font(.system(size: 17, weight: .medium))
|
||||
.foregroundStyle(RosettaColors.Adaptive.text)
|
||||
@@ -200,6 +211,7 @@ private extension ChatListSearchContent {
|
||||
|
||||
return Button {
|
||||
viewModel.addToRecent(user)
|
||||
onOpenDialog(ChatRoute(user: user))
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
AvatarView(
|
||||
|
||||
@@ -4,11 +4,13 @@ import SwiftUI
|
||||
|
||||
struct ChatListView: View {
|
||||
@Binding var isSearchActive: Bool
|
||||
var onChatDetailVisibilityChange: ((Bool) -> Void)? = nil
|
||||
@StateObject private var viewModel = ChatListViewModel()
|
||||
@State private var searchText = ""
|
||||
@State private var navigationPath: [ChatRoute] = []
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $navigationPath) {
|
||||
ZStack {
|
||||
RosettaColors.Adaptive.background
|
||||
.ignoresSafeArea()
|
||||
@@ -17,7 +19,12 @@ struct ChatListView: View {
|
||||
ChatListSearchContent(
|
||||
searchText: searchText,
|
||||
viewModel: viewModel,
|
||||
onSelectRecent: { searchText = $0 }
|
||||
onSelectRecent: { searchText = $0 },
|
||||
onOpenDialog: { route in
|
||||
isSearchActive = false
|
||||
searchText = ""
|
||||
navigationPath.append(route)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
normalContent
|
||||
@@ -36,6 +43,20 @@ struct ChatListView: View {
|
||||
.onChange(of: searchText) { _, newValue in
|
||||
viewModel.setSearchQuery(newValue)
|
||||
}
|
||||
.navigationDestination(for: ChatRoute.self) { route in
|
||||
ChatDetailView(
|
||||
route: route,
|
||||
onPresentedChange: { isPresented in
|
||||
onChatDetailVisibilityChange?(isPresented)
|
||||
}
|
||||
)
|
||||
}
|
||||
.onAppear {
|
||||
onChatDetailVisibilityChange?(!navigationPath.isEmpty)
|
||||
}
|
||||
.onChange(of: navigationPath) { _, newPath in
|
||||
onChatDetailVisibilityChange?(!newPath.isEmpty)
|
||||
}
|
||||
}
|
||||
.tint(RosettaColors.figmaBlue)
|
||||
}
|
||||
@@ -81,11 +102,14 @@ private extension ChatListView {
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
}
|
||||
|
||||
func chatRow(_ dialog: Dialog) -> some View {
|
||||
ChatRowView(dialog: dialog)
|
||||
NavigationLink(value: ChatRoute(dialog: dialog)) {
|
||||
ChatRowView(dialog: dialog)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowSeparator(.visible)
|
||||
.listRowSeparatorTint(RosettaColors.Adaptive.divider)
|
||||
@@ -175,5 +199,4 @@ private extension ChatListView {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview { ChatListView(isSearchActive: .constant(false)) }
|
||||
|
||||
#Preview { ChatListView(isSearchActive: .constant(false), onChatDetailVisibilityChange: nil) }
|
||||
|
||||
@@ -19,16 +19,14 @@ final class ChatListViewModel: ObservableObject {
|
||||
|
||||
private var searchTask: Task<Void, Never>?
|
||||
private var lastSearchedText = ""
|
||||
private static let maxRecent = 20
|
||||
|
||||
private var recentKey: String {
|
||||
"rosetta_recent_searches_\(SessionManager.shared.currentPublicKey)"
|
||||
}
|
||||
private var searchHandlerToken: UUID?
|
||||
private var recentSearchesCancellable: AnyCancellable?
|
||||
private let recentRepository = RecentSearchesRepository.shared
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init() {
|
||||
loadRecentSearches()
|
||||
configureRecentSearches()
|
||||
setupSearchCallback()
|
||||
}
|
||||
|
||||
@@ -61,11 +59,12 @@ final class ChatListViewModel: ObservableObject {
|
||||
// MARK: - Actions
|
||||
|
||||
func setSearchQuery(_ query: String) {
|
||||
searchQuery = query
|
||||
searchQuery = normalizeSearchInput(query)
|
||||
triggerServerSearch()
|
||||
}
|
||||
|
||||
func deleteDialog(_ dialog: Dialog) {
|
||||
MessageRepository.shared.deleteDialog(dialog.opponentKey)
|
||||
DialogRepository.shared.deleteDialog(opponentKey: dialog.opponentKey)
|
||||
}
|
||||
|
||||
@@ -84,13 +83,21 @@ final class ChatListViewModel: ObservableObject {
|
||||
// MARK: - Server Search
|
||||
|
||||
private func setupSearchCallback() {
|
||||
if let token = searchHandlerToken {
|
||||
ProtocolManager.shared.removeSearchResultHandler(token)
|
||||
}
|
||||
|
||||
Self.logger.debug("Setting up search callback")
|
||||
ProtocolManager.shared.onSearchResult = { [weak self] packet in
|
||||
searchHandlerToken = ProtocolManager.shared.addSearchResultHandler { [weak self] packet in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
Self.logger.debug("Search callback: self is nil")
|
||||
return
|
||||
}
|
||||
guard !self.searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
self.isServerSearching = false
|
||||
return
|
||||
}
|
||||
Self.logger.debug("📥 Search results received: \(packet.users.count) users")
|
||||
self.serverSearchResults = packet.users
|
||||
self.isServerSearching = false
|
||||
@@ -130,7 +137,7 @@ final class ChatListViewModel: ObservableObject {
|
||||
guard !currentQuery.isEmpty, currentQuery == trimmed else { return }
|
||||
|
||||
let connState = ProtocolManager.shared.connectionState
|
||||
let hash = SessionManager.shared.privateKeyHash
|
||||
let hash = SessionManager.shared.privateKeyHash ?? ProtocolManager.shared.privateHash
|
||||
|
||||
guard connState == .authenticated, let hash else {
|
||||
self.isServerSearching = false
|
||||
@@ -146,50 +153,32 @@ final class ChatListViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizeSearchInput(_ input: String) -> String {
|
||||
input.replacingOccurrences(of: "@", with: "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
// MARK: - Recent Searches
|
||||
|
||||
func addToRecent(_ user: SearchUser) {
|
||||
let recent = RecentSearch(
|
||||
publicKey: user.publicKey,
|
||||
title: user.title,
|
||||
username: user.username,
|
||||
lastSeenText: user.online == 1 ? "online" : "last seen recently"
|
||||
)
|
||||
recentSearches.removeAll { $0.publicKey == user.publicKey }
|
||||
recentSearches.insert(recent, at: 0)
|
||||
if recentSearches.count > Self.maxRecent {
|
||||
recentSearches = Array(recentSearches.prefix(Self.maxRecent))
|
||||
}
|
||||
saveRecentSearches()
|
||||
recentRepository.add(user)
|
||||
}
|
||||
|
||||
func removeRecentSearch(publicKey: String) {
|
||||
recentSearches.removeAll { $0.publicKey == publicKey }
|
||||
saveRecentSearches()
|
||||
recentRepository.remove(publicKey: publicKey)
|
||||
}
|
||||
|
||||
func clearRecentSearches() {
|
||||
recentSearches = []
|
||||
saveRecentSearches()
|
||||
recentRepository.clearAll()
|
||||
}
|
||||
|
||||
private func loadRecentSearches() {
|
||||
if let data = UserDefaults.standard.data(forKey: recentKey),
|
||||
let list = try? JSONDecoder().decode([RecentSearch].self, from: data) {
|
||||
recentSearches = list
|
||||
return
|
||||
}
|
||||
let oldKey = "rosetta_recent_searches"
|
||||
if let data = UserDefaults.standard.data(forKey: oldKey),
|
||||
let list = try? JSONDecoder().decode([RecentSearch].self, from: data) {
|
||||
recentSearches = list
|
||||
saveRecentSearches()
|
||||
UserDefaults.standard.removeObject(forKey: oldKey)
|
||||
}
|
||||
}
|
||||
|
||||
private func saveRecentSearches() {
|
||||
guard let data = try? JSONEncoder().encode(recentSearches) else { return }
|
||||
UserDefaults.standard.set(data, forKey: recentKey)
|
||||
private func configureRecentSearches() {
|
||||
recentRepository.setAccount(SessionManager.shared.currentPublicKey)
|
||||
recentSearches = recentRepository.recentSearches
|
||||
recentSearchesCancellable = recentRepository.$recentSearches
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] list in
|
||||
self?.recentSearches = list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user