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:
parent
73652513d9
commit
0eb1372937
@ -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 */,
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
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?
|
||||
|
||||
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 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)' }"
|
||||
|
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 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] = [:]
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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: [])
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user