mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-19 19:46:42 +00:00
Fix note detail relay popover
This commit is contained in:
parent
7d1c2999eb
commit
d9ab565b5f
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user