Drafts list
This commit is contained in:
parent
a7924feb76
commit
fbbcb0d07d
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
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 Pachyderm
|
||||
|
||||
struct StatusContentTypeKey: MigratablePreferenceKey {
|
||||
static var defaultValue: StatusContentType { .plain }
|
||||
public struct StatusContentTypeKey: MigratablePreferenceKey {
|
||||
public static var defaultValue: StatusContentType { .plain }
|
||||
}
|
||||
|
||||
struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {
|
||||
|
Loading…
x
Reference in New Issue
Block a user