From 76f3fdefdfd9386f1e59c8cc994b28bf7b69f7d4 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Thu, 11 Dec 2025 00:19:18 +0300 Subject: [PATCH] profile update --- yobble/Resources/Localizable.xcstrings | 36 ++- yobble/Views/Chat/MessageProfileView.swift | 255 +++++++++++++++------ 2 files changed, 215 insertions(+), 76 deletions(-) diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 05f1ecc..fc75f28 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -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" diff --git a/yobble/Views/Chat/MessageProfileView.swift b/yobble/Views/Chat/MessageProfileView.swift index 24a6207..79290ae 100644 --- a/yobble/Views/Chat/MessageProfileView.swift +++ b/yobble/Views/Chat/MessageProfileView.swift @@ -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(title: String, description: String? = nil, @ViewBuilder content: () -> Content) -> some View { + private func section( + 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(@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 {