From 2237b7bbb1382f706c7d49317f36c7c8fba7a678 Mon Sep 17 00:00:00 2001 From: Bob <160986752+bob2402@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:23:56 +0800 Subject: [PATCH] refactor parseContent (#458) Co-authored-by: BlowaterNostr --- app/UI/app_update.tsx | 6 +- app/UI/message-panel.tsx | 44 +++-- app/UI/message.test.ts | 349 +++++++++++++++++++++++---------------- app/UI/message.ts | 313 ++++++++++++++++++----------------- app/features/dm.ts | 3 +- app/nostr.ts | 2 - 6 files changed, 399 insertions(+), 318 deletions(-) diff --git a/app/UI/app_update.tsx b/app/UI/app_update.tsx index 1159da9..3e911ef 100644 --- a/app/UI/app_update.tsx +++ b/app/UI/app_update.tsx @@ -666,9 +666,9 @@ export function generateTags( const parsedTextItems = parseContent(args.content); for (const item of parsedTextItems) { if (item.type === "nevent") { - eTags.set(item.event.pointer.id, [item.event.pointer.relays?.[0] || "", "mention"]); - if (item.event.pointer.pubkey) { - pTags.add(item.event.pointer.pubkey.hex); + eTags.set(item.nevent.pointer.id, [item.nevent.pointer.relays?.[0] || "", "mention"]); + if (item.nevent.pointer.pubkey) { + pTags.add(item.nevent.pointer.pubkey.hex); } } else if (item.type === "npub") { pTags.add(item.pubkey.hex); diff --git a/app/UI/message-panel.tsx b/app/UI/message-panel.tsx index 8a4cce1..dc5c1d4 100644 --- a/app/UI/message-panel.tsx +++ b/app/UI/message-panel.tsx @@ -204,36 +204,36 @@ export function ParseMessageContent( const parsedContentItems = parseContent(message.content); const vnode = []; - let start = 0; for (const item of parsedContentItems) { - vnode.push(message.content.slice(start, item.start)); - const itemStr = message.content.slice(item.start, item.end + 1); - if (item.type == "url") { - if (urlIsImage(itemStr)) { + if (item.type === "raw" || item.type === "tag") { + vnode.push(item.text); + } else if (item.type === "url") { + if (urlIsImage(item.text)) { vnode.push( , ); - } else if (urlIsVideo(itemStr)) { + } else if (urlIsVideo(item.text)) { vnode.push( , ); } else { vnode.push( - - {itemStr} + + {item.text} , ); } - } else if (item.type == "npub") { - const profile = getters.profileGetter.getProfileByPublicKey(item.pubkey); + } else if (item.type === "npub" || item.type === "nprofile") { + const pubkey = item.type == "npub" ? item.pubkey : item.nprofile.pubkey; + const profile = getters.profileGetter.getProfileByPublicKey(pubkey); const name = profile?.profile.name || profile?.profile.display_name; vnode.push( emit({ type: "ViewUserDetail", - pubkey: item.pubkey, + pubkey: pubkey, })} > - {name ? `@${name}` : item.pubkey.bech32()} + {name ? `@${name}` : pubkey.bech32()} , ); - } else if (item.type == "note") { + } else if (item.type === "note") { const event = getters.getEventByID(item.noteID); if (event == undefined || event.kind == NostrKind.DIRECT_MESSAGE) { - vnode.push(itemStr); + vnode.push(item.text); emit({ type: "SyncEvent", eventID: item.noteID.hex, @@ -259,26 +259,22 @@ export function ParseMessageContent( const profile = getters.profileGetter.getProfileByPublicKey(event.publicKey); vnode.push(Card(event, profile?.profile, emit, event.publicKey)); } - } else if (item.type == "nevent") { - const event = getters.getEventByID(NoteID.FromString(item.event.pointer.id)); + } else if (item.type === "nevent") { + const event = getters.getEventByID(NoteID.FromString(item.nevent.pointer.id)); if ( event == undefined || event.kind == NostrKind.DIRECT_MESSAGE ) { - vnode.push(itemStr); + vnode.push(item.text); emit({ type: "SyncEvent", - eventID: item.event.pointer.id, + eventID: item.nevent.pointer.id, }); } else { const profile = getters.profileGetter.getProfileByPublicKey(event.publicKey); vnode.push(Card(event, profile ? profile.profile : undefined, emit, event.publicKey)); } } - - start = item.end + 1; } - vnode.push(message.content.slice(start)); - return vnode; } diff --git a/app/UI/message.test.ts b/app/UI/message.test.ts index 968fd44..3873c01 100644 --- a/app/UI/message.test.ts +++ b/app/UI/message.test.ts @@ -1,175 +1,195 @@ -import { assertEquals } from "https://deno.land/std@0.202.0/testing/asserts.ts"; -import { ChatMessage, findUrlInString, groupContinuousMessages, parseContent } from "./message.ts"; +import { assertEquals } from "https://deno.land/std@0.202.0/assert/assert_equals.ts"; +import { + ChatMessage, + ContentItem, + findUrlInString, + groupContinuousMessages, + parseContent, +} from "./message.ts"; import { PrivateKey, PublicKey } from "../../libs/nostr.ts/key.ts"; import { NostrKind } from "../../libs/nostr.ts/nostr.ts"; -import { Nevent, NostrAddress } from "../../libs/nostr.ts/nip19.ts"; +import { Nevent, NostrAddress, NostrProfile } from "../../libs/nostr.ts/nip19.ts"; Deno.test("inline parse", async (t) => { - const data = [ + const data: { + input: string; + output: ContentItem[]; + }[] = [ + { + input: "", + output: [], + }, { input: `nothing`, - output: [], + output: [{ + text: "nothing", + type: "raw", + }], }, { input: `https://nostr.build/i/f91187675750791b652f7e129b374c2b682d7cc0e9dbc28def58ffdf66508867.jpg`, output: [{ + text: + "https://nostr.build/i/f91187675750791b652f7e129b374c2b682d7cc0e9dbc28def58ffdf66508867.jpg", type: "url", - start: 0, - end: 89, }], }, { input: ` https://nostr.build/i/f91187675750791b652f7e129b374c2b682d7cc0e9dbc28def58ffdf66508867.jpg`, - output: [{ - type: "url", - start: 1, - end: 90, - }], + output: [ + { + text: " ", + type: "raw", + }, + { + text: + "https://nostr.build/i/f91187675750791b652f7e129b374c2b682d7cc0e9dbc28def58ffdf66508867.jpg", + type: "url", + }, + ], }, { input: `https://nostr.build/i/f91187675750791b652f7e129b374c2b682d7cc0e9dbc28def58ffdf66508867.jpg `, - output: [{ - type: "url", - start: 0, - end: 89, - }], + output: [ + { + text: + "https://nostr.build/i/f91187675750791b652f7e129b374c2b682d7cc0e9dbc28def58ffdf66508867.jpg", + type: "url", + }, + { + text: " ", + type: "raw", + }, + ], }, { input: ` https://nostr.build/i/f91187675750791b652f7e129b374c2b682d7cc0e9dbc28def58ffdf66508867.jpg `, output: [{ + text: " ", + type: "raw", + }, { + text: + "https://nostr.build/i/f91187675750791b652f7e129b374c2b682d7cc0e9dbc28def58ffdf66508867.jpg", type: "url", - start: 1, - end: 90, + }, { + text: " ", + type: "raw", }], }, { input: `Hi https://some.jpg`, - output: [{ - type: "url", - start: 3, - end: 18, - }], + output: [ + { + text: "Hi ", + type: "raw", + }, + { + text: "https://some.jpg", + type: "url", + }, + ], }, { input: `Hi https://some.jpg http://some.jpg`, output: [{ - type: "url", - start: 3, - end: 18, + text: "Hi ", + type: "raw", }, { + text: "https://some.jpg", + type: "url", + }, { + text: " ", + type: "raw", + }, { + text: "http://some.jpg", type: "url", - start: 20, - end: 34, - }], - }, - { - input: `nostr:npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4ylログボ`, - output: [{ - type: "npub", - pubkey: PublicKey.FromHex("f34d34b94c1dd0bb552803761e00cc7d3851f7bc8b9f0bf49edc3637b450aefd"), - start: 0, - end: 68, - }], - }, - { - input: `sherryiscutenpub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4ylログボ`, - output: [{ - type: "npub", - pubkey: PublicKey.FromHex("f34d34b94c1dd0bb552803761e00cc7d3851f7bc8b9f0bf49edc3637b450aefd"), - start: 12, - end: 74, }], }, { input: `npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl`, output: [{ + text: "npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", type: "npub", - pubkey: PublicKey.FromHex("f34d34b94c1dd0bb552803761e00cc7d3851f7bc8b9f0bf49edc3637b450aefd"), - start: 0, - end: 62, + pubkey: PublicKey.FromBech32( + "npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + ) as PublicKey, }], }, + { + input: `nostr:npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4ylログボ`, + output: [{ + text: "nostr:npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + type: "npub", + pubkey: PublicKey.FromBech32( + "npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + ) as PublicKey, + }, { + text: "ログボ", + type: "raw", + }], + }, + { + input: `sherryiscutenpub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4ylログボ`, + output: [{ + text: "sherryiscute", + type: "raw", + }, { + text: "npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + type: "npub", + pubkey: PublicKey.FromBech32( + "npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + ) as PublicKey, + }, { + text: "ログボ", + type: "raw", + }], + }, + { input: `nostr:nprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4`, output: [{ - type: "npub", - pubkey: PublicKey.FromHex("98fb85057818fb2b6d5d54a1a9b199c19adeaa31b824b9ebd1e6209d825b2c93"), - start: 0, - end: 598, - relays: [ - "wss://nos.lol", - "wss://relay.damus.io", - "wss://brb.io", - "wss://nostr.orangepill.dev", - "wss://relay.current.fyi", - "wss://nostr.wine", - "wss://relay.snort.social", - "wss://eden.nostr.land", - "wss://relay.nostrgraph.net", - "wss://puravida.nostr.land", - "wss://nostr.zebedee.cloud", - "wss://purplepag.es", - "wss://atlas.nostr.land", - "wss://nostr.mutinywallet.com", - ], + type: "nprofile", + text: + "nostr:nprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4", + nprofile: NostrProfile.decode( + "nprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4", + ) as NostrProfile, }], }, { input: `sherryiscutenprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4 123`, output: [{ - type: "npub", - pubkey: PublicKey.FromHex("98fb85057818fb2b6d5d54a1a9b199c19adeaa31b824b9ebd1e6209d825b2c93"), - start: 12, - end: 604, - relays: [ - "wss://nos.lol", - "wss://relay.damus.io", - "wss://brb.io", - "wss://nostr.orangepill.dev", - "wss://relay.current.fyi", - "wss://nostr.wine", - "wss://relay.snort.social", - "wss://eden.nostr.land", - "wss://relay.nostrgraph.net", - "wss://puravida.nostr.land", - "wss://nostr.zebedee.cloud", - "wss://purplepag.es", - "wss://atlas.nostr.land", - "wss://nostr.mutinywallet.com", - ], + text: "sherryiscute", + type: "raw", + }, { + type: "nprofile", + text: + "nprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4", + nprofile: NostrProfile.decode( + "nprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4", + ) as NostrProfile, + }, { + text: " 123", + type: "raw", }], }, { input: `nprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4`, output: [{ - type: "npub", - pubkey: PublicKey.FromHex("98fb85057818fb2b6d5d54a1a9b199c19adeaa31b824b9ebd1e6209d825b2c93"), - start: 0, - end: 592, - relays: [ - "wss://nos.lol", - "wss://relay.damus.io", - "wss://brb.io", - "wss://nostr.orangepill.dev", - "wss://relay.current.fyi", - "wss://nostr.wine", - "wss://relay.snort.social", - "wss://eden.nostr.land", - "wss://relay.nostrgraph.net", - "wss://puravida.nostr.land", - "wss://nostr.zebedee.cloud", - "wss://purplepag.es", - "wss://atlas.nostr.land", - "wss://nostr.mutinywallet.com", - ], + type: "nprofile", + text: + "nprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4", + nprofile: NostrProfile.decode( + "nprofile1qqsf37u9q4up37etd4w4fgdfkxvurxk74gcmsf9ea0g7vgyasfdjeycpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqscamnwvaz7tmzwf3zu6t0qyd8wumn8ghj7mn0wd68ytn0wfskuem9wp5kcmpwv3jhvqghwaehxw309aex2mrp0yhxxatjwfjkuapwveukjqgswaehxw309ahx7um5wgh8w6twv5q3samnwvaz7tmjv4kxz7fwwdhx7un59eek7cmfv9kqz9thwden5te0v4jx2m3wdehhxarj9ekxzmnyqyd8wumn8ghj7un9d3shjtnwdaehgun8wfshq6pwdejhgqgewaehxw309ac82unpwe5kgcfwdehhxarj9ekxzmnyqyvhwumn8ghj7mn0wd68ytn6v43x2er9v5hxxmr0w4jqzynhwden5te0wp6hyurvv4cxzeewv4esz9nhwden5te0v96xcctn9ehx7um5wghxcctwvsq3camnwvaz7tmwdaehgu3wd46hg6tw09mkzmrvv46zucm0d5lxp0l4", + ) as NostrProfile, }], }, { @@ -177,8 +197,8 @@ Deno.test("inline parse", async (t) => { `naddr1qqxnzd3exsmnjvphxqunqv33qgsp7hwmlh5zccs55shzpfued50pznvypj0wwzn00dtyjzlqkr04w4grqsqqqa28vct2px`, output: [{ type: "naddr", - start: 0, - end: 99, + text: + "naddr1qqxnzd3exsmnjvphxqunqv33qgsp7hwmlh5zccs55shzpfued50pznvypj0wwzn00dtyjzlqkr04w4grqsqqqa28vct2px", addr: new NostrAddress({ pubkey: PublicKey.FromHex( "1f5ddbfde82c6214a42e20a7996d1e114d840c9ee70a6f7b56490be0b0df5755", @@ -193,8 +213,10 @@ Deno.test("inline parse", async (t) => { input: `nostr:nevent1qqsz25j8nrppstgmyry8hgsg4fggtfa6xnym2n4c2xth7usxtydtgpcpp4mhxue69uhhjctzw5hx6egzyze7g05vclndlu36x0vjzw37jykcjkcu8ep9qfqwpjvahmlrq6947qcyqqqqqqgj5mjek`, output: [{ - end: 161, - event: new Nevent( + text: + "nostr:nevent1qqsz25j8nrppstgmyry8hgsg4fggtfa6xnym2n4c2xth7usxtydtgpcpp4mhxue69uhhjctzw5hx6egzyze7g05vclndlu36x0vjzw37jykcjkcu8ep9qfqwpjvahmlrq6947qcyqqqqqqgj5mjek", + type: "nevent", + nevent: new Nevent( { id: "25524798c2182d1b20c87ba208aa5085a7ba34c9b54eb851977f7206591ab407", kind: 1, @@ -206,42 +228,90 @@ Deno.test("inline parse", async (t) => { ], }, ), - start: 0, - type: "nevent", }], }, { input: `Thank you #[0]​ #[2]#[3]`, - output: [{ - type: "tag", - start: 10, - end: 13, - }, { - type: "tag", - start: 16, - end: 19, - }, { - type: "tag", - start: 20, - end: 23, - }], + output: [ + { + type: "raw", + text: "Thank you ", + }, + { + type: "tag", + text: "#[0]", + }, + { + type: "raw", + text: "​ ", + }, + { + type: "tag", + text: "#[2]", + }, + { + type: "tag", + text: "#[3]", + }, + ], }, { input: - "You have been invited to group npub1k9p03z0gqsz2dqvjrkp6337lq5tl9nzj4wx0sfrpjmje2ze8nyls424ds3", + `nostr:npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl https://example.com`, + output: [ + { + type: "npub", + text: "nostr:npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + pubkey: PublicKey.FromBech32( + "npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + ) as PublicKey, + }, + { + type: "raw", + text: " ", + }, + { + type: "url", + text: "https://example.com", + }, + ], + }, + { + input: + `hi https://example.com nostr:npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl`, + output: [ + { + type: "raw", + text: "hi ", + }, + { + type: "url", + text: "https://example.com", + }, + { + type: "raw", + text: " ", + }, + { + type: "npub", + text: "nostr:npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + pubkey: PublicKey.FromBech32( + "npub17dxnfw2vrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7s6ng4yl", + ) as PublicKey, + }, + ], + }, + { + input: `pubkey error: nostr:npub1xxxxxxxxrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7sxxxxxx`, output: [{ - type: "npub", - pubkey: PublicKey.FromBech32( - "npub1k9p03z0gqsz2dqvjrkp6337lq5tl9nzj4wx0sfrpjmje2ze8nyls424ds3", - ), - start: 31, - end: 93, + type: "raw", + text: "pubkey error: nostr:npub1xxxxxxxxrhgtk4fgqdmpuqxv05u9raau3w0shay7msmr0dzs4m7sxxxxxx", }], }, ]; - for (const [i, test] of data.entries()) { - await t.step(test.input, () => { - assertEquals(test.output, Array.from(parseContent(test.input))); + for (const test of data) { + await t.step(`t-${test.input}`, () => { + assertEquals(Array.from(parseContent(test.input)), test.output); }); } }); @@ -258,7 +328,6 @@ Deno.test("message group", () => { pubkey: "", sig: "", tags: [], - parsedContentItems: [], parsedTags: { e: [], p: [], diff --git a/app/UI/message.ts b/app/UI/message.ts index 5b1170d..459fc1c 100644 --- a/app/UI/message.ts +++ b/app/UI/message.ts @@ -1,168 +1,187 @@ import { PublicKey } from "../../libs/nostr.ts/key.ts"; import { DirectedMessage_Event, Parsed_Event } from "../nostr.ts"; -import { Nevent, NostrAddress, NostrProfile, NoteID } from "../../libs/nostr.ts/nip19.ts"; import { NostrKind } from "../../libs/nostr.ts/nostr.ts"; +import { Nevent, NostrAddress, NostrProfile, NoteID } from "../../libs/nostr.ts/nip19.ts"; -export function* parseContent(content: string) { - // URLs - yield* match(/https?:\/\/[^\s]+/g, content, "url"); - - // npubs - yield* match(/(nostr:)?npub[0-9a-z]{59}/g, content, "npub"); - - //nprofile - yield* match(/(nostr:)?nprofile[0-9a-z]+/g, content, "nprofile"); - - //naddr - yield* match(/(nostr:)?naddr[0-9a-z]+/g, content, "naddr"); - - // notes - yield* match(/note[0-9a-z]{59}/g, content, "note"); - - // nevent - yield* match(/(nostr:)?nevent[0-9a-z]+/g, content, "nevent"); - - // tags - yield* match(/#\[[0-9]+\]/g, content, "tag"); -} - -function* match(regex: RegExp, content: string, type: ItemType): Generator { - let match; - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#return_value - // If the match succeeds, the exec() method returns an array and - // updates the lastIndex property of the regular expression object. - while ((match = regex.exec(content)) !== null) { - const urlStartPosition = match.index; - if (urlStartPosition == undefined) { - return; - } - const urlEndPosition = urlStartPosition + match[0].length - 1; - if (type == "note") { - const noteID = NoteID.FromBech32(content.slice(urlStartPosition, urlEndPosition + 1)); - if (noteID instanceof Error) { - // ignore - } else { - yield { - type: type, - noteID: noteID, - start: urlStartPosition, - end: urlEndPosition, - }; - } - } else if (type == "npub") { - let bech32: string; - if (match[0].startsWith("nostr:")) { - bech32 = content.slice(urlStartPosition + 6, urlEndPosition + 1); - } else { - bech32 = content.slice(urlStartPosition, urlEndPosition + 1); - } - const pubkey = PublicKey.FromBech32(bech32); - if (pubkey instanceof Error) { - // ignore - } else { - yield { - type: type, - pubkey: pubkey, - start: urlStartPosition, - end: urlEndPosition, - }; - } - } else if (type == "nprofile") { - let bech32: string; - if (match[0].startsWith("nostr:")) { - bech32 = content.slice(urlStartPosition + 6, urlEndPosition + 1); - } else { - bech32 = content.slice(urlStartPosition, urlEndPosition + 1); - } - const decoded_nProfile = NostrProfile.decode(bech32); - if (decoded_nProfile instanceof Error) { - // ignore - } else { - const pubkey = decoded_nProfile.pubkey; - - yield { - type: "npub", - pubkey: pubkey, - start: urlStartPosition, - end: urlEndPosition, - relays: decoded_nProfile.relays, - }; - } - } else if (type == "naddr") { - let bech32: string; - if (match[0].startsWith("nostr:")) { - bech32 = content.slice(urlStartPosition + 6, urlEndPosition + 1); - } else { - bech32 = content.slice(urlStartPosition, urlEndPosition + 1); - } - const decoded_nAddr = NostrAddress.decode(bech32); - if (decoded_nAddr instanceof Error) { - // ignore - } else { - yield { - type: "naddr", - start: urlStartPosition, - end: urlEndPosition, - addr: decoded_nAddr, - }; - } - } else if (type == "nevent") { - let bech32: string; - if (match[0].startsWith("nostr:")) { - bech32 = content.slice(urlStartPosition + 6, urlEndPosition + 1); - } else { - bech32 = content.slice(urlStartPosition, urlEndPosition + 1); - } - const decoded_nEvent = Nevent.decode(bech32); - if (decoded_nEvent instanceof Error) { - // ignore - } else { - yield { - type: "nevent", - start: urlStartPosition, - end: urlEndPosition, - event: decoded_nEvent, - }; - } - } else { - yield { - type: type, - start: urlStartPosition, - end: urlEndPosition, - }; - } - } -} - -type otherItemType = "url" | "tag"; -type ItemType = otherItemType | "note" | "npub" | "nprofile" | "naddr" | "nevent"; +type ItemType = "url" | "tag" | "note" | "npub" | "nprofile" | "naddr" | "nevent"; export type ContentItem = { - type: otherItemType; - start: number; - end: number; + type: "raw" | "url" | "tag"; + text: string; } | { type: "npub"; + text: string; pubkey: PublicKey; - start: number; - end: number; - relays?: string[]; +} | { + type: "nprofile"; + text: string; + nprofile: NostrProfile; } | { type: "note"; + text: string; noteID: NoteID; - start: number; - end: number; } | { type: "naddr"; - start: number; - end: number; + text: string; addr: NostrAddress; } | { type: "nevent"; - start: number; - end: number; - event: Nevent; + text: string; + nevent: Nevent; }; +export function* parseContent(content: string): Iterable { + if (content.length === 0) { + return; + } + const first_match = match_first(content); + if (!first_match) { + yield { text: content, type: "raw" }; + return; + } + + const text = content.substring(first_match.start, first_match.end); + const bech32 = text.startsWith("nostr:") ? text.slice(6) : text; + const raw_string_before = content.substring(0, first_match.start); + + if (first_match.name === "npub") { + const pubkey = PublicKey.FromBech32(bech32); + if (pubkey instanceof Error) { + yield { + type: "raw", + text: content.slice(0, first_match.end), + }; + yield* parseContent(content.slice(first_match.end)); + return; + } else { + if (raw_string_before) { + yield { text: raw_string_before, type: "raw" }; + } + yield { text, type: first_match.name, pubkey }; + yield* parseContent(content.slice(first_match.end)); + return; + } + } else if (first_match.name === "nprofile") { + const decoded_nProfile = NostrProfile.decode(bech32); + if (decoded_nProfile instanceof Error) { + yield { + type: "raw", + text: content.slice(0, first_match.end), + }; + yield* parseContent(content.slice(first_match.end)); + return; + } else { + if (raw_string_before) { + yield { text: raw_string_before, type: "raw" }; + } + yield { text, type: first_match.name, nprofile: decoded_nProfile }; + yield* parseContent(content.slice(first_match.end)); + return; + } + } else if (first_match.name === "note") { + const noteID = NoteID.FromBech32(bech32); + if (noteID instanceof Error) { + yield { + type: "raw", + text: content.slice(0, first_match.end), + }; + yield* parseContent(content.slice(first_match.end)); + return; + } else { + if (raw_string_before) { + yield { text: raw_string_before, type: "raw" }; + } + yield { text, type: first_match.name, noteID }; + yield* parseContent(content.slice(first_match.end)); + return; + } + } else if (first_match.name === "naddr") { + const addr = NostrAddress.decode(bech32); + if (addr instanceof Error) { + yield { + type: "raw", + text: content.slice(0, first_match.end), + }; + yield* parseContent(content.slice(first_match.end)); + return; + } else { + if (raw_string_before) { + yield { text: raw_string_before, type: "raw" }; + } + yield { text, type: first_match.name, addr }; + yield* parseContent(content.slice(first_match.end)); + return; + } + } else if (first_match.name === "nevent") { + const nevent = Nevent.decode(bech32); + if (nevent instanceof Error) { + yield { + type: "raw", + text: content.slice(0, first_match.end), + }; + yield* parseContent(content.slice(first_match.end)); + return; + } else { + if (raw_string_before) { + yield { text: raw_string_before, type: "raw" }; + } + yield { text, type: first_match.name, nevent }; + yield* parseContent(content.slice(first_match.end)); + return; + } + } else { + if (raw_string_before) { + yield { text: raw_string_before, type: "raw" }; + } + yield { text, type: first_match.name }; + yield* parseContent(content.slice(first_match.end)); + } +} + +function match_first(content: string) { + if (content.length === 0) { + return; + } + + const regexs: { name: ItemType; regex: RegExp }[] = [ + { name: "url", regex: /https?:\/\/[^\s]+/ }, + { name: "npub", regex: /(nostr:)?npub[0-9a-z]{59}/ }, + { name: "nprofile", regex: /(nostr:)?nprofile[0-9a-z]+/ }, + { name: "naddr", regex: /(nostr:)?naddr[0-9a-z]+/ }, + { name: "note", regex: /(nostr:)?note[0-9a-z]{59}/ }, + { name: "nevent", regex: /(nostr:)?nevent[0-9a-z]+/ }, + { name: "tag", regex: /#\[[0-9]+\]/ }, + ]; + + let first_match: { + name: ItemType; + start: number; + end: number; + } | undefined; + for (const r of regexs) { + const matched = r.regex.exec(content); + if (matched == null) { + continue; + } + const start = matched.index; + const end = matched.index + matched[0].length; + + // Return the matching string with the maximum length + if (first_match == undefined) { + first_match = { name: r.name, start, end }; + continue; + } + if (start < first_match.start) { + first_match = { name: r.name, start, end }; + continue; + } + if (first_match.start == start && end > first_match.end) { + first_match = { name: r.name, start, end }; + continue; + } + } + return first_match; +} + // Think of ChatMessage as an materialized view of NostrEvent export type ChatMessage = { readonly type: "image" | "text"; diff --git a/app/features/dm.ts b/app/features/dm.ts index d7cd3ba..30ca446 100644 --- a/app/features/dm.ts +++ b/app/features/dm.ts @@ -3,7 +3,7 @@ import { InvalidKey, PublicKey } from "../../libs/nostr.ts/key.ts"; import { NostrAccountContext, NostrEvent, NostrKind } from "../../libs/nostr.ts/nostr.ts"; import { ConnectionPool } from "../../libs/nostr.ts/relay-pool.ts"; import { DirectMessageGetter } from "../UI/app_update.tsx"; -import { ChatMessage, parseContent } from "../UI/message.ts"; +import { ChatMessage } from "../UI/message.ts"; import { compare, DirectedMessage_Event, @@ -297,7 +297,6 @@ async function parseDM( parsedTags, publicKey, decryptedContent: decrypted, - parsedContentItems: Array.from(parseContent(decrypted)), }, }; } diff --git a/app/nostr.ts b/app/nostr.ts index 1296779..771e750 100644 --- a/app/nostr.ts +++ b/app/nostr.ts @@ -5,7 +5,6 @@ import { PublicKey } from "../libs/nostr.ts/key.ts"; import * as nostr from "../libs/nostr.ts/nostr.ts"; import { NostrKind, TagPubKey } from "../libs/nostr.ts/nostr.ts"; import { ProfileData } from "./features/profile.ts"; -import { ContentItem } from "./UI/message.ts"; import { prepareEncryptedNostrEvent, prepareNormalNostrEvent } from "../libs/nostr.ts/event.ts"; type TotolChunks = string; @@ -47,7 +46,6 @@ export type Profile_Nostr_Event = Parsed_Event & { export type DirectedMessage_Event = Parsed_Event & { decryptedContent: string; - parsedContentItems: ContentItem[]; }; export type Encrypted_Event = DirectedMessage_Event;