From 57999955220a8c9311d7e2e13e2d1840863c6bf9 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Wed, 30 Oct 2024 17:33:46 -0400 Subject: [PATCH 1/4] parse and publish hashtags in notes when posting --- Nos/Models/NoteParser.swift | 21 +++++- NosTests/Models/NoteParserTests+Parse.swift | 71 ++++++++++++++++++++- NosTests/Models/NoteParserTests.swift | 19 +----- 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/Nos/Models/NoteParser.swift b/Nos/Models/NoteParser.swift index 7c655c3f8..f0ee84990 100644 --- a/Nos/Models/NoteParser.swift +++ b/Nos/Models/NoteParser.swift @@ -19,9 +19,26 @@ struct NoteParser { /// Parses attributed text generated when composing a note and returns /// the content and tags. func parse(attributedText: AttributedString) -> (String, [[String]]) { - cleanLinks(in: attributedText) + let (content, tags) = cleanLinks(in: attributedText) + let hashtags = hashtags(in: content) + return (content, tags + hashtags) } - + + func hashtags(in content: String) -> [[String]] { + let pattern = "(?<=^|\\s)#([a-zA-Z0-9]{2,256})(?=\\s|$)" + let regex = try! NSRegularExpression(pattern: pattern) // swiftlint:disable:this force_try + let matches = regex.matches(in: content, range: NSRange(content.startIndex..., in: content)) + + let hashtags = matches.map { match -> [String] in + if let range = Range(match.range(at: 1), in: content) { + return ["t", String(content[range])] + } + return [] + } + + return hashtags + } + /// Parses the content and tags stored in a note and returns an attributed text with tagged entities replaced /// with readable names. func parse(content: String, tags: [[String]], context: NSManagedObjectContext) -> AttributedString { diff --git a/NosTests/Models/NoteParserTests+Parse.swift b/NosTests/Models/NoteParserTests+Parse.swift index 8f76bdcd9..82243bb4f 100644 --- a/NosTests/Models/NoteParserTests+Parse.swift +++ b/NosTests/Models/NoteParserTests+Parse.swift @@ -2,7 +2,7 @@ import Foundation import UIKit import XCTest -/// Collection of tests that exercise NoteParser.parse() function. This fubction +/// Collection of tests that exercise NoteParser.parse() function. This function /// is the one Nos uses for converting editor generated text to note content /// when publishing. extension NoteParserTests { @@ -69,6 +69,23 @@ extension NoteParserTests { XCTAssertEqual(content, expected) } + /// Example taken from [NIP-27](https://github.com/nostr-protocol/nips/blob/master/27.md) + func testMentionWithNpub() throws { + let mention = "@mattn" + let npub = "npub1937vv2nf06360qn9y8el6d8sevnndy7tuh5nzre4gj05xc32tnwqauhaj6" + let hex = "2c7cc62a697ea3a7826521f3fd34f0cb273693cbe5e9310f35449f43622a5cdc" + let link = "nostr:\(npub)" + let markdown = "hello [\(mention)](\(link))" + let attributedString = try AttributedString(markdown: markdown) + let (content, tags) = sut.parse( + attributedText: attributedString + ) + let expectedContent = "hello nostr:\(npub)" + let expectedTags = [["p", hex]] + XCTAssertEqual(content, expectedContent) + XCTAssertEqual(tags, expectedTags) + } + @MainActor func testTwoMentionsWithEmojiBeforeAndAfter() throws { // Arrange let name = "🍐 mattn 🍐" @@ -91,4 +108,56 @@ extension NoteParserTests { // Assert XCTAssertEqual(content, expected) } + + @MainActor func test_parse_returns_hashtag() throws { + // Arrange + let text = "#photography" + + // Act + let expected = [["t", "photography"]] + let result = sut.hashtags(in: text) + + // Assert + XCTAssertEqual(result, expected) + } + + @MainActor func test_parse_returns_multiple_hashtags() throws { + // Arrange + let text = "#photography #birds #canada" + + // Act + let expected = [["t", "photography"], ["t", "birds"], ["t", "canada"]] + let result = sut.hashtags(in: text) + + // Assert + XCTAssertEqual(result, expected) + } + + @MainActor func test_parse_returns_no_hashtags() throws { + // Arrange + let text = "example.com#photography" + + // Act + let expected: [[String]] = [] + let result = sut.hashtags(in: text) + + // Assert + XCTAssertEqual(result, expected) + } + + func test_parse_mention_and_hashtag() throws { + let mention = "@mattn" + let npub = "npub1937vv2nf06360qn9y8el6d8sevnndy7tuh5nzre4gj05xc32tnwqauhaj6" + let hex = "2c7cc62a697ea3a7826521f3fd34f0cb273693cbe5e9310f35449f43622a5cdc" + let link = "nostr:\(npub)" + let markdown = "hello [\(mention)](\(link)) #greetings #hi" + let attributedString = try AttributedString(markdown: markdown) + let (content, tags) = sut.parse( + attributedText: attributedString + ) + let expectedContent = "hello nostr:\(npub) #greetings #hi" + let expectedTags = [["p", hex], ["t", "greetings"], ["t", "hi"]] + XCTAssertEqual(content, expectedContent) + XCTAssertEqual(tags, expectedTags) + } } diff --git a/NosTests/Models/NoteParserTests.swift b/NosTests/Models/NoteParserTests.swift index 80a0d024c..2796e2cbd 100644 --- a/NosTests/Models/NoteParserTests.swift +++ b/NosTests/Models/NoteParserTests.swift @@ -93,24 +93,7 @@ final class NoteParserTests: CoreDataTestCase { XCTAssertEqual(links[safe: 0]?.key, nip05) XCTAssertEqual(links[safe: 0]?.value, URL(string: webLink)) } - - /// Example taken from [NIP-27](https://github.com/nostr-protocol/nips/blob/master/27.md) - func testMentionWithNPub() throws { - let mention = "@mattn" - let npub = "npub1937vv2nf06360qn9y8el6d8sevnndy7tuh5nzre4gj05xc32tnwqauhaj6" - let hex = "2c7cc62a697ea3a7826521f3fd34f0cb273693cbe5e9310f35449f43622a5cdc" - let link = "nostr:\(npub)" - let markdown = "hello [\(mention)](\(link))" - let attributedString = try AttributedString(markdown: markdown) - let (content, tags) = sut.parse( - attributedText: attributedString - ) - let expectedContent = "hello nostr:\(npub)" - let expectedTags = [["p", hex]] - XCTAssertEqual(content, expectedContent) - XCTAssertEqual(tags, expectedTags) - } - + @MainActor func testContentWithMixedMentions() throws { let content = "hello nostr:npub1937vv2nf06360qn9y8el6d8sevnndy7tuh5nzre4gj05xc32tnwqauhaj6 and #[1]" let displayName1 = "npub1937vv..." From 94b79634169cf2ecee6b2879385299517e28ad27 Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Mon, 18 Nov 2024 14:49:00 -0500 Subject: [PATCH 2/4] update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 781c17400..3b8a77834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Release Notes -- Fix typo in minimum age warning +- Parse and publish hashtags in notes when posting. +- Fix typo in minimum age warning. - Fix crash when tapping Post button on macOS. [#1687](https://github.com/planetary-social/nos/issues/1687) - Fix tapping follower notification not opening follower profile. [#11](https://github.com/verse-pbc/issues/issues/11) From b3dabedf35429732af12bf50e75cc1dc8911330b Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Tue, 3 Dec 2024 15:27:28 -0500 Subject: [PATCH 3/4] lowercase hashtags; terminate hashtags with punctuation --- Nos/Models/NoteParser.swift | 4 ++-- NosTests/Models/NoteParserTests+Parse.swift | 24 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Nos/Models/NoteParser.swift b/Nos/Models/NoteParser.swift index f0ee84990..dd4a18634 100644 --- a/Nos/Models/NoteParser.swift +++ b/Nos/Models/NoteParser.swift @@ -25,13 +25,13 @@ struct NoteParser { } func hashtags(in content: String) -> [[String]] { - let pattern = "(?<=^|\\s)#([a-zA-Z0-9]{2,256})(?=\\s|$)" + let pattern = "(?<=^|\\s)#([a-zA-Z0-9]{2,256})(?=\\s|[.,!?;:]|$)" let regex = try! NSRegularExpression(pattern: pattern) // swiftlint:disable:this force_try let matches = regex.matches(in: content, range: NSRange(content.startIndex..., in: content)) let hashtags = matches.map { match -> [String] in if let range = Range(match.range(at: 1), in: content) { - return ["t", String(content[range])] + return ["t", String(content[range].lowercased())] } return [] } diff --git a/NosTests/Models/NoteParserTests+Parse.swift b/NosTests/Models/NoteParserTests+Parse.swift index 82243bb4f..5668d7699 100644 --- a/NosTests/Models/NoteParserTests+Parse.swift +++ b/NosTests/Models/NoteParserTests+Parse.swift @@ -121,6 +121,30 @@ extension NoteParserTests { XCTAssertEqual(result, expected) } + @MainActor func test_parse_returns_hashtag_lowercased() throws { + // Arrange + let text = "#DOGS" + + // Act + let expected = [["t", "dogs"]] + let result = sut.hashtags(in: text) + + // Assert + XCTAssertEqual(result, expected) + } + + @MainActor func test_parse_returns_hashtag_without_punctuation() throws { + // Arrange + let text = "check out my #hashtag! #hello, #world." + + // Act + let expected = [["t", "hashtag"], ["t", "hello"], ["t", "world"]] + let result = sut.hashtags(in: text) + + // Assert + XCTAssertEqual(result, expected) + } + @MainActor func test_parse_returns_multiple_hashtags() throws { // Arrange let text = "#photography #birds #canada" From 52d4056715e72348ec0c8534a864a1443b2011ad Mon Sep 17 00:00:00 2001 From: Josh Brown Date: Wed, 4 Dec 2024 17:13:10 -0500 Subject: [PATCH 4/4] fix CHANGELOG after release and merge --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d1300d5..d0d4ef525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Release Notes +- Nos now publishes the hashtags it finds in your note when you post. This means it works the way you’ve always expected it to work. [#44](https://github.com/verse-pbc/issues/issues/44) + ## [1.0.3] - 2024-12-04Z ### Release Notes -- Nos now publishes the hashtags it finds in your note when you post. This means it works the way you’ve always expected it to work. [#44](https://github.com/verse-pbc/issues/issues/44) - Added support for user setting and displaying pronouns. - Added display of website urls for user profiles. - Updated note header UI to make it more readable. [#23](https://github.com/verse-pbc/issues/issues/23)