forked from
baileytownsend.dev/pds-dash-fork
A fork of pds-dash-fork for pds.solanaceae.net
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>