mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 09:41:02 +00:00
Post editor: Drafts support
This commit is contained in:
parent
f1219d319b
commit
9cf863d8c3
6 changed files with 94 additions and 59 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -42,7 +45,12 @@ struct StatusEditorAccessoryView: View {
|
||||||
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle")
|
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle")
|
||||||
}
|
}
|
||||||
|
|
||||||
visibilityMenu
|
Button {
|
||||||
|
isDrafsSheetDisplayed = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "archivebox")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
if !viewModel.statusText.string.isEmpty {
|
||||||
|
isDismissAlertPresented = true
|
||||||
|
} else {
|
||||||
dismiss()
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ extension Visibility {
|
||||||
case .priv:
|
case .priv:
|
||||||
return "Followers"
|
return "Followers"
|
||||||
case .direct:
|
case .direct:
|
||||||
return "Private Mention"
|
return "Private"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue