mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-18 19:23:40 +00:00
Add support for parsing and displaying lnurl invoices
This commit is contained in:
parent
7dd6edaac5
commit
a40b9268f6
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type {Filter} from "nostr-tools"
|
||||||
import {onMount, onDestroy} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import {Filter} from "nostr-tools"
|
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {last, equals, partition, always, uniqBy, sortBy, prop} from "ramda"
|
import {last, equals, partition, always, uniqBy, sortBy, prop} from "ramda"
|
||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {pluck} from 'ramda'
|
import type {Filter} from "nostr-tools"
|
||||||
import {Filter} from 'nostr-tools'
|
import {pluck} from "ramda"
|
||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
import {debounce} from 'throttle-debounce'
|
import {debounce} from "throttle-debounce"
|
||||||
import {createLocalDate} from 'src/util/misc'
|
import {createLocalDate} from "src/util/misc"
|
||||||
import Input from "src/partials/Input.svelte"
|
import Input from "src/partials/Input.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Modal from "src/partials/Modal.svelte"
|
import Modal from "src/partials/Modal.svelte"
|
||||||
@ -20,29 +20,29 @@
|
|||||||
until: null,
|
until: null,
|
||||||
authors: [],
|
authors: [],
|
||||||
search: "",
|
search: "",
|
||||||
'#t': [],
|
"#t": [],
|
||||||
'#p': [],
|
"#p": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
let modal = null
|
let modal = null
|
||||||
|
|
||||||
const applyFilter = debounce(300, () => {
|
const applyFilter = debounce(300, () => {
|
||||||
if (modal !== 'maxi') {
|
if (modal !== "maxi") {
|
||||||
const _filter = {} as Filter
|
const _filter = {} as Filter
|
||||||
|
|
||||||
if (filter.since) _filter.since = createLocalDate(filter.since).setHours(23, 59, 59, 0) / 1000
|
if (filter.since) _filter.since = createLocalDate(filter.since).setHours(23, 59, 59, 0) / 1000
|
||||||
if (filter.until) _filter.until = createLocalDate(filter.until).setHours(23, 59, 59, 0) / 1000
|
if (filter.until) _filter.until = createLocalDate(filter.until).setHours(23, 59, 59, 0) / 1000
|
||||||
if (filter.authors.length > 0) _filter.authors = pluck('pubkey', filter.authors)
|
if (filter.authors.length > 0) _filter.authors = pluck("pubkey", filter.authors)
|
||||||
if (filter.search) _filter.search = filter.search
|
if (filter.search) _filter.search = filter.search
|
||||||
if (filter['#t'].length > 0) _filter['#t'] = pluck('name', filter['#t'])
|
if (filter["#t"].length > 0) _filter["#t"] = pluck("name", filter["#t"])
|
||||||
if (filter['#p'].length > 0) _filter['#p'] = pluck('pubkey', filter['#p'])
|
if (filter["#p"].length > 0) _filter["#p"] = pluck("pubkey", filter["#p"])
|
||||||
|
|
||||||
onChange(_filter)
|
onChange(_filter)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
modal = 'maxi'
|
modal = "maxi"
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
@ -55,71 +55,75 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex gap-2 justify-end" in:fly={{y: 20}}>
|
<div class="flex justify-end gap-2" in:fly={{y: 20}}>
|
||||||
<i class="fa fa-search cursor-pointer" on:click={() => {modal = modal ? null : 'mini'}} />
|
<i
|
||||||
|
class="fa fa-search cursor-pointer"
|
||||||
|
on:click={() => {
|
||||||
|
modal = modal ? null : "mini"
|
||||||
|
}} />
|
||||||
<i class="fa fa-sliders cursor-pointer" on:click={open} />
|
<i class="fa fa-sliders cursor-pointer" on:click={open} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if modal}
|
{#if modal}
|
||||||
<Modal {onEscape} mini={modal === 'mini'}>
|
<Modal {onEscape} mini={modal === "mini"}>
|
||||||
<Content size="lg">
|
<Content size="lg">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<strong>Search</strong>
|
<strong>Search</strong>
|
||||||
<Input bind:value={filter.search} on:input={applyFilter}>
|
<Input bind:value={filter.search} on:input={applyFilter}>
|
||||||
<i slot="before" class="fa fa-search" />
|
<i slot="before" class="fa fa-search" />
|
||||||
</Input>
|
</Input>
|
||||||
</div>
|
</div>
|
||||||
{#if modal === 'maxi'}
|
{#if modal === "maxi"}
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<strong>Since</strong>
|
<strong>Since</strong>
|
||||||
<Input type="date" bind:value={filter.since} />
|
<Input type="date" bind:value={filter.since} />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<strong>Until</strong>
|
||||||
|
<Input type="date" bind:value={filter.until} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
{#if !hide.includes("authors")}
|
||||||
<strong>Until</strong>
|
<div class="flex flex-col gap-1">
|
||||||
<Input type="date" bind:value={filter.until} />
|
<strong>Authors</strong>
|
||||||
|
<MultiSelect search={$searchPeople} bind:value={filter.authors}>
|
||||||
|
<div slot="item" let:item>
|
||||||
|
<div class="-my-1">
|
||||||
|
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MultiSelect>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !hide.includes("#t")}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<strong>Topics</strong>
|
||||||
|
<MultiSelect search={$searchTopics} bind:value={filter["#t"]}>
|
||||||
|
<div slot="item" let:item>
|
||||||
|
<div class="-my-1">
|
||||||
|
#{item.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MultiSelect>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !hide.includes("#p")}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<strong>Mentions</strong>
|
||||||
|
<MultiSelect search={$searchPeople} bind:value={filter["#p"]}>
|
||||||
|
<div slot="item" let:item>
|
||||||
|
<div class="-my-1">
|
||||||
|
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MultiSelect>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Anchor type="button-accent" on:click={submit}>Apply Filters</Anchor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{#if !hide.includes('authors')}
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<strong>Authors</strong>
|
|
||||||
<MultiSelect search={$searchPeople} bind:value={filter.authors}>
|
|
||||||
<div slot="item" let:item>
|
|
||||||
<div class="-my-1">
|
|
||||||
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MultiSelect>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if !hide.includes('#t')}
|
</Content>
|
||||||
<div class="flex flex-col gap-1">
|
</Modal>
|
||||||
<strong>Topics</strong>
|
|
||||||
<MultiSelect search={$searchTopics} bind:value={filter['#t']}>
|
|
||||||
<div slot="item" let:item>
|
|
||||||
<div class="-my-1">
|
|
||||||
#{item.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MultiSelect>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if !hide.includes('#p')}
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<strong>Mentions</strong>
|
|
||||||
<MultiSelect search={$searchPeople} bind:value={filter['#p']}>
|
|
||||||
<div slot="item" let:item>
|
|
||||||
<div class="-my-1">
|
|
||||||
<PersonBadge inert person={getPersonWithFallback(item.pubkey)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MultiSelect>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<Anchor type="button-accent" on:click={submit}>Apply Filters</Anchor>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Content>
|
|
||||||
</Modal>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import {displayPerson, parseContent, getLabelQuality, displayRelay, Tags} from "src/util/nostr"
|
import {displayPerson, parseContent, getLabelQuality, displayRelay, Tags} from "src/util/nostr"
|
||||||
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 Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
@ -29,15 +30,22 @@
|
|||||||
let rating = note.kind === 1985 ? getLabelQuality("review/relay", note) : null
|
let rating = note.kind === 1985 ? getLabelQuality("review/relay", note) : null
|
||||||
|
|
||||||
const links = []
|
const links = []
|
||||||
|
const invoices = []
|
||||||
const ranges = []
|
const ranges = []
|
||||||
|
|
||||||
// Find links and preceding whitespace
|
// 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]
|
||||||
|
|
||||||
if (type === "link" && !value.startsWith("ws")) {
|
if (type === "link") {
|
||||||
links.push(value)
|
links.push(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "lnurl") {
|
||||||
|
invoices.push(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["link", "lnurl"].includes(type) && !value.startsWith("ws")) {
|
||||||
const prev = content[i - 1]
|
const prev = content[i - 1]
|
||||||
const next = content[i + 1]
|
const next = content[i + 1]
|
||||||
|
|
||||||
@ -197,6 +205,11 @@
|
|||||||
{" "}
|
{" "}
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
|
{#if invoices.length > 0}
|
||||||
|
<div on:click|stopPropagation>
|
||||||
|
<QRCode fullWidth onClick="copy" code={invoices[0]} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if showMedia && links.length > 0}
|
{#if showMedia && links.length > 0}
|
||||||
<div on:click|stopPropagation>
|
<div on:click|stopPropagation>
|
||||||
<MediaSet {links} />
|
<MediaSet {links} />
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
import QRCode from "qrcode"
|
import QRCode from "qrcode"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import Input from "src/partials/Input.svelte"
|
import Input from "src/partials/Input.svelte"
|
||||||
@ -7,6 +8,8 @@
|
|||||||
import {toast} from "src/partials/state"
|
import {toast} from "src/partials/state"
|
||||||
|
|
||||||
export let code
|
export let code
|
||||||
|
export let onClick = "navigate"
|
||||||
|
export let fullWidth = false
|
||||||
|
|
||||||
let canvas
|
let canvas
|
||||||
|
|
||||||
@ -21,11 +24,20 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="m-auto flex max-w-sm flex-col gap-4 rounded border border-solid border-gray-6 bg-gray-8 p-4">
|
class={cx("rounded-xl border border-solid border-gray-6 bg-gray-8 p-4", {
|
||||||
<Anchor external href={code}>
|
"m-auto max-w-sm": !fullWidth,
|
||||||
<canvas class="m-auto rounded" bind:this={canvas} />
|
})}>
|
||||||
</Anchor>
|
<div class="m-auto flex max-w-sm flex-col gap-4">
|
||||||
<Input value={code}>
|
<Anchor
|
||||||
<button slot="after" class="fa fa-copy" on:click={copy} />
|
external
|
||||||
</Input>
|
href={onClick === "navigate" ? code : null}
|
||||||
|
on:click={onClick === "copy" ? copy : null}>
|
||||||
|
<canvas class="m-auto rounded-xl" bind:this={canvas} />
|
||||||
|
</Anchor>
|
||||||
|
{#if onClick === "navigate"}
|
||||||
|
<Input value={code}>
|
||||||
|
<button slot="after" class="fa fa-copy" on:click={copy} />
|
||||||
|
</Input>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
const className = cx(
|
const className = cx(
|
||||||
$$props.class,
|
$$props.class,
|
||||||
"rounded shadow-inset py-2 px-4 pr-10 w-full bg-input text-black",
|
"rounded-xl shadow-inset py-2 px-4 pr-10 w-full bg-input text-black",
|
||||||
"placeholder:text-gray-5 border border-solid border-gray-3"
|
"placeholder:text-gray-5 border border-solid border-gray-3"
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
@ -265,6 +265,14 @@ export const parseContent = ({content, tags = []}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseLNUrl = () => {
|
||||||
|
const lnurl = first(text.match(/^lnbc[\d\w]+/i))
|
||||||
|
|
||||||
|
if (lnurl) {
|
||||||
|
return ["lnurl", lnurl, lnurl]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const parseUrl = () => {
|
const parseUrl = () => {
|
||||||
const raw = first(text.match(/^([a-z\+:]{2,30}:\/\/)?[^\s]+\.[a-z]{2,6}[^\s]*[^\.!?,:\s]/gi))
|
const raw = first(text.match(/^([a-z\+:]{2,30}:\/\/)?[^\s]+\.[a-z]{2,6}[^\s]*[^\.!?,:\s]/gi))
|
||||||
|
|
||||||
@ -292,7 +300,13 @@ export const parseContent = ({content, tags = []}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (text) {
|
while (text) {
|
||||||
const part = parseNewline() || parseMention() || parseTopic() || parseBech32() || parseUrl()
|
const part =
|
||||||
|
parseNewline() ||
|
||||||
|
parseMention() ||
|
||||||
|
parseTopic() ||
|
||||||
|
parseBech32() ||
|
||||||
|
parseUrl() ||
|
||||||
|
parseLNUrl()
|
||||||
|
|
||||||
if (part) {
|
if (part) {
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
|
Loading…
Reference in New Issue
Block a user