diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 5d644cb..05f1ecc 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -182,6 +182,9 @@ }, "Profile" : { + }, + "Push включены — приходят все новые сообщения." : { + "comment" : "Message profile notifications subtitle on" }, "Push-уведомления" : { "localizations" : { @@ -214,6 +217,9 @@ }, "Yobble Passport" : { + }, + "Автоудаление" : { + "comment" : "Message profile auto delete alert title\nMessage profile auto delete title" }, "Автоудаление аккаунта" : { "localizations" : { @@ -227,6 +233,9 @@ }, "Аккаунт не найден." : { + }, + "Аккаунт удалён" : { + "comment" : "Message profile deleted tag" }, "Активные сессии" : { "comment" : "Заголовок экрана активных сессий", @@ -249,7 +258,7 @@ }, "Безопасность" : { - "comment" : "Заголовок экрана настроек безопасности", + "comment" : "Message profile safety section title\nЗаголовок экрана настроек безопасности", "localizations" : { "en" : { "stringUnit" : { @@ -261,13 +270,25 @@ }, "Безопасность аккаунта" : { + }, + "Блокировка и жалобы доступны из профиля, как в Telegram." : { + "comment" : "Message profile safety section description" }, "Блокировка контакта \"%1$@\" появится позже." : { "comment" : "Contacts block placeholder message" }, + "Блокировка чата пока в дизайне. Готовим отдельный экран со статусом и жалобой." : { + "comment" : "Message profile block alert message" + }, "Бот" : { "comment" : "Тип сессии — бот" }, + "был(а) %@" : { + "comment" : "Message profile last seen relative format" + }, + "в сети" : { + "comment" : "Message profile online status" + }, "В чате пока нет сообщений." : { }, @@ -319,7 +340,10 @@ } }, "Видео" : { - "comment" : "Video message placeholder" + "comment" : "Message profile video action\nVideo message placeholder" + }, + "Видео созвоны появятся вместе с звонками. Интерфейс повторит Telegram." : { + "comment" : "Message profile video action description" }, "Видимость и контент" : { "localizations" : { @@ -424,6 +448,12 @@ } } }, + "Вы в его контактах" : { + "comment" : "Message profile contact tag" + }, + "Вы в его чёрном списке" : { + "comment" : "Message profile blacklist tag" + }, "Вы всегда можете отключить двухфакторную защиту, но мы рекомендуем оставлять её включённой для безопасности." : { "comment" : "Рекомендация оставить 2FA включенной" }, @@ -475,6 +505,9 @@ "Глобальный поиск" : { "comment" : "Global search section" }, + "Голосовые звонки пока недоступны. Как только включим WebRTC, кнопка оживёт." : { + "comment" : "Message profile call action description" + }, "Готово" : { "comment" : "Profile update success title\nЗаголовок успешного уведомления", "localizations" : { @@ -513,11 +546,17 @@ "Двухфакторная аутентификация настроена." : { "comment" : "Сообщение после успешного подтверждения кода 2FA" }, + "Действия в чате" : { + "comment" : "Message profile quick actions title" + }, "Десктоп" : { "comment" : "Тип сессии — десктоп" }, "Для начала, мы рекомендуем настроить параметры безопасности вашего аккаунта." : { + }, + "Добавить в контакты" : { + "comment" : "Message profile add to contacts title" }, "Добавить друзей" : { "comment" : "Add friends", @@ -530,6 +569,9 @@ } } }, + "Добавить контакт" : { + "comment" : "Message profile add contact alert title" + }, "Добавление новых блокировок появится позже." : { "comment" : "Add blocked user placeholder message" }, @@ -568,9 +610,18 @@ }, "Если предпочитаете классический вход, используйте логин и пароль." : { + }, + "Жалоба" : { + "comment" : "Message profile report alert title" + }, + "Заблокирован" : { + "comment" : "Message profile blocked tag" }, "Заблокированные" : { + }, + "Заблокировать" : { + "comment" : "Message profile block alert title\nMessage profile block title" }, "Заблокировать контакт" : { "comment" : "Contacts context action block" @@ -688,6 +739,9 @@ "Защита приложением будет добавлена в будущих обновлениях." : { "comment" : "Сообщение заглушки пароля на приложение" }, + "Звонок" : { + "comment" : "Message profile call action" + }, "Здесь не будут чаты" : { "localizations" : { "en" : { @@ -718,6 +772,9 @@ } } }, + "Избранное" : { + "comment" : "Message profile self chat type" + }, "Избранные сообщения" : { "comment" : "Saved messages title" }, @@ -746,6 +803,12 @@ "Изображение" : { "comment" : "Image message placeholder" }, + "Имя в чате" : { + + }, + "Имя, логин и статус — как в профиле Telegram." : { + "comment" : "Message profile about description" + }, "Инвайт-код (необязательно)" : { "comment" : "Инвайт-код", "localizations" : { @@ -768,9 +831,15 @@ } } }, + "История медиа синхронизируется. Как только появятся первые вложения, они покажутся здесь списком превью." : { + "comment" : "Message profile media footer" + }, "Ищем пользователей…" : { "comment" : "Global search loading" }, + "Как в Telegram — главные кнопки всегда под рукой." : { + "comment" : "Message profile quick actions description" + }, "Как сбросить пароль?" : { "comment" : "FAQ question: reset password" }, @@ -801,6 +870,9 @@ "Кликер в разработке" : { "comment" : "Concept tab placeholder title" }, + "Кнопка поделиться соберёт ссылку, QR и кнопку пересылки контакта." : { + "comment" : "Message profile share alert message" + }, "Код дружбы" : { "comment" : "Friend code badge" }, @@ -813,6 +885,9 @@ "Коды восстановления" : { "comment" : "Раздел кодов восстановления 2FA" }, + "Контакт" : { + "comment" : "Message profile contact section title" + }, "Контактов пока нет" : { "comment" : "Contacts empty state title" }, @@ -931,6 +1006,9 @@ } } }, + "Личный чат" : { + "comment" : "Message profile private chat type" + }, "Логин" : { "comment" : "Логин", "extractionState" : "stale", @@ -979,6 +1057,9 @@ }, "Массовая отчистка" : { + }, + "Медиа, ссылки и файлы" : { + "comment" : "Message profile media title" }, "Мессенджер-режим сейчас проработан примерно на 50%." : { @@ -1050,6 +1131,9 @@ } } }, + "Мы постепенно повторяем знакомый паттерн Telegram, чтобы переход был комфортным. Укажите, что ещё ожидать на экране профиля — добавим приоритетно." : { + "comment" : "Message profile footer" + }, "Мы свяжемся с вами по адресу %@, как только ответим." : { "comment" : "feedback: success email", "localizations" : { @@ -1072,6 +1156,12 @@ } } }, + "На Yobble" : { + + }, + "На платформе с %@" : { + "comment" : "Message profile joined format" + }, "Назад" : { }, @@ -1174,6 +1264,9 @@ } } }, + "Настройки чата" : { + "comment" : "Message profile chat settings title" + }, "Начальная настройка" : { }, @@ -1520,6 +1613,9 @@ } } }, + "О пользователе" : { + "comment" : "Message profile about title" + }, "О приложении" : { "localizations" : { "en" : { @@ -1599,6 +1695,9 @@ }, "Отображаемое имя" : { + }, + "Отправим ссылку или QR — как в Telegram." : { + "comment" : "Message profile share contact subtitle" }, "Отправить код ещё раз" : { @@ -1661,12 +1760,18 @@ }, "Очистить всё" : { + }, + "Очистить историю" : { + "comment" : "Message profile clear history title" }, "Очистить кэш (кроме текущего)" : { }, "Очистить кэш текущего пользователя" : { + }, + "Очистка истории" : { + "comment" : "Message profile clear history alert title" }, "Ошибка" : { "comment" : "Common error title\nContacts load error title\nProfile update error title\nЗаголовок сообщения об ошибке", @@ -1844,6 +1949,12 @@ "Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : { "comment" : "FAQ answer: reset password" }, + "Перестанет появляться в чате и не сможет писать." : { + "comment" : "Message profile block subtitle" + }, + "Плитки как в Telegram — скоро здесь появятся вложения из чата." : { + "comment" : "Message profile media description" + }, "По умолчанию это полноценная соцсеть с лентой, историями и подписками. Если нужно только общение без лишнего контента, переключитесь на режим “Только чаты”. Переключить режим можно в любой момент." : { }, @@ -1895,6 +2006,12 @@ } } }, + "Поделиться" : { + "comment" : "Message profile share alert title" + }, + "Поделиться профилем" : { + "comment" : "Message profile share contact title" + }, "Подключение" : { }, @@ -1918,6 +2035,12 @@ } } }, + "Подтверждённый профиль" : { + "comment" : "Message profile verified tag" + }, + "Пожаловаться" : { + "comment" : "Message profile report title" + }, "Пожалуйста, введите корректный e-mail." : { "comment" : "feedback: email error", "localizations" : { @@ -1933,7 +2056,7 @@ }, "Поиск" : { - + "comment" : "Message profile search action" }, "Поиск отменён." : { "comment" : "Search cancelled" @@ -2011,6 +2134,12 @@ } } }, + "Пользователь снова сможет писать вам." : { + "comment" : "Message profile unblock subtitle" + }, + "Пользователь удалён" : { + "comment" : "Message profile deleted user status" + }, "Помощь" : { "comment" : "Help Center", "localizations" : { @@ -2023,13 +2152,16 @@ } }, "Понятно" : { - "comment" : "Chat creation error acknowledgment" + "comment" : "Chat creation error acknowledgment\nPlaceholder alert dismiss" }, "Попробовать снова можно через %d сек" : { }, "Попробуйте изменить запрос поиска." : { + }, + "Последний визит" : { + }, "Последний вход: %@" : { "comment" : "Дата последнего входа в сессию" @@ -2045,6 +2177,12 @@ } } }, + "Появится мутация на 1 час, 1 день или навсегда." : { + "comment" : "Message profile mute action description" + }, + "Появится отдельная запись в адресной книге Yobble." : { + "comment" : "Message profile add to contacts subtitle" + }, "Правила сервиса" : { }, @@ -2083,6 +2221,9 @@ } } }, + "Приватный диалог" : { + "comment" : "Message profile default chat type" + }, "Приглашение достигло лимита использования." : { "localizations" : { "en" : { @@ -2228,7 +2369,7 @@ "comment" : "Contacts placeholder message" }, "Профиль" : { - "comment" : "Message profile placeholder nav title", + "comment" : "Message profile navigation title", "localizations" : { "en" : { "stringUnit" : { @@ -2241,9 +2382,6 @@ "Профиль в разработке" : { "comment" : "Search placeholder title" }, - "Профиль для сообщений пока в разработке." : { - "comment" : "Message profile placeholder title" - }, "Профиль и поиск" : { "localizations" : { "en" : { @@ -2264,7 +2402,7 @@ }, "Разблокировать" : { - "comment" : "Unblock confirmation action" + "comment" : "Message profile unblock alert title\nMessage profile unblock title\nUnblock confirmation action" }, "Разрешить пересылку сообщений" : { "localizations" : { @@ -2379,8 +2517,14 @@ } } }, + "Редактор контактов скоро появится. Мы сохраним имя, телефон и заметку." : { + "comment" : "Message profile add contact alert message" + }, "Режим" : { + }, + "Режим автоудаления появится чуть позже. Мы добавим пресеты на 24 часа, 7 дней и 1 месяц — совсем как в Telegram." : { + "comment" : "Message profile auto delete alert message" }, "Режим мессенжера" : { @@ -2501,12 +2645,18 @@ "Скоро" : { "comment" : "Add blocked user placeholder title\nContacts placeholder title\nЗаголовок заглушки" }, - "Скоро здесь появится информация о собеседнике, статусе и дополнительных действиях." : { - "comment" : "Message profile placeholder description" + "Скоро можно будет искать сообщения, ссылки и файлы в этом чате." : { + "comment" : "Message profile search action description" + }, + "Скоро можно будет очистить сообщения выборочно или целиком. Пока подготовим дизайн." : { + "comment" : "Message profile clear history alert message" }, "Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : { "comment" : "Concept tab placeholder description" }, + "Скоро появится разблокировка с подтверждением и синхронизацией." : { + "comment" : "Message profile unblock alert message" + }, "Слишком много запросов." : { "localizations" : { "en" : { @@ -2557,6 +2707,9 @@ }, "Сообщение слишком длинное." : { + }, + "Сообщения пока сохраняются навсегда." : { + "comment" : "Message profile auto delete subtitle" }, "Сообщите о материалах" : { "comment" : "feedback category subtitle: content", @@ -2569,6 +2722,9 @@ } } }, + "Сообщите о спаме или нарушении правил." : { + "comment" : "Message profile report subtitle" + }, "Сохранение..." : { }, @@ -2693,8 +2849,20 @@ } } }, + "Тип диалога" : { + + }, + "Тишина" : { + "comment" : "Message profile mute action" + }, + "Тишина включена. Чат не тревожит до включения сигнала." : { + "comment" : "Message profile notifications subtitle off" + }, "Только чаты (готово 60%)" : { + }, + "Тонкие настройки диалога по образцу профиля Telegram." : { + "comment" : "Message profile chat settings description" }, "Ты шо ебанутый? А ниче тот факт что новый пароль должен отличаться от старого." : { "localizations" : { @@ -2721,6 +2889,7 @@ } }, "Уведомления" : { + "comment" : "Message profile notifications title", "localizations" : { "en" : { "stringUnit" : { @@ -2742,6 +2911,9 @@ "Удалить контакт" : { "comment" : "Contacts context action delete" }, + "Удалить переписку только для себя." : { + "comment" : "Message profile clear history subtitle" + }, "Удалить фото" : { "comment" : "Avatar delete" }, @@ -2765,6 +2937,12 @@ } } }, + "Управление карточкой собеседника." : { + "comment" : "Message profile contact section description" + }, + "Форма жалобы появится чуть позже — добавим прикрепление скриншотов и тип нарушения." : { + "comment" : "Message profile report alert message" + }, "Функция пока недоступна." : { "comment" : "Сообщение заглушки" }, @@ -2892,6 +3070,9 @@ }, "Этот аккаунт недоступен." : { + }, + "Юзернейм" : { + }, "Я ознакомился и принимаю правила сервиса" : { diff --git a/yobble/Views/Chat/MessageProfileView.swift b/yobble/Views/Chat/MessageProfileView.swift index afbbe71..24a6207 100644 --- a/yobble/Views/Chat/MessageProfileView.swift +++ b/yobble/Views/Chat/MessageProfileView.swift @@ -6,103 +6,685 @@ // import SwiftUI - struct MessageProfileView: View { let chat: PrivateChatListItem let currentUserId: String? private let avatarSize: CGFloat = 96 - var body: some View { - ScrollView { - VStack(spacing: 24) { - profileAvatar + @State private var areNotificationsEnabled: Bool = true + @State private var placeholderAlert: PlaceholderAlert? - VStack(spacing: 4) { + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 24) { + headerCard + quickActionsSection + aboutSection + mediaPreviewSection + chatSettingsSection + contactActionsSection + safetySection + footerHint + } + .padding(.horizontal, 20) + .padding(.vertical, 24) + } + .background(Color(UIColor.systemGroupedBackground).ignoresSafeArea()) + .navigationTitle(NSLocalizedString("Профиль", comment: "Message profile navigation title")) + .navigationBarTitleDisplayMode(.inline) + .alert(item: $placeholderAlert) { alert in + Alert( + title: Text(alert.title), + message: Text(alert.message), + dismissButton: .default(Text(NSLocalizedString("Понятно", comment: "Placeholder alert dismiss"))) + ) + } + } + + // MARK: - Header + + private var headerCard: some View { + VStack(spacing: 16) { + profileAvatar + .overlay(alignment: .bottomTrailing) { + officialBadge + } + + VStack(spacing: 6) { + HStack(spacing: 6) { Text(displayName) - .font(.title3) + .font(.title2) .fontWeight(.semibold) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) + } + + if let login = loginDisplay { + Text(login) + .font(.subheadline) + .foregroundColor(.secondary) + } + + if let status = presenceStatus { + HStack(spacing: 6) { + Circle() + .fill(status.isOnline ? Color.green : Color.gray.opacity(0.4)) + .frame(width: 8, height: 8) + Text(status.text) + .font(.footnote) + .foregroundColor(.secondary) + } + } + } + + if let bio = profileBio { + Text(bio) + .font(.body) + .multilineTextAlignment(.center) + } + + if !statusTags.isEmpty { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 120), spacing: 8)], spacing: 8) { + ForEach(statusTags) { tag in + HStack(spacing: 6) { + Image(systemName: tag.icon) + .font(.system(size: 12, weight: .semibold)) + Text(tag.text) + .font(.caption) + .fontWeight(.medium) + } + .padding(.vertical, 6) + .padding(.horizontal, 10) + .foregroundColor(tag.tint) + .background(tag.background) + .clipShape(Capsule()) + } + } + } + } + .padding(24) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 32, style: .continuous) + .fill(headerGradient) + ) + .overlay( + RoundedRectangle(cornerRadius: 32, style: .continuous) + .stroke(Color.white.opacity(0.08), lineWidth: 1) + ) + .shadow(color: Color.black.opacity(0.08), radius: 20, x: 0, y: 10) + } + + @ViewBuilder + private var officialBadge: some View { + if isOfficial { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + .padding(6) + .background(Circle().fill(Color.accentColor)) + .offset(x: 6, y: 6) + } + } + + private var headerGradient: LinearGradient { + let first = isOfficial ? Color.accentColor : Color.accentColor.opacity(0.6) + let second = Color.accentColor.opacity(isOfficial ? 0.6 : 0.3) + let third = Color(UIColor.secondarySystemBackground) + return LinearGradient(colors: [first, second, third], startPoint: .topLeading, endPoint: .bottomTrailing) + } + + // 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") + ) { + card { + HStack(spacing: 12) { + ForEach(quickActionItems) { action in + quickActionButton(action) + } + } + } + } + } + + private func quickActionButton(_ action: ProfileQuickAction) -> some View { + Button(action: { + showPlaceholderAction(title: action.title, message: action.description) + }) { + VStack(spacing: 10) { + Circle() + .fill(action.tint.opacity(0.15)) + .frame(width: 64, height: 64) + .overlay( + Image(systemName: action.icon) + .font(.system(size: 22, weight: .semibold)) + .foregroundColor(action.tint) + ) + Text(action.title) + .font(.footnote) + .multilineTextAlignment(.center) + .foregroundColor(.primary) + } + .frame(maxWidth: .infinity) + } + .buttonStyle(.plain) + } + + private var aboutSection: some View { + 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 { - Text(login) - .font(.subheadline) + 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 { + section( + title: NSLocalizedString("Медиа, ссылки и файлы", comment: "Message profile media title"), + description: NSLocalizedString("Плитки как в Telegram — скоро здесь появятся вложения из чата.", comment: "Message profile media description") + ) { + card { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) { + ForEach(Array(sharedMediaPlaceholderIcons.enumerated()), id: \.offset) { index, icon in + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(sharedMediaPlaceholderColors[index % sharedMediaPlaceholderColors.count]) + .frame(height: 72) + .overlay( + Image(systemName: icon) + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(.white.opacity(0.9)) + ) + .overlay( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(Color.white.opacity(0.18), lineWidth: 1) + ) + } + } + .frame(maxWidth: .infinity) + + Text(NSLocalizedString("История медиа синхронизируется. Как только появятся первые вложения, они покажутся здесь списком превью.", comment: "Message profile media footer")) + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 12) + } + } + } + + private var chatSettingsSection: some View { + section( + title: NSLocalizedString("Настройки чата", comment: "Message profile chat settings title"), + description: NSLocalizedString("Тонкие настройки диалога по образцу профиля Telegram.", comment: "Message profile chat settings description") + ) { + card { + VStack(spacing: 0) { + Toggle(isOn: $areNotificationsEnabled) { + VStack(alignment: .leading, spacing: 2) { + Text(NSLocalizedString("Уведомления", comment: "Message profile notifications title")) + Text( + areNotificationsEnabled + ? NSLocalizedString("Push включены — приходят все новые сообщения.", comment: "Message profile notifications subtitle on") + : NSLocalizedString("Тишина включена. Чат не тревожит до включения сигнала.", comment: "Message profile notifications subtitle off") + ) + .font(.caption) + .foregroundColor(.secondary) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .padding(.vertical, 4) + + rowDivider + + buttonRow( + icon: "timer", + title: NSLocalizedString("Автоудаление", comment: "Message profile auto delete title"), + subtitle: NSLocalizedString("Сообщения пока сохраняются навсегда.", comment: "Message profile auto delete subtitle"), + iconTint: .orange + ) { + showPlaceholderAction( + title: NSLocalizedString("Автоудаление", comment: "Message profile auto delete alert title"), + message: NSLocalizedString("Режим автоудаления появится чуть позже. Мы добавим пресеты на 24 часа, 7 дней и 1 месяц — совсем как в Telegram.", comment: "Message profile auto delete alert message") + ) + } + + rowDivider + + buttonRow( + icon: "text.bubble", + title: NSLocalizedString("Очистить историю", comment: "Message profile clear history title"), + subtitle: NSLocalizedString("Удалить переписку только для себя.", comment: "Message profile clear history subtitle"), + iconTint: .blue + ) { + showPlaceholderAction( + title: NSLocalizedString("Очистка истории", comment: "Message profile clear history alert title"), + message: NSLocalizedString("Скоро можно будет очистить сообщения выборочно или целиком. Пока подготовим дизайн.", comment: "Message profile clear history alert message") + ) + } + } + } + } + } + + private var contactActionsSection: some View { + section( + title: NSLocalizedString("Контакт", comment: "Message profile contact section title"), + description: NSLocalizedString("Управление карточкой собеседника.", comment: "Message profile contact section description") + ) { + card { + VStack(spacing: 0) { + buttonRow( + icon: "person.badge.plus", + title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"), + subtitle: NSLocalizedString("Появится отдельная запись в адресной книге Yobble.", comment: "Message profile add to contacts subtitle"), + iconTint: .accentColor + ) { + showPlaceholderAction( + title: NSLocalizedString("Добавить контакт", comment: "Message profile add contact alert title"), + message: NSLocalizedString("Редактор контактов скоро появится. Мы сохраним имя, телефон и заметку.", comment: "Message profile add contact alert message") + ) + } + + rowDivider + + buttonRow( + icon: "paperplane.fill", + title: NSLocalizedString("Поделиться профилем", comment: "Message profile share contact title"), + subtitle: NSLocalizedString("Отправим ссылку или QR — как в Telegram.", comment: "Message profile share contact subtitle"), + iconTint: .purple + ) { + showPlaceholderAction( + title: NSLocalizedString("Поделиться", comment: "Message profile share alert title"), + message: NSLocalizedString("Кнопка поделиться соберёт ссылку, QR и кнопку пересылки контакта.", comment: "Message profile share alert message") + ) + } + } + } + } + } + + private var safetySection: some View { + section( + title: NSLocalizedString("Безопасность", comment: "Message profile safety section title"), + description: NSLocalizedString("Блокировка и жалобы доступны из профиля, как в Telegram.", comment: "Message profile safety section description") + ) { + card { + VStack(spacing: 0) { + buttonRow( + icon: "hand.raised.slash.fill", + title: isBlockedByCurrentUser + ? NSLocalizedString("Разблокировать", comment: "Message profile unblock title") + : NSLocalizedString("Заблокировать", comment: "Message profile block title"), + subtitle: isBlockedByCurrentUser + ? NSLocalizedString("Пользователь снова сможет писать вам.", comment: "Message profile unblock subtitle") + : NSLocalizedString("Перестанет появляться в чате и не сможет писать.", comment: "Message profile block subtitle"), + iconTint: .red, + destructive: true + ) { + let message = isBlockedByCurrentUser + ? NSLocalizedString("Скоро появится разблокировка с подтверждением и синхронизацией.", comment: "Message profile unblock alert message") + : NSLocalizedString("Блокировка чата пока в дизайне. Готовим отдельный экран со статусом и жалобой.", comment: "Message profile block alert message") + showPlaceholderAction( + title: isBlockedByCurrentUser + ? NSLocalizedString("Разблокировать", comment: "Message profile unblock alert title") + : NSLocalizedString("Заблокировать", comment: "Message profile block alert title"), + message: message + ) + } + + rowDivider + + buttonRow( + icon: "exclamationmark.bubble.fill", + title: NSLocalizedString("Пожаловаться", comment: "Message profile report title"), + subtitle: NSLocalizedString("Сообщите о спаме или нарушении правил.", comment: "Message profile report subtitle"), + iconTint: .orange, + destructive: true + ) { + showPlaceholderAction( + title: NSLocalizedString("Жалоба", comment: "Message profile report alert title"), + message: NSLocalizedString("Форма жалобы появится чуть позже — добавим прикрепление скриншотов и тип нарушения.", comment: "Message profile report alert message") + ) + } + } + } + } + } + + private var footerHint: some View { + Text(NSLocalizedString("Мы постепенно повторяем знакомый паттерн Telegram, чтобы переход был комфортным. Укажите, что ещё ожидать на экране профиля — добавим приоритетно.", comment: "Message profile footer")) + .font(.footnote) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 12) + .padding(.bottom, 24) + } + + // MARK: - Helper Builders + + private func section(title: String, 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) + } + } + content() + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func card(@ViewBuilder content: () -> Content) -> some View { + VStack(alignment: .leading, spacing: 16) { + content() + } + .padding(20) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 28, style: .continuous) + .fill(Color(UIColor.secondarySystemGroupedBackground)) + ) + } + + private func infoRow(icon: String, 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) + } + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.caption) + .foregroundColor(.secondary) + Text(value) + .font(.body) + } + + Spacer() + } + .padding(.vertical, 4) + } + + private func buttonRow( + icon: String, + title: String, + subtitle: String? = nil, + iconTint: Color, + destructive: Bool = false, + action: @escaping () -> Void + ) -> some View { + Button(action: action) { + HStack(spacing: 12) { + iconBackground(color: iconTint.opacity(0.15)) { + Image(systemName: icon) + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(iconTint) + } + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.body) + .foregroundColor(destructive ? .red : .primary) + if let subtitle { + Text(subtitle) + .font(.caption) .foregroundColor(.secondary) } } - Text(NSLocalizedString("Профиль для сообщений пока в разработке.", comment: "Message profile placeholder title")) - .font(.body) - .multilineTextAlignment(.center) + Spacer() - Text(NSLocalizedString("Скоро здесь появится информация о собеседнике, статусе и дополнительных действиях.", comment: "Message profile placeholder description")) - .font(.footnote) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) + Image(systemName: "chevron.right") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(Color(UIColor.tertiaryLabel)) } - .padding(.horizontal, 24) - .padding(.top, 60) - .frame(maxWidth: .infinity) + .padding(.vertical, 2) } - .background(Color(UIColor.systemBackground)) - .navigationTitle(NSLocalizedString("Профиль", comment: "Message profile placeholder nav title")) - .navigationBarTitleDisplayMode(.inline) + .buttonStyle(.plain) } - private var displayName: String { - if let custom = trimmed(chat.chatData?.customName) { - return custom + private func iconBackground(color: Color, @ViewBuilder content: () -> Content) -> some View { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(color) + .frame(width: 44, height: 44) + .overlay(content()) + } + + private var rowDivider: some View { + Rectangle() + .fill(Color(UIColor.separator).opacity(0.4)) + .frame(height: 1) + .padding(.vertical, 8) + } + + private func showPlaceholderAction(title: String, message: String) { + placeholderAlert = PlaceholderAlert(title: title, message: message) + } + + // MARK: - Derived Data + + private var profileBio: String? { + trimmed(chat.chatData?.bio) + } + + private var membershipDescription: String? { + 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"), + formatted + ) + } + + private var chatTypeDescription: String { + switch chat.chatType { + case .self: + return NSLocalizedString("Избранное", comment: "Message profile self chat type") + case .privateChat: + return NSLocalizedString("Личный чат", comment: "Message profile private chat type") + case .unknown: + fallthrough + default: + return NSLocalizedString("Приватный диалог", comment: "Message profile default chat type") } - 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 { + private var presenceStatus: PresenceStatus? { if isDeletedUser { - return Color(.systemGray5) + return PresenceStatus( + text: NSLocalizedString("Пользователь удалён", comment: "Message profile deleted user status"), + isOnline: false + ) } - return isOfficial ? Color.accentColor.opacity(0.85) : Color.accentColor.opacity(0.15) + + guard let lastSeen = chat.chatData?.lastSeen else { return nil } + let lastSeenDate = Date(timeIntervalSince1970: TimeInterval(lastSeen)) + let interval = Date().timeIntervalSince(lastSeenDate) + + if interval < 5 * 60 { + return PresenceStatus( + text: NSLocalizedString("в сети", comment: "Message profile online status"), + isOnline: true + ) + } + + let relative = MessageProfileView.relativeFormatter.localizedString(for: lastSeenDate, relativeTo: Date()) + return PresenceStatus( + text: String( + format: NSLocalizedString("был(а) %@", comment: "Message profile last seen relative format"), + relative + ), + isOnline: false + ) } - private var avatarTextColor: Color { - if isDeletedUser { - return Color.accentColor - } - return isOfficial ? Color.white : Color.accentColor - } + private var statusTags: [StatusTag] { + var tags: [StatusTag] = [] - 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 isOfficial { + tags.append( + StatusTag( + icon: "checkmark.seal.fill", + text: NSLocalizedString("Подтверждённый профиль", comment: "Message profile verified tag"), + background: Color.white.opacity(0.18), + tint: .white + ) + ) + } + + if let relationship = chat.chatData?.relationship { + if relationship.isCurrentUserInContactsOfTarget { + tags.append( + StatusTag( + icon: "person.2.fill", + text: NSLocalizedString("Вы в его контактах", comment: "Message profile contact tag"), + background: Color.white.opacity(0.14), + tint: .white + ) + ) + } + + if relationship.isCurrentUserInBlacklistOfTarget { + tags.append( + StatusTag( + icon: "hand.thumbsdown.fill", + text: NSLocalizedString("Вы в его чёрном списке", comment: "Message profile blacklist tag"), + background: Color.red.opacity(0.4), + tint: .white + ) + ) + } + + if relationship.isTargetUserBlockedByCurrentUser { + tags.append( + StatusTag( + icon: "hand.raised.slash.fill", + text: NSLocalizedString("Заблокирован", comment: "Message profile blocked tag"), + background: Color.orange.opacity(0.4), + tint: .white + ) + ) } } - if let login = trimmed(chat.chatData?.login) { - return String(login.prefix(1)).uppercased() + if isDeletedUser { + tags.append( + StatusTag( + icon: "person.crop.circle.badge.xmark", + text: NSLocalizedString("Аккаунт удалён", comment: "Message profile deleted tag"), + background: Color.white.opacity(0.12), + tint: .white + ) + ) } - return "?" + return tags + } + + private var sharedMediaPlaceholderIcons: [String] { + [ + "photo.on.rectangle", + "doc.text.fill", + "link", + "paperclip", + "music.note", + "video.fill" + ] + } + + private var sharedMediaPlaceholderColors: [Color] { + [ + Color.accentColor.opacity(0.8), + Color.purple.opacity(0.8), + Color.blue.opacity(0.7), + Color.orange.opacity(0.8), + Color.green.opacity(0.7), + Color.pink.opacity(0.8) + ] + } + + private var quickActionItems: [ProfileQuickAction] { + [ + ProfileQuickAction( + icon: "phone.fill", + title: NSLocalizedString("Звонок", comment: "Message profile call action"), + 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: "magnifyingglass", + title: NSLocalizedString("Поиск", comment: "Message profile search action"), + description: NSLocalizedString("Скоро можно будет искать сообщения, ссылки и файлы в этом чате.", comment: "Message profile search action description"), + 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 + ) + ] + } + + private var isBlockedByCurrentUser: Bool { + chat.chatData?.relationship?.isTargetUserBlockedByCurrentUser ?? false } private var avatarUrl: URL? { @@ -150,10 +732,108 @@ struct MessageProfileView: View { ) } + 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 func trimmed(_ text: String?) -> String? { guard let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return nil } return text } + + 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 + } + + // MARK: - Formatters & Models + + private static let relativeFormatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + return formatter + }() + + private static let joinedFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .none + return formatter + }() +} + +private struct PresenceStatus { + let text: String + let isOnline: Bool +} + +private struct StatusTag: Identifiable { + let id = UUID() + let icon: String + let text: String + let background: Color + let tint: Color +} + +private struct ProfileQuickAction: Identifiable { + let id = UUID() + let icon: String + let title: String + let description: String + let tint: Color +} + +private struct PlaceholderAlert: Identifiable { + let id = UUID() + let title: String + let message: String }