profile update

This commit is contained in:
cheykrym 2025-12-11 00:19:18 +03:00
parent 1c8e80df1f
commit 76f3fdefdf
2 changed files with 215 additions and 76 deletions

View File

@ -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"

View File

@ -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) {
@ -114,6 +116,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 {
@ -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,54 +226,77 @@ 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
)
}
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,14 +521,16 @@ 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) {
Text(title)
.font(.caption)
@ -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
)
]
}
@ -781,6 +876,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 }
@ -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 {