Compare commits

..

3 Commits

9 changed files with 216 additions and 215 deletions
-1
View File
@@ -19,6 +19,5 @@ VITE_DEFAULT_SEARCH_RELAYS=relay.ditto.pub,antiprimal.net,relay.vertexlab.io
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
VITE_THUMBNAIL_URL=
VITE_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN=
-1
View File
@@ -28,7 +28,6 @@ node_modules/
.pnpm-store/
build/
.svelte-kit/
.next/
# Rust/Tauri
*target/
+3 -3
View File
@@ -1,4 +1,4 @@
@import "tailwindcss";
@import 'tailwindcss';
@config "../tailwind.config.js";
@@ -131,7 +131,7 @@
}
@utility content-padding-y {
@apply pt-4 pb-4 sm:pt-8 sm:pb-8 md:pt-12 md:pb-12;
@apply pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12;
}
@utility content-sizing {
@@ -139,7 +139,7 @@
}
@utility content {
@apply m-auto w-full max-w-3xl px-4 pt-4 pb-4 sm:px-8 sm:pt-8 sm:pb-8 md:px-12 md:pt-12 md:pb-12;
@apply m-auto w-full max-w-3xl px-4 sm:px-8 md:px-12 pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12;
}
@utility heading {
+89 -62
View File
@@ -1,44 +1,75 @@
<script lang="ts">
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
import {isRelayUrl, getTagValue} from "@welshman/util"
import {Capacitor} from "@capacitor/core"
import {isRelayUrl, getTagValue, normalizeRelayUrl, displayRelayUrl} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal"
import {
dufflepud,
IMAGE_CONTENT_TYPES,
PLATFORM_URL,
IMAGE_CONTENT_TYPES,
VIDEO_CONTENT_TYPES,
THUMBNAIL_URL,
displayRoom,
isRoomId,
splitRoomId,
} from "@app/core/state"
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
let hideImage = $state(false)
const url = value.url.toString()
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url)
const roomReference = call(() => {
if (!isRoomId(url)) {
return undefined
}
const [roomUrl, h] = splitRoomId(url)
if (!roomUrl || !h || !isRelayUrl(roomUrl)) {
return undefined
}
return {url: normalizeRelayUrl(roomUrl), h}
})
const relayReference = call(() => {
if (roomReference || !isRelayUrl(url)) {
return undefined
}
return normalizeRelayUrl(url)
})
const label = call(() => {
if (roomReference) {
const spaceName = displayRelayUrl(roomReference.url)
const roomName = displayRoom(roomReference.url, roomReference.h)
return `~${spaceName} / ${roomName}`
}
if (relayReference) {
return `~${displayRelayUrl(relayReference)}`
}
return displayUrl(url)
})
const fileType = getTagValue("file-type", event.tags) || ""
const [href, external] = call(() => {
if (roomReference) return [makeRoomPath(roomReference.url, roomReference.h), false]
if (relayReference) return [makeSpacePath(relayReference), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true]
})
const fileType = getTagValue("file-type", event.tags) || ""
const getVideoPoster = (videoUrl: string): string | undefined => {
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
}
return undefined
}
const loadPreview = async () => {
const json = await postJson(dufflepud("link/preview"), {url})
@@ -56,50 +87,46 @@
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
</script>
{#if isRoomOrRelay}
<ContentLinkUrl {url} class="bg-alt my-2 block p-4 leading-normal whitespace-nowrap" />
{:else}
<Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
<video
controls
src={url}
poster={getVideoPoster(url)}
preload="metadata"
class="max-h-96 rounded-box object-contain object-center">
<track kind="captions" />
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
</button>
{:else}
{#await loadPreview()}
<div class="center my-12 w-full">
<span class="loading loading-spinner"></span>
<Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
<track kind="captions" />
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
</button>
{:else if roomReference || relayReference}
<div class="bg-alt p-4 leading-normal">
<Icon icon={LinkRound} size={3} class="inline-block" />
<span class="ml-2">{label}</span>
</div>
{:else}
{#await loadPreview()}
<div class="center my-12 w-full">
<span class="loading loading-spinner"></span>
</div>
{:then preview}
<div class="bg-alt flex max-w-xl flex-col leading-normal">
{#if preview.image && !hideImage}
<img
alt=""
onerror={onError}
src={preview.image}
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
{/if}
<div class="flex flex-col gap-2 p-4">
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
>{preview.title || displayUrl(url)}</strong>
<p>{ellipsize(preview.description, 140)}</p>
</div>
{:then preview}
<div class="bg-alt flex max-w-xl flex-col leading-normal">
{#if preview.image && !hideImage}
<img
alt=""
onerror={onError}
src={preview.image}
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
{/if}
<div class="flex flex-col gap-2 p-4">
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
>{preview.title || displayUrl(url)}</strong>
<p>{ellipsize(preview.description, 140)}</p>
</div>
</div>
{:catch}
<p class="bg-alt p-12 text-center leading-normal">
Unable to load a preview for {url}
</p>
{/await}
{/if}
</div>
</Link>
{/if}
</div>
{:catch}
<p class="bg-alt p-12 text-center leading-normal">
Unable to load a preview for {url}
</p>
{/await}
{/if}
</div>
</Link>
+59 -5
View File
@@ -1,18 +1,69 @@
<script lang="ts">
import {displayUrl} from "@welshman/lib"
import {getTagValue} from "@welshman/util"
import {call, displayUrl} from "@welshman/lib"
import {isRelayUrl, getTagValue, normalizeRelayUrl, displayRelayUrl} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte"
import {pushModal} from "@app/util/modal"
import {IMAGE_CONTENT_TYPES} from "@app/core/state"
import {
PLATFORM_URL,
IMAGE_CONTENT_TYPES,
displayRoom,
isRoomId,
splitRoomId,
} from "@app/core/state"
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
const url = value.url.toString()
const roomReference = call(() => {
if (!isRoomId(url)) {
return undefined
}
const [roomUrl, h] = splitRoomId(url)
if (!roomUrl || !h || !isRelayUrl(roomUrl)) {
return undefined
}
return {url: normalizeRelayUrl(roomUrl), h}
})
const relayReference = call(() => {
if (roomReference || !isRelayUrl(url)) {
return undefined
}
return normalizeRelayUrl(url)
})
const label = call(() => {
if (roomReference) {
const spaceName = displayRelayUrl(roomReference.url)
const roomName = displayRoom(roomReference.url, roomReference.h)
return `~${spaceName} / ${roomName}`
}
if (relayReference) {
return `~${displayRelayUrl(relayReference)}`
}
return displayUrl(url)
})
const fileType = getTagValue("file-type", event.tags) || ""
const [href, external] = call(() => {
if (roomReference) return [makeRoomPath(roomReference.url, roomReference.h), false]
if (relayReference) return [makeSpacePath(relayReference), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true]
})
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
</script>
@@ -27,5 +78,8 @@
{displayUrl(url)}
</a>
{:else}
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
<Link {external} {href} class="link-content whitespace-nowrap">
<Icon icon={LinkRound} size={3} class="inline-block" />
{label}
</Link>
{/if}
-67
View File
@@ -1,67 +0,0 @@
<script lang="ts">
import {call, displayUrl} from "@welshman/lib"
import {displayRelayUrl, isRelayUrl, normalizeRelayUrl} from "@welshman/util"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import {PLATFORM_URL, displayRoom, isRoomId, splitRoomId} from "@app/core/state"
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
const {
url,
class: className = "",
}: {
url: string
class?: string
} = $props()
const roomReference = call(() => {
if (!isRoomId(url)) {
return undefined
}
const [roomUrl, h] = splitRoomId(url)
if (!roomUrl || !h || !isRelayUrl(roomUrl)) {
return undefined
}
return {url: normalizeRelayUrl(roomUrl), h}
})
const relayReference = call(() => {
if (roomReference || !isRelayUrl(url)) {
return undefined
}
return normalizeRelayUrl(url)
})
const label = call(() => {
if (roomReference) {
const spaceName = displayRelayUrl(roomReference.url)
const roomName = displayRoom(roomReference.url, roomReference.h)
return `~${spaceName} / ${roomName}`
}
if (relayReference) {
return `~${displayRelayUrl(relayReference)}`
}
return displayUrl(url)
})
const [href, external] = call(() => {
if (roomReference) return [makeRoomPath(roomReference.url, roomReference.h), false]
if (relayReference) return [makeSpacePath(relayReference), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true]
})
</script>
<Link {external} {href} class={className}>
<Icon icon={LinkRound} size={3} class="inline-block" />
<span class="ml-2">{label}</span>
</Link>
+2 -1
View File
@@ -62,7 +62,8 @@
{@render children?.()}
<!-- a little extra something for ios -->
<div class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
<div
class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
</div>
<div
class="hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
-2
View File
@@ -210,8 +210,6 @@ export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const THUMBNAIL_URL = import.meta.env.VITE_THUMBNAIL_URL
export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," +
[
+63 -73
View File
@@ -1,8 +1,8 @@
import {page} from "$app/stores"
import type {Unsubscriber} from "svelte/store"
import {last, call, ifLet, assoc, chunk, WEEK, ago} from "@welshman/lib"
import {derived, get} from "svelte/store"
import {last, call, ifLet, assoc, chunk, sleep, identity, WEEK, ago} from "@welshman/lib"
import {PollResponse} from "nostr-tools/kinds"
import {merged} from "@welshman/store"
import {
getListTags,
getRelayTagValues,
@@ -21,11 +21,12 @@ import {
unionFilters,
getTagValue,
} from "@welshman/util"
import type {Filter, List, PublishedList, TrustedEvent} from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util"
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
import {
pubkey,
loadRelay,
userFollowList,
userRelayList,
userMessagingRelayList,
loadRelayList,
@@ -48,6 +49,7 @@ import {
loadGroupList,
userSpaceUrls,
userGroupList,
bootstrapPubkeys,
decodeRelay,
getSpaceUrlsFromGroupList,
getSpaceRoomsFromGroupList,
@@ -72,8 +74,6 @@ const pullOneWithFallback = async (
signal: AbortSignal,
onEvent?: (event: TrustedEvent) => void,
) => {
if (signal.aborted) return
const cachedEvents = repository.query([filter]).filter(isSignedEvent)
const since = last(cachedEvents.slice(10))?.created_at || 0
@@ -86,12 +86,6 @@ const pullOneWithFallback = async (
const shouldFallback =
!hasNegentropy(url) ||
(await new Promise(resolve => {
if (signal.aborted) {
resolve(false)
return
}
// If teardown wins while the diff is opening, skip the fallback path and let cleanup stay in control.
const diff = new Difference({relay: url, filter, events: cachedEvents, signal})
diff.on(DifferenceEvent.Error, () => {
@@ -117,7 +111,9 @@ export const pullWithFallback = async ({url, signal, filters, onEvent}: SyncOpts
if (signal.aborted) return
await Promise.all(filters.map(filter => pullOneWithFallback(url, filter, signal, onEvent)))
for (const filter of filters) {
pullOneWithFallback(url, filter, signal, onEvent)
}
}
const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
@@ -127,8 +123,6 @@ const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
}
const pullAndListen = (options: SyncOpts) => {
if (options.signal.aborted) return
pullWithFallback(options)
listen(options)
}
@@ -203,7 +197,7 @@ const syncUserRoomMembership = (url: string, h: string) => {
const syncUserData = () => {
const unsubscribersByKey = new Map<string, Unsubscriber>()
const syncGroupList = ($userGroupList: List | undefined) => {
const unsubscribeGroupList = userGroupList.subscribe($userGroupList => {
if ($userGroupList) {
const keys = new Set<string>()
@@ -232,35 +226,43 @@ const syncUserData = () => {
}
}
}
}
const syncRelayList = ($userRelayList: PublishedList | undefined) => {
const pubkey = $userRelayList?.event?.pubkey
if (!pubkey) return
loadBlossomServerList(pubkey)
loadBlockedRelayList(pubkey)
loadFollowList(pubkey)
loadGroupList(pubkey)
loadMuteList(pubkey)
loadProfile(pubkey)
loadSettings(pubkey)
loadFeedsForPubkey(pubkey)
}
const unsubscribeGroupList = merged([userGroupList]).subscribe(([$userGroupList]) => {
syncGroupList($userGroupList)
})
const unsubscribeRelayList = merged([userRelayList]).subscribe(([$userRelayList]) => {
syncRelayList($userRelayList)
const unsubscribeRelayList = userRelayList.subscribe($userRelayList => {
if ($userRelayList) {
loadBlossomServerList($userRelayList.event.pubkey)
loadBlockedRelayList($userRelayList.event.pubkey)
loadFollowList($userRelayList.event.pubkey)
loadGroupList($userRelayList.event.pubkey)
loadMuteList($userRelayList.event.pubkey)
loadProfile($userRelayList.event.pubkey)
loadSettings($userRelayList.event.pubkey)
loadFeedsForPubkey($userRelayList.event.pubkey)
}
})
const unsubscribeFollows = userFollowList.subscribe(async $userFollowList => {
for (const pubkeys of chunk(10, get(bootstrapPubkeys))) {
// This isn't urgent, avoid clogging other stuff up
await sleep(1000)
await Promise.all(
pubkeys.flatMap(pk => [
loadRelayList(pk),
loadGroupList(pk),
loadProfile(pk),
loadFollowList(pk),
loadMuteList(pk),
]),
)
}
})
return () => {
unsubscribersByKey.forEach(call)
unsubscribeGroupList()
unsubscribeRelayList()
unsubscribeFollows()
}
}
@@ -319,7 +321,7 @@ const syncSpace = (url: string, rooms: string[]) => {
}
const syncSpaces = () => {
const store = merged([userGroupList, page])
const store = derived([userGroupList, page], identity)
const unsubscribersByUrl = new Map<string, Unsubscriber>()
const roomsByUrl = new Map<string, string>()
@@ -381,7 +383,6 @@ const syncDMs = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>()
let currentPubkey: string | undefined
let currentShouldUnwrap = false
const unsubscribeAll = () => {
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
@@ -390,34 +391,6 @@ const syncDMs = () => {
}
}
const syncPubkey = ($pubkey: string | undefined, $shouldUnwrap: boolean) => {
if ($pubkey !== currentPubkey) {
unsubscribeAll()
}
if ($pubkey && $shouldUnwrap) {
loadRelayList($pubkey)
.then(() => loadMessagingRelayList($pubkey))
.then($l => {
if ($l && currentPubkey === $pubkey && currentShouldUnwrap === $shouldUnwrap) {
subscribeAll($pubkey, getRelayTagValues(getListTags($l)))
}
})
}
currentPubkey = $pubkey
currentShouldUnwrap = $shouldUnwrap
}
const syncList = ($userMessagingRelayList: List | undefined) => {
const $pubkey = pubkey.get()
const $shouldUnwrap = shouldUnwrap.get()
if ($pubkey && $shouldUnwrap) {
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
}
}
const subscribeAll = (pubkey: string, urls: string[]) => {
// Start syncing newly added relays
for (const url of urls) {
@@ -435,17 +408,34 @@ const syncDMs = () => {
}
}
const unsubscribePubkey = merged([pubkey, shouldUnwrap]).subscribe(([$pubkey, $shouldUnwrap]) => {
syncPubkey($pubkey, $shouldUnwrap)
})
// When pubkey changes, re-sync
const unsubscribePubkey = derived([pubkey, shouldUnwrap], identity).subscribe(
([$pubkey, $shouldUnwrap]) => {
if ($pubkey !== currentPubkey) {
unsubscribeAll()
}
// When user messaging relays change, update synchronization
const unsubscribeList = merged([userMessagingRelayList]).subscribe(
([$userMessagingRelayList]) => {
syncList($userMessagingRelayList)
// If we have a pubkey, refresh our user's relay list then sync our subscriptions
if ($pubkey && $shouldUnwrap) {
loadRelayList($pubkey)
.then(() => loadMessagingRelayList($pubkey))
.then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l))))
}
currentPubkey = $pubkey
},
)
// When user messaging relays change, update synchronization
const unsubscribeList = userMessagingRelayList.subscribe($userMessagingRelayList => {
const $pubkey = pubkey.get()
const $shouldUnwrap = shouldUnwrap.get()
if ($pubkey && $shouldUnwrap) {
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
}
})
return () => {
unsubscribeAll()
unsubscribePubkey()