Fix note detail relay popover

This commit is contained in:
Jonathan Staab 2023-04-12 11:21:26 -05:00
parent 7d1c2999eb
commit d9ab565b5f
4 changed files with 106 additions and 83 deletions

View File

@ -4,6 +4,9 @@
- Split out Note pieces - Split out Note pieces
- Move global modals to child components? - Move global modals to child components?
- Combine app/agent, rename app2 - Combine app/agent, rename app2
- Some elements bleed through reply image modal
- DM view pushes user back to the bottom
- note.replies might be empty in note detail pre-load
- [ ] Improve topic suggestions and rendering - [ ] Improve topic suggestions and rendering
- [ ] Add topic search - [ ] Add topic search
- [ ] Relays bounty - [ ] Relays bounty

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {objOf, is} from "ramda" import {objOf, reverse} from "ramda"
import {navigate} from "svelte-routing" import {navigate} from "svelte-routing"
import {fly} from "svelte/transition" import {fly} from "svelte/transition"
import {splice} from 'hurdak/lib/hurdak'
import {warn} from "src/util/logger" import {warn} from "src/util/logger"
import {displayPerson, parseContent, Tags} from "src/util/nostr" import {displayPerson, parseContent, Tags} from "src/util/nostr"
import MediaSet from "src/partials/MediaSet.svelte" import MediaSet from "src/partials/MediaSet.svelte"
@ -20,59 +21,70 @@
export let showEntire = false export let showEntire = false
export let showMedia = user.getSetting("showMedia") export let showMedia = user.getSetting("showMedia")
const truncateAt = maxLength * 0.6
const shouldTruncate = !showEntire && note.content.length > maxLength
let content = parseContent(note)
const links = [] const links = []
const entities = [] const entities = []
const shouldTruncate = !showEntire && note.content.length > maxLength * 0.6 const ranges = []
const content = parseContent(note)
let l = 0 // Find links and preceding whitespace
for (let i = 0; i < content.length; i++) { for (let i = 0; i < content.length; i++) {
const {type, value} = content[i] const {type, value} = content[i]
// Find links on their own line and remove them from content
if ( if (
(type === "link" && !value.startsWith("ws")) || (type === "link" && !value.startsWith("ws")) ||
["nostr:note", "nostr:nevent"].includes(type) ["nostr:note", "nostr:nevent"].includes(type)
) { ) {
const prev = content[i - 1] if (type === 'link') {
const next = content[i + 1]
if (type === "link") {
links.push(value) links.push(value)
} else { } else {
entities.push({type, value}) entities.push({type, value})
} }
// If the link is surrounded by line breaks (or content start/end), remove const prev = content[i - 1]
// the link along with trailing whitespace const next = content[i + 1]
if (showMedia && (!prev || prev.type === "newline") && (!next || next.type === "newline")) {
if ((!prev || prev.type === "newline") && (!next || next.type === "newline")) {
let n = 0 let n = 0
for (let j = i - 1; ; j--) {
for (let j = i + 1; j < content.length; j++) { if (content[j]?.type === "newline") {
if (content[j].type !== "newline") { n += 1
break
}
n++
}
content.splice(i, n + 1)
i = i - n
}
}
// Keep track of total characters, if we're not dealing with a string just guess
if (typeof value === "string") {
l += value.length
// Content[i] may be undefined if we're on a linebreak that was spliced out
if (is(String, content[i]?.value) && shouldTruncate && l > maxLength && type !== "newline") {
content[i].value = value.trim()
content.splice(i + 1, content.length, {type: "text", value: "..."})
break
}
} else { } else {
l += 30 break
}
}
ranges.push({i, n})
}
}
}
// Remove links and preceding line breaks if they're on their own line
if (showMedia) {
for (const {i, n} of reverse(ranges)) {
content = splice(i - n, n, content)
}
}
// Truncate content if needed
let l = 0
if (shouldTruncate) {
for (const i in content) {
const prev = content[i - 1]
// Avoid adding an ellipsis right after a newline
if (l > truncateAt && prev?.type != 'newline') {
content = content.slice(0, i).concat({type: "text", value: "..."})
break
}
if (typeof content[i].value === "string") {
l += content[i].value.length
}
} }
} }
@ -105,11 +117,11 @@
<br /> <br />
{/each} {/each}
{:else if type === "link"} {:else if type === "link"}
<Anchor external href={value}> <Anchor external href={value} class="ml-1">
{value.replace(/https?:\/\/(www\.)?/, "")} {value.replace(/https?:\/\/(www\.)?/, "")}
</Anchor> </Anchor>
{:else if type.startsWith("nostr:")} {:else if type.startsWith("nostr:")}
<Anchor href={"/" + value.entity}> <Anchor href={"/" + value.entity} class="ml-1">
{#if value.pubkey} {#if value.pubkey}
{displayPerson(getPersonWithFallback(value.pubkey))} {displayPerson(getPersonWithFallback(value.pubkey))}
{:else} {:else}

View File

@ -26,6 +26,8 @@
feedRelay = relay feedRelay = relay
} }
$: displayNote = asDisplayEvent(note)
onMount(async () => { onMount(async () => {
if (!note.pubkey) { if (!note.pubkey) {
await network.load({ await network.load({
@ -57,17 +59,17 @@
}) })
</script> </script>
{#if !loading && !note.content} {#if !loading && !displayNote.pubkey}
<div in:fly={{y: 20}}> <div in:fly={{y: 20}}>
<Content size="lg" class="text-center">Sorry, we weren't able to find this note.</Content> <Content size="lg" class="text-center">Sorry, we weren't able to find this note.</Content>
</div> </div>
{:else if note.pubkey} {:else if displayNote.pubkey}
<div in:fly={{y: 20}} class="m-auto flex w-full max-w-2xl flex-col gap-4 p-4"> <div in:fly={{y: 20}} class="m-auto flex w-full max-w-2xl flex-col gap-4 p-4">
<Note <Note
showContext showContext
depth={6} depth={6}
anchorId={note.id} anchorId={displayNote.id}
note={asDisplayEvent(note)} note={displayNote}
{invertColors} {invertColors}
{feedRelay} {feedRelay}
{setFeedRelay} /> {setFeedRelay} />
@ -80,6 +82,6 @@
{#if feedRelay} {#if feedRelay}
<Modal onEscape={() => setFeedRelay(null)}> <Modal onEscape={() => setFeedRelay(null)}>
<RelayFeed {feedRelay} notes={[note]} depth={6} showContext /> <RelayFeed {feedRelay} notes={[displayNote]} depth={6} showContext />
</Modal> </Modal>
{/if} {/if}

View File

@ -150,34 +150,21 @@ export const mergeFilter = (filter, extra) =>
is(Array, filter) ? filter.map(mergeLeft(extra)) : {...filter, ...extra} is(Array, filter) ? filter.map(mergeLeft(extra)) : {...filter, ...extra}
export const parseContent = ({content, tags = []}) => { export const parseContent = ({content, tags = []}) => {
const text = content.trim()
const result = [] const result = []
let buffer = "", let text = content.trim()
i = 0 let buffer = ""
const push = (type, text, value = null) => { const parseNewline = () => {
if (buffer) { const newline = first(text.match(/^\n+/))
result.push({type: "text", value: buffer})
buffer = "" if (newline) {
} return ["newline", newline, newline]
}
result.push({type, value: value || text})
i += text.length
}
for (; i < text.length; ) {
const prev = last(result)
const tail = text.slice(i)
const newLine = tail.match(/^\n+/)
if (newLine) {
push("newline", newLine[0])
continue
} }
const parseMention = () => {
// Convert legacy mentions to bech32 entities // Convert legacy mentions to bech32 entities
const mentionMatch = tail.match(/^#\[(\d+)\]/i) const mentionMatch = text.match(/^#\[(\d+)\]/i)
if (mentionMatch) { if (mentionMatch) {
const i = parseInt(mentionMatch[1]) const i = parseInt(mentionMatch[1])
@ -197,41 +184,50 @@ export const parseContent = ({content, tags = []}) => {
entity = nip19.neventEncode(data) entity = nip19.neventEncode(data)
} }
push(`nostr:${type}`, mentionMatch[0], {...data, entity}) return [`nostr:${type}`, mentionMatch[0], {...data, entity}]
continue }
} }
} }
const topicMatch = tail.match(/^#\w+/i) const parseTopic = () => {
const topic = first(text.match(/^#\w+/i))
if (topicMatch) { if (topic) {
push("topic", topicMatch[0]) return ["topic", topic, topic]
continue }
} }
const bech32Match = tail.match(/^(nostr:)?n(event|ote|profile|pub)1[\d\w]+/i) const parseBech32 = () => {
const bech32 = first(text.match(/^(nostr:)?n(event|ote|profile|pub)1[\d\w]+/i))
if (bech32Match) { if (bech32) {
try { try {
const entity = bech32Match[0].replace("nostr:", "") const entity = bech32[0].replace("nostr:", "")
const {type, data} = nip19.decode(entity) as {type: string; data: object} const {type, data} = nip19.decode(entity) as {type: string; data: object}
const value = type === "note" ? {id: data} : data const value = type === "note" ? {id: data} : data
push(`nostr:${type}`, bech32Match[0], {...value, entity}) return [`nostr:${type}`, bech32[0], {...value, entity}]
continue
} catch (e) { } catch (e) {
console.log(e) console.log(e)
// pass // pass
} }
} }
}
const urlMatch = tail.match( const parseUrl = () => {
/^((http|ws)s?:\/\/)?[-a-z0-9:%_\+~#=\.]+\.[a-z]{1,6}[-a-z0-9:%_\+~#\?&\/=;\.]*/gi const raw = first(
text.match(/^((http|ws)s?:\/\/)?[-a-z0-9:%_\+~#=\.]+\.[a-z]{1,6}[-a-z0-9:%_\+~#\?&\/=;\.]*/gi)
) )
// Skip url if it's just the end of a filepath // Skip url if it's just the end of a filepath
if (urlMatch && (prev?.type !== "text" || !prev.value.endsWith("/"))) { if (raw) {
let url = urlMatch[0] const prev = last(result)
if (prev?.type === "text" && prev.value.endsWith("/")) {
return
}
let url = raw
// Skip ellipses and very short non-urls // Skip ellipses and very short non-urls
if (!url.match(/\.\./) && url.length > 4) { if (!url.match(/\.\./) && url.length > 4) {
@ -244,26 +240,36 @@ export const parseContent = ({content, tags = []}) => {
url = "https://" + url url = "https://" + url
} }
push("link", urlMatch[0], url) return ["link", raw, url]
continue }
} }
} }
while (text) {
const part = parseNewline() || parseMention() || parseTopic() || parseBech32() || parseUrl()
if (part) {
if (buffer) {
result.push({type: "text", value: buffer})
buffer = ""
}
const [type, raw, value] = part
result.push({type, value})
text = text.slice(raw.length)
} else {
// Instead of going character by character and re-running all the above regular expressions // Instead of going character by character and re-running all the above regular expressions
// a million times, try to match the next word and add it to the buffer // a million times, try to match the next word and add it to the buffer
const wordMatch = tail.match(/^[\w\d]+ ?/i) const match = first(text.match(/^[\w\d]+ ?/i)) || text[0]
if (wordMatch) { buffer += match
buffer += wordMatch[0] text = text.slice(match.length)
i += wordMatch[0].length
} else {
buffer += text[i]
i += 1
} }
} }
if (buffer) { if (buffer) {
result.push({type: "text", value: buffer}) result.push({type: 'text', value: buffer})
} }
return result return result