From 6c371f868fb8e2c1ec7d242bdd23925057029aa9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 18 Apr 2023 21:55:14 -0400 Subject: [PATCH] Initial share extension implementation --- .../Sources/ComposeUI/ComposeUIConfig.swift | 8 + .../Controllers/ComposeController.swift | 2 +- .../ComposeUI/Views/MainTextView.swift | 14 +- .../Base.lproj/MainInterface.storyboard | 24 ++ ShareExtension/Info.plist | 18 ++ ShareExtension/ShareExtension.entitlements | 14 ++ ShareExtension/ShareHostingController.swift | 154 ++++++++++++ ShareExtension/ShareMastodonContext.swift | 92 +++++++ ShareExtension/ShareViewController.swift | 124 ++++++++++ Tusker.xcodeproj/project.pbxproj | 231 +++++++++++++++++- 10 files changed, 678 insertions(+), 3 deletions(-) create mode 100644 ShareExtension/Base.lproj/MainInterface.storyboard create mode 100644 ShareExtension/Info.plist create mode 100644 ShareExtension/ShareExtension.entitlements create mode 100644 ShareExtension/ShareHostingController.swift create mode 100644 ShareExtension/ShareMastodonContext.swift create mode 100644 ShareExtension/ShareViewController.swift diff --git a/Packages/ComposeUI/Sources/ComposeUI/ComposeUIConfig.swift b/Packages/ComposeUI/Sources/ComposeUI/ComposeUIConfig.swift index 0cc3694a..fe51bb1f 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/ComposeUIConfig.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/ComposeUIConfig.swift @@ -12,16 +12,24 @@ import PencilKit import TuskerComponents public struct ComposeUIConfig { + // Config + public var allowSwitchingDrafts = true + public var textSelectionStartsAtBeginning = false + + // Style public var backgroundColor = Color(uiColor: .systemBackground) public var groupedBackgroundColor = Color(uiColor: .systemGroupedBackground) public var groupedCellBackgroundColor = Color(uiColor: .systemBackground) public var fillColor = Color(uiColor: .systemFill) public var avatarStyle = AvatarImageView.Style.roundRect + + // Preferences public var useTwitterKeyboard = false public var contentType = StatusContentType.plain public var automaticallySaveDrafts = false public var requireAttachmentDescriptions = false + // Host callbacks public var dismiss: @MainActor (DismissMode) -> Void = { _ in } public var presentAssetPicker: ((@MainActor @escaping ([PHPickerResult]) -> Void) -> Void)? public var presentDrawing: ((PKDrawing, @escaping (PKDrawing) -> Void) -> Void)? diff --git a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift index df2db563..365417c6 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Controllers/ComposeController.swift @@ -351,7 +351,7 @@ public final class ComposeController: ViewController { @ViewBuilder private var postButton: some View { - if draft.hasContent { + if draft.hasContent || !controller.config.allowSwitchingDrafts { Button(action: controller.postStatus) { Text("Post") } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift index 079584c6..f44c3d59 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/MainTextView.swift @@ -15,6 +15,7 @@ struct MainTextView: View { @State private var hasFirstAppeared = false @State private var height: CGFloat? + @State private var updateSelection: ((UITextView) -> Void)? private let minHeight: CGFloat = 150 private var effectiveHeight: CGFloat { height ?? minHeight } @@ -34,7 +35,7 @@ struct MainTextView: View { .accessibilityHidden(true) } - MainWrappedTextViewRepresentable(text: $draft.text, becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder, textDidChange: textDidChange) + MainWrappedTextViewRepresentable(text: $draft.text, becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder, updateSelection: $updateSelection, textDidChange: textDidChange) } .frame(height: effectiveHeight) .onAppear(perform: becomeFirstResponderOnFirstAppearance) @@ -44,6 +45,11 @@ struct MainTextView: View { if !hasFirstAppeared { hasFirstAppeared = true controller.mainComposeTextViewBecomeFirstResponder = true + if config.textSelectionStartsAtBeginning { + updateSelection = { textView in + textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument) + } + } } } @@ -57,6 +63,7 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable { @Binding var text: String @Binding var becomeFirstResponder: Bool + @Binding var updateSelection: ((UITextView) -> Void)? let textDidChange: (UITextView) -> Void @EnvironmentObject private var controller: ComposeController @@ -85,6 +92,11 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable { context.coordinator.text = $text + if let updateSelection { + updateSelection(uiView) + self.updateSelection = nil + } + // wait until the next runloop iteration so that SwiftUI view updates have finished and // the text view knows its new content size DispatchQueue.main.async { diff --git a/ShareExtension/Base.lproj/MainInterface.storyboard b/ShareExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..286a5089 --- /dev/null +++ b/ShareExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist new file mode 100644 index 00000000..4b1f7e70 --- /dev/null +++ b/ShareExtension/Info.plist @@ -0,0 +1,18 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + TRUEPREDICATE + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + diff --git a/ShareExtension/ShareExtension.entitlements b/ShareExtension/ShareExtension.entitlements new file mode 100644 index 00000000..d5e9f9a9 --- /dev/null +++ b/ShareExtension/ShareExtension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.space.vaccor.Tusker + + com.apple.security.network.client + + + diff --git a/ShareExtension/ShareHostingController.swift b/ShareExtension/ShareHostingController.swift new file mode 100644 index 00000000..11836d2b --- /dev/null +++ b/ShareExtension/ShareHostingController.swift @@ -0,0 +1,154 @@ +// +// ShareHostingController.swift +// ShareExtension +// +// Created by Shadowfacts on 4/17/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import SwiftUI +import ComposeUI +import TuskerComponents +import WebURLFoundationExtras +import Combine +import TuskerPreferences + +class ShareHostingController: UIHostingController { + private static func fetchAvatar(_ url: URL) async -> UIImage? { + guard let (data, _) = try? await URLSession.shared.data(from: url), + let image = UIImage(data: data) else { + return nil + } + return await image.byPreparingThumbnail(ofSize: CGSize(width: 50, height: 50)) ?? image + } + + private let controller: ComposeController + + private var cancellables = Set() + + init(draft: Draft, mastodonContext: ShareMastodonContext) { + controller = ComposeController( + draft: draft, + config: ComposeUIConfig(), + mastodonController: mastodonContext, + fetchAvatar: Self.fetchAvatar, + fetchStatus: { _ in fatalError("replies aren't allowed in share sheet") }, + displayNameLabel: { account, style, _ in AnyView(Text(account.displayName).font(.system(style))) }, + replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") }, + emojiImageView: { + AnyView(AsyncImage(url: URL($0.url)!) { + $0 + .resizable() + .aspectRatio(contentMode: .fit) + } placeholder: { + Image(systemName: "smiley.fill") + }) + } + ) + super.init(rootView: View(controller: controller)) + + updateConfig() + + mastodonContext.$ownAccount + .sink { [unowned self] in self.controller.currentAccount = $0 } + .store(in: &cancellables) + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + + private func updateConfig() { + var config = ComposeUIConfig() + config.allowSwitchingDrafts = false + config.textSelectionStartsAtBeginning = true + // note: in the share sheet, we ignore this preference + config.automaticallySaveDrafts = false + + config.backgroundColor = Color(uiColor: .appBackground) + config.groupedBackgroundColor = Color(uiColor: .appGroupedBackground) + config.groupedCellBackgroundColor = Color(uiColor: .appGroupedCellBackground) + config.fillColor = Color(uiColor: .appFill) + switch Preferences.shared.avatarStyle { + case .roundRect: + config.avatarStyle = .roundRect + case .circle: + config.avatarStyle = .circle + } + config.useTwitterKeyboard = Preferences.shared.useTwitterKeyboard + config.contentType = Preferences.shared.statusContentType + config.requireAttachmentDescriptions = Preferences.shared.requireAttachmentDescriptions + + config.dismiss = { [unowned self] in self.dismiss(mode: $0) } + + controller.config = config + } + + private func dismiss(mode: DismissMode) { + guard let extensionContext else { return } + switch mode { + case .cancel: + extensionContext.cancelRequest(withError: Error.cancelled) + case .post: + extensionContext.completeRequest(returningItems: nil) + } + } + + struct View: SwiftUI.View { + let controller: ComposeController + + var body: some SwiftUI.View { + ControllerView(controller: { controller }) + } + } + + enum Error: Swift.Error { + case cancelled + } +} + +// todo: shouldn't just copy this from the main Colors.swift +extension UIColor { + static let appBackground = UIColor { traitCollection in + if case .dark = traitCollection.userInterfaceStyle, + !Preferences.shared.pureBlackDarkMode { + return UIColor(hue: 230/360, saturation: 23/100, brightness: 10/100, alpha: 1) + } else { + return .systemBackground + } + } + + static let appGroupedBackground = UIColor { traitCollection in + if case .dark = traitCollection.userInterfaceStyle, + !Preferences.shared.pureBlackDarkMode { + if traitCollection.userInterfaceLevel == .elevated { + return UIColor(hue: 230/360, saturation: 23/100, brightness: 10/100, alpha: 1) + } else { + return UIColor(hue: 230/360, saturation: 23/100, brightness: 5/100, alpha: 1) + } + } else { + return .systemGroupedBackground + } + } + + static let appGroupedCellBackground = UIColor { traitCollection in + if case .dark = traitCollection.userInterfaceStyle { + if Preferences.shared.pureBlackDarkMode { + return .secondarySystemBackground + } else { + return .appFill + } + } else { + return .systemBackground + } + } + + static let appFill = UIColor { traitCollection in + if case .dark = traitCollection.userInterfaceStyle, + !Preferences.shared.pureBlackDarkMode { + return UIColor(hue: 230/360, saturation: 20/100, brightness: 17/100, alpha: 1) + } else { + return .systemFill + } + } +} diff --git a/ShareExtension/ShareMastodonContext.swift b/ShareExtension/ShareMastodonContext.swift new file mode 100644 index 00000000..a4adbc7b --- /dev/null +++ b/ShareExtension/ShareMastodonContext.swift @@ -0,0 +1,92 @@ +// +// ShareMastodonContext.swift +// ShareExtension +// +// Created by Shadowfacts on 4/17/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import Pachyderm +import ComposeUI +import UserAccounts +import InstanceFeatures +import Combine + +class ShareMastodonContext: ComposeMastodonContext, ObservableObject { + let accountInfo: UserAccountInfo? + let client: Client + let instanceFeatures: InstanceFeatures + + @MainActor + private var customEmojis: [Emoji]? + + @Published var ownAccount: Account? + + init(accountInfo: UserAccountInfo) { + self.accountInfo = accountInfo + self.client = Client(baseURL: accountInfo.instanceURL, accessToken: accountInfo.accessToken) + self.instanceFeatures = InstanceFeatures() + + Task { @MainActor in + async let instance = try? await run(Client.getInstance()).0 + async let nodeInfo: NodeInfo? = await withCheckedContinuation({ continuation in + self.client.nodeInfo { response in + switch response { + case .success(let nodeInfo, _): + continuation.resume(returning: nodeInfo) + case .failure(_): + continuation.resume(returning: nil) + } + } + }) + guard let instance = await instance else { return } + self.instanceFeatures.update(instance: instance, nodeInfo: await nodeInfo) + } + + Task { @MainActor in + if let account = try? await run(Client.getSelfAccount()).0 { + self.ownAccount = account + } + } + } + + // MARK: CompmoseMastodonContext + + 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) + } + } + }) + } + + @MainActor + func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) { + if let customEmojis { + completion(customEmojis) + } else { + Task.detached { @MainActor in + let emojis = (try? await self.run(Client.getCustomEmoji()).0) ?? [] + self.customEmojis = emojis + completion(emojis) + } + } + } + + func searchCachedAccounts(query: String) -> [AccountProtocol] { + return [] + } + + func cachedRelationship(for accountID: String) -> RelationshipProtocol? { + return nil + } + + func searchCachedHashtags(query: String) -> [Hashtag] { + return [] + } +} diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift new file mode 100644 index 00000000..4270ad67 --- /dev/null +++ b/ShareExtension/ShareViewController.swift @@ -0,0 +1,124 @@ +// +// ShareViewController.swift +// ShareExtension +// +// Created by Shadowfacts on 4/17/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import SwiftUI +import UserAccounts +import ComposeUI +import UniformTypeIdentifiers +import TuskerPreferences + +class ShareViewController: UIViewController { + + private var state: State = .loading + + required init?(coder: NSCoder) { + super.init(coder: coder) + + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.tintColor = Preferences.shared.accentColor.color + + if let account = UserAccountsManager.shared.getMostRecentAccount() { + Task { @MainActor in + let draft = await createDraft(account: account) + state = .ok + + let context = ShareMastodonContext(accountInfo: account) + let host = ShareHostingController(draft: draft, mastodonContext: context) + let nav = UINavigationController(rootViewController: host) + self.addChild(nav) + nav.view.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(nav.view) + NSLayoutConstraint.activate([ + nav.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), + nav.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), + nav.view.topAnchor.constraint(equalTo: self.view.topAnchor), + nav.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), + ]) + nav.didMove(toParent: self) + } + } else { + state = .notLoggedIn + } + } + + private func createDraft(account: UserAccountInfo) async -> Draft { + let (text, attachments) = await getDraftConfigurationFromExtensionContext() + let draft = Draft( + accountID: account.id, + text: text, + contentWarning: "", + inReplyToID: nil, + // TODO: get the default visibility from preferences + visibility: .public, + localOnly: false + ) + draft.attachments = attachments + return draft + } + + private func getDraftConfigurationFromExtensionContext() async -> (String, [DraftAttachment]) { + guard let extensionContext, + let inputItem = (extensionContext.inputItems as? [NSExtensionItem])?.first, + let itemProvider = inputItem.attachments?.first else { + return ("", []) + } + if let url: NSURL = await getObject(from: itemProvider) { + if let title = inputItem.attributedTitle ?? inputItem.attributedContentText { + return ("\n\n\(title.string)\n\(url.absoluteString ?? "")", []) + } else { + return ("\n\n\(url.absoluteString ?? "")", []) + } + } else if let text: NSString = await getObject(from: itemProvider) { + return ("\n\n\(text)", []) + } else if let attachment: DraftAttachment = await getObject(from: itemProvider) { + return ("", [attachment]) + } else if let attributedContent = inputItem.attributedContentText { + return ("\n\n\(attributedContent.string)", []) + } else { + return ("", []) + } + } + + private func getObject(from itemProvider: NSItemProvider) async -> T? { + guard itemProvider.canLoadObject(ofClass: T.self) else { + return nil + } + return await withCheckedContinuation({ continuation in + itemProvider.loadObject(ofClass: T.self) { object, error in + continuation.resume(returning: object as? T) + } + }) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if case .notLoggedIn = state { + let alert = UIAlertController(title: "Not Logged In", message: "You need to log in to an account through the app before you can post.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [unowned self] _ in + self.extensionContext!.cancelRequest(withError: Error.notLoggedIn) + })) + present(alert, animated: true) + } + } + + enum State { + case loading + case notLoggedIn + case ok + } + + enum Error: Swift.Error { + case notLoggedIn + } + +} diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index aab086d6..53242ada 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -220,6 +220,16 @@ D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */; }; D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */; }; D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */; }; + D6A4531629EF64BA00032932 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4531529EF64BA00032932 /* ShareViewController.swift */; }; + D6A4531929EF64BA00032932 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6A4531729EF64BA00032932 /* MainInterface.storyboard */; }; + D6A4531D29EF64BA00032932 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6A4531329EF64BA00032932 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D6A4532429EF665200032932 /* ComposeUI in Frameworks */ = {isa = PBXBuildFile; productRef = D6A4532329EF665200032932 /* ComposeUI */; }; + D6A4532629EF665600032932 /* InstanceFeatures in Frameworks */ = {isa = PBXBuildFile; productRef = D6A4532529EF665600032932 /* InstanceFeatures */; }; + D6A4532829EF665800032932 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D6A4532729EF665800032932 /* Pachyderm */; }; + D6A4532A29EF665A00032932 /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D6A4532929EF665A00032932 /* TuskerPreferences */; }; + D6A4532C29EF665D00032932 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D6A4532B29EF665D00032932 /* UserAccounts */; }; + D6A4532E29EF7DDD00032932 /* ShareHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */; }; + D6A4533029EF7DEE00032932 /* ShareMastodonContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4532F29EF7DEE00032932 /* ShareMastodonContext.swift */; }; D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */; }; D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; }; D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */; }; @@ -335,6 +345,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + D6A4531B29EF64BA00032932 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = D6A4531229EF64BA00032932; + remoteInfo = ShareExtension; + }; D6D4DDE1212518A200E1C4BB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; @@ -365,6 +382,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + D6A4531D29EF64BA00032932 /* ShareExtension.appex in Embed Foundation Extensions */, D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; @@ -597,6 +615,13 @@ D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowNotificationGroupTableViewCell.xib; sourceTree = ""; }; D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = ""; }; D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountTableViewCell.xib; sourceTree = ""; }; + D6A4531329EF64BA00032932 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + D6A4531529EF64BA00032932 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + D6A4531829EF64BA00032932 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + D6A4531A29EF64BA00032932 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D6A4531E29EF64BA00032932 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; + D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareHostingController.swift; sourceTree = ""; }; + D6A4532F29EF7DEE00032932 /* ShareMastodonContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareMastodonContext.swift; sourceTree = ""; }; D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherViewController.swift; sourceTree = ""; }; D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FastAccountSwitcherViewController.xib; sourceTree = ""; }; D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastSwitchingAccountView.swift; sourceTree = ""; }; @@ -723,6 +748,18 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + D6A4531029EF64BA00032932 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D6A4532829EF665800032932 /* Pachyderm in Frameworks */, + D6A4532A29EF665A00032932 /* TuskerPreferences in Frameworks */, + D6A4532629EF665600032932 /* InstanceFeatures in Frameworks */, + D6A4532C29EF665D00032932 /* UserAccounts in Frameworks */, + D6A4532429EF665200032932 /* ComposeUI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6D4DDC9212518A000E1C4BB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1264,6 +1301,19 @@ path = "Status Action Account List"; sourceTree = ""; }; + D6A4531429EF64BA00032932 /* ShareExtension */ = { + isa = PBXGroup; + children = ( + D6A4531E29EF64BA00032932 /* ShareExtension.entitlements */, + D6A4532F29EF7DEE00032932 /* ShareMastodonContext.swift */, + D6A4531529EF64BA00032932 /* ShareViewController.swift */, + D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */, + D6A4531729EF64BA00032932 /* MainInterface.storyboard */, + D6A4531A29EF64BA00032932 /* Info.plist */, + ); + path = ShareExtension; + sourceTree = ""; + }; D6A4DCC92553666600D9DE31 /* Fast Account Switcher */ = { isa = PBXGroup; children = ( @@ -1408,6 +1458,7 @@ D6D4DDE3212518A200E1C4BB /* TuskerTests */, D6D4DDEE212518A200E1C4BB /* TuskerUITests */, D6E343A9265AAD6B00C4AA01 /* OpenInTusker */, + D6A4531429EF64BA00032932 /* ShareExtension */, D6D4DDCD212518A000E1C4BB /* Products */, D65A37F221472F300087646E /* Frameworks */, ); @@ -1420,6 +1471,7 @@ D6D4DDE0212518A200E1C4BB /* TuskerTests.xctest */, D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */, D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */, + D6A4531329EF64BA00032932 /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -1578,6 +1630,30 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + D6A4531229EF64BA00032932 /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = D6A4532229EF64BA00032932 /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + D6A4530F29EF64BA00032932 /* Sources */, + D6A4531029EF64BA00032932 /* Frameworks */, + D6A4531129EF64BA00032932 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ShareExtension; + packageProductDependencies = ( + D6A4532329EF665200032932 /* ComposeUI */, + D6A4532529EF665600032932 /* InstanceFeatures */, + D6A4532729EF665800032932 /* Pachyderm */, + D6A4532929EF665A00032932 /* TuskerPreferences */, + D6A4532B29EF665D00032932 /* UserAccounts */, + ); + productName = ShareExtension; + productReference = D6A4531329EF64BA00032932 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; D6D4DDCB212518A000E1C4BB /* Tusker */ = { isa = PBXNativeTarget; buildConfigurationList = D6D4DDF4212518A200E1C4BB /* Build configuration list for PBXNativeTarget "Tusker" */; @@ -1594,6 +1670,7 @@ ); dependencies = ( D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */, + D6A4531C29EF64BA00032932 /* PBXTargetDependency */, ); name = Tusker; packageProductDependencies = ( @@ -1676,10 +1753,13 @@ D6D4DDC4212518A000E1C4BB /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1250; + LastSwiftUpdateCheck = 1430; LastUpgradeCheck = 1400; ORGANIZATIONNAME = Shadowfacts; TargetAttributes = { + D6A4531229EF64BA00032932 = { + CreatedOnToolsVersion = 14.3; + }; D6D4DDCB212518A000E1C4BB = { CreatedOnToolsVersion = 10.0; LastSwiftMigration = 1420; @@ -1728,11 +1808,20 @@ D6D4DDDF212518A200E1C4BB /* TuskerTests */, D6D4DDEA212518A200E1C4BB /* TuskerUITests */, D6E343A7265AAD6B00C4AA01 /* OpenInTusker */, + D6A4531229EF64BA00032932 /* ShareExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + D6A4531129EF64BA00032932 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D6A4531929EF64BA00032932 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6D4DDCA212518A000E1C4BB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1832,6 +1921,16 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + D6A4530F29EF64BA00032932 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D6A4532E29EF7DDD00032932 /* ShareHostingController.swift in Sources */, + D6A4531629EF64BA00032932 /* ShareViewController.swift in Sources */, + D6A4533029EF7DEE00032932 /* ShareMastodonContext.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6D4DDC8212518A000E1C4BB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2150,6 +2249,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + D6A4531C29EF64BA00032932 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D6A4531229EF64BA00032932 /* ShareExtension */; + targetProxy = D6A4531B29EF64BA00032932 /* PBXContainerItemProxy */; + }; D6D4DDE2212518A200E1C4BB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D6D4DDCB212518A000E1C4BB /* Tusker */; @@ -2169,6 +2273,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + D6A4531729EF64BA00032932 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D6A4531829EF64BA00032932 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2349,6 +2461,93 @@ }; name = Dist; }; + D6A4531F29EF64BA00032932 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D6A4532029EF64BA00032932 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D6A4532129EF64BA00032932 /* Dist */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.ShareExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Dist; + }; D6D4DDF2212518A200E1C4BB /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D6D706A829498C82000827ED /* Tusker.xcconfig */; @@ -2667,6 +2866,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + D6A4532229EF64BA00032932 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D6A4531F29EF64BA00032932 /* Debug */, + D6A4532029EF64BA00032932 /* Release */, + D6A4532129EF64BA00032932 /* Dist */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D6D4DDC7212518A000E1C4BB /* Build configuration list for PBXProject "Tusker" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2791,6 +3000,26 @@ isa = XCSwiftPackageProductDependency; productName = Pachyderm; }; + D6A4532329EF665200032932 /* ComposeUI */ = { + isa = XCSwiftPackageProductDependency; + productName = ComposeUI; + }; + D6A4532529EF665600032932 /* InstanceFeatures */ = { + isa = XCSwiftPackageProductDependency; + productName = InstanceFeatures; + }; + D6A4532729EF665800032932 /* Pachyderm */ = { + isa = XCSwiftPackageProductDependency; + productName = Pachyderm; + }; + D6A4532929EF665A00032932 /* TuskerPreferences */ = { + isa = XCSwiftPackageProductDependency; + productName = TuskerPreferences; + }; + D6A4532B29EF665D00032932 /* UserAccounts */ = { + isa = XCSwiftPackageProductDependency; + productName = UserAccounts; + }; D6B0026D29B5248800C70BE2 /* UserAccounts */ = { isa = XCSwiftPackageProductDependency; productName = UserAccounts;