Compare commits

..

No commits in common. "19939dcf6147cdc1280f37d6105adcd14f7ae39a" and "b34f3c2a1a53d11149f44ae6f0f7c847967e9120" have entirely different histories.

4 changed files with 162 additions and 1318 deletions

View File

@ -172,7 +172,6 @@ struct ChatProfile: Decodable {
let permissions: ChatPermissions?
let profilePermissions: ChatProfilePermissions?
let relationship: RelationshipStatus?
let rating: Double?
let isOfficial: Bool
private enum CodingKeys: String, CodingKey {
@ -188,7 +187,6 @@ struct ChatProfile: Decodable {
case permissions
case profilePermissions
case relationship
case rating
case isOfficial
case isVerified
}
@ -207,45 +205,12 @@ struct ChatProfile: Decodable {
self.permissions = try container.decodeIfPresent(ChatPermissions.self, forKey: .permissions)
self.profilePermissions = try container.decodeIfPresent(ChatProfilePermissions.self, forKey: .profilePermissions)
self.relationship = try container.decodeIfPresent(RelationshipStatus.self, forKey: .relationship)
let ratingPayload = try container.decodeIfPresent(ChatProfileRatingPayload.self, forKey: .rating)
self.rating = ratingPayload?.resolvedRating
let explicitOfficial = try container.decodeIfPresent(Bool.self, forKey: .isOfficial)
let verifiedFlag = try container.decodeIfPresent(Bool.self, forKey: .isVerified)
self.isOfficial = explicitOfficial ?? verifiedFlag ?? false
}
}
private struct ChatProfileRatingPayload: Decodable {
let status: String?
let rating: Double?
private enum CodingKeys: String, CodingKey {
case rating
case status
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decodeIfPresent(String.self, forKey: .status)
if let doubleValue = try? container.decode(Double.self, forKey: .rating) {
rating = doubleValue
} else if let stringValue = try? container.decode(String.self, forKey: .rating),
let doubleValue = Double(stringValue) {
rating = doubleValue
} else if let intValue = try? container.decode(Int.self, forKey: .rating) {
rating = Double(intValue)
} else {
rating = nil
}
}
var resolvedRating: Double? {
guard status?.lowercased() == "fine" else { return nil }
return rating
}
}
extension ChatProfile {
init(
userId: String,
@ -260,7 +225,6 @@ extension ChatProfile {
permissions: ChatPermissions? = nil,
profilePermissions: ChatProfilePermissions? = nil,
relationship: RelationshipStatus? = nil,
rating: Double? = nil,
isOfficial: Bool = false
) {
self.userId = userId
@ -275,7 +239,6 @@ extension ChatProfile {
self.permissions = permissions
self.profilePermissions = profilePermissions
self.relationship = relationship
self.rating = rating
self.isOfficial = isOfficial
}
}
@ -310,25 +273,9 @@ struct ChatProfilePermissions: Decodable {
}
struct RelationshipStatus: Decodable {
let isTargetInContactsOfCurrentUser: Bool
let isCurrentUserInContactsOfTarget: Bool
let isTargetUserBlockedByCurrentUser: Bool
let isCurrentUserInBlacklistOfTarget: Bool
private enum CodingKeys: String, CodingKey {
case isTargetInContactsOfCurrentUser
case isCurrentUserInContactsOfTarget
case isTargetUserBlockedByCurrentUser
case isCurrentUserInBlacklistOfTarget
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.isTargetInContactsOfCurrentUser = try container.decodeIfPresent(Bool.self, forKey: .isTargetInContactsOfCurrentUser) ?? false
self.isCurrentUserInContactsOfTarget = try container.decodeIfPresent(Bool.self, forKey: .isCurrentUserInContactsOfTarget) ?? false
self.isTargetUserBlockedByCurrentUser = try container.decodeIfPresent(Bool.self, forKey: .isTargetUserBlockedByCurrentUser) ?? false
self.isCurrentUserInBlacklistOfTarget = try container.decodeIfPresent(Bool.self, forKey: .isCurrentUserInBlacklistOfTarget) ?? false
}
}
enum JSONValue: Decodable {

View File

@ -20,9 +20,6 @@
}
}
},
"%@" : {
"comment" : "Message profile joined format"
},
"%@ %@" : {
"localizations" : {
"ru" : {
@ -33,9 +30,6 @@
}
}
},
"%@ из 5" : {
"comment" : "Message profile rating format"
},
"%@: %@" : {
"localizations" : {
"ru" : {
@ -188,9 +182,6 @@
},
"Profile" : {
},
"Push включены — приходят все новые сообщения." : {
"comment" : "Message profile notifications subtitle on"
},
"Push-уведомления" : {
"localizations" : {
@ -223,9 +214,6 @@
},
"Yobble Passport" : {
},
"Автоудаление" : {
"comment" : "Message profile auto delete alert title\nMessage profile auto delete title"
},
"Автоудаление аккаунта" : {
"localizations" : {
@ -239,9 +227,6 @@
},
"Аккаунт не найден." : {
},
"Аккаунт удалён" : {
"comment" : "Message profile deleted tag"
},
"Активные сессии" : {
"comment" : "Заголовок экрана активных сессий",
@ -264,7 +249,7 @@
},
"Безопасность" : {
"comment" : "Message profile safety section title\nЗаголовок экрана настроек безопасности",
"comment" : "Заголовок экрана настроек безопасности",
"localizations" : {
"en" : {
"stringUnit" : {
@ -276,31 +261,13 @@
},
"Безопасность аккаунта" : {
},
"Биография" : {
},
"Блокировка и жалобы доступны из профиля, как в 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 user in contacts tag"
},
"в сети" : {
"comment" : "Message profile online status"
},
"В чате пока нет сообщений." : {
},
@ -352,10 +319,7 @@
}
},
"Видео" : {
"comment" : "Message profile video action\nVideo message placeholder"
},
"Видео созвоны появятся вместе с звонками. Интерфейс повторит Telegram." : {
"comment" : "Message profile video action description"
"comment" : "Video message placeholder"
},
"Видимость и контент" : {
"localizations" : {
@ -460,12 +424,6 @@
}
}
},
"Вы в его контактах" : {
"comment" : "Message profile contact tag"
},
"Вы в его чёрном списке" : {
"comment" : "Message profile blacklist tag"
},
"Вы всегда можете отключить двухфакторную защиту, но мы рекомендуем оставлять её включённой для безопасности." : {
"comment" : "Рекомендация оставить 2FA включенной"
},
@ -517,9 +475,6 @@
"Глобальный поиск" : {
"comment" : "Global search section"
},
"Голосовые звонки пока недоступны. Как только включим WebRTC, кнопка оживёт." : {
"comment" : "Message profile call action description"
},
"Готово" : {
"comment" : "Profile update success title\nЗаголовок успешного уведомления",
"localizations" : {
@ -543,9 +498,6 @@
},
"Данные и кэш" : {
},
"Дата регистрации в Yobble" : {
},
"Двухфакторная аутентификация" : {
"comment" : "Заголовок экрана 2FA\nПереход к настройкам двухфакторной аутентификации",
@ -566,9 +518,6 @@
},
"Для начала, мы рекомендуем настроить параметры безопасности вашего аккаунта." : {
},
"Добавить в контакты" : {
"comment" : "Message profile add to contacts title"
},
"Добавить друзей" : {
"comment" : "Add friends",
@ -581,9 +530,6 @@
}
}
},
"Добавить контакт" : {
"comment" : "Message profile add contact alert title"
},
"Добавление новых блокировок появится позже." : {
"comment" : "Add blocked user placeholder message"
},
@ -595,9 +541,6 @@
},
"Добро пожаловать в Yobble!" : {
},
"Дополнительные действия." : {
"comment" : "Message profile more action description"
},
"Другие устройства (%d)" : {
"comment" : "Заголовок секции других устройств с количеством"
@ -625,24 +568,9 @@
},
"Если предпочитаете классический вход, используйте логин и пароль." : {
},
"Ещё" : {
"comment" : "Message profile more action"
},
"Ещё…" : {
},
"Жалоба" : {
"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"
@ -662,9 +590,6 @@
"Завершить эту сессию?" : {
"comment" : "Заголовок подтверждения завершения отдельной сессии"
},
"Заглушить" : {
"comment" : "Message profile mute action"
},
"Заглушка: Push-уведомления" : {
},
@ -763,9 +688,6 @@
"Защита приложением будет добавлена в будущих обновлениях." : {
"comment" : "Сообщение заглушки пароля на приложение"
},
"Звонок" : {
"comment" : "Message profile call action"
},
"Здесь не будут чаты" : {
"localizations" : {
"en" : {
@ -796,9 +718,6 @@
}
}
},
"Избранное" : {
"comment" : "Message profile self chat type"
},
"Избранные сообщения" : {
"comment" : "Saved messages title"
},
@ -827,12 +746,6 @@
"Изображение" : {
"comment" : "Image message placeholder"
},
"Имя в чате" : {
},
"Имя, логин и статус — как в профиле Telegram." : {
"comment" : "Message profile about description"
},
"Инвайт-код (необязательно)" : {
"comment" : "Инвайт-код",
"localizations" : {
@ -855,9 +768,6 @@
}
}
},
"История медиа синхронизируется. Как только появятся первые вложения, они покажутся здесь списком превью." : {
"comment" : "Message profile media footer"
},
"Ищем пользователей…" : {
"comment" : "Global search loading"
},
@ -891,9 +801,6 @@
"Кликер в разработке" : {
"comment" : "Concept tab placeholder title"
},
"Кнопка поделиться соберёт ссылку, QR и кнопку пересылки контакта." : {
"comment" : "Message profile share alert message"
},
"Код дружбы" : {
"comment" : "Friend code badge"
},
@ -906,9 +813,6 @@
"Коды восстановления" : {
"comment" : "Раздел кодов восстановления 2FA"
},
"Контакт" : {
"comment" : "Message profile contact section title"
},
"Контактов пока нет" : {
"comment" : "Contacts empty state title"
},
@ -1027,9 +931,6 @@
}
}
},
"Личный чат" : {
"comment" : "Message profile private chat type"
},
"Логин" : {
"comment" : "Логин",
"extractionState" : "stale",
@ -1078,9 +979,6 @@
},
"Массовая отчистка" : {
},
"Медиа, ссылки и файлы" : {
"comment" : "Message profile media title"
},
"Мессенджер-режим сейчас проработан примерно на 50%." : {
@ -1152,9 +1050,6 @@
}
}
},
"Мы постепенно повторяем знакомый паттерн Telegram, чтобы переход был комфортным. Укажите, что ещё ожидать на экране профиля — добавим приоритетно." : {
"comment" : "Message profile footer"
},
"Мы свяжемся с вами по адресу %@, как только ответим." : {
"comment" : "feedback: success email",
"localizations" : {
@ -1177,12 +1072,6 @@
}
}
},
"На Yobble" : {
},
"На платформе с %@" : {
"comment" : "Message profile joined format"
},
"Назад" : {
},
@ -1285,9 +1174,6 @@
}
}
},
"Настройки чата" : {
"comment" : "Message profile chat settings title"
},
"Начальная настройка" : {
},
@ -1503,9 +1389,6 @@
}
}
},
"Недоступно" : {
"comment" : "Message profile rating unavailable"
},
"Неизвестная ошибка" : {
"localizations" : {
"en" : {
@ -1637,9 +1520,6 @@
}
}
},
"О пользователе" : {
"comment" : "Message profile about title"
},
"О приложении" : {
"localizations" : {
"en" : {
@ -1719,9 +1599,6 @@
},
"Отображаемое имя" : {
},
"Отправим ссылку или QR — как в Telegram." : {
"comment" : "Message profile share contact subtitle"
},
"Отправить код ещё раз" : {
@ -1784,18 +1661,12 @@
},
"Очистить всё" : {
},
"Очистить историю" : {
"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Заголовок сообщения об ошибке",
@ -1973,12 +1844,6 @@
"Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : {
"comment" : "FAQ answer: reset password"
},
"Перестанет появляться в чате и не сможет писать." : {
"comment" : "Message profile block subtitle"
},
"Плитки как в Telegram — скоро здесь появятся вложения из чата." : {
"comment" : "Message profile media description"
},
"По умолчанию это полноценная соцсеть с лентой, историями и подписками. Если нужно только общение без лишнего контента, переключитесь на режим “Только чаты”. Переключить режим можно в любой момент." : {
},
@ -2030,12 +1895,6 @@
}
}
},
"Поделиться" : {
"comment" : "Message profile share alert title"
},
"Поделиться профилем" : {
"comment" : "Message profile share contact title"
},
"Подключение" : {
},
@ -2059,12 +1918,6 @@
}
}
},
"Подтверждённый профиль" : {
"comment" : "Message profile verified tag"
},
"Пожаловаться" : {
"comment" : "Message profile report title"
},
"Пожалуйста, введите корректный e-mail." : {
"comment" : "feedback: email error",
"localizations" : {
@ -2080,7 +1933,7 @@
},
"Поиск" : {
"comment" : "Message profile search action"
},
"Поиск отменён." : {
"comment" : "Search cancelled"
@ -2158,12 +2011,6 @@
}
}
},
"Пользователь снова сможет писать вам." : {
"comment" : "Message profile unblock subtitle"
},
"Пользователь удалён" : {
"comment" : "Message profile deleted user status"
},
"Помощь" : {
"comment" : "Help Center",
"localizations" : {
@ -2176,16 +2023,13 @@
}
},
"Понятно" : {
"comment" : "Chat creation error acknowledgment\nPlaceholder alert dismiss"
"comment" : "Chat creation error acknowledgment"
},
"Попробовать снова можно через %d сек" : {
},
"Попробуйте изменить запрос поиска." : {
},
"Последний визит" : {
},
"Последний вход: %@" : {
"comment" : "Дата последнего входа в сессию"
@ -2201,12 +2045,6 @@
}
}
},
"Появится мутация на 1 час, 1 день или навсегда." : {
"comment" : "Message profile mute action description"
},
"Появится отдельная запись в адресной книге Yobble." : {
"comment" : "Message profile add to contacts subtitle"
},
"Правила сервиса" : {
},
@ -2245,9 +2083,6 @@
}
}
},
"Приватный диалог" : {
"comment" : "Message profile default chat type"
},
"Приглашение достигло лимита использования." : {
"localizations" : {
"en" : {
@ -2393,7 +2228,7 @@
"comment" : "Contacts placeholder message"
},
"Профиль" : {
"comment" : "Message profile navigation title",
"comment" : "Message profile placeholder nav title",
"localizations" : {
"en" : {
"stringUnit" : {
@ -2406,6 +2241,9 @@
"Профиль в разработке" : {
"comment" : "Search placeholder title"
},
"Профиль для сообщений пока в разработке." : {
"comment" : "Message profile placeholder title"
},
"Профиль и поиск" : {
"localizations" : {
"en" : {
@ -2424,12 +2262,9 @@
},
"Публичная информация" : {
},
"Публичное имя" : {
},
"Разблокировать" : {
"comment" : "Message profile unblock alert title\nMessage profile unblock title\nUnblock confirmation action"
"comment" : "Unblock confirmation action"
},
"Разрешить пересылку сообщений" : {
"localizations" : {
@ -2544,20 +2379,11 @@
}
}
},
"Редактор контактов скоро появится. Мы сохраним имя, телефон и заметку." : {
"comment" : "Message profile add contact alert message"
},
"Режим" : {
},
"Режим автоудаления появится чуть позже. Мы добавим пресеты на 24 часа, 7 дней и 1 месяц — совсем как в Telegram." : {
"comment" : "Message profile auto delete alert message"
},
"Режим мессенжера" : {
},
"Рейтинг собеседника" : {
"comment" : "Message profile rating title"
},
"Сборка:" : {
"localizations" : {
@ -2675,20 +2501,11 @@
"Скоро" : {
"comment" : "Add blocked user placeholder title\nContacts placeholder title\nЗаголовок заглушки"
},
"Скоро можно будет искать сообщения, ссылки и файлы в этом чате." : {
"comment" : "Message profile search action description"
},
"Скоро можно будет очистить сообщения выборочно или целиком. Пока подготовим дизайн." : {
"comment" : "Message profile clear history alert message"
"Скоро здесь появится информация о собеседнике, статусе и дополнительных действиях." : {
"comment" : "Message profile placeholder description"
},
"Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : {
"comment" : "Concept tab placeholder description"
},
"Скоро появится разблокировка с подтверждением и синхронизацией." : {
"comment" : "Message profile unblock alert message"
},
"Скрыть" : {
},
"Слишком много запросов." : {
"localizations" : {
@ -2740,9 +2557,6 @@
},
"Сообщение слишком длинное." : {
},
"Сообщения пока сохраняются навсегда." : {
"comment" : "Message profile auto delete subtitle"
},
"Сообщите о материалах" : {
"comment" : "feedback category subtitle: content",
@ -2755,9 +2569,6 @@
}
}
},
"Сообщите о спаме или нарушении правил." : {
"comment" : "Message profile report subtitle"
},
"Сохранение..." : {
},
@ -2882,17 +2693,8 @@
}
}
},
"Тип диалога" : {
},
"Тишина включена. Чат не тревожит до включения сигнала." : {
"comment" : "Message profile notifications subtitle off"
},
"Только чаты (готово 60%)" : {
},
"Тонкие настройки диалога по образцу профиля Telegram." : {
"comment" : "Message profile chat settings description"
},
"Ты шо ебанутый? А ниче тот факт что новый пароль должен отличаться от старого." : {
"localizations" : {
@ -2919,7 +2721,6 @@
}
},
"Уведомления" : {
"comment" : "Message profile notifications title",
"localizations" : {
"en" : {
"stringUnit" : {
@ -2941,9 +2742,6 @@
"Удалить контакт" : {
"comment" : "Contacts context action delete"
},
"Удалить переписку только для себя." : {
"comment" : "Message profile clear history subtitle"
},
"Удалить фото" : {
"comment" : "Avatar delete"
},
@ -2967,12 +2765,6 @@
}
}
},
"Управление карточкой собеседника." : {
"comment" : "Message profile contact section description"
},
"Форма жалобы появится чуть позже — добавим прикрепление скриншотов и тип нарушения." : {
"comment" : "Message profile report alert message"
},
"Функция пока недоступна." : {
"comment" : "Сообщение заглушки"
},
@ -3100,9 +2892,6 @@
},
"Этот аккаунт недоступен." : {
},
"Юзернейм" : {
},
"Я ознакомился и принимаю правила сервиса" : {

File diff suppressed because it is too large Load Diff

View File

@ -52,7 +52,7 @@ struct PrivateChatView: View {
}
NavigationLink(
destination: MessageProfileView(chat: chat, currentUserId: currentUserId),
destination: MessageProfilePlaceholderView(chat: chat, currentUserId: currentUserId),
isActive: $isProfilePresented
) {
EmptyView()
@ -723,6 +723,156 @@ private var headerPlaceholderAvatar: some View {
}
}
struct MessageProfilePlaceholderView: View {
let chat: PrivateChatListItem
let currentUserId: String?
private let avatarSize: CGFloat = 96
var body: some View {
ScrollView {
VStack(spacing: 24) {
profileAvatar
VStack(spacing: 4) {
Text(displayName)
.font(.title3)
.fontWeight(.semibold)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
if let login = loginDisplay {
Text(login)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
Text(NSLocalizedString("Профиль для сообщений пока в разработке.", comment: "Message profile placeholder title"))
.font(.body)
.multilineTextAlignment(.center)
Text(NSLocalizedString("Скоро здесь появится информация о собеседнике, статусе и дополнительных действиях.", comment: "Message profile placeholder description"))
.font(.footnote)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 24)
.padding(.top, 60)
.frame(maxWidth: .infinity)
}
.background(Color(UIColor.systemBackground))
.navigationTitle(NSLocalizedString("Профиль", comment: "Message profile placeholder nav title"))
.navigationBarTitleDisplayMode(.inline)
}
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
}
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 var avatarUrl: URL? {
guard let chatData = chat.chatData,
let fileId = chatData.avatars?.current?.fileId else {
return nil
}
let userId = chatData.userId
return URL(string: "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(userId)?file_id=\(fileId)")
}
@ViewBuilder
private var profileAvatar: some View {
if let url = avatarUrl,
let fileId = chat.chatData?.avatars?.current?.fileId,
let userId = currentUserId {
CachedAvatarView(url: url, fileId: fileId, userId: userId) {
placeholderAvatar
}
.aspectRatio(contentMode: .fill)
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
} else {
placeholderAvatar
}
}
private var placeholderAvatar: some View {
Circle()
.fill(avatarBackgroundColor)
.frame(width: avatarSize, height: avatarSize)
.overlay(
Group {
if isDeletedUser {
Image(systemName: "person.slash")
.symbolRenderingMode(.hierarchical)
.font(.system(size: avatarSize * 0.45, weight: .semibold))
.foregroundColor(avatarTextColor)
} else {
Text(avatarInitial)
.font(.system(size: avatarSize * 0.45, weight: .semibold))
.foregroundColor(avatarTextColor)
}
}
)
}
private func trimmed(_ text: String?) -> String? {
guard let text = text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else {
return nil
}
return text
}
}
#if canImport(UIKit)
private struct LegacyMultilineTextView: UIViewRepresentable {