@@ -19,6 +19,10 @@
|
|||||||
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
|
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
|
||||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8B28236B92006E126D /* PubkeyView.swift */; };
|
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8B28236B92006E126D /* PubkeyView.swift */; };
|
||||||
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8D28236FE4006E126D /* NoteContentView.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 */; };
|
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */; };
|
||||||
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
|
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
|
||||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD5281D995700B3DE84 /* ActionBarModel.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
4C3BEFD5281D995700B3DE84 /* ActionBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBarModel.swift; sourceTree = "<group>"; };
|
||||||
@@ -175,6 +183,9 @@
|
|||||||
4C3BEFDD281DD59C00B3DE84 /* ParsedRefs.swift */,
|
4C3BEFDD281DD59C00B3DE84 /* ParsedRefs.swift */,
|
||||||
4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */,
|
4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */,
|
||||||
4C7FF7D42823313F009601DB /* Mentions.swift */,
|
4C7FF7D42823313F009601DB /* Mentions.swift */,
|
||||||
|
4C363A912825FCF2006E126D /* ProfileUpdate.swift */,
|
||||||
|
4C363A93282704FA006E126D /* Post.swift */,
|
||||||
|
4C363A952827096D006E126D /* PostBlock.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -219,6 +230,7 @@
|
|||||||
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
|
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
|
||||||
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
|
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
|
||||||
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
|
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
|
||||||
|
4C363A8F28247A1D006E126D /* NostrLink.swift */,
|
||||||
);
|
);
|
||||||
path = Nostr;
|
path = Nostr;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -452,6 +464,7 @@
|
|||||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
||||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
||||||
|
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
|
||||||
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
||||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
||||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||||
@@ -468,6 +481,7 @@
|
|||||||
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
||||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
||||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
||||||
|
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
||||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||||
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
|
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
|
||||||
@@ -477,7 +491,9 @@
|
|||||||
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
|
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
|
||||||
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
|
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
|
||||||
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
||||||
|
4C363A962827096D006E126D /* PostBlock.swift in Sources */,
|
||||||
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
|
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
|
||||||
|
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
|
||||||
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
|
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
|
||||||
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
|
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
|
||||||
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ struct ContentView: View {
|
|||||||
@State var events: [NostrEvent] = []
|
@State var events: [NostrEvent] = []
|
||||||
@State var friend_events: [NostrEvent] = []
|
@State var friend_events: [NostrEvent] = []
|
||||||
@State var notifications: [NostrEvent] = []
|
@State var notifications: [NostrEvent] = []
|
||||||
|
@State var active_profile: String? = nil
|
||||||
|
@State var profile_open: Bool = false
|
||||||
|
|
||||||
// connect retry timer
|
// connect retry timer
|
||||||
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
|
||||||
@@ -146,6 +148,9 @@ struct ContentView: View {
|
|||||||
func MainContent(damus: DamusState) -> some View {
|
func MainContent(damus: DamusState) -> some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack {
|
VStack {
|
||||||
|
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
switch selected_timeline {
|
switch selected_timeline {
|
||||||
case .home:
|
case .home:
|
||||||
PostingTimelineView
|
PostingTimelineView
|
||||||
@@ -168,10 +173,23 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Damus", displayMode: .inline)
|
.navigationBarTitle("Damus", displayMode: .inline)
|
||||||
|
|
||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.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 {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if let damus = self.damus {
|
if let damus = self.damus {
|
||||||
@@ -197,6 +215,25 @@ struct ContentView: View {
|
|||||||
.environmentObject(profiles)
|
.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
|
.onReceive(handle_notify(.boost)) { notif in
|
||||||
let ev = notif.object as! NostrEvent
|
let ev = notif.object as! NostrEvent
|
||||||
let boost = make_boost_event(ev, privkey: privkey, pubkey: pubkey)
|
let boost = make_boost_event(ev, privkey: privkey, pubkey: pubkey)
|
||||||
@@ -229,7 +266,7 @@ struct ContentView: View {
|
|||||||
switch post_res {
|
switch post_res {
|
||||||
case .post(let post):
|
case .post(let post):
|
||||||
print("post \(post.content)")
|
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))
|
self.damus?.pool.send(.event(new_ev))
|
||||||
case .cancel:
|
case .cancel:
|
||||||
active_sheet = nil
|
active_sheet = nil
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>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>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
|||||||
@@ -7,9 +7,19 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
enum MentionType {
|
enum MentionType {
|
||||||
case pubkey
|
case pubkey
|
||||||
case event
|
case event
|
||||||
|
|
||||||
|
var ref: String {
|
||||||
|
switch self {
|
||||||
|
case .pubkey:
|
||||||
|
return "p"
|
||||||
|
case .event:
|
||||||
|
return "e"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Mention {
|
struct Mention {
|
||||||
@@ -42,38 +52,15 @@ enum Block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParsedMentions {
|
func render_blocks(blocks: [Block]) -> String {
|
||||||
let blocks: [Block]
|
return blocks.reduce("") { str, block in
|
||||||
}
|
switch block {
|
||||||
|
case .mention(let m):
|
||||||
class Parser {
|
return str + "#[\(m.index)]"
|
||||||
var pos: Int
|
case .text(let txt):
|
||||||
var str: String
|
return str + txt
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
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 {
|
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
|
var starting_from: Int = 0
|
||||||
|
|
||||||
while p.pos < content.count {
|
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))
|
blocks.append(parse_textblock(str: p.str, from: starting_from, to: p.str.count))
|
||||||
return blocks
|
return blocks
|
||||||
}
|
}
|
||||||
@@ -142,3 +129,17 @@ func parse_mention(_ p: Parser, tags: [[String]]) -> Mention? {
|
|||||||
return Mention(index: digit, type: kind, ref: ref)
|
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
69
damus/Models/Post.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
31
damus/Models/PostBlock.swift
Normal file
31
damus/Models/PostBlock.swift
Normal 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)))
|
||||||
|
}
|
||||||
14
damus/Models/ProfileUpdate.swift
Normal file
14
damus/Models/ProfileUpdate.swift
Normal 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
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ struct Profile: Decodable {
|
|||||||
let picture: String?
|
let picture: String?
|
||||||
|
|
||||||
static func displayName(profile: Profile?, pubkey: String) -> String {
|
static func displayName(profile: Profile?, pubkey: String) -> String {
|
||||||
return profile?.name ?? String(pubkey.prefix(16))
|
return profile?.name ?? abbrev_pubkey(pubkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
let kind: Int
|
let kind: Int
|
||||||
let content: String
|
let content: String
|
||||||
|
|
||||||
|
lazy var blocks: [Block] = {
|
||||||
|
return parse_mentions(content: self.content, tags: self.tags)
|
||||||
|
}()
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
let p = pow.map { String($0) } ?? "?"
|
let p = pow.map { String($0) } ?? "?"
|
||||||
return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) pow \(p) content '\(content)' }"
|
return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) pow \(p) content '\(content)' }"
|
||||||
|
|||||||
96
damus/Nostr/NostrLink.swift
Normal file
96
damus/Nostr/NostrLink.swift
Normal 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) }
|
||||||
|
}
|
||||||
@@ -9,62 +9,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
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 {
|
class Profiles: ObservableObject {
|
||||||
@Published var profiles: [String: TimestampedProfile] = [:]
|
@Published var profiles: [String: TimestampedProfile] = [:]
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ extension Notification.Name {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static var profile_update: Notification.Name {
|
||||||
|
return Notification.Name("profile_update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static var switched_timeline: Notification.Name {
|
static var switched_timeline: Notification.Name {
|
||||||
return Notification.Name("switched_timeline")
|
return Notification.Name("switched_timeline")
|
||||||
@@ -44,8 +50,8 @@ extension Notification.Name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static var click_profile_pic: Notification.Name {
|
static var open_profile: Notification.Name {
|
||||||
return Notification.Name("click_profile_pic")
|
return Notification.Name("open_profile")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
extension UIImage {
|
extension UIImage {
|
||||||
func decodedImage(_ size: Int) -> UIImage {
|
func decodedImage(_ size: Int) -> UIImage {
|
||||||
@@ -26,3 +27,60 @@ extension UIImage {
|
|||||||
return UIImage(cgImage: decodedImage, scale: scale, orientation: .up)
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,41 @@
|
|||||||
|
|
||||||
import Foundation
|
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 {
|
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)
|
let sub = substring(p.str, start: p.pos, end: p.pos + s.count)
|
||||||
if sub == s {
|
if sub == s {
|
||||||
p.pos += s.count
|
p.pos += s.count
|
||||||
@@ -16,7 +50,7 @@ func parse_str(_ p: Parser, _ s: String) -> Bool {
|
|||||||
return false
|
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)
|
let ind = p.str.index(p.str.startIndex, offsetBy: p.pos)
|
||||||
|
|
||||||
if p.str[ind] == c {
|
if p.str[ind] == c {
|
||||||
@@ -40,3 +74,19 @@ func parse_digit(_ p: Parser) -> Int? {
|
|||||||
|
|
||||||
return 0
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
||||||
EventActionBar(event: event,
|
EventActionBar(event: event,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ struct EventView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(event)
|
NoteContentView(event: event, profiles: profiles)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
|
|
||||||
|
|||||||
@@ -7,46 +7,69 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
func NoteContentView(_ ev: NostrEvent) -> some View {
|
|
||||||
let txt = parse_mentions(content: ev.content, tags: ev.tags)
|
func render_note_content(ev: NostrEvent, profiles: Profiles) -> String {
|
||||||
.reduce("") { str, block in
|
return ev.blocks.reduce("") { str, block in
|
||||||
switch block {
|
switch block {
|
||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
return str + mention_str(m)
|
return str + mention_str(m, profiles: profiles)
|
||||||
case .text(let txt):
|
case .text(let txt):
|
||||||
return str + 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 {
|
switch m.type {
|
||||||
case .pubkey:
|
case .pubkey:
|
||||||
let pk = m.ref.ref_id
|
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:
|
case .event:
|
||||||
let evid = m.ref.ref_id
|
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 {
|
struct NoteContentView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -12,26 +12,6 @@ enum NostrPostResult {
|
|||||||
case cancel
|
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 {
|
struct PostView: View {
|
||||||
@State var post: String = ""
|
@State var post: String = ""
|
||||||
@FocusState var focus: Bool
|
@FocusState var focus: Bool
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ struct ProfileView: View {
|
|||||||
ProfileName(pubkey: pubkey, profile: data)
|
ProfileName(pubkey: pubkey, profile: data)
|
||||||
.font(.title)
|
.font(.title)
|
||||||
//.border(Color.green)
|
//.border(Color.green)
|
||||||
|
Text("\(pubkey)")
|
||||||
|
.textSelection(.enabled)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(id_to_color(pubkey))
|
||||||
}
|
}
|
||||||
Text(data?.about ?? "")
|
Text(data?.about ?? "")
|
||||||
//.border(Color.red)
|
//.border(Color.red)
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ struct PubkeyView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func abbrev_pubkey(_ pubkey: String) -> String {
|
func abbrev_pubkey(_ pubkey: String, amount: Int = 8) -> String {
|
||||||
return pubkey.prefix(4) + ":" + pubkey.suffix(4)
|
return pubkey.prefix(amount) + ":" + pubkey.suffix(amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -43,6 +43,127 @@ class damusTests: XCTestCase {
|
|||||||
XCTAssertTrue(parsed[2].is_text)
|
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 {
|
func testParseInvalidMention() throws {
|
||||||
let parsed = parse_mentions(content: "this is #[0] a mention", tags: [])
|
let parsed = parse_mentions(content: "this is #[0] a mention", tags: [])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user