diff --git a/Montana-iOS/Montana Messenger/Montana Messenger/SettingsView.swift b/Montana-iOS/Montana Messenger/Montana Messenger/SettingsView.swift index a6c3578..a9182fc 100644 --- a/Montana-iOS/Montana Messenger/Montana Messenger/SettingsView.swift +++ b/Montana-iOS/Montana Messenger/Montana Messenger/SettingsView.swift @@ -1,107 +1,126 @@ import SwiftUI import Combine -@MainActor -final class SettingsStore: ObservableObject { - static let shared = SettingsStore() - @AppStorage("settings.notifications") var notifications: Bool = true - @AppStorage("settings.faceID") var faceID: Bool = true - @AppStorage("settings.silentMode") var silentMode: Bool = false -} - struct SettingsView: View { - @StateObject private var store = SettingsStore.shared - @State private var showAbout = false + @EnvironmentObject private var identity: IdentityManager @State private var showAccountID = false - @State private var showResetAlert = false + @State private var showAbout = false + @State private var showExport = false + @State private var showWipe = false + @State private var editingName = false + @State private var nameInput = "" var body: some View { NavigationStack { ScrollView { VStack(spacing: ClaudeTheme.Spacing.xl) { - ProfileHeader() + if let id = identity.identity { + ProfileHeader(identity: id, onEditName: { + nameInput = id.displayName + editingName = true + }) .padding(.top, ClaudeTheme.Spacing.xl) - .padding(.bottom, ClaudeTheme.Spacing.sm) - SettingsSection(title: "Идентичность") { - SettingsRow(icon: "key.fill", title: "Mnemonic фраза", subtitle: "12 слов · BIP-39") {} - SettingsRow(icon: "person.text.rectangle.fill", title: "Account ID", subtitle: "0x7a3f…b921") { - showAccountID = true + SettingsSection(title: "Идентичность") { + SettingsRow(icon: "person.text.rectangle.fill", + title: "Account ID", + subtitle: id.accountID) { + showAccountID = true + } + SettingsRow(icon: "key.fill", + title: "Экспорт ключей", + subtitle: "только на новое устройство") { + showExport = true + } } - SettingsRow(icon: "qrcode", title: "Поделиться контактом") {} - } - SettingsSection(title: "Безопасность") { - SettingsToggle(icon: "faceid", title: "Face ID", isOn: $store.faceID) - SettingsRow(icon: "lock.shield.fill", title: "PIN-код", subtitle: "Включён") {} - SettingsRow(icon: "shield.lefthalf.filled", title: "Постквантовая криптография", subtitle: "ML-DSA-65") {} - } + SettingsSection(title: "Сеть") { + SettingsInfo(icon: "antenna.radiowaves.left.and.right", + title: "Сервер", + value: "mess.montana.quest") + SettingsInfo(icon: "shield.lefthalf.filled", + title: "Шифрование", + value: "X25519 + ChaCha20-Poly1305") + } - SettingsSection(title: "Уведомления") { - SettingsToggle(icon: "bell.fill", title: "Push-уведомления", isOn: $store.notifications) - SettingsToggle(icon: "moon.fill", title: "Не беспокоить", isOn: $store.silentMode) - } + SettingsSection(title: "О приложении") { + SettingsRow(icon: "doc.text.fill", + title: "Манифест Монтаны") { showAbout = true } + SettingsInfo(icon: "info.circle.fill", + title: "Версия", + value: "1.0.0 (1)") + } - SettingsSection(title: "Сеть Montana") { - SettingsRow(icon: "network", title: "Узлы", subtitle: "мос · фра · зел") {} - SettingsRow(icon: "clock.arrow.2.circlepath", title: "TimeChain", subtitle: "Окно 2891844") {} - SettingsRow(icon: "antenna.radiowaves.left.and.right", title: "Соединение", subtitle: "Helsinki Reality") {} - } + Button(role: .destructive) { showWipe = true } label: { + Text("Стереть идентичность") + .font(ClaudeTheme.Typography.headline) + .foregroundStyle(ClaudeTheme.Palette.danger) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background( + RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous) + .fill(ClaudeTheme.Palette.surface) + ) + .overlay( + RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous) + .strokeBorder(ClaudeTheme.Palette.danger.opacity(0.3), lineWidth: 0.5) + ) + } + .padding(.horizontal, ClaudeTheme.Spacing.lg) - SettingsSection(title: "О приложении") { - SettingsRow(icon: "info.circle.fill", title: "Версия", subtitle: "0.1.0 (1)") {} - SettingsRow(icon: "doc.text.fill", title: "Манифест Монтаны") { showAbout = true } - SettingsRow(icon: "heart.fill", title: "Автор", subtitle: "Алик Монтана") {} + Footer().padding(.bottom, ClaudeTheme.Spacing.xxl) } - - Button { showResetAlert = true } label: { - Text("Сбросить идентичность") - .font(ClaudeTheme.Typography.headline) - .foregroundStyle(ClaudeTheme.Palette.danger) - .frame(maxWidth: .infinity) - .padding(.vertical, 14) - .background( - RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous) - .fill(ClaudeTheme.Palette.surface) - ) - .overlay( - RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous) - .strokeBorder(ClaudeTheme.Palette.danger.opacity(0.3), lineWidth: 0.5) - ) - } - .padding(.horizontal, ClaudeTheme.Spacing.lg) - - Footer().padding(.bottom, ClaudeTheme.Spacing.xxl) } } .claudeBackground() .navigationTitle("Настройки") .navigationBarTitleDisplayMode(.inline) - .alert("Сбросить идентичность?", isPresented: $showResetAlert) { + .alert("Стереть идентичность?", isPresented: $showWipe) { Button("Отмена", role: .cancel) {} - Button("Сбросить", role: .destructive) {} + Button("Стереть", role: .destructive) { identity.wipe() } } message: { - Text("Все ключи и сообщения будут стёрты с устройства.") + Text("Все ключи и переписка будут удалены с устройства. Если у тебя нет резервной копии ключей — восстановить аккаунт будет невозможно.") + } + .alert("Изменить имя", isPresented: $editingName) { + TextField("Имя", text: $nameInput) + Button("Сохранить") { + let trimmed = nameInput.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { identity.setDisplayName(trimmed) } + } + Button("Отмена", role: .cancel) {} + } + .sheet(isPresented: $showAbout) { AboutView() } + .sheet(isPresented: $showAccountID) { + if let id = identity.identity { AccountIDView(identity: id) } + } + .sheet(isPresented: $showExport) { + if let id = identity.identity { ExportKeysView(identity: id) } } - .sheet(isPresented: $showAbout) { AboutMontanaView() } - .sheet(isPresented: $showAccountID) { AccountIDView() } } } } private struct ProfileHeader: View { + let identity: Identity + let onEditName: () -> Void + var body: some View { VStack(spacing: ClaudeTheme.Spacing.md) { - AvatarView(name: "Алик Монтана", size: 96) + AvatarView(name: identity.displayName, size: 96) .shadow(color: ClaudeTheme.Palette.gold.opacity(0.35), radius: 18, y: 4) - VStack(spacing: 4) { - Text("Алик Монтана") - .font(.system(size: 24, weight: .semibold, design: .serif)) - .foregroundStyle(ClaudeTheme.Palette.textPrimary) - Text("@alik · 0x7a3f…b921") - .font(ClaudeTheme.Typography.caption) - .foregroundStyle(ClaudeTheme.Palette.textTertiary) + Button(action: onEditName) { + HStack(spacing: 6) { + Text(identity.displayName) + .font(.system(size: 24, weight: .semibold, design: .serif)) + .foregroundStyle(ClaudeTheme.Palette.textPrimary) + Image(systemName: "pencil") + .font(.system(size: 12)) + .foregroundStyle(ClaudeTheme.Palette.textTertiary) + } } + Text(identity.accountID) + .font(ClaudeTheme.Typography.mono) + .foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.7)) } .frame(maxWidth: .infinity) } @@ -113,12 +132,12 @@ private struct Footer: View { Text("Ӂ") .font(.system(size: 36, weight: .semibold, design: .serif)) .foregroundStyle(ClaudeTheme.Palette.goldGradient) - Text("MONTANA · TIMECHAIN") + Text("MONTANA") .font(.system(size: 10, weight: .medium)) .tracking(3) .foregroundStyle(ClaudeTheme.Palette.textTertiary) Text("XXIIVIIIMMXXXI") - .font(.system(size: 9, weight: .regular, design: .serif)) + .font(.system(size: 9, design: .serif)) .tracking(2) .foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.45)) } @@ -159,48 +178,50 @@ private struct SettingsRow: View { let action: () -> Void var body: some View { - Button(action: action) { - HStack(spacing: ClaudeTheme.Spacing.md) { - Image(systemName: icon) - .font(.system(size: 14)) - .foregroundStyle(ClaudeTheme.Palette.bubbleOutText) - .frame(width: 28, height: 28) - .background( - RoundedRectangle(cornerRadius: 7, style: .continuous) - .fill(ClaudeTheme.Palette.goldGradient) - ) - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(ClaudeTheme.Typography.body) - .foregroundStyle(ClaudeTheme.Palette.textPrimary) - if let subtitle { - Text(subtitle) - .font(ClaudeTheme.Typography.caption) - .foregroundStyle(ClaudeTheme.Palette.textTertiary) - } + Button(action: action) { rowBody(showChevron: true) } + } + + private func rowBody(showChevron: Bool) -> some View { + HStack(spacing: ClaudeTheme.Spacing.md) { + Image(systemName: icon) + .font(.system(size: 14)) + .foregroundStyle(ClaudeTheme.Palette.bubbleOutText) + .frame(width: 28, height: 28) + .background( + RoundedRectangle(cornerRadius: 7, style: .continuous) + .fill(ClaudeTheme.Palette.goldGradient) + ) + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(ClaudeTheme.Typography.body) + .foregroundStyle(ClaudeTheme.Palette.textPrimary) + if let subtitle { + Text(subtitle) + .font(ClaudeTheme.Typography.caption) + .foregroundStyle(ClaudeTheme.Palette.textTertiary) + .lineLimit(1) } - Spacer() + } + Spacer() + if showChevron { Image(systemName: "chevron.right") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(ClaudeTheme.Palette.textTertiary) } - .padding(.horizontal, ClaudeTheme.Spacing.md) - .padding(.vertical, ClaudeTheme.Spacing.md) - .overlay(alignment: .bottom) { - Rectangle() - .fill(ClaudeTheme.Palette.divider) - .frame(height: 0.5) - .padding(.leading, 56) - } - .contentShape(Rectangle()) } + .padding(.horizontal, ClaudeTheme.Spacing.md) + .padding(.vertical, ClaudeTheme.Spacing.md) + .overlay(alignment: .bottom) { + Rectangle().fill(ClaudeTheme.Palette.divider).frame(height: 0.5).padding(.leading, 56) + } + .contentShape(Rectangle()) } } -private struct SettingsToggle: View { +private struct SettingsInfo: View { let icon: String let title: String - @Binding var isOn: Bool + let value: String var body: some View { HStack(spacing: ClaudeTheme.Spacing.md) { @@ -216,98 +237,30 @@ private struct SettingsToggle: View { .font(ClaudeTheme.Typography.body) .foregroundStyle(ClaudeTheme.Palette.textPrimary) Spacer() - Toggle("", isOn: $isOn) - .labelsHidden() - .tint(ClaudeTheme.Palette.gold) + Text(value) + .font(ClaudeTheme.Typography.caption) + .foregroundStyle(ClaudeTheme.Palette.textTertiary) } .padding(.horizontal, ClaudeTheme.Spacing.md) .padding(.vertical, ClaudeTheme.Spacing.md) .overlay(alignment: .bottom) { - Rectangle() - .fill(ClaudeTheme.Palette.divider) - .frame(height: 0.5) - .padding(.leading, 56) - } - } -} - -private struct AboutMontanaView: View { - @Environment(\.dismiss) private var dismiss - var body: some View { - NavigationStack { - ScrollView { - VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) { - Text("Ӂ") - .font(.system(size: 80, weight: .semibold, design: .serif)) - .foregroundStyle(ClaudeTheme.Palette.goldGradient) - .frame(maxWidth: .infinity) - .padding(.top, ClaudeTheme.Spacing.xl) - - VStack(spacing: 4) { - Text("MONTANA") - .font(.system(size: 28, weight: .bold, design: .serif)) - .tracking(8) - .foregroundStyle(ClaudeTheme.Palette.textPrimary) - Text("TIMECHAIN PROTOCOL") - .font(.system(size: 11, weight: .medium)) - .tracking(4) - .foregroundStyle(ClaudeTheme.Palette.gold) - } - .frame(maxWidth: .infinity) - - VStack(alignment: .leading, spacing: 12) { - Text("Цифровая собственность.") - Text("Постквантовая криптография.") - Text("Твой ключ — твоя идентичность.") - Text("Никто не читает твои сообщения. Никто не знает, кто ты. Это нормально.") - .padding(.top, 8) - .foregroundStyle(ClaudeTheme.Palette.textSecondary) - } - .font(ClaudeTheme.Typography.serif) - .foregroundStyle(ClaudeTheme.Palette.textPrimary) - .padding(.horizontal, ClaudeTheme.Spacing.xl) - .padding(.top, ClaudeTheme.Spacing.lg) - - Spacer() - - VStack(spacing: 4) { - Text("XXIIVIIIMMXXXI") - .font(.system(size: 11, design: .serif)) - .tracking(2) - .foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.6)) - Text("Genesis · 9.01.2026") - .font(.system(size: 10)) - .foregroundStyle(ClaudeTheme.Palette.textTertiary) - } - .frame(maxWidth: .infinity) - .padding(.bottom, ClaudeTheme.Spacing.xl) - } - } - .claudeBackground() - .navigationTitle("Манифест") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Готово") { dismiss() } - .foregroundStyle(ClaudeTheme.Palette.gold) - } - } + Rectangle().fill(ClaudeTheme.Palette.divider).frame(height: 0.5).padding(.leading, 56) } } } private struct AccountIDView: View { @Environment(\.dismiss) private var dismiss - private let accountID = "0x7a3f9b1c8d4e2f0a5b6c9d8e7f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f921" + let identity: Identity var body: some View { NavigationStack { VStack(spacing: ClaudeTheme.Spacing.xl) { - AvatarView(name: "Алик Монтана", size: 120) + AvatarView(name: identity.displayName, size: 120) .padding(.top, ClaudeTheme.Spacing.xxl) .shadow(color: ClaudeTheme.Palette.gold.opacity(0.4), radius: 22, y: 6) - Text("Алик Монтана") + Text(identity.displayName) .font(ClaudeTheme.Typography.title) .foregroundStyle(ClaudeTheme.Palette.textPrimary) @@ -316,10 +269,9 @@ private struct AccountIDView: View { .font(.system(size: 11, weight: .medium)) .tracking(2) .foregroundStyle(ClaudeTheme.Palette.textTertiary) - Text(accountID) - .font(ClaudeTheme.Typography.mono) + Text(identity.accountID) + .font(.system(size: 18, design: .monospaced)) .foregroundStyle(ClaudeTheme.Palette.gold) - .multilineTextAlignment(.center) .padding(ClaudeTheme.Spacing.md) .background( RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous) @@ -332,8 +284,14 @@ private struct AccountIDView: View { } .padding(.horizontal, ClaudeTheme.Spacing.lg) + Text("Поделись этим Account ID с тем, кто хочет тебе написать.") + .font(ClaudeTheme.Typography.caption) + .foregroundStyle(ClaudeTheme.Palette.textTertiary) + .multilineTextAlignment(.center) + .padding(.horizontal, ClaudeTheme.Spacing.xl) + Button { - UIPasteboard.general.string = accountID + UIPasteboard.general.string = identity.accountID } label: { HStack { Image(systemName: "doc.on.doc") @@ -360,3 +318,145 @@ private struct AccountIDView: View { } } } + +private struct ExportKeysView: View { + @Environment(\.dismiss) private var dismiss + let identity: Identity + @State private var revealed = false + + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) { + HStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(ClaudeTheme.Palette.danger) + Text("Кто угодно с этими ключами может выдать себя за тебя. Не делись ими ни с кем кроме своих устройств.") + .font(ClaudeTheme.Typography.callout) + .foregroundStyle(ClaudeTheme.Palette.textSecondary) + } + .padding(ClaudeTheme.Spacing.md) + .background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.danger.opacity(0.08))) + + if revealed { + keyBlock(title: "ed_seed", value: identity.edSeedB64) + keyBlock(title: "x_seed", value: identity.xSeedB64) + + Button { + UIPasteboard.general.string = "ed_seed=\(identity.edSeedB64)\nx_seed=\(identity.xSeedB64)\nname=\(identity.displayName)" + } label: { + HStack { Image(systemName: "doc.on.doc"); Text("Копировать оба ключа") } + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(ClaudeTheme.Palette.bubbleOutText) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(Capsule().fill(ClaudeTheme.Palette.goldGradient)) + } + } else { + Button { revealed = true } label: { + Text("Показать ключи") + .font(ClaudeTheme.Typography.headline) + .foregroundStyle(ClaudeTheme.Palette.gold) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.surface)) + .overlay(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 1)) + } + } + } + .padding(ClaudeTheme.Spacing.lg) + } + .claudeBackground() + .navigationTitle("Экспорт ключей") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Готово") { dismiss() } + .foregroundStyle(ClaudeTheme.Palette.gold) + } + } + } + } + + private func keyBlock(title: String, value: String) -> some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.system(size: 11, weight: .semibold)) + .tracking(2) + .foregroundStyle(ClaudeTheme.Palette.textTertiary) + Text(value) + .font(.system(size: 12, design: .monospaced)) + .foregroundStyle(ClaudeTheme.Palette.gold) + .textSelection(.enabled) + .padding(ClaudeTheme.Spacing.md) + .frame(maxWidth: .infinity, alignment: .leading) + .background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.surface)) + .overlay(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 0.5)) + } + } +} + +private struct AboutView: View { + @Environment(\.dismiss) private var dismiss + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) { + Text("Ӂ") + .font(.system(size: 80, weight: .semibold, design: .serif)) + .foregroundStyle(ClaudeTheme.Palette.goldGradient) + .frame(maxWidth: .infinity) + .padding(.top, ClaudeTheme.Spacing.xl) + + VStack(spacing: 4) { + Text("MONTANA") + .font(.system(size: 28, weight: .bold, design: .serif)) + .tracking(8) + .foregroundStyle(ClaudeTheme.Palette.textPrimary) + Text("MESSENGER") + .font(.system(size: 11, weight: .medium)) + .tracking(4) + .foregroundStyle(ClaudeTheme.Palette.gold) + } + .frame(maxWidth: .infinity) + + VStack(alignment: .leading, spacing: 12) { + Text("Цифровая собственность.") + Text("Постквантовая криптография.") + Text("Твой ключ — твоя идентичность.") + Text("Никто не читает твои сообщения. Никто не знает, кто ты. Это нормально.") + .padding(.top, 8) + .foregroundStyle(ClaudeTheme.Palette.textSecondary) + } + .font(ClaudeTheme.Typography.serif) + .foregroundStyle(ClaudeTheme.Palette.textPrimary) + .padding(.horizontal, ClaudeTheme.Spacing.xl) + .padding(.top, ClaudeTheme.Spacing.lg) + + Spacer(minLength: 40) + + VStack(spacing: 4) { + Text("XXIIVIIIMMXXXI") + .font(.system(size: 11, design: .serif)) + .tracking(2) + .foregroundStyle(ClaudeTheme.Palette.gold.opacity(0.6)) + Text("Genesis · 9.01.2026") + .font(.system(size: 10)) + .foregroundStyle(ClaudeTheme.Palette.textTertiary) + } + .frame(maxWidth: .infinity) + .padding(.bottom, ClaudeTheme.Spacing.xl) + } + } + .claudeBackground() + .navigationTitle("Манифест") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Готово") { dismiss() } + .foregroundStyle(ClaudeTheme.Palette.gold) + } + } + } + } +}