1
0
mirror of git://jb55.com/damus synced 2024-09-18 19:23:49 +00:00

more mention progress

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin 2022-05-07 13:50:19 -07:00
parent 73652513d9
commit 0eb1372937
21 changed files with 611 additions and 144 deletions

View File

@ -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 */,

View File

@ -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

View File

@ -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>

View File

@ -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
}

69
damus/Models/Post.swift Normal file
View File

@ -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
}

View File

@ -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)))
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)' }"

View File

@ -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) }
}

View File

@ -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] = [:]

View File

@ -31,6 +31,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")
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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,

View File

@ -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)

View File

@ -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
}
let md_opts: AttributedString.MarkdownParsingOptions =
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
guard let txt = try? AttributedString(markdown: txt, options: md_opts) else {
return Text(ev.content)
}
return Text(txt)
}
func mention_str(_ m: Mention) -> String {
struct NoteContentView: View {
let event: NostrEvent
let profiles: Profiles
@State var content: String = ""
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)
}
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, 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 {

View File

@ -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

View File

@ -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)

View File

@ -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)
}
/*

View File

@ -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: [])