diff --git a/damus/Models/EventRef.swift b/damus/Models/EventRef.swift index 475545e1..11055ec6 100644 --- a/damus/Models/EventRef.swift +++ b/damus/Models/EventRef.swift @@ -78,6 +78,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set { } case .text: return + case .hashtag: + return } } } diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift index 38a9dcc0..8fb1b019 100644 --- a/damus/Models/Mentions.swift +++ b/damus/Models/Mentions.swift @@ -36,12 +36,20 @@ struct IdBlock: Identifiable { enum Block { case text(String) case mention(Mention) + case hashtag(String) - var is_text: Bool { - if case .text = self { - return true + var is_hashtag: String? { + if case .hashtag(let htag) = self { + return htag } - return false + return nil + } + + var is_text: String? { + if case .text(let txt) = self { + return txt + } + return nil } var is_mention: Bool { @@ -59,6 +67,8 @@ func render_blocks(blocks: [Block]) -> String { return str + "#[\(m.index)]" case .text(let txt): return str + txt + case .hashtag(let htag): + return "#" + htag } } } @@ -73,9 +83,8 @@ func parse_mentions(content: String, tags: [[String]]) -> [Block] { var starting_from: Int = 0 while p.pos < content.count { - if (!consume_until(p, match: { $0 == "#" })) { - blocks.append(parse_textblock(str: p.str, from: starting_from, to: p.str.count)) - return blocks + if !consume_until(p, match: { $0 == "#" }) { + break } let pre_mention = p.pos @@ -83,14 +92,61 @@ func parse_mentions(content: String, tags: [[String]]) -> [Block] { blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention)) blocks.append(.mention(mention)) starting_from = p.pos + } else if let hashtag = parse_hashtag(p) { + blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention)) + blocks.append(.hashtag(hashtag)) + starting_from = p.pos } else { p.pos += 1 } } + if p.str.count - starting_from > 0 { + blocks.append(parse_textblock(str: p.str, from: starting_from, to: p.str.count)) + } + return blocks } +func parse_while(_ p: Parser, match: (Character) -> Bool) -> String? { + var i: Int = 0 + let sub = substring(p.str, start: p.pos, end: p.str.count) + let start = p.pos + for c in sub { + if match(c) { + p.pos += 1 + } else { + break + } + i += 1 + } + + let end = start + i + if start == end { + return nil + } + return String(substring(p.str, start: start, end: end)) +} + +func is_hashtag_char(_ c: Character) -> Bool { + return c.isLetter || c.isNumber +} + +func parse_hashtag(_ p: Parser) -> String? { + let start = p.pos + + if !parse_char(p, "#") { + return nil + } + + guard let str = parse_while(p, match: is_hashtag_char) else { + p.pos = start + return nil + } + + return str +} + func parse_mention(_ p: Parser, tags: [[String]]) -> Mention? { let start = p.pos diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 0d733c1d..88929c5e 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -15,6 +15,8 @@ func render_note_content(ev: NostrEvent, profiles: Profiles) -> String { return str + mention_str(m, profiles: profiles) case .text(let txt): return str + txt + case .hashtag(let htag): + return str + hashtag_str(htag) } } } @@ -49,14 +51,18 @@ struct NoteContentView: View { if m.type == .pubkey && m.ref.ref_id == profile.pubkey { content = render_note_content(ev: event, profiles: profiles) } - case .text: - return + case .text: return + case .hashtag: return } } } } } +func hashtag_str(_ htag: String) -> String { + return "[#\(htag)](nostr:hashtag:\(htag))" +} + func mention_str(_ m: Mention, profiles: Profiles) -> String { switch m.type { case .pubkey: diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift index fee9c5e3..1b7dd361 100644 --- a/damus/Views/ProfileView.swift +++ b/damus/Views/ProfileView.swift @@ -22,9 +22,9 @@ struct ProfileView: View { @EnvironmentObject var profiles: Profiles var TopSection: some View { - VStack{ + VStack(alignment: .leading) { let data = profiles.lookup(id: profile.pubkey) - HStack { + HStack(alignment: .top) { ProfilePicView(pubkey: profile.pubkey, size: PFP_SIZE!, highlight: .custom(Color.black, 2), image_cache: damus.image_cache) .environmentObject(profiles) diff --git a/damusTests/ReplyTests.swift b/damusTests/ReplyTests.swift index 85552d51..e4a42032 100644 --- a/damusTests/ReplyTests.swift +++ b/damusTests/ReplyTests.swift @@ -97,9 +97,9 @@ class ReplyTests: XCTestCase { XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 3) - XCTAssertTrue(parsed[0].is_text) - XCTAssertTrue(parsed[1].is_mention) - XCTAssertTrue(parsed[2].is_text) + XCTAssertEqual(parsed[0].is_text!, "this is ") + XCTAssertNotNil(parsed[1].is_mention) + XCTAssertEqual(parsed[2].is_text!, " a mention") } func testEmptyPostReference() throws { @@ -349,14 +349,7 @@ class ReplyTests: XCTestCase { XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) - XCTAssertTrue(parsed[0].is_text) - - guard case .text(let txt) = parsed[0] else { - XCTAssertTrue(false) - return - } - - XCTAssertEqual(txt, "this is #[0] a mention") + XCTAssertEqual(parsed[0].is_text!, "this is #[0] a mention") } diff --git a/damusTests/damusTests.swift b/damusTests/damusTests.swift index f04aaa8f..3bb19422 100644 --- a/damusTests/damusTests.swift +++ b/damusTests/damusTests.swift @@ -61,7 +61,7 @@ class damusTests: XCTestCase { XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) - XCTAssertTrue(parsed[0].is_text) + XCTAssertNotNil(parsed[0].is_text) } func testParseMentionBlank() { @@ -71,12 +71,31 @@ class damusTests: XCTestCase { XCTAssertEqual(parsed.count, 0) } + func testParseHashtag() { + let parsed = parse_mentions(content: "some hashtag #bitcoin derp", tags: []) + + XCTAssertNotNil(parsed) + XCTAssertEqual(parsed.count, 3) + XCTAssertEqual(parsed[0].is_text!, "some hashtag ") + XCTAssertEqual(parsed[1].is_hashtag!, "bitcoin") + XCTAssertEqual(parsed[2].is_text!, " derp") + } + + func testParseHashtagEnd() { + let parsed = parse_mentions(content: "some hashtag #bitcoin", tags: []) + + XCTAssertNotNil(parsed) + XCTAssertEqual(parsed.count, 2) + XCTAssertEqual(parsed[0].is_text!, "some hashtag ") + XCTAssertEqual(parsed[1].is_hashtag!, "bitcoin") + } + func testParseMentionOnlyText() { let parsed = parse_mentions(content: "there is no mention here", tags: [["e", "event_id"]]) XCTAssertNotNil(parsed) XCTAssertEqual(parsed.count, 1) - XCTAssertTrue(parsed[0].is_text) + XCTAssertEqual(parsed[0].is_text!, "there is no mention here") guard case .text(let txt) = parsed[0] else { XCTAssertTrue(false)