ActionViewController.swift (14022B)
1 // 2 // ActionViewController.swift 3 // highlighter action extension 4 // 5 // Created by Daniel DβAquino on 2024-08-09. 6 // 7 8 import UIKit 9 import MobileCoreServices 10 import UniformTypeIdentifiers 11 import SwiftUI 12 13 struct ShareExtensionView: View { 14 @State var highlighter_state: HighlighterState = .loading 15 let extensionContext: NSExtensionContext 16 @State var state: DamusState? = nil 17 @State var signedEvent: String? = nil 18 19 @State private var selectedText = "" 20 @State private var selectedTextHeight: CGFloat = .zero 21 @State private var selectedTextWidth: CGFloat = .zero 22 23 @Environment(\.scenePhase) var scenePhase 24 25 var body: some View { 26 VStack(spacing: 15) { 27 if let state { 28 switch self.highlighter_state { 29 case .loading: 30 ProgressView() 31 case .no_highlight_text: 32 Group { 33 Text("No text selected", comment: "Title indicating that a highlight cannot be posted because no text was selected.") 34 .font(.largeTitle) 35 .multilineTextAlignment(.center) 36 .padding() 37 Text("You cannot post a highlight because you have selected no text on the page! Please close this, select some text, and try again.", comment: "Label explaining a highlight cannot be made because there was no selected text, and some instructions on how to resolve the issue") 38 .multilineTextAlignment(.center) 39 Button(action: { 40 self.done() 41 }, label: { 42 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight") 43 }) 44 .foregroundStyle(.secondary) 45 } 46 case .not_logged_in: 47 Group { 48 Text("Not logged in", comment: "Title indicating that a highlight cannot be posted because the user is not logged in.") 49 .font(.largeTitle) 50 .multilineTextAlignment(.center) 51 .padding() 52 Text("You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again.", comment: "Label explaining a highlight cannot be made because the user is not logged in") 53 .multilineTextAlignment(.center) 54 Button(action: { 55 self.done() 56 }, label: { 57 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight") 58 }) 59 .foregroundStyle(.secondary) 60 } 61 case .loaded(let highlighted_text, let source_url): 62 PostView( 63 action: .highlighting(HighlightContentDraft(selected_text: highlighted_text, source: .external_url(source_url))), 64 damus_state: state 65 ) 66 case .failed(let error): 67 Group { 68 Text("Error", comment: "Title indicating that an error has occurred.") 69 .font(.largeTitle) 70 .multilineTextAlignment(.center) 71 .padding() 72 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") 73 .multilineTextAlignment(.center) 74 Text("Error: \(error)") 75 Button(action: { 76 self.done() 77 }, label: { 78 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight") 79 }) 80 .foregroundStyle(.secondary) 81 } 82 case .posted(event: let event): 83 Group { 84 Image(systemName: "checkmark.circle.fill") 85 .resizable() 86 .frame(width: 60, height: 60) 87 Text("Posted", comment: "Title indicating that the user has posted a highlight successfully") 88 .font(.largeTitle) 89 .multilineTextAlignment(.center) 90 .padding(.bottom) 91 92 Link(destination: URL(string: "damus:\(event.id.bech32)")!, label: { 93 Text("Go to the app", comment: "Button label giving the user the option to go to the app after posting a highlight") 94 }) 95 .buttonStyle(GradientButtonStyle()) 96 Button(action: { 97 self.done() 98 }, label: { 99 Text("Close", comment: "Button label giving the user the option to close the sheet from which they posted a highlight") 100 }) 101 .foregroundStyle(.secondary) 102 } 103 case .cancelled: 104 Group { 105 Text("Cancelled", comment: "Title indicating that the user has cancelled.") 106 .font(.largeTitle) 107 .padding() 108 Button(action: { 109 self.done() 110 }, label: { 111 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight") 112 }) 113 .foregroundStyle(.secondary) 114 } 115 case .posting: 116 Group { 117 ProgressView() 118 .frame(width: 20, height: 20) 119 Text("Posting", comment: "Title indicating that the highlight post is being published to the network") 120 .font(.largeTitle) 121 .multilineTextAlignment(.center) 122 .padding(.bottom) 123 Text("Your highlight is being broadcasted to the network. Please wait.", comment: "Label explaining there their highlight publishing action is in progress") 124 .multilineTextAlignment(.center) 125 .padding() 126 } 127 } 128 } 129 } 130 .onAppear(perform: { 131 self.loadSharedUrl() 132 guard let keypair = get_saved_keypair() else { return } 133 guard keypair.privkey != nil else { 134 self.highlighter_state = .not_logged_in 135 return 136 } 137 self.state = DamusState(keypair: keypair) 138 self.state?.nostrNetwork.connect() 139 }) 140 .onChange(of: self.highlighter_state) { 141 if case .cancelled = highlighter_state { 142 self.done() 143 } 144 } 145 .onReceive(handle_notify(.post)) { post_notification in 146 switch post_notification { 147 case .post(let post): 148 self.post(post) 149 case .cancel: 150 self.highlighter_state = .cancelled 151 } 152 } 153 .onChange(of: scenePhase) { (phase: ScenePhase) in 154 guard let state else { return } 155 switch phase { 156 case .background: 157 print("txn: π HIGHLIGHTER BACKGROUNDED") 158 Task { @MainActor in 159 state.ndb.close() 160 } 161 break 162 case .inactive: 163 print("txn: π HIGHLIGHTER INACTIVE") 164 break 165 case .active: 166 print("txn: π HIGHLIGHTER ACTIVE") 167 state.nostrNetwork.pool.ping() 168 @unknown default: 169 break 170 } 171 } 172 .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in 173 guard let state else { return } 174 print("txn: π HIGHLIGHTER ACTIVE NOTIFY") 175 if state.ndb.reopen() { 176 print("txn: HIGHLIGHTER NOSTRDB REOPENED") 177 } else { 178 print("txn: HIGHLIGHTER NOSTRDB FAILED TO REOPEN closed: \(state.ndb.is_closed)") 179 } 180 } 181 .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { obj in 182 guard let state else { return } 183 print("txn: π HIGHLIGHTER BACKGROUNDED") 184 Task { @MainActor in 185 state.ndb.close() 186 } 187 } 188 } 189 190 func loadSharedUrl() { 191 guard 192 let extensionItem = extensionContext.inputItems.first as? NSExtensionItem, 193 let itemProvider = extensionItem.attachments?.first else { 194 self.highlighter_state = .failed(error: "Can't get itemProvider") 195 return 196 } 197 198 let propertyList = UTType.propertyList.identifier 199 if itemProvider.hasItemConformingToTypeIdentifier(propertyList) { 200 itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in 201 guard let dictionary = item as? NSDictionary else { return } 202 if error != nil { 203 self.highlighter_state = .failed(error: "Error loading plist item: \(error?.localizedDescription ?? "Unknown")") 204 return 205 } 206 OperationQueue.main.addOperation { 207 if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary, 208 let urlString = results["URL"] as? String, 209 let selection = results["selectedText"] as? String, 210 let url = URL(string: urlString) { 211 guard selection != "" else { 212 self.highlighter_state = .no_highlight_text 213 return 214 } 215 self.highlighter_state = .loaded(highlighted_text: selection, source_url: url) 216 } 217 else { 218 self.highlighter_state = .failed(error: "Cannot load results") 219 } 220 } 221 }) 222 } 223 else { 224 self.highlighter_state = .failed(error: "No plist detected") 225 } 226 } 227 228 func post(_ post: NostrPost) { 229 self.highlighter_state = .posting 230 guard let state else { 231 self.highlighter_state = .failed(error: "Damus state not initialized") 232 return 233 } 234 guard let full_keypair = state.keypair.to_full() else { 235 self.highlighter_state = .not_logged_in 236 return 237 } 238 guard let posted_event = post.to_event(keypair: full_keypair) else { 239 self.highlighter_state = .failed(error: "Cannot convert post data into a nostr event") 240 return 241 } 242 state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in 243 if flushed_event.event.id == posted_event.id { 244 DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias 245 self.highlighter_state = .posted(event: flushed_event.event) 246 }) 247 } 248 else { 249 self.highlighter_state = .failed(error: "Flushed event is not the event we just tried to post.") 250 } 251 })) 252 } 253 254 func done() { 255 self.extensionContext.completeRequest(returningItems: [], completionHandler: nil) 256 } 257 258 enum HighlighterState: Equatable { 259 case loading 260 case no_highlight_text 261 case not_logged_in 262 case loaded(highlighted_text: String, source_url: URL) 263 case posting 264 case posted(event: NostrEvent) 265 case cancelled 266 case failed(error: String) 267 } 268 } 269 270 class ActionViewController: UIViewController { 271 override func viewDidLoad() { 272 super.viewDidLoad() 273 self.view.tintColor = UIColor(DamusColors.purple) 274 275 DispatchQueue.main.async { 276 let contentView = UIHostingController(rootView: ShareExtensionView(extensionContext: self.extensionContext!)) 277 self.addChild(contentView) 278 self.view.addSubview(contentView.view) 279 280 // set up constraints 281 contentView.view.translatesAutoresizingMaskIntoConstraints = false 282 contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true 283 contentView.view.bottomAnchor.constraint (equalTo: self.view.bottomAnchor).isActive = true 284 contentView.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true 285 contentView.view.rightAnchor.constraint (equalTo: self.view.rightAnchor).isActive = true 286 } 287 } 288 }