patch contact

This commit is contained in:
cheykrym 2025-12-13 03:36:47 +03:00
parent d85921e9fe
commit 430f21bf62
3 changed files with 136 additions and 18 deletions

View File

@ -2483,9 +2483,6 @@
},
"Пропустить" : {
},
"Просмотр \"%1$@\" появится позже." : {
"comment" : "Contacts placeholder message"
},
"Профиль" : {
"comment" : "Message profile navigation title",

View File

@ -2,6 +2,7 @@ import SwiftUI
import Foundation
struct ContactsTab: View {
@ObservedObject private var loginViewModel: LoginViewModel
@State private var contacts: [Contact] = []
@State private var isLoading = false
@State private var loadError: String?
@ -9,16 +10,29 @@ struct ContactsTab: View {
@State private var activeAlert: ContactsAlert?
@State private var hasMore = true
@State private var offset = 0
@State private var creatingChatForContactId: UUID?
@State private var pendingChatItem: PrivateChatListItem?
@State private var isPendingChatActive = false
private let contactsService = ContactsService()
private let chatService = ChatService()
private let pageSize = 25
private var currentUserId: String? {
let identifier = loginViewModel.userId
return identifier.isEmpty ? nil : identifier
}
init(viewModel: LoginViewModel) {
self._loginViewModel = ObservedObject(wrappedValue: viewModel)
}
var body: some View {
List {
if isLoading && contacts.isEmpty {
loadingState
}
if let loadError, contacts.isEmpty {
errorState(loadError)
} else if contacts.isEmpty {
@ -26,13 +40,13 @@ struct ContactsTab: View {
} else {
ForEach(Array(contacts.enumerated()), id: \.element.id) { index, contact in
Button {
showContactPlaceholder(for: contact)
openChat(for: contact)
} label: {
ContactRow(contact: contact)
ContactRow(contact: contact, isLoading: creatingChatForContactId == contact.id)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
// .disabled(contact.isDeleted)
.disabled(contact.isDeleted || creatingChatForContactId == contact.id)
.contextMenu {
Button {
handleContactAction(.edit, for: contact)
@ -106,6 +120,7 @@ struct ContactsTab: View {
)
}
}
.overlay(pendingChatNavigationLink)
}
private var loadingState: some View {
@ -219,26 +234,126 @@ struct ContactsTab: View {
isLoading = false
}
private func showContactPlaceholder(for contact: Contact) {
activeAlert = .info(
title: NSLocalizedString("Скоро", comment: "Contacts placeholder title"),
message: String(
format: NSLocalizedString("Просмотр \"%1$@\" появится позже.", comment: "Contacts placeholder message"),
contact.displayName
)
)
}
private func handleContactAction(_ action: ContactAction, for contact: Contact) {
activeAlert = .info(
title: NSLocalizedString("Скоро", comment: "Contacts placeholder title"),
message: action.placeholderMessage(for: contact)
)
}
private var pendingChatNavigationLink: some View {
NavigationLink(
destination: pendingChatDestination,
isActive: Binding(
get: { isPendingChatActive && pendingChatItem != nil },
set: { newValue in
if !newValue {
isPendingChatActive = false
pendingChatItem = nil
}
}
)
) {
EmptyView()
}
.hidden()
}
@ViewBuilder
private var pendingChatDestination: some View {
if let pendingChatItem {
PrivateChatView(chat: pendingChatItem, currentUserId: currentUserId)
} else {
EmptyView()
}
}
private func openChat(for contact: Contact) {
guard creatingChatForContactId == nil else { return }
guard !contact.isDeleted else { return }
creatingChatForContactId = contact.id
chatService.createOrFindPrivateChat(targetUserId: contact.id.uuidString) { result in
DispatchQueue.main.async {
creatingChatForContactId = nil
switch result {
case .success(let data):
let chatItem = PrivateChatListItem(
chatId: data.chatId,
chatType: data.chatType,
chatData: chatProfile(for: contact),
lastMessage: nil,
createdAt: nil,
unreadCount: 0
)
pendingChatItem = chatItem
isPendingChatActive = true
case .failure(let error):
activeAlert = .error(message: friendlyChatCreationMessage(for: error))
}
}
}
}
private func chatProfile(for contact: Contact) -> ChatProfile {
ChatProfile(
userId: contact.id.uuidString,
login: contact.login,
fullName: contact.fullName,
customName: contact.customName,
createdAt: contact.createdAt,
isOfficial: false
)
}
private func friendlyChatCreationMessage(for error: Error) -> String {
if let chatError = error as? ChatServiceError {
return chatError.errorDescription ?? NSLocalizedString("Не удалось открыть чат.", comment: "Chat creation fallback")
}
if let networkError = error as? NetworkError {
switch networkError {
case .unauthorized:
return NSLocalizedString("Сессия истекла. Войдите снова.", comment: "Chat creation unauthorized")
case .invalidURL, .noResponse:
return NSLocalizedString("Ошибка соединения с сервером.", comment: "Chat creation connection")
case .network(let underlying):
return String(format: NSLocalizedString("Ошибка сети: %@", comment: "Chat creation network error"), underlying.localizedDescription)
case .server(let statusCode, let data):
if let data {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let payload = try? decoder.decode(ErrorResponse.self, from: data) {
if let detail = payload.detail?.trimmingCharacters(in: .whitespacesAndNewlines), !detail.isEmpty {
return detail
}
if let message = payload.data?.message?.trimmingCharacters(in: .whitespacesAndNewlines), !message.isEmpty {
return message
}
}
if let raw = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty {
return raw
}
}
return String(format: NSLocalizedString("Ошибка сервера (%@).", comment: "Chat creation server status"), "\(statusCode)")
}
}
return NSLocalizedString("Произошла неизвестная ошибка. Попробуйте позже.", comment: "Chat creation unknown error")
}
}
private struct ContactRow: View {
let contact: Contact
let isLoading: Bool
init(contact: Contact, isLoading: Bool = false) {
self.contact = contact
self.isLoading = isLoading
}
var body: some View {
HStack(alignment: .top, spacing: 10) {
@ -289,6 +404,12 @@ private struct ContactRow: View {
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if isLoading {
ProgressView()
.scaleEffect(0.8)
.padding(.top, 4)
}
}
.padding(.vertical, 6)
}

View File

@ -69,7 +69,7 @@ struct MainView: View {
.opacity(selectedTab == 2 ? 1 : 0)
.allowsHitTesting(selectedTab == 2)
ContactsTab()
ContactsTab(viewModel: viewModel)
.opacity(selectedTab == 4 ? 1 : 0)
SettingsView(viewModel: viewModel)