Add Damus Share Feature
This PR change adds Damus Share feature to the app that allows the users to share Photos and URLs from foreign apps. Changelog-Added: Add Damus Share Feature Signed-off-by: Swift Coder <scoder1747@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -213,3 +213,27 @@ enum HighlightSource: Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ShareContent {
|
||||||
|
let title: String
|
||||||
|
let content: ContentType
|
||||||
|
|
||||||
|
enum ContentType {
|
||||||
|
case link(URL)
|
||||||
|
case media([PreUploadedMedia])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLinkURL() -> URL? {
|
||||||
|
if case let .link(url) = content {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMediaArray() -> [PreUploadedMedia] {
|
||||||
|
if case let .media(mediaArray) = content {
|
||||||
|
return mediaArray
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ enum PostAction {
|
|||||||
case quoting(NostrEvent)
|
case quoting(NostrEvent)
|
||||||
case posting(PostTarget)
|
case posting(PostTarget)
|
||||||
case highlighting(HighlightContentDraft)
|
case highlighting(HighlightContentDraft)
|
||||||
|
case sharing(ShareContent)
|
||||||
|
|
||||||
var ev: NostrEvent? {
|
var ev: NostrEvent? {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -42,6 +43,8 @@ enum PostAction {
|
|||||||
return nil
|
return nil
|
||||||
case .highlighting:
|
case .highlighting:
|
||||||
return nil
|
return nil
|
||||||
|
case .sharing(_):
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,6 +60,7 @@ struct PostView: View {
|
|||||||
@State var imagePastedFromPasteboard: UIImage? = nil
|
@State var imagePastedFromPasteboard: UIImage? = nil
|
||||||
@State var imageUploadConfirmPasteboard: Bool = false
|
@State var imageUploadConfirmPasteboard: Bool = false
|
||||||
@State var references: [RefId] = []
|
@State var references: [RefId] = []
|
||||||
|
@State var imageUploadConfirmDamusShare: Bool = false
|
||||||
@State var filtered_pubkeys: Set<Pubkey> = []
|
@State var filtered_pubkeys: Set<Pubkey> = []
|
||||||
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
|
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
|
||||||
@State var newCursorIndex: Int?
|
@State var newCursorIndex: Int?
|
||||||
@@ -217,6 +221,8 @@ struct PostView: View {
|
|||||||
damus_state.drafts.post = nil
|
damus_state.drafts.post = nil
|
||||||
case .highlighting(let draft):
|
case .highlighting(let draft):
|
||||||
damus_state.drafts.highlights.removeValue(forKey: draft.source)
|
damus_state.drafts.highlights.removeValue(forKey: draft.source)
|
||||||
|
case .sharing(_):
|
||||||
|
damus_state.drafts.post = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -391,6 +397,11 @@ struct PostView: View {
|
|||||||
else if case .highlighting(let draft) = action {
|
else if case .highlighting(let draft) = action {
|
||||||
HighlightDraftContentView(draft: draft)
|
HighlightDraftContentView(draft: draft)
|
||||||
}
|
}
|
||||||
|
else if case .sharing(let draft) = action,
|
||||||
|
let url = draft.getLinkURL() {
|
||||||
|
LinkViewRepresentable(meta: .url(url))
|
||||||
|
.frame(height: 50)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
@@ -499,6 +510,19 @@ struct PostView: View {
|
|||||||
}
|
}
|
||||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
||||||
}
|
}
|
||||||
|
// This alert seeks confirmation about media-upload from Damus Share Extension
|
||||||
|
.alert(NSLocalizedString("Are you sure you want to upload the selected media?", comment: "Alert message asking if the user wants to upload media."), isPresented: $imageUploadConfirmDamusShare) {
|
||||||
|
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
|
||||||
|
Task {
|
||||||
|
for media in preUploadedMedia {
|
||||||
|
if let mediaToUpload = generateMediaUpload(media) {
|
||||||
|
await self.handle_upload(media: mediaToUpload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
||||||
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
let loaded_draft = load_draft()
|
let loaded_draft = load_draft()
|
||||||
|
|
||||||
@@ -512,6 +536,15 @@ struct PostView: View {
|
|||||||
fill_target_content(target: target)
|
fill_target_content(target: target)
|
||||||
case .highlighting(let draft):
|
case .highlighting(let draft):
|
||||||
references = [draft.source.ref()]
|
references = [draft.source.ref()]
|
||||||
|
case .sharing(let content):
|
||||||
|
if let url = content.getLinkURL() {
|
||||||
|
self.post = NSMutableAttributedString(string: "\(content.title)\n\(String(url.absoluteString))")
|
||||||
|
} else {
|
||||||
|
self.preUploadedMedia = content.getMediaArray()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
|
self.imageUploadConfirmDamusShare = true // display Confirm Sheet after 1 sec
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
@@ -661,6 +694,8 @@ func set_draft_for_post(drafts: Drafts, action: PostAction, artifacts: DraftArti
|
|||||||
drafts.post = artifacts
|
drafts.post = artifacts
|
||||||
case .highlighting(let draft):
|
case .highlighting(let draft):
|
||||||
drafts.highlights[draft.source] = artifacts
|
drafts.highlights[draft.source] = artifacts
|
||||||
|
case .sharing(_):
|
||||||
|
drafts.post = artifacts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,6 +709,8 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts?
|
|||||||
return drafts.post
|
return drafts.post
|
||||||
case .highlighting(let draft):
|
case .highlighting(let draft):
|
||||||
return drafts.highlights[draft.source]
|
return drafts.highlights[draft.source]
|
||||||
|
case .sharing(_):
|
||||||
|
return drafts.post
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,6 +786,8 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post
|
|||||||
break
|
break
|
||||||
case .highlighting(let draft):
|
case .highlighting(let draft):
|
||||||
break
|
break
|
||||||
|
case .sharing(_):
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// append additional tags
|
// append additional tags
|
||||||
|
|||||||
22
share extension/Info.plist
Normal file
22
share extension/Info.plist
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionAttributes</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionActivationRule</key>
|
||||||
|
<string>TRUEPREDICATE</string>
|
||||||
|
<key>NSExtensionActivationSupportsImage</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.share-services</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
356
share extension/ShareViewController.swift
Normal file
356
share extension/ShareViewController.swift
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
//
|
||||||
|
// ShareViewController.swift
|
||||||
|
// share extension
|
||||||
|
//
|
||||||
|
// Created by Swift on 11/4/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Social
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
let this_app: UIApplication = UIApplication()
|
||||||
|
|
||||||
|
class ShareViewController: SLComposeServiceViewController {
|
||||||
|
private var contentView: UIHostingController<ShareExtensionView>?
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.view.tintColor = UIColor(DamusColors.purple)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let contentView = UIHostingController(rootView: ShareExtensionView(extensionContext: self.extensionContext!,
|
||||||
|
dismissParent: { [weak self] in
|
||||||
|
self?.dismissSelf()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
self.addChild(contentView)
|
||||||
|
self.contentView = contentView
|
||||||
|
self.view.addSubview(contentView.view)
|
||||||
|
|
||||||
|
// set up constraints
|
||||||
|
contentView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
|
||||||
|
contentView.view.bottomAnchor.constraint (equalTo: self.view.bottomAnchor).isActive = true
|
||||||
|
contentView.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
|
||||||
|
contentView.view.rightAnchor.constraint (equalTo: self.view.rightAnchor).isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissSelf() {
|
||||||
|
super.didSelectCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShareExtensionView: View {
|
||||||
|
@State private var share_state: ShareState = .loading
|
||||||
|
let extensionContext: NSExtensionContext
|
||||||
|
@State private var state: DamusState? = nil
|
||||||
|
@State private var preUploadedMedia: [PreUploadedMedia] = []
|
||||||
|
var dismissParent: (() -> Void)?
|
||||||
|
|
||||||
|
@Environment(\.scenePhase) var scenePhase
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
switch self.share_state {
|
||||||
|
case .loading:
|
||||||
|
ProgressView()
|
||||||
|
case .no_content:
|
||||||
|
Group {
|
||||||
|
Text("No content availabe to share", comment: "Title indicating that there was no available content to share")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
Text("There is no content available to share at this time. Please close this view and try again.", comment: "Label explaining that no content is available to share and instructing the user to close the view and try again.")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
self.done()
|
||||||
|
}, label: {
|
||||||
|
Text("Close", comment: "Button label giving the user the option to close the view when no content is available to share")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .not_logged_in:
|
||||||
|
Group {
|
||||||
|
Text("Not Logged In", comment: "Title indicating that sharing cannot proceed because the user is not logged in.")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Text("You cannot share content because you are not logged in. Please close this view, log in to your account, and try again.", comment: "Label explaining that sharing cannot proceed because the user is not logged in.")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
self.done()
|
||||||
|
}, label: {
|
||||||
|
Text("Close", comment: "Button label giving the user the option to close the sheet due to not being logged in.")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .loaded(let content):
|
||||||
|
PostView(
|
||||||
|
action: .sharing(content),
|
||||||
|
damus_state: state! // state will have a value at this point
|
||||||
|
)
|
||||||
|
case .cancelled:
|
||||||
|
Group {
|
||||||
|
Text("Cancelled", comment: "Title indicating that the user has cancelled.")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.padding()
|
||||||
|
Button(action: {
|
||||||
|
self.done()
|
||||||
|
}, label: {
|
||||||
|
Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to share.")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .failed(let error):
|
||||||
|
Group {
|
||||||
|
Text("Error", comment: "Title indicating that an error has occurred.")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
Text("An unexpected error occurred. Please contact Damus support via [Nostr](damus:npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955) or [email](support@damus.io) with the error message below.", comment: "Label explaining there was an error, and suggesting next steps")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
Text("Error: \(error)")
|
||||||
|
Button(action: {
|
||||||
|
done()
|
||||||
|
}, label: {
|
||||||
|
Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying share.")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .posted(event: let event):
|
||||||
|
Group {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
Text("Shared", comment: "Title indicating that the user has shared content successfully")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
|
Link(destination: URL(string: "damus:\(event.id.bech32)")!, label: {
|
||||||
|
Text("Go to the app", comment: "Button label giving the user the option to go to the app after sharing content")
|
||||||
|
})
|
||||||
|
.buttonStyle(GradientButtonStyle())
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
self.done()
|
||||||
|
}, label: {
|
||||||
|
Text("Close", comment: "Button label giving the user the option to close the sheet from which they shared content")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .posting:
|
||||||
|
Group {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
Text("Sharing", comment: "Title indicating that the content is being published to the network")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.bottom)
|
||||||
|
Text("Your content is being broadcasted to the network. Please wait.", comment: "Label explaining that their content sharing action is in progress")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear(perform: {
|
||||||
|
if setDamusState() {
|
||||||
|
self.loadSharedContent()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onDisappear {
|
||||||
|
Task { @MainActor in
|
||||||
|
self.state?.ndb.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.post)) { post_notification in
|
||||||
|
switch post_notification {
|
||||||
|
case .post(let post):
|
||||||
|
self.post(post)
|
||||||
|
case .cancel:
|
||||||
|
self.share_state = .cancelled
|
||||||
|
dismissParent?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||||
|
guard let state else { return }
|
||||||
|
switch phase {
|
||||||
|
case .background:
|
||||||
|
print("txn: 📙 SHARE BACKGROUNDED")
|
||||||
|
Task { @MainActor in
|
||||||
|
state.ndb.close()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .inactive:
|
||||||
|
print("txn: 📙 SHARE INACTIVE")
|
||||||
|
break
|
||||||
|
case .active:
|
||||||
|
print("txn: 📙 SHARE ACTIVE")
|
||||||
|
state.pool.ping()
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
||||||
|
guard let state else { return }
|
||||||
|
print("SHARE ACTIVE NOTIFY")
|
||||||
|
if state.ndb.reopen() {
|
||||||
|
print("SHARE NOSTRDB REOPENED")
|
||||||
|
} else {
|
||||||
|
print(" SHARE NOSTRDB FAILED TO REOPEN closed: \(state.ndb.is_closed)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { obj in
|
||||||
|
guard let state else { return }
|
||||||
|
print("txn: 📙 SHARE BACKGROUNDED")
|
||||||
|
Task { @MainActor in
|
||||||
|
state.ndb.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func post(_ post: NostrPost) {
|
||||||
|
self.share_state = .posting
|
||||||
|
guard let state else {
|
||||||
|
self.share_state = .failed(error: "Damus state not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let full_keypair = state.keypair.to_full() else {
|
||||||
|
self.share_state = .not_logged_in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let posted_event = post.to_event(keypair: full_keypair) else {
|
||||||
|
self.share_state = .failed(error: "Cannot convert post data into a nostr event")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.postbox.send(posted_event, on_flush: .once({ flushed_event in
|
||||||
|
if flushed_event.event.id == posted_event.id {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias
|
||||||
|
self.share_state = .posted(event: flushed_event.event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.share_state = .failed(error: "Flushed event is not the event we just tried to post.")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func setDamusState() -> Bool {
|
||||||
|
guard let keypair = get_saved_keypair(),
|
||||||
|
keypair.privkey != nil else {
|
||||||
|
self.share_state = .not_logged_in
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
state = DamusState(keypair: keypair)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSharedContent() {
|
||||||
|
guard let extensionItem = extensionContext.inputItems.first as? NSExtensionItem else {
|
||||||
|
share_state = .failed(error: "Unable to get item provider")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = ""
|
||||||
|
|
||||||
|
// Check for the attributed text from the extension item
|
||||||
|
if let attributedContentData = extensionItem.userInfo?[NSExtensionItemAttributedContentTextKey] as? Data {
|
||||||
|
if let attributedText = try? NSAttributedString(data: attributedContentData, options: [.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) {
|
||||||
|
let plainText = attributedText.string
|
||||||
|
print("Extracted Text: \(plainText)")
|
||||||
|
title = plainText
|
||||||
|
} else {
|
||||||
|
print("Failed to decode RTF content.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("Content is not in RTF format or data is unavailable.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through all attachments to handle multiple images
|
||||||
|
for itemProvider in extensionItem.attachments ?? [] {
|
||||||
|
if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { (item, error) in
|
||||||
|
if let url = item as? URL {
|
||||||
|
self.share_state = .loaded(ShareContent(title: title, content: .link(url)))
|
||||||
|
} else {
|
||||||
|
self.share_state = .failed(error: "Failed to load text content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (item, error) in
|
||||||
|
if let url = item as? URL {
|
||||||
|
|
||||||
|
attemptAcquireResourceAndChooseMedia(
|
||||||
|
url: url,
|
||||||
|
fallback: processImage,
|
||||||
|
unprocessedEnum: {.unprocessed_image($0)},
|
||||||
|
processedEnum: {.processed_image($0)})
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self.share_state = .failed(error: "Failed to load image content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: UTType.movie.identifier) { (item, error) in
|
||||||
|
if let url = item as? URL {
|
||||||
|
attemptAcquireResourceAndChooseMedia(
|
||||||
|
url: url,
|
||||||
|
fallback: processVideo,
|
||||||
|
unprocessedEnum: {.unprocessed_video($0)},
|
||||||
|
processedEnum: {.processed_video($0)}
|
||||||
|
)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self.share_state = .failed(error: "Failed to load video content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
share_state = .no_content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func attemptAcquireResourceAndChooseMedia(url: URL, fallback: (URL) -> URL?, unprocessedEnum: (URL) -> PreUploadedMedia, processedEnum: (URL) -> PreUploadedMedia) {
|
||||||
|
if url.startAccessingSecurityScopedResource() {
|
||||||
|
// Have permission from system to use url out of scope
|
||||||
|
print("Acquired permission to security scoped resource")
|
||||||
|
chooseMedia(unprocessedEnum(url))
|
||||||
|
} else {
|
||||||
|
// Need to copy URL to non-security scoped location
|
||||||
|
guard let newUrl = fallback(url) else { return }
|
||||||
|
chooseMedia(processedEnum(newUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chooseMedia(_ media: PreUploadedMedia) {
|
||||||
|
self.preUploadedMedia.append(media)
|
||||||
|
if extensionItem.attachments?.count == preUploadedMedia.count {
|
||||||
|
self.share_state = .loaded(ShareContent(title: "", content: .media(preUploadedMedia)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func done() {
|
||||||
|
extensionContext.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ShareState {
|
||||||
|
case loading
|
||||||
|
case no_content
|
||||||
|
case not_logged_in
|
||||||
|
case loaded(ShareContent)
|
||||||
|
case failed(error: String)
|
||||||
|
case cancelled
|
||||||
|
case posting
|
||||||
|
case posted(event: NostrEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
18
share extension/share extension.entitlements
Normal file
18
share extension/share extension.entitlements
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.damus</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.jb55.damus2</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Reference in New Issue
Block a user