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
};