profile update
This commit is contained in:
parent
1c8e80df1f
commit
76f3fdefdf
@ -20,6 +20,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@" : {
|
||||
"comment" : "Message profile joined format"
|
||||
},
|
||||
"%@ %@" : {
|
||||
"localizations" : {
|
||||
"ru" : {
|
||||
@ -270,6 +273,9 @@
|
||||
},
|
||||
"Безопасность аккаунта" : {
|
||||
|
||||
},
|
||||
"Биография" : {
|
||||
|
||||
},
|
||||
"Блокировка и жалобы доступны из профиля, как в Telegram." : {
|
||||
"comment" : "Message profile safety section description"
|
||||
@ -531,6 +537,9 @@
|
||||
},
|
||||
"Данные и кэш" : {
|
||||
|
||||
},
|
||||
"Дата регистрации в Yobble" : {
|
||||
|
||||
},
|
||||
"Двухфакторная аутентификация" : {
|
||||
"comment" : "Заголовок экрана 2FA\nПереход к настройкам двухфакторной аутентификации",
|
||||
@ -546,9 +555,6 @@
|
||||
"Двухфакторная аутентификация настроена." : {
|
||||
"comment" : "Сообщение после успешного подтверждения кода 2FA"
|
||||
},
|
||||
"Действия в чате" : {
|
||||
"comment" : "Message profile quick actions title"
|
||||
},
|
||||
"Десктоп" : {
|
||||
"comment" : "Тип сессии — десктоп"
|
||||
},
|
||||
@ -583,6 +589,9 @@
|
||||
},
|
||||
"Добро пожаловать в Yobble!" : {
|
||||
|
||||
},
|
||||
"Дополнительные действия." : {
|
||||
"comment" : "Message profile more action description"
|
||||
},
|
||||
"Другие устройства (%d)" : {
|
||||
"comment" : "Заголовок секции других устройств с количеством"
|
||||
@ -610,6 +619,12 @@
|
||||
},
|
||||
"Если предпочитаете классический вход, используйте логин и пароль." : {
|
||||
|
||||
},
|
||||
"Ещё" : {
|
||||
"comment" : "Message profile more action"
|
||||
},
|
||||
"Ещё…" : {
|
||||
|
||||
},
|
||||
"Жалоба" : {
|
||||
"comment" : "Message profile report alert title"
|
||||
@ -641,6 +656,9 @@
|
||||
"Завершить эту сессию?" : {
|
||||
"comment" : "Заголовок подтверждения завершения отдельной сессии"
|
||||
},
|
||||
"Заглушить" : {
|
||||
"comment" : "Message profile mute action"
|
||||
},
|
||||
"Заглушка: Push-уведомления" : {
|
||||
|
||||
},
|
||||
@ -837,9 +855,6 @@
|
||||
"Ищем пользователей…" : {
|
||||
"comment" : "Global search loading"
|
||||
},
|
||||
"Как в Telegram — главные кнопки всегда под рукой." : {
|
||||
"comment" : "Message profile quick actions description"
|
||||
},
|
||||
"Как сбросить пароль?" : {
|
||||
"comment" : "FAQ question: reset password"
|
||||
},
|
||||
@ -2400,6 +2415,9 @@
|
||||
},
|
||||
"Публичная информация" : {
|
||||
|
||||
},
|
||||
"Публичное имя" : {
|
||||
|
||||
},
|
||||
"Разблокировать" : {
|
||||
"comment" : "Message profile unblock alert title\nMessage profile unblock title\nUnblock confirmation action"
|
||||
@ -2656,6 +2674,9 @@
|
||||
},
|
||||
"Скоро появится разблокировка с подтверждением и синхронизацией." : {
|
||||
"comment" : "Message profile unblock alert message"
|
||||
},
|
||||
"Скрыть" : {
|
||||
|
||||
},
|
||||
"Слишком много запросов." : {
|
||||
"localizations" : {
|
||||
@ -2851,9 +2872,6 @@
|
||||
},
|
||||
"Тип диалога" : {
|
||||
|
||||
},
|
||||
"Тишина" : {
|
||||
"comment" : "Message profile mute action"
|
||||
},
|
||||
"Тишина включена. Чат не тревожит до включения сигнала." : {
|
||||
"comment" : "Message profile notifications subtitle off"
|
||||
|
||||
@ -13,11 +13,13 @@ struct MessageProfileView: View {
|
||||
|
||||
@State private var areNotificationsEnabled: Bool = true
|
||||
@State private var placeholderAlert: PlaceholderAlert?
|
||||
@State private var isBioExpanded: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 24) {
|
||||
headerCard
|
||||
DescriptionSection
|
||||
quickActionsSection
|
||||
aboutSection
|
||||
mediaPreviewSection
|
||||
@ -59,11 +61,11 @@ struct MessageProfileView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
if let login = loginDisplay {
|
||||
Text(login)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
// if let login = loginDisplay {
|
||||
// Text(login)
|
||||
// .font(.subheadline)
|
||||
// .foregroundColor(.secondary)
|
||||
// }
|
||||
|
||||
if let status = presenceStatus {
|
||||
HStack(spacing: 6) {
|
||||
@ -77,11 +79,11 @@ struct MessageProfileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if let bio = profileBio {
|
||||
Text(bio)
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
// if let bio = profileBio {
|
||||
// Text(bio)
|
||||
// .font(.body)
|
||||
// .multilineTextAlignment(.center)
|
||||
// }
|
||||
|
||||
if !statusTags.isEmpty {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120), spacing: 8)], spacing: 8) {
|
||||
@ -115,6 +117,58 @@ struct MessageProfileView: View {
|
||||
.shadow(color: Color.black.opacity(0.08), radius: 20, x: 0, y: 10)
|
||||
}
|
||||
|
||||
private var DescriptionSection: some View {
|
||||
Group {
|
||||
|
||||
if let full = publicFullName{
|
||||
Section(){
|
||||
card {
|
||||
VStack(spacing: 0) {
|
||||
infoRow(
|
||||
title: NSLocalizedString("Публичное имя", comment: ""),
|
||||
value: full
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let bio = profileBio {
|
||||
card {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
|
||||
// Заголовок
|
||||
Text("Биография")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// Основной текст (обрезанный или полный)
|
||||
Text(isBioExpanded ? bio : bioFirstLines(bio, count: 4))
|
||||
.font(.body)
|
||||
.textSelection(.enabled) // ← копирование
|
||||
.animation(.easeInOut, value: isBioExpanded)
|
||||
|
||||
// Кнопка "ещё / скрыть" если строк больше 4
|
||||
if bio.lineCount > 4 {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: { isBioExpanded.toggle() }) {
|
||||
Text(isBioExpanded ? "Скрыть" : "Ещё…")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.buttonStyle(.plain)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var officialBadge: some View {
|
||||
if isOfficial {
|
||||
@ -137,10 +191,7 @@ struct MessageProfileView: View {
|
||||
// MARK: - Sections
|
||||
|
||||
private var quickActionsSection: some View {
|
||||
section(
|
||||
title: NSLocalizedString("Действия в чате", comment: "Message profile quick actions title"),
|
||||
description: NSLocalizedString("Как в Telegram — главные кнопки всегда под рукой.", comment: "Message profile quick actions description")
|
||||
) {
|
||||
section() {
|
||||
card {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(quickActionItems) { action in
|
||||
@ -175,22 +226,12 @@ struct MessageProfileView: View {
|
||||
}
|
||||
|
||||
private var aboutSection: some View {
|
||||
section(
|
||||
title: NSLocalizedString("О пользователе", comment: "Message profile about title"),
|
||||
description: NSLocalizedString("Имя, логин и статус — как в профиле Telegram.", comment: "Message profile about description")
|
||||
) {
|
||||
Section(){
|
||||
card {
|
||||
VStack(spacing: 0) {
|
||||
infoRow(
|
||||
icon: "person.text.rectangle",
|
||||
title: NSLocalizedString("Имя в чате", comment: ""),
|
||||
value: displayName
|
||||
)
|
||||
|
||||
if let login = loginDisplay {
|
||||
rowDivider
|
||||
infoRow(
|
||||
icon: "at",
|
||||
title: NSLocalizedString("Юзернейм", comment: ""),
|
||||
value: login
|
||||
)
|
||||
@ -199,30 +240,63 @@ struct MessageProfileView: View {
|
||||
if let membership = membershipDescription {
|
||||
rowDivider
|
||||
infoRow(
|
||||
icon: "calendar",
|
||||
title: NSLocalizedString("На Yobble", comment: ""),
|
||||
title: NSLocalizedString("Дата регистрации в Yobble", comment: ""),
|
||||
value: membership
|
||||
)
|
||||
}
|
||||
|
||||
if let status = presenceStatus, !status.isOnline {
|
||||
rowDivider
|
||||
infoRow(
|
||||
icon: "clock",
|
||||
title: NSLocalizedString("Последний визит", comment: ""),
|
||||
value: status.text
|
||||
)
|
||||
}
|
||||
|
||||
rowDivider
|
||||
infoRow(
|
||||
icon: "lock.shield",
|
||||
title: NSLocalizedString("Тип диалога", comment: ""),
|
||||
value: chatTypeDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// section(
|
||||
// title: NSLocalizedString("О пользователе", comment: "Message profile about title"),
|
||||
// description: NSLocalizedString("Имя, логин и статус — как в профиле Telegram.", comment: "Message profile about description")
|
||||
// ) {
|
||||
// card {
|
||||
// VStack(spacing: 0) {
|
||||
// infoRow(
|
||||
// icon: "person.text.rectangle",
|
||||
// title: NSLocalizedString("Имя в чате", comment: ""),
|
||||
// value: displayName
|
||||
// )
|
||||
//
|
||||
// if let login = loginDisplay {
|
||||
// rowDivider
|
||||
// infoRow(
|
||||
// icon: "at",
|
||||
// title: NSLocalizedString("Юзернейм", comment: ""),
|
||||
// value: login
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// if let membership = membershipDescription {
|
||||
// rowDivider
|
||||
// infoRow(
|
||||
// icon: "calendar",
|
||||
// title: NSLocalizedString("На Yobble", comment: ""),
|
||||
// value: membership
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// if let status = presenceStatus, !status.isOnline {
|
||||
// rowDivider
|
||||
// infoRow(
|
||||
// icon: "clock",
|
||||
// title: NSLocalizedString("Последний визит", comment: ""),
|
||||
// value: status.text
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// rowDivider
|
||||
// infoRow(
|
||||
// icon: "lock.shield",
|
||||
// title: NSLocalizedString("Тип диалога", comment: ""),
|
||||
// value: chatTypeDescription
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private var mediaPreviewSection: some View {
|
||||
@ -407,22 +481,34 @@ struct MessageProfileView: View {
|
||||
|
||||
// MARK: - Helper Builders
|
||||
|
||||
private func section<Content: View>(title: String, description: String? = nil, @ViewBuilder content: () -> Content) -> some View {
|
||||
private func section<Content: View>(
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
@ViewBuilder content: () -> Content
|
||||
) -> some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
if let description {
|
||||
Text(description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// показать только если реально есть текст
|
||||
if (title?.isEmpty == false) || (description?.isEmpty == false) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if let title, !title.isEmpty {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
}
|
||||
if let description, !description.isEmpty {
|
||||
Text(description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
|
||||
private func card<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
content()
|
||||
@ -435,12 +521,14 @@ struct MessageProfileView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func infoRow(icon: String, title: String, value: String) -> some View {
|
||||
private func infoRow(icon: String? = nil, title: String, value: String) -> some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
iconBackground(color: .accentColor.opacity(0.18)) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(.accentColor)
|
||||
if let icon {
|
||||
iconBackground(color: .accentColor.opacity(0.18)) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@ -522,7 +610,8 @@ struct MessageProfileView: View {
|
||||
guard let createdAt = chat.chatData?.createdAt else { return nil }
|
||||
let formatted = MessageProfileView.joinedFormatter.string(from: createdAt)
|
||||
return String(
|
||||
format: NSLocalizedString("На платформе с %@", comment: "Message profile joined format"),
|
||||
// format: NSLocalizedString("На платформе с %@", comment: "Message profile joined format"),
|
||||
format: NSLocalizedString("%@", comment: "Message profile joined format"),
|
||||
formatted
|
||||
)
|
||||
}
|
||||
@ -662,11 +751,17 @@ struct MessageProfileView: View {
|
||||
description: NSLocalizedString("Голосовые звонки пока недоступны. Как только включим WebRTC, кнопка оживёт.", comment: "Message profile call action description"),
|
||||
tint: .green
|
||||
),
|
||||
// ProfileQuickAction(
|
||||
// icon: "video.fill",
|
||||
// title: NSLocalizedString("Видео", comment: "Message profile video action"),
|
||||
// description: NSLocalizedString("Видео созвоны появятся вместе с звонками. Интерфейс повторит Telegram.", comment: "Message profile video action description"),
|
||||
// tint: .purple
|
||||
// ),
|
||||
ProfileQuickAction(
|
||||
icon: "video.fill",
|
||||
title: NSLocalizedString("Видео", comment: "Message profile video action"),
|
||||
description: NSLocalizedString("Видео созвоны появятся вместе с звонками. Интерфейс повторит Telegram.", comment: "Message profile video action description"),
|
||||
tint: .purple
|
||||
icon: "bell.slash.fill",
|
||||
title: NSLocalizedString("Заглушить", comment: "Message profile mute action"),
|
||||
description: NSLocalizedString("Появится мутация на 1 час, 1 день или навсегда.", comment: "Message profile mute action description"),
|
||||
tint: .orange
|
||||
),
|
||||
ProfileQuickAction(
|
||||
icon: "magnifyingglass",
|
||||
@ -675,10 +770,10 @@ struct MessageProfileView: View {
|
||||
tint: .blue
|
||||
),
|
||||
ProfileQuickAction(
|
||||
icon: "bell.slash.fill",
|
||||
title: NSLocalizedString("Тишина", comment: "Message profile mute action"),
|
||||
description: NSLocalizedString("Появится мутация на 1 час, 1 день или навсегда.", comment: "Message profile mute action description"),
|
||||
tint: .orange
|
||||
icon: "ellipsis.circle",
|
||||
title: NSLocalizedString("Ещё", comment: "Message profile more action"),
|
||||
description: NSLocalizedString("Дополнительные действия.", comment: "Message profile more action description"),
|
||||
tint: .gray
|
||||
)
|
||||
]
|
||||
}
|
||||
@ -782,6 +877,19 @@ struct MessageProfileView: View {
|
||||
return NSLocalizedString("Неизвестный пользователь", comment: "Message profile fallback title")
|
||||
}
|
||||
|
||||
private var publicFullName: String? {
|
||||
guard
|
||||
let custom = trimmed(chat.chatData?.customName),
|
||||
!custom.isEmpty, // custom должен быть
|
||||
let full = trimmed(chat.chatData?.fullName),
|
||||
!full.isEmpty // full должен быть
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return full
|
||||
}
|
||||
|
||||
private var loginDisplay: String? {
|
||||
guard let login = trimmed(chat.chatData?.login) else { return nil }
|
||||
return "@\(login)"
|
||||
@ -809,6 +917,19 @@ struct MessageProfileView: View {
|
||||
formatter.timeStyle = .none
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private func bioFirstLines(_ text: String, count: Int) -> String {
|
||||
let lines = text.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
if lines.count <= count { return text }
|
||||
return lines.prefix(count).joined(separator: "\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var lineCount: Int {
|
||||
return self.split(separator: "\n", omittingEmptySubsequences: false).count
|
||||
}
|
||||
}
|
||||
|
||||
private struct PresenceStatus {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user