mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-18 19:23:40 +00:00
Refactor NoteContent
This commit is contained in:
parent
61f44e340a
commit
7c9c2ee692
@ -1,5 +1,7 @@
|
|||||||
# Current
|
# Current
|
||||||
|
|
||||||
|
- [ ] Support other kinds
|
||||||
|
- Fix note truncation
|
||||||
- [ ] Feeds load forever if a modal is open
|
- [ ] Feeds load forever if a modal is open
|
||||||
- [ ] Support other list types than 30001
|
- [ ] Support other list types than 30001
|
||||||
- [ ] Fix connection management stuff. Have GPT help
|
- [ ] Fix connection management stuff. Have GPT help
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
onChange(newFilter)
|
onChange(newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
const applySearch = debounce(200, applyFilter)
|
const applySearch = debounce(400, applyFilter)
|
||||||
|
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
_filter.search = ""
|
_filter.search = ""
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {objOf, reverse} from "ramda"
|
import {pluck, without} from "ramda"
|
||||||
import {fly} from "svelte/transition"
|
import {switcher, switcherFn} from "hurdak/lib/hurdak"
|
||||||
import {splice, switcher, switcherFn} from "hurdak/lib/hurdak"
|
|
||||||
import {warn} from "src/util/logger"
|
|
||||||
import {displayPerson, getLabelQuality, displayRelay, Tags} from "src/util/nostr"
|
import {displayPerson, getLabelQuality, displayRelay, Tags} from "src/util/nostr"
|
||||||
import {parseContent} from "src/util/notes"
|
import {parseContent, truncateContent, LINK, INVOICE, NEWLINE, TOPIC} from "src/util/notes"
|
||||||
import {modal} from "src/partials/state"
|
import {modal} from "src/partials/state"
|
||||||
import MediaSet from "src/partials/MediaSet.svelte"
|
import MediaSet from "src/partials/MediaSet.svelte"
|
||||||
import QRCode from "src/partials/QRCode.svelte"
|
import QRCode from "src/partials/QRCode.svelte"
|
||||||
import Card from "src/partials/Card.svelte"
|
|
||||||
import Spinner from "src/partials/Spinner.svelte"
|
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Rating from "src/partials/Rating.svelte"
|
import Rating from "src/partials/Rating.svelte"
|
||||||
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
import NoteContentNewline from "src/app/shared/NoteContentNewline.svelte"
|
||||||
import {sampleRelays} from "src/agent/relays"
|
import NoteContentTopic from "src/app/shared/NoteContentTopic.svelte"
|
||||||
|
import NoteContentLink from "src/app/shared/NoteContentLink.svelte"
|
||||||
|
import NoteContentPerson from "src/app/shared/NoteContentPerson.svelte"
|
||||||
|
import NoteContentQuote from "src/app/shared/NoteContentQuote.svelte"
|
||||||
|
import NoteContentEntity from "src/app/shared/NoteContentEntity.svelte"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import network from "src/agent/network"
|
|
||||||
import {getPersonWithFallback} from "src/agent/db"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
|
|
||||||
export let note
|
export let note
|
||||||
@ -24,107 +23,26 @@
|
|||||||
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 getLinks = parts =>
|
||||||
const shouldTruncate = !showEntire && note.content.length > maxLength
|
pluck(
|
||||||
|
"value",
|
||||||
let content = parseContent(note)
|
parts.filter(x => x.type === LINK && x.canDisplay)
|
||||||
let rating = note.kind === 1985 ? getLabelQuality("review/relay", note) : null
|
)
|
||||||
|
|
||||||
const links = []
|
|
||||||
const invoices = []
|
|
||||||
const ranges = []
|
|
||||||
|
|
||||||
// Find links and preceding whitespace
|
|
||||||
for (let i = 0; i < content.length; i++) {
|
|
||||||
const {type, value} = content[i]
|
|
||||||
|
|
||||||
if (type === "link") {
|
|
||||||
links.push(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "lnurl") {
|
|
||||||
invoices.push(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (["link", "lnurl"].includes(type) && !value.startsWith("ws")) {
|
|
||||||
const prev = content[i - 1]
|
|
||||||
const next = content[i + 1]
|
|
||||||
|
|
||||||
if ((!prev || prev.type === "newline") && (!next || next.type === "newline")) {
|
|
||||||
let n = 1
|
|
||||||
for (let j = i - 1; ; j--) {
|
|
||||||
if (content[j]?.type === "newline") {
|
|
||||||
n += 1
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ranges.push({i: i + 1, 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 (let i = 0; i < content.length; i++) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isStandalone = i => {
|
const isStandalone = i => {
|
||||||
return (
|
return (
|
||||||
!content[i - 1] ||
|
!shortContent[i - 1] ||
|
||||||
content[i - 1].type === "newline" ||
|
shortContent[i - 1].type === NEWLINE ||
|
||||||
!content[i + 1] ||
|
!shortContent[i + 1] ||
|
||||||
content[i + 1].type === "newline"
|
shortContent[i + 1].type === NEWLINE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadQuote = async ({id, relays}) => {
|
const fullContent = parseContent(note)
|
||||||
// Follow relay hints
|
const shortContent = truncateContent(fullContent, {maxLength, showEntire, showMedia})
|
||||||
relays = (relays || []).map(objOf("url")).concat(Tags.from(note).equals(id).relays())
|
const rating = note.kind === 1985 ? getLabelQuality("review/relay", note) : null
|
||||||
|
const links = getLinks(shortContent)
|
||||||
try {
|
const extraLinks = without(links, getLinks(fullContent))
|
||||||
const [event] = await network.load({
|
|
||||||
relays: sampleRelays(relays),
|
|
||||||
filter: [{ids: [id]}],
|
|
||||||
})
|
|
||||||
|
|
||||||
return event || Promise.reject()
|
|
||||||
} catch (e) {
|
|
||||||
warn(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openPerson = pubkey => modal.push({type: "person/feed", pubkey})
|
|
||||||
|
|
||||||
const openQuote = id => {
|
|
||||||
modal.push({type: "note/detail", note: {id}})
|
|
||||||
}
|
|
||||||
|
|
||||||
const openTopic = topic => {
|
|
||||||
modal.push({type: "topic/feed", topic})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 overflow-hidden text-ellipsis">
|
<div class="flex flex-col gap-2 overflow-hidden text-ellipsis">
|
||||||
@ -156,64 +74,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each content as { type, value }, i}
|
{#each shortContent as { type, value }, i}
|
||||||
{#if type === "newline" && i > 0}
|
{#if type === NEWLINE}
|
||||||
{#each value as _}
|
<NoteContentNewline {value} />
|
||||||
<br />
|
{:else if type === TOPIC}
|
||||||
{/each}
|
<NoteContentTopic {value} />
|
||||||
{:else if type === "topic"}
|
{:else if type === INVOICE}
|
||||||
<Anchor killEvent on:click={() => openTopic(value)}>#{value}</Anchor>
|
<div on:click|stopPropagation>
|
||||||
{:else if type === "link"}
|
<QRCode fullWidth onClick="copy" code={value} />
|
||||||
<Anchor external href={value}>
|
</div>
|
||||||
{value.replace(/https?:\/\/(www\.)?/, "")}
|
{:else if type === LINK}
|
||||||
</Anchor>
|
<NoteContentLink {value} showMedia={showMedia && isStandalone(i)} />
|
||||||
|
{:else if type.match(/^nostr:np(rofile|ub)$/)}
|
||||||
|
<NoteContentPerson {value} />
|
||||||
|
{:else if type.startsWith("nostr:") && showMedia && isStandalone(i) && value.id !== anchorId}
|
||||||
|
<NoteContentQuote {note} {value}>
|
||||||
|
<div slot="note-content" let:quote>
|
||||||
|
<svelte:self note={quote} />
|
||||||
|
</div>
|
||||||
|
</NoteContentQuote>
|
||||||
{:else if type.startsWith("nostr:")}
|
{:else if type.startsWith("nostr:")}
|
||||||
{#if showMedia && value.id && isStandalone(i) && value.id !== anchorId}
|
<NoteContentEntity {value} />
|
||||||
<Card interactive invertColors class="my-2" on:click={() => openQuote(value.id)}>
|
|
||||||
{#await loadQuote(value)}
|
|
||||||
<Spinner />
|
|
||||||
{:then quote}
|
|
||||||
{@const person = getPersonWithFallback(quote.pubkey)}
|
|
||||||
<div class="mb-4 flex items-center gap-4">
|
|
||||||
<PersonCircle size={6} {person} />
|
|
||||||
<Anchor
|
|
||||||
stopPropagation
|
|
||||||
type="unstyled"
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
on:click={() => openPerson(quote.pubkey)}>
|
|
||||||
<h2 class="text-lg">{displayPerson(person)}</h2>
|
|
||||||
</Anchor>
|
|
||||||
</div>
|
|
||||||
<svelte:self note={quote} />
|
|
||||||
{:catch}
|
|
||||||
<p class="mb-1 py-24 text-center text-gray-5" in:fly={{y: 20}}>
|
|
||||||
Unable to load a preview for quoted event
|
|
||||||
</p>
|
|
||||||
{/await}
|
|
||||||
</Card>
|
|
||||||
{:else if type.match(/np(rofile|ub)$/)}
|
|
||||||
@<Anchor killEvent on:click={() => openPerson(value.pubkey)}>
|
|
||||||
{displayPerson(getPersonWithFallback(value.pubkey))}
|
|
||||||
</Anchor>
|
|
||||||
{:else}
|
|
||||||
<Anchor killEvent href={"/" + value.entity}>
|
|
||||||
{value.entity.slice(0, 16) + "..."}
|
|
||||||
</Anchor>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
{:else}
|
||||||
{value}
|
{value}
|
||||||
{/if}
|
{/if}
|
||||||
{" "}
|
{" "}
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
{#if invoices.length > 0}
|
{#if showMedia && extraLinks.length > 0}
|
||||||
<div on:click|stopPropagation>
|
<MediaSet links={extraLinks} />
|
||||||
<QRCode fullWidth onClick="copy" code={invoices[0]} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if showMedia && links.length > 0}
|
|
||||||
<div on:click|stopPropagation>
|
|
||||||
<MediaSet {links} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
9
src/app/shared/NoteContentEntity.svelte
Normal file
9
src/app/shared/NoteContentEntity.svelte
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Anchor killEvent href={"/" + value.entity}>
|
||||||
|
{value.entity.slice(0, 16) + "..."}
|
||||||
|
</Anchor>
|
26
src/app/shared/NoteContentLink.svelte
Normal file
26
src/app/shared/NoteContentLink.svelte
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {annotateMedia} from "src/util/misc"
|
||||||
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
|
import Media from "src/partials/Media.svelte"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let showMedia
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(value)
|
||||||
|
|
||||||
|
let hidden = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showMedia && value.canDisplay}
|
||||||
|
<div class="py-2">
|
||||||
|
<Media link={annotateMedia(value.url)} onClose={close} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Anchor external href={value.url}>
|
||||||
|
{value.url.replace(/https?:\/\/(www\.)?/, "")}
|
||||||
|
</Anchor>
|
||||||
|
{/if}
|
7
src/app/shared/NoteContentNewline.svelte
Normal file
7
src/app/shared/NoteContentNewline.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each value as _}
|
||||||
|
<br />
|
||||||
|
{/each}
|
14
src/app/shared/NoteContentPerson.svelte
Normal file
14
src/app/shared/NoteContentPerson.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {displayPerson} from "src/util/nostr"
|
||||||
|
import {modal} from "src/partials/state"
|
||||||
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
|
||||||
|
const openPerson = () => modal.push({type: "person/feed", pubkey: value.pubkey})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@<Anchor killEvent on:click={openPerson}>
|
||||||
|
{displayPerson(getPersonWithFallback(value.pubkey))}
|
||||||
|
</Anchor>
|
65
src/app/shared/NoteContentQuote.svelte
Normal file
65
src/app/shared/NoteContentQuote.svelte
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {objOf} from "ramda"
|
||||||
|
import {fly} from "svelte/transition"
|
||||||
|
import {warn} from "src/util/logger"
|
||||||
|
import {displayPerson, Tags} from "src/util/nostr"
|
||||||
|
import {modal} from "src/partials/state"
|
||||||
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
|
import Card from "src/partials/Card.svelte"
|
||||||
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
|
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
||||||
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
|
import {sampleRelays} from "src/agent/relays"
|
||||||
|
import network from "src/agent/network"
|
||||||
|
|
||||||
|
export let note
|
||||||
|
export let value
|
||||||
|
|
||||||
|
const openPerson = pubkey => modal.push({type: "person/feed", pubkey})
|
||||||
|
|
||||||
|
const loadQuote = async () => {
|
||||||
|
const {id, relays} = value
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [event] = await network.load({
|
||||||
|
relays: sampleRelays(
|
||||||
|
(relays || []).map(objOf("url")).concat(Tags.from(note).equals(id).relays())
|
||||||
|
),
|
||||||
|
filter: [{ids: [id]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
return event || Promise.reject()
|
||||||
|
} catch (e) {
|
||||||
|
warn(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openQuote = () => {
|
||||||
|
modal.push({type: "note/detail", note: {id: value.id}})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<Card interactive invertColors class="my-2" on:click={openQuote}>
|
||||||
|
{#await loadQuote()}
|
||||||
|
<Spinner />
|
||||||
|
{:then quote}
|
||||||
|
{@const person = getPersonWithFallback(quote.pubkey)}
|
||||||
|
<div class="mb-4 flex items-center gap-4">
|
||||||
|
<PersonCircle size={6} {person} />
|
||||||
|
<Anchor
|
||||||
|
stopPropagation
|
||||||
|
type="unstyled"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
on:click={() => openPerson(quote.pubkey)}>
|
||||||
|
<h2 class="text-lg">{displayPerson(person)}</h2>
|
||||||
|
</Anchor>
|
||||||
|
</div>
|
||||||
|
<slot name="note-content" {quote} />
|
||||||
|
{:catch}
|
||||||
|
<p class="mb-1 py-24 text-center text-gray-5" in:fly={{y: 20}}>
|
||||||
|
Unable to load a preview for quoted event
|
||||||
|
</p>
|
||||||
|
{/await}
|
||||||
|
</Card>
|
||||||
|
</div>
|
12
src/app/shared/NoteContentTopic.svelte
Normal file
12
src/app/shared/NoteContentTopic.svelte
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {modal} from "src/partials/state"
|
||||||
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
|
||||||
|
const openTopic = topic => {
|
||||||
|
modal.push({type: "topic/feed", topic})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Anchor killEvent on:click={() => openTopic(value)}>#{value}</Anchor>
|
@ -19,8 +19,8 @@
|
|||||||
<br />
|
<br />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if type === "link"}
|
{:else if type === "link"}
|
||||||
<Anchor external href={value}>
|
<Anchor external href={value.url}>
|
||||||
{value.replace(/https?:\/\/(www\.)?/, "")}
|
{value.url.replace(/https?:\/\/(www\.)?/, "")}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
{:else if type.startsWith("nostr:")}
|
{:else if type.startsWith("nostr:")}
|
||||||
<Anchor external href={"/" + value.entity}>
|
<Anchor external href={"/" + value.entity}>
|
||||||
|
@ -75,13 +75,13 @@
|
|||||||
$or: [{"kind0.name": {$type: "string"}}, {"kind0.display_name": {$type: "string"}}],
|
$or: [{"kind0.name": {$type: "string"}}, {"kind0.display_name": {$type: "string"}}],
|
||||||
})
|
})
|
||||||
.map(person => {
|
.map(person => {
|
||||||
const {name, about, display_name} = person.kind0
|
const {name, nip05, about, display_name} = person.kind0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
person,
|
person,
|
||||||
type: "person",
|
type: "person",
|
||||||
id: person.pubkey,
|
id: person.pubkey,
|
||||||
text: "@" + [name, about, display_name].filter(identity).join(" "),
|
text: "@" + [name, about, nip05, display_name].filter(identity).join(" "),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -116,7 +116,7 @@
|
|||||||
camera icon to scan with your device's camera instead.
|
camera icon to scan with your device's camera instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Input bind:value={q} placeholder="Search for people or topics">
|
<Input autofocus bind:value={q} placeholder="Search for people or topics">
|
||||||
<i slot="before" class="fa-solid fa-search" />
|
<i slot="before" class="fa-solid fa-search" />
|
||||||
<i
|
<i
|
||||||
slot="after"
|
slot="after"
|
||||||
|
@ -33,6 +33,8 @@
|
|||||||
"w-10 h-10 flex justify-center items-center rounded-full bg-gray-8 text-white whitespace-nowrap border border-solid border-gray-7",
|
"w-10 h-10 flex justify-center items-center rounded-full bg-gray-8 text-white whitespace-nowrap border border-solid border-gray-7",
|
||||||
"button-accent":
|
"button-accent":
|
||||||
"py-2 px-4 rounded-full bg-accent text-white whitespace-nowrap border border-solid border-accent-light hover:bg-accent-light",
|
"py-2 px-4 rounded-full bg-accent text-white whitespace-nowrap border border-solid border-accent-light hover:bg-accent-light",
|
||||||
|
"button-minimal":
|
||||||
|
"py-2 px-4 rounded-full whitespace-nowrap border border-solid border-gray-2",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,43 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {sortBy} from "ramda"
|
import {sortBy} from "ramda"
|
||||||
import {slide} from "svelte/transition"
|
|
||||||
import {annotateMedia} from "src/util/misc"
|
import {annotateMedia} from "src/util/misc"
|
||||||
import Media from "src/partials/Media.svelte"
|
import Media from "src/partials/Media.svelte"
|
||||||
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Modal from "src/partials/Modal.svelte"
|
import Modal from "src/partials/Modal.svelte"
|
||||||
|
|
||||||
export let links
|
export let links
|
||||||
export let onClose = null
|
|
||||||
|
|
||||||
let hidden = false
|
|
||||||
let showModal = false
|
let showModal = false
|
||||||
|
|
||||||
// Put previews last since we need to load them asynchronously
|
// Put previews last since we need to load them asynchronously
|
||||||
const annotated = sortBy(l => (l.type === "preview" ? 1 : 0), links.map(annotateMedia))
|
const annotated = sortBy(
|
||||||
|
l => (l.type === "preview" ? 1 : 0),
|
||||||
const close = () => {
|
links.map(link => annotateMedia(link.url))
|
||||||
onClose?.()
|
)
|
||||||
hidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const openModal = () => {
|
const openModal = () => {
|
||||||
showModal = true
|
showModal = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
showModal = false
|
showModal = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !hidden}
|
<div class="my-8 flex justify-center">
|
||||||
<div in:slide class="relative">
|
<Anchor type="button-minimal" on:click={openModal}>
|
||||||
<Media link={annotated[0]} onClose={close} />
|
<i class="fa fa-plus" /> Show all {annotated.length} link previews
|
||||||
{#if annotated.length > 1}
|
</Anchor>
|
||||||
<p class="text-gray-500 cursor-pointer py-4 text-center underline" on:click={openModal}>
|
</div>
|
||||||
<i class="fa fa-plus" /> Show all {annotated.length} link previews
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if showModal}
|
{#if showModal}
|
||||||
<Modal onEscape={closeModal}>
|
<Modal onEscape={closeModal}>
|
||||||
|
@ -6,6 +6,7 @@ import {tryJson, avg} from "src/util/misc"
|
|||||||
import {invoiceAmount} from "src/util/lightning"
|
import {invoiceAmount} from "src/util/lightning"
|
||||||
|
|
||||||
export const noteKinds = [1, 1985, 30023, 30018, 10001, 1063, 9802]
|
export const noteKinds = [1, 1985, 30023, 30018, 10001, 1063, 9802]
|
||||||
|
// export const noteKinds = [9802]
|
||||||
export const personKinds = [0, 2, 3, 10001, 10002]
|
export const personKinds = [0, 2, 3, 10001, 10002]
|
||||||
export const userKinds = personKinds.concat([10000, 30001, 30078])
|
export const userKinds = personKinds.concat([10000, 30001, 30078])
|
||||||
export const appDataKeys = [
|
export const appDataKeys = [
|
||||||
|
@ -3,6 +3,19 @@ import {nip19} from "nostr-tools"
|
|||||||
import {first} from "hurdak/lib/hurdak"
|
import {first} from "hurdak/lib/hurdak"
|
||||||
import {fromNostrURI} from "src/util/nostr"
|
import {fromNostrURI} from "src/util/nostr"
|
||||||
|
|
||||||
|
export const NEWLINE = "newline"
|
||||||
|
export const TEXT = "text"
|
||||||
|
export const TOPIC = "topic"
|
||||||
|
export const LINK = "link"
|
||||||
|
export const INVOICE = "invoice"
|
||||||
|
export const NOSTR_NOTE = "nostr:note"
|
||||||
|
export const NOSTR_NEVENT = "nostr:nevent"
|
||||||
|
export const NOSTR_NPUB = "nostr:npub"
|
||||||
|
export const NOSTR_NPROFILE = "nostr:nprofile"
|
||||||
|
export const NOSTR_NADDR = "nostr:naddr"
|
||||||
|
|
||||||
|
const canDisplayUrl = url => !url.match(/\.(apk|docx|xlsx|csv|dmg)/)
|
||||||
|
|
||||||
export const parseContent = ({content, tags = []}) => {
|
export const parseContent = ({content, tags = []}) => {
|
||||||
const result = []
|
const result = []
|
||||||
let text = content.trim()
|
let text = content.trim()
|
||||||
@ -12,7 +25,7 @@ export const parseContent = ({content, tags = []}) => {
|
|||||||
const newline = first(text.match(/^\n+/))
|
const newline = first(text.match(/^\n+/))
|
||||||
|
|
||||||
if (newline) {
|
if (newline) {
|
||||||
return ["newline", newline, newline]
|
return [NEWLINE, newline, newline]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +61,7 @@ export const parseContent = ({content, tags = []}) => {
|
|||||||
|
|
||||||
// Skip numeric topics
|
// Skip numeric topics
|
||||||
if (topic && !topic.match(/^#\d+$/)) {
|
if (topic && !topic.match(/^#\d+$/)) {
|
||||||
return ["topic", topic, topic.slice(1)]
|
return [TOPIC, topic, topic.slice(1)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,11 +90,11 @@ export const parseContent = ({content, tags = []}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseLNUrl = () => {
|
const parseInvoice = () => {
|
||||||
const lnurl = first(text.match(/^ln(bc|url)[\d\w]{50,1000}/i))
|
const invoice = first(text.match(/^ln(bc|url)[\d\w]{50,1000}/i))
|
||||||
|
|
||||||
if (lnurl) {
|
if (invoice) {
|
||||||
return ["lnurl", lnurl, lnurl]
|
return [INVOICE, invoice, invoice]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +120,7 @@ export const parseContent = ({content, tags = []}) => {
|
|||||||
url = "https://" + url
|
url = "https://" + url
|
||||||
}
|
}
|
||||||
|
|
||||||
return ["link", raw, url]
|
return [LINK, raw, {url, canDisplay: canDisplayUrl(url)}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +131,7 @@ export const parseContent = ({content, tags = []}) => {
|
|||||||
parseTopic() ||
|
parseTopic() ||
|
||||||
parseBech32() ||
|
parseBech32() ||
|
||||||
parseUrl() ||
|
parseUrl() ||
|
||||||
parseLNUrl()
|
parseInvoice()
|
||||||
|
|
||||||
if (part) {
|
if (part) {
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
@ -141,7 +154,42 @@ export const parseContent = ({content, tags = []}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
result.push({type: "text", value: buffer})
|
result.push({type: TEXT, value: buffer})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const truncateContent = (content, {showEntire, maxLength, showMedia}) => {
|
||||||
|
if (showEntire) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = 0
|
||||||
|
const result = []
|
||||||
|
const truncateAt = maxLength * 0.6
|
||||||
|
|
||||||
|
for (const part of content) {
|
||||||
|
const isText = [TOPIC, TEXT].includes(part.type)
|
||||||
|
const isMedia = [LINK, INVOICE].includes(part.type) || part.type.startsWith("nostr:")
|
||||||
|
|
||||||
|
if (isText) {
|
||||||
|
length += part.value.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMedia) {
|
||||||
|
length += showMedia ? maxLength / 3 : part.value.length
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(part)
|
||||||
|
|
||||||
|
if (length > truncateAt) {
|
||||||
|
if (isText || (isMedia && !showMedia)) {
|
||||||
|
result.push({type: TEXT, value: "..."})
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
Loading…
Reference in New Issue
Block a user