Merge Highlighter

This brings Daniel's highlighter safari extension to master/testflight.
Previously we only had it on the 1.10 release branch. This also includes
some extended virtual addressing fixes to fix push notifications, we
also update the push notification server address since that seems to
have been missed.

Daniel D’Aquino (8):
      Update push notification server address
      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 (5):
      lmdb: patch semaphore names to use shared group container prefix
      Revert "ux: Mute selected text"
      notifications: add extended virtual addressing entitlement
      highlighter: add extended virtual addressing entitlement
This commit is contained in:
William Casarin
2024-09-01 07:26:57 -07:00
46 changed files with 2284 additions and 275 deletions
@@ -2,6 +2,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>
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>
+4 -9
View File
@@ -94,8 +94,8 @@ enum OpenWalletError: Error {
}
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
if let url = URL(string: "\(wallet.link)\(invoice)"), this_app.canOpenURL(url) {
this_app.open(url)
} else {
guard let store_link = wallet.appStoreLink else {
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
}
guard UIApplication.shared.canOpenURL(url) else {
guard this_app.canOpenURL(url) else {
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)
}
}
func present_sheet(_ sheet: Sheets) {
notify(.present_sheet(sheet))
}
+45 -42
View File
@@ -13,9 +13,7 @@ struct SelectableText: View {
let event: NostrEvent?
let attributedString: AttributedString
let textAlignment: NSTextAlignment
@State private var showHighlightPost = false
@State private var showMutePost = false
@State private var selectedText = ""
@State private var highlightPostingState: HighlightPostingState = .hide
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
@@ -38,9 +36,9 @@ struct SelectableText: View {
fixedWidth: selectedTextWidth,
textAlignment: self.textAlignment,
enableHighlighting: self.enableHighlighting(),
showHighlightPost: $showHighlightPost,
showMutePost: $showMutePost,
selectedText: $selectedText,
postHighlight: { selectedText in
self.highlightPostingState = .show_post_view(highlighted_text: selectedText)
},
height: $selectedTextHeight
)
.padding([.leading, .trailing], -1.0)
@@ -55,17 +53,19 @@ struct SelectableText: View {
self.selectedTextWidth = newSize.width
}
}
.sheet(isPresented: $showHighlightPost) {
if let event {
HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText)
.presentationDragIndicator(.visible)
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
}
}
.sheet(isPresented: $showMutePost) {
AddMuteItemView(state: damus_state, new_text: $selectedText)
.sheet(isPresented: Binding(get: {
return self.highlightPostingState.show()
}, 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)
.presentationDetents([.height(300), .medium, .large])
.presentationDetents([.height(selectedTextHeight + 450), .medium, .large])
}
}
.frame(height: selectedTextHeight)
}
@@ -73,17 +73,34 @@ struct SelectableText: View {
func enableHighlighting() -> Bool {
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 {
@Binding var showHighlightPost: Bool
@Binding var showMutePost: Bool
@Binding var selectedText: String
var postHighlight: (String) -> Void
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, showMutePost: Binding<Bool>, selectedText: Binding<String>) {
self._showHighlightPost = showHighlightPost
self._showMutePost = showMutePost
self._selectedText = selectedText
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void) {
self.postHighlight = postHighlight
super.init(frame: frame, textContainer: textContainer)
}
@@ -95,24 +112,13 @@ fileprivate class TextView: UITextView {
if action == #selector(highlightText(_:)) {
return true
}
if action == #selector(muteText(_:)) {
return true
}
return super.canPerformAction(action, withSender: sender)
}
@objc public func highlightText(_ sender: Any?) {
guard let selectedRange = self.selectedTextRange else { return }
selectedText = self.text(in: selectedRange) ?? ""
showHighlightPost.toggle()
}
@objc public func muteText(_ sender: Any?) {
guard let selectedRange = self.selectedTextRange else { return }
selectedText = self.text(in: selectedRange) ?? ""
showMutePost.toggle()
guard let selectedText = self.text(in: selectedRange) else { return }
self.postHighlight(selectedText)
}
}
@@ -125,13 +131,11 @@ fileprivate class TextView: UITextView {
let fixedWidth: CGFloat
let textAlignment: NSTextAlignment
let enableHighlighting: Bool
@Binding var showHighlightPost: Bool
@Binding var showMutePost: Bool
@Binding var selectedText: String
let postHighlight: (String) -> Void
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, showMutePost: $showMutePost, selectedText: $selectedText)
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight)
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
@@ -144,8 +148,7 @@ fileprivate class TextView: UITextView {
let menuController = UIMenuController.shared
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
let muteItem = UIMenuItem(title: "Mute", action: #selector(view.muteText(_:)))
menuController.menuItems = self.enableHighlighting ? [highlightItem, muteItem] : []
menuController.menuItems = self.enableHighlighting ? [highlightItem] : []
return view
}
+1 -1
View File
@@ -12,7 +12,7 @@ enum NoteContent {
case content(String, TagsSequence?)
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)
} else {
self = .note(note)
+7 -3
View File
@@ -57,6 +57,10 @@ enum Sheets: Identifiable {
}
}
func present_sheet(_ sheet: Sheets) {
notify(.present_sheet(sheet))
}
struct ContentView: View {
let keypair: Keypair
let appDelegate: AppDelegate?
@@ -819,7 +823,7 @@ func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [Nos
func setup_notifications() {
UIApplication.shared.registerForRemoteNotifications()
this_app.registerForRemoteNotifications()
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
@@ -1051,7 +1055,7 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
//let post = tup.0
//let to_relays = tup.1
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
}
postbox.send(new_ev)
@@ -1112,7 +1116,7 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
}
case .hashtag(let ht):
result(.filter(.filter_hashtag([ht.hashtag])))
case .param, .quote:
case .param, .quote, .reference:
// doesn't really make sense here
break
case .naddr(let naddr):
+1 -1
View File
@@ -151,7 +151,7 @@ public class CameraService: NSObject, Identifiable {
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: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!,
this_app.open(URL(string: UIApplication.openSettingsURLString)!,
options: [:], completionHandler: nil)
}, secondaryAction: nil)
+23
View File
@@ -0,0 +1,23 @@
//
// CommentItem.swift
// damus
//
// Created by Daniel DAquino 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())
}
}
+1 -1
View File
@@ -109,7 +109,7 @@ func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
case let (.pubkey(pk), .pubkey(follow_pk)):
return pk == follow_pk
case (.hashtag, .pubkey), (.pubkey, .hashtag),
(.event, _), (.quote, _), (.param, _), (.naddr, _):
(.event, _), (.quote, _), (.param, _), (.naddr, _), (.reference(_), _):
return false
}
}
+73
View File
@@ -74,6 +74,79 @@ class DamusState: HeadlessDamusState {
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
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
func add_zap(zap: Zapping) -> Bool {
+1
View File
@@ -28,4 +28,5 @@ class Drafts: ObservableObject {
@Published var post: DraftArtifacts? = nil
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
@Published var highlights: [HighlightSource: DraftArtifacts] = [:]
}
+182 -1
View File
@@ -13,22 +13,203 @@ struct HighlightEvent {
var event_ref: String? = nil
var url_ref: URL? = nil
var context: String? = nil
// MARK: - Initializers and parsers
static func parse(from ev: NostrEvent) -> HighlightEvent {
var highlight = HighlightEvent(event: ev)
var best_url_source: (url: URL, tagged_as_source: Bool)? = nil
for tag in ev.tags {
guard tag.count >= 2 else { continue }
switch tag[0].string() {
case "e": 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()
default:
break
}
}
if let best_url_source {
highlight.url_ref = best_url_source.url
}
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)
}
}
}
-43
View File
@@ -256,46 +256,3 @@ func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
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
View File
@@ -17,10 +17,84 @@ struct NostrPost {
self.kind = kind
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] {
return parse_note_content(content: .content(content, nil)).blocks
}
+1 -1
View File
@@ -220,7 +220,7 @@ extension PushNotificationClient {
case .local_test(let host):
URL(string: "http://\(host ?? "localhost:8000")") ?? Constants.PUSH_NOTIFICATION_SERVER_TEST_BASE_URL
case .production:
Constants.PURPLE_API_PRODUCTION_BASE_URL
Constants.PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL
}
}
+12 -7
View File
@@ -122,20 +122,22 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case hashtag(Hashtag)
case param(TagElem)
case naddr(NAddr)
case reference(String)
var key: RefKey {
switch self {
case .event: return .e
case .pubkey: return .p
case .quote: return .q
case .hashtag: return .t
case .param: return .d
case .naddr: return .a
case .event: return .e
case .pubkey: return .p
case .quote: return .q
case .hashtag: return .t
case .param: return .d
case .naddr: return .a
case .reference: return .r
}
}
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
case e, p, t, d, q, a
case e, p, t, d, q, a, r
var keychar: AsciiCharacter {
self.rawValue
@@ -159,6 +161,8 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case .param(let string): return string.string()
case .naddr(let naddr):
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 .d: return .param(t1)
case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0))
case .r: return .reference(t1.string())
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ class Constants {
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
// MARK: Push notification server
static let PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL: URL = URL(string: "http://45.33.32.5:8000")!
static let PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL: URL = URL(string: "https://notify.damus.io")!
static let PUSH_NOTIFICATION_SERVER_TEST_BASE_URL: URL = URL(string: "http://localhost:8000")!
// MARK: Purple
+11
View File
@@ -0,0 +1,11 @@
//
// DamusAliases.swift
// damus
//
// Created by Daniel DAquino on 2024-08-12.
//
import Foundation
import UIKit
let this_app: UIApplication = UIApplication.shared
+1 -1
View File
@@ -30,7 +30,7 @@ public struct DismissKeyboardOnTap: ViewModifier {
}
public func end_editing() {
UIApplication.shared.connectedScenes
this_app.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
+1 -2
View File
@@ -11,8 +11,7 @@ import UIKit
class Theme {
static var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
return this_app
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
+1 -1
View File
@@ -116,7 +116,7 @@ struct AddRelayView: View {
}
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()
}) {
+10
View File
@@ -73,6 +73,16 @@ func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
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
{
return time_ago_since(Date(timeIntervalSince1970: Double(created_at)))
@@ -17,7 +17,8 @@ struct ReplyPart: View {
Group {
if event.known_kind == .highlight {
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 {
let replying_to = events.lookup(reply_ref.note_id)
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
@@ -9,12 +9,12 @@ import SwiftUI
// Modified from Reply Description
struct HighlightDescription: View {
let event: NostrEvent
let highlight_event: HighlightEvent
let highlighted_event: NostrEvent?
let ndb: Ndb
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)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -24,30 +24,6 @@ struct HighlightDescription: View {
struct HighlightDescription_Previews: PreviewProvider {
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 {
Group {
if options.contains(.wide) {
Main.padding(.horizontal)
} else {
Main
} else {
Main.padding(.horizontal)
}
}
}
@@ -92,6 +92,18 @@ struct HighlightBodyView: View {
var Main: some View {
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 {
var attributedString: AttributedString {
var attributedString: AttributedString = ""
@@ -119,14 +131,17 @@ struct HighlightBodyView: View {
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
alignment: .leading
)
.padding(.horizontal)
.padding(.bottom, 10)
if let url = event.url_ref {
HighlightLink(state: state, url: url, content: event.event.content)
.padding(.horizontal)
} else {
if let evRef = event.event_ref {
if let eventHex = hex_decode_id(evRef) {
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
.padding(.horizontal)
.padding(.top, 5)
}
}
+3 -3
View File
@@ -8,7 +8,7 @@ import SwiftUI
struct AddMuteItemView: View {
let state: DamusState
@Binding var new_text: String
@State var new_text: String = ""
@State var expiration: DamusDuration = .indefinite
@Environment(\.dismiss) var dismiss
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
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()
}) {
@@ -108,6 +108,6 @@ struct AddMuteItemView: View {
struct AddMuteItemView_Previews: PreviewProvider {
static var previews: some View {
AddMuteItemView(state: test_damus_state, new_text: .constant(""))
AddMuteItemView(state: test_damus_state)
}
}
+7 -5
View File
@@ -15,8 +15,6 @@ struct MutelistView: View {
@State var hashtags: [MuteItem] = []
@State var threads: [MuteItem] = []
@State var words: [MuteItem] = []
@State var new_text: String = ""
func RemoveAction(item: MuteItem) -> some View {
Button {
@@ -122,9 +120,13 @@ struct MutelistView: View {
}
}
.sheet(isPresented: $show_add_muteitem, onDismiss: { self.show_add_muteitem = false }) {
AddMuteItemView(state: damus_state, new_text: $new_text)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
if #available(iOS 16.0, *) {
AddMuteItemView(state: damus_state)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
} else {
AddMuteItemView(state: damus_state)
}
}
}
}
@@ -96,7 +96,7 @@ struct DamusAppNotificationView: View {
@MainActor
func open_url(url: URL) {
UIApplication.shared.open(url)
this_app.open(url)
}
var body: some View {
+61 -30
View File
@@ -30,15 +30,18 @@ enum PostAction {
case replying_to(NostrEvent)
case quoting(NostrEvent)
case posting(PostTarget)
case highlighting(HighlightContentDraft)
var ev: NostrEvent? {
switch self {
case .replying_to(let ev):
return ev
case .quoting(let ev):
return ev
case .posting:
return nil
case .replying_to(let ev):
return ev
case .quoting(let ev):
return ev
case .posting:
return nil
case .highlighting:
return nil
}
}
}
@@ -128,7 +131,12 @@ struct PostView: View {
}
var posting_disabled: Bool {
return is_post_empty || uploading_disabled
switch action {
case .highlighting(_):
return false
default:
return is_post_empty || uploading_disabled
}
}
// Returns a valid height for the text box, even when textHeight is not a number
@@ -204,6 +212,8 @@ struct PostView: View {
damus_state.drafts.quotes.removeValue(forKey: quoting)
case .posting:
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 {
BuilderEventView(damus: damus_state, event: ev)
}
else if case .highlighting(let draft) = action {
HighlightDraftContentView(draft: draft)
}
}
.padding(.horizontal)
}
@@ -454,14 +467,15 @@ struct PostView: View {
let loaded_draft = load_draft()
switch action {
case .replying_to(let replying_to):
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
case .quoting(let quoting):
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
case .posting(let target):
guard !loaded_draft else { break }
fill_target_content(target: target)
case .replying_to(let replying_to):
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
case .quoting(let quoting):
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
case .posting(let target):
guard !loaded_draft else { break }
fill_target_content(target: target)
case .highlighting(let draft):
references = [draft.source.ref()]
}
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
case .posting:
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]
case .posting:
return drafts.post
case .highlighting(let draft):
return drafts.highlights[draft.source]
}
}
@@ -669,27 +687,40 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post
var tags: [[String]] = []
switch action {
case .replying_to(let replying_to):
// start off with the reply tags
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
case .replying_to(let replying_to):
// start off with the reply tags
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
case .quoting(let ev):
content.append(" nostr:" + bech32_note_id(ev.id))
case .quoting(let ev):
content.append(" nostr:" + bech32_note_id(ev.id))
if let quoted_ev = state.events.lookup(ev.id) {
tags.append(["p", quoted_ev.pubkey.hex()])
}
case .posting(let postTarget):
break
}
// include pubkeys
tags += pubkeys.map { pk in
["p", pk.hex()]
if let quoted_ev = state.events.lookup(ev.id) {
tags.append(["p", quoted_ev.pubkey.hex()])
}
case .posting(let postTarget):
break
case .highlighting(let draft):
break
}
// append additional tags
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)
}
+1 -1
View File
@@ -356,7 +356,7 @@ struct ZapSheetViewIfPossible: View {
extension View {
func hideKeyboard() {
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)
}
}
+3 -3
View File
@@ -240,7 +240,7 @@ class ReplyTests: XCTestCase {
let content = "this is a @\(pk.npub) mention"
let blocks = parse_post_blocks(content: content)
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(blocks.count, 3)
@@ -255,7 +255,7 @@ class ReplyTests: XCTestCase {
let content = "this is a @\(nsec) mention"
let blocks = parse_post_blocks(content: content)
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(blocks.count, 3)
@@ -275,7 +275,7 @@ class ReplyTests: XCTestCase {
]
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.tags[2][1].string(), pubkey.description)
+2 -2
View File
@@ -193,7 +193,7 @@ class damusTests: XCTestCase {
func testMakeHashtagPost() {
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.content, "#damus some content #bitcoin derp #かっこいい wow")
@@ -270,7 +270,7 @@ class damusTests: XCTestCase {
private func createEventFromContentString(_ content: String) -> NostrEvent {
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")
return test_note
}
@@ -0,0 +1,287 @@
//
// ActionViewController.swift
// highlighter action extension
//
// Created by Daniel DAquino 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 DAquino on 2024-08-12.
//
import Foundation
import UIKit
let this_app: UIApplication = UIApplication()
+30
View File
@@ -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
}
}
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>
+7
View File
@@ -335,6 +335,10 @@ extension NdbNote {
public var referenced_mute_items: References<MuteItem> {
References<MuteItem>(tags: self.tags)
}
public var referenced_comment_items: References<CommentItem> {
References<CommentItem>(tags: self.tags)
}
public var references: References<RefId> {
References<RefId>(tags: self.tags)
@@ -355,6 +359,9 @@ extension NdbNote {
if known_kind == .dm {
return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
}
else if known_kind == .highlight {
return self.referenced_comment_items.first?.content ?? ""
}
return content
}
+11 -2
View File
@@ -4893,8 +4893,17 @@ mdb_env_setup_locks(MDB_env *env, MDB_name *fname, int mode, int *excl)
#ifdef MDB_SHORT_SEMNAMES
encbuf[9] = '\0'; /* drop name from 15 chars to 14 chars */
#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
* remove both semaphores before doing anything else.
*/