patch chats list

This commit is contained in:
cheykrym 2025-10-06 05:30:05 +03:00
parent bc3ec97d67
commit b8d22c814c
4 changed files with 237 additions and 20 deletions

View File

@ -17,6 +17,10 @@ struct TopBarView: View {
return title == "Home" return title == "Home"
} }
var isChatsTab: Bool {
return title == "Chats"
}
var isProfileTab: Bool { var isProfileTab: Bool {
return title == "Profile" return title == "Profile"
} }
@ -81,6 +85,15 @@ struct TopBarView: View {
.foregroundColor(.primary) .foregroundColor(.primary)
} }
} }
} else if isChatsTab {
// Кнопка поиска
Button(action: {
// пока ничего не делаем
}) {
Image(systemName: "magnifyingglass")
.imageScale(.large)
.foregroundColor(.primary)
}
} else if isProfileTab { } else if isProfileTab {
NavigationLink(destination: SettingsView(viewModel: viewModel)) { NavigationLink(destination: SettingsView(viewModel: viewModel)) {
Image(systemName: "wrench") Image(systemName: "wrench")

View File

@ -98,6 +98,9 @@
} }
} }
} }
},
"Вы" : {
}, },
"Вы предложили: %@" : { "Вы предложили: %@" : {

View File

@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
struct ChatsTab: View { struct ChatsTab: View {
var currentUserId: String? = nil
@StateObject private var viewModel = PrivateChatsViewModel() @StateObject private var viewModel = PrivateChatsViewModel()
var body: some View { var body: some View {
@ -53,7 +54,7 @@ struct ChatsTab: View {
} }
ForEach(viewModel.chats) { chat in ForEach(viewModel.chats) { chat in
ChatRowView(chat: chat) ChatRowView(chat: chat, currentUserId: currentUserId)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onAppear { .onAppear {
viewModel.loadMoreIfNeeded(currentItem: chat) viewModel.loadMoreIfNeeded(currentItem: chat)
@ -126,6 +127,7 @@ struct ChatsTab: View {
private struct ChatRowView: View { private struct ChatRowView: View {
let chat: PrivateChatListItem let chat: PrivateChatListItem
let currentUserId: String?
private var title: String { private var title: String {
switch chat.chatType { switch chat.chatType {
@ -145,12 +147,47 @@ private struct ChatRowView: View {
} }
} }
private var subtitle: String { private var officialFullName: String? {
guard let name = chat.chatData?.fullName?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty else {
return nil
}
return name
}
private var loginDisplay: String? {
guard let login = chat.chatData?.login?.trimmingCharacters(in: .whitespacesAndNewlines), !login.isEmpty else {
return nil
}
return "@\(login)"
}
private var isOfficial: Bool {
officialFullName != nil
}
private var avatarBackgroundColor: Color {
isOfficial ? Color.accentColor.opacity(0.85) : Color.accentColor.opacity(0.15)
}
private var avatarTextColor: Color {
isOfficial ? Color.white : Color.accentColor
}
private var messagePreview: String {
guard let message = chat.lastMessage else { guard let message = chat.lastMessage else {
return NSLocalizedString("Нет сообщений", comment: "") return NSLocalizedString("Нет сообщений", comment: "")
} }
if let content = message.content, !content.isEmpty { let body = messageBody(for: message)
guard let prefix = authorPrefix(for: message) else {
return body
}
return body.isEmpty ? prefix : "\(body)"
// return body.isEmpty ? prefix : "\(prefix): \(body)"
}
private func messageBody(for message: MessageItem) -> String {
if let content = trimmed(message.content), !content.isEmpty {
return content return content
} }
@ -161,50 +198,158 @@ private struct ChatRowView: View {
return NSLocalizedString("Сообщение", comment: "") return NSLocalizedString("Сообщение", comment: "")
} }
private func authorPrefix(for message: MessageItem) -> String? {
let isCurrentUser = currentUserId.map { $0 == message.senderId } ?? false
if isCurrentUser {
return NSLocalizedString("Вы", comment: "")
}
let profile = message.senderData ?? chat.chatData
return displayName(for: profile)
}
private func displayName(for profile: ChatProfile?) -> String? {
guard let profile else { return nil }
let login = trimmed(profile.login)
let customName = trimmed(profile.customName)
let fullName = trimmed(profile.fullName)
let isOfficialProfile = fullName != nil
if isOfficialProfile, let login {
if let customName {
return "@\(login) (\(customName))"
}
return "@\(login)"
}
if let customName {
return customName
}
if let login {
return "@\(login)"
}
return fullName
}
private func trimmed(_ string: String?) -> String? {
guard let string = string?.trimmingCharacters(in: .whitespacesAndNewlines), !string.isEmpty else {
return nil
}
return string
}
private var timestamp: String? { private var timestamp: String? {
let date = chat.lastMessage?.createdAt ?? chat.createdAt let date = chat.lastMessage?.createdAt ?? chat.createdAt
guard let date else { return nil } guard let date else { return nil }
return ChatRowView.timeFormatter.string(from: date) return ChatRowView.formattedTimestamp(for: date)
} }
private var initial: String { private var initial: String {
return String(title.prefix(1)).uppercased() let sourceName: String
if let full = officialFullName {
sourceName = full
} else if let custom = chat.chatData?.customName, !custom.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
sourceName = custom
} else if let login = chat.chatData?.login?.trimmingCharacters(in: .whitespacesAndNewlines), !login.isEmpty {
sourceName = login
} else {
sourceName = NSLocalizedString("Неизвестный пользователь", comment: "")
}
if let character = sourceName.first(where: { !$0.isWhitespace && $0 != "@" }) {
return String(character).uppercased()
}
return "?"
} }
private var subtitleColor: Color { private var subtitleColor: Color {
chat.unreadCount > 0 ? .primary : .secondary chat.unreadCount > 0 ? .primary : .secondary
} }
private var shouldShowReadStatus: Bool {
guard let message = chat.lastMessage else { return false }
let isCurrentUser = currentUserId.map { $0 == message.senderId } ?? false
return isCurrentUser
}
private var readStatusIconName: String {
guard let message = chat.lastMessage else { return "" }
return message.isViewed == true ? "checkmark.circle.fill" : "checkmark.circle"
}
private var readStatusColor: Color {
guard let message = chat.lastMessage else { return .secondary }
return message.isViewed == true ? Color.accentColor : Color.secondary
}
var body: some View { var body: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Circle() Circle()
.fill(Color.accentColor.opacity(0.15)) .fill(avatarBackgroundColor)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.overlay( .overlay(
Text(initial) Text(initial)
.font(.headline) .font(.headline)
.foregroundColor(Color.accentColor) .foregroundColor(avatarTextColor)
) )
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(title) if let officialName = officialFullName {
.fontWeight(chat.unreadCount > 0 ? .semibold : .regular) HStack(spacing: 6) {
.foregroundColor(.primary) Text(officialName)
.lineLimit(1) .fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
.truncationMode(.tail)
Text(subtitle) Image(systemName: "checkmark.seal.fill")
.foregroundColor(Color.accentColor)
.font(.caption)
}
// if let login = loginDisplay {
// Text(login)
// .font(.footnote)
// .foregroundColor(.secondary)
// .lineLimit(1)
// .truncationMode(.tail)
// }
} else {
Text(title)
.fontWeight(chat.unreadCount > 0 ? .semibold : .regular)
.foregroundColor(.primary)
.lineLimit(1)
.truncationMode(.tail)
}
Text(messagePreview)
.font(.subheadline) .font(.subheadline)
.foregroundColor(subtitleColor) .foregroundColor(subtitleColor)
.lineLimit(2) .lineLimit(2)
.truncationMode(.tail)
} }
.frame(maxWidth: .infinity, alignment: .leading)
Spacer() Spacer()
VStack(alignment: .trailing, spacing: 6) { VStack(alignment: .trailing, spacing: 6) {
if let timestamp { if let timestamp {
Text(timestamp) HStack(spacing: 4) {
.font(.caption) if shouldShowReadStatus {
.foregroundColor(.secondary) Image(systemName: readStatusIconName)
.foregroundColor(readStatusColor)
.font(.caption2)
}
Text(timestamp)
.font(.caption)
.foregroundColor(.secondary)
}
} }
if chat.unreadCount > 0 { if chat.unreadCount > 0 {
@ -222,12 +367,68 @@ private struct ChatRowView: View {
.padding(.vertical, 8) .padding(.vertical, 8)
} }
private static let timeFormatter: DateFormatter = { private static func formattedTimestamp(for date: Date) -> String {
let calendar = Calendar.current
let locale = Locale.current
let now = Date()
let startOfNow = calendar.startOfDay(for: now)
let startOfDate = calendar.startOfDay(for: date)
let diff = calendar.dateComponents([.day], from: startOfDate, to: startOfNow).day ?? 0
if diff <= 0 {
return timeString(for: date, locale: locale)
}
if diff == 1 {
return locale.identifier.lowercased().hasPrefix("ru") ? "Вчера" : "Yesterday"
}
if diff < 7 {
return weekdayFormatter(for: locale).string(from: date)
}
if diff < 365 {
return monthDayFormatter(for: locale).string(from: date)
}
return fullDateFormatter(for: locale).string(from: date)
}
private static func timeString(for date: Date, locale: Locale) -> String {
let timeFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: locale) ?? ""
let is12Hour = timeFormat.contains("a") || timeFormat.contains("h") || timeFormat.contains("K")
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateStyle = .short formatter.locale = locale
formatter.timeStyle = .short if is12Hour {
formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "h:mm a", options: 0, locale: locale) ?? "h:mm a"
} else {
formatter.dateFormat = "HH:mm"
}
return formatter.string(from: date)
}
private static func weekdayFormatter(for locale: Locale) -> DateFormatter {
let formatter = DateFormatter()
formatter.locale = locale
formatter.setLocalizedDateFormatFromTemplate("EEE")
return formatter return formatter
}() }
private static func monthDayFormatter(for locale: Locale) -> DateFormatter {
let formatter = DateFormatter()
formatter.locale = locale
formatter.setLocalizedDateFormatFromTemplate("MMM d")
return formatter
}
private static func fullDateFormatter(for locale: Locale) -> DateFormatter {
let formatter = DateFormatter()
formatter.locale = locale
formatter.dateFormat = "dd.MM.yy"
return formatter
}
} }
struct ChatsTab_Previews: PreviewProvider { struct ChatsTab_Previews: PreviewProvider {

View File

@ -48,7 +48,7 @@ struct MainView: View {
FeedbackTab() FeedbackTab()
.opacity(selectedTab == 1 ? 1 : 0) .opacity(selectedTab == 1 ? 1 : 0)
ChatsTab() ChatsTab(currentUserId: viewModel.userId.isEmpty ? nil : viewModel.userId)
.opacity(selectedTab == 2 ? 1 : 0) .opacity(selectedTab == 2 ? 1 : 0)
ProfileTab() ProfileTab()