pds-dash/src/lib/PostComponent.svelte
ari cff9eed1a4
All checks were successful
Deploy / Deploy (push) Successful in 33s
Dynamic post loading (#2)
Dynamically load the posts so that you can scroll a chronologically sorted timeline infinitely

Reviewed-on: #2
Co-authored-by: ari <ariadna@omg.lol>
Co-committed-by: ari <ariadna@omg.lol>
2025-04-22 03:14:37 +00:00

290 lines
7.1 KiB
Svelte

<script lang="ts">
import { Post } from "./pdsfetch";
import { Config } from "../../config";
import { onMount } from "svelte";
import moment from "moment";
let { post }: { post: Post } = $props();
// State for image carousel
let currentImageIndex = $state(0);
// Functions to navigate carousel
function nextImage() {
if (post.imagesCid && currentImageIndex < post.imagesCid.length - 1) {
currentImageIndex++;
}
}
function prevImage() {
if (currentImageIndex > 0) {
currentImageIndex--;
}
}
// Function to preload an image
function preloadImage(index: number): void {
if (!post.imagesCid || index < 0 || index >= post.imagesCid.length) return;
const img = new Image();
img.src = `${Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${post.authorDid}&cid=${post.imagesCid[index]}`;
}
// Preload adjacent images when current index changes
$effect(() => {
if (post.imagesCid && post.imagesCid.length > 1) {
// Preload next image if available
if (currentImageIndex < post.imagesCid.length - 1) {
preloadImage(currentImageIndex + 1);
}
// Preload previous image if available
if (currentImageIndex > 0) {
preloadImage(currentImageIndex - 1);
}
}
});
// Initial preload of images
onMount(() => {
if (post.imagesCid && post.imagesCid.length > 1) {
// Preload the next image if it exists
if (post.imagesCid.length > 1) {
preloadImage(1);
}
}
});
</script>
<div id="postContainer">
<div id="postHeader">
{#if post.authorAvatarCid}
<img
id="avatar"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.authorAvatarCid}"
alt="avatar of {post.displayName}"
/>
{/if}
<div id="headerText">
<a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
>{post.displayName}</a
>
<p id="handle">
<a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}"
>{post.authorHandle}</a
>
<a
id="postLink"
href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}"
>{moment(post.timenotstamp).isBefore(moment().subtract(1, "month"))
? moment(post.timenotstamp).format("MMM D, YYYY")
: moment(post.timenotstamp).fromNow()}</a
>
</p>
</div>
</div>
<div id="postContent">
{#if post.replyingUri}
<a
id="replyingText"
href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post
.replyingUri.rkey}">replying to {post.replyingUri.repo}</a
>
{/if}
{#if post.quotingUri}
<a
id="quotingText"
href="{Config.FRONTEND_URL}/profile/{post.quotingUri.repo}/post/{post
.quotingUri.rkey}">quoting {post.quotingUri.repo}</a
>
{/if}
<div id="postText">{post.text}</div>
{#if post.imagesCid && post.imagesCid.length > 0}
<div id="carouselContainer">
<img
id="embedImages"
alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post
.imagesCid[currentImageIndex]}"
/>
{#if post.imagesCid.length > 1}
<div id="carouselControls">
<button
id="prevBtn"
onclick={prevImage}
disabled={currentImageIndex === 0}></button
>
<div id="carouselIndicators">
{#each post.imagesCid as _, i}
<div
class="indicator {i === currentImageIndex ? 'active' : ''}"
></div>
{/each}
</div>
<button
id="nextBtn"
onclick={nextImage}
disabled={currentImageIndex === post.imagesCid.length - 1}
></button
>
</div>
{/if}
</div>
{/if}
{#if post.videosLinkCid}
<!-- svelte-ignore a11y_media_has_caption -->
<video
id="embedVideo"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}"
controls
></video>
{/if}
</div>
</div>
<style>
a:hover {
text-decoration: underline;
}
#postContainer {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
background-color: var(--background-color);
margin-bottom: 15px;
overflow-wrap: break-word;
}
#postHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
background-color: var(--header-background-color);
padding: 0px 0px;
height: fit-content;
border-bottom: 1px solid var(--border-color);
font-weight: bold;
overflow-wrap: break-word;
height: 60px;
}
#displayName {
color: var(--text-color);
font-size: 1.2em;
padding: 0;
margin: 0;
}
#handle {
color: var(--border-color);
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postLink {
color: var(--border-color);
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postContent {
display: flex;
text-align: start;
flex-direction: column;
padding: 10px;
background-color: var(--content-background-color);
color: var(--text-color);
overflow-wrap: break-word;
white-space: pre-line;
}
#replyingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#quotingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#postText {
margin: 0;
padding: 0;
}
#headerText {
margin-left: 10px;
font-size: 0.9em;
text-align: start;
overflow-wrap: break-word;
overflow: hidden;
}
#avatar {
height: 100%;
margin: 0px;
margin-left: 0px;
border-right: var(--border-color) 1px solid;
}
#carouselContainer {
position: relative;
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: column;
align-items: center;
}
#carouselControls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
margin-top: 5px;
}
#carouselIndicators {
display: flex;
gap: 5px;
}
.indicator {
width: 8px;
height: 8px;
background-color: var(--indicator-inactive-color);
}
.indicator.active {
background-color: var(--indicator-active-color);
}
#prevBtn,
#nextBtn {
background-color: rgba(31, 17, 69, 0.7);
color: var(--text-color);
border: 1px solid var(--border-color);
width: 30px;
height: 30px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
#prevBtn:disabled,
#nextBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#embedVideo {
width: 100%;
max-width: 500px;
margin-top: 10px;
align-self: center;
}
#embedImages {
min-width: min(100%, 500px);
max-width: min(100%, 500px);
max-height: 500px;
object-fit: contain;
margin: 0;
}
</style>