From 7dc4882f27e8a5bc5717e8331fc745efa24303b6 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Sat, 13 Dec 2025 02:38:46 +0300 Subject: [PATCH] add block user from settings --- yobble/Network/BlockedUsersService.swift | 17 +++ yobble/Resources/Localizable.xcstrings | 16 ++- .../Views/Tab/Settings/BlockedUsersView.swift | 126 ++++++++++++++++-- 3 files changed, 144 insertions(+), 15 deletions(-) diff --git a/yobble/Network/BlockedUsersService.swift b/yobble/Network/BlockedUsersService.swift index 0cbe2f0..73efb76 100644 --- a/yobble/Network/BlockedUsersService.swift +++ b/yobble/Network/BlockedUsersService.swift @@ -135,6 +135,15 @@ final class BlockedUsersService { func add(userId: UUID, completion: @escaping (Result) -> Void) { let request = BlockedUserCreateRequest(userId: userId, login: nil) + add(request: request, completion: completion) + } + + func add(login: String, completion: @escaping (Result) -> Void) { + let request = BlockedUserCreateRequest(userId: nil, login: login) + add(request: request, completion: completion) + } + + private func add(request: BlockedUserCreateRequest, completion: @escaping (Result) -> Void) { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase @@ -187,6 +196,14 @@ final class BlockedUsersService { } } + func add(login: String) async throws -> BlockedUserInfo { + try await withCheckedThrowingContinuation { continuation in + add(login: login) { result in + continuation.resume(with: result) + } + } + } + private static func decodeDate(from decoder: Decoder) throws -> Date { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 519782f..bf8feb9 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -335,6 +335,9 @@ "Введите пароль" : { "comment" : "Пароль\nПоле ввода пароля на приложение" }, + "Введите юзернейм человека, которого нужно заблокировать. Символ @ указывать не нужно." : { + "comment" : "Blocked users add login footer" + }, "Веб" : { "comment" : "Тип сессии — веб" }, @@ -590,9 +593,6 @@ "Добавить контакт" : { "comment" : "Message profile add contact alert title" }, - "Добавление новых блокировок появится позже." : { - "comment" : "Add blocked user placeholder message" - }, "Добавьте контакты, чтобы быстрее выходить на связь." : { "comment" : "Contacts empty state subtitle" }, @@ -648,7 +648,7 @@ }, "Заблокировать" : { - "comment" : "Message profile block title" + "comment" : "Blocked users add confirm\nBlocked users add title\nMessage profile block title" }, "Заблокировать контакт" : { "comment" : "Contacts context action block" @@ -1106,6 +1106,9 @@ } } }, + "Логин пользователя" : { + "comment" : "Blocked users add login header" + }, "Логин уже занят." : { "localizations" : { "en" : { @@ -1256,6 +1259,9 @@ "Напишите нам через форму обратной связи в разделе \"Поддержка\"." : { "comment" : "FAQ answer: support" }, + "Например, username" : { + "comment" : "Blocked users add login placeholder" + }, "Например: заметил неточную информацию в статье..." : { "comment" : "feedback placeholder: content", "localizations" : { @@ -2788,7 +2794,7 @@ "comment" : "Кнопка копирования кода восстановления" }, "Скоро" : { - "comment" : "Add blocked user placeholder title\nContacts placeholder title\nЗаголовок заглушки" + "comment" : "Contacts placeholder title\nЗаголовок заглушки" }, "Скоро можно будет искать сообщения, ссылки и файлы в этом чате." : { "comment" : "Message profile search action description" diff --git a/yobble/Views/Tab/Settings/BlockedUsersView.swift b/yobble/Views/Tab/Settings/BlockedUsersView.swift index 536520b..c23c18b 100644 --- a/yobble/Views/Tab/Settings/BlockedUsersView.swift +++ b/yobble/Views/Tab/Settings/BlockedUsersView.swift @@ -11,6 +11,11 @@ struct BlockedUsersView: View { @State private var removingUserIds: Set = [] @State private var activeAlert: ActiveAlert? @State private var errorMessageDown: String? + @State private var isAddUserSheetPresented = false + @State private var newBlockedUserLogin = "" + @State private var addBlockedUserError: String? + @State private var isProcessingAddBlockedUser = false + @FocusState private var isAddBlockedUserFieldFocused: Bool private let blockedUsersService = BlockedUsersService() private let limit = 20 @@ -32,7 +37,7 @@ struct BlockedUsersView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { - activeAlert = .addPlaceholder + isAddUserSheetPresented = true } label: { Image(systemName: "plus") } @@ -41,14 +46,11 @@ struct BlockedUsersView: View { .task { await loadBlockedUsers() } + .sheet(isPresented: $isAddUserSheetPresented, onDismiss: resetAddBlockedUserForm) { + addBlockedUserSheet + } .alert(item: $activeAlert) { alert in switch alert { - case .addPlaceholder: - return Alert( - title: Text(NSLocalizedString("Скоро", comment: "Add blocked user placeholder title")), - message: Text(NSLocalizedString("Добавление новых блокировок появится позже.", comment: "Add blocked user placeholder message")), - dismissButton: .default(Text(NSLocalizedString("OK", comment: "Common OK"))) - ) case .error(_, let message): return Alert( title: Text(NSLocalizedString("Ошибка", comment: "Common error title")), @@ -78,6 +80,58 @@ struct BlockedUsersView: View { } } + private var addBlockedUserSheet: some View { + NavigationView { + Form { + Section( + header: Text(NSLocalizedString("Логин пользователя", comment: "Blocked users add login header")), + footer: Text(NSLocalizedString("Введите юзернейм человека, которого нужно заблокировать. Символ @ указывать не нужно.", comment: "Blocked users add login footer")) + .font(.footnote) + .foregroundColor(.secondary) + ) { + TextField(NSLocalizedString("Например, username", comment: "Blocked users add login placeholder"), text: $newBlockedUserLogin) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(.asciiCapable) + .focused($isAddBlockedUserFieldFocused) + } + + if let addBlockedUserError { + Section { + Text(addBlockedUserError) + .font(.footnote) + .foregroundColor(.red) + .multilineTextAlignment(.leading) + } + } + } + .navigationTitle(NSLocalizedString("Заблокировать", comment: "Blocked users add title")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(NSLocalizedString("Отмена", comment: "Common cancel")) { + isAddUserSheetPresented = false + } + } + ToolbarItem(placement: .confirmationAction) { + if isProcessingAddBlockedUser { + ProgressView() + } else { + Button(NSLocalizedString("Заблокировать", comment: "Blocked users add confirm")) { + submitAddBlockedUser() + } + .disabled(!canSubmitNewBlockedUser) + } + } + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + isAddBlockedUserFieldFocused = true + } + } + } + } + private var usersSection: some View { Section(header: Text(NSLocalizedString("Заблокированные", comment: ""))) { ForEach(Array(blockedUsers.enumerated()), id: \.element.id) { index, user in @@ -99,6 +153,14 @@ struct BlockedUsersView: View { } } + private var trimmedNewBlockedUserLogin: String { + newBlockedUserLogin.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private var canSubmitNewBlockedUser: Bool { + !trimmedNewBlockedUserLogin.isEmpty && !isProcessingAddBlockedUser + } + private func userRow(_ user: BlockedUser, index: Int) -> some View { HStack(spacing: 12) { Circle() @@ -185,6 +247,53 @@ struct BlockedUsersView: View { } } + private func submitAddBlockedUser() { + guard canSubmitNewBlockedUser else { return } + + let login = trimmedNewBlockedUserLogin + isProcessingAddBlockedUser = true + addBlockedUserError = nil + + Task { + await performAddBlockedUser(login: login) + } + } + + private func resetAddBlockedUserForm() { + newBlockedUserLogin = "" + addBlockedUserError = nil + isProcessingAddBlockedUser = false + isAddBlockedUserFieldFocused = false + } + + private func performAddBlockedUser(login: String) async { + do { + let payload = try await blockedUsersService.add(login: login) + let newUser = BlockedUser(payload: payload) + + await MainActor.run { + let existed = blockedUsers.contains(where: { $0.id == newUser.id }) + blockedUsers.removeAll { $0.id == newUser.id } + blockedUsers.insert(newUser, at: 0) + if !existed { + offset += 1 + } + isAddUserSheetPresented = false + } + } catch { + if AppConfig.DEBUG { + print("[BlockedUsersView] add blocked user failed: \(error)") + } + await MainActor.run { + addBlockedUserError = error.localizedDescription + } + } + + await MainActor.run { + isProcessingAddBlockedUser = false + } + } + @MainActor private func loadBlockedUsers() async { errorMessageDown = nil @@ -296,13 +405,10 @@ private struct BlockedUser: Identifiable, Equatable { } private enum ActiveAlert: Identifiable { - case addPlaceholder case error(id: UUID = UUID(), message: String) var id: String { switch self { - case .addPlaceholder: - return "addPlaceholder" case .error(let id, _): return id.uuidString }