Compare commits

..

23 Commits

Author SHA1 Message Date
mplorentz 81ab03311a Cleanup remaining files 2026-04-08 17:10:12 +00:00
mplorentz 11fa8fd720 Factor video code and stores out of voice.ts 2026-04-08 17:10:12 +00:00
mplorentz 3cff8271e6 Clean up VideoCallContent 2026-04-08 17:10:12 +00:00
mplorentz 2c4fe7bcf3 Simplify how video call layout is stored 2026-04-08 17:10:12 +00:00
mplorentz fb60d493b9 incorporate page layout changes from dev and other cleanup 2026-04-08 17:10:12 +00:00
mplorentz 39ce62d903 Add permissions for cameras on mobile 2026-04-08 17:10:12 +00:00
mplorentz 4d90092f4b Hide screen share button on ios and android 2026-04-08 17:10:12 +00:00
mplorentz ceca21e867 Show unread indicator on chat icon in VoiceWidget 2026-04-08 17:10:12 +00:00
mplorentz 7353dab8c2 Allow clicking voice widget to go back to call 2026-04-08 17:10:12 +00:00
mplorentz 31d7041e5c rework video + text chat display controls 2026-04-08 17:10:12 +00:00
mplorentz b545f225a5 Style pin icon more better 2026-04-08 17:10:12 +00:00
mplorentz 1ccd6070df Style voice widget icons to be less red 2026-04-08 17:10:12 +00:00
mplorentz 2661f449dd Add video settings to VoiceCallAudioSettingsDialog 2026-04-08 17:10:12 +00:00
mplorentz 9e4ac16673 Fix merge artifacts 2026-04-08 17:10:12 +00:00
mplorentz 236b9e011e Add settings button to configure audio devices in call 2026-04-08 17:10:12 +00:00
mplorentz 8f6f628bd7 Change screen sharing icon 2026-04-08 17:10:12 +00:00
mplorentz 7c27846d0d Improve pinned video layout 2026-04-08 17:10:12 +00:00
mplorentz 9b080996d0 Add a button to spotlight a video feed 2026-04-08 17:10:12 +00:00
mplorentz 4e23fb3bba Add basic screen sharing 2026-04-08 17:10:12 +00:00
mplorentz 9f6b16089b add video to livekit calls 2026-04-08 17:10:12 +00:00
Jon Staab 65ca8a7fd8 Remove follow graph building 2026-04-08 09:46:56 -07:00
nayan9617 7f1e98dcb2 Fix fallback pull race after abort (#167)
Co-authored-by: nayan9617 <nayanp4925@gmail.com>
Co-committed-by: nayan9617 <nayanp4925@gmail.com>
2026-04-08 16:43:04 +00:00
priyanshu_bharti 4c19ee823b 73-video-thumbnails (#142)
This PR implements video thumbnails for `.mov`, `.webm`, and `.mp4` files in the `ContentLinkBlock` component.

Changes:
- Added the `poster` attribute to the `<video>` tag.
- Set the poster source to `{url}#t=1` to capture a clear preview frame at the 1-second mark.
- Verified locally that thumbnails are now correctly displayed instead of a black/empty box.

Closes #73

Reviewed-on: #142
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-08 16:07:11 +00:00
7 changed files with 104 additions and 71 deletions
+1
View File
@@ -19,5 +19,6 @@ 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_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_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
VITE_THUMBNAIL_URL=
VITE_GLITCHTIP_API_KEY= VITE_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN= GLITCHTIP_AUTH_TOKEN=
+1
View File
@@ -28,6 +28,7 @@ node_modules/
.pnpm-store/ .pnpm-store/
build/ build/
.svelte-kit/ .svelte-kit/
.next/
# Rust/Tauri # Rust/Tauri
*target/ *target/
+3 -3
View File
@@ -1,4 +1,4 @@
@import 'tailwindcss'; @import "tailwindcss";
@config "../tailwind.config.js"; @config "../tailwind.config.js";
@@ -141,7 +141,7 @@
} }
@utility content-padding-y { @utility content-padding-y {
@apply pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12; @apply pt-4 pb-4 sm:pt-8 sm:pb-8 md:pt-12 md:pb-12;
} }
@utility content-sizing { @utility content-sizing {
@@ -149,7 +149,7 @@
} }
@utility content { @utility content {
@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; @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;
} }
@utility heading { @utility heading {
+22 -2
View File
@@ -1,12 +1,19 @@
<script lang="ts"> <script lang="ts">
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib" import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
import {isRelayUrl, getTagValue} from "@welshman/util" import {isRelayUrl, getTagValue} from "@welshman/util"
import {Capacitor} from "@capacitor/core"
import {preventDefault, stopPropagation} from "@lib/html" import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte" import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte" import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {dufflepud, PLATFORM_URL, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state" import {
dufflepud,
PLATFORM_URL,
IMAGE_CONTENT_TYPES,
VIDEO_CONTENT_TYPES,
THUMBNAIL_URL,
} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props() const {value, event} = $props()
@@ -22,6 +29,14 @@
return [url, true] return [url, true]
}) })
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 loadPreview = async () => {
const json = await postJson(dufflepud("link/preview"), {url}) const json = await postJson(dufflepud("link/preview"), {url})
@@ -42,7 +57,12 @@
<Link {external} {href} class="my-2 block"> <Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box"> <div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)} {#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"> <video
controls
src={url}
poster={getVideoPoster(url)}
preload="metadata"
class="max-h-96 rounded-box object-contain object-center">
<track kind="captions" /> <track kind="captions" />
</video> </video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)} {:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
+1 -2
View File
@@ -62,8 +62,7 @@
{@render children?.()} {@render children?.()}
<!-- a little extra something for ios --> <!-- a little extra something for ios -->
<div <div class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
</div> </div>
<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"> 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,6 +210,8 @@ export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com" export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const THUMBNAIL_URL = import.meta.env.VITE_THUMBNAIL_URL
export const NIP46_PERMS = export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," + "nip44_encrypt,nip44_decrypt," +
[ [
+74 -64
View File
@@ -1,8 +1,8 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import type {Unsubscriber} from "svelte/store" import type {Unsubscriber} from "svelte/store"
import {derived, get} from "svelte/store" import {last, call, ifLet, assoc, chunk, WEEK, ago} from "@welshman/lib"
import {last, call, ifLet, assoc, chunk, sleep, identity, WEEK, ago} from "@welshman/lib"
import {PollResponse} from "nostr-tools/kinds" import {PollResponse} from "nostr-tools/kinds"
import {merged} from "@welshman/store"
import { import {
getListTags, getListTags,
getRelayTagValues, getRelayTagValues,
@@ -21,12 +21,11 @@ import {
unionFilters, unionFilters,
getTagValue, getTagValue,
} from "@welshman/util" } from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util" import type {Filter, List, PublishedList, TrustedEvent} from "@welshman/util"
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net" import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
import { import {
pubkey, pubkey,
loadRelay, loadRelay,
userFollowList,
userRelayList, userRelayList,
userMessagingRelayList, userMessagingRelayList,
loadRelayList, loadRelayList,
@@ -49,7 +48,6 @@ import {
loadGroupList, loadGroupList,
userSpaceUrls, userSpaceUrls,
userGroupList, userGroupList,
bootstrapPubkeys,
decodeRelay, decodeRelay,
getSpaceUrlsFromGroupList, getSpaceUrlsFromGroupList,
getSpaceRoomsFromGroupList, getSpaceRoomsFromGroupList,
@@ -74,6 +72,8 @@ const pullOneWithFallback = async (
signal: AbortSignal, signal: AbortSignal,
onEvent?: (event: TrustedEvent) => void, onEvent?: (event: TrustedEvent) => void,
) => { ) => {
if (signal.aborted) return
const cachedEvents = repository.query([filter]).filter(isSignedEvent) const cachedEvents = repository.query([filter]).filter(isSignedEvent)
const since = last(cachedEvents.slice(10))?.created_at || 0 const since = last(cachedEvents.slice(10))?.created_at || 0
@@ -86,6 +86,12 @@ const pullOneWithFallback = async (
const shouldFallback = const shouldFallback =
!hasNegentropy(url) || !hasNegentropy(url) ||
(await new Promise(resolve => { (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}) const diff = new Difference({relay: url, filter, events: cachedEvents, signal})
diff.on(DifferenceEvent.Error, () => { diff.on(DifferenceEvent.Error, () => {
@@ -111,9 +117,7 @@ export const pullWithFallback = async ({url, signal, filters, onEvent}: SyncOpts
if (signal.aborted) return if (signal.aborted) return
for (const filter of filters) { await Promise.all(filters.map(filter => pullOneWithFallback(url, filter, signal, onEvent)))
pullOneWithFallback(url, filter, signal, onEvent)
}
} }
const listen = ({url, signal, filters, onEvent}: SyncOpts) => { const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
@@ -123,6 +127,8 @@ const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
} }
const pullAndListen = (options: SyncOpts) => { const pullAndListen = (options: SyncOpts) => {
if (options.signal.aborted) return
pullWithFallback(options) pullWithFallback(options)
listen(options) listen(options)
} }
@@ -197,7 +203,7 @@ const syncUserRoomMembership = (url: string, h: string) => {
const syncUserData = () => { const syncUserData = () => {
const unsubscribersByKey = new Map<string, Unsubscriber>() const unsubscribersByKey = new Map<string, Unsubscriber>()
const unsubscribeGroupList = userGroupList.subscribe($userGroupList => { const syncGroupList = ($userGroupList: List | undefined) => {
if ($userGroupList) { if ($userGroupList) {
const keys = new Set<string>() const keys = new Set<string>()
@@ -226,43 +232,35 @@ 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 = userRelayList.subscribe($userRelayList => { const unsubscribeRelayList = merged([userRelayList]).subscribe(([$userRelayList]) => {
if ($userRelayList) { syncRelayList($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 () => { return () => {
unsubscribersByKey.forEach(call) unsubscribersByKey.forEach(call)
unsubscribeGroupList() unsubscribeGroupList()
unsubscribeRelayList() unsubscribeRelayList()
unsubscribeFollows()
} }
} }
@@ -321,7 +319,7 @@ const syncSpace = (url: string, rooms: string[]) => {
} }
const syncSpaces = () => { const syncSpaces = () => {
const store = derived([userGroupList, page], identity) const store = merged([userGroupList, page])
const unsubscribersByUrl = new Map<string, Unsubscriber>() const unsubscribersByUrl = new Map<string, Unsubscriber>()
const roomsByUrl = new Map<string, string>() const roomsByUrl = new Map<string, string>()
@@ -383,6 +381,7 @@ const syncDMs = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>() const unsubscribersByUrl = new Map<string, Unsubscriber>()
let currentPubkey: string | undefined let currentPubkey: string | undefined
let currentShouldUnwrap = false
const unsubscribeAll = () => { const unsubscribeAll = () => {
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) { for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
@@ -391,6 +390,34 @@ 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[]) => { const subscribeAll = (pubkey: string, urls: string[]) => {
// Start syncing newly added relays // Start syncing newly added relays
for (const url of urls) { for (const url of urls) {
@@ -408,33 +435,16 @@ const syncDMs = () => {
} }
} }
// When pubkey changes, re-sync const unsubscribePubkey = merged([pubkey, shouldUnwrap]).subscribe(([$pubkey, $shouldUnwrap]) => {
const unsubscribePubkey = derived([pubkey, shouldUnwrap], identity).subscribe( syncPubkey($pubkey, $shouldUnwrap)
([$pubkey, $shouldUnwrap]) => { })
if ($pubkey !== currentPubkey) {
unsubscribeAll()
}
// 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 // When user messaging relays change, update synchronization
const unsubscribeList = userMessagingRelayList.subscribe($userMessagingRelayList => { const unsubscribeList = merged([userMessagingRelayList]).subscribe(
const $pubkey = pubkey.get() ([$userMessagingRelayList]) => {
const $shouldUnwrap = shouldUnwrap.get() syncList($userMessagingRelayList)
},
if ($pubkey && $shouldUnwrap) { )
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
}
})
return () => { return () => {
unsubscribeAll() unsubscribeAll()