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

View File

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

View File

@ -8,6 +8,7 @@
import SwiftUI
struct ChatsTab: View {
var currentUserId: String? = nil
@StateObject private var viewModel = PrivateChatsViewModel()
var body: some View {
@ -53,7 +54,7 @@ struct ChatsTab: View {
}
ForEach(viewModel.chats) { chat in
ChatRowView(chat: chat)
ChatRowView(chat: chat, currentUserId: currentUserId)
.contentShape(Rectangle())
.onAppear {
viewModel.loadMoreIfNeeded(currentItem: chat)
@ -126,6 +127,7 @@ struct ChatsTab: View {
private struct ChatRowView: View {
let chat: PrivateChatListItem
let currentUserId: String?
private var title: String {
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 {
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
}
@ -161,50 +198,158 @@ private struct ChatRowView: View {
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? {
let date = chat.lastMessage?.createdAt ?? chat.createdAt
guard let date else { return nil }
return ChatRowView.timeFormatter.string(from: date)
return ChatRowView.formattedTimestamp(for: date)
}
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 {
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 {
HStack(spacing: 12) {
Circle()
.fill(Color.accentColor.opacity(0.15))
.fill(avatarBackgroundColor)
.frame(width: 44, height: 44)
.overlay(
Text(initial)
.font(.headline)
.foregroundColor(Color.accentColor)
.foregroundColor(avatarTextColor)
)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.fontWeight(chat.unreadCount > 0 ? .semibold : .regular)
.foregroundColor(.primary)
.lineLimit(1)
if let officialName = officialFullName {
HStack(spacing: 6) {
Text(officialName)
.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)
.foregroundColor(subtitleColor)
.lineLimit(2)
.truncationMode(.tail)
}
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
VStack(alignment: .trailing, spacing: 6) {
if let timestamp {
Text(timestamp)
.font(.caption)
.foregroundColor(.secondary)
HStack(spacing: 4) {
if shouldShowReadStatus {
Image(systemName: readStatusIconName)
.foregroundColor(readStatusColor)
.font(.caption2)
}
Text(timestamp)
.font(.caption)
.foregroundColor(.secondary)
}
}
if chat.unreadCount > 0 {
@ -222,12 +367,68 @@ private struct ChatRowView: View {
.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()
formatter.dateStyle = .short
formatter.timeStyle = .short
formatter.locale = locale
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
}()
}
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 {

View File

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