Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse and publish hashtags in notes when posting #1693

Merged
merged 10 commits into from
Dec 10, 2024
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ 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)
- Fixed galleries expanding past the width of the screen when there are lots of links or images. [#24](https://github.com/verse-pbc/issues/issues/24)
- Added support for user setting and displaying pronouns.
- Added display of website urls for user profiles.

## [1.0.2] - 2024-11-26Z

### Release Notes
- Fix typo in minimum age warning
joshuatbrown marked this conversation as resolved.
Show resolved Hide resolved
- 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)
- Adjusted note header UI to make it more readable. [#23](https://github.com/verse-pbc/issues/issues/23)
Expand Down
21 changes: 19 additions & 2 deletions Nos/Models/NoteParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
71 changes: 70 additions & 1 deletion NosTests/Models/NoteParserTests+Parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 🍐"
Expand All @@ -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)
}
}
19 changes: 1 addition & 18 deletions NosTests/Models/NoteParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down