add history load
This commit is contained in:
		
							parent
							
								
									c9bfab0b14
								
							
						
					
					
						commit
						207187a439
					
				@ -5,6 +5,11 @@ struct PrivateChatListData: Decodable {
 | 
			
		||||
    let hasMore: Bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct PrivateChatHistoryData: Decodable {
 | 
			
		||||
    let items: [MessageItem]
 | 
			
		||||
    let hasMore: Bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct PrivateChatListItem: Decodable, Identifiable {
 | 
			
		||||
    enum ChatType: String, Decodable {
 | 
			
		||||
        case `self`
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,51 @@ final class ChatService {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func fetchPrivateChatHistory(
 | 
			
		||||
        chatId: String,
 | 
			
		||||
        beforeMessageId: String?,
 | 
			
		||||
        limit: Int,
 | 
			
		||||
        completion: @escaping (Result<PrivateChatHistoryData, Error>) -> Void
 | 
			
		||||
    ) {
 | 
			
		||||
        var query: [String: String?] = [
 | 
			
		||||
            "chat_id": chatId,
 | 
			
		||||
            "limit": String(limit)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        if let beforeMessageId,
 | 
			
		||||
           !beforeMessageId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
 | 
			
		||||
            query["before_message_id"] = beforeMessageId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        client.request(
 | 
			
		||||
            path: "/v1/chat/private/history",
 | 
			
		||||
            method: .get,
 | 
			
		||||
            query: query,
 | 
			
		||||
            requiresAuth: true
 | 
			
		||||
        ) { [decoder] result in
 | 
			
		||||
            switch result {
 | 
			
		||||
            case .success(let response):
 | 
			
		||||
                do {
 | 
			
		||||
                    let apiResponse = try decoder.decode(APIResponse<PrivateChatHistoryData>.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] decode private chat history failed: \(debugMessage)")
 | 
			
		||||
                    }
 | 
			
		||||
                    completion(.failure(ChatServiceError.decoding(debugDescription: debugMessage)))
 | 
			
		||||
                }
 | 
			
		||||
            case .failure(let error):
 | 
			
		||||
                completion(.failure(error))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static func decodeDate(from decoder: Decoder) throws -> Date {
 | 
			
		||||
        let container = try decoder.singleValueContainer()
 | 
			
		||||
        let string = try container.decode(String.self)
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "sourceLanguage" : "ru",
 | 
			
		||||
  "strings" : {
 | 
			
		||||
    "(без текста)" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "@%@" : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
        "en" : {
 | 
			
		||||
@ -57,15 +60,9 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Chat ID:" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Companion ID" : {
 | 
			
		||||
      "comment" : "Search placeholder companion title"
 | 
			
		||||
    },
 | 
			
		||||
    "Companion ID:" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "DEBUG UPDATE" : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
@ -210,6 +207,9 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "В чате пока нет сообщений." : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Ваш e-mail" : {
 | 
			
		||||
      "comment" : "feedback: email placeholder",
 | 
			
		||||
@ -414,6 +414,9 @@
 | 
			
		||||
    },
 | 
			
		||||
    "Заглушка: Хранилище данных" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Загружаем ранние сообщения…" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Загружаем чаты…" : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
@ -424,6 +427,9 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Загрузка сообщений…" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Загрузка..." : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
@ -872,6 +878,9 @@
 | 
			
		||||
    },
 | 
			
		||||
    "Не удалось выполнить поиск." : {
 | 
			
		||||
      "comment" : "Search error fallback\nSearch service decoding error"
 | 
			
		||||
    },
 | 
			
		||||
    "Не удалось загрузить историю чата." : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Не удалось загрузить профиль." : {
 | 
			
		||||
      "comment" : "Profile service decoding error\nProfile unexpected status"
 | 
			
		||||
@ -1048,9 +1057,6 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Неизвестный" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Неизвестный пользователь" : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
@ -2232,9 +2238,6 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Экран чата в разработке" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Язык" : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										128
									
								
								yobble/ViewModels/PrivateChatViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								yobble/ViewModels/PrivateChatViewModel.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,128 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
@MainActor
 | 
			
		||||
final class PrivateChatViewModel: ObservableObject {
 | 
			
		||||
    @Published private(set) var messages: [MessageItem] = []
 | 
			
		||||
    @Published private(set) var isInitialLoading: Bool = false
 | 
			
		||||
    @Published private(set) var isLoadingMore: Bool = false
 | 
			
		||||
    @Published var errorMessage: String?
 | 
			
		||||
 | 
			
		||||
    private let chatService: ChatService
 | 
			
		||||
    private let chatId: String
 | 
			
		||||
    private let pageSize: Int
 | 
			
		||||
    private var hasMore: Bool = true
 | 
			
		||||
    private var didLoadInitially: Bool = false
 | 
			
		||||
 | 
			
		||||
    init(chatId: String, chatService: ChatService = ChatService(), pageSize: Int = 30) {
 | 
			
		||||
        self.chatId = chatId
 | 
			
		||||
        self.chatService = chatService
 | 
			
		||||
        self.pageSize = pageSize
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func loadInitialHistory(force: Bool = false) {
 | 
			
		||||
        if !force && didLoadInitially { return }
 | 
			
		||||
        guard !isInitialLoading else { return }
 | 
			
		||||
 | 
			
		||||
        isInitialLoading = true
 | 
			
		||||
        errorMessage = nil
 | 
			
		||||
        hasMore = true
 | 
			
		||||
 | 
			
		||||
        chatService.fetchPrivateChatHistory(chatId: chatId, beforeMessageId: nil, limit: pageSize) { [weak self] result in
 | 
			
		||||
            guard let self else { return }
 | 
			
		||||
 | 
			
		||||
            switch result {
 | 
			
		||||
            case .success(let data):
 | 
			
		||||
                self.hasMore = data.hasMore
 | 
			
		||||
                self.messages = Self.merge(existing: [], newMessages: data.items)
 | 
			
		||||
                self.didLoadInitially = true
 | 
			
		||||
            case .failure(let error):
 | 
			
		||||
                self.errorMessage = self.message(for: error)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self.isInitialLoading = false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func refresh() {
 | 
			
		||||
        didLoadInitially = false
 | 
			
		||||
        loadInitialHistory(force: true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func loadMoreIfNeeded(for message: MessageItem) {
 | 
			
		||||
        guard didLoadInitially, !isInitialLoading, hasMore, !isLoadingMore else { return }
 | 
			
		||||
        guard let first = messages.first, first.id == message.id else { return }
 | 
			
		||||
 | 
			
		||||
        isLoadingMore = true
 | 
			
		||||
 | 
			
		||||
        chatService.fetchPrivateChatHistory(chatId: chatId, beforeMessageId: message.id, limit: pageSize) { [weak self] result in
 | 
			
		||||
            guard let self else { return }
 | 
			
		||||
 | 
			
		||||
            switch result {
 | 
			
		||||
            case .success(let data):
 | 
			
		||||
                self.hasMore = data.hasMore
 | 
			
		||||
                if !data.items.isEmpty {
 | 
			
		||||
                    self.messages = Self.merge(existing: self.messages, newMessages: data.items)
 | 
			
		||||
                }
 | 
			
		||||
            case .failure(let error):
 | 
			
		||||
                self.errorMessage = self.message(for: error)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self.isLoadingMore = false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func message(for error: Error) -> String {
 | 
			
		||||
        if let chatError = error as? ChatServiceError {
 | 
			
		||||
            return chatError.errorDescription ?? NSLocalizedString("Не удалось загрузить историю чата.", comment: "")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let networkError = error as? NetworkError {
 | 
			
		||||
            switch networkError {
 | 
			
		||||
            case .unauthorized:
 | 
			
		||||
                return NSLocalizedString("Сессия истекла. Войдите снова.", comment: "")
 | 
			
		||||
            case .invalidURL, .noResponse:
 | 
			
		||||
                return NSLocalizedString("Ошибка соединения с сервером.", comment: "")
 | 
			
		||||
            case .network(let underlying):
 | 
			
		||||
                return String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), underlying.localizedDescription)
 | 
			
		||||
            case .server(let statusCode, _):
 | 
			
		||||
                return String(format: NSLocalizedString("Ошибка сервера (%@).", comment: ""), "\(statusCode)")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return NSLocalizedString("Неизвестная ошибка. Попробуйте позже.", comment: "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static func merge(existing: [MessageItem], newMessages: [MessageItem]) -> [MessageItem] {
 | 
			
		||||
        var combined: [MessageItem] = []
 | 
			
		||||
        combined.reserveCapacity(existing.count + newMessages.count)
 | 
			
		||||
 | 
			
		||||
        var seen: Set<String> = []
 | 
			
		||||
 | 
			
		||||
        for message in newMessages {
 | 
			
		||||
            if seen.insert(message.id).inserted {
 | 
			
		||||
                combined.append(message)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for message in existing {
 | 
			
		||||
            if seen.insert(message.id).inserted {
 | 
			
		||||
                combined.append(message)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        combined.sort(by: compare)
 | 
			
		||||
        return combined
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static func compare(lhs: MessageItem, rhs: MessageItem) -> Bool {
 | 
			
		||||
        if let lhsDate = lhs.createdAt, let rhsDate = rhs.createdAt, lhsDate != rhsDate {
 | 
			
		||||
            return lhsDate < rhsDate
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let lhsId = Int(lhs.messageId), let rhsId = Int(rhs.messageId), lhsId != rhsId {
 | 
			
		||||
            return lhsId < rhsId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return lhs.messageId < rhs.messageId
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										211
									
								
								yobble/Views/Chat/PrivateChatView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								yobble/Views/Chat/PrivateChatView.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,211 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct PrivateChatView: View {
 | 
			
		||||
    let chat: PrivateChatListItem
 | 
			
		||||
    let currentUserId: String?
 | 
			
		||||
 | 
			
		||||
    @StateObject private var viewModel: PrivateChatViewModel
 | 
			
		||||
    @State private var hasPositionedToBottom: Bool = false
 | 
			
		||||
 | 
			
		||||
    init(chat: PrivateChatListItem, currentUserId: String?) {
 | 
			
		||||
        self.chat = chat
 | 
			
		||||
        self.currentUserId = currentUserId
 | 
			
		||||
        _viewModel = StateObject(wrappedValue: PrivateChatViewModel(chatId: chat.chatId))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        ScrollViewReader { proxy in
 | 
			
		||||
            content
 | 
			
		||||
                .onChange(of: viewModel.messages.count) { _ in
 | 
			
		||||
                    guard !viewModel.isLoadingMore,
 | 
			
		||||
                          let lastId = viewModel.messages.last?.id else { return }
 | 
			
		||||
                    DispatchQueue.main.async {
 | 
			
		||||
                        withAnimation(.easeInOut(duration: 0.2)) {
 | 
			
		||||
                            proxy.scrollTo(lastId, anchor: .bottom)
 | 
			
		||||
                        }
 | 
			
		||||
                        hasPositionedToBottom = true
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
        .navigationTitle(title)
 | 
			
		||||
        .navigationBarTitleDisplayMode(.inline)
 | 
			
		||||
        .task {
 | 
			
		||||
            viewModel.loadInitialHistory()
 | 
			
		||||
        }
 | 
			
		||||
        .onChange(of: viewModel.isInitialLoading) { isLoading in
 | 
			
		||||
            if isLoading {
 | 
			
		||||
                hasPositionedToBottom = false
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ViewBuilder
 | 
			
		||||
    private var content: some View {
 | 
			
		||||
        if viewModel.isInitialLoading && viewModel.messages.isEmpty {
 | 
			
		||||
            ProgressView(NSLocalizedString("Загрузка сообщений…", comment: ""))
 | 
			
		||||
                .frame(maxWidth: .infinity, maxHeight: .infinity)
 | 
			
		||||
        } else if let error = viewModel.errorMessage, viewModel.messages.isEmpty {
 | 
			
		||||
            errorView(message: error)
 | 
			
		||||
        } else {
 | 
			
		||||
            ScrollView {
 | 
			
		||||
                LazyVStack(alignment: .leading, spacing: 12) {
 | 
			
		||||
                    if viewModel.isLoadingMore {
 | 
			
		||||
                        loadingMoreView
 | 
			
		||||
                    } else if viewModel.messages.isEmpty {
 | 
			
		||||
                        emptyState
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    ForEach(viewModel.messages) { message in
 | 
			
		||||
                        messageRow(for: message)
 | 
			
		||||
                            .id(message.id)
 | 
			
		||||
                            .onAppear {
 | 
			
		||||
                                guard hasPositionedToBottom else { return }
 | 
			
		||||
                                viewModel.loadMoreIfNeeded(for: message)
 | 
			
		||||
                            }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if let message = viewModel.errorMessage,
 | 
			
		||||
                       !message.isEmpty,
 | 
			
		||||
                       !viewModel.messages.isEmpty {
 | 
			
		||||
                        errorBanner(message: message)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .padding(.vertical, 12)
 | 
			
		||||
            }
 | 
			
		||||
            .refreshable {
 | 
			
		||||
                viewModel.refresh()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var emptyState: some View {
 | 
			
		||||
        Text(NSLocalizedString("В чате пока нет сообщений.", comment: ""))
 | 
			
		||||
            .font(.footnote)
 | 
			
		||||
            .foregroundColor(.secondary)
 | 
			
		||||
            .frame(maxWidth: .infinity, alignment: .center)
 | 
			
		||||
            .padding(.top, 32)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var loadingMoreView: some View {
 | 
			
		||||
        HStack {
 | 
			
		||||
            ProgressView()
 | 
			
		||||
            Text(NSLocalizedString("Загружаем ранние сообщения…", comment: ""))
 | 
			
		||||
                .font(.caption)
 | 
			
		||||
        }
 | 
			
		||||
        .frame(maxWidth: .infinity)
 | 
			
		||||
        .padding(.vertical, 8)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func errorView(message: String) -> some View {
 | 
			
		||||
        VStack(spacing: 12) {
 | 
			
		||||
            Text(message)
 | 
			
		||||
                .font(.body)
 | 
			
		||||
                .multilineTextAlignment(.center)
 | 
			
		||||
                .foregroundColor(.primary)
 | 
			
		||||
 | 
			
		||||
            Button(action: { viewModel.refresh() }) {
 | 
			
		||||
                Text(NSLocalizedString("Повторить", comment: ""))
 | 
			
		||||
                    .font(.body)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .padding()
 | 
			
		||||
        .frame(maxWidth: .infinity, maxHeight: .infinity)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func messageRow(for message: MessageItem) -> some View {
 | 
			
		||||
        let isCurrentUser = currentUserId.map { $0 == message.senderId } ?? false
 | 
			
		||||
        return HStack {
 | 
			
		||||
            if isCurrentUser { Spacer(minLength: 32) }
 | 
			
		||||
 | 
			
		||||
            VStack(alignment: isCurrentUser ? .trailing : .leading, spacing: 6) {
 | 
			
		||||
                if !isCurrentUser {
 | 
			
		||||
                    Text(senderName(for: message))
 | 
			
		||||
                        .font(.caption)
 | 
			
		||||
                        .foregroundColor(.secondary)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Text(contentText(for: message))
 | 
			
		||||
                    .font(.body)
 | 
			
		||||
                    .foregroundColor(isCurrentUser ? .white : .primary)
 | 
			
		||||
                    .frame(maxWidth: .infinity, alignment: isCurrentUser ? .trailing : .leading)
 | 
			
		||||
 | 
			
		||||
                Text(timestamp(for: message))
 | 
			
		||||
                    .font(.caption2)
 | 
			
		||||
                    .foregroundColor(isCurrentUser ? Color.white.opacity(0.8) : .secondary)
 | 
			
		||||
            }
 | 
			
		||||
            .padding(.vertical, 10)
 | 
			
		||||
            .padding(.horizontal, 12)
 | 
			
		||||
            .background(isCurrentUser ? Color.accentColor : Color(.secondarySystemBackground))
 | 
			
		||||
            .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
 | 
			
		||||
 | 
			
		||||
            if !isCurrentUser { Spacer(minLength: 32) }
 | 
			
		||||
        }
 | 
			
		||||
        .padding(.horizontal, 16)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func senderName(for message: MessageItem) -> String {
 | 
			
		||||
        if let full = message.senderData?.fullName, !full.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
 | 
			
		||||
            return full
 | 
			
		||||
        }
 | 
			
		||||
        if let custom = message.senderData?.customName, !custom.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
 | 
			
		||||
            return custom
 | 
			
		||||
        }
 | 
			
		||||
        if let login = message.senderData?.login, !login.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
 | 
			
		||||
            return "@\(login)"
 | 
			
		||||
        }
 | 
			
		||||
        return message.senderId
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func timestamp(for message: MessageItem) -> String {
 | 
			
		||||
        guard let date = message.createdAt else {
 | 
			
		||||
            return ""
 | 
			
		||||
        }
 | 
			
		||||
        return Self.timeFormatter.string(from: date)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func contentText(for message: MessageItem) -> String {
 | 
			
		||||
        guard let content = message.content?.trimmingCharacters(in: .whitespacesAndNewlines), !content.isEmpty else {
 | 
			
		||||
            return NSLocalizedString("(без текста)", comment: "")
 | 
			
		||||
        }
 | 
			
		||||
        return content
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func errorBanner(message: String) -> some View {
 | 
			
		||||
        HStack {
 | 
			
		||||
            Image(systemName: "exclamationmark.triangle.fill")
 | 
			
		||||
                .foregroundColor(.orange)
 | 
			
		||||
            Text(message)
 | 
			
		||||
                .font(.footnote)
 | 
			
		||||
                .foregroundColor(.primary)
 | 
			
		||||
        }
 | 
			
		||||
        .padding()
 | 
			
		||||
        .background(Color(.secondarySystemBackground))
 | 
			
		||||
        .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
 | 
			
		||||
        .padding(.horizontal, 16)
 | 
			
		||||
        .padding(.top, 8)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static let timeFormatter: DateFormatter = {
 | 
			
		||||
        let formatter = DateFormatter()
 | 
			
		||||
        formatter.dateStyle = .none
 | 
			
		||||
        formatter.timeStyle = .short
 | 
			
		||||
        return formatter
 | 
			
		||||
    }()
 | 
			
		||||
 | 
			
		||||
    private var title: String {
 | 
			
		||||
        if let full = chat.chatData?.fullName, !full.isEmpty {
 | 
			
		||||
            return full
 | 
			
		||||
        }
 | 
			
		||||
        if let custom = chat.chatData?.customName, !custom.isEmpty {
 | 
			
		||||
            return custom
 | 
			
		||||
        }
 | 
			
		||||
        if let login = chat.chatData?.login, !login.isEmpty {
 | 
			
		||||
            return "@\(login)"
 | 
			
		||||
        }
 | 
			
		||||
        return NSLocalizedString("Чат", comment: "")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: - Preview
 | 
			
		||||
 | 
			
		||||
// Previews intentionally omitted - MessageItem has custom decoding-only initializer.
 | 
			
		||||
@ -320,7 +320,7 @@ struct ChatsTab: View {
 | 
			
		||||
        }
 | 
			
		||||
        .background(
 | 
			
		||||
            NavigationLink(
 | 
			
		||||
                destination: ChatPlaceholderView(chat: chat),
 | 
			
		||||
                destination: PrivateChatView(chat: chat, currentUserId: currentUserId),
 | 
			
		||||
                tag: chat.chatId,
 | 
			
		||||
                selection: $selectedChatId
 | 
			
		||||
            ) {
 | 
			
		||||
@ -965,66 +965,6 @@ struct ChatsTab_Previews: PreviewProvider {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private struct ChatPlaceholderView: View {
 | 
			
		||||
    let chat: PrivateChatListItem
 | 
			
		||||
 | 
			
		||||
    private var companionId: String {
 | 
			
		||||
        if let profileId = chat.chatData?.userId {
 | 
			
		||||
            return profileId
 | 
			
		||||
        }
 | 
			
		||||
        if let senderId = chat.lastMessage?.senderId {
 | 
			
		||||
            return senderId
 | 
			
		||||
        }
 | 
			
		||||
        return NSLocalizedString("Неизвестный", comment: "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(spacing: 16) {
 | 
			
		||||
            Text(NSLocalizedString("Экран чата в разработке", comment: ""))
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
 | 
			
		||||
            VStack(alignment: .leading, spacing: 8) {
 | 
			
		||||
                HStack {
 | 
			
		||||
                    Text("Chat ID:")
 | 
			
		||||
                        .font(.subheadline)
 | 
			
		||||
                        .foregroundColor(.secondary)
 | 
			
		||||
                    Text(chat.chatId)
 | 
			
		||||
                        .font(.body.monospaced())
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if companionId != NSLocalizedString("Неизвестный", comment: "") {
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        Text("Companion ID:")
 | 
			
		||||
                            .font(.subheadline)
 | 
			
		||||
                            .foregroundColor(.secondary)
 | 
			
		||||
                        Text(companionId)
 | 
			
		||||
                            .font(.body.monospaced())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .frame(maxWidth: .infinity, alignment: .leading)
 | 
			
		||||
 | 
			
		||||
            Spacer()
 | 
			
		||||
        }
 | 
			
		||||
        .padding()
 | 
			
		||||
        .navigationTitle(title)
 | 
			
		||||
        .navigationBarTitleDisplayMode(.inline)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var title: String {
 | 
			
		||||
        if let full = chat.chatData?.fullName, !full.isEmpty {
 | 
			
		||||
            return full
 | 
			
		||||
        }
 | 
			
		||||
        if let custom = chat.chatData?.customName, !custom.isEmpty {
 | 
			
		||||
            return custom
 | 
			
		||||
        }
 | 
			
		||||
        if let login = chat.chatData?.login, !login.isEmpty {
 | 
			
		||||
            return "@\(login)"
 | 
			
		||||
        }
 | 
			
		||||
        return NSLocalizedString("Чат", comment: "")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension Notification.Name {
 | 
			
		||||
    static let debugRefreshChats = Notification.Name("debugRefreshChats")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user