patch chats list
This commit is contained in:
parent
bc3ec97d67
commit
b8d22c814c
@ -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")
|
||||||
|
|||||||
@ -98,6 +98,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Вы" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Вы предложили: %@" : {
|
"Вы предложили: %@" : {
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user