A fork of pds-dash-fork for pds.solanaceae.net
at main 201 lines 6.0 kB view raw
1<script lang="ts"> 2 import { Post } from "./pdsfetch"; 3 import { Config } from "../../config"; 4 import { onMount, onDestroy } from "svelte"; 5 import moment from "moment"; 6 import { blueskyHandleFromDid } from "./pdsfetch"; 7 import Hls from "hls.js"; 8 let { post }: { post: Post } = $props(); 9 10 // State for image carousel 11 let currentImageIndex = $state(0); 12 13 // Local state for reply handle text 14 let replyingHandle: string | null = $state(null); 15 16 // Video element ref and HLS instance 17 let videoEl: HTMLVideoElement | null = $state(null); 18 let hls: Hls | null = null; 19 20 // Update replying handle when replyingUri changes 21 $effect(() => { 22 if (post.replyingUri?.repo) { 23 // fire and forget; update when resolved 24 blueskyHandleFromDid(post.replyingUri.repo) 25 .then((h) => { 26 replyingHandle = h || null; 27 }) 28 .catch(() => { 29 replyingHandle = null; 30 }); 31 } else { 32 replyingHandle = null; 33 } 34 }); 35 36 // Functions to navigate carousel 37 function nextImage() { 38 if (post.imagesCid && currentImageIndex < post.imagesCid.length - 1) { 39 currentImageIndex++; 40 } 41 } 42 43 function prevImage() { 44 if (currentImageIndex > 0) { 45 currentImageIndex--; 46 } 47 } 48 49 // Function to preload an image 50 function preloadImage(index: number): void { 51 if (!post.imagesCid || index < 0 || index >= post.imagesCid.length) return; 52 53 const img = new Image(); 54 img.src = `https://cdn.bsky.app/img/feed_thumbnail/plain/${post.authorDid}/${post.imagesCid[index]}@jpeg`; 55 } 56 57 // Initialize HLS playback when mounted if there's a video 58 onMount(() => { 59 // Preload the next image if it exists 60 if (post.imagesCid && post.imagesCid.length > 1) { 61 if (post.imagesCid.length > 1) { 62 preloadImage(1); 63 } 64 } 65 66 if (post.videosLinkCid && videoEl) { 67 const src = `https://video.cdn.bsky.app/hls/${post.authorDid}/${post.videosLinkCid}/playlist.m3u8`; 68 try { 69 if (Hls.isSupported()) { 70 hls = new Hls(); 71 hls.loadSource(src); 72 hls.attachMedia(videoEl); 73 } else if (videoEl.canPlayType("application/vnd.apple.mpegurl")) { 74 // Safari / iOS native HLS 75 videoEl.src = src; 76 } else { 77 // As a basic fallback, set src; some browsers may still handle it 78 videoEl.src = src; 79 } 80 } catch (_) { 81 // Ignore init errors; controls will remain and user can retry 82 } 83 } 84 }); 85 86 onDestroy(() => { 87 if (hls) { 88 hls.destroy(); 89 hls = null; 90 } 91 }); 92</script> 93 94<div id="postContainer"> 95 <div id="postHeader"> 96 {#if post.authorAvatarCid} 97 <img 98 id="avatar" 99 src="https://cdn.bsky.app/img/feed_thumbnail/plain/{post.authorDid}/{post.authorAvatarCid}@jpeg" 100 alt="avatar of {post.displayName}" 101 /> 102 {/if} 103 <div id="headerText"> 104 <a id="displayName" href="{Config.FRONTEND_PROFILE_URL}/{post.authorDid}" 105 >{post.displayName}</a 106 > 107 <p id="handle"> 108 <a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}" 109 >@{post.authorHandle}</a 110 > 111 112 <a 113 id="postLink" 114 style="text-decoration: underline;" 115 href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}" 116 >{moment(post.timenotstamp).isBefore(moment().subtract(1, "month")) 117 ? moment(post.timenotstamp).format("MMM D, YYYY") 118 : moment(post.timenotstamp).fromNow()}</a 119 > 120 </p> 121 </div> 122 </div> 123 <div id="postContent"> 124 {#if post.replyingUri} 125 <a 126 id="replyingText" 127 style="text-decoration: underline;" 128 href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post 129 .replyingUri.rkey}" 130 >replying to {replyingHandle 131 ? `@${replyingHandle}` 132 : post.replyingUri.repo}</a 133 > 134 {/if} 135 {#if post.quotingUri} 136 <a 137 id="quotingText" 138 href="{Config.FRONTEND_URL}/profile/{post.quotingUri.repo}/post/{post 139 .quotingUri.rkey}">quoting {post.quotingUri.repo}</a 140 > 141 {/if} 142 <div id="postText"> 143 {#each post.richText.segments() as segment} 144 {#if segment.mention} 145 <a href="{Config.FRONTEND_URL}/profile/{segment.mention.did}" 146 >{segment.text}</a 147 > 148 {:else if segment.link} 149 <a style="text-decoration: underline" href={segment.link.uri} 150 >{segment.text}</a 151 > 152 {:else if segment.text} 153 {segment.text} 154 {/if} 155 {/each} 156 </div> 157 {#if post.imagesCid && post.imagesCid.length > 0} 158 <div id="carouselContainer"> 159 <img 160 id="embedImages" 161 alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}" 162 src="https://cdn.bsky.app/img/feed_thumbnail/plain/{post.authorDid}/{post 163 .imagesCid[currentImageIndex]}@jpeg" 164 /> 165 166 {#if post.imagesCid.length > 1} 167 <div id="carouselControls"> 168 <button 169 id="prevBtn" 170 onclick={prevImage} 171 disabled={currentImageIndex === 0}></button 172 > 173 <div id="carouselIndicators"> 174 {#each post.imagesCid as _, i} 175 <div 176 class="indicator {i === currentImageIndex ? 'active' : ''}" 177 ></div> 178 {/each} 179 </div> 180 <button 181 id="nextBtn" 182 onclick={nextImage} 183 disabled={currentImageIndex === post.imagesCid.length - 1} 184 ></button 185 > 186 </div> 187 {/if} 188 </div> 189 {/if} 190 {#if post.videosLinkCid} 191 <!-- svelte-ignore a11y_media_has_caption --> 192 <video id="embedVideo" bind:this={videoEl} controls></video> 193 {/if} 194 {#if post.gifLink} 195 <img id="embedVideo" src={post.gifLink} alt="Post GIF" /> 196 {/if} 197 </div> 198</div> 199 200<style> 201</style>