Drafts list
This commit is contained in:
parent
a7924feb76
commit
fbbcb0d07d
@ -246,7 +246,9 @@ public final class ComposeController: ViewController {
|
|||||||
let oldDraft = self.draft
|
let oldDraft = self.draft
|
||||||
self.draft = newDraft
|
self.draft = newDraft
|
||||||
|
|
||||||
if !oldDraft.hasContent {
|
if oldDraft.hasContent {
|
||||||
|
oldDraft.lastModified = Date()
|
||||||
|
} else if oldDraft.hasContent {
|
||||||
DraftsPersistentContainer.shared.viewContext.delete(oldDraft)
|
DraftsPersistentContainer.shared.viewContext.delete(oldDraft)
|
||||||
}
|
}
|
||||||
DraftsPersistentContainer.shared.save()
|
DraftsPersistentContainer.shared.save()
|
||||||
@ -256,6 +258,8 @@ public final class ComposeController: ViewController {
|
|||||||
isDisappearing = true
|
isDisappearing = true
|
||||||
if deleteDraftOnDisappear && (!draft.hasContent || didPostSuccessfully || userConfirmedDelete) {
|
if deleteDraftOnDisappear && (!draft.hasContent || didPostSuccessfully || userConfirmedDelete) {
|
||||||
DraftsPersistentContainer.shared.viewContext.delete(draft)
|
DraftsPersistentContainer.shared.viewContext.delete(draft)
|
||||||
|
} else {
|
||||||
|
draft.lastModified = Date()
|
||||||
}
|
}
|
||||||
DraftsPersistentContainer.shared.save()
|
DraftsPersistentContainer.shared.save()
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,14 @@ import Photos
|
|||||||
struct AttachmentThumbnailView: View {
|
struct AttachmentThumbnailView: View {
|
||||||
let attachment: DraftAttachment
|
let attachment: DraftAttachment
|
||||||
var contentMode: ContentMode = .fit
|
var contentMode: ContentMode = .fit
|
||||||
|
var thumbnailSize: CGSize?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
AttachmentThumbnailViewContent(attachment: attachment, contentMode: contentMode)
|
AttachmentThumbnailViewContent(
|
||||||
|
attachment: attachment,
|
||||||
|
contentMode: contentMode,
|
||||||
|
thumbnailSize: thumbnailSize
|
||||||
|
)
|
||||||
.id(attachment.id)
|
.id(attachment.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +29,7 @@ struct AttachmentThumbnailView: View {
|
|||||||
private struct AttachmentThumbnailViewContent: View {
|
private struct AttachmentThumbnailViewContent: View {
|
||||||
var attachment: DraftAttachment
|
var attachment: DraftAttachment
|
||||||
var contentMode: ContentMode = .fit
|
var contentMode: ContentMode = .fit
|
||||||
|
var thumbnailSize: CGSize?
|
||||||
@State private var mode: Mode = .empty
|
@State private var mode: Mode = .empty
|
||||||
@EnvironmentObject private var composeController: ComposeController
|
@EnvironmentObject private var composeController: ComposeController
|
||||||
|
|
||||||
@ -78,7 +84,7 @@ private struct AttachmentThumbnailViewContent: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let size = CGSize(width: 80, height: 80)
|
let size = thumbnailSize ?? CGSize(width: 80, height: 80)
|
||||||
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { image, _ in
|
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { image, _ in
|
||||||
if let image {
|
if let image {
|
||||||
self.mode = .image(image)
|
self.mode = .image(image)
|
||||||
@ -99,7 +105,7 @@ private struct AttachmentThumbnailViewContent: View {
|
|||||||
if let image = UIImage(data: data),
|
if let image = UIImage(data: data),
|
||||||
// using prepareThumbnail on images from PHPicker results in extremely high memory usage,
|
// using prepareThumbnail on images from PHPicker results in extremely high memory usage,
|
||||||
// crashing share extension. see FB12186346
|
// crashing share extension. see FB12186346
|
||||||
let prepared = await image.byPreparingForDisplay() {
|
let prepared = await thumbnailImage(image) {
|
||||||
self.mode = .image(prepared)
|
self.mode = .image(prepared)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,6 +116,14 @@ private struct AttachmentThumbnailViewContent: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func thumbnailImage(_ image: UIImage) async -> UIImage? {
|
||||||
|
if let thumbnailSize {
|
||||||
|
await image.byPreparingThumbnail(ofSize: thumbnailSize)
|
||||||
|
} else {
|
||||||
|
await image.byPreparingForDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadVideoThumbnail(url: URL) async {
|
private func loadVideoThumbnail(url: URL) async {
|
||||||
let asset = AVURLAsset(url: url)
|
let asset = AVURLAsset(url: url)
|
||||||
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
||||||
|
@ -13,10 +13,19 @@ struct ComposeView: View {
|
|||||||
@State private var poster: PostService? = nil
|
@State private var poster: PostService? = nil
|
||||||
@FocusState private var focusedField: FocusableField?
|
@FocusState private var focusedField: FocusableField?
|
||||||
@EnvironmentObject private var controller: ComposeController
|
@EnvironmentObject private var controller: ComposeController
|
||||||
|
@State private var isShowingDrafts = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
navigation
|
navigation
|
||||||
.environmentObject(mastodonController.instanceFeatures)
|
.environmentObject(mastodonController.instanceFeatures)
|
||||||
|
.sheet(isPresented: $isShowingDrafts) {
|
||||||
|
DraftsView(
|
||||||
|
currentDraft: draft,
|
||||||
|
isShowingDrafts: $isShowingDrafts,
|
||||||
|
accountInfo: mastodonController.accountInfo!,
|
||||||
|
selectDraft: self.selectDraft
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -71,7 +80,7 @@ struct ComposeView: View {
|
|||||||
.modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController))
|
.modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarActions(draft: draft, controller: controller)
|
ToolbarActions(draft: draft, controller: controller, isShowingDrafts: $isShowingDrafts)
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
ToolbarItem(placement: .bottomOrnament) {
|
ToolbarItem(placement: .bottomOrnament) {
|
||||||
toolbarView
|
toolbarView
|
||||||
@ -97,6 +106,11 @@ struct ComposeView: View {
|
|||||||
private func addAttachments(_ itemProviders: [NSItemProvider]) {
|
private func addAttachments(_ itemProviders: [NSItemProvider]) {
|
||||||
AttachmentsSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: itemProviders)
|
AttachmentsSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: itemProviders)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func selectDraft(_ draft: Draft) {
|
||||||
|
controller.selectDraft(draft)
|
||||||
|
isShowingDrafts = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct NavigationTitleModifier: ViewModifier {
|
private struct NavigationTitleModifier: ViewModifier {
|
||||||
@ -132,8 +146,9 @@ public struct NavigationTitlePreferenceKey: PreferenceKey {
|
|||||||
private struct ToolbarActions: ToolbarContent {
|
private struct ToolbarActions: ToolbarContent {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
// Prior to iOS 16, the toolbar content doesn't seem to have access
|
// Prior to iOS 16, the toolbar content doesn't seem to have access
|
||||||
// to the environment form the containing view.
|
// to the environment from the containing view.
|
||||||
let controller: ComposeController
|
let controller: ComposeController
|
||||||
|
@Binding var isShowingDrafts: Bool
|
||||||
|
|
||||||
var body: some ToolbarContent {
|
var body: some ToolbarContent {
|
||||||
ToolbarItem(placement: .cancellationAction) { ToolbarCancelButton(draft: draft) }
|
ToolbarItem(placement: .cancellationAction) { ToolbarCancelButton(draft: draft) }
|
||||||
@ -147,7 +162,9 @@ private struct ToolbarActions: ToolbarContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var draftsButton: some View {
|
private var draftsButton: some View {
|
||||||
Button(action: controller.showDrafts) {
|
Button {
|
||||||
|
isShowingDrafts = true
|
||||||
|
} label: {
|
||||||
Text("Drafts")
|
Text("Drafts")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,9 +67,6 @@ private struct LanguageButton: View {
|
|||||||
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $hasChanged)
|
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $hasChanged)
|
||||||
.buttonStyle(LanguageButtonStyle())
|
.buttonStyle(LanguageButtonStyle())
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification), perform: currentInputModeChanged)
|
.onReceive(NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification), perform: currentInputModeChanged)
|
||||||
.onChange(of: draft.id) { _ in
|
|
||||||
hasChanged = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
158
Packages/ComposeUI/Sources/ComposeUI/Views/DraftsView.swift
Normal file
158
Packages/ComposeUI/Sources/ComposeUI/Views/DraftsView.swift
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
//
|
||||||
|
// DraftsView.swift
|
||||||
|
// ComposeUI
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/27/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UserAccounts
|
||||||
|
|
||||||
|
struct DraftsView: View {
|
||||||
|
let currentDraft: Draft
|
||||||
|
@Binding var isShowingDrafts: Bool
|
||||||
|
let accountInfo: UserAccountInfo
|
||||||
|
let selectDraft: (Draft) -> Void
|
||||||
|
@State private var draftForDifferentReply: Draft? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
navigationView
|
||||||
|
.alertWithData("Different Reply", data: $draftForDifferentReply) { draft in
|
||||||
|
Button(role: .cancel) {
|
||||||
|
draftForDifferentReply = nil
|
||||||
|
} label: {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
selectDraft(draft)
|
||||||
|
} label: {
|
||||||
|
Text("Restore Draft")
|
||||||
|
}
|
||||||
|
} message: { _ in
|
||||||
|
Text("The selected draft is a reply to a different post, do you wish to use it?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var navigationView: some View {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
NavigationStack {
|
||||||
|
navigationRoot
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NavigationView {
|
||||||
|
navigationRoot
|
||||||
|
}
|
||||||
|
.navigationViewStyle(.stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var navigationRoot: some View {
|
||||||
|
DraftsListView(currentDraft: currentDraft, isShowingDrafts: $isShowingDrafts, accountInfo: accountInfo, selectDraft: selectDraft)
|
||||||
|
.environment(\.managedObjectContext, DraftsPersistentContainer.shared.viewContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DraftsListView: View {
|
||||||
|
let currentDraft: Draft
|
||||||
|
@Binding var isShowingDrafts: Bool
|
||||||
|
let accountInfo: UserAccountInfo
|
||||||
|
let selectDraft: (Draft) -> Void
|
||||||
|
@FetchRequest(sortDescriptors: [SortDescriptor(\Draft.lastModified, order: .reverse)]) private var drafts: FetchedResults<Draft>
|
||||||
|
@Environment(\.composeUIConfig.userActivityForDraft) private var userActivityForDraft
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(drafts) { draft in
|
||||||
|
Button {
|
||||||
|
self.selectDraft(draft)
|
||||||
|
} label: {
|
||||||
|
DraftRowView(draft: draft)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
deleteDraft(draft)
|
||||||
|
} label: {
|
||||||
|
Label("Delete Draft", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDrag {
|
||||||
|
userActivityForDraft(draft) ?? NSItemProvider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indices in
|
||||||
|
indices.map { drafts[$0] }.forEach(deleteDraft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.navigationTitle("Drafts")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
cancelButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
drafts.nsPredicate = NSPredicate(
|
||||||
|
format: "accountID == %@ AND id != %@ AND lastModified != nil",
|
||||||
|
accountInfo.id,
|
||||||
|
currentDraft.id as NSUUID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cancelButton: some View {
|
||||||
|
Button {
|
||||||
|
isShowingDrafts = false
|
||||||
|
} label: {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteDraft(_ draft: Draft) {
|
||||||
|
DraftsPersistentContainer.shared.viewContext.delete(draft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DraftRowView: View {
|
||||||
|
@ObservedObject var draft: Draft
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if draft.editedStatusID != nil {
|
||||||
|
// shouldn't happen unless the app crashed/was killed during an edit
|
||||||
|
Text("Edit")
|
||||||
|
.font(.body.bold())
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
}
|
||||||
|
|
||||||
|
if draft.contentWarningEnabled && !draft.contentWarning.isEmpty {
|
||||||
|
Text(draft.contentWarning)
|
||||||
|
.font(.body.bold())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(draft.text)
|
||||||
|
.font(.body)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
ForEach(draft.draftAttachments) { attachment in
|
||||||
|
AttachmentThumbnailView(attachment: attachment, thumbnailSize: CGSize(width: 50, height: 50))
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||||
|
.frame(height: 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let lastModified = draft.lastModified {
|
||||||
|
Text(lastModified.formatted(.abbreviatedTimeAgo))
|
||||||
|
.font(.body)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,8 +8,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
struct StatusContentTypeKey: MigratablePreferenceKey {
|
public struct StatusContentTypeKey: MigratablePreferenceKey {
|
||||||
static var defaultValue: StatusContentType { .plain }
|
public static var defaultValue: StatusContentType { .plain }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {
|
struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user