add profile placeholder
This commit is contained in:
parent
98de0eb313
commit
41c25009a1
@ -1420,7 +1420,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Неизвестный пользователь" : {
|
"Неизвестный пользователь" : {
|
||||||
"comment" : "Deleted user display name\nUnknown chat title",
|
"comment" : "Deleted user display name\nMessage profile fallback title\nUnknown chat title",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -2228,6 +2228,7 @@
|
|||||||
"comment" : "Contacts placeholder message"
|
"comment" : "Contacts placeholder message"
|
||||||
},
|
},
|
||||||
"Профиль" : {
|
"Профиль" : {
|
||||||
|
"comment" : "Message profile placeholder nav title",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -2240,6 +2241,9 @@
|
|||||||
"Профиль в разработке" : {
|
"Профиль в разработке" : {
|
||||||
"comment" : "Search placeholder title"
|
"comment" : "Search placeholder title"
|
||||||
},
|
},
|
||||||
|
"Профиль для сообщений пока в разработке." : {
|
||||||
|
"comment" : "Message profile placeholder title"
|
||||||
|
},
|
||||||
"Профиль и поиск" : {
|
"Профиль и поиск" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@ -2497,6 +2501,9 @@
|
|||||||
"Скоро" : {
|
"Скоро" : {
|
||||||
"comment" : "Add blocked user placeholder title\nContacts placeholder title\nЗаголовок заглушки"
|
"comment" : "Add blocked user placeholder title\nContacts placeholder title\nЗаголовок заглушки"
|
||||||
},
|
},
|
||||||
|
"Скоро здесь появится информация о собеседнике, статусе и дополнительных действиях." : {
|
||||||
|
"comment" : "Message profile placeholder description"
|
||||||
|
},
|
||||||
"Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : {
|
"Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : {
|
||||||
"comment" : "Concept tab placeholder description"
|
"comment" : "Concept tab placeholder description"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -19,6 +19,7 @@ struct PrivateChatView: View {
|
|||||||
@State private var inputTab: ComposerTab = .chat
|
@State private var inputTab: ComposerTab = .chat
|
||||||
@State private var isVideoPreferred: Bool = false
|
@State private var isVideoPreferred: Bool = false
|
||||||
@State private var legacyComposerHeight: CGFloat = 40
|
@State private var legacyComposerHeight: CGFloat = 40
|
||||||
|
@State private var isProfilePresented: Bool = false
|
||||||
@FocusState private var isComposerFocused: Bool
|
@FocusState private var isComposerFocused: Bool
|
||||||
@EnvironmentObject private var messageCenter: IncomingMessageCenter
|
@EnvironmentObject private var messageCenter: IncomingMessageCenter
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@ -30,6 +31,7 @@ struct PrivateChatView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
content
|
content
|
||||||
@ -48,6 +50,15 @@ struct PrivateChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigationLink(
|
||||||
|
destination: MessageProfilePlaceholderView(chat: chat, currentUserId: currentUserId),
|
||||||
|
isActive: $isProfilePresented
|
||||||
|
) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
.navigationTitle(toolbarTitle)
|
.navigationTitle(toolbarTitle)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarBackButtonHidden(true)
|
.navigationBarBackButtonHidden(true)
|
||||||
@ -573,11 +584,11 @@ struct PrivateChatView: View {
|
|||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
nameStatusView
|
nameStatusButton
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
avatarView
|
avatarButton
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, minHeight: headerAvatarSize, alignment: .center)
|
.frame(maxWidth: .infinity, minHeight: headerAvatarSize, alignment: .center)
|
||||||
}
|
}
|
||||||
@ -620,6 +631,20 @@ struct PrivateChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
private func nameText(_ text: String, weight: Font.Weight) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
@ -651,6 +676,10 @@ struct PrivateChatView: View {
|
|||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openProfile() {
|
||||||
|
isProfilePresented = true
|
||||||
|
}
|
||||||
|
|
||||||
private var headerPlaceholderAvatar: some View {
|
private var headerPlaceholderAvatar: some View {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(avatarBackgroundColor)
|
.fill(avatarBackgroundColor)
|
||||||
@ -672,6 +701,157 @@ struct PrivateChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return full
|
||||||
|
}
|
||||||
|
if let login = trimmed(chat.chatData?.login) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
private struct LegacyMultilineTextView: UIViewRepresentable {
|
private struct LegacyMultilineTextView: UIViewRepresentable {
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user