mirror of
git://jb55.com/damus
synced 2024-09-19 11:43:44 +00:00
Parse links in profiles
Changelog-Added: Parse links in profiles
This commit is contained in:
commit
6f5f86114b
@ -49,6 +49,7 @@
|
|||||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
|
4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
|
||||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA328296DEE006E126D /* SearchModel.swift */; };
|
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA328296DEE006E126D /* SearchModel.swift */; };
|
||||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA728297703006E126D /* InsertSort.swift */; };
|
4C363AA828297703006E126D /* InsertSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA728297703006E126D /* InsertSort.swift */; };
|
||||||
|
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3A1D322960DB0500558C0F /* Markdown.swift */; };
|
||||||
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; };
|
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; };
|
||||||
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79C2833036D00E1F516 /* FollowingView.swift */; };
|
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79C2833036D00E1F516 /* FollowingView.swift */; };
|
||||||
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79E2833115300E1F516 /* FollowButtonView.swift */; };
|
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79E2833115300E1F516 /* FollowButtonView.swift */; };
|
||||||
@ -203,6 +204,7 @@
|
|||||||
4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||||
4C363AA328296DEE006E126D /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
|
4C363AA328296DEE006E126D /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
|
||||||
4C363AA728297703006E126D /* InsertSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertSort.swift; sourceTree = "<group>"; };
|
4C363AA728297703006E126D /* InsertSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertSort.swift; sourceTree = "<group>"; };
|
||||||
|
4C3A1D322960DB0500558C0F /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = "<group>"; };
|
||||||
4C3AC79A28306D7B00E1F516 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = "<group>"; };
|
4C3AC79A28306D7B00E1F516 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = "<group>"; };
|
||||||
4C3AC79C2833036D00E1F516 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = "<group>"; };
|
4C3AC79C2833036D00E1F516 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = "<group>"; };
|
||||||
4C3AC79E2833115300E1F516 /* FollowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowButtonView.swift; sourceTree = "<group>"; };
|
4C3AC79E2833115300E1F516 /* FollowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowButtonView.swift; sourceTree = "<group>"; };
|
||||||
@ -527,6 +529,7 @@
|
|||||||
4C7FF7D628233637009601DB /* Util */ = {
|
4C7FF7D628233637009601DB /* Util */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4C3A1D322960DB0500558C0F /* Markdown.swift */,
|
||||||
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
||||||
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
||||||
4C363A8328233689006E126D /* Parser.swift */,
|
4C363A8328233689006E126D /* Parser.swift */,
|
||||||
@ -884,6 +887,7 @@
|
|||||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
|
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
|
||||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
|
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
|
||||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||||
|
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
|
||||||
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
|
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
|
||||||
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
||||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
||||||
|
43
damus/Util/Markdown.swift
Normal file
43
damus/Util/Markdown.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// Markdown.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Lionello Lunesu on 2022-12-28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct Markdown {
|
||||||
|
private let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||||
|
|
||||||
|
/// Ensure the specified URL has a scheme by prepending "https://" if it's absent.
|
||||||
|
static func withScheme(_ url: any StringProtocol) -> any StringProtocol {
|
||||||
|
return url.contains("://") ? url : "https://" + url
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func parse(content: String) -> AttributedString {
|
||||||
|
// Similar to the parsing in NoteContentView
|
||||||
|
let md_opts: AttributedString.MarkdownParsingOptions =
|
||||||
|
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||||
|
|
||||||
|
if let txt = try? AttributedString(markdown: content, options: md_opts) {
|
||||||
|
return txt
|
||||||
|
} else {
|
||||||
|
return AttributedString(stringLiteral: content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process the input text and add markdown for any embedded URLs.
|
||||||
|
public func process(_ input: String) -> AttributedString {
|
||||||
|
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
|
||||||
|
var output = input
|
||||||
|
// Start with the last match, because replacing the first would invalidate all subsequent indices
|
||||||
|
for match in matches.reversed() {
|
||||||
|
guard let range = Range(match.range, in: input) else { continue }
|
||||||
|
let url = input[range]
|
||||||
|
output.replaceSubrange(range, with: "[\(url)](\(Markdown.withScheme(url)))")
|
||||||
|
}
|
||||||
|
// TODO: escape unintentional markdown
|
||||||
|
return Markdown.parse(content: output)
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,9 @@ import SwiftUI
|
|||||||
struct FollowUserView: View {
|
struct FollowUserView: View {
|
||||||
let target: FollowTarget
|
let target: FollowTarget
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
|
|
||||||
|
static let markdown = Markdown()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
let pmodel = ProfileModel(pubkey: target.pubkey, damus: damus_state)
|
let pmodel = ProfileModel(pubkey: target.pubkey, damus: damus_state)
|
||||||
@ -23,8 +25,8 @@ struct FollowUserView: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
let profile = damus_state.profiles.lookup(id: target.pubkey)
|
let profile = damus_state.profiles.lookup(id: target.pubkey)
|
||||||
ProfileName(pubkey: target.pubkey, profile: profile, contacts: damus_state.contacts, show_friend_confirmed: false)
|
ProfileName(pubkey: target.pubkey, profile: profile, contacts: damus_state.contacts, show_friend_confirmed: false)
|
||||||
if let about = profile.flatMap { $0.about } {
|
if let about = profile?.about {
|
||||||
Text(about)
|
Text(FollowUserView.markdown.process(about))
|
||||||
.lineLimit(3)
|
.lineLimit(3)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
}
|
}
|
||||||
|
@ -70,17 +70,10 @@ struct NoteContentView: View {
|
|||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
|
|
||||||
func MainContent() -> some View {
|
func MainContent() -> some View {
|
||||||
let md_opts: AttributedString.MarkdownParsingOptions =
|
|
||||||
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
|
||||||
|
|
||||||
return VStack(alignment: .leading) {
|
return VStack(alignment: .leading) {
|
||||||
if let txt = try? AttributedString(markdown: artifacts.content, options: md_opts) {
|
Text(Markdown.parse(content: artifacts.content))
|
||||||
Text(txt)
|
.font(eventviewsize_to_font(size))
|
||||||
.font(eventviewsize_to_font(size))
|
|
||||||
} else {
|
|
||||||
Text(artifacts.content)
|
|
||||||
.font(eventviewsize_to_font(size))
|
|
||||||
}
|
|
||||||
if show_images && artifacts.images.count > 0 {
|
if show_images && artifacts.images.count > 0 {
|
||||||
ImageCarousel(urls: artifacts.images)
|
ImageCarousel(urls: artifacts.images)
|
||||||
} else if !show_images && artifacts.images.count > 0 {
|
} else if !show_images && artifacts.images.count > 0 {
|
||||||
|
@ -152,7 +152,9 @@ struct ProfileView: View {
|
|||||||
.environmentObject(user_settings)
|
.environmentObject(user_settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static let markdown = Markdown()
|
||||||
|
|
||||||
var DMButton: some View {
|
var DMButton: some View {
|
||||||
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
|
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
|
||||||
let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey)
|
let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey)
|
||||||
@ -188,7 +190,6 @@ struct ProfileView: View {
|
|||||||
|
|
||||||
DMButton
|
DMButton
|
||||||
|
|
||||||
|
|
||||||
if profile.pubkey != damus_state.pubkey {
|
if profile.pubkey != damus_state.pubkey {
|
||||||
FollowButtonView(
|
FollowButtonView(
|
||||||
target: profile.get_follow_target(),
|
target: profile.get_follow_target(),
|
||||||
@ -205,7 +206,7 @@ struct ProfileView: View {
|
|||||||
ProfileNameView(pubkey: profile.pubkey, profile: data, contacts: damus_state.contacts)
|
ProfileNameView(pubkey: profile.pubkey, profile: data, contacts: damus_state.contacts)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
|
|
||||||
Text(data?.about ?? "")
|
Text(ProfileView.markdown.process(data?.about ?? ""))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
43
damusTests/MarkdownTests.swift
Normal file
43
damusTests/MarkdownTests.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// MarkdownTests.swift
|
||||||
|
// damusTests
|
||||||
|
//
|
||||||
|
// Created by Lionello Lunesu on 2022-12-28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import damus
|
||||||
|
|
||||||
|
class MarkdownTests: XCTestCase {
|
||||||
|
let md_opts: AttributedString.MarkdownParsingOptions =
|
||||||
|
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_convert_link() throws {
|
||||||
|
let helper = Markdown()
|
||||||
|
let md = helper.process("prologue https://nostr.build epilogue")
|
||||||
|
let expected = try AttributedString(markdown: "prologue [https://nostr.build](https://nostr.build) epilogue", options: md_opts)
|
||||||
|
XCTAssertEqual(md, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_convert_link_no_scheme() throws {
|
||||||
|
let helper = Markdown()
|
||||||
|
let md = helper.process("prologue damus.io epilogue")
|
||||||
|
let expected = try AttributedString(markdown: "prologue [damus.io](https://damus.io) epilogue", options: md_opts)
|
||||||
|
XCTAssertEqual(md, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_convert_links() throws {
|
||||||
|
let helper = Markdown()
|
||||||
|
let md = helper.process("prologue damus.io https://nostr.build epilogue")
|
||||||
|
let expected = try AttributedString(markdown: "prologue [damus.io](https://damus.io) [https://nostr.build](https://nostr.build) epilogue", options: md_opts)
|
||||||
|
XCTAssertEqual(md, expected)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user