diff --git a/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift b/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift index a59cfcbb..56a6e243 100644 --- a/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift +++ b/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift @@ -48,27 +48,6 @@ struct PushNotificationsView: View { .listRowBackground(theme.primaryBackgroundColor) .transition(.move(edge: .bottom)) } - - Section { - VStack(alignment: .leading) { - Text("Auth key:") - Text(pushNotifications.notificationsAuthKeyAsKey.base64EncodedString()) - .font(.footnote) - .foregroundColor(.gray) - } - VStack(alignment: .leading) { - Text("Public key:") - Text(pushNotifications.notificationsPrivateKeyAsKey.publicKey.x963Representation.base64EncodedString()) - .font(.footnote) - .foregroundColor(.gray) - } - } header: { - Text("Keys information") - } footer: { - Text("Your notifications are sent through a proxy server and are encrypted using a public/private key pair that is stored only on your device. The public key is sent to the server, so it can encrypt your notifications so that only your device can decrypt them.") - } - .listRowBackground(theme.primaryBackgroundColor) - } .navigationTitle("Push Notifications") .scrollContentBackground(.hidden) diff --git a/IceCubesNotifications/NotificationService.swift b/IceCubesNotifications/NotificationService.swift index 549c6cff..36cbef40 100644 --- a/IceCubesNotifications/NotificationService.swift +++ b/IceCubesNotifications/NotificationService.swift @@ -21,7 +21,6 @@ class NotificationService: UNNotificationServiceExtension { guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String, let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else { - bestAttemptContent.title = "Failied to decode payload as base 64" contentHandler(bestAttemptContent) return } @@ -29,14 +28,12 @@ class NotificationService: UNNotificationServiceExtension { guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String, let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()), let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) else { - bestAttemptContent.title = "Failied to inflate public key" contentHandler(bestAttemptContent) return } guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String, let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) else { - bestAttemptContent.title = "Failied to inflate salt" contentHandler(bestAttemptContent) return } @@ -47,7 +44,6 @@ class NotificationService: UNNotificationServiceExtension { privateKey: privateKey, publicKey: publicKey), let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) else { - bestAttemptContent.title = "Failied to JSON decode the Notification" contentHandler(bestAttemptContent) return } diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index d6638385..2f5230a9 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -11,6 +11,7 @@ public class UserPreferences: ObservableObject { @AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = [] @AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari + @AppStorage("draft_posts") public var draftsPosts: [String] = [] public var pushNotificationsCount: Int { get { diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index 947a5144..c7cf9031 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -5,12 +5,15 @@ import Models import Env struct StatusEditorAccessoryView: View { + @Environment(\.dismiss) private var dismiss + + @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var theme: Theme @EnvironmentObject private var currentInstance: CurrentInstance @FocusState.Binding var isSpoilerTextFocused: Bool @ObservedObject var viewModel: StatusEditorViewModel - @State private var isPrivacySheetDisplayed: Bool = false + @State private var isDrafsSheetDisplayed: Bool = false var body: some View { VStack(spacing: 0) { @@ -41,8 +44,13 @@ struct StatusEditorAccessoryView: View { } label: { Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle") } + + Button { + isDrafsSheetDisplayed = true + } label: { + Image(systemName: "archivebox") + } - visibilityMenu Spacer() @@ -53,6 +61,40 @@ struct StatusEditorAccessoryView: View { .padding(.vertical, 12) .background(.ultraThinMaterial) } + .sheet(isPresented: $isDrafsSheetDisplayed) { + draftsSheetView + } + } + + private var draftsSheetView: some View { + NavigationStack { + List { + ForEach(preferences.draftsPosts, id: \.self) { draft in + Text(draft) + .lineLimit(3) + .listRowBackground(theme.primaryBackgroundColor) + .onTapGesture { + viewModel.insertStatusText(text: draft) + isDrafsSheetDisplayed = false + } + } + .onDelete { indexes in + if let index = indexes.first { + preferences.draftsPosts.remove(at: index) + } + } + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel", action: { dismiss() }) + } + } + .scrollContentBackground(.hidden) + .background(theme.secondaryBackgroundColor) + .navigationTitle("Drafts") + .navigationBarTitleDisplayMode(.inline) + } + .presentationDetents([.medium]) } @@ -61,30 +103,4 @@ struct StatusEditorAccessoryView: View { .foregroundColor(.gray) .font(.callout) } - - private var visibilityMenu: some View { - Button { - isPrivacySheetDisplayed = true - } label: { - Image(systemName: viewModel.visibility.iconName) - } - .sheet(isPresented: $isPrivacySheetDisplayed) { - Form { - Section("Post visibility") { - ForEach(Models.Visibility.allCases, id: \.self) { visibility in - Button { - viewModel.visibility = visibility - isPrivacySheetDisplayed = false - } label: { - Label(visibility.title, systemImage: visibility.iconName) - } - } - .listRowBackground(theme.primaryBackgroundColor) - } - } - .presentationDetents([.height(300)]) - .scrollContentBackground(.hidden) - .background(theme.secondaryBackgroundColor) - } - } } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 3fc6fd78..51a26fab 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -9,6 +9,7 @@ import PhotosUI import NukeUI public struct StatusEditorView: View { + @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var theme: Theme @EnvironmentObject private var client: Client @EnvironmentObject private var currentAccount: CurrentAccount @@ -17,6 +18,8 @@ public struct StatusEditorView: View { @StateObject private var viewModel: StatusEditorViewModel @FocusState private var isSpoilerTextFocused: Bool + @State private var isDismissAlertPresented: Bool = false + public init(mode: StatusEditorViewModel.Mode) { _viewModel = StateObject(wrappedValue: .init(mode: mode)) } @@ -88,13 +91,30 @@ public struct StatusEditorView: View { } ToolbarItem(placement: .navigationBarLeading) { Button { - dismiss() + if !viewModel.statusText.string.isEmpty { + isDismissAlertPresented = true + } else { + dismiss() + } } label: { Text("Cancel") } } } } + .confirmationDialog("", + isPresented: $isDismissAlertPresented, + actions: { + Button("Delete Draft", role: .destructive) { + dismiss() + } + Button("Save Draft") { + preferences.draftsPosts.insert(viewModel.statusText.string, at: 0) + dismiss() + } + Button("Cancel", role: .cancel) { } + }) + .interactiveDismissDisabled(!viewModel.statusText.string.isEmpty) } @ViewBuilder @@ -116,10 +136,8 @@ public struct StatusEditorView: View { if let account = currentAccount.account { HStack { AvatarView(url: account.avatar, size: .status) - VStack(alignment: .leading, spacing: 0) { - account.displayNameWithEmojis - .font(.subheadline) - .fontWeight(.semibold) + VStack(alignment: .leading, spacing: 4) { + privacyMenu Text("@\(account.acct)") .font(.footnote) .foregroundColor(.gray) @@ -128,4 +146,29 @@ public struct StatusEditorView: View { } } } + + private var privacyMenu: some View { + Menu { + Section("Post visibility") { + ForEach(Models.Visibility.allCases, id: \.self) { visibility in + Button { + viewModel.visibility = visibility + } label: { + Label(visibility.title, systemImage: visibility.iconName) + } + } + } + } label: { + HStack { + Label(viewModel.visibility.title, systemImage: viewModel.visibility.iconName) + Image(systemName: "chevron.down") + } + .font(.footnote) + .padding(4) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(theme.tintColor, lineWidth: 1) + ) + } + } } diff --git a/Packages/Status/Sources/Status/Ext/Visibility.swift b/Packages/Status/Sources/Status/Ext/Visibility.swift index 2a37505b..774f6d80 100644 --- a/Packages/Status/Sources/Status/Ext/Visibility.swift +++ b/Packages/Status/Sources/Status/Ext/Visibility.swift @@ -27,7 +27,7 @@ extension Visibility { case .priv: return "Followers" case .direct: - return "Private Mention" + return "Private" } } }