Drafts list

This commit is contained in:
Shadowfacts 2025-01-27 15:42:26 -05:00
parent a7924feb76
commit fbbcb0d07d
6 changed files with 204 additions and 14 deletions

View File

@ -246,7 +246,9 @@ public final class ComposeController: ViewController {
let oldDraft = self.draft
self.draft = newDraft
if !oldDraft.hasContent {
if oldDraft.hasContent {
oldDraft.lastModified = Date()
} else if oldDraft.hasContent {
DraftsPersistentContainer.shared.viewContext.delete(oldDraft)
}
DraftsPersistentContainer.shared.save()
@ -256,6 +258,8 @@ public final class ComposeController: ViewController {
isDisappearing = true
if deleteDraftOnDisappear && (!draft.hasContent || didPostSuccessfully || userConfirmedDelete) {
DraftsPersistentContainer.shared.viewContext.delete(draft)
} else {
draft.lastModified = Date()
}
DraftsPersistentContainer.shared.save()
}

View File

@ -13,9 +13,14 @@ import Photos
struct AttachmentThumbnailView: View {
let attachment: DraftAttachment
var contentMode: ContentMode = .fit
var thumbnailSize: CGSize?
var body: some View {
AttachmentThumbnailViewContent(attachment: attachment, contentMode: contentMode)
AttachmentThumbnailViewContent(
attachment: attachment,
contentMode: contentMode,
thumbnailSize: thumbnailSize
)
.id(attachment.id)
}
@ -24,6 +29,7 @@ struct AttachmentThumbnailView: View {
private struct AttachmentThumbnailViewContent: View {
var attachment: DraftAttachment
var contentMode: ContentMode = .fit
var thumbnailSize: CGSize?
@State private var mode: Mode = .empty
@EnvironmentObject private var composeController: ComposeController
@ -78,7 +84,7 @@ private struct AttachmentThumbnailViewContent: View {
}
}
} 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
if let image {
self.mode = .image(image)
@ -99,7 +105,7 @@ private struct AttachmentThumbnailViewContent: View {
if let image = UIImage(data: data),
// using prepareThumbnail on images from PHPicker results in extremely high memory usage,
// crashing share extension. see FB12186346
let prepared = await image.byPreparingForDisplay() {
let prepared = await thumbnailImage(image) {
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 {
let asset = AVURLAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)

View File

@ -13,10 +13,19 @@ struct ComposeView: View {
@State private var poster: PostService? = nil
@FocusState private var focusedField: FocusableField?
@EnvironmentObject private var controller: ComposeController
@State private var isShowingDrafts = false
var body: some View {
navigation
.environmentObject(mastodonController.instanceFeatures)
.sheet(isPresented: $isShowingDrafts) {
DraftsView(
currentDraft: draft,
isShowingDrafts: $isShowingDrafts,
accountInfo: mastodonController.accountInfo!,
selectDraft: self.selectDraft
)
}
}
@ViewBuilder
@ -71,7 +80,7 @@ struct ComposeView: View {
.modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarActions(draft: draft, controller: controller)
ToolbarActions(draft: draft, controller: controller, isShowingDrafts: $isShowingDrafts)
#if os(visionOS)
ToolbarItem(placement: .bottomOrnament) {
toolbarView
@ -97,6 +106,11 @@ struct ComposeView: View {
private func addAttachments(_ itemProviders: [NSItemProvider]) {
AttachmentsSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: itemProviders)
}
private func selectDraft(_ draft: Draft) {
controller.selectDraft(draft)
isShowingDrafts = false
}
}
private struct NavigationTitleModifier: ViewModifier {
@ -132,8 +146,9 @@ public struct NavigationTitlePreferenceKey: PreferenceKey {
private struct ToolbarActions: ToolbarContent {
@ObservedObject var draft: Draft
// 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
@Binding var isShowingDrafts: Bool
var body: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) { ToolbarCancelButton(draft: draft) }
@ -147,7 +162,9 @@ private struct ToolbarActions: ToolbarContent {
}
private var draftsButton: some View {
Button(action: controller.showDrafts) {
Button {
isShowingDrafts = true
} label: {
Text("Drafts")
}
}

View File

@ -67,9 +67,6 @@ private struct LanguageButton: View {
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $hasChanged)
.buttonStyle(LanguageButtonStyle())
.onReceive(NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification), perform: currentInputModeChanged)
.onChange(of: draft.id) { _ in
hasChanged = false
}
}
}

View 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)
}
}
}
}

View File

@ -8,8 +8,8 @@
import Foundation
import Pachyderm
struct StatusContentTypeKey: MigratablePreferenceKey {
static var defaultValue: StatusContentType { .plain }
public struct StatusContentTypeKey: MigratablePreferenceKey {
public static var defaultValue: StatusContentType { .plain }
}
struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {