Merge Highlighter into release_1.10
Daniel D’Aquino (7):
Add convenience functions
Simplify SelectableText state management
Add support for rendering highlights with comments
Add support for adding comments when creating a highlight
Add highlighter extension
Fix highlight tag ambiguity with specifiers
Improve handling of NostrDB when switching apps
William Casarin (4):
lmdb: patch semaphore names to use group container prefix
notifications: add extended virtual addressing entitlement
highlighter: add extended virtual addressing entitlement
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
|
|||||||
+1176
-6
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1540"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D703D7162C66E47100A400EA"
|
||||||
|
BuildableName = "HighlighterActionExtension.appex"
|
||||||
|
BlueprintName = "HighlighterActionExtension"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<RemoteRunnable
|
||||||
|
runnableDebuggingMode = "1"
|
||||||
|
BundleIdentifier = "com.apple.mobilesafari"
|
||||||
|
RemotePath = "/Library/Developer/CoreSimulator/Volumes/iOS_21F79/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.5.simruntime/Contents/Resources/RuntimeRoot/Applications/MobileSafari.app">
|
||||||
|
</RemoteRunnable>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -94,8 +94,8 @@ enum OpenWalletError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
||||||
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
if let url = URL(string: "\(wallet.link)\(invoice)"), this_app.canOpenURL(url) {
|
||||||
UIApplication.shared.open(url)
|
this_app.open(url)
|
||||||
} else {
|
} else {
|
||||||
guard let store_link = wallet.appStoreLink else {
|
guard let store_link = wallet.appStoreLink else {
|
||||||
throw OpenWalletError.no_wallet_to_open
|
throw OpenWalletError.no_wallet_to_open
|
||||||
@@ -105,11 +105,11 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
|||||||
throw OpenWalletError.store_link_invalid
|
throw OpenWalletError.store_link_invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
guard UIApplication.shared.canOpenURL(url) else {
|
guard this_app.canOpenURL(url) else {
|
||||||
throw OpenWalletError.system_cannot_open_store_link
|
throw OpenWalletError.system_cannot_open_store_link
|
||||||
}
|
}
|
||||||
|
|
||||||
UIApplication.shared.open(url)
|
this_app.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +122,3 @@ struct InvoiceView_Previews: PreviewProvider {
|
|||||||
.frame(width: 300, height: 200)
|
.frame(width: 300, height: 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func present_sheet(_ sheet: Sheets) {
|
|
||||||
notify(.present_sheet(sheet))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ struct SelectableText: View {
|
|||||||
let event: NostrEvent?
|
let event: NostrEvent?
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
let textAlignment: NSTextAlignment
|
let textAlignment: NSTextAlignment
|
||||||
@State private var showHighlightPost = false
|
@State private var highlightPostingState: HighlightPostingState = .hide
|
||||||
@State private var selectedText = ""
|
|
||||||
@State private var selectedTextHeight: CGFloat = .zero
|
@State private var selectedTextHeight: CGFloat = .zero
|
||||||
@State private var selectedTextWidth: CGFloat = .zero
|
@State private var selectedTextWidth: CGFloat = .zero
|
||||||
|
|
||||||
@@ -37,8 +36,9 @@ struct SelectableText: View {
|
|||||||
fixedWidth: selectedTextWidth,
|
fixedWidth: selectedTextWidth,
|
||||||
textAlignment: self.textAlignment,
|
textAlignment: self.textAlignment,
|
||||||
enableHighlighting: self.enableHighlighting(),
|
enableHighlighting: self.enableHighlighting(),
|
||||||
showHighlightPost: $showHighlightPost,
|
postHighlight: { selectedText in
|
||||||
selectedText: $selectedText,
|
self.highlightPostingState = .show_post_view(highlighted_text: selectedText)
|
||||||
|
},
|
||||||
height: $selectedTextHeight
|
height: $selectedTextHeight
|
||||||
)
|
)
|
||||||
.padding([.leading, .trailing], -1.0)
|
.padding([.leading, .trailing], -1.0)
|
||||||
@@ -53,11 +53,18 @@ struct SelectableText: View {
|
|||||||
self.selectedTextWidth = newSize.width
|
self.selectedTextWidth = newSize.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showHighlightPost) {
|
.sheet(isPresented: Binding(get: {
|
||||||
if let event {
|
return self.highlightPostingState.show()
|
||||||
HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText)
|
}, set: { newValue in
|
||||||
|
self.highlightPostingState = newValue ? .show_post_view(highlighted_text: self.highlightPostingState.highlighted_text() ?? "") : .hide
|
||||||
|
})) {
|
||||||
|
if let event, case .show_post_view(let highlighted_text) = self.highlightPostingState {
|
||||||
|
PostView(
|
||||||
|
action: .highlighting(.init(selected_text: highlighted_text, source: .event(event))),
|
||||||
|
damus_state: damus_state
|
||||||
|
)
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
|
.presentationDetents([.height(selectedTextHeight + 450), .medium, .large])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: selectedTextHeight)
|
.frame(height: selectedTextHeight)
|
||||||
@@ -66,15 +73,34 @@ struct SelectableText: View {
|
|||||||
func enableHighlighting() -> Bool {
|
func enableHighlighting() -> Bool {
|
||||||
self.event != nil
|
self.event != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HighlightPostingState {
|
||||||
|
case hide
|
||||||
|
case show_post_view(highlighted_text: String)
|
||||||
|
|
||||||
|
func show() -> Bool {
|
||||||
|
if case .show_post_view(let highlighted_text) = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func highlighted_text() -> String? {
|
||||||
|
switch self {
|
||||||
|
case .hide:
|
||||||
|
return nil
|
||||||
|
case .show_post_view(highlighted_text: let highlighted_text):
|
||||||
|
return highlighted_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate class TextView: UITextView {
|
fileprivate class TextView: UITextView {
|
||||||
@Binding var showHighlightPost: Bool
|
var postHighlight: (String) -> Void
|
||||||
@Binding var selectedText: String
|
|
||||||
|
|
||||||
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, selectedText: Binding<String>) {
|
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void) {
|
||||||
self._showHighlightPost = showHighlightPost
|
self.postHighlight = postHighlight
|
||||||
self._selectedText = selectedText
|
|
||||||
super.init(frame: frame, textContainer: textContainer)
|
super.init(frame: frame, textContainer: textContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +117,8 @@ fileprivate class TextView: UITextView {
|
|||||||
|
|
||||||
@objc public func highlightText(_ sender: Any?) {
|
@objc public func highlightText(_ sender: Any?) {
|
||||||
guard let selectedRange = self.selectedTextRange else { return }
|
guard let selectedRange = self.selectedTextRange else { return }
|
||||||
selectedText = self.text(in: selectedRange) ?? ""
|
guard let selectedText = self.text(in: selectedRange) else { return }
|
||||||
showHighlightPost.toggle()
|
self.postHighlight(selectedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -105,12 +131,11 @@ fileprivate class TextView: UITextView {
|
|||||||
let fixedWidth: CGFloat
|
let fixedWidth: CGFloat
|
||||||
let textAlignment: NSTextAlignment
|
let textAlignment: NSTextAlignment
|
||||||
let enableHighlighting: Bool
|
let enableHighlighting: Bool
|
||||||
@Binding var showHighlightPost: Bool
|
let postHighlight: (String) -> Void
|
||||||
@Binding var selectedText: String
|
|
||||||
@Binding var height: CGFloat
|
@Binding var height: CGFloat
|
||||||
|
|
||||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
||||||
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, selectedText: $selectedText)
|
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight)
|
||||||
view.isEditable = false
|
view.isEditable = false
|
||||||
view.dataDetectorTypes = .all
|
view.dataDetectorTypes = .all
|
||||||
view.isSelectable = true
|
view.isSelectable = true
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ enum NoteContent {
|
|||||||
case content(String, TagsSequence?)
|
case content(String, TagsSequence?)
|
||||||
|
|
||||||
init(note: NostrEvent, keypair: Keypair) {
|
init(note: NostrEvent, keypair: Keypair) {
|
||||||
if note.known_kind == .dm {
|
if note.known_kind == .dm || note.known_kind == .highlight {
|
||||||
self = .content(note.get_content(keypair), note.tags)
|
self = .content(note.get_content(keypair), note.tags)
|
||||||
} else {
|
} else {
|
||||||
self = .note(note)
|
self = .note(note)
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ enum Sheets: Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func present_sheet(_ sheet: Sheets) {
|
||||||
|
notify(.present_sheet(sheet))
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let appDelegate: AppDelegate?
|
let appDelegate: AppDelegate?
|
||||||
@@ -877,7 +881,7 @@ func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [Nos
|
|||||||
|
|
||||||
|
|
||||||
func setup_notifications() {
|
func setup_notifications() {
|
||||||
UIApplication.shared.registerForRemoteNotifications()
|
this_app.registerForRemoteNotifications()
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
|
|
||||||
center.getNotificationSettings { settings in
|
center.getNotificationSettings { settings in
|
||||||
@@ -1109,7 +1113,7 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
|||||||
//let post = tup.0
|
//let post = tup.0
|
||||||
//let to_relays = tup.1
|
//let to_relays = tup.1
|
||||||
print("post \(post.content)")
|
print("post \(post.content)")
|
||||||
guard let new_ev = post_to_event(post: post, keypair: keypair) else {
|
guard let new_ev = post.to_event(keypair: keypair) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
postbox.send(new_ev)
|
postbox.send(new_ev)
|
||||||
@@ -1170,7 +1174,7 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
|||||||
}
|
}
|
||||||
case .hashtag(let ht):
|
case .hashtag(let ht):
|
||||||
result(.filter(.filter_hashtag([ht.hashtag])))
|
result(.filter(.filter_hashtag([ht.hashtag])))
|
||||||
case .param, .quote:
|
case .param, .quote, .reference:
|
||||||
// doesn't really make sense here
|
// doesn't really make sense here
|
||||||
break
|
break
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ public class CameraService: NSObject, Identifiable {
|
|||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.alertError = AlertError(title: "Camera Access", message: "Damus needs camera and microphone access. Enable in settings.", primaryButtonTitle: "Go to settings", secondaryButtonTitle: nil, primaryAction: {
|
self.alertError = AlertError(title: "Camera Access", message: "Damus needs camera and microphone access. Enable in settings.", primaryButtonTitle: "Go to settings", secondaryButtonTitle: nil, primaryAction: {
|
||||||
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!,
|
this_app.open(URL(string: UIApplication.openSettingsURLString)!,
|
||||||
options: [:], completionHandler: nil)
|
options: [:], completionHandler: nil)
|
||||||
|
|
||||||
}, secondaryAction: nil)
|
}, secondaryAction: nil)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// CommentItem.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2024-08-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CommentItem: TagConvertible {
|
||||||
|
static let TAG_KEY: String = "comment"
|
||||||
|
let content: String
|
||||||
|
var tag: [String] {
|
||||||
|
return [Self.TAG_KEY, content]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func from_tag(tag: TagSequence) -> CommentItem? {
|
||||||
|
guard tag.count == 2 else { return nil }
|
||||||
|
guard tag[0].string() == Self.TAG_KEY else { return nil }
|
||||||
|
|
||||||
|
return CommentItem(content: tag[1].string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,7 +109,7 @@ func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
|||||||
case let (.pubkey(pk), .pubkey(follow_pk)):
|
case let (.pubkey(pk), .pubkey(follow_pk)):
|
||||||
return pk == follow_pk
|
return pk == follow_pk
|
||||||
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||||
(.event, _), (.quote, _), (.param, _), (.naddr, _):
|
(.event, _), (.quote, _), (.param, _), (.naddr, _), (.reference(_), _):
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,79 @@ class DamusState: HeadlessDamusState {
|
|||||||
self.emoji_provider = emoji_provider
|
self.emoji_provider = emoji_provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
convenience init?(keypair: Keypair) {
|
||||||
|
// nostrdb
|
||||||
|
var mndb = Ndb()
|
||||||
|
if mndb == nil {
|
||||||
|
// try recovery
|
||||||
|
print("DB ISSUE! RECOVERING")
|
||||||
|
mndb = Ndb.safemode()
|
||||||
|
|
||||||
|
// out of space or something?? maybe we need a in-memory fallback
|
||||||
|
if mndb == nil {
|
||||||
|
logout(nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||||
|
let home: HomeModel = HomeModel()
|
||||||
|
let sub_id = UUID().uuidString
|
||||||
|
|
||||||
|
guard let ndb = mndb else { return nil }
|
||||||
|
let pubkey = keypair.pubkey
|
||||||
|
|
||||||
|
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
||||||
|
let model_cache = RelayModelCache()
|
||||||
|
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||||
|
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||||
|
|
||||||
|
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||||
|
|
||||||
|
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||||
|
for relay in bootstrap_relays {
|
||||||
|
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
||||||
|
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||||
|
|
||||||
|
if let nwc_str = settings.nostr_wallet_connect,
|
||||||
|
let nwc = WalletConnectURL(str: nwc_str) {
|
||||||
|
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||||
|
}
|
||||||
|
self.init(
|
||||||
|
pool: pool,
|
||||||
|
keypair: keypair,
|
||||||
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
|
boosts: EventCounter(our_pubkey: pubkey),
|
||||||
|
contacts: Contacts(our_pubkey: pubkey),
|
||||||
|
mutelist_manager: MutelistManager(user_keypair: keypair),
|
||||||
|
profiles: Profiles(ndb: ndb),
|
||||||
|
dms: home.dms,
|
||||||
|
previews: PreviewCache(),
|
||||||
|
zaps: Zaps(our_pubkey: pubkey),
|
||||||
|
lnurls: LNUrls(),
|
||||||
|
settings: settings,
|
||||||
|
relay_filters: relay_filters,
|
||||||
|
relay_model_cache: model_cache,
|
||||||
|
drafts: Drafts(),
|
||||||
|
events: EventCache(ndb: ndb),
|
||||||
|
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||||
|
postbox: PostBox(pool: pool),
|
||||||
|
bootstrap_relays: bootstrap_relays,
|
||||||
|
replies: ReplyCounter(our_pubkey: pubkey),
|
||||||
|
wallet: WalletModel(settings: settings),
|
||||||
|
nav: navigationCoordinator,
|
||||||
|
music: MusicController(onChange: { _ in }),
|
||||||
|
video: VideoController(),
|
||||||
|
ndb: ndb,
|
||||||
|
quote_reposts: .init(our_pubkey: pubkey),
|
||||||
|
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func add_zap(zap: Zapping) -> Bool {
|
func add_zap(zap: Zapping) -> Bool {
|
||||||
// store generic zap mapping
|
// store generic zap mapping
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ class Drafts: ObservableObject {
|
|||||||
@Published var post: DraftArtifacts? = nil
|
@Published var post: DraftArtifacts? = nil
|
||||||
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
|
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
|
||||||
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
|
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
|
||||||
|
@Published var highlights: [HighlightSource: DraftArtifacts] = [:]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,21 +14,202 @@ struct HighlightEvent {
|
|||||||
var url_ref: URL? = nil
|
var url_ref: URL? = nil
|
||||||
var context: String? = nil
|
var context: String? = nil
|
||||||
|
|
||||||
|
// MARK: - Initializers and parsers
|
||||||
|
|
||||||
static func parse(from ev: NostrEvent) -> HighlightEvent {
|
static func parse(from ev: NostrEvent) -> HighlightEvent {
|
||||||
var highlight = HighlightEvent(event: ev)
|
var highlight = HighlightEvent(event: ev)
|
||||||
|
|
||||||
|
var best_url_source: (url: URL, tagged_as_source: Bool)? = nil
|
||||||
|
|
||||||
for tag in ev.tags {
|
for tag in ev.tags {
|
||||||
guard tag.count >= 2 else { continue }
|
guard tag.count >= 2 else { continue }
|
||||||
switch tag[0].string() {
|
switch tag[0].string() {
|
||||||
case "e": highlight.event_ref = tag[1].string()
|
case "e": highlight.event_ref = tag[1].string()
|
||||||
case "a": highlight.event_ref = tag[1].string()
|
case "a": highlight.event_ref = tag[1].string()
|
||||||
case "r": highlight.url_ref = URL(string: tag[1].string())
|
case "r":
|
||||||
|
if tag.count >= 3,
|
||||||
|
tag[2].string() == HighlightSource.TAG_SOURCE_ELEMENT,
|
||||||
|
let url = URL(string: tag[1].string()) {
|
||||||
|
// URL marked as source. Very good candidate
|
||||||
|
best_url_source = (url: url, tagged_as_source: true)
|
||||||
|
}
|
||||||
|
else if tag.count >= 3 && tag[2].string() != HighlightSource.TAG_SOURCE_ELEMENT {
|
||||||
|
// URL marked as something else (not source). Not the source we are after
|
||||||
|
}
|
||||||
|
else if let url = URL(string: tag[1].string()), tag.count == 2 {
|
||||||
|
// Unmarked URL. This might be what we are after (For NIP-84 backwards compatibility)
|
||||||
|
if (best_url_source?.tagged_as_source ?? false) == false {
|
||||||
|
// No URL candidates marked as the source. Mark this as the best option we have
|
||||||
|
best_url_source = (url: url, tagged_as_source: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
case "context": highlight.context = tag[1].string()
|
case "context": highlight.context = tag[1].string()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let best_url_source {
|
||||||
|
highlight.url_ref = best_url_source.url
|
||||||
|
}
|
||||||
|
|
||||||
return highlight
|
return highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Getting information about source
|
||||||
|
|
||||||
|
func source_description_info(highlighted_event: NostrEvent?) -> ReplyDesc {
|
||||||
|
var others_count = 0
|
||||||
|
var highlighted_authors: [Pubkey] = []
|
||||||
|
var i = event.tags.count
|
||||||
|
|
||||||
|
if let highlighted_event {
|
||||||
|
highlighted_authors.append(highlighted_event.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in event.tags {
|
||||||
|
if let pubkey_with_role = PubkeyWithRole.from_tag(tag: tag) {
|
||||||
|
others_count += 1
|
||||||
|
if highlighted_authors.count < 2 {
|
||||||
|
if let highlighted_event, pubkey_with_role.pubkey == highlighted_event.pubkey {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
switch pubkey_with_role.role {
|
||||||
|
case .author:
|
||||||
|
highlighted_authors.append(pubkey_with_role.pubkey)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReplyDesc(pubkeys: highlighted_authors, others: others_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func source_description_text(ndb: Ndb, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String {
|
||||||
|
let description_info = self.source_description_info(highlighted_event: highlighted_event)
|
||||||
|
let pubkeys = description_info.pubkeys
|
||||||
|
|
||||||
|
let bundle = bundleForLocale(locale: locale)
|
||||||
|
|
||||||
|
if pubkeys.count == 0 {
|
||||||
|
return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let profile_txn = NdbTxn(ndb: ndb) else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let names: [String] = pubkeys.map { pk in
|
||||||
|
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn)
|
||||||
|
|
||||||
|
return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
let uniqueNames: [String] = Array(Set(names))
|
||||||
|
return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames.first ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper structures
|
||||||
|
|
||||||
|
extension HighlightEvent {
|
||||||
|
struct PubkeyWithRole: TagKey, TagConvertible {
|
||||||
|
let pubkey: Pubkey
|
||||||
|
let role: Role
|
||||||
|
|
||||||
|
var tag: [String] {
|
||||||
|
if let role_text = self.role.rawValue {
|
||||||
|
return [keychar.description, self.pubkey.hex(), role_text]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return [keychar.description, self.pubkey.hex()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var keychar: AsciiCharacter { "p" }
|
||||||
|
|
||||||
|
static func from_tag(tag: TagSequence) -> HighlightEvent.PubkeyWithRole? {
|
||||||
|
var i = tag.makeIterator()
|
||||||
|
|
||||||
|
guard tag.count >= 2,
|
||||||
|
let t0 = i.next(),
|
||||||
|
let key = t0.single_char,
|
||||||
|
key == "p",
|
||||||
|
let t1 = i.next(),
|
||||||
|
let pubkey = t1.id().map(Pubkey.init)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
let t3: String? = i.next()?.string()
|
||||||
|
let role = Role(rawValue: t3)
|
||||||
|
return PubkeyWithRole(pubkey: pubkey, role: role)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role: RawRepresentable {
|
||||||
|
case author
|
||||||
|
case editor
|
||||||
|
case mention
|
||||||
|
case other(String)
|
||||||
|
case no_role
|
||||||
|
|
||||||
|
typealias RawValue = String?
|
||||||
|
var rawValue: String? {
|
||||||
|
switch self {
|
||||||
|
case .author: "author"
|
||||||
|
case .editor: "editor"
|
||||||
|
case .mention: "mention"
|
||||||
|
case .other(let role): role
|
||||||
|
case .no_role: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(rawValue: String?) {
|
||||||
|
switch rawValue {
|
||||||
|
case "author": self = .author
|
||||||
|
case "editor": self = .editor
|
||||||
|
case "mention": self = .mention
|
||||||
|
default:
|
||||||
|
if let rawValue {
|
||||||
|
self = .other(rawValue)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self = .no_role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HighlightContentDraft: Hashable {
|
||||||
|
let selected_text: String
|
||||||
|
let source: HighlightSource
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HighlightSource: Hashable {
|
||||||
|
static let TAG_SOURCE_ELEMENT = "source"
|
||||||
|
case event(NostrEvent)
|
||||||
|
case external_url(URL)
|
||||||
|
|
||||||
|
func tags() -> [[String]] {
|
||||||
|
switch self {
|
||||||
|
case .event(let event):
|
||||||
|
return [ ["e", "\(event.id)", HighlightSource.TAG_SOURCE_ELEMENT] ]
|
||||||
|
case .external_url(let url):
|
||||||
|
return [ ["r", "\(url)", HighlightSource.TAG_SOURCE_ELEMENT] ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ref() -> RefId {
|
||||||
|
switch self {
|
||||||
|
case .event(let event):
|
||||||
|
return .event(event.id)
|
||||||
|
case .external_url(let url):
|
||||||
|
return .reference(url.absoluteString)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,46 +256,3 @@ func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PostTags {
|
|
||||||
let blocks: [Block]
|
|
||||||
let tags: [[String]]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert
|
|
||||||
func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
|
|
||||||
var new_tags = tags
|
|
||||||
|
|
||||||
for post_block in post_blocks {
|
|
||||||
switch post_block {
|
|
||||||
case .mention(let mention):
|
|
||||||
switch(mention.ref) {
|
|
||||||
case .note, .nevent:
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
new_tags.append(mention.ref.tag)
|
|
||||||
case .hashtag(let hashtag):
|
|
||||||
new_tags.append(["t", hashtag.lowercased()])
|
|
||||||
case .text: break
|
|
||||||
case .invoice: break
|
|
||||||
case .relay: break
|
|
||||||
case .url(let url):
|
|
||||||
new_tags.append(["r", url.absoluteString])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PostTags(blocks: post_blocks, tags: new_tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? {
|
|
||||||
let post_blocks = parse_post_blocks(content: post.content)
|
|
||||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: post.tags)
|
|
||||||
let content = post_tags.blocks
|
|
||||||
.map(\.asString)
|
|
||||||
.joined(separator: "")
|
|
||||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: post.kind.rawValue, tags: post_tags.tags)
|
|
||||||
}
|
|
||||||
|
|||||||
+75
-1
@@ -17,10 +17,84 @@ struct NostrPost {
|
|||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func to_event(keypair: FullKeypair) -> NostrEvent? {
|
||||||
|
let post_blocks = self.parse_blocks()
|
||||||
|
let post_tags = self.make_post_tags(post_blocks: post_blocks, tags: self.tags)
|
||||||
|
let content = post_tags.blocks
|
||||||
|
.map(\.asString)
|
||||||
|
.joined(separator: "")
|
||||||
|
|
||||||
|
if self.kind == .highlight {
|
||||||
|
var new_tags = post_tags.tags.filter({ $0[safe: 0] != "comment" })
|
||||||
|
if content.count > 0 {
|
||||||
|
new_tags.append(["comment", content])
|
||||||
|
}
|
||||||
|
return NostrEvent(content: self.content, keypair: keypair.to_keypair(), kind: self.kind.rawValue, tags: new_tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: self.kind.rawValue, tags: post_tags.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse_blocks() -> [Block] {
|
||||||
|
guard let content_for_parsing = self.default_content_for_block_parsing() else { return [] }
|
||||||
|
return parse_post_blocks(content: content_for_parsing)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func default_content_for_block_parsing() -> String? {
|
||||||
|
switch kind {
|
||||||
|
case .highlight:
|
||||||
|
return tags.filter({ $0[safe: 0] == "comment" }).first?[safe: 1]
|
||||||
|
default:
|
||||||
|
return self.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the post's contents to find more tags to apply to the final nostr event
|
||||||
|
private func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
|
||||||
|
var new_tags = tags
|
||||||
|
|
||||||
|
for post_block in post_blocks {
|
||||||
|
switch post_block {
|
||||||
|
case .mention(let mention):
|
||||||
|
switch(mention.ref) {
|
||||||
|
case .note, .nevent:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.kind == .highlight, case .pubkey(_) = mention.ref {
|
||||||
|
var new_tag = mention.ref.tag
|
||||||
|
new_tag.append("mention")
|
||||||
|
new_tags.append(new_tag)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
new_tags.append(mention.ref.tag)
|
||||||
|
}
|
||||||
|
case .hashtag(let hashtag):
|
||||||
|
new_tags.append(["t", hashtag.lowercased()])
|
||||||
|
case .text: break
|
||||||
|
case .invoice: break
|
||||||
|
case .relay: break
|
||||||
|
case .url(let url):
|
||||||
|
new_tags.append(self.kind == .highlight ? ["r", url.absoluteString, "mention"] : ["r", url.absoluteString])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PostTags(blocks: post_blocks, tags: new_tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper structures and functions
|
||||||
|
|
||||||
|
/// A struct used for temporarily holding tag information that was parsed from a post contents to aid in building a nostr event
|
||||||
|
fileprivate struct PostTags {
|
||||||
|
let blocks: [Block]
|
||||||
|
let tags: [[String]]
|
||||||
|
}
|
||||||
|
|
||||||
/// Return a list of tags
|
|
||||||
func parse_post_blocks(content: String) -> [Block] {
|
func parse_post_blocks(content: String) -> [Block] {
|
||||||
return parse_note_content(content: .content(content, nil)).blocks
|
return parse_note_content(content: .content(content, nil)).blocks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case hashtag(Hashtag)
|
case hashtag(Hashtag)
|
||||||
case param(TagElem)
|
case param(TagElem)
|
||||||
case naddr(NAddr)
|
case naddr(NAddr)
|
||||||
|
case reference(String)
|
||||||
|
|
||||||
var key: RefKey {
|
var key: RefKey {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -131,11 +132,12 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case .hashtag: return .t
|
case .hashtag: return .t
|
||||||
case .param: return .d
|
case .param: return .d
|
||||||
case .naddr: return .a
|
case .naddr: return .a
|
||||||
|
case .reference: return .r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
||||||
case e, p, t, d, q, a
|
case e, p, t, d, q, a, r
|
||||||
|
|
||||||
var keychar: AsciiCharacter {
|
var keychar: AsciiCharacter {
|
||||||
self.rawValue
|
self.rawValue
|
||||||
@@ -159,6 +161,8 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case .param(let string): return string.string()
|
case .param(let string): return string.string()
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier
|
return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier
|
||||||
|
case .reference(let string):
|
||||||
|
return string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +183,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case .t: return .hashtag(Hashtag(hashtag: t1.string()))
|
case .t: return .hashtag(Hashtag(hashtag: t1.string()))
|
||||||
case .d: return .param(t1)
|
case .d: return .param(t1)
|
||||||
case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0))
|
case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0))
|
||||||
|
case .r: return .reference(t1.string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// DamusAliases.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2024-08-12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
let this_app: UIApplication = UIApplication.shared
|
||||||
@@ -30,7 +30,7 @@ public struct DismissKeyboardOnTap: ViewModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func end_editing() {
|
public func end_editing() {
|
||||||
UIApplication.shared.connectedScenes
|
this_app.connectedScenes
|
||||||
.filter {$0.activationState == .foregroundActive}
|
.filter {$0.activationState == .foregroundActive}
|
||||||
.map {$0 as? UIWindowScene}
|
.map {$0 as? UIWindowScene}
|
||||||
.compactMap({$0})
|
.compactMap({$0})
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import UIKit
|
|||||||
class Theme {
|
class Theme {
|
||||||
|
|
||||||
static var safeAreaInsets: UIEdgeInsets? {
|
static var safeAreaInsets: UIEdgeInsets? {
|
||||||
return UIApplication
|
return this_app
|
||||||
.shared
|
|
||||||
.connectedScenes
|
.connectedScenes
|
||||||
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
|
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
|
||||||
.first { $0.isKeyWindow }?.safeAreaInsets
|
.first { $0.isKeyWindow }?.safeAreaInsets
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ struct AddRelayView: View {
|
|||||||
}
|
}
|
||||||
new_relay = ""
|
new_relay = ""
|
||||||
|
|
||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// blame the porn bots for this code too
|
||||||
|
func should_blur_images(damus_state: DamusState, ev: NostrEvent) -> Bool {
|
||||||
|
return should_blur_images(
|
||||||
|
settings: damus_state.settings,
|
||||||
|
contacts: damus_state.contacts,
|
||||||
|
ev: ev,
|
||||||
|
our_pubkey: damus_state.pubkey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func format_relative_time(_ created_at: UInt32) -> String
|
func format_relative_time(_ created_at: UInt32) -> String
|
||||||
{
|
{
|
||||||
return time_ago_since(Date(timeIntervalSince1970: Double(created_at)))
|
return time_ago_since(Date(timeIntervalSince1970: Double(created_at)))
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ struct ReplyPart: View {
|
|||||||
Group {
|
Group {
|
||||||
if event.known_kind == .highlight {
|
if event.known_kind == .highlight {
|
||||||
let highlighted_note = event.highlighted_note_id().flatMap { events.lookup($0) }
|
let highlighted_note = event.highlighted_note_id().flatMap { events.lookup($0) }
|
||||||
HighlightDescription(event: event, highlighted_event: highlighted_note, ndb: ndb)
|
let highlight_note = HighlightEvent.parse(from: event)
|
||||||
|
HighlightDescription(highlight_event: highlight_note, highlighted_event: highlighted_note, ndb: ndb)
|
||||||
} else if let reply_ref = event.thread_reply()?.reply {
|
} else if let reply_ref = event.thread_reply()?.reply {
|
||||||
let replying_to = events.lookup(reply_ref.note_id)
|
let replying_to = events.lookup(reply_ref.note_id)
|
||||||
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
|
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import SwiftUI
|
|||||||
|
|
||||||
// Modified from Reply Description
|
// Modified from Reply Description
|
||||||
struct HighlightDescription: View {
|
struct HighlightDescription: View {
|
||||||
let event: NostrEvent
|
let highlight_event: HighlightEvent
|
||||||
let highlighted_event: NostrEvent?
|
let highlighted_event: NostrEvent?
|
||||||
let ndb: Ndb
|
let ndb: Ndb
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
(Text(Image(systemName: "highlighter")) + Text(verbatim: " \(highlight_desc(ndb: ndb, event: event, highlighted_event: highlighted_event))"))
|
(Text(Image(systemName: "highlighter")) + Text(verbatim: " \(highlight_event.source_description_text(ndb: ndb, highlighted_event: highlighted_event))"))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -24,30 +24,6 @@ struct HighlightDescription: View {
|
|||||||
|
|
||||||
struct HighlightDescription_Previews: PreviewProvider {
|
struct HighlightDescription_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
HighlightDescription(event: test_note, highlighted_event: test_note, ndb: test_damus_state.ndb)
|
HighlightDescription(highlight_event: HighlightEvent.parse(from: test_note), highlighted_event: nil, ndb: test_damus_state.ndb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func highlight_desc(ndb: Ndb, event: NostrEvent, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String {
|
|
||||||
let desc = make_reply_description(event, replying_to: highlighted_event)
|
|
||||||
let pubkeys = desc.pubkeys
|
|
||||||
|
|
||||||
let bundle = bundleForLocale(locale: locale)
|
|
||||||
|
|
||||||
if pubkeys.count == 0 {
|
|
||||||
return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let profile_txn = NdbTxn(ndb: ndb) else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
let names: [String] = pubkeys.map { pk in
|
|
||||||
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn)
|
|
||||||
|
|
||||||
return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
let uniqueNames: [String] = Array(Set(names))
|
|
||||||
return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames.first ?? "")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// HighlightDraftContentView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by eric on 5/26/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct HighlightDraftContentView: View {
|
||||||
|
let draft: HighlightContentDraft
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
var attributedString: AttributedString {
|
||||||
|
var attributedString = AttributedString(draft.selected_text)
|
||||||
|
|
||||||
|
if let range = attributedString.range(of: draft.selected_text) {
|
||||||
|
attributedString[range].backgroundColor = DamusColors.highlight
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributedString
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(attributedString)
|
||||||
|
.lineSpacing(5)
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
||||||
|
alignment: .leading
|
||||||
|
)
|
||||||
|
|
||||||
|
if case .external_url(let url) = draft.source {
|
||||||
|
LinkViewRepresentable(meta: .url(url))
|
||||||
|
.frame(height: 50)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
//
|
|
||||||
// HighlightPostView.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by eric on 5/26/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct HighlightPostView: View {
|
|
||||||
let damus_state: DamusState
|
|
||||||
let event: NostrEvent
|
|
||||||
@Binding var selectedText: String
|
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
VStack {
|
|
||||||
HStack(spacing: 5.0) {
|
|
||||||
Button(action: {
|
|
||||||
dismiss()
|
|
||||||
}, label: {
|
|
||||||
Text("Cancel", comment: "Button to cancel out of highlighting a note.")
|
|
||||||
.padding(10)
|
|
||||||
})
|
|
||||||
.buttonStyle(NeutralButtonStyle())
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(NSLocalizedString("Post", comment: "Button to post a highlight.")) {
|
|
||||||
let tags: [[String]] = [ ["e", "\(self.event.id)"] ]
|
|
||||||
|
|
||||||
let kind = NostrKind.highlight.rawValue
|
|
||||||
guard let ev = NostrEvent(content: selectedText, keypair: damus_state.keypair, kind: kind, tags: tags) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
damus_state.postbox.send(ev)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.bold()
|
|
||||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.foregroundColor(DamusColors.neutral3)
|
|
||||||
.padding(.top, 5)
|
|
||||||
}
|
|
||||||
.frame(height: 30)
|
|
||||||
.padding()
|
|
||||||
.padding(.top, 15)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
var attributedString: AttributedString {
|
|
||||||
var attributedString = AttributedString(selectedText)
|
|
||||||
|
|
||||||
if let range = attributedString.range(of: selectedText) {
|
|
||||||
attributedString[range].backgroundColor = DamusColors.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributedString
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(attributedString)
|
|
||||||
.lineSpacing(5)
|
|
||||||
.padding(10)
|
|
||||||
}
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
|
||||||
alignment: .leading
|
|
||||||
)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -59,9 +59,9 @@ struct HighlightBodyView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if options.contains(.wide) {
|
if options.contains(.wide) {
|
||||||
Main.padding(.horizontal)
|
|
||||||
} else {
|
|
||||||
Main
|
Main
|
||||||
|
} else {
|
||||||
|
Main.padding(.horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +92,18 @@ struct HighlightBodyView: View {
|
|||||||
|
|
||||||
var Main: some View {
|
var Main: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
|
||||||
|
if self.event.event.referenced_comment_items.first?.content != nil {
|
||||||
|
let all_options = options.union(.no_action_bar)
|
||||||
|
NoteContentView(
|
||||||
|
damus_state: self.state,
|
||||||
|
event: self.event.event,
|
||||||
|
blur_images: should_blur_images(damus_state: self.state, ev: self.event.event),
|
||||||
|
size: .normal,
|
||||||
|
options: all_options
|
||||||
|
).padding(.vertical, 10)
|
||||||
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
var attributedString: AttributedString {
|
var attributedString: AttributedString {
|
||||||
var attributedString: AttributedString = ""
|
var attributedString: AttributedString = ""
|
||||||
@@ -119,14 +131,17 @@ struct HighlightBodyView: View {
|
|||||||
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
||||||
alignment: .leading
|
alignment: .leading
|
||||||
)
|
)
|
||||||
|
.padding(.horizontal)
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
|
|
||||||
if let url = event.url_ref {
|
if let url = event.url_ref {
|
||||||
HighlightLink(state: state, url: url, content: event.event.content)
|
HighlightLink(state: state, url: url, content: event.event.content)
|
||||||
|
.padding(.horizontal)
|
||||||
} else {
|
} else {
|
||||||
if let evRef = event.event_ref {
|
if let evRef = event.event_ref {
|
||||||
if let eventHex = hex_decode_id(evRef) {
|
if let eventHex = hex_decode_id(evRef) {
|
||||||
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
|
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
|
||||||
|
.padding(.horizontal)
|
||||||
.padding(.top, 5)
|
.padding(.top, 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
|
|||||||
|
|
||||||
new_text = ""
|
new_text = ""
|
||||||
|
|
||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ struct DamusAppNotificationView: View {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func open_url(url: URL) {
|
func open_url(url: URL) {
|
||||||
UIApplication.shared.open(url)
|
this_app.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ enum PostAction {
|
|||||||
case replying_to(NostrEvent)
|
case replying_to(NostrEvent)
|
||||||
case quoting(NostrEvent)
|
case quoting(NostrEvent)
|
||||||
case posting(PostTarget)
|
case posting(PostTarget)
|
||||||
|
case highlighting(HighlightContentDraft)
|
||||||
|
|
||||||
var ev: NostrEvent? {
|
var ev: NostrEvent? {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -39,6 +40,8 @@ enum PostAction {
|
|||||||
return ev
|
return ev
|
||||||
case .posting:
|
case .posting:
|
||||||
return nil
|
return nil
|
||||||
|
case .highlighting:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,8 +131,13 @@ struct PostView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var posting_disabled: Bool {
|
var posting_disabled: Bool {
|
||||||
|
switch action {
|
||||||
|
case .highlighting(_):
|
||||||
|
return false
|
||||||
|
default:
|
||||||
return is_post_empty || uploading_disabled
|
return is_post_empty || uploading_disabled
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Returns a valid height for the text box, even when textHeight is not a number
|
// Returns a valid height for the text box, even when textHeight is not a number
|
||||||
func get_valid_text_height() -> CGFloat {
|
func get_valid_text_height() -> CGFloat {
|
||||||
@@ -204,6 +212,8 @@ struct PostView: View {
|
|||||||
damus_state.drafts.quotes.removeValue(forKey: quoting)
|
damus_state.drafts.quotes.removeValue(forKey: quoting)
|
||||||
case .posting:
|
case .posting:
|
||||||
damus_state.drafts.post = nil
|
damus_state.drafts.post = nil
|
||||||
|
case .highlighting(let draft):
|
||||||
|
damus_state.drafts.highlights.removeValue(forKey: draft.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -371,6 +381,9 @@ struct PostView: View {
|
|||||||
if case .quoting(let ev) = action {
|
if case .quoting(let ev) = action {
|
||||||
BuilderEventView(damus: damus_state, event: ev)
|
BuilderEventView(damus: damus_state, event: ev)
|
||||||
}
|
}
|
||||||
|
else if case .highlighting(let draft) = action {
|
||||||
|
HighlightDraftContentView(draft: draft)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
@@ -460,8 +473,9 @@ struct PostView: View {
|
|||||||
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
|
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
|
||||||
case .posting(let target):
|
case .posting(let target):
|
||||||
guard !loaded_draft else { break }
|
guard !loaded_draft else { break }
|
||||||
|
|
||||||
fill_target_content(target: target)
|
fill_target_content(target: target)
|
||||||
|
case .highlighting(let draft):
|
||||||
|
references = [draft.source.ref()]
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
@@ -597,6 +611,8 @@ func set_draft_for_post(drafts: Drafts, action: PostAction, artifacts: DraftArti
|
|||||||
drafts.quotes[ev] = artifacts
|
drafts.quotes[ev] = artifacts
|
||||||
case .posting:
|
case .posting:
|
||||||
drafts.post = artifacts
|
drafts.post = artifacts
|
||||||
|
case .highlighting(let draft):
|
||||||
|
drafts.highlights[draft.source] = artifacts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +624,8 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts?
|
|||||||
return drafts.quotes[ev]
|
return drafts.quotes[ev]
|
||||||
case .posting:
|
case .posting:
|
||||||
return drafts.post
|
return drafts.post
|
||||||
|
case .highlighting(let draft):
|
||||||
|
return drafts.highlights[draft.source]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,16 +699,29 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post
|
|||||||
}
|
}
|
||||||
case .posting(let postTarget):
|
case .posting(let postTarget):
|
||||||
break
|
break
|
||||||
}
|
case .highlighting(let draft):
|
||||||
|
break
|
||||||
// include pubkeys
|
|
||||||
tags += pubkeys.map { pk in
|
|
||||||
["p", pk.hex()]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// append additional tags
|
// append additional tags
|
||||||
tags += uploadedMedias.compactMap { $0.metadata?.to_tag() }
|
tags += uploadedMedias.compactMap { $0.metadata?.to_tag() }
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .highlighting(let draft):
|
||||||
|
tags.append(contentsOf: draft.source.tags())
|
||||||
|
if !(content.isEmpty || content.allSatisfy { $0.isWhitespace }) {
|
||||||
|
tags.append(["comment", content])
|
||||||
|
}
|
||||||
|
tags += pubkeys.map { pk in
|
||||||
|
["p", pk.hex(), "mention"]
|
||||||
|
}
|
||||||
|
return NostrPost(content: draft.selected_text, kind: .highlight, tags: tags)
|
||||||
|
default:
|
||||||
|
tags += pubkeys.map { pk in
|
||||||
|
["p", pk.hex()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NostrPost(content: content, kind: .text, tags: tags)
|
return NostrPost(content: content, kind: .text, tags: tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ struct ZapSheetViewIfPossible: View {
|
|||||||
extension View {
|
extension View {
|
||||||
func hideKeyboard() {
|
func hideKeyboard() {
|
||||||
let resign = #selector(UIResponder.resignFirstResponder)
|
let resign = #selector(UIResponder.resignFirstResponder)
|
||||||
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
|
this_app.sendAction(resign, to: nil, from: nil, for: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ class ReplyTests: XCTestCase {
|
|||||||
let content = "this is a @\(pk.npub) mention"
|
let content = "this is a @\(pk.npub) mention"
|
||||||
let blocks = parse_post_blocks(content: content)
|
let blocks = parse_post_blocks(content: content)
|
||||||
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
|
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
|
||||||
let ev = post_to_event(post: post, keypair: test_keypair_full)!
|
let ev = post.to_event(keypair: test_keypair_full)!
|
||||||
|
|
||||||
XCTAssertEqual(ev.tags.count, 2)
|
XCTAssertEqual(ev.tags.count, 2)
|
||||||
XCTAssertEqual(blocks.count, 3)
|
XCTAssertEqual(blocks.count, 3)
|
||||||
@@ -255,7 +255,7 @@ class ReplyTests: XCTestCase {
|
|||||||
let content = "this is a @\(nsec) mention"
|
let content = "this is a @\(nsec) mention"
|
||||||
let blocks = parse_post_blocks(content: content)
|
let blocks = parse_post_blocks(content: content)
|
||||||
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
|
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
|
||||||
let ev = post_to_event(post: post, keypair: test_keypair_full)!
|
let ev = post.to_event(keypair: test_keypair_full)!
|
||||||
|
|
||||||
XCTAssertEqual(ev.tags.count, 2)
|
XCTAssertEqual(ev.tags.count, 2)
|
||||||
XCTAssertEqual(blocks.count, 3)
|
XCTAssertEqual(blocks.count, 3)
|
||||||
@@ -275,7 +275,7 @@ class ReplyTests: XCTestCase {
|
|||||||
]
|
]
|
||||||
|
|
||||||
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", tags: tags)
|
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", tags: tags)
|
||||||
let ev = post_to_event(post: post, keypair: test_keypair_full)!
|
let ev = post.to_event(keypair: test_keypair_full)!
|
||||||
|
|
||||||
XCTAssertEqual(ev.content, "this is a (nostr:\(pubkey.npub)) mention")
|
XCTAssertEqual(ev.content, "this is a (nostr:\(pubkey.npub)) mention")
|
||||||
XCTAssertEqual(ev.tags[2][1].string(), pubkey.description)
|
XCTAssertEqual(ev.tags[2][1].string(), pubkey.description)
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class damusTests: XCTestCase {
|
|||||||
|
|
||||||
func testMakeHashtagPost() {
|
func testMakeHashtagPost() {
|
||||||
let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", tags: [])
|
let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", tags: [])
|
||||||
let ev = post_to_event(post: post, keypair: test_keypair_full)!
|
let ev = post.to_event(keypair: test_keypair_full)!
|
||||||
|
|
||||||
XCTAssertEqual(ev.tags.count, 3)
|
XCTAssertEqual(ev.tags.count, 3)
|
||||||
XCTAssertEqual(ev.content, "#damus some content #bitcoin derp #かっこいい wow")
|
XCTAssertEqual(ev.content, "#damus some content #bitcoin derp #かっこいい wow")
|
||||||
@@ -270,7 +270,7 @@ class damusTests: XCTestCase {
|
|||||||
|
|
||||||
private func createEventFromContentString(_ content: String) -> NostrEvent {
|
private func createEventFromContentString(_ content: String) -> NostrEvent {
|
||||||
let post = NostrPost(content: content, tags: [])
|
let post = NostrPost(content: content, tags: [])
|
||||||
guard let ev = post_to_event(post: post, keypair: test_keypair_full) else {
|
guard let ev = post.to_event(keypair: test_keypair_full) else {
|
||||||
XCTFail("Could not create event")
|
XCTFail("Could not create event")
|
||||||
return test_note
|
return test_note
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
//
|
||||||
|
// ActionViewController.swift
|
||||||
|
// highlighter action extension
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2024-08-09.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MobileCoreServices
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ShareExtensionView: View {
|
||||||
|
@State var highlighter_state: HighlighterState = .loading
|
||||||
|
let extensionContext: NSExtensionContext
|
||||||
|
@State var state: DamusState? = nil
|
||||||
|
@State var signedEvent: String? = nil
|
||||||
|
|
||||||
|
@State private var selectedText = ""
|
||||||
|
@State private var selectedTextHeight: CGFloat = .zero
|
||||||
|
@State private var selectedTextWidth: CGFloat = .zero
|
||||||
|
|
||||||
|
@Environment(\.scenePhase) var scenePhase
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
if let state {
|
||||||
|
switch self.highlighter_state {
|
||||||
|
case .loading:
|
||||||
|
ProgressView()
|
||||||
|
case .no_highlight_text:
|
||||||
|
Group {
|
||||||
|
Text("No text selected", comment: "Title indicating that a highlight cannot be posted because no text was selected.")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
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")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
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 post a highlight")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .not_logged_in:
|
||||||
|
Group {
|
||||||
|
Text("Not logged in", comment: "Title indicating that a highlight cannot be posted because the user is not logged in.")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
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")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
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 post a highlight")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .loaded(let highlighted_text, let source_url):
|
||||||
|
PostView(
|
||||||
|
action: .highlighting(HighlightContentDraft(selected_text: highlighted_text, source: .external_url(source_url))),
|
||||||
|
damus_state: state
|
||||||
|
)
|
||||||
|
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: {
|
||||||
|
self.done()
|
||||||
|
}, label: {
|
||||||
|
Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .posted(event: let event):
|
||||||
|
Group {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
Text("Posted", comment: "Title indicating that the user has posted a highlight 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 posting a highlight")
|
||||||
|
})
|
||||||
|
.buttonStyle(GradientButtonStyle())
|
||||||
|
Button(action: {
|
||||||
|
self.done()
|
||||||
|
}, label: {
|
||||||
|
Text("Close", comment: "Button label giving the user the option to close the sheet from which they posted a highlight")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
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 post a highlight")
|
||||||
|
})
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
case .posting:
|
||||||
|
Group {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
Text("Posting", comment: "Title indicating that the highlight post is being published to the network")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.bottom)
|
||||||
|
Text("Your highlight is being broadcasted to the network. Please wait.", comment: "Label explaining there their highlight publishing action is in progress")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear(perform: {
|
||||||
|
self.loadSharedUrl()
|
||||||
|
guard let keypair = get_saved_keypair() else { return }
|
||||||
|
guard keypair.privkey != nil else {
|
||||||
|
self.highlighter_state = .not_logged_in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.state = DamusState(keypair: keypair)
|
||||||
|
})
|
||||||
|
.onChange(of: self.highlighter_state) {
|
||||||
|
if case .cancelled = highlighter_state {
|
||||||
|
self.done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.post)) { post_notification in
|
||||||
|
switch post_notification {
|
||||||
|
case .post(let post):
|
||||||
|
self.post(post)
|
||||||
|
case .cancel:
|
||||||
|
self.highlighter_state = .cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||||
|
guard let state else { return }
|
||||||
|
switch phase {
|
||||||
|
case .background:
|
||||||
|
print("txn: 📙 HIGHLIGHTER BACKGROUNDED")
|
||||||
|
Task { @MainActor in
|
||||||
|
state.ndb.close()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .inactive:
|
||||||
|
print("txn: 📙 HIGHLIGHTER INACTIVE")
|
||||||
|
break
|
||||||
|
case .active:
|
||||||
|
print("txn: 📙 HIGHLIGHTER ACTIVE")
|
||||||
|
state.pool.ping()
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
||||||
|
guard let state else { return }
|
||||||
|
print("txn: 📙 HIGHLIGHTER ACTIVE NOTIFY")
|
||||||
|
if state.ndb.reopen() {
|
||||||
|
print("txn: HIGHLIGHTER NOSTRDB REOPENED")
|
||||||
|
} else {
|
||||||
|
print("txn: HIGHLIGHTER 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: 📙 HIGHLIGHTER BACKGROUNDED")
|
||||||
|
Task { @MainActor in
|
||||||
|
state.ndb.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSharedUrl() {
|
||||||
|
guard
|
||||||
|
let extensionItem = extensionContext.inputItems.first as? NSExtensionItem,
|
||||||
|
let itemProvider = extensionItem.attachments?.first else {
|
||||||
|
self.highlighter_state = .failed(error: "Can't get itemProvider")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let propertyList = UTType.propertyList.identifier
|
||||||
|
if itemProvider.hasItemConformingToTypeIdentifier(propertyList) {
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in
|
||||||
|
guard let dictionary = item as? NSDictionary else { return }
|
||||||
|
if error != nil {
|
||||||
|
self.highlighter_state = .failed(error: "Error loading plist item: \(error?.localizedDescription ?? "Unknown")")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
OperationQueue.main.addOperation {
|
||||||
|
if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary,
|
||||||
|
let urlString = results["URL"] as? String,
|
||||||
|
let selection = results["selectedText"] as? String,
|
||||||
|
let url = URL(string: urlString) {
|
||||||
|
guard selection != "" else {
|
||||||
|
self.highlighter_state = .no_highlight_text
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.highlighter_state = .loaded(highlighted_text: selection, source_url: url)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.highlighter_state = .failed(error: "Cannot load results")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.highlighter_state = .failed(error: "No plist detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func post(_ post: NostrPost) {
|
||||||
|
self.highlighter_state = .posting
|
||||||
|
guard let state else {
|
||||||
|
self.highlighter_state = .failed(error: "Damus state not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let full_keypair = state.keypair.to_full() else {
|
||||||
|
self.highlighter_state = .not_logged_in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let posted_event = post.to_event(keypair: full_keypair) else {
|
||||||
|
self.highlighter_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.highlighter_state = .posted(event: flushed_event.event)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.highlighter_state = .failed(error: "Flushed event is not the event we just tried to post.")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func done() {
|
||||||
|
self.extensionContext.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HighlighterState: Equatable {
|
||||||
|
case loading
|
||||||
|
case no_highlight_text
|
||||||
|
case not_logged_in
|
||||||
|
case loaded(highlighted_text: String, source_url: URL)
|
||||||
|
case posting
|
||||||
|
case posted(event: NostrEvent)
|
||||||
|
case cancelled
|
||||||
|
case failed(error: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionViewController: UIViewController {
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.view.tintColor = UIColor(DamusColors.purple)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let contentView = UIHostingController(rootView: ShareExtensionView(extensionContext: self.extensionContext!))
|
||||||
|
self.addChild(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// HighlighterExtensionAliases.swift
|
||||||
|
// highlighter action extension
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2024-08-12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
let this_app: UIApplication = UIApplication()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?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>NSExtensionJavaScriptPreprocessingFile</key>
|
||||||
|
<string>getSelection</string>
|
||||||
|
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExtensionServiceAllowsTouchBarItem</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExtensionServiceFinderPreviewIconName</key>
|
||||||
|
<string>NSActionTemplate</string>
|
||||||
|
<key>NSExtensionServiceTouchBarBezelColorName</key>
|
||||||
|
<string>TouchBarBezel</string>
|
||||||
|
<key>NSExtensionServiceTouchBarIconName</key>
|
||||||
|
<string>NSActionTemplate</string>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.ui-services</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).ActionViewController</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "highlighter.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
},
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"color" : {
|
||||||
|
"reference" : "systemPurpleColor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
var Share = function() {};
|
||||||
|
|
||||||
|
Share.prototype = {
|
||||||
|
run: function(arguments) {
|
||||||
|
arguments.completionFunction({"URL": document.URL, "selectedText": document.getSelection().toString()});
|
||||||
|
},
|
||||||
|
finalize: function(arguments) {
|
||||||
|
// alert shared!
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var ExtensionPreprocessingJS = new Share
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?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.developer.kernel.extended-virtual-addressing</key>
|
||||||
|
<true/>
|
||||||
|
<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>
|
||||||
@@ -336,6 +336,10 @@ extension NdbNote {
|
|||||||
References<MuteItem>(tags: self.tags)
|
References<MuteItem>(tags: self.tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var referenced_comment_items: References<CommentItem> {
|
||||||
|
References<CommentItem>(tags: self.tags)
|
||||||
|
}
|
||||||
|
|
||||||
public var references: References<RefId> {
|
public var references: References<RefId> {
|
||||||
References<RefId>(tags: self.tags)
|
References<RefId>(tags: self.tags)
|
||||||
}
|
}
|
||||||
@@ -355,6 +359,9 @@ extension NdbNote {
|
|||||||
if known_kind == .dm {
|
if known_kind == .dm {
|
||||||
return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
|
return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
|
||||||
}
|
}
|
||||||
|
else if known_kind == .highlight {
|
||||||
|
return self.referenced_comment_items.first?.content ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -4893,8 +4893,17 @@ mdb_env_setup_locks(MDB_env *env, MDB_name *fname, int mode, int *excl)
|
|||||||
#ifdef MDB_SHORT_SEMNAMES
|
#ifdef MDB_SHORT_SEMNAMES
|
||||||
encbuf[9] = '\0'; /* drop name from 15 chars to 14 chars */
|
encbuf[9] = '\0'; /* drop name from 15 chars to 14 chars */
|
||||||
#endif
|
#endif
|
||||||
sprintf(env->me_txns->mti_rmname, "/MDBr%s", encbuf);
|
|
||||||
sprintf(env->me_txns->mti_wmname, "/MDBw%s", encbuf);
|
#define DEF_STR(x) #x
|
||||||
|
#define DEF_TO_STRING(x) DEF_STR(x)
|
||||||
|
sprintf(env->me_txns->mti_rmname, DEF_TO_STRING(MDB_SEM_NAME_PREFIX) "/MDBr%s", encbuf);
|
||||||
|
sprintf(env->me_txns->mti_wmname, DEF_TO_STRING(MDB_SEM_NAME_PREFIX) "/MDBw%s", encbuf);
|
||||||
|
#undef DEF_STR
|
||||||
|
#undef DEF_TO_STRING
|
||||||
|
|
||||||
|
printf("mdb_env_setup_locks: using semnames '%s' (%d), '%s' (%d)\n",
|
||||||
|
env->me_txns->mti_rmname, strlen(env->me_txns->mti_rmname),
|
||||||
|
env->me_txns->mti_wmname, strlen(env->me_txns->mti_wmname));
|
||||||
/* Clean up after a previous run, if needed: Try to
|
/* Clean up after a previous run, if needed: Try to
|
||||||
* remove both semaphores before doing anything else.
|
* remove both semaphores before doing anything else.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user