commit 0eb1372937d217479ccb50fe2d28c0e8d3d964ae
parent 73652513d91c0c780dc6bfb3d74f336dfa90711a
Author: William Casarin <jb55@jb55.com>
Date: Sat, 7 May 2022 13:50:19 -0700
more mention progress
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
21 files changed, 608 insertions(+), 141 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -19,6 +19,10 @@
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8B28236B92006E126D /* PubkeyView.swift */; };
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8D28236FE4006E126D /* NoteContentView.swift */; };
+ 4C363A9028247A1D006E126D /* NostrLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8F28247A1D006E126D /* NostrLink.swift */; };
+ 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A912825FCF2006E126D /* ProfileUpdate.swift */; };
+ 4C363A94282704FA006E126D /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A93282704FA006E126D /* Post.swift */; };
+ 4C363A962827096D006E126D /* PostBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A952827096D006E126D /* PostBlock.swift */; };
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */; };
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */; };
@@ -91,6 +95,10 @@
4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
4C363A8B28236B92006E126D /* PubkeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubkeyView.swift; sourceTree = "<group>"; };
4C363A8D28236FE4006E126D /* NoteContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentView.swift; sourceTree = "<group>"; };
+ 4C363A8F28247A1D006E126D /* NostrLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrLink.swift; sourceTree = "<group>"; };
+ 4C363A912825FCF2006E126D /* ProfileUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileUpdate.swift; sourceTree = "<group>"; };
+ 4C363A93282704FA006E126D /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
+ 4C363A952827096D006E126D /* PostBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBlock.swift; sourceTree = "<group>"; };
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModel.swift; sourceTree = "<group>"; };
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrKind.swift; sourceTree = "<group>"; };
4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBarModel.swift; sourceTree = "<group>"; };
@@ -175,6 +183,9 @@
4C3BEFDD281DD59C00B3DE84 /* ParsedRefs.swift */,
4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */,
4C7FF7D42823313F009601DB /* Mentions.swift */,
+ 4C363A912825FCF2006E126D /* ProfileUpdate.swift */,
+ 4C363A93282704FA006E126D /* Post.swift */,
+ 4C363A952827096D006E126D /* PostBlock.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -219,6 +230,7 @@
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
+ 4C363A8F28247A1D006E126D /* NostrLink.swift */,
);
path = Nostr;
sourceTree = "<group>";
@@ -452,6 +464,7 @@
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
+ 4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
@@ -468,6 +481,7 @@
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
+ 4C363A94282704FA006E126D /* Post.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
@@ -477,7 +491,9 @@
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
+ 4C363A962827096D006E126D /* PostBlock.swift in Sources */,
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
+ 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -58,6 +58,8 @@ struct ContentView: View {
@State var events: [NostrEvent] = []
@State var friend_events: [NostrEvent] = []
@State var notifications: [NostrEvent] = []
+ @State var active_profile: String? = nil
+ @State var profile_open: Bool = false
// connect retry timer
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
@@ -146,6 +148,9 @@ struct ContentView: View {
func MainContent(damus: DamusState) -> some View {
NavigationView {
VStack {
+ NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
+ EmptyView()
+ }
switch selected_timeline {
case .home:
PostingTimelineView
@@ -168,10 +173,23 @@ struct ContentView: View {
}
}
.navigationBarTitle("Damus", displayMode: .inline)
+
}
.navigationViewStyle(.stack)
}
+ var MaybeProfileView: some View {
+ Group {
+ if let pk = self.active_profile {
+ let profile_model = ProfileModel(pubkey: pk, damus: damus!)
+ ProfileView(damus: damus!, profile: profile_model)
+ .environmentObject(profiles)
+ } else {
+ EmptyView()
+ }
+ }
+ }
+
var body: some View {
VStack {
if let damus = self.damus {
@@ -197,6 +215,25 @@ struct ContentView: View {
.environmentObject(profiles)
}
}
+ .onOpenURL { url in
+ guard let link = decode_nostr_uri(url.absoluteString) else {
+ return
+ }
+
+ switch link {
+ case .ref(let ref):
+ if ref.key == "p" {
+ active_profile = ref.ref_id
+ profile_open = true
+ } else if ref.key == "e" {
+ // TODO open event view
+ }
+ case .filter:
+ break
+ // TODO: handle filter searches?
+ }
+
+ }
.onReceive(handle_notify(.boost)) { notif in
let ev = notif.object as! NostrEvent
let boost = make_boost_event(ev, privkey: privkey, pubkey: pubkey)
@@ -229,7 +266,7 @@ struct ContentView: View {
switch post_res {
case .post(let post):
print("post \(post.content)")
- let new_ev = post.to_event(privkey: privkey, pubkey: pubkey)
+ let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
self.damus?.pool.send(.event(new_ev))
case .cancel:
active_sheet = nil
diff --git a/damus/Info.plist b/damus/Info.plist
@@ -2,6 +2,19 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>CFBundleURLName</key>
+ <string>io.damus.nostr</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>nostr</string>
+ </array>
+ </dict>
+ </array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift
@@ -7,9 +7,19 @@
import Foundation
+
enum MentionType {
case pubkey
case event
+
+ var ref: String {
+ switch self {
+ case .pubkey:
+ return "p"
+ case .event:
+ return "e"
+ }
+ }
}
struct Mention {
@@ -42,38 +52,15 @@ enum Block {
}
}
-struct ParsedMentions {
- let blocks: [Block]
-}
-
-class Parser {
- var pos: Int
- var str: String
-
- init(pos: Int, str: String) {
- self.pos = pos
- self.str = str
- }
-}
-
-func consume_until(_ p: Parser, match: Character) -> Bool {
- var i: Int = 0
- let sub = substring(p.str, start: p.pos, end: p.str.count)
- for c in sub {
- if c == match {
- p.pos += i
- return true
+func render_blocks(blocks: [Block]) -> String {
+ return blocks.reduce("") { str, block in
+ switch block {
+ case .mention(let m):
+ return str + "#[\(m.index)]"
+ case .text(let txt):
+ return str + txt
}
- i += 1
}
-
- return false
-}
-
-func substring(_ s: String, start: Int, end: Int) -> Substring {
- let ind = s.index(s.startIndex, offsetBy: start)
- let end = s.index(s.startIndex, offsetBy: end)
- return s[ind..<end]
}
func parse_textblock(str: String, from: Int, to: Int) -> Block {
@@ -86,7 +73,7 @@ func parse_mentions(content: String, tags: [[String]]) -> [Block] {
var starting_from: Int = 0
while p.pos < content.count {
- if (!consume_until(p, match: "#")) {
+ if (!consume_until(p, match: { $0 == "#" })) {
blocks.append(parse_textblock(str: p.str, from: starting_from, to: p.str.count))
return blocks
}
@@ -142,3 +129,17 @@ func parse_mention(_ p: Parser, tags: [[String]]) -> Mention? {
return Mention(index: digit, type: kind, ref: ref)
}
+func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
+ let new_ev = NostrEvent(content: post.content, pubkey: pubkey)
+ for id in post.references {
+ var tag = [id.key, id.ref_id]
+ if let relay_id = id.relay_id {
+ tag.append(relay_id)
+ }
+ new_ev.tags.append(tag)
+ }
+ new_ev.calculate_id()
+ new_ev.sign(privkey: privkey)
+ return new_ev
+}
+
diff --git a/damus/Models/Post.swift b/damus/Models/Post.swift
@@ -0,0 +1,69 @@
+//
+// Post.swift
+// damus
+//
+// Created by William Casarin on 2022-05-07.
+//
+
+import Foundation
+
+struct NostrPost {
+ let content: String
+ let references: [ReferencedId]
+}
+
+// TODO: parse nostr:{e,p}:pubkey uris as well
+func parse_post_mention_type(_ p: Parser) -> MentionType? {
+ if parse_char(p, "@") {
+ return .pubkey
+ }
+
+ if parse_char(p, "&") {
+ return .event
+ }
+
+ return nil
+}
+
+func parse_post_reference(_ p: Parser) -> ReferencedId? {
+ let start = p.pos
+
+ guard let typ = parse_post_mention_type(p) else {
+ return parse_nostr_ref_uri(p)
+ }
+
+ guard let id = parse_hexstr(p, len: 64) else {
+ p.pos = start
+ return nil
+ }
+
+ return ReferencedId(ref_id: id, relay_id: nil, key: typ.ref)
+}
+
+
+/// Return a list of tags
+func parse_post_blocks(content: String) -> [PostBlock] {
+ let p = Parser(pos: 0, str: content)
+ var blocks: [PostBlock] = []
+ var starting_from: Int = 0
+
+ if content.count == 0 {
+ return []
+ }
+
+ while p.pos < content.count {
+ let pre_mention = p.pos
+ if let reference = parse_post_reference(p) {
+ blocks.append(parse_post_textblock(str: p.str, from: starting_from, to: pre_mention))
+ blocks.append(.ref(reference))
+ starting_from = p.pos
+ } else {
+ p.pos += 1
+ }
+ }
+
+ blocks.append(parse_post_textblock(str: content, from: starting_from, to: content.count))
+
+ return blocks
+}
+
diff --git a/damus/Models/PostBlock.swift b/damus/Models/PostBlock.swift
@@ -0,0 +1,31 @@
+//
+// PostBlock.swift
+// damus
+//
+// Created by William Casarin on 2022-05-07.
+//
+
+import Foundation
+
+enum PostBlock {
+ case text(String)
+ case ref(ReferencedId)
+
+ var is_text: Bool {
+ if case .text = self {
+ return true
+ }
+ return false
+ }
+
+ var is_ref: Bool {
+ if case .ref = self {
+ return true
+ }
+ return false
+ }
+}
+
+func parse_post_textblock(str: String, from: Int, to: Int) -> PostBlock {
+ return .text(String(substring(str, start: from, end: to)))
+}
diff --git a/damus/Models/ProfileUpdate.swift b/damus/Models/ProfileUpdate.swift
@@ -0,0 +1,14 @@
+//
+// ProfileUpdate.swift
+// damus
+//
+// Created by William Casarin on 2022-05-06.
+//
+
+import Foundation
+
+
+struct ProfileUpdate {
+ let pubkey: String
+ let profile: Profile
+}
diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift
@@ -14,7 +14,7 @@ struct Profile: Decodable {
let picture: String?
static func displayName(profile: Profile?, pubkey: String) -> String {
- return profile?.name ?? String(pubkey.prefix(16))
+ return profile?.name ?? abbrev_pubkey(pubkey)
}
}
diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift
@@ -49,6 +49,10 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
let kind: Int
let content: String
+ lazy var blocks: [Block] = {
+ return parse_mentions(content: self.content, tags: self.tags)
+ }()
+
var description: String {
let p = pow.map { String($0) } ?? "?"
return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) pow \(p) content '\(content)' }"
diff --git a/damus/Nostr/NostrLink.swift b/damus/Nostr/NostrLink.swift
@@ -0,0 +1,96 @@
+//
+// NostrLink.swift
+// damus
+//
+// Created by William Casarin on 2022-05-05.
+//
+
+import Foundation
+
+
+enum NostrLink {
+ case ref(ReferencedId)
+ case filter(NostrFilter)
+}
+
+func encode_pubkey_uri(_ ref: ReferencedId) -> String {
+ return "p:" + ref.ref_id
+}
+
+// TODO: bech32 and relay hints
+func encode_event_id_uri(_ ref: ReferencedId) -> String {
+ return "e:" + ref.ref_id
+}
+
+func parse_nostr_ref_uri_type(_ p: Parser) -> String? {
+ if parse_char(p, "p") {
+ return "p"
+ }
+
+ if parse_char(p, "e") {
+ return "e"
+ }
+
+ return nil
+}
+
+func parse_hexstr(_ p: Parser, len: Int) -> String? {
+ var i: Int = 0
+
+ if len % 2 != 0 {
+ return nil
+ }
+
+ let start = p.pos
+
+ while i < len {
+ guard parse_hex_char(p) != nil else {
+ p.pos = start
+ return nil
+ }
+ i += 1
+ }
+
+ return String(substring(p.str, start: start, end: p.pos))
+}
+
+func parse_nostr_ref_uri(_ p: Parser) -> ReferencedId? {
+ let start = p.pos
+
+ if !parse_str(p, "nostr:") {
+ return nil
+ }
+
+ guard let typ = parse_nostr_ref_uri_type(p) else {
+ p.pos = start
+ return nil
+ }
+
+ if !parse_char(p, ":") {
+ p.pos = start
+ return nil
+ }
+
+ guard let pk = parse_hexstr(p, len: 64) else {
+ p.pos = start
+ return nil
+ }
+
+ // TODO: parse relays from nostr uris
+ return ReferencedId(ref_id: pk, relay_id: nil, key: typ)
+}
+
+func decode_nostr_uri(_ s: String) -> NostrLink? {
+ let uri = s.replacingOccurrences(of: "nostr:", with: "")
+
+ let parts = uri.split(separator: ":")
+ .reduce(into: Array<String>()) { acc, str in
+ guard let decoded = str.removingPercentEncoding else {
+ return
+ }
+ acc.append(decoded)
+ return
+ }
+
+ return tag_to_refid(parts).map { .ref($0) }
+}
diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift
@@ -9,62 +9,6 @@ import Foundation
import UIKit
import Combine
-class ImageCache {
- private let lock = NSLock()
-
- lazy var cache: NSCache<AnyObject, AnyObject> = {
- let cache = NSCache<AnyObject, AnyObject>()
- cache.totalCostLimit = 1024 * 1024 * 100 // 100MB
- return cache
- }()
-
- func lookup(for url: URL) -> UIImage? {
- lock.lock(); defer { lock.unlock() }
-
- if let decoded = cache.object(forKey: url as AnyObject) as? UIImage {
- return decoded
- }
-
- return nil
- }
-
- func remove(for url: URL) {
- lock.lock(); defer { lock.unlock() }
- cache.removeObject(forKey: url as AnyObject)
- }
-
- func insert(_ image: UIImage?, for url: URL) {
- guard let image = image else { return remove(for: url) }
- let decodedImage = image.decodedImage(Int(PFP_SIZE!))
- lock.lock(); defer { lock.unlock() }
- cache.setObject(decodedImage, forKey: url as AnyObject)
- }
-
- subscript(_ key: URL) -> UIImage? {
- get {
- return lookup(for: key)
- }
- set {
- return insert(newValue, for: key)
- }
- }
-}
-
-func load_image(cache: ImageCache, from url: URL) -> AnyPublisher<UIImage?, Never> {
- if let image = cache[url] {
- return Just(image).eraseToAnyPublisher()
- }
- return URLSession.shared.dataTaskPublisher(for: url)
- .map { (data, response) -> UIImage? in return UIImage(data: data) }
- .catch { error in return Just(nil) }
- .handleEvents(receiveOutput: { image in
- guard let image = image else { return }
- cache[url] = image
- })
- .subscribe(on: DispatchQueue.global(qos: .background))
- .receive(on: RunLoop.main)
- .eraseToAnyPublisher()
-}
class Profiles: ObservableObject {
@Published var profiles: [String: TimestampedProfile] = [:]
diff --git a/damus/Notifications.swift b/damus/Notifications.swift
@@ -32,6 +32,12 @@ extension Notification.Name {
}
extension Notification.Name {
+ static var profile_update: Notification.Name {
+ return Notification.Name("profile_update")
+ }
+}
+
+extension Notification.Name {
static var switched_timeline: Notification.Name {
return Notification.Name("switched_timeline")
}
@@ -44,8 +50,8 @@ extension Notification.Name {
}
extension Notification.Name {
- static var click_profile_pic: Notification.Name {
- return Notification.Name("click_profile_pic")
+ static var open_profile: Notification.Name {
+ return Notification.Name("open_profile")
}
}
diff --git a/damus/Util/ImageCache.swift b/damus/Util/ImageCache.swift
@@ -7,6 +7,7 @@
import Foundation
import SwiftUI
+import Combine
extension UIImage {
func decodedImage(_ size: Int) -> UIImage {
@@ -26,3 +27,60 @@ extension UIImage {
return UIImage(cgImage: decodedImage, scale: scale, orientation: .up)
}
}
+
+class ImageCache {
+ private let lock = NSLock()
+
+ lazy var cache: NSCache<AnyObject, AnyObject> = {
+ let cache = NSCache<AnyObject, AnyObject>()
+ cache.totalCostLimit = 1024 * 1024 * 100 // 100MB
+ return cache
+ }()
+
+ func lookup(for url: URL) -> UIImage? {
+ lock.lock(); defer { lock.unlock() }
+
+ if let decoded = cache.object(forKey: url as AnyObject) as? UIImage {
+ return decoded
+ }
+
+ return nil
+ }
+
+ func remove(for url: URL) {
+ lock.lock(); defer { lock.unlock() }
+ cache.removeObject(forKey: url as AnyObject)
+ }
+
+ func insert(_ image: UIImage?, for url: URL) {
+ guard let image = image else { return remove(for: url) }
+ let decodedImage = image.decodedImage(Int(PFP_SIZE!))
+ lock.lock(); defer { lock.unlock() }
+ cache.setObject(decodedImage, forKey: url as AnyObject)
+ }
+
+ subscript(_ key: URL) -> UIImage? {
+ get {
+ return lookup(for: key)
+ }
+ set {
+ return insert(newValue, for: key)
+ }
+ }
+}
+
+func load_image(cache: ImageCache, from url: URL) -> AnyPublisher<UIImage?, Never> {
+ if let image = cache[url] {
+ return Just(image).eraseToAnyPublisher()
+ }
+ return URLSession.shared.dataTaskPublisher(for: url)
+ .map { (data, response) -> UIImage? in return UIImage(data: data) }
+ .catch { error in return Just(nil) }
+ .handleEvents(receiveOutput: { image in
+ guard let image = image else { return }
+ cache[url] = image
+ })
+ .subscribe(on: DispatchQueue.global(qos: .background))
+ .receive(on: RunLoop.main)
+ .eraseToAnyPublisher()
+}
diff --git a/damus/Util/Parser.swift b/damus/Util/Parser.swift
@@ -7,7 +7,41 @@
import Foundation
+class Parser {
+ var pos: Int
+ var str: String
+
+ init(pos: Int, str: String) {
+ self.pos = pos
+ self.str = str
+ }
+}
+
+func consume_until(_ p: Parser, match: (Character) -> Bool) -> Bool {
+ var i: Int = 0
+ let sub = substring(p.str, start: p.pos, end: p.str.count)
+ for c in sub {
+ if match(c) {
+ p.pos += i
+ return true
+ }
+ i += 1
+ }
+
+ return false
+}
+
+func substring(_ s: String, start: Int, end: Int) -> Substring {
+ let ind = s.index(s.startIndex, offsetBy: start)
+ let end = s.index(s.startIndex, offsetBy: end)
+ return s[ind..<end]
+}
+
+
func parse_str(_ p: Parser, _ s: String) -> Bool {
+ if p.pos + s.count > p.str.count {
+ return false
+ }
let sub = substring(p.str, start: p.pos, end: p.pos + s.count)
if sub == s {
p.pos += s.count
@@ -16,7 +50,7 @@ func parse_str(_ p: Parser, _ s: String) -> Bool {
return false
}
-func parse_char(_ p: Parser, _ c: Character) -> Bool{
+func parse_char(_ p: Parser, _ c: Character) -> Bool {
let ind = p.str.index(p.str.startIndex, offsetBy: p.pos)
if p.str[ind] == c {
@@ -40,3 +74,19 @@ func parse_digit(_ p: Parser) -> Int? {
return 0
}
+
+
+func parse_hex_char(_ p: Parser) -> Character? {
+ let ind = p.str.index(p.str.startIndex, offsetBy: p.pos)
+
+ if let c = p.str[ind].unicodeScalars.first {
+ // hex chars
+ let d = c.value
+ if (d >= 48 && d <= 57) || (d >= 97 && d <= 102) || (d >= 65 && d <= 70) {
+ p.pos += 1
+ return p.str[ind]
+ }
+ }
+
+ return nil
+}
diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift
@@ -128,7 +128,7 @@ struct ChatView: View {
}
}
- NoteContentView(event)
+ NoteContentView(event: event, profiles: profiles)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
EventActionBar(event: event,
diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift
@@ -74,7 +74,7 @@ struct EventView: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
- NoteContentView(event)
+ NoteContentView(event: event, profiles: profiles)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift
@@ -7,46 +7,69 @@
import SwiftUI
-func NoteContentView(_ ev: NostrEvent) -> some View {
- let txt = parse_mentions(content: ev.content, tags: ev.tags)
- .reduce("") { str, block in
- switch block {
- case .mention(let m):
- return str + mention_str(m)
- case .text(let txt):
- return str + txt
- }
+
+func render_note_content(ev: NostrEvent, profiles: Profiles) -> String {
+ return ev.blocks.reduce("") { str, block in
+ switch block {
+ case .mention(let m):
+ return str + mention_str(m, profiles: profiles)
+ case .text(let txt):
+ return str + txt
}
+ }
+}
+
+struct NoteContentView: View {
+ let event: NostrEvent
+ let profiles: Profiles
- let md_opts: AttributedString.MarkdownParsingOptions =
- .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
+ @State var content: String = ""
- guard let txt = try? AttributedString(markdown: txt, options: md_opts) else {
- return Text(ev.content)
+ func MainContent() -> some View {
+ let md_opts: AttributedString.MarkdownParsingOptions =
+ .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
+
+ guard let txt = try? AttributedString(markdown: content, options: md_opts) else {
+ return Text(event.content)
+ }
+
+ return Text(txt)
}
- return Text(txt)
+ var body: some View {
+ MainContent()
+ .onAppear() {
+ self.content = render_note_content(ev: event, profiles: profiles)
+ }
+ .onReceive(handle_notify(.profile_update)) { notif in
+ let profile = notif.object as! ProfileUpdate
+ for block in event.blocks {
+ switch block {
+ case .mention(let m):
+ if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
+ content = render_note_content(ev: event, profiles: profiles)
+ }
+ case .text:
+ return
+ }
+ }
+ }
+ }
}
-func mention_str(_ m: Mention) -> String {
+func mention_str(_ m: Mention, profiles: Profiles) -> String {
switch m.type {
case .pubkey:
let pk = m.ref.ref_id
- return "[@\(abbrev_pubkey(pk))](nostr:\(encode_pubkey(m.ref)))"
+ let profile = profiles.lookup(id: pk)
+ let disp = Profile.displayName(profile: profile, pubkey: pk)
+ return "[@\(disp)](nostr:\(encode_pubkey_uri(m.ref)))"
case .event:
let evid = m.ref.ref_id
- return "[*\(abbrev_pubkey(evid))](nostr:\(encode_event_id(m.ref)))"
+ return "[&\(abbrev_pubkey(evid))](nostr:\(encode_event_id_uri(m.ref)))"
}
}
-// TODO: bech32 and relay hints
-func encode_event_id(_ ref: ReferencedId) -> String {
- return "e_" + ref.ref_id
-}
-
-func encode_pubkey(_ ref: ReferencedId) -> String {
- return "p_" + ref.ref_id
-}
/*
struct NoteContentView_Previews: PreviewProvider {
diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift
@@ -12,26 +12,6 @@ enum NostrPostResult {
case cancel
}
-struct NostrPost {
- let content: String
- let references: [ReferencedId]
-
- public func to_event(privkey: String, pubkey: String) -> NostrEvent {
- let new_ev = NostrEvent(content: content, pubkey: pubkey)
- for id in references {
- var tag = [id.key, id.ref_id]
- if let relay_id = id.relay_id {
- tag.append(relay_id)
- }
- new_ev.tags.append(tag)
- }
- new_ev.calculate_id()
- new_ev.sign(privkey: privkey)
- return new_ev
- }
-}
-
-
struct PostView: View {
@State var post: String = ""
@FocusState var focus: Bool
diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift
@@ -31,6 +31,10 @@ struct ProfileView: View {
ProfileName(pubkey: pubkey, profile: data)
.font(.title)
//.border(Color.green)
+ Text("\(pubkey)")
+ .textSelection(.enabled)
+ .font(.footnote)
+ .foregroundColor(id_to_color(pubkey))
}
Text(data?.about ?? "")
//.border(Color.red)
diff --git a/damus/Views/PubkeyView.swift b/damus/Views/PubkeyView.swift
@@ -20,8 +20,8 @@ struct PubkeyView: View {
}
}
-func abbrev_pubkey(_ pubkey: String) -> String {
- return pubkey.prefix(4) + ":" + pubkey.suffix(4)
+func abbrev_pubkey(_ pubkey: String, amount: Int = 8) -> String {
+ return pubkey.prefix(amount) + ":" + pubkey.suffix(amount)
}
/*
diff --git a/damusTests/damusTests.swift b/damusTests/damusTests.swift
@@ -43,6 +43,127 @@ class damusTests: XCTestCase {
XCTAssertTrue(parsed[2].is_text)
}
+ func testEmptyPostReference() throws {
+ let parsed = parse_post_blocks(content: "")
+ XCTAssertEqual(parsed.count, 0)
+ }
+
+ func testInvalidPostReference() throws {
+ let pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e24"
+ let content = "this is a @\(pk) mention"
+ let parsed = parse_post_blocks(content: content)
+ XCTAssertEqual(parsed.count, 1)
+ guard case .text(let txt) = parsed[0] else {
+ XCTAssert(false)
+ return
+ }
+ XCTAssertEqual(txt, content)
+ }
+
+ func testInvalidPostReferenceEmptyAt() throws {
+ let content = "this is a @ mention"
+ let parsed = parse_post_blocks(content: content)
+ XCTAssertEqual(parsed.count, 1)
+ guard case .text(let txt) = parsed[0] else {
+ XCTAssert(false)
+ return
+ }
+ XCTAssertEqual(txt, content)
+ }
+
+ func testParsePostUriReference() throws {
+ let id = "6fec2ee6cfff779fe8560976b3d9df782b74577f0caefa7a77c0ed4c3749b5de"
+ let parsed = parse_post_blocks(content: "this is a nostr:e:\(id) event mention")
+
+ XCTAssertNotNil(parsed)
+ XCTAssertEqual(parsed.count, 3)
+ XCTAssertTrue(parsed[0].is_text)
+ XCTAssertTrue(parsed[1].is_ref)
+ XCTAssertTrue(parsed[2].is_text)
+
+ guard case .ref(let ref) = parsed[1] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(ref.ref_id, id)
+ XCTAssertEqual(ref.key, "e")
+ XCTAssertNil(ref.relay_id)
+
+ guard case .text(let t1) = parsed[0] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(t1, "this is a ")
+
+ guard case .text(let t2) = parsed[2] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(t2, " event mention")
+ }
+
+ func testParsePostEventReference() throws {
+ let pk = "6fec2ee6cfff779fe8560976b3d9df782b74577f0caefa7a77c0ed4c3749b5de"
+ let parsed = parse_post_blocks(content: "this is a &\(pk) event mention")
+
+ XCTAssertNotNil(parsed)
+ XCTAssertEqual(parsed.count, 3)
+ XCTAssertTrue(parsed[0].is_text)
+ XCTAssertTrue(parsed[1].is_ref)
+ XCTAssertTrue(parsed[2].is_text)
+
+ guard case .ref(let ref) = parsed[1] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(ref.ref_id, pk)
+ XCTAssertEqual(ref.key, "e")
+ XCTAssertNil(ref.relay_id)
+
+ guard case .text(let t1) = parsed[0] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(t1, "this is a ")
+
+ guard case .text(let t2) = parsed[2] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(t2, " event mention")
+ }
+
+ func testParsePostPubkeyReference() throws {
+ let pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
+ let parsed = parse_post_blocks(content: "this is a @\(pk) mention")
+
+ XCTAssertNotNil(parsed)
+ XCTAssertEqual(parsed.count, 3)
+ XCTAssertTrue(parsed[0].is_text)
+ XCTAssertTrue(parsed[1].is_ref)
+ XCTAssertTrue(parsed[2].is_text)
+
+ guard case .ref(let ref) = parsed[1] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(ref.ref_id, pk)
+ XCTAssertEqual(ref.key, "p")
+ XCTAssertNil(ref.relay_id)
+
+ guard case .text(let t1) = parsed[0] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(t1, "this is a ")
+
+ guard case .text(let t2) = parsed[2] else {
+ XCTAssertTrue(false)
+ return
+ }
+ XCTAssertEqual(t2, " mention")
+ }
+
func testParseInvalidMention() throws {
let parsed = parse_mentions(content: "this is #[0] a mention", tags: [])