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:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ struct SelectableText: View {
|
||||
let event: NostrEvent?
|
||||
let attributedString: AttributedString
|
||||
let textAlignment: NSTextAlignment
|
||||
@State private var showHighlightPost = false
|
||||
@State private var selectedText = ""
|
||||
@State private var highlightPostingState: HighlightPostingState = .hide
|
||||
@State private var selectedTextHeight: CGFloat = .zero
|
||||
@State private var selectedTextWidth: CGFloat = .zero
|
||||
|
||||
@@ -37,8 +36,9 @@ struct SelectableText: View {
|
||||
fixedWidth: selectedTextWidth,
|
||||
textAlignment: self.textAlignment,
|
||||
enableHighlighting: self.enableHighlighting(),
|
||||
showHighlightPost: $showHighlightPost,
|
||||
selectedText: $selectedText,
|
||||
postHighlight: { selectedText in
|
||||
self.highlightPostingState = .show_post_view(highlighted_text: selectedText)
|
||||
},
|
||||
height: $selectedTextHeight
|
||||
)
|
||||
.padding([.leading, .trailing], -1.0)
|
||||
@@ -53,11 +53,18 @@ 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: 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(selectedTextHeight + 450), .medium, .large])
|
||||
}
|
||||
}
|
||||
.frame(height: selectedTextHeight)
|
||||
@@ -66,15 +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 selectedText: String
|
||||
var postHighlight: (String) -> Void
|
||||
|
||||
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, selectedText: Binding<String>) {
|
||||
self._showHighlightPost = showHighlightPost
|
||||
self._selectedText = selectedText
|
||||
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void) {
|
||||
self.postHighlight = postHighlight
|
||||
super.init(frame: frame, textContainer: textContainer)
|
||||
}
|
||||
|
||||
@@ -91,8 +117,8 @@ fileprivate class TextView: UITextView {
|
||||
|
||||
@objc public func highlightText(_ sender: Any?) {
|
||||
guard let selectedRange = self.selectedTextRange else { return }
|
||||
selectedText = self.text(in: selectedRange) ?? ""
|
||||
showHighlightPost.toggle()
|
||||
guard let selectedText = self.text(in: selectedRange) else { return }
|
||||
self.postHighlight(selectedText)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -105,12 +131,11 @@ fileprivate class TextView: UITextView {
|
||||
let fixedWidth: CGFloat
|
||||
let textAlignment: NSTextAlignment
|
||||
let enableHighlighting: Bool
|
||||
@Binding var showHighlightPost: 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, selectedText: $selectedText)
|
||||
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight)
|
||||
view.isEditable = false
|
||||
view.dataDetectorTypes = .all
|
||||
view.isSelectable = true
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
@@ -877,7 +881,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
|
||||
@@ -1109,7 +1113,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)
|
||||
@@ -1170,7 +1174,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):
|
||||
|
||||
@@ -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
damus/Models/CommentItem.swift
Normal file
23
damus/Models/CommentItem.swift
Normal file
@@ -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)):
|
||||
return pk == follow_pk
|
||||
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||
(.event, _), (.quote, _), (.param, _), (.naddr, _):
|
||||
(.event, _), (.quote, _), (.param, _), (.naddr, _), (.reference(_), _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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] = [:]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
damus/Util/DamusAliases.swift
Normal file
11
damus/Util/DamusAliases.swift
Normal file
@@ -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() {
|
||||
UIApplication.shared.connectedScenes
|
||||
this_app.connectedScenes
|
||||
.filter {$0.activationState == .foregroundActive}
|
||||
.map {$0 as? UIWindowScene}
|
||||
.compactMap({$0})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}) {
|
||||
|
||||
@@ -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 ?? "")
|
||||
}
|
||||
|
||||
42
damus/Views/Events/Highlight/HighlightDraftContentView.swift
Normal file
42
damus/Views/Events/Highlight/HighlightDraftContentView.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user