From af89cea3fbd035bd980035bdd17d658cecbe2815 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Thu, 11 Dec 2025 03:14:51 +0300 Subject: [PATCH] add block user --- yobble/Network/ProfileService.swift | 104 +++++++++++++++++++++ yobble/Resources/Localizable.xcstrings | 6 ++ yobble/Views/Chat/MessageProfileView.swift | 73 ++++++++++----- 3 files changed, 162 insertions(+), 21 deletions(-) diff --git a/yobble/Network/ProfileService.swift b/yobble/Network/ProfileService.swift index dada0c4..1d4c02d 100644 --- a/yobble/Network/ProfileService.swift +++ b/yobble/Network/ProfileService.swift @@ -74,6 +74,57 @@ final class ProfileService { } } + func fetchProfile(userId: UUID, completion: @escaping (Result) -> Void) { + fetchProfile(userId: userId.uuidString, completion: completion) + } + + func fetchProfile(userId: String, completion: @escaping (Result) -> Void) { + let sanitizedId = userId.trimmingCharacters(in: .whitespacesAndNewlines) + + client.request( + path: "/v1/profile/\(sanitizedId)", + method: .get, + requiresAuth: true + ) { [decoder] result in + switch result { + case .success(let response): + do { + let profile = try Self.decodeProfileResponse( + data: response.data, + decoder: decoder, + requestedId: sanitizedId + ) + completion(.success(profile)) + } catch { + if AppConfig.DEBUG { + print("[ProfileService] decode profile by id failed: \(error)") + } + completion(.failure(error)) + } + case .failure(let error): + if case let NetworkError.server(_, data) = error, + let data, + let message = Self.errorMessage(from: data) { + completion(.failure(ProfileServiceError.unexpectedStatus(message))) + return + } + completion(.failure(error)) + } + } + } + + func fetchProfile(userId: UUID) async throws -> ChatProfile { + try await fetchProfile(userId: userId.uuidString) + } + + func fetchProfile(userId: String) async throws -> ChatProfile { + try await withCheckedThrowingContinuation { continuation in + fetchProfile(userId: userId, completion: { result in + continuation.resume(with: result) + }) + } + } + func updateProfile(_ payload: ProfileUpdateRequestPayload, completion: @escaping (Result) -> Void) { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase @@ -201,6 +252,59 @@ final class ProfileService { } } + private static func decodeProfileResponse( + data: Data, + decoder: JSONDecoder, + requestedId: String + ) throws -> ChatProfile { + let defaultErrorMessage = NSLocalizedString("Не удалось загрузить профиль.", comment: "Profile unexpected status") + var dictionaryDecodeError: String? + + do { + let apiResponse = try decoder.decode(APIResponse<[String: ChatProfile]>.self, from: data) + guard apiResponse.status == "fine" else { + let message = apiResponse.detail ?? defaultErrorMessage + throw ProfileServiceError.unexpectedStatus(message) + } + + let normalizedKey = requestedId.lowercased() + if let profile = apiResponse.data[requestedId] + ?? apiResponse.data[normalizedKey] + ?? apiResponse.data[requestedId.uppercased()] + ?? apiResponse.data.first?.value { + return profile + } + + throw ProfileServiceError.unexpectedStatus( + NSLocalizedString("Профиль не найден.", comment: "Profile by id missing") + ) + } catch let error as ProfileServiceError { + throw error + } catch { + dictionaryDecodeError = Self.describeDecodingError(error: error, data: data) + } + + do { + let apiResponse = try decoder.decode(APIResponse.self, from: data) + guard apiResponse.status == "fine" else { + let message = apiResponse.detail ?? defaultErrorMessage + throw ProfileServiceError.unexpectedStatus(message) + } + return apiResponse.data + } catch let error as ProfileServiceError { + throw error + } catch { + let singleError = Self.describeDecodingError(error: error, data: data) + let combined: String + if let dictionaryDecodeError { + combined = dictionaryDecodeError + "\nOR\n" + singleError + } else { + combined = singleError + } + throw ProfileServiceError.decoding(debugDescription: combined) + } + } + 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 b5a7ad7..d218b2e 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -1348,6 +1348,9 @@ } } }, + "Не удалось обновить профиль" : { + "comment" : "Message profile refresh error title" + }, "Не удалось обработать данные чатов." : { "localizations" : { "en" : { @@ -2383,6 +2386,9 @@ } } }, + "Профиль не найден." : { + "comment" : "Profile by id missing" + }, "Профиль пока не загружен. Попробуйте позже." : { "comment" : "Profile not ready error" }, diff --git a/yobble/Views/Chat/MessageProfileView.swift b/yobble/Views/Chat/MessageProfileView.swift index 9d7f4cb..a6ba210 100644 --- a/yobble/Views/Chat/MessageProfileView.swift +++ b/yobble/Views/Chat/MessageProfileView.swift @@ -11,16 +11,19 @@ struct MessageProfileView: View { let currentUserId: String? private let avatarSize: CGFloat = 96 private let blockedUsersService = BlockedUsersService() + private let profileService = ProfileService() @State private var areNotificationsEnabled: Bool = true @State private var placeholderAlert: PlaceholderAlert? @State private var isBioExpanded: Bool = false + @State private var chatProfile: ChatProfile? @State private var isBlockedByCurrentUserState: Bool @State private var isProcessingBlockAction = false init(chat: PrivateChatListItem, currentUserId: String?) { self.chat = chat self.currentUserId = currentUserId + _chatProfile = State(initialValue: chat.chatData) _isBlockedByCurrentUserState = State(initialValue: chat.chatData?.relationship?.isTargetUserBlockedByCurrentUser ?? false) } @@ -526,7 +529,7 @@ struct MessageProfileView: View { private func handleBlockToggleTap() { guard !isProcessingBlockAction else { return } - guard let userIdString = chat.chatData?.userId, + guard let userIdString = currentChatProfile?.userId, let userId = UUID(uuidString: userIdString) else { placeholderAlert = PlaceholderAlert( title: NSLocalizedString("Ошибка", comment: "Common error title"), @@ -552,20 +555,44 @@ struct MessageProfileView: View { await MainActor.run { isBlockedByCurrentUserState = shouldBlock - isProcessingBlockAction = false } + + await refreshChatProfile(userId: userId) } catch { if AppConfig.DEBUG { print("[MessageProfileView] block action failed: \(error)") } await MainActor.run { - isProcessingBlockAction = false placeholderAlert = PlaceholderAlert( title: NSLocalizedString("Ошибка", comment: "Common error title"), message: error.localizedDescription ) } } + + await MainActor.run { + isProcessingBlockAction = false + } + } + + private func refreshChatProfile(userId: UUID) async { + do { + let profile = try await profileService.fetchProfile(userId: userId) + await MainActor.run { + chatProfile = profile + isBlockedByCurrentUserState = profile.relationship?.isTargetUserBlockedByCurrentUser ?? isBlockedByCurrentUserState + } + } catch { + if AppConfig.DEBUG { + print("[MessageProfileView] refresh profile failed: \(error)") + } + await MainActor.run { + placeholderAlert = PlaceholderAlert( + title: NSLocalizedString("Не удалось обновить профиль", comment: "Message profile refresh error title"), + message: error.localizedDescription + ) + } + } } private var mediaCategories: [MediaCategory] { @@ -585,11 +612,11 @@ struct MessageProfileView: View { // MARK: - Derived Data private var profileBio: String? { - trimmed(chat.chatData?.bio) + trimmed(currentChatProfile?.bio) } private var membershipDescription: String? { - guard let createdAt = chat.chatData?.createdAt else { return nil } + guard let createdAt = currentChatProfile?.createdAt else { return nil } let formatted = MessageProfileView.joinedFormatter.string(from: createdAt) return String( // format: NSLocalizedString("На платформе с %@", comment: "Message profile joined format"), @@ -599,12 +626,12 @@ struct MessageProfileView: View { } private var shouldShowRelationshipQuickActions: Bool { - guard let relationship = chat.chatData?.relationship else { return false } + guard let relationship = currentChatProfile?.relationship else { return false } return !relationship.isTargetInContactsOfCurrentUser } private var ratingDisplayValue: String { - guard let rating = chat.chatData?.rating else { + guard let rating = currentChatProfile?.rating else { return NSLocalizedString("Недоступно", comment: "Message profile rating unavailable") } @@ -638,7 +665,7 @@ struct MessageProfileView: View { ) } - guard let lastSeen = chat.chatData?.lastSeen else { return nil } + guard let lastSeen = currentChatProfile?.lastSeen else { return nil } let lastSeenDate = Date(timeIntervalSince1970: TimeInterval(lastSeen)) let interval = Date().timeIntervalSince(lastSeenDate) @@ -673,7 +700,7 @@ struct MessageProfileView: View { ) } - if let relationship = chat.chatData?.relationship { + if let relationship = currentChatProfile?.relationship { if relationship.isTargetInContactsOfCurrentUser { tags.append( StatusTag( @@ -795,10 +822,14 @@ struct MessageProfileView: View { ] } + private var currentChatProfile: ChatProfile? { + chatProfile ?? chat.chatData + } + private var isBlockedByCurrentUser: Bool { isBlockedByCurrentUserState } private var avatarUrl: URL? { - guard let chatData = chat.chatData, + guard let chatData = currentChatProfile, let fileId = chatData.avatars?.current?.fileId else { return nil } @@ -809,7 +840,7 @@ struct MessageProfileView: View { @ViewBuilder private var profileAvatar: some View { if let url = avatarUrl, - let fileId = chat.chatData?.avatars?.current?.fileId, + let fileId = currentChatProfile?.avatars?.current?.fileId, let userId = currentUserId { CachedAvatarView(url: url, fileId: fileId, userId: userId) { placeholderAvatar @@ -857,7 +888,7 @@ struct MessageProfileView: View { } private var avatarInitial: String { - if let name = trimmed(chat.chatData?.customName) ?? trimmed(chat.chatData?.fullName) { + if let name = trimmed(currentChatProfile?.customName) ?? trimmed(currentChatProfile?.fullName) { let components = name.split(separator: " ") let initials = components.prefix(2).compactMap { $0.first } if !initials.isEmpty { @@ -865,7 +896,7 @@ struct MessageProfileView: View { } } - if let login = trimmed(chat.chatData?.login) { + if let login = trimmed(currentChatProfile?.login) { return String(login.prefix(1)).uppercased() } @@ -880,13 +911,13 @@ struct MessageProfileView: View { } private var displayName: String { - if let custom = trimmed(chat.chatData?.customName) { + if let custom = trimmed(currentChatProfile?.customName) { return custom } - if let full = trimmed(chat.chatData?.fullName) { + if let full = trimmed(currentChatProfile?.fullName) { return full } - if let login = trimmed(chat.chatData?.login) { + if let login = trimmed(currentChatProfile?.login) { return "@\(login)" } return NSLocalizedString("Неизвестный пользователь", comment: "Message profile fallback title") @@ -894,9 +925,9 @@ struct MessageProfileView: View { private var publicFullName: String? { guard - let custom = trimmed(chat.chatData?.customName), + let custom = trimmed(currentChatProfile?.customName), !custom.isEmpty, // custom должен быть - let full = trimmed(chat.chatData?.fullName), + let full = trimmed(currentChatProfile?.fullName), !full.isEmpty // full должен быть else { return nil @@ -906,16 +937,16 @@ struct MessageProfileView: View { } private var loginDisplay: String? { - guard let login = trimmed(chat.chatData?.login) else { return nil } + guard let login = trimmed(currentChatProfile?.login) else { return nil } return "@\(login)" } private var isDeletedUser: Bool { - trimmed(chat.chatData?.login) == nil + trimmed(currentChatProfile?.login) == nil } private var isOfficial: Bool { - chat.chatData?.isOfficial ?? false + currentChatProfile?.isOfficial ?? false } // MARK: - Formatters & Models