Compare commits
No commits in common. "b34f3c2a1a53d11149f44ae6f0f7c847967e9120" and "ab9f5eca716de91763532af686a2c43c47742898" have entirely different histories.
b34f3c2a1a
...
ab9f5eca71
@ -719,7 +719,7 @@
|
||||
}
|
||||
},
|
||||
"Избранные сообщения" : {
|
||||
"comment" : "Saved messages title"
|
||||
|
||||
},
|
||||
"Изменение контакта \"%1$@\" появится позже." : {
|
||||
"comment" : "Contacts edit placeholder message"
|
||||
@ -1420,7 +1420,7 @@
|
||||
}
|
||||
},
|
||||
"Неизвестный пользователь" : {
|
||||
"comment" : "Deleted user display name\nMessage profile fallback title\nUnknown chat title",
|
||||
"comment" : "Deleted user display name",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -1628,9 +1628,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Оффлайн" : {
|
||||
"comment" : "Offline status placeholder"
|
||||
},
|
||||
"Оценка %d" : {
|
||||
"comment" : "feedback: rating accessibility",
|
||||
"localizations" : {
|
||||
@ -2228,7 +2225,6 @@
|
||||
"comment" : "Contacts placeholder message"
|
||||
},
|
||||
"Профиль" : {
|
||||
"comment" : "Message profile placeholder nav title",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -2241,9 +2237,6 @@
|
||||
"Профиль в разработке" : {
|
||||
"comment" : "Search placeholder title"
|
||||
},
|
||||
"Профиль для сообщений пока в разработке." : {
|
||||
"comment" : "Message profile placeholder title"
|
||||
},
|
||||
"Профиль и поиск" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2501,9 +2494,6 @@
|
||||
"Скоро" : {
|
||||
"comment" : "Add blocked user placeholder title\nContacts placeholder title\nЗаголовок заглушки"
|
||||
},
|
||||
"Скоро здесь появится информация о собеседнике, статусе и дополнительных действиях." : {
|
||||
"comment" : "Message profile placeholder description"
|
||||
},
|
||||
"Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : {
|
||||
"comment" : "Concept tab placeholder description"
|
||||
},
|
||||
|
||||
@ -7,7 +7,6 @@ struct PrivateChatView: View {
|
||||
let chat: PrivateChatListItem
|
||||
let currentUserId: String?
|
||||
private let bottomAnchorId = "PrivateChatBottomAnchor"
|
||||
private let headerAvatarSize: CGFloat = 36
|
||||
|
||||
let lineLimitInChat = 6
|
||||
|
||||
@ -19,10 +18,8 @@ struct PrivateChatView: View {
|
||||
@State private var inputTab: ComposerTab = .chat
|
||||
@State private var isVideoPreferred: Bool = false
|
||||
@State private var legacyComposerHeight: CGFloat = 40
|
||||
@State private var isProfilePresented: Bool = false
|
||||
@FocusState private var isComposerFocused: Bool
|
||||
@EnvironmentObject private var messageCenter: IncomingMessageCenter
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
init(chat: PrivateChatListItem, currentUserId: String?) {
|
||||
self.chat = chat
|
||||
@ -31,63 +28,26 @@ struct PrivateChatView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollViewReader { proxy in
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
content
|
||||
.onChange(of: viewModel.messages.count) { _ in
|
||||
guard !viewModel.isLoadingMore else { return }
|
||||
scrollToBottom(proxy: proxy)
|
||||
}
|
||||
.onChange(of: scrollToBottomTrigger) { _ in
|
||||
scrollToBottom(proxy: proxy)
|
||||
}
|
||||
|
||||
if !isBottomAnchorVisible {
|
||||
scrollToBottomButton(proxy: proxy)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.bottom, 4)
|
||||
ScrollViewReader { proxy in
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
content
|
||||
.onChange(of: viewModel.messages.count) { _ in
|
||||
guard !viewModel.isLoadingMore else { return }
|
||||
scrollToBottom(proxy: proxy)
|
||||
}
|
||||
.onChange(of: scrollToBottomTrigger) { _ in
|
||||
scrollToBottom(proxy: proxy)
|
||||
}
|
||||
|
||||
if !isBottomAnchorVisible {
|
||||
scrollToBottomButton(proxy: proxy)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(
|
||||
destination: MessageProfilePlaceholderView(chat: chat, currentUserId: currentUserId),
|
||||
isActive: $isProfilePresented
|
||||
) {
|
||||
EmptyView()
|
||||
}
|
||||
.hidden()
|
||||
}
|
||||
.navigationTitle(toolbarTitle)
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
// .toolbar {
|
||||
// ToolbarItem(placement: .principal) {
|
||||
// chatToolbarContent
|
||||
// }
|
||||
// }
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .principal) {
|
||||
Button(action: openProfile) {
|
||||
nameStatusView
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
avatarButton
|
||||
}
|
||||
}
|
||||
.task {
|
||||
viewModel.loadInitialHistory()
|
||||
}
|
||||
@ -494,383 +454,16 @@ struct PrivateChatView: View {
|
||||
}()
|
||||
|
||||
private var title: String {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
backButton
|
||||
|
||||
// Spacer(minLength: 0)
|
||||
|
||||
nameStatusButton
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
// Spacer(minLength: 0)
|
||||
|
||||
avatarButton
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: headerAvatarSize, alignment: .center)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var nameStatusView: some View {
|
||||
VStack(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)
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var avatarView: some View {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private var nameStatusButton: some View {
|
||||
Button(action: openProfile) {
|
||||
nameStatusView
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var avatarButton: some View {
|
||||
Button(action: openProfile) {
|
||||
avatarView
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
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 backButton: some View {
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.frame(width: headerAvatarSize, height: headerAvatarSize)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
private func openProfile() {
|
||||
isProfilePresented = true
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageProfilePlaceholderView: View {
|
||||
let chat: PrivateChatListItem
|
||||
let currentUserId: String?
|
||||
private let avatarSize: CGFloat = 96
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
profileAvatar
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text(displayName)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
if let login = loginDisplay {
|
||||
Text(login)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Text(NSLocalizedString("Профиль для сообщений пока в разработке.", comment: "Message profile placeholder title"))
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(NSLocalizedString("Скоро здесь появится информация о собеседнике, статусе и дополнительных действиях.", comment: "Message profile placeholder description"))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 60)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(Color(UIColor.systemBackground))
|
||||
.navigationTitle(NSLocalizedString("Профиль", comment: "Message profile placeholder nav title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private var displayName: String {
|
||||
if let custom = trimmed(chat.chatData?.customName) {
|
||||
return custom
|
||||
}
|
||||
if let full = trimmed(chat.chatData?.fullName) {
|
||||
if let full = chat.chatData?.fullName, !full.isEmpty {
|
||||
return full
|
||||
}
|
||||
if let login = trimmed(chat.chatData?.login) {
|
||||
if let custom = chat.chatData?.customName, !custom.isEmpty {
|
||||
return custom
|
||||
}
|
||||
if let login = chat.chatData?.login, !login.isEmpty {
|
||||
return "@\(login)"
|
||||
}
|
||||
return NSLocalizedString("Неизвестный пользователь", comment: "Message profile fallback title")
|
||||
}
|
||||
|
||||
private var loginDisplay: String? {
|
||||
guard let login = trimmed(chat.chatData?.login) else { return nil }
|
||||
return "@\(login)"
|
||||
}
|
||||
|
||||
private var isDeletedUser: Bool {
|
||||
trimmed(chat.chatData?.login) == nil
|
||||
}
|
||||
|
||||
private var isOfficial: Bool {
|
||||
chat.chatData?.isOfficial ?? false
|
||||
}
|
||||
|
||||
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 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 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)")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var profileAvatar: some View {
|
||||
if let url = avatarUrl,
|
||||
let fileId = chat.chatData?.avatars?.current?.fileId,
|
||||
let userId = currentUserId {
|
||||
CachedAvatarView(url: url, fileId: fileId, userId: userId) {
|
||||
placeholderAvatar
|
||||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
placeholderAvatar
|
||||
}
|
||||
}
|
||||
|
||||
private var placeholderAvatar: some View {
|
||||
Circle()
|
||||
.fill(avatarBackgroundColor)
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.overlay(
|
||||
Group {
|
||||
if isDeletedUser {
|
||||
Image(systemName: "person.slash")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.system(size: avatarSize * 0.45, weight: .semibold))
|
||||
.foregroundColor(avatarTextColor)
|
||||
} else {
|
||||
Text(avatarInitial)
|
||||
.font(.system(size: avatarSize * 0.45, weight: .semibold))
|
||||
.foregroundColor(avatarTextColor)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func trimmed(_ text: String?) -> String? {
|
||||
guard let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return text
|
||||
return NSLocalizedString("Чат", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user