diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 4a65c94..a0815d7 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -581,6 +581,9 @@ "Добавить контакт" : { "comment" : "Message profile add contact alert title" }, + "Добавление контакта появится позже." : { + "comment" : "Contact add placeholder message" + }, "Добавление новых блокировок появится позже." : { "comment" : "Add blocked user placeholder message" }, @@ -810,7 +813,7 @@ } }, "Изменение фото недоступно" : { - "comment" : "Contact edit avatar unavailable title", + "comment" : "Contact add avatar unavailable title\nContact edit avatar unavailable title", "localizations" : { "en" : { "stringUnit" : { @@ -1171,7 +1174,7 @@ "comment" : "Сообщение при недоступной отправке письма" }, "Мы пока не можем обновить фото контакта." : { - "comment" : "Contact edit avatar unavailable message", + "comment" : "Contact add avatar unavailable message\nContact edit avatar unavailable message", "localizations" : { "en" : { "stringUnit" : { @@ -1661,6 +1664,9 @@ "Новое сообщение" : { "comment" : "Default banner subtitle" }, + "Новый контакт" : { + "comment" : "Contact add title" + }, "Новый пароль" : { "comment" : "Новый пароль", "localizations" : { @@ -2459,7 +2465,7 @@ }, "Публичная информация" : { - "comment" : "Profile info section title" + "comment" : "Contact add public info section title\nProfile info section title" }, "Публичное имя" : { @@ -2806,7 +2812,7 @@ "comment" : "Сообщение после активации 2FA" }, "Сохранить" : { - "comment" : "Contact edit save button", + "comment" : "Contact add save button\nContact edit save button", "localizations" : { "en" : { "stringUnit" : { diff --git a/yobble/Views/Chat/MessageProfileView.swift b/yobble/Views/Chat/MessageProfileView.swift index 532b348..9fe9e47 100644 --- a/yobble/Views/Chat/MessageProfileView.swift +++ b/yobble/Views/Chat/MessageProfileView.swift @@ -287,11 +287,23 @@ struct MessageProfileView: View { if shouldShowRelationshipQuickActions { rowDivider - filledActionButton( - title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"), - tint: Color.accentColor - ) { - handleAddContactTap() + if let profile = currentChatProfile { + NavigationLink { + ContactAddView(contact: ContactEditInfo(profile: profile)) + } label: { + filledActionLabel( + title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"), + tint: Color.accentColor + ) + } + .buttonStyle(.plain) + } else { + filledActionButton( + title: NSLocalizedString("Добавить в контакты", comment: "Message profile add to contacts title"), + tint: Color.accentColor + ) { + handleAddContactTap() + } } rowDivider @@ -501,23 +513,31 @@ struct MessageProfileView: View { action: @escaping () -> Void ) -> some View { Button(action: action) { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.body) - .fontWeight(.semibold) - if let subtitle { - Text(subtitle) - .font(.caption) - .foregroundColor(tint.opacity(0.7)) - } - } - .foregroundColor(tint) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 10) + filledActionLabel(title: title, subtitle: subtitle, tint: tint) } .buttonStyle(.plain) } + private func filledActionLabel( + title: String, + subtitle: String? = nil, + tint: Color + ) -> some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.body) + .fontWeight(.semibold) + if let subtitle { + Text(subtitle) + .font(.caption) + .foregroundColor(tint.opacity(0.7)) + } + } + .foregroundColor(tint) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 10) + } + private func iconBackground(color: Color, @ViewBuilder content: () -> Content) -> some View { RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(color) diff --git a/yobble/Views/Contacts/ContactAddView.swift b/yobble/Views/Contacts/ContactAddView.swift new file mode 100644 index 0000000..d5c6f6c --- /dev/null +++ b/yobble/Views/Contacts/ContactAddView.swift @@ -0,0 +1,137 @@ +import SwiftUI + +struct ContactAddView: View { + let contact: ContactEditInfo + + @State private var displayName: String + @State private var activeAlert: ContactAddAlert? + + init(contact: ContactEditInfo) { + self.contact = contact + let initialName = contact.preferredName + _displayName = State(initialValue: initialName) + } + + var body: some View { + Form { + avatarSection + + Section(header: Text(NSLocalizedString("Публичная информация", comment: "Contact add public info section title"))) { + TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName) + } + } + .navigationTitle(NSLocalizedString("Новый контакт", comment: "Contact add title")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(NSLocalizedString("Сохранить", comment: "Contact add save button")) { + handleSaveTap() + } + .disabled(!hasChanges) + } + } + .alert(item: $activeAlert) { item in + Alert( + title: Text(item.title), + message: Text(item.message), + dismissButton: .default(Text(NSLocalizedString("Понятно", comment: "Placeholder alert dismiss"))) + ) + } + } + + private var avatarSection: some View { + Section { + HStack { + Spacer() + VStack(spacing: 8) { + avatarView + .frame(width: 120, height: 120) + .clipShape(Circle()) + + Button(NSLocalizedString("Изменить фото", comment: "Edit avatar button title")) { + showAvatarUnavailableAlert() + } + } + Spacer() + } + } + .listRowBackground(Color(UIColor.systemGroupedBackground)) + } + + @ViewBuilder + private var avatarView: some View { + if let url = avatarURL, + let fileId = contact.avatarFileId { + CachedAvatarView(url: url, fileId: fileId, userId: contact.userId) { + avatarPlaceholder + } + .aspectRatio(contentMode: .fill) + } else { + avatarPlaceholder + } + } + + private var avatarPlaceholder: some View { + Circle() + .fill(Color.accentColor.opacity(0.15)) + .overlay( + Text(avatarInitial) + .font(.system(size: 48, weight: .semibold)) + .foregroundColor(.accentColor) + ) + } + + private var avatarInitial: String { + let trimmedName = displayName.trimmedNonEmpty ?? contact.preferredName + if let first = trimmedName.trimmingCharacters(in: .whitespacesAndNewlines).first { + return String(first).uppercased() + } + if let login = contact.login?.trimmingCharacters(in: .whitespacesAndNewlines), !login.isEmpty { + return String(login.prefix(1)).uppercased() + } + return "?" + } + + private var avatarURL: URL? { + guard let fileId = contact.avatarFileId else { return nil } + return URL(string: "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(contact.userId)?file_id=\(fileId)") + } + + private var hasChanges: Bool { + let trimmed = displayName.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return false } + + if let existing = contact.customName?.trimmedNonEmpty { + return trimmed != existing + } + + return true + } + + private func showAvatarUnavailableAlert() { + activeAlert = ContactAddAlert( + title: NSLocalizedString("Изменение фото недоступно", comment: "Contact add avatar unavailable title"), + message: NSLocalizedString("Мы пока не можем обновить фото контакта.", comment: "Contact add avatar unavailable message") + ) + } + + private func handleSaveTap() { + activeAlert = ContactAddAlert( + title: NSLocalizedString("Скоро", comment: "Common soon title"), + message: NSLocalizedString("Добавление контакта появится позже.", comment: "Contact add placeholder message") + ) + } +} + +private struct ContactAddAlert: Identifiable { + let id = UUID() + let title: String + let message: String +} + +private extension String { + var trimmedNonEmpty: String? { + let value = trimmingCharacters(in: .whitespacesAndNewlines) + return value.isEmpty ? nil : value + } +}