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" : { "localizations" : {
"ru" : { "ru" : {
@ -270,6 +273,9 @@
}, },
"Безопасность аккаунта" : { "Безопасность аккаунта" : {
},
"Биография" : {
}, },
"Блокировка и жалобы доступны из профиля, как в Telegram." : { "Блокировка и жалобы доступны из профиля, как в Telegram." : {
"comment" : "Message profile safety section description" "comment" : "Message profile safety section description"
@ -531,6 +537,9 @@
}, },
"Данные и кэш" : { "Данные и кэш" : {
},
"Дата регистрации в Yobble" : {
}, },
"Двухфакторная аутентификация" : { "Двухфакторная аутентификация" : {
"comment" : "Заголовок экрана 2FA\nПереход к настройкам двухфакторной аутентификации", "comment" : "Заголовок экрана 2FA\nПереход к настройкам двухфакторной аутентификации",
@ -546,9 +555,6 @@
"Двухфакторная аутентификация настроена." : { "Двухфакторная аутентификация настроена." : {
"comment" : "Сообщение после успешного подтверждения кода 2FA" "comment" : "Сообщение после успешного подтверждения кода 2FA"
}, },
"Действия в чате" : {
"comment" : "Message profile quick actions title"
},
"Десктоп" : { "Десктоп" : {
"comment" : "Тип сессии — десктоп" "comment" : "Тип сессии — десктоп"
}, },
@ -583,6 +589,9 @@
}, },
"Добро пожаловать в Yobble!" : { "Добро пожаловать в Yobble!" : {
},
"Дополнительные действия." : {
"comment" : "Message profile more action description"
}, },
"Другие устройства (%d)" : { "Другие устройства (%d)" : {
"comment" : "Заголовок секции других устройств с количеством" "comment" : "Заголовок секции других устройств с количеством"
@ -610,6 +619,12 @@
}, },
"Если предпочитаете классический вход, используйте логин и пароль." : { "Если предпочитаете классический вход, используйте логин и пароль." : {
},
"Ещё" : {
"comment" : "Message profile more action"
},
"Ещё…" : {
}, },
"Жалоба" : { "Жалоба" : {
"comment" : "Message profile report alert title" "comment" : "Message profile report alert title"
@ -641,6 +656,9 @@
"Завершить эту сессию?" : { "Завершить эту сессию?" : {
"comment" : "Заголовок подтверждения завершения отдельной сессии" "comment" : "Заголовок подтверждения завершения отдельной сессии"
}, },
"Заглушить" : {
"comment" : "Message profile mute action"
},
"Заглушка: Push-уведомления" : { "Заглушка: Push-уведомления" : {
}, },
@ -837,9 +855,6 @@
"Ищем пользователей…" : { "Ищем пользователей…" : {
"comment" : "Global search loading" "comment" : "Global search loading"
}, },
"Как в Telegram — главные кнопки всегда под рукой." : {
"comment" : "Message profile quick actions description"
},
"Как сбросить пароль?" : { "Как сбросить пароль?" : {
"comment" : "FAQ question: reset password" "comment" : "FAQ question: reset password"
}, },
@ -2400,6 +2415,9 @@
}, },
"Публичная информация" : { "Публичная информация" : {
},
"Публичное имя" : {
}, },
"Разблокировать" : { "Разблокировать" : {
"comment" : "Message profile unblock alert title\nMessage profile unblock title\nUnblock confirmation action" "comment" : "Message profile unblock alert title\nMessage profile unblock title\nUnblock confirmation action"
@ -2656,6 +2674,9 @@
}, },
"Скоро появится разблокировка с подтверждением и синхронизацией." : { "Скоро появится разблокировка с подтверждением и синхронизацией." : {
"comment" : "Message profile unblock alert message" "comment" : "Message profile unblock alert message"
},
"Скрыть" : {
}, },
"Слишком много запросов." : { "Слишком много запросов." : {
"localizations" : { "localizations" : {
@ -2851,9 +2872,6 @@
}, },
"Тип диалога" : { "Тип диалога" : {
},
"Тишина" : {
"comment" : "Message profile mute action"
}, },
"Тишина включена. Чат не тревожит до включения сигнала." : { "Тишина включена. Чат не тревожит до включения сигнала." : {
"comment" : "Message profile notifications subtitle off" "comment" : "Message profile notifications subtitle off"

View File

@ -13,11 +13,13 @@ struct MessageProfileView: View {
@State private var areNotificationsEnabled: Bool = true @State private var areNotificationsEnabled: Bool = true
@State private var placeholderAlert: PlaceholderAlert? @State private var placeholderAlert: PlaceholderAlert?
@State private var isBioExpanded: Bool = false
var body: some View { var body: some View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
VStack(spacing: 24) { VStack(spacing: 24) {
headerCard headerCard
DescriptionSection
quickActionsSection quickActionsSection
aboutSection aboutSection
mediaPreviewSection mediaPreviewSection
@ -59,11 +61,11 @@ struct MessageProfileView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
if let login = loginDisplay { // if let login = loginDisplay {
Text(login) // Text(login)
.font(.subheadline) // .font(.subheadline)
.foregroundColor(.secondary) // .foregroundColor(.secondary)
} // }
if let status = presenceStatus { if let status = presenceStatus {
HStack(spacing: 6) { HStack(spacing: 6) {
@ -77,11 +79,11 @@ struct MessageProfileView: View {
} }
} }
if let bio = profileBio { // if let bio = profileBio {
Text(bio) // Text(bio)
.font(.body) // .font(.body)
.multilineTextAlignment(.center) // .multilineTextAlignment(.center)
} // }
if !statusTags.isEmpty { if !statusTags.isEmpty {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120), spacing: 8)], spacing: 8) { 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) .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 @ViewBuilder
private var officialBadge: some View { private var officialBadge: some View {
if isOfficial { if isOfficial {
@ -137,10 +191,7 @@ struct MessageProfileView: View {
// MARK: - Sections // MARK: - Sections
private var quickActionsSection: some View { private var quickActionsSection: some View {
section( section() {
title: NSLocalizedString("Действия в чате", comment: "Message profile quick actions title"),
description: NSLocalizedString("Как в Telegram — главные кнопки всегда под рукой.", comment: "Message profile quick actions description")
) {
card { card {
HStack(spacing: 12) { HStack(spacing: 12) {
ForEach(quickActionItems) { action in ForEach(quickActionItems) { action in
@ -175,22 +226,12 @@ struct MessageProfileView: View {
} }
private var aboutSection: some View { private var aboutSection: some View {
section( Section(){
title: NSLocalizedString("О пользователе", comment: "Message profile about title"),
description: NSLocalizedString("Имя, логин и статус — как в профиле Telegram.", comment: "Message profile about description")
) {
card { card {
VStack(spacing: 0) { VStack(spacing: 0) {
infoRow(
icon: "person.text.rectangle",
title: NSLocalizedString("Имя в чате", comment: ""),
value: displayName
)
if let login = loginDisplay { if let login = loginDisplay {
rowDivider
infoRow( infoRow(
icon: "at",
title: NSLocalizedString("Юзернейм", comment: ""), title: NSLocalizedString("Юзернейм", comment: ""),
value: login value: login
) )
@ -199,30 +240,63 @@ struct MessageProfileView: View {
if let membership = membershipDescription { if let membership = membershipDescription {
rowDivider rowDivider
infoRow( infoRow(
icon: "calendar", title: NSLocalizedString("Дата регистрации в Yobble", comment: ""),
title: NSLocalizedString("На Yobble", comment: ""),
value: membership value: membership
) )
} }
if let status = presenceStatus, !status.isOnline { }
rowDivider }
infoRow(
icon: "clock",
title: NSLocalizedString("Последний визит", comment: ""),
value: status.text
)
} }
rowDivider // section(
infoRow( // title: NSLocalizedString("О пользователе", comment: "Message profile about title"),
icon: "lock.shield", // description: NSLocalizedString("Имя, логин и статус как в профиле Telegram.", comment: "Message profile about description")
title: NSLocalizedString("Тип диалога", comment: ""), // ) {
value: chatTypeDescription // 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 { private var mediaPreviewSection: some View {
@ -407,22 +481,34 @@ struct MessageProfileView: View {
// MARK: - Helper Builders // 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: 12) {
// показать только если реально есть текст
if (title?.isEmpty == false) || (description?.isEmpty == false) {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
if let title, !title.isEmpty {
Text(title) Text(title)
.font(.headline) .font(.headline)
if let description { }
if let description, !description.isEmpty {
Text(description) Text(description)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
}
content() content()
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
private func card<Content: View>(@ViewBuilder content: () -> Content) -> some View { private func card<Content: View>(@ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
content() content()
@ -435,13 +521,15 @@ 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) { HStack(alignment: .top, spacing: 12) {
if let icon {
iconBackground(color: .accentColor.opacity(0.18)) { iconBackground(color: .accentColor.opacity(0.18)) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 17, weight: .semibold)) .font(.system(size: 17, weight: .semibold))
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
}
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(title) Text(title)
@ -522,7 +610,8 @@ struct MessageProfileView: View {
guard let createdAt = chat.chatData?.createdAt else { return nil } guard let createdAt = chat.chatData?.createdAt else { return nil }
let formatted = MessageProfileView.joinedFormatter.string(from: createdAt) let formatted = MessageProfileView.joinedFormatter.string(from: createdAt)
return String( return String(
format: NSLocalizedString("На платформе с %@", comment: "Message profile joined format"), // format: NSLocalizedString("На платформе с %@", comment: "Message profile joined format"),
format: NSLocalizedString("%@", comment: "Message profile joined format"),
formatted formatted
) )
} }
@ -662,11 +751,17 @@ struct MessageProfileView: View {
description: NSLocalizedString("Голосовые звонки пока недоступны. Как только включим WebRTC, кнопка оживёт.", comment: "Message profile call action description"), description: NSLocalizedString("Голосовые звонки пока недоступны. Как только включим WebRTC, кнопка оживёт.", comment: "Message profile call action description"),
tint: .green 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( ProfileQuickAction(
icon: "video.fill", icon: "bell.slash.fill",
title: NSLocalizedString("Видео", comment: "Message profile video action"), title: NSLocalizedString("Заглушить", comment: "Message profile mute action"),
description: NSLocalizedString("Видео созвоны появятся вместе с звонками. Интерфейс повторит Telegram.", comment: "Message profile video action description"), description: NSLocalizedString("Появится мутация на 1 час, 1 день или навсегда.", comment: "Message profile mute action description"),
tint: .purple tint: .orange
), ),
ProfileQuickAction( ProfileQuickAction(
icon: "magnifyingglass", icon: "magnifyingglass",
@ -675,10 +770,10 @@ struct MessageProfileView: View {
tint: .blue tint: .blue
), ),
ProfileQuickAction( ProfileQuickAction(
icon: "bell.slash.fill", icon: "ellipsis.circle",
title: NSLocalizedString("Тишина", comment: "Message profile mute action"), title: NSLocalizedString("Ещё", comment: "Message profile more action"),
description: NSLocalizedString("Появится мутация на 1 час, 1 день или навсегда.", comment: "Message profile mute action description"), description: NSLocalizedString("Дополнительные действия.", comment: "Message profile more action description"),
tint: .orange tint: .gray
) )
] ]
} }
@ -782,6 +877,19 @@ struct MessageProfileView: View {
return NSLocalizedString("Неизвестный пользователь", comment: "Message profile fallback title") 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? { private var loginDisplay: String? {
guard let login = trimmed(chat.chatData?.login) else { return nil } guard let login = trimmed(chat.chatData?.login) else { return nil }
return "@\(login)" return "@\(login)"
@ -809,6 +917,19 @@ struct MessageProfileView: View {
formatter.timeStyle = .none formatter.timeStyle = .none
return formatter 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 { private struct PresenceStatus {