patch chats list
This commit is contained in:
parent
bc3ec97d67
commit
b8d22c814c
@ -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")
|
||||
|
||||
@ -98,6 +98,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Вы" : {
|
||||
|
||||
},
|
||||
"Вы предложили: %@" : {
|
||||
|
||||
|
||||
@ -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,51 +198,159 @@ 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) {
|
||||
if let officialName = officialFullName {
|
||||
HStack(spacing: 6) {
|
||||
Text(officialName)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
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(subtitle)
|
||||
Text(messagePreview)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(subtitleColor)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 6) {
|
||||
if let timestamp {
|
||||
HStack(spacing: 4) {
|
||||
if shouldShowReadStatus {
|
||||
Image(systemName: readStatusIconName)
|
||||
.foregroundColor(readStatusColor)
|
||||
.font(.caption2)
|
||||
}
|
||||
|
||||
Text(timestamp)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if chat.unreadCount > 0 {
|
||||
Text("\(chat.unreadCount)")
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user