From 99a12c58deee5115bd2b373ece36f95b674ffdc6 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 3 Feb 2025 12:14:49 -0500 Subject: [PATCH] Let new ComposeView stand alone from ComposeController --- .../Sources/ComposeUI/API/PostService.swift | 33 ++--- .../ComposeUI/ComposeMastodonContext.swift | 2 +- .../Sources/ComposeUI/ComposeUIConfig.swift | 4 + .../Controllers/ComposeController.swift | 16 +-- .../CoreData/DraftsPersistentContainer.swift | 2 +- .../Views/ComposeNavigationBarActions.swift | 54 +++++--- .../Sources/ComposeUI/Views/ComposeView.swift | 116 +++++++++++++++--- .../Sources/ComposeUI/Views/DraftEditor.swift | 13 +- .../ComposeUI/Views/EmojiTextField.swift | 16 +-- .../ComposeUI/Views/ReplyStatusView.swift | 16 ++- .../Pachyderm/Sources/Pachyderm/Client.swift | 17 +-- ShareExtension/ShareMastodonContext.swift | 13 +- Tusker/API/MastodonController.swift | 12 +- Tusker/Scenes/ComposeSceneDelegate.swift | 22 ++-- .../Compose/ComposeHostingController.swift | 108 +++++++++------- .../ComposeHostingViewController.swift | 14 --- .../Main/BaseMainTabBarViewController.swift | 5 +- Tusker/Screens/Main/Duckable+Root.swift | 9 +- 18 files changed, 285 insertions(+), 187 deletions(-) delete mode 100644 Tusker/Screens/Compose/ComposeHostingViewController.swift diff --git a/Packages/ComposeUI/Sources/ComposeUI/API/PostService.swift b/Packages/ComposeUI/Sources/ComposeUI/API/PostService.swift index 5084b502..cd88a914 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/API/PostService.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/API/PostService.swift @@ -12,20 +12,20 @@ import UniformTypeIdentifiers @MainActor final class PostService: ObservableObject { - private let mastodonController: ComposeMastodonContext + private let mastodonController: any ComposeMastodonContext private let config: ComposeUIConfig private let draft: Draft @Published var currentStep = 1 @Published private(set) var totalSteps = 2 - init(mastodonController: ComposeMastodonContext, config: ComposeUIConfig, draft: Draft) { + init(mastodonController: any ComposeMastodonContext, config: ComposeUIConfig, draft: Draft) { self.mastodonController = mastodonController self.config = config self.draft = draft } - func post() async throws { + func post() async throws(Error) { guard draft.hasContent || draft.editedStatusID != nil else { return } @@ -106,12 +106,12 @@ final class PostService: ObservableObject { let (status, _) = try await mastodonController.run(request) currentStep += 1 mastodonController.storeCreatedStatus(status) - } catch let error as Client.Error { + } catch { throw Error.posting(error) } } - private func uploadAttachments() async throws -> [String] { + private func uploadAttachments() async throws(Error) -> [String] { // 2 steps (request data, then upload) for each attachment self.totalSteps += 2 * draft.attachments.count @@ -131,7 +131,7 @@ final class PostService: ObservableObject { do { (data, utType) = try await getData(for: attachment) currentStep += 1 - } catch let error as DraftAttachment.ExportError { + } catch { throw Error.attachmentData(index: index, cause: error) } let uploaded = try await uploadAttachment(index: index, data: data, utType: utType, description: attachment.attachmentDescription) @@ -141,20 +141,21 @@ final class PostService: ObservableObject { return attachments } - private func getData(for attachment: DraftAttachment) async throws -> (Data, UTType) { - return try await withCheckedThrowingContinuation { continuation in + private func getData(for attachment: DraftAttachment) async throws(DraftAttachment.ExportError) -> (Data, UTType) { + let result = await withCheckedContinuation { continuation in attachment.getData(features: mastodonController.instanceFeatures) { result in - switch result { - case let .success(res): - continuation.resume(returning: res) - case let .failure(error): - continuation.resume(throwing: error) - } + continuation.resume(returning: result) } } + switch result { + case .success(let result): + return result + case .failure(let error): + throw error + } } - private func uploadAttachment(index: Int, data: Data, utType: UTType, description: String?) async throws -> Attachment { + private func uploadAttachment(index: Int, data: Data, utType: UTType, description: String?) async throws(Error) -> Attachment { guard let mimeType = utType.preferredMIMEType else { throw Error.attachmentMissingMimeType(index: index, type: utType) } @@ -166,7 +167,7 @@ final class PostService: ObservableObject { let req = Client.upload(attachment: formAttachment, description: description) do { return try await mastodonController.run(req).0 - } catch let error as Client.Error { + } catch { throw Error.attachmentUpload(index: index, cause: error) } } diff --git a/Packages/ComposeUI/Sources/ComposeUI/ComposeMastodonContext.swift b/Packages/ComposeUI/Sources/ComposeUI/ComposeMastodonContext.swift index 3a0952e9..2febcb26 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/ComposeMastodonContext.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/ComposeMastodonContext.swift @@ -15,7 +15,7 @@ public protocol ComposeMastodonContext { var accountInfo: UserAccountInfo? { get } var instanceFeatures: InstanceFeatures { get } - func run(_ request: Request) async throws -> (Result, Pagination?) + func run(_ request: Request) async throws(Client.Error) -> (Result, Pagination?) func getCustomEmojis() async -> [Emoji] diff --git a/Packages/ComposeUI/Sources/ComposeUI/ComposeUIConfig.swift b/Packages/ComposeUI/Sources/ComposeUI/ComposeUIConfig.swift index d0f9be7b..ee55e476 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/ComposeUIConfig.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/ComposeUIConfig.swift @@ -15,6 +15,7 @@ public struct ComposeUIConfig { // Config public var allowSwitchingDrafts = true public var textSelectionStartsAtBeginning = false + public var deleteDraftOnDisappear = true // Style public var backgroundColor = Color(uiColor: .systemBackground) @@ -33,6 +34,9 @@ public struct ComposeUIConfig { public var presentAssetPicker: ((@MainActor @escaping ([PHPickerResult]) -> Void) -> Void)? public var presentDrawing: ((PKDrawing, @escaping (PKDrawing) -> Void) -> Void)? public var userActivityForDraft: ((Draft) -> NSItemProvider?) = { _ in nil } + public var fetchAvatar: AvatarImageView.FetchAvatar = { _ in nil } + public var displayNameLabel: (any AccountProtocol, Font.TextStyle, CGFloat) -> AnyView = { _, _, _ in AnyView(EmptyView()) } + public var replyContentView: (any StatusProtocol, @escaping (CGFloat) -> Void) -> AnyView = { _, _ in AnyView(EmptyView()) } public init() { } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift index d782df5e..797faba3 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift @@ -132,17 +132,11 @@ public final class ComposeController: ViewController { } public var view: some View { - if Preferences.shared.hasFeatureFlag(.composeRewrite) { - ComposeUI.ComposeView(draft: draft, mastodonController: mastodonController) - .environment(\.currentAccount, currentAccount) - .environment(\.composeUIConfig, config) - } else { - ComposeView(poster: poster) - .environment(\.managedObjectContext, DraftsPersistentContainer.shared.viewContext) - .environmentObject(draft) - .environmentObject(mastodonController.instanceFeatures) - .environment(\.composeUIConfig, config) - } + ComposeView(poster: poster) + .environment(\.managedObjectContext, DraftsPersistentContainer.shared.viewContext) + .environmentObject(draft) + .environmentObject(mastodonController.instanceFeatures) + .environment(\.composeUIConfig, config) } @MainActor diff --git a/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftsPersistentContainer.swift b/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftsPersistentContainer.swift index c4a5a3ab..8d370478 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftsPersistentContainer.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftsPersistentContainer.swift @@ -12,7 +12,7 @@ import Pachyderm private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsPersistentContainer") -public class DraftsPersistentContainer: NSPersistentContainer { +public final class DraftsPersistentContainer: NSPersistentContainer { public static let shared = DraftsPersistentContainer() diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeNavigationBarActions.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeNavigationBarActions.swift index 33daac00..2544da3a 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeNavigationBarActions.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeNavigationBarActions.swift @@ -11,49 +11,67 @@ import InstanceFeatures struct ComposeNavigationBarActions: ToolbarContent { @ObservedObject var draft: Draft - // Prior to iOS 16, the toolbar content doesn't seem to have access - // to the environment from the containing view. - let controller: ComposeController @Binding var isShowingDrafts: Bool let isPosting: Bool + let cancel: (_ deleteDraft: Bool) -> Void + let postStatus: () async -> Void var body: some ToolbarContent { - ToolbarItem(placement: .cancellationAction) { ToolbarCancelButton(draft: draft) } + ToolbarItem(placement: .cancellationAction) { + ToolbarCancelButton(draft: draft, isPosting: isPosting, cancel: cancel) + } #if targetEnvironment(macCatalyst) - ToolbarItem(placement: .topBarTrailing) { DraftsButton(isShowingDrafts: $isShowingDrafts) } - ToolbarItem(placement: .confirmationAction) { PostButton(draft: draft, isPosting: isPosting) } + ToolbarItem(placement: .topBarTrailing) { + DraftsButton(isShowingDrafts: $isShowingDrafts) + } + ToolbarItem(placement: .confirmationAction) { + PostButton(draft: draft, isPosting: isPosting) + } #else - ToolbarItem(placement: .confirmationAction) { PostOrDraftsButton(draft: draft, isShowingDrafts: $isShowingDrafts, isPosting: isPosting) } + ToolbarItem(placement: .confirmationAction) { + PostOrDraftsButton(draft: draft, isShowingDrafts: $isShowingDrafts, isPosting: isPosting, postStatus: postStatus) + } #endif } } private struct ToolbarCancelButton: View { let draft: Draft + let isPosting: Bool + let cancel: (_ deleteDraft: Bool) -> Void @EnvironmentObject private var controller: ComposeController + @State private var isShowingSaveDraftSheet = false var body: some View { - Button(role: .cancel, action: controller.cancel) { + Button(role: .cancel, action: self.showConfirmationOrCancel) { Text("Cancel") } - .disabled(controller.isPosting) - .confirmationDialog("Are you sure?", isPresented: $controller.isShowingSaveDraftSheet) { + .disabled(isPosting) + .confirmationDialog("Are you sure?", isPresented: $isShowingSaveDraftSheet) { // edit drafts can't be saved if draft.editedStatusID == nil { - Button(action: { controller.cancel(deleteDraft: false) }) { + Button(action: { cancel(false) }) { Text("Save Draft") } - Button(role: .destructive, action: { controller.cancel(deleteDraft: true) }) { + Button(role: .destructive, action: { cancel(true) }) { Text("Delete Draft") } } else { - Button(role: .destructive, action: { controller.cancel(deleteDraft: true) }) { + Button(role: .destructive, action: { cancel(true) }) { Text("Cancel Edit") } } } } + + private func showConfirmationOrCancel() { + if draft.hasContent { + isShowingSaveDraftSheet = true + } else { + cancel(true) + } + } } #if !targetEnvironment(macCatalyst) @@ -61,11 +79,12 @@ private struct PostOrDraftsButton: View { @DraftObserving var draft: Draft @Binding var isShowingDrafts: Bool let isPosting: Bool + let postStatus: () async -> Void @Environment(\.composeUIConfig.allowSwitchingDrafts) private var allowSwitchingDrafts var body: some View { if !draftIsEmpty || draft.editedStatusID != nil || !allowSwitchingDrafts { - PostButton(draft: draft, isPosting: isPosting) + PostButton(draft: draft, isPosting: isPosting, postStatus: postStatus) } else { DraftsButton(isShowingDrafts: $isShowingDrafts) } @@ -80,12 +99,17 @@ private struct PostOrDraftsButton: View { private struct PostButton: View { @DraftObserving var draft: Draft let isPosting: Bool + let postStatus: () async -> Void @EnvironmentObject private var instanceFeatures: InstanceFeatures @Environment(\.composeUIConfig.requireAttachmentDescriptions) private var requireAttachmentDescriptions @EnvironmentObject private var controller: ComposeController var body: some View { - Button(action: controller.postStatus) { + Button { + Task { + await postStatus() + } + } label: { Text(draft.editedStatusID == nil ? "Post" : "Edit") } .keyboardShortcut(.return, modifiers: .command) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeView.swift index 24111b76..ef36c929 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeView.swift @@ -7,21 +7,60 @@ import SwiftUI import CoreData +import Pachyderm -struct ComposeView: View { - @ObservedObject var draft: Draft +public struct ComposeView: View { + @State var draft: Draft let mastodonController: any ComposeMastodonContext -// @State private var poster: PostService? = nil - @FocusState private var focusedField: FocusableField? - @EnvironmentObject private var controller: ComposeController - @State private var isShowingDrafts = false + let currentAccount: (any AccountProtocol)? + let config: ComposeUIConfig - // TODO: replace this with an @State owned by this view - var poster: PostService? { - controller.poster + public init( + initialDraft: Draft, + mastodonController: any ComposeMastodonContext, + currentAccount: (any AccountProtocol)?, + config: ComposeUIConfig + ) { + self.draft = initialDraft + self.mastodonController = mastodonController + self.currentAccount = currentAccount + self.config = config } - var body: some View { + public var body: some View { + ComposeViewBody(draft: draft, mastodonController: mastodonController, setDraft: self.setDraft) + .environment(\.composeUIConfig, config) + .environment(\.currentAccount, currentAccount) + } + + private func setDraft(_ draft: Draft) { + let oldDraft = self.draft + self.draft = draft + + if oldDraft.hasContent { + oldDraft.lastModified = Date() + } else { + DraftsPersistentContainer.shared.viewContext.delete(oldDraft) + } + DraftsPersistentContainer.shared.save() + } +} + +// TODO: see if this can be broken up further +private struct ComposeViewBody: View { + @ObservedObject var draft: Draft + let mastodonController: any ComposeMastodonContext + let setDraft: (Draft) -> Void + @State private var poster: PostService? + @State private var postError: PostService.Error? + @State private var didPostSuccessfully = false + @FocusState private var focusedField: FocusableField? + @State private var isShowingDrafts = false + @State private var isDismissing = false + @State private var userConfirmedDelete = false + @Environment(\.composeUIConfig) private var config + + public var body: some View { navigation .environmentObject(mastodonController.instanceFeatures) .sheet(isPresented: $isShowingDrafts) { @@ -29,9 +68,13 @@ struct ComposeView: View { currentDraft: draft, isShowingDrafts: $isShowingDrafts, accountInfo: mastodonController.accountInfo!, - selectDraft: self.selectDraft + selectDraft: { + self.setDraft($0) + self.isShowingDrafts = false + } ) } + .onDisappear(perform: self.deleteOrSaveDraft) } @ViewBuilder @@ -86,7 +129,7 @@ struct ComposeView: View { .modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController)) .navigationBarTitleDisplayMode(.inline) .toolbar { - ComposeNavigationBarActions(draft: draft, controller: controller, isShowingDrafts: $isShowingDrafts, isPosting: poster != nil) + ComposeNavigationBarActions(draft: draft, isShowingDrafts: $isShowingDrafts, isPosting: poster != nil, cancel: self.cancel(deleteDraft:), postStatus: self.postStatus) #if os(visionOS) ToolbarItem(placement: .bottomOrnament) { toolbarView @@ -113,9 +156,47 @@ struct ComposeView: View { AttachmentsSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: itemProviders) } - private func selectDraft(_ draft: Draft) { - controller.selectDraft(draft) - isShowingDrafts = false + private func deleteOrSaveDraft() { + if isDismissing, + !draft.hasContent || didPostSuccessfully || userConfirmedDelete { + DraftsPersistentContainer.shared.viewContext.delete(draft) + } else { + draft.lastModified = Date() + } + DraftsPersistentContainer.shared.save() + } + + private func cancel(deleteDraft: Bool) { + isDismissing = true + userConfirmedDelete = deleteDraft + config.dismiss(.cancel) + } + + private func postStatus() async { + guard poster == nil, + draft.editedStatusID != nil || draft.hasContent else { + return + } + + let poster = PostService(mastodonController: mastodonController, config: config, draft: draft) + self.poster = poster + + do { + try await poster.post() + + isDismissing = true + didPostSuccessfully = true + + // wait .25 seconds so the user can see the progress bar has completed + try? await Task.sleep(nanoseconds: 250_000_000) + + // don't unset the poster, so the ui remains disabled while dismissing + + config.dismiss(.post) + } catch { + self.postError = error + self.poster = nil + } } } @@ -135,9 +216,10 @@ private struct NavigationTitleModifier: ViewModifier { } func body(content: Content) -> some View { + let title = navigationTitle content - .navigationTitle(navigationTitle) - .preference(key: NavigationTitlePreferenceKey.self, value: navigationTitle) + .navigationTitle(title) + .preference(key: NavigationTitlePreferenceKey.self, value: title) } } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/DraftEditor.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/DraftEditor.swift index b58411ce..6529da3c 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/DraftEditor.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/DraftEditor.swift @@ -8,6 +8,7 @@ import SwiftUI import Pachyderm import TuskerComponents +import TuskerPreferences struct DraftEditor: View { @ObservedObject var draft: Draft @@ -55,15 +56,15 @@ struct DraftEditor: View { private struct AvatarView: View { let account: (any AccountProtocol)? - @Environment(\.composeUIConfig.avatarStyle) private var avatarStyle - @EnvironmentObject private var controller: ComposeController + @PreferenceObserving(\.$avatarStyle) private var avatarStyle + @Environment(\.composeUIConfig.fetchAvatar) private var fetchAvatar var body: some View { AvatarImageView( url: account?.avatar, size: 50, - style: avatarStyle, - fetchAvatar: controller.fetchAvatar + style: avatarStyle == .circle ? .circle : .roundRect, + fetchAvatar: fetchAvatar ) .accessibilityHidden(true) } @@ -71,11 +72,11 @@ private struct AvatarView: View { private struct AccountNameView: View { let account: any AccountProtocol - @EnvironmentObject private var controller: ComposeController + @Environment(\.composeUIConfig.displayNameLabel) private var displayNameLabel var body: some View { HStack(spacing: 4) { - controller.displayNameLabel(account, .body, 16) + displayNameLabel(account, .body, 16) .lineLimit(1) Text(verbatim: "@\(account.acct)") diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift index fc710ced..0a919726 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/EmojiTextField.swift @@ -10,7 +10,7 @@ import SwiftUI struct EmojiTextField: UIViewRepresentable { typealias UIViewType = UITextField - @EnvironmentObject private var controller: ComposeController + @Environment(\.composeUIConfig.fillColor) private var fillColor @Environment(\.colorScheme) private var colorScheme @Environment(\.composeInputBox) private var inputBox @@ -64,7 +64,7 @@ struct EmojiTextField: UIViewRepresentable { context.coordinator.focusNextView = focusNextView #if !os(visionOS) - uiView.backgroundColor = colorScheme == .dark ? UIColor(controller.config.fillColor) : .secondarySystemBackground + uiView.backgroundColor = colorScheme == .dark ? UIColor(fillColor) : .secondarySystemBackground #endif if becomeFirstResponder?.wrappedValue == true { @@ -76,7 +76,7 @@ struct EmojiTextField: UIViewRepresentable { } func makeCoordinator() -> Coordinator { - let coordinator = Coordinator(controller: controller, text: $text, focusNextView: focusNextView) + let coordinator = Coordinator(text: $text, focusNextView: focusNextView) DispatchQueue.main.async { inputBox.wrappedValue = coordinator } @@ -84,7 +84,6 @@ struct EmojiTextField: UIViewRepresentable { } class Coordinator: NSObject, UITextFieldDelegate, ComposeInput { - let controller: ComposeController var text: Binding var focusNextView: Binding? var maxLength: Int? @@ -94,8 +93,7 @@ struct EmojiTextField: UIViewRepresentable { weak var textField: UITextField? - init(controller: ComposeController, text: Binding, focusNextView: Binding?, maxLength: Int? = nil) { - self.controller = controller + init(text: Binding, focusNextView: Binding?, maxLength: Int? = nil) { self.text = text self.focusNextView = focusNextView self.maxLength = maxLength @@ -118,16 +116,10 @@ struct EmojiTextField: UIViewRepresentable { } func textFieldDidBeginEditing(_ textField: UITextField) { - DispatchQueue.main.async { - self.controller.currentInput = self - } autocompleteState = textField.updateAutocompleteState(permittedModes: .emojis) } func textFieldDidEndEditing(_ textField: UITextField) { - DispatchQueue.main.async { - self.controller.currentInput = nil - } autocompleteState = textField.updateAutocompleteState(permittedModes: .emojis) } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift index eb46e382..c81c19a3 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift @@ -8,13 +8,17 @@ import SwiftUI import Pachyderm import TuskerComponents +import TuskerPreferences struct ReplyStatusView: View { let status: any StatusProtocol let rowTopInset: CGFloat let globalFrameOutsideList: CGRect - @EnvironmentObject private var controller: ComposeController + @PreferenceObserving(\.$avatarStyle) private var avatarStyle + @Environment(\.composeUIConfig.displayNameLabel) private var displayNameLabel + @Environment(\.composeUIConfig.replyContentView) private var replyContentView + @Environment(\.composeUIConfig.fetchAvatar) private var fetchAvatar @State private var displayNameHeight: CGFloat? @State private var contentHeight: CGFloat? @@ -27,7 +31,7 @@ struct ReplyStatusView: View { VStack(alignment: .leading, spacing: 0) { HStack { - controller.displayNameLabel(status.account, .body, 17) + displayNameLabel(status.account, .body, 17) .lineLimit(1) .layoutPriority(1) @@ -46,7 +50,7 @@ struct ReplyStatusView: View { } }) - controller.replyContentView(status) { newHeight in + replyContentView(status) { newHeight in // otherwise, with long in-reply-to statuses, the main content text view position seems not to update // and it ends up partially behind the header DispatchQueue.main.async { @@ -80,8 +84,8 @@ struct ReplyStatusView: View { AvatarImageView( url: status.account.avatar, size: 50, - style: controller.config.avatarStyle, - fetchAvatar: controller.fetchAvatar + style: avatarStyle == .circle ? .circle : .roundRect, + fetchAvatar: fetchAvatar ) } .frame(width: 50, height: 50) @@ -90,7 +94,7 @@ struct ReplyStatusView: View { } -private struct DisplayNameHeightPrefKey: PreferenceKey { +private struct DisplayNameHeightPrefKey: SwiftUI.PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() diff --git a/Packages/Pachyderm/Sources/Pachyderm/Client.swift b/Packages/Pachyderm/Sources/Pachyderm/Client.swift index 6065e841..558c481b 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Client.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Client.swift @@ -118,17 +118,18 @@ public struct Client: Sendable { } @discardableResult - public func run(_ request: Request) async throws -> (Result, Pagination?) { - return try await withCheckedThrowingContinuation { continuation in + public func run(_ request: Request) async throws(Error) -> (Result, Pagination?) { + let response = await withCheckedContinuation { continuation in run(request) { response in - switch response { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let result, let pagination): - continuation.resume(returning: (result, pagination)) - } + continuation.resume(returning: response) } } + switch response { + case .failure(let error): + throw error + case .success(let result, let pagination): + return (result, pagination) + } } func createURLRequest(request: Request) -> URLRequest? { diff --git a/ShareExtension/ShareMastodonContext.swift b/ShareExtension/ShareMastodonContext.swift index 29c28cfe..6b7a7d50 100644 --- a/ShareExtension/ShareMastodonContext.swift +++ b/ShareExtension/ShareMastodonContext.swift @@ -45,17 +45,8 @@ final class ShareMastodonContext: ComposeMastodonContext, ObservableObject, Send // MARK: ComposeMastodonContext - func run(_ request: Request) async throws -> (Result, Pagination?) { - return try await withCheckedThrowingContinuation({ continuation in - client.run(request) { response in - switch response { - case .success(let result, let pagination): - continuation.resume(returning: (result, pagination)) - case .failure(let error): - continuation.resume(throwing: error) - } - } - }) + func run(_ request: Request) async throws(Client.Error) -> (Result, Pagination?) { + return try await client.run(request) } @MainActor diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index f587b90a..84d40ed3 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -152,6 +152,7 @@ final class MastodonController: ObservableObject, Sendable { return client.run(request, completion: completion) } + // TODO: remove this in favor of just using the typed throws run(_:) everywhere func runResponse(_ request: Request) async -> Response { let response = await withCheckedContinuation({ continuation in client.run(request) { response in @@ -161,15 +162,8 @@ final class MastodonController: ObservableObject, Sendable { return response } - func run(_ request: Request) async throws -> (Result, Pagination?) { - let response = await runResponse(request) - try Task.checkCancellation() - switch response { - case .failure(let error): - throw error - case .success(let result, let pagination): - return (result, pagination) - } + func run(_ request: Request) async throws(Client.Error) -> (Result, Pagination?) { + return try await client.run(request) } /// - Returns: A tuple of client ID and client secret. diff --git a/Tusker/Scenes/ComposeSceneDelegate.swift b/Tusker/Scenes/ComposeSceneDelegate.swift index b86d95e6..efb78920 100644 --- a/Tusker/Scenes/ComposeSceneDelegate.swift +++ b/Tusker/Scenes/ComposeSceneDelegate.swift @@ -68,10 +68,11 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg window!.rootViewController = composeVC window!.makeKeyAndVisible() - updateTitle(draft: composeVC.controller.draft) - composeVC.controller.$draft - .sink { [unowned self] in self.updateTitle(draft: $0) } - .store(in: &cancellables) + // TODO: get the title out here somehow +// updateTitle(draft: composeVC.controller.draft) +// composeVC.controller.$draft +// .sink { [unowned self] in self.updateTitle(draft: $0) } +// .store(in: &cancellables) NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil) themePrefChanged() @@ -80,12 +81,13 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg func sceneWillResignActive(_ scene: UIScene) { DraftsPersistentContainer.shared.save() - if let window = window, - let nav = window.rootViewController as? UINavigationController, - let compose = nav.topViewController as? ComposeHostingController, - !compose.controller.didPostSuccessfully { - scene.userActivity = UserActivityManager.editDraftActivity(id: compose.controller.draft.id, accountID: scene.session.mastodonController!.accountInfo!.id) - } + // TODO: update user activity +// if let window = window, +// let nav = window.rootViewController as? UINavigationController, +// let compose = nav.topViewController as? ComposeHostingController, +// !compose.controller.didPostSuccessfully { +// scene.userActivity = UserActivityManager.editDraftActivity(id: compose.controller.draft.id, accountID: scene.session.mastodonController!.accountInfo!.id) +// } } func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { diff --git a/Tusker/Screens/Compose/ComposeHostingController.swift b/Tusker/Screens/Compose/ComposeHostingController.swift index ef5e276b..55402403 100644 --- a/Tusker/Screens/Compose/ComposeHostingController.swift +++ b/Tusker/Screens/Compose/ComposeHostingController.swift @@ -27,7 +27,6 @@ class ComposeHostingController: UIHostingController() @@ -35,28 +34,17 @@ class ComposeHostingController: UIHostingController Void)? private var drawingCompletion: ((PKDrawing) -> Void)? + @ObservableObjectBox private var config: ComposeUIConfig + @ObservableObjectBox private var currentAccount: AccountMO? + init(draft: Draft?, mastodonController: MastodonController) { let draft = draft ?? mastodonController.createDraft() - self.controller = ComposeController( - draft: draft, - config: ComposeUIConfig(), - mastodonController: mastodonController, - fetchAvatar: { @MainActor in await ImageCache.avatars.get($0).1 }, - fetchAttachment: { @MainActor in await ImageCache.attachments.get($0).1 }, - fetchStatus: { mastodonController.persistentContainer.status(for: $0) }, - displayNameLabel: { AnyView(AccountDisplayNameView(account: $0, textStyle: $1, emojiSize: $2)) }, - replyContentView: { AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) }, - emojiImageView: { AnyView(CustomEmojiImageView(emoji: $0)) } - ) - - if let account = mastodonController.account { - controller.currentAccount = account - } - self.mastodonController = mastodonController + self.config = ComposeUIConfig() + self.currentAccount = mastodonController.account - super.init(rootView: View(mastodonController: mastodonController, controller: controller)) + super.init(rootView: View(initialDraft: draft, mastodonController: mastodonController, config: _config, currentAccount: _currentAccount)) self.updateConfig() @@ -65,11 +53,12 @@ class ComposeHostingController: UIHostingController Bool { - return controller.canPaste(itemProviders: itemProviders) +// return controller.canPaste(itemProviders: itemProviders) + return false + // TODO: pasting } override func paste(itemProviders: [NSItemProvider]) { - controller.paste(itemProviders: itemProviders) +// controller.paste(itemProviders: itemProviders) } private func dismiss(mode: DismissMode) { @@ -148,43 +142,69 @@ class ComposeHostingController: UIHostingController, currentAccount: ObservableObjectBox) { + self.initialDraft = initialDraft + self.mastodonController = mastodonController + self._config = ObservedObject(wrappedValue: config) + self._currentAccount = ObservedObject(wrappedValue: currentAccount) + } var body: some SwiftUI.View { - ControllerView(controller: { controller }) - .task { - if controller.currentAccount == nil, - let account = try? await mastodonController.getOwnAccount() { - controller.currentAccount = account - } + ComposeView( + initialDraft: initialDraft, + mastodonController: mastodonController, + currentAccount: currentAccount, + config: config + ) + .task { + if currentAccount == nil, + let account = try? await mastodonController.getOwnAccount() { + currentAccount = account } + } } } } +@MainActor +@propertyWrapper +private final class ObservableObjectBox: ObservableObject { + @Published var wrappedValue: T + init(wrappedValue: T) { + self.wrappedValue = wrappedValue + } +} + #if canImport(Duckable) extension ComposeHostingController: DuckableViewController { func duckableViewControllerShouldDuck() -> DuckAttemptAction { - if controller.isPosting { - return .block - } else if controller.draft.hasContent { - return .duck - } else { - return .dismiss - } +// if controller.isPosting { +// return .block +// } else if controller.draft.hasContent { +// return .duck +// } else { +// return .dismiss +// } + // TODO: ducking + return .dismiss } func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) { - controller.deleteDraftOnDisappear = false + config.deleteDraftOnDisappear = false - withAnimation(.linear(duration: duration).delay(delay)) { - controller.showToolbar = false - } + // TODO: figure out if this is still necessary +// withAnimation(.linear(duration: duration).delay(delay)) { +// controller.showToolbar = false +// } } func duckableViewControllerDidFinishAnimatingDuck() { - controller.showToolbar = true +// controller.showToolbar = true } } #endif diff --git a/Tusker/Screens/Compose/ComposeHostingViewController.swift b/Tusker/Screens/Compose/ComposeHostingViewController.swift deleted file mode 100644 index 1ac4e184..00000000 --- a/Tusker/Screens/Compose/ComposeHostingViewController.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// ComposeHostingController.swift -// Tusker -// -// Created by Shadowfacts on 8/18/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import SwiftUI -import UIKit - -class ComposeHostingController: UIHostingController { - -} diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift index f3eb8313..006b8a8e 100644 --- a/Tusker/Screens/Main/BaseMainTabBarViewController.swift +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -178,8 +178,9 @@ extension BaseMainTabBarViewController: StateRestorableViewController { var activity: NSUserActivity? if let presentedNav = presentedViewController as? UINavigationController, let compose = presentedNav.viewControllers.first as? ComposeHostingController { - let draft = compose.controller.draft - activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID) + // TODO: this +// let draft = compose.controller.draft +// activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID) } else if let vc = (selectedViewController as? any NavigationControllerProtocol)?.topViewController as? StateRestorableViewController { activity = vc.stateRestorationActivity() } diff --git a/Tusker/Screens/Main/Duckable+Root.swift b/Tusker/Screens/Main/Duckable+Root.swift index 5ab80ca5..1dd46dda 100644 --- a/Tusker/Screens/Main/Duckable+Root.swift +++ b/Tusker/Screens/Main/Duckable+Root.swift @@ -16,10 +16,11 @@ import ComposeUI extension DuckableContainerViewController: AccountSwitchableViewController { func stateRestorationActivity() -> NSUserActivity? { var activity = (child as? TuskerRootViewController)?.stateRestorationActivity() - if let compose = duckedViewController as? ComposeHostingController, - compose.controller.draft.hasContent { - activity = UserActivityManager.addDuckedDraft(to: activity, draft: compose.controller.draft) - } + // TODO: this +// if let compose = duckedViewController as? ComposeHostingController, +// compose.controller.draft.hasContent { +// activity = UserActivityManager.addDuckedDraft(to: activity, draft: compose.controller.draft) +// } return activity }