Fix chat send button behavior

This commit is contained in:
2026-03-02 03:22:19 +05:00
parent d1fcc04125
commit 8238fd1940
27 changed files with 3423 additions and 610 deletions

View File

@@ -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(

View File

@@ -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) }

View File

@@ -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
}
}
}