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)
.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)

View file

@ -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
}

View file

@ -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 {

View file

@ -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<Bool>.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) {
@ -42,7 +45,12 @@ struct StatusEditorAccessoryView: View {
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle")
}
visibilityMenu
Button {
isDrafsSheetDisplayed = true
} label: {
Image(systemName: "archivebox")
}
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)
}
}
}

View file

@ -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 {
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)
)
}
}
}

View file

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