add creation chat
This commit is contained in:
		
							parent
							
								
									21d120cb3d
								
							
						
					
					
						commit
						a937db4385
					
				@ -395,7 +395,7 @@
 | 
			
		||||
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 | 
			
		||||
				CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
 | 
			
		||||
				CODE_SIGN_STYLE = Automatic;
 | 
			
		||||
				CURRENT_PROJECT_VERSION = 6;
 | 
			
		||||
				CURRENT_PROJECT_VERSION = 7;
 | 
			
		||||
				DEVELOPMENT_TEAM = V22H44W47J;
 | 
			
		||||
				ENABLE_HARDENED_RUNTIME = YES;
 | 
			
		||||
				ENABLE_PREVIEWS = YES;
 | 
			
		||||
@ -435,7 +435,7 @@
 | 
			
		||||
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 | 
			
		||||
				CODE_SIGN_ENTITLEMENTS = yobble/yobble.entitlements;
 | 
			
		||||
				CODE_SIGN_STYLE = Automatic;
 | 
			
		||||
				CURRENT_PROJECT_VERSION = 6;
 | 
			
		||||
				CURRENT_PROJECT_VERSION = 7;
 | 
			
		||||
				DEVELOPMENT_TEAM = V22H44W47J;
 | 
			
		||||
				ENABLE_HARDENED_RUNTIME = YES;
 | 
			
		||||
				ENABLE_PREVIEWS = YES;
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,28 @@ struct PrivateChatHistoryData: Decodable {
 | 
			
		||||
    let hasMore: Bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct PrivateChatCreateData: Decodable {
 | 
			
		||||
    let chatId: String
 | 
			
		||||
    let chatType: PrivateChatListItem.ChatType
 | 
			
		||||
    let status: String
 | 
			
		||||
    let message: String
 | 
			
		||||
 | 
			
		||||
    private enum CodingKeys: String, CodingKey {
 | 
			
		||||
        case chatId
 | 
			
		||||
        case chatType
 | 
			
		||||
        case status
 | 
			
		||||
        case message
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init(from decoder: Decoder) throws {
 | 
			
		||||
        let container = try decoder.container(keyedBy: CodingKeys.self)
 | 
			
		||||
        self.chatId = try container.decodeFlexibleString(forKey: .chatId)
 | 
			
		||||
        self.chatType = try container.decode(PrivateChatListItem.ChatType.self, forKey: .chatType)
 | 
			
		||||
        self.status = try container.decode(String.self, forKey: .status)
 | 
			
		||||
        self.message = try container.decode(String.self, forKey: .message)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct PrivateMessageSendData: Decodable {
 | 
			
		||||
    let messageId: String
 | 
			
		||||
    let chatId: String
 | 
			
		||||
@ -180,6 +202,34 @@ struct ChatProfile: Decodable {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension ChatProfile {
 | 
			
		||||
    init(
 | 
			
		||||
        userId: String,
 | 
			
		||||
        login: String?,
 | 
			
		||||
        fullName: String?,
 | 
			
		||||
        customName: String?,
 | 
			
		||||
        bio: String? = nil,
 | 
			
		||||
        lastSeen: Int? = nil,
 | 
			
		||||
        createdAt: Date? = nil,
 | 
			
		||||
        stories: [JSONValue] = [],
 | 
			
		||||
        permissions: ChatPermissions? = nil,
 | 
			
		||||
        profilePermissions: ChatProfilePermissions? = nil,
 | 
			
		||||
        relationship: RelationshipStatus? = nil
 | 
			
		||||
    ) {
 | 
			
		||||
        self.userId = userId
 | 
			
		||||
        self.login = login
 | 
			
		||||
        self.fullName = fullName
 | 
			
		||||
        self.customName = customName
 | 
			
		||||
        self.bio = bio
 | 
			
		||||
        self.lastSeen = lastSeen
 | 
			
		||||
        self.createdAt = createdAt
 | 
			
		||||
        self.stories = stories
 | 
			
		||||
        self.permissions = permissions
 | 
			
		||||
        self.profilePermissions = profilePermissions
 | 
			
		||||
        self.relationship = relationship
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ChatPermissions: Decodable {
 | 
			
		||||
    let youCanSendMessage: Bool
 | 
			
		||||
    let youCanPublicInvitePermission: Bool
 | 
			
		||||
 | 
			
		||||
@ -111,6 +111,43 @@ final class ChatService {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func createOrFindPrivateChat(
 | 
			
		||||
        targetUserId: String,
 | 
			
		||||
        completion: @escaping (Result<PrivateChatCreateData, Error>) -> Void
 | 
			
		||||
    ) {
 | 
			
		||||
        let query: [String: String?] = [
 | 
			
		||||
            "target_user_id": targetUserId
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        client.request(
 | 
			
		||||
            path: "/v1/chat/private/create",
 | 
			
		||||
            method: .post,
 | 
			
		||||
            query: query,
 | 
			
		||||
            requiresAuth: true
 | 
			
		||||
        ) { [decoder] result in
 | 
			
		||||
            switch result {
 | 
			
		||||
            case .success(let response):
 | 
			
		||||
                do {
 | 
			
		||||
                    let apiResponse = try decoder.decode(APIResponse<PrivateChatCreateData>.self, from: response.data)
 | 
			
		||||
                    guard apiResponse.status == "fine" else {
 | 
			
		||||
                        let message = apiResponse.detail ?? NSLocalizedString("Не удалось создать чат.", comment: "")
 | 
			
		||||
                        completion(.failure(ChatServiceError.unexpectedStatus(message)))
 | 
			
		||||
                        return
 | 
			
		||||
                    }
 | 
			
		||||
                    completion(.success(apiResponse.data))
 | 
			
		||||
                } catch {
 | 
			
		||||
                    let debugMessage = Self.describeDecodingError(error: error, data: response.data)
 | 
			
		||||
                    if AppConfig.DEBUG {
 | 
			
		||||
                        print("[ChatService] create private chat decode failed: \(debugMessage)")
 | 
			
		||||
                    }
 | 
			
		||||
                    completion(.failure(ChatServiceError.decoding(debugDescription: debugMessage)))
 | 
			
		||||
                }
 | 
			
		||||
            case .failure(let error):
 | 
			
		||||
                completion(.failure(error))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func sendPrivateMessage(
 | 
			
		||||
        chatId: String,
 | 
			
		||||
        content: String,
 | 
			
		||||
 | 
			
		||||
@ -939,6 +939,12 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Не удалось открыть чат" : {
 | 
			
		||||
      "comment" : "Chat creation error title"
 | 
			
		||||
    },
 | 
			
		||||
    "Не удалось открыть чат." : {
 | 
			
		||||
      "comment" : "Chat creation fallback"
 | 
			
		||||
    },
 | 
			
		||||
    "Не удалось отправить сообщение." : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
@ -954,6 +960,9 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Не удалось создать чат." : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Не удалось сохранить изменения профиля." : {
 | 
			
		||||
      "comment" : "Profile update unexpected status"
 | 
			
		||||
@ -1303,6 +1312,7 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Ошибка сервера (%@)." : {
 | 
			
		||||
      "comment" : "Chat creation server status",
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
        "en" : {
 | 
			
		||||
          "stringUnit" : {
 | 
			
		||||
@ -1323,6 +1333,7 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Ошибка сети: %@" : {
 | 
			
		||||
      "comment" : "Chat creation network error",
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
        "en" : {
 | 
			
		||||
          "stringUnit" : {
 | 
			
		||||
@ -1333,7 +1344,7 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Ошибка соединения с сервером." : {
 | 
			
		||||
      "comment" : "Search error connection",
 | 
			
		||||
      "comment" : "Chat creation connection\nSearch error connection",
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
        "en" : {
 | 
			
		||||
          "stringUnit" : {
 | 
			
		||||
@ -1565,6 +1576,9 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Понятно" : {
 | 
			
		||||
      "comment" : "Chat creation error acknowledgment"
 | 
			
		||||
    },
 | 
			
		||||
    "Попробуйте изменить запрос поиска." : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
@ -1715,6 +1729,9 @@
 | 
			
		||||
    "Произошла неизвестная ошибка." : {
 | 
			
		||||
      "comment" : "Search unknown error"
 | 
			
		||||
    },
 | 
			
		||||
    "Произошла неизвестная ошибка. Попробуйте позже." : {
 | 
			
		||||
      "comment" : "Chat creation unknown error"
 | 
			
		||||
    },
 | 
			
		||||
    "Произошла ошибка." : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
        "en" : {
 | 
			
		||||
@ -1911,7 +1928,7 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Сессия истекла. Войдите снова." : {
 | 
			
		||||
      "comment" : "Search error unauthorized",
 | 
			
		||||
      "comment" : "Chat creation unauthorized\nSearch error unauthorized",
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
        "en" : {
 | 
			
		||||
          "stringUnit" : {
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ struct ChatsTab: View {
 | 
			
		||||
    @Binding var searchRevealProgress: CGFloat
 | 
			
		||||
    @Binding var searchText: String
 | 
			
		||||
    private let searchService = SearchService()
 | 
			
		||||
    private let chatService = ChatService()
 | 
			
		||||
    @AppStorage("chatRowMessageLineLimit") private var messageLineLimitSetting: Int = 2
 | 
			
		||||
    @StateObject private var viewModel = PrivateChatsViewModel()
 | 
			
		||||
    @State private var selectedChatId: String?
 | 
			
		||||
@ -24,7 +25,11 @@ struct ChatsTab: View {
 | 
			
		||||
    @State private var isGlobalSearchLoading: Bool = false
 | 
			
		||||
    @State private var globalSearchError: String?
 | 
			
		||||
    @State private var globalSearchTask: Task<Void, Never>? = nil
 | 
			
		||||
    @State private var selectedSearchUserId: UUID?
 | 
			
		||||
    @State private var creatingChatForUserId: UUID?
 | 
			
		||||
    @State private var chatCreationError: String?
 | 
			
		||||
    @State private var isChatCreationAlertPresented: Bool = false
 | 
			
		||||
    @State private var pendingChatItem: PrivateChatListItem?
 | 
			
		||||
    @State private var isPendingChatActive: Bool = false
 | 
			
		||||
 | 
			
		||||
    private let searchRevealDistance: CGFloat = 90
 | 
			
		||||
 | 
			
		||||
@ -53,6 +58,23 @@ struct ChatsTab: View {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .alert(
 | 
			
		||||
                NSLocalizedString("Не удалось открыть чат", comment: "Chat creation error title"),
 | 
			
		||||
                isPresented: Binding(
 | 
			
		||||
                    get: { isChatCreationAlertPresented && chatCreationError != nil },
 | 
			
		||||
                    set: { newValue in
 | 
			
		||||
                        isChatCreationAlertPresented = newValue
 | 
			
		||||
                        if !newValue {
 | 
			
		||||
                            chatCreationError = nil
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                presenting: chatCreationError
 | 
			
		||||
            ) { _ in
 | 
			
		||||
                Button(NSLocalizedString("Понятно", comment: "Chat creation error acknowledgment"), role: .cancel) { }
 | 
			
		||||
            } message: { error in
 | 
			
		||||
                Text(error)
 | 
			
		||||
            }
 | 
			
		||||
            .onDisappear {
 | 
			
		||||
                globalSearchTask?.cancel()
 | 
			
		||||
                globalSearchTask = nil
 | 
			
		||||
@ -69,80 +91,111 @@ struct ChatsTab: View {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var chatList: some View {
 | 
			
		||||
        List {
 | 
			
		||||
        ZStack {
 | 
			
		||||
            List {
 | 
			
		||||
//            VStack(spacing: 0) {
 | 
			
		||||
//                searchBar
 | 
			
		||||
//                    .padding(.horizontal, 16)
 | 
			
		||||
//                    .padding(.vertical, 8)
 | 
			
		||||
//            }
 | 
			
		||||
//            .background(Color(UIColor.systemBackground))
 | 
			
		||||
            
 | 
			
		||||
            if let message = viewModel.errorMessage {
 | 
			
		||||
                Section {
 | 
			
		||||
                    HStack(alignment: .top, spacing: 8) {
 | 
			
		||||
                        Image(systemName: "exclamationmark.triangle.fill")
 | 
			
		||||
                            .foregroundColor(.orange)
 | 
			
		||||
                        Text(message)
 | 
			
		||||
                            .font(.subheadline)
 | 
			
		||||
                            .foregroundColor(.orange)
 | 
			
		||||
                        Spacer(minLength: 0)
 | 
			
		||||
                        Button(action: { viewModel.refresh() }) {
 | 
			
		||||
                            Text(NSLocalizedString("Обновить", comment: ""))
 | 
			
		||||
 | 
			
		||||
                if let message = viewModel.errorMessage {
 | 
			
		||||
                    Section {
 | 
			
		||||
                        HStack(alignment: .top, spacing: 8) {
 | 
			
		||||
                            Image(systemName: "exclamationmark.triangle.fill")
 | 
			
		||||
                                .foregroundColor(.orange)
 | 
			
		||||
                            Text(message)
 | 
			
		||||
                                .font(.subheadline)
 | 
			
		||||
                                .foregroundColor(.orange)
 | 
			
		||||
                            Spacer(minLength: 0)
 | 
			
		||||
                            Button(action: { viewModel.refresh() }) {
 | 
			
		||||
                                Text(NSLocalizedString("Обновить", comment: ""))
 | 
			
		||||
                                    .font(.subheadline)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        .padding(.vertical, 4)
 | 
			
		||||
                    }
 | 
			
		||||
                    .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if isSearching {
 | 
			
		||||
                    Section(header: localSearchHeader) {
 | 
			
		||||
                        if localSearchResults.isEmpty {
 | 
			
		||||
                            emptySearchResultView
 | 
			
		||||
                                .listRowInsets(EdgeInsets(top: 24, leading: 16, bottom: 24, trailing: 16))
 | 
			
		||||
                                .listRowSeparator(.hidden)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            ForEach(localSearchResults) { chat in
 | 
			
		||||
                                chatRowItem(for: chat)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    .padding(.vertical, 4)
 | 
			
		||||
                }
 | 
			
		||||
                .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if isSearching {
 | 
			
		||||
                Section(header: localSearchHeader) {
 | 
			
		||||
                    if localSearchResults.isEmpty {
 | 
			
		||||
                        emptySearchResultView
 | 
			
		||||
                            .listRowInsets(EdgeInsets(top: 24, leading: 16, bottom: 24, trailing: 16))
 | 
			
		||||
                            .listRowSeparator(.hidden)
 | 
			
		||||
                    Section(header: globalSearchHeader) {
 | 
			
		||||
                        globalSearchContent
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    if let message = viewModel.errorMessage, viewModel.chats.isEmpty {
 | 
			
		||||
                        errorState(message: message)
 | 
			
		||||
                    } else if viewModel.chats.isEmpty {
 | 
			
		||||
                        emptyState
 | 
			
		||||
                    } else {
 | 
			
		||||
                        ForEach(localSearchResults) { chat in
 | 
			
		||||
 | 
			
		||||
                        ForEach(viewModel.chats) { chat in
 | 
			
		||||
                            chatRowItem(for: chat)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Section(header: globalSearchHeader) {
 | 
			
		||||
                    globalSearchContent
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                if let message = viewModel.errorMessage, viewModel.chats.isEmpty {
 | 
			
		||||
                    errorState(message: message)
 | 
			
		||||
                } else if viewModel.chats.isEmpty {
 | 
			
		||||
                    emptyState
 | 
			
		||||
                } else {
 | 
			
		||||
                
 | 
			
		||||
                    ForEach(viewModel.chats) { chat in
 | 
			
		||||
                        chatRowItem(for: chat)
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    if viewModel.isLoadingMore {
 | 
			
		||||
                        loadingMoreRow
 | 
			
		||||
                        if viewModel.isLoadingMore {
 | 
			
		||||
                            loadingMoreRow
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .listStyle(.plain)
 | 
			
		||||
        .modifier(ScrollDismissesKeyboardModifier())
 | 
			
		||||
        .simultaneousGesture(searchBarGesture)
 | 
			
		||||
        .simultaneousGesture(tapToDismissKeyboardGesture)
 | 
			
		||||
//        .safeAreaInset(edge: .top) {
 | 
			
		||||
//            VStack(spacing: 0) {
 | 
			
		||||
//                searchBar
 | 
			
		||||
//                    .padding(.horizontal, 16)
 | 
			
		||||
//                    .padding(.top, 8)
 | 
			
		||||
//                    .padding(.bottom, 8)
 | 
			
		||||
//                Divider()
 | 
			
		||||
            .listStyle(.plain)
 | 
			
		||||
            .modifier(ScrollDismissesKeyboardModifier())
 | 
			
		||||
            .simultaneousGesture(searchBarGesture)
 | 
			
		||||
            .simultaneousGesture(tapToDismissKeyboardGesture)
 | 
			
		||||
//            .safeAreaInset(edge: .top) {
 | 
			
		||||
//                VStack(spacing: 0) {
 | 
			
		||||
//                    searchBar
 | 
			
		||||
//                        .padding(.horizontal, 16)
 | 
			
		||||
//                        .padding(.top, 8)
 | 
			
		||||
//                        .padding(.bottom, 8)
 | 
			
		||||
//                    Divider()
 | 
			
		||||
//                }
 | 
			
		||||
//                .background(Color(UIColor.systemBackground))
 | 
			
		||||
//            }
 | 
			
		||||
//            .background(Color(UIColor.systemBackground))
 | 
			
		||||
//        }
 | 
			
		||||
 | 
			
		||||
            pendingChatNavigationLink
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 var searchBarGesture: some Gesture {
 | 
			
		||||
@ -404,7 +457,7 @@ struct ChatsTab: View {
 | 
			
		||||
 | 
			
		||||
    private func globalSearchRow(for user: UserSearchResult) -> some View {
 | 
			
		||||
        Button {
 | 
			
		||||
            selectedSearchUserId = user.userId
 | 
			
		||||
            openPrivateChat(for: user)
 | 
			
		||||
        } label: {
 | 
			
		||||
            HStack(spacing: 12) {
 | 
			
		||||
                Circle()
 | 
			
		||||
@ -440,21 +493,17 @@ struct ChatsTab: View {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .frame(maxWidth: .infinity, alignment: .leading)
 | 
			
		||||
 | 
			
		||||
                if creatingChatForUserId == user.userId {
 | 
			
		||||
                    ProgressView()
 | 
			
		||||
                        .progressViewStyle(CircularProgressViewStyle())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .padding(.vertical, 8)
 | 
			
		||||
        }
 | 
			
		||||
        .buttonStyle(.plain)
 | 
			
		||||
        .disabled(creatingChatForUserId != nil)
 | 
			
		||||
        .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
 | 
			
		||||
        .background(
 | 
			
		||||
            NavigationLink(
 | 
			
		||||
                destination: SearchResultPlaceholderView(userId: user.userId),
 | 
			
		||||
                tag: user.userId,
 | 
			
		||||
                selection: $selectedSearchUserId
 | 
			
		||||
            ) {
 | 
			
		||||
                EmptyView()
 | 
			
		||||
            }
 | 
			
		||||
            .hidden()
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -480,7 +529,6 @@ private extension ChatsTab {
 | 
			
		||||
            globalSearchResults = []
 | 
			
		||||
            globalSearchError = nil
 | 
			
		||||
            isGlobalSearchLoading = false
 | 
			
		||||
            selectedSearchUserId = nil
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -499,7 +547,6 @@ private extension ChatsTab {
 | 
			
		||||
                    isGlobalSearchLoading = false
 | 
			
		||||
                    globalSearchError = nil
 | 
			
		||||
                    globalSearchTask = nil
 | 
			
		||||
                    selectedSearchUserId = nil
 | 
			
		||||
                }
 | 
			
		||||
            } catch is CancellationError {
 | 
			
		||||
                // Ignore cancellation
 | 
			
		||||
@ -510,7 +557,6 @@ private extension ChatsTab {
 | 
			
		||||
                    isGlobalSearchLoading = false
 | 
			
		||||
                    globalSearchError = friendlyErrorMessage(for: error)
 | 
			
		||||
                    globalSearchTask = nil
 | 
			
		||||
                    selectedSearchUserId = nil
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -522,7 +568,6 @@ private extension ChatsTab {
 | 
			
		||||
        globalSearchResults = []
 | 
			
		||||
        globalSearchError = nil
 | 
			
		||||
        isGlobalSearchLoading = false
 | 
			
		||||
        selectedSearchUserId = nil
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func secondaryLine(for user: UserSearchResult) -> String? {
 | 
			
		||||
@ -552,6 +597,91 @@ private extension ChatsTab {
 | 
			
		||||
        return nil
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func openPrivateChat(for user: UserSearchResult) {
 | 
			
		||||
        guard creatingChatForUserId == nil else { return }
 | 
			
		||||
 | 
			
		||||
        dismissKeyboard()
 | 
			
		||||
        creatingChatForUserId = user.userId
 | 
			
		||||
        chatCreationError = nil
 | 
			
		||||
 | 
			
		||||
        chatService.createOrFindPrivateChat(targetUserId: user.userId.uuidString) { result in
 | 
			
		||||
            creatingChatForUserId = nil
 | 
			
		||||
 | 
			
		||||
            switch result {
 | 
			
		||||
            case .success(let data):
 | 
			
		||||
                let chatItem = PrivateChatListItem(
 | 
			
		||||
                    chatId: data.chatId,
 | 
			
		||||
                    chatType: data.chatType,
 | 
			
		||||
                    chatData: chatProfile(from: user),
 | 
			
		||||
                    lastMessage: nil,
 | 
			
		||||
                    createdAt: nil,
 | 
			
		||||
                    unreadCount: 0
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                pendingChatItem = chatItem
 | 
			
		||||
                isPendingChatActive = true
 | 
			
		||||
                viewModel.refresh()
 | 
			
		||||
            case .failure(let error):
 | 
			
		||||
                chatCreationError = friendlyChatCreationMessage(for: error)
 | 
			
		||||
                isChatCreationAlertPresented = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func chatProfile(from user: UserSearchResult) -> ChatProfile {
 | 
			
		||||
        let profile = user.profile
 | 
			
		||||
        let login = profile?.login ?? user.login
 | 
			
		||||
        let fullName = user.officialFullName ?? user.fullName ?? profile?.fullName
 | 
			
		||||
        let customName = user.preferredCustomName ?? user.customName ?? profile?.customName
 | 
			
		||||
 | 
			
		||||
        return ChatProfile(
 | 
			
		||||
            userId: user.userId.uuidString,
 | 
			
		||||
            login: login,
 | 
			
		||||
            fullName: fullName,
 | 
			
		||||
            customName: customName,
 | 
			
		||||
            bio: profile?.bio,
 | 
			
		||||
            lastSeen: profile?.lastSeen,
 | 
			
		||||
            createdAt: profile?.createdAt
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    func friendlyErrorMessage(for error: Error) -> String {
 | 
			
		||||
        if let searchError = error as? SearchServiceError {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user