From f47181877e560f4818d5c7b20c6fbf5d1e7ad354 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Wed, 10 Dec 2025 03:46:54 +0300 Subject: [PATCH] edit top bar in msg --- yobble/Resources/Localizable.xcstrings | 7 +- yobble/Views/Chat/PrivateChatView.swift | 194 +++++++++++++++++++++++- 2 files changed, 191 insertions(+), 10 deletions(-) diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 6b46b46..65779b2 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -719,7 +719,7 @@ } }, "Избранные сообщения" : { - + "comment" : "Saved messages title" }, "Изменение контакта \"%1$@\" появится позже." : { "comment" : "Contacts edit placeholder message" @@ -1420,7 +1420,7 @@ } }, "Неизвестный пользователь" : { - "comment" : "Deleted user display name", + "comment" : "Deleted user display name\nUnknown chat title", "localizations" : { "en" : { "stringUnit" : { @@ -1628,6 +1628,9 @@ } } }, + "Оффлайн" : { + "comment" : "Offline status placeholder" + }, "Оценка %d" : { "comment" : "feedback: rating accessibility", "localizations" : { diff --git a/yobble/Views/Chat/PrivateChatView.swift b/yobble/Views/Chat/PrivateChatView.swift index d440e23..918c731 100644 --- a/yobble/Views/Chat/PrivateChatView.swift +++ b/yobble/Views/Chat/PrivateChatView.swift @@ -7,6 +7,7 @@ struct PrivateChatView: View { let chat: PrivateChatListItem let currentUserId: String? private let bottomAnchorId = "PrivateChatBottomAnchor" + private let headerAvatarSize: CGFloat = 36 let lineLimitInChat = 6 @@ -46,8 +47,13 @@ struct PrivateChatView: View { } } } - .navigationTitle(title) + .navigationTitle(toolbarTitle) .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + chatToolbarContent + } + } .task { viewModel.loadInitialHistory() } @@ -454,16 +460,188 @@ struct PrivateChatView: View { }() private var title: String { - if let full = chat.chatData?.fullName, !full.isEmpty { - return full + switch chat.chatType { + case .self: + return NSLocalizedString("Избранные сообщения", comment: "Saved messages title") + case .privateChat, .unknown: + if let custom = trimmed(chat.chatData?.customName) { + return custom + } + if let full = trimmed(chat.chatData?.fullName) { + return full + } + if let login = trimmed(chat.chatData?.login) { + return "@\(login)" + } + return NSLocalizedString("Неизвестный пользователь", comment: "Unknown chat title") } - if let custom = chat.chatData?.customName, !custom.isEmpty { - return custom + } + + private var toolbarTitle: String { + officialDisplayName ?? title + } + + private var offlineStatusText: String { + NSLocalizedString("Оффлайн", comment: "Offline status placeholder") + } + + private var loginDisplay: String? { + guard let login = trimmed(chat.chatData?.login) else { + return nil } - if let login = chat.chatData?.login, !login.isEmpty { - return "@\(login)" + return "@\(login)" + } + + private var isOfficial: Bool { + chat.chatData?.isOfficial ?? false + } + + private var officialDisplayName: String? { + guard isOfficial else { return nil } + + if let customName = trimmed(chat.chatData?.customName) { + return customName } - return NSLocalizedString("Чат", comment: "") + + if let name = trimmed(chat.chatData?.fullName) { + return NSLocalizedString(name, comment: "Official chat name") + } + + return loginDisplay + } + + private var isDeletedUser: Bool { + guard chat.chatType != .self else { return false } + return trimmed(chat.chatData?.login) == nil + } + + private var avatarBackgroundColor: Color { + if isDeletedUser { + return Color(.systemGray5) + } + return isOfficial ? Color.accentColor.opacity(0.85) : Color.accentColor.opacity(0.15) + } + + private var avatarTextColor: Color { + if isDeletedUser { + return Color.accentColor + } + return isOfficial ? Color.white : Color.accentColor + } + + private var deletedUserSymbolName: String { + "person.slash" + } + + private var avatarUrl: URL? { + guard let chatData = chat.chatData, + let fileId = chatData.avatars?.current?.fileId else { + return nil + } + let userId = chatData.userId + return URL(string: "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(userId)?file_id=\(fileId)") + } + + private var avatarInitial: String { + if let name = trimmed(chat.chatData?.customName) ?? trimmed(chat.chatData?.fullName) { + let components = name.split(separator: " ") + let initials = components.prefix(2).compactMap { $0.first } + if !initials.isEmpty { + return initials.map { String($0) }.joined().uppercased() + } + } + + if let login = trimmed(chat.chatData?.login) { + return String(login.prefix(1)).uppercased() + } + + return "?" + } + + private func trimmed(_ string: String?) -> String? { + guard let string = string?.trimmingCharacters(in: .whitespacesAndNewlines), !string.isEmpty else { + return nil + } + return string + } + + private var chatToolbarContent: some View { + HStack(spacing: 12) { + + VStack(alignment: .leading, spacing: 2) { + if let officialName = officialDisplayName { + HStack(spacing: 4) { + nameText(officialName, weight: .semibold) + + Image(systemName: "checkmark.seal.fill") + .foregroundColor(Color.accentColor) + .font(.caption) + } + } else { + nameText(title, weight: .semibold) + } + + Text(offlineStatusText) + .font(.caption) + .foregroundColor(.secondary) + } + + if let url = avatarUrl, + let fileId = chat.chatData?.avatars?.current?.fileId, + let userId = currentUserId { + CachedAvatarView(url: url, fileId: fileId, userId: userId) { + headerPlaceholderAvatar + } + .aspectRatio(contentMode: .fill) + .frame(width: headerAvatarSize, height: headerAvatarSize) + .clipShape(Circle()) + } else { + headerPlaceholderAvatar + } + + } + .frame(maxWidth: .infinity, alignment: .center) + } + + private func nameText(_ text: String, weight: Font.Weight) -> some View { + Group { + if #available(iOS 16.0, *) { + Text(text) + .font(.headline) + .fontWeight(weight) + .foregroundColor(.primary) + .lineLimit(1) + .truncationMode(.tail) + .strikethrough(isDeletedUser, color: Color.secondary) + } else { + Text(text) + .font(.headline) + .fontWeight(weight) + .foregroundColor(.primary) + .lineLimit(1) + .truncationMode(.tail) + } + } + } + + private var headerPlaceholderAvatar: some View { + Circle() + .fill(avatarBackgroundColor) + .frame(width: headerAvatarSize, height: headerAvatarSize) + .overlay( + Group { + if isDeletedUser { + Image(systemName: deletedUserSymbolName) + .symbolRenderingMode(.hierarchical) + .font(.system(size: headerAvatarSize * 0.45, weight: .semibold)) + .foregroundColor(avatarTextColor) + } else { + Text(avatarInitial) + .font(.system(size: headerAvatarSize * 0.5, weight: .semibold)) + .foregroundColor(avatarTextColor) + } + } + ) } }