239 lines
7.3 KiB
Swift
239 lines
7.3 KiB
Swift
//
|
||
// ChatsTab.swift
|
||
// VolnahubApp
|
||
//
|
||
// Created by cheykrym on 09/06/2025.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
struct ChatsTab: View {
|
||
@StateObject private var viewModel = PrivateChatsViewModel()
|
||
|
||
var body: some View {
|
||
content
|
||
.background(Color(UIColor.systemBackground))
|
||
.onAppear {
|
||
viewModel.loadInitialChats()
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var content: some View {
|
||
if viewModel.isInitialLoading && viewModel.chats.isEmpty {
|
||
loadingState
|
||
} else if let message = viewModel.errorMessage, viewModel.chats.isEmpty {
|
||
errorState(message: message)
|
||
} else if viewModel.chats.isEmpty {
|
||
emptyState
|
||
} else {
|
||
chatList
|
||
}
|
||
}
|
||
|
||
private var chatList: some View {
|
||
List {
|
||
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))
|
||
}
|
||
|
||
ForEach(viewModel.chats) { chat in
|
||
ChatRowView(chat: chat)
|
||
.contentShape(Rectangle())
|
||
.onAppear {
|
||
viewModel.loadMoreIfNeeded(currentItem: chat)
|
||
}
|
||
}
|
||
|
||
if viewModel.isLoadingMore {
|
||
loadingMoreRow
|
||
}
|
||
}
|
||
.listStyle(.plain)
|
||
}
|
||
|
||
private var loadingState: some View {
|
||
VStack(spacing: 12) {
|
||
ProgressView()
|
||
Text(NSLocalizedString("Загружаем чаты…", comment: ""))
|
||
.font(.subheadline)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
}
|
||
|
||
private func errorState(message: String) -> some View {
|
||
VStack(spacing: 12) {
|
||
Image(systemName: "exclamationmark.bubble")
|
||
.font(.system(size: 48))
|
||
.foregroundColor(.orange)
|
||
Text(message)
|
||
.font(.body)
|
||
.multilineTextAlignment(.center)
|
||
.foregroundColor(.primary)
|
||
Button(action: { viewModel.loadInitialChats(force: true) }) {
|
||
Text(NSLocalizedString("Повторить", comment: ""))
|
||
.font(.headline)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
}
|
||
.padding()
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
}
|
||
|
||
private var emptyState: some View {
|
||
VStack(spacing: 12) {
|
||
Image(systemName: "bubble.left")
|
||
.font(.system(size: 48))
|
||
.foregroundColor(.secondary)
|
||
Text(NSLocalizedString("Пока что у вас нет чатов", comment: ""))
|
||
.font(.body)
|
||
.foregroundColor(.secondary)
|
||
Button(action: { viewModel.refresh() }) {
|
||
Text(NSLocalizedString("Обновить", comment: ""))
|
||
}
|
||
.buttonStyle(.bordered)
|
||
}
|
||
.padding()
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
}
|
||
|
||
private var loadingMoreRow: some View {
|
||
HStack {
|
||
Spacer()
|
||
ProgressView()
|
||
.padding(.vertical, 12)
|
||
Spacer()
|
||
}
|
||
.listRowSeparator(.hidden)
|
||
}
|
||
}
|
||
|
||
private struct ChatRowView: View {
|
||
let chat: PrivateChatListItem
|
||
|
||
private var title: String {
|
||
switch chat.chatType {
|
||
case .self:
|
||
return NSLocalizedString("Избранные сообщения", comment: "")
|
||
case .privateChat, .unknown:
|
||
if let custom = chat.chatData?.customName, !custom.isEmpty {
|
||
return custom
|
||
}
|
||
if let full = chat.chatData?.fullName, !full.isEmpty {
|
||
return full
|
||
}
|
||
if let login = chat.chatData?.login, !login.isEmpty {
|
||
return "@\(login)"
|
||
}
|
||
return NSLocalizedString("Неизвестный пользователь", comment: "")
|
||
}
|
||
}
|
||
|
||
private var subtitle: String {
|
||
guard let message = chat.lastMessage else {
|
||
return NSLocalizedString("Нет сообщений", comment: "")
|
||
}
|
||
|
||
if let content = message.content, !content.isEmpty {
|
||
return content
|
||
}
|
||
|
||
if message.mediaLink != nil {
|
||
return NSLocalizedString("Вложение", comment: "")
|
||
}
|
||
|
||
return NSLocalizedString("Сообщение", comment: "")
|
||
}
|
||
|
||
private var timestamp: String? {
|
||
let date = chat.lastMessage?.createdAt ?? chat.createdAt
|
||
guard let date else { return nil }
|
||
return ChatRowView.timeFormatter.string(from: date)
|
||
}
|
||
|
||
private var initial: String {
|
||
return String(title.prefix(1)).uppercased()
|
||
}
|
||
|
||
private var subtitleColor: Color {
|
||
chat.unreadCount > 0 ? .primary : .secondary
|
||
}
|
||
|
||
var body: some View {
|
||
HStack(spacing: 12) {
|
||
Circle()
|
||
.fill(Color.accentColor.opacity(0.15))
|
||
.frame(width: 44, height: 44)
|
||
.overlay(
|
||
Text(initial)
|
||
.font(.headline)
|
||
.foregroundColor(Color.accentColor)
|
||
)
|
||
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text(title)
|
||
.fontWeight(chat.unreadCount > 0 ? .semibold : .regular)
|
||
.foregroundColor(.primary)
|
||
.lineLimit(1)
|
||
|
||
Text(subtitle)
|
||
.font(.subheadline)
|
||
.foregroundColor(subtitleColor)
|
||
.lineLimit(2)
|
||
}
|
||
|
||
Spacer()
|
||
|
||
VStack(alignment: .trailing, spacing: 6) {
|
||
if let timestamp {
|
||
Text(timestamp)
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
if chat.unreadCount > 0 {
|
||
Text("\(chat.unreadCount)")
|
||
.font(.caption2.bold())
|
||
.foregroundColor(.white)
|
||
.padding(.horizontal, 8)
|
||
.padding(.vertical, 4)
|
||
.background(
|
||
Capsule().fill(Color.accentColor)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
.padding(.vertical, 8)
|
||
}
|
||
|
||
private static let timeFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.dateStyle = .short
|
||
formatter.timeStyle = .short
|
||
return formatter
|
||
}()
|
||
}
|
||
|
||
struct ChatsTab_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
ChatsTab()
|
||
.environmentObject(ThemeManager())
|
||
}
|
||
}
|