appview-less bluesky client
1import type { PostWithUri } from './at/fetch';
2
3const LINE_HEIGHT = 20; // approx line height in px
4const CHARS_PER_LINE = 66; // approx chars per line
5const BASE_HEIGHT = 100; // header + footer + padding
6const IMAGE_HEIGHT = 300; // constrained height for single images
7const IMAGE_GRID_HEIGHT = 200; // height for grid of images
8const VIDEO_HEIGHT = 300; // default video height
9const LINK_CARD_HEIGHT = 100; // external link card
10const QUOTE_FALLBACK_HEIGHT = 100; // fallback for quote
11
12// idk this is dumb idk i hate virtual lists and shit bleh
13export const estimatePostHeight = (post: PostWithUri | undefined | null, depth = 0): number => {
14 if (!post) return 150; // default fallback if post is missing
15 if (depth > 1) return 0; // prevent infinite recursion
16
17 const record = post.record;
18 let height = BASE_HEIGHT;
19
20 // 1. text height
21 if (record.text) {
22 const lines = Math.ceil(record.text.length / CHARS_PER_LINE) || 1;
23 // add a bit more for newlines if present
24 const newlines = (record.text.match(/\n/g) || []).length;
25 height += (Math.max(lines, newlines + 1) * LINE_HEIGHT);
26 }
27
28 // 2. embeds
29 if (record.embed) {
30 const embed = record.embed;
31
32 if (embed.$type === 'app.bsky.embed.images') {
33 // images
34 if (embed.images.length === 1) {
35 const aspect = embed.images[0].aspectRatio;
36 if (aspect) {
37 // w / h = a => h = w / a
38 // assuming max width of ~500px in feed
39 const calculatedHeight = 500 / (aspect.width / aspect.height);
40 height += Math.min(calculatedHeight, 500); // clamp max height
41 } else {
42 height += IMAGE_HEIGHT;
43 }
44 } else {
45 height += IMAGE_GRID_HEIGHT;
46 }
47 } else if (embed.$type === 'app.bsky.embed.video') {
48 // video
49 const aspect = embed.aspectRatio;
50 if (aspect) {
51 const calculatedHeight = 500 / (aspect.width / aspect.height);
52 height += Math.min(calculatedHeight, 500);
53 } else {
54 height += VIDEO_HEIGHT;
55 }
56 } else if (embed.$type === 'app.bsky.embed.record') {
57 // quote post
58 height += QUOTE_FALLBACK_HEIGHT;
59
60 } else if (embed.$type === 'app.bsky.embed.recordWithMedia') {
61 // recordWithMedia
62 // media part
63 const media = embed.media;
64 if (media.$type === 'app.bsky.embed.images') {
65 height += (media.images.length === 1 ? IMAGE_HEIGHT : IMAGE_GRID_HEIGHT);
66 } else if (media.$type === 'app.bsky.embed.video') {
67 height += VIDEO_HEIGHT;
68 }
69 // quote part
70 height += QUOTE_FALLBACK_HEIGHT;
71 } else if (embed.$type === 'app.bsky.embed.external') {
72 height += LINK_CARD_HEIGHT;
73 }
74 }
75
76 return Math.round(height);
77};