import { RichText } from '@atproto/api'; import Hls from 'hls.js'; export interface Text { val: string; setInnerHtml: boolean; } interface Reason { $type: string; by: { displayName: string; } } const formatPost: ({ post, reason, isRoot }: { post: any; reason: Reason; isRoot: boolean }) => { createdAt: string; images: any[]; isRepost: boolean; repostBy: string | null | undefined; handle: string; avatar: string; text: Text[]; uri: string; card: any; replyPost: any; username: string; } | null = ({ post, reason, isRoot }) => { if (post.$type === "app.bsky.embed.record#viewNotFound" || post.$type === 'app.bsky.embed.record#viewBlocked') { return null } if (post.$type === "app.bsky.graph.defs#listView") { // Handle list view return { username: post.creator.displayName, handle: post.creator.handle, avatar: post.creator.avatar, text: [{ val: post.description, setInnerHtml: false }], createdAt: post.indexedAt, uri: post.uri, images: [], card: null, replyPost: null, isRepost: false, repostBy: null, }; } // Existing post handling code const facets = post.record.facets || []; const rawText = post.record.text; const rt: RichText = new RichText({ text: rawText, facets }); const text: Text[] = []; for (const segment of rt.segments()) { if (segment.isLink()) { text.push({ val: `${segment.text}`, setInnerHtml: true, }) } else if (segment.isMention()) { text.push({ val: `${segment.text}`, setInnerHtml: true, }) } else if (segment.isTag()) { text.push({ val: `${segment.text}`, setInnerHtml: true, }) } else { text.push({ val: segment.text, setInnerHtml: false, }) } } const replyPost = post.embed?.$type === 'app.bsky.embed.record#view' ? post.embed.record : post.embed?.record?.record?.$type === 'app.bsky.embed.record#viewRecord' && post.embed.record.record const formattedReply = replyPost && { ...replyPost, record: replyPost.value || replyPost.record, embed: (replyPost?.embeds || [])[0] } const author = post.author || post.creator return { username: author.displayName, handle: author.handle, avatar: author.avatar, // todo fallback text, createdAt: post.record.createdAt, uri: post.uri, images: [ ...post.embed?.images || [], ...post.embed?.media?.images || [], ...[(post.embed?.media?.external)].filter(Boolean).map((image: any) => ({ ...image, alt: image.title, thumb: image.uri })) ], video: post.embed?.$type === 'app.bsky.embed.video#view' && post.embed, card: post.embed?.$type === 'app.bsky.embed.external#view' && post.embed?.external, replyPost: isRoot && formattedReply && formatPost({ post: formattedReply, reason: { $type: '', by: { displayName: '' } }, isRoot: false }), isRepost: reason?.$type === 'app.bsky.feed.defs#reasonRepost', repostBy: reason?.by?.displayName } }; export const formatData = (data: any) => (data.feed || []).map((post: { post: any; reason: any; isRoot: any; }) => formatPost({ ...post, isRoot: true })) export const getContentAfterLastSlash = (str: string): string => { const lastIndex: number = str.lastIndexOf("/"); if (lastIndex !== -1) { return str.substring(lastIndex + 1); } else { return str; } } export const timeDifference = (previous: Date): string => { const current: Date = new Date(); const msPerMinute: number = 60 * 1000; const msPerHour: number = msPerMinute * 60; const msPerDay: number = msPerHour * 24; const msPerMonth: number = msPerDay * 30; const msPerYear: number = msPerDay * 365; const elapsed: number = current.getTime() - previous.getTime(); if (elapsed < msPerMinute) { return Math.floor(elapsed / 1000) + 's'; } else if (elapsed < msPerHour) { return Math.floor(elapsed / msPerMinute) + 'm'; } else if (elapsed < msPerDay) { return Math.floor(elapsed / msPerHour) + 'h'; } else if (elapsed < msPerMonth) { return Math.floor(elapsed / msPerDay) + 'd'; } else if (elapsed < msPerYear) { return Math.floor(elapsed / msPerMonth) + ' mo'; } else { return Math.floor(elapsed / msPerYear) + ' yr'; } } export const fetchVideo = async (video: any, videoRef: any, disableAutoplay: boolean = false) => { if (!('IntersectionObserver' in window)) { console.error('IntersectionObserver not supported'); return; } if (!videoRef) { console.error('Video element not found'); return; } const observerOptions = { root: null, // Viewport is the root by default threshold: 0.5 // 50% of the video must be visible to trigger playback }; const onIntersect = async (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // Video is in view - load and play if (Hls.isSupported()) { var hls = new Hls(); hls.loadSource(video.playlist); // Load the HLS manifest hls.attachMedia(videoRef); // Attach to video element hls.on(Hls.Events.MANIFEST_PARSED, () => { if (!disableAutoplay) { videoRef.play(); } }); } else if (videoRef.canPlayType('application/vnd.apple.mpegurl')) { // Fallback for native HLS support in Safari videoRef.src = video.playlist; videoRef.addEventListener('loadedmetadata', () => { if (!disableAutoplay) { videoRef.play(); } }); } // Optionally, unobserve once video starts playing if you don't need to watch for it going out of view observer.unobserve(videoRef); } }); }; // Create the observer const observer = new IntersectionObserver(onIntersect, observerOptions); observer.observe(videoRef); // Observe the video element };