Post editor: Drafts support

This commit is contained in:
Thomas Ricouard 2023-01-11 12:44:34 +01:00
parent f1219d319b
commit 9cf863d8c3
6 changed files with 94 additions and 59 deletions

View file

@ -48,27 +48,6 @@ struct PushNotificationsView: View {
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
.transition(.move(edge: .bottom)) .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") .navigationTitle("Push Notifications")
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)

View file

@ -21,7 +21,6 @@ class NotificationService: UNNotificationServiceExtension {
guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String, guard let encodedPayload = bestAttemptContent.userInfo["m"] as? String,
let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else { let payload = Data(base64Encoded: encodedPayload.URLSafeBase64ToBase64()) else {
bestAttemptContent.title = "Failied to decode payload as base 64"
contentHandler(bestAttemptContent) contentHandler(bestAttemptContent)
return return
} }
@ -29,14 +28,12 @@ class NotificationService: UNNotificationServiceExtension {
guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String, guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String,
let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()), let publicKeyData = Data(base64Encoded: encodedPublicKey.URLSafeBase64ToBase64()),
let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) else { let publicKey = try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) else {
bestAttemptContent.title = "Failied to inflate public key"
contentHandler(bestAttemptContent) contentHandler(bestAttemptContent)
return return
} }
guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String, guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String,
let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) else { let salt = Data(base64Encoded: encodedSalt.URLSafeBase64ToBase64()) else {
bestAttemptContent.title = "Failied to inflate salt"
contentHandler(bestAttemptContent) contentHandler(bestAttemptContent)
return return
} }
@ -47,7 +44,6 @@ class NotificationService: UNNotificationServiceExtension {
privateKey: privateKey, privateKey: privateKey,
publicKey: publicKey), publicKey: publicKey),
let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) else { let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) else {
bestAttemptContent.title = "Failied to JSON decode the Notification"
contentHandler(bestAttemptContent) contentHandler(bestAttemptContent)
return return
} }

View file

@ -11,6 +11,7 @@ public class UserPreferences: ObservableObject {
@AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = [] @AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = []
@AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari @AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari
@AppStorage("draft_posts") public var draftsPosts: [String] = []
public var pushNotificationsCount: Int { public var pushNotificationsCount: Int {
get { get {

View file

@ -5,12 +5,15 @@ import Models
import Env import Env
struct StatusEditorAccessoryView: View { struct StatusEditorAccessoryView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance @EnvironmentObject private var currentInstance: CurrentInstance
@FocusState<Bool>.Binding var isSpoilerTextFocused: Bool @FocusState<Bool>.Binding var isSpoilerTextFocused: Bool
@ObservedObject var viewModel: StatusEditorViewModel @ObservedObject var viewModel: StatusEditorViewModel
@State private var isPrivacySheetDisplayed: Bool = false @State private var isDrafsSheetDisplayed: Bool = false
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -41,8 +44,13 @@ struct StatusEditorAccessoryView: View {
} label: { } label: {
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle") Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle")
} }
Button {
isDrafsSheetDisplayed = true
} label: {
Image(systemName: "archivebox")
}
visibilityMenu
Spacer() Spacer()
@ -53,6 +61,40 @@ struct StatusEditorAccessoryView: View {
.padding(.vertical, 12) .padding(.vertical, 12)
.background(.ultraThinMaterial) .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) .foregroundColor(.gray)
.font(.callout) .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)
}
}
} }

View file

@ -9,6 +9,7 @@ import PhotosUI
import NukeUI import NukeUI
public struct StatusEditorView: View { public struct StatusEditorView: View {
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentAccount: CurrentAccount
@ -17,6 +18,8 @@ public struct StatusEditorView: View {
@StateObject private var viewModel: StatusEditorViewModel @StateObject private var viewModel: StatusEditorViewModel
@FocusState private var isSpoilerTextFocused: Bool @FocusState private var isSpoilerTextFocused: Bool
@State private var isDismissAlertPresented: Bool = false
public init(mode: StatusEditorViewModel.Mode) { public init(mode: StatusEditorViewModel.Mode) {
_viewModel = StateObject(wrappedValue: .init(mode: mode)) _viewModel = StateObject(wrappedValue: .init(mode: mode))
} }
@ -88,13 +91,30 @@ public struct StatusEditorView: View {
} }
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button { Button {
dismiss() if !viewModel.statusText.string.isEmpty {
isDismissAlertPresented = true
} else {
dismiss()
}
} label: { } label: {
Text("Cancel") 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 @ViewBuilder
@ -116,10 +136,8 @@ public struct StatusEditorView: View {
if let account = currentAccount.account { if let account = currentAccount.account {
HStack { HStack {
AvatarView(url: account.avatar, size: .status) AvatarView(url: account.avatar, size: .status)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 4) {
account.displayNameWithEmojis privacyMenu
.font(.subheadline)
.fontWeight(.semibold)
Text("@\(account.acct)") Text("@\(account.acct)")
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .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)
)
}
}
} }

View file

@ -27,7 +27,7 @@ extension Visibility {
case .priv: case .priv:
return "Followers" return "Followers"
case .direct: case .direct:
return "Private Mention" return "Private"
} }
} }
} }