Skip to content

Commit

Permalink
Merge pull request #1582 from planetary-social/remove-media-link
Browse files Browse the repository at this point in the history
#1487: Remove media links from notes
  • Loading branch information
joshuatbrown authored Oct 4, 2024
2 parents 1ac90b7 + 503192c commit 8b51a24
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Release Notes
- Updated the media viewer that displays images, videos, and web pages inside of notes. [#1538](https://github.com/planetary-social/nos/issues/1538)
- Removed image and video link text from notes. Now only the images and videos will appear, without the link. [#1487](https://github.com/planetary-social/nos/issues/1487)
- Added new translations for the app so you can use it in Korean, Chinese Simplified, Swedish, and more! Thanks to alternative, 안마리 (everyscreennetwork), Andypsl8, Dženan (Dzenan), ObjectifMoon, ra5pvt1n, and everyone else who contributed translations on Crowdin!

### Internal Changes
Expand Down
16 changes: 14 additions & 2 deletions Nos/Extensions/URL+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,26 @@ extension URL {

return URL(string: "https://\(absoluteString)") ?? self
}

/// Returns `true` if the URL is an image, as determined by the path extension.
/// Currently supports `png`, `jpg`, `jpeg`, `gif`, and `webp`. For all other path extensions, returns `false`.
var isImage: Bool {
let imageExtensions = ["png", "jpg", "jpeg", "gif", "webp"]
return imageExtensions.contains(pathExtension.lowercased())
}


/// Returns `true` if the URL is media; that is, an image or a video. Shorthand for ``isImage`` `||` ``isVideo``.
var isMedia: Bool {
isImage || isVideo
}

/// Returns `true` if the URL is a video, as determined by the path extension.
/// Currently supports `mp4`. For all other path extensions, returns `false`.
var isVideo: Bool {
let videoExtensions = ["mp4"]
return videoExtensions.contains(pathExtension.lowercased())
}

/// Returns `absoluteString` but without a trailing slash if one exists. If no trailing slash exists, returns
/// `absoluteString` as-is.
/// - Returns: The absolute string of the URL without a trailing slash.
Expand Down
49 changes: 31 additions & 18 deletions Nos/Models/URLParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import Logger

/// Parses unformatted urls in a string and replace them with markdown links
struct URLParser {
// swiftlint:disable line_length
/// A regular expression pattern for matching URLs.
/// - Note: Uses rules from the [Domain Name System](https://en.wikipedia.org/wiki/Domain_Name_System#Domain_name_syntax,_internationalization)
/// page on Wikipedia.
static let urlRegex = "(\\s*)(?<url>((https?://){1}|(?<![\\w@.]))([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\\.){1,127}[a-z]{2,63}\\b[-a-zA-Z0-9@:%_\\+.~#?&/=]*(?<![.,!?\\)\\]]))"
// swiftlint:enable line_length

/// Returns an array with all unformatted urls
func findUnformattedURLs(in content: String) throws -> [URL] {
// swiftlint:disable line_length
Expand All @@ -21,9 +28,9 @@ struct URLParser {
return links
}

/// Creates a new string with all URLs and any preceding and trailing
/// whitespace removed and removed duplicate newlines, and returns the new
/// string and an array of all the URLs.
/// Creates a new string with all URLs and any preceding and trailing whitespace removed and duplicate newlines
/// removed, and returns the new string and an array of all the URLs. The new string will not contain URLs that
/// link to media (image or video).
func replaceUnformattedURLs(
in content: String
) -> (String, [URL]) {
Expand All @@ -45,21 +52,18 @@ struct URLParser {

return (mutableString as String, urls)
}


/// Replaces raw URLs with Markdown links in the given `mutableString`. Removes media URLs (image and video) from
/// `mutableString` and converts all other URLs to pretty Markdown links for display.
/// - Parameter mutableString: The mutable string that may contain URLs and will be modified for display.
/// - Returns: All URLs found in the given `mutableString`.
private func replaceRawDomainsWithMarkdownLinks(
in mutableString: NSMutableString
) -> [URL] {
// The following pattern uses rules from the Domain Name System page on
// Wikipedia:
// https://en.wikipedia.org/wiki/Domain_Name_System#Domain_name_syntax,_internationalization

// swiftlint:disable:next line_length
let regexPattern = "(\\s*)(?<url>((https?://){1}|(?<![\\w@.]))([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\\.){1,127}[a-z]{2,63}\\b[-a-zA-Z0-9@:%_\\+.~#?&/=]*(?<![.,!?\\)\\]]))"

var urls: [URL] = []
do {
let string = String(mutableString)
let regex = try NSRegularExpression(pattern: regexPattern)
let regex = try NSRegularExpression(pattern: Self.urlRegex)
let range = NSRange(location: 0, length: mutableString.length)

let matches = regex.matches(in: string, range: range).reversed()
Expand All @@ -70,12 +74,21 @@ struct URLParser {
// maintain original order of links by inserting at index 0
// (we're looping in reverse)
urls.insert(url.addingSchemeIfNeeded(), at: 0)
let prettyURL = url.truncatedMarkdownLink
regex.replaceMatches(
in: mutableString,
range: match.range,
withTemplate: "$1\(prettyURL)"
)

if url.isMedia {
regex.replaceMatches(
in: mutableString,
range: match.range,
withTemplate: ""
)
} else {
let prettyURL = url.truncatedMarkdownLink
regex.replaceMatches(
in: mutableString,
range: match.range,
withTemplate: "$1\(prettyURL)"
)
}
}
}
} catch {
Expand Down
32 changes: 32 additions & 0 deletions NosTests/Extensions/URLExtensionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,38 @@ final class URLExtensionTests: XCTestCase {
XCTAssertTrue(url.isImage)
}

func test_isMedia_returns_false_for_non_media() throws {
let youTubeURL = try XCTUnwrap(URL(string: "https://www.youtube.com/watch?v=sB6HY8r983c"))
XCTAssertFalse(youTubeURL.isMedia)

let pdfURL = try XCTUnwrap(URL(string: "http://example.com/test.pdf"))
XCTAssertFalse(pdfURL.isMedia)

let webURL = try XCTUnwrap(URL(string: "https://nos.social"))
XCTAssertFalse(webURL.isMedia)
}

func test_isMedia_returns_true_for_media() throws {
let pngURL = try XCTUnwrap(URL(string: "http://example.com/test.png"))
XCTAssertTrue(pngURL.isMedia)

let webpURL = try XCTUnwrap(URL(string: "http://example.com/test.webp"))
XCTAssertTrue(webpURL.isMedia)

let mp4URL = try XCTUnwrap(URL(string: "http://example.com/test.mp4"))
XCTAssertTrue(mp4URL.isMedia)
}

func test_isVideo_returns_false_for_mp3() throws {
let url = try XCTUnwrap(URL(string: "http://example.com/test.mp3"))
XCTAssertFalse(url.isVideo)
}

func test_isVideo_returns_true_for_mp4() throws {
let url = try XCTUnwrap(URL(string: "http://example.com/test.mp4"))
XCTAssertTrue(url.isVideo)
}

func test_strippingTrailingSlash_when_no_trailing_slash() throws {
let url = try XCTUnwrap(URL(string: "wss://relay.nos.social"))
XCTAssertEqual(url.strippingTrailingSlash(), "wss://relay.nos.social")
Expand Down
8 changes: 3 additions & 5 deletions NosTests/Models/URLParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class URLParserTests: XCTestCase {
func testExtractURLs() throws {
// swiftlint:disable line_length
let string = "Classifieds incoming... 👀\n\nhttps://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg"
let expectedString = "Classifieds incoming... 👀\n\n[nostr.build...](https://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg)"
let expectedString = "Classifieds incoming... 👀"
// swiftlint:enable line_length
let expectedURLs = [
URL(string: "https://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg")!
Expand Down Expand Up @@ -61,8 +61,6 @@ class URLParserTests: XCTestCase {
"""
let expectedString = """
A few links...
[nostr.build...](https://nostr.build/i/2170fa01a69bca5ad0334430ccb993e41bb47eb15a4b4dbdfbee45585f63d503.jpg)
[nos.social](https://nos.social)
[nostr.com...](https://www.nostr.com/get-started)
"""
Expand Down Expand Up @@ -125,7 +123,7 @@ class URLParserTests: XCTestCase {

func testExtractURLsWithImage() throws {
let string = "Hello, world!https://cdn.ymaws.com/footprints.jpg"
let expectedString = "Hello, world![cdn.ymaws.com...](https://cdn.ymaws.com/footprints.jpg)"
let expectedString = "Hello, world!"
let expectedURLs = [
URL(string: "https://cdn.ymaws.com/footprints.jpg")!
]
Expand All @@ -138,7 +136,7 @@ class URLParserTests: XCTestCase {

func testExtractURLsWithImageWithExtraNewlines() throws {
let string = "https://cdn.ymaws.com/footprints.jpg\n\nHello, world!"
let expectedString = "[cdn.ymaws.com...](https://cdn.ymaws.com/footprints.jpg)\n\nHello, world!"
let expectedString = "Hello, world!"
let expectedURLs = [
URL(string: "https://cdn.ymaws.com/footprints.jpg")!
]
Expand Down

0 comments on commit 8b51a24

Please sign in to comment.