+1
src/components/bookmarks/bookmark-feed-item.tsx
+1
src/components/bookmarks/bookmark-feed-item.tsx
+1
-1
src/components/embeds/embed.tsx
+1
-1
src/components/embeds/embed.tsx
+177
-122
src/components/embeds/image-embed.tsx
+177
-122
src/components/embeds/image-embed.tsx
···
2
2
3
3
import { openModal } from '~/globals/modals';
4
4
5
-
import ImageViewerModalLazy from '../images/image-viewer-modal-lazy';
5
+
import ImageViewerModalLazy from '~/components/images/image-viewer-modal-lazy';
6
6
7
7
export interface ImageEmbedProps {
8
8
/** Expected to be static */
···
12
12
borderless?: boolean;
13
13
/** Expected to be static */
14
14
standalone?: boolean;
15
-
/** Expected to be static */
16
-
interactive?: boolean;
17
-
}
18
-
19
-
const enum RenderMode {
20
-
MULTIPLE,
21
-
MULTIPLE_SQUARE,
22
-
STANDALONE,
23
15
}
24
16
25
17
const ImageEmbed = (props: ImageEmbedProps) => {
···
36
28
return Math.max(min, Math.min(max, value));
37
29
};
38
30
39
-
const isCloseToThreeByFour = (ratio: number): boolean => {
40
-
return Math.abs(ratio - 3 / 4) < 0.01;
41
-
};
42
-
43
-
const isCloseToFourByThree = (ratio: number): boolean => {
44
-
return Math.abs(ratio - 4 / 3) < 0.01;
45
-
};
46
-
47
-
const getClampedAspectRatio = (image: AppBskyEmbedImages.ViewImage) => {
31
+
const getAspectRatio = (image: AppBskyEmbedImages.ViewImage): number => {
48
32
const dims = image.aspectRatio;
49
33
50
34
const width = dims ? dims.width : 1;
51
35
const height = dims ? dims.height : 1;
52
36
const ratio = width / height;
53
37
38
+
return ratio;
39
+
};
40
+
41
+
const clampBetween3_4And4_3 = (ratio: number): number => {
54
42
return clamp(ratio, 3 / 4, 4 / 3);
55
43
};
56
44
57
-
const deriveMultiMediaHeight = (ratioA: number, ratioB: number) => {
58
-
if (isCloseToFourByThree(ratioA) && isCloseToFourByThree(ratioB)) {
59
-
return 184;
60
-
}
45
+
const clampBetween3_4And16_9 = (ratio: number): number => {
46
+
return clamp(ratio, 3 / 4, 16 / 9);
47
+
};
61
48
62
-
if (
63
-
(isCloseToThreeByFour(ratioA) && isCloseToFourByThree(ratioB)) ||
64
-
(isCloseToThreeByFour(ratioA) && isCloseToThreeByFour(ratioB))
65
-
) {
66
-
return 235;
67
-
}
68
-
69
-
return ratioA === 1 && ratioB === 1 ? 245 : 200;
49
+
const AltIndicator = () => {
50
+
return (
51
+
<div class="pointer-events-none absolute bottom-0 right-0 p-2">
52
+
<div class="flex h-4 items-center rounded bg-p-neutral-950/60 px-1 text-[9px] font-bold tracking-wider text-white">
53
+
ALT
54
+
</div>
55
+
</div>
56
+
);
70
57
};
71
58
72
59
const StandaloneRenderer = (props: ImageEmbedProps) => {
···
75
62
const images = embed.images;
76
63
const length = images.length;
77
64
65
+
const render = (index: number, img: AppBskyEmbedImages.ViewImage) => {
66
+
return (
67
+
<img
68
+
src={/* @once */ img.thumb}
69
+
alt={/* @once */ img.alt}
70
+
class="h-full w-full cursor-pointer object-cover text-[0px]"
71
+
onClick={() => {
72
+
openModal(() => <ImageViewerModalLazy images={images} active={index} />);
73
+
}}
74
+
/>
75
+
);
76
+
};
77
+
78
78
if (length === 1) {
79
-
const image = images[0];
80
-
const dims = image.aspectRatio;
79
+
const img = images[0];
80
+
const dims = img.aspectRatio;
81
81
82
82
const width = dims ? dims.width : 16;
83
83
const height = dims ? dims.height : 9;
···
85
85
86
86
return (
87
87
<div class="max-w-full self-start overflow-hidden rounded-md border border-outline">
88
-
<div class="max-h-80 min-h-16 min-w-16" style={{ 'aspect-ratio': ratio }}>
89
-
<img src={/* @once */ image.thumb} class="h-full w-full object-contain text-[0px]" />
88
+
<div class="relative max-h-80 min-h-16 min-w-16 max-w-full" style={{ 'aspect-ratio': ratio }}>
89
+
<img
90
+
src={/* @once */ img.thumb}
91
+
alt={/* @once */ img.alt}
92
+
class="h-full w-full cursor-pointer object-contain text-[0px]"
93
+
onClick={() => {
94
+
openModal(() => <ImageViewerModalLazy images={images} active={0} />);
95
+
}}
96
+
/>
90
97
91
98
{/* beautiful hack that ensures we're always using the maximum possible dimension */}
92
99
<div class="h-screen w-screen"></div>
100
+
101
+
{/* @once */ img.alt && <AltIndicator />}
93
102
</div>
94
103
</div>
95
104
);
96
105
}
97
106
98
107
if (length === 2) {
99
-
const a = images[0];
100
-
const b = images[1];
108
+
const rs = images.map(getAspectRatio);
109
+
const [crA, crB] = rs.map(clampBetween3_4And4_3);
101
110
102
-
const ratioA = getClampedAspectRatio(a);
103
-
const ratioB = getClampedAspectRatio(b);
104
-
const totalRatio = ratioA + ratioB;
111
+
const totalRatio = crA + crB;
112
+
113
+
const nodes = images.map((img, idx) => {
114
+
return (
115
+
<div class="relative overflow-hidden rounded-md border border-outline">
116
+
{/* @once */ render(idx, img)}
117
+
{/* @once */ img.alt && <AltIndicator />}
118
+
</div>
119
+
);
120
+
});
105
121
106
122
return (
107
123
<div
108
124
class="grid gap-1.5"
109
125
style={{
110
126
'aspect-ratio': totalRatio,
111
-
'grid-template-columns': `minmax(0, ${Math.floor(ratioA * 100)}fr) minmax(0, ${Math.floor(ratioB * 100)}fr)`,
127
+
'grid-template-columns': `minmax(0, ${Math.floor(crA * 100)}fr) minmax(0, ${Math.floor(crB * 100)}fr)`,
112
128
}}
113
129
>
114
-
<div class="overflow-hidden rounded-md border border-outline">
115
-
<img src={/* @once */ a.thumb} class="h-full w-full object-cover text-[0px]" />
116
-
</div>
117
-
<div class="overflow-hidden rounded-md border border-outline">
118
-
<img src={/* @once */ b.thumb} class="h-full w-full object-cover text-[0px]" />
119
-
</div>
130
+
{nodes}
120
131
</div>
121
132
);
122
133
}
123
134
124
135
if (length >= 3) {
125
-
const ratios = images.map(getClampedAspectRatio);
136
+
const rs = images.map(getAspectRatio);
137
+
const crs = rs.map(clampBetween3_4And4_3);
126
138
127
-
const height = deriveMultiMediaHeight(ratios[0], ratios[1]);
128
-
const widths = ratios.map((ratio) => Math.floor(ratio * height));
139
+
// 448px - 2px = maximum possible screen width (desktop)
140
+
// 80px = width covered by avatar and padding in timeline item (64px on the left, 16px on the right)
141
+
// 16px = random value to make it clear there's more items to the right
142
+
// 220px = reasonable height limit
143
+
const height = Math.min((1 / crs[0]) * (Math.min(window.innerWidth, 448 - 2) - 80 - 16), 220);
144
+
const widths = crs.map((ratio) => Math.floor(ratio * height));
129
145
130
146
const nodes = images.map((img, idx) => {
131
147
const h = `${height}px`;
132
148
const w = `${widths[idx]}px`;
133
-
const r = ratios[idx];
149
+
const r = crs[idx];
134
150
135
151
return (
136
-
<div
137
-
class="box-content shrink-0 overflow-hidden rounded-md border border-outline"
138
-
style={{ height: h, width: w, 'aspect-ratio': r }}
139
-
>
140
-
<img src={/* @once */ img.thumb} class="h-full w-full object-cover text-[0px]" />
152
+
<div class="shrink-0" style={{ width: w, height: h, 'aspect-ratio': r }}>
153
+
<div class="relative h-full w-full overflow-hidden rounded-md border border-outline">
154
+
{/* @once */ render(idx, img)}
155
+
{/* @once */ img.alt && <AltIndicator />}
156
+
</div>
141
157
</div>
142
158
);
143
159
});
144
160
145
-
return <div class="-mx-4 flex gap-1.5 overflow-x-auto px-4">{nodes}</div>;
161
+
return (
162
+
<div
163
+
class="-mr-4 flex gap-1.5 overflow-x-auto pr-4 scrollbar-hide"
164
+
style={{
165
+
'margin-left': `calc(var(--embed-left-gutter, 16px) * -1)`,
166
+
'padding-left': `var(--embed-left-gutter, 16px)`,
167
+
}}
168
+
>
169
+
{nodes}
170
+
</div>
171
+
);
146
172
}
147
173
148
174
return null;
149
175
};
150
176
151
177
const NonStandaloneRenderer = (props: ImageEmbedProps) => {
152
-
const { embed, borderless, interactive } = props;
178
+
const { embed, borderless } = props;
153
179
154
180
const images = embed.images;
155
181
const length = images.length;
156
182
157
-
const render = (index: number, mode: RenderMode) => {
158
-
const { alt, thumb } = images[index];
183
+
if (length === 1) {
184
+
const img = images[0];
185
+
186
+
return (
187
+
<div class={`aspect-video` + (!borderless ? ` overflow-hidden rounded-md border border-outline` : ``)}>
188
+
<img
189
+
src={/* @once */ img.thumb}
190
+
alt={/* @once */ img.alt}
191
+
class="h-full w-full object-contain text-[0px]"
192
+
/>
193
+
</div>
194
+
);
195
+
}
159
196
160
-
let cn: string | undefined;
161
-
let ratio: string | undefined;
197
+
if (length === 2) {
198
+
const rs = images.map(getAspectRatio);
199
+
const [crA, crB] = rs.map(clampBetween3_4And16_9);
162
200
163
-
if (mode === RenderMode.MULTIPLE) {
164
-
cn = `min-h-0 grow basis-0 overflow-hidden`;
165
-
} else if (mode === RenderMode.MULTIPLE_SQUARE) {
166
-
cn = `aspect-square overflow-hidden`;
167
-
} else if (mode === RenderMode.STANDALONE) {
168
-
cn = `aspect-video overflow-hidden`;
169
-
}
201
+
const totalRatio = crA + crB;
170
202
171
-
return (
172
-
<div class={`relative bg-background ` + cn} style={{ 'aspect-ratio': ratio }}>
203
+
const nodes = images.map((img) => {
204
+
return (
173
205
<img
174
-
src={thumb}
175
-
title={alt}
176
-
class={
177
-
`h-full w-full object-contain text-[0px]` +
178
-
(interactive ? ` cursor-pointer` : ``) +
179
-
// prettier-ignore
180
-
(props.blur ? ` scale-125` + (!borderless ? ` blur` : ` blur-lg`) : ``)
181
-
}
182
-
onClick={() => {
183
-
if (interactive) {
184
-
openModal(() => <ImageViewerModalLazy active={index} images={images} />);
185
-
}
186
-
}}
206
+
src={/* @once */ img.thumb}
207
+
alt={/* @once */ img.alt}
208
+
class="h-full w-full object-cover text-[0px]"
187
209
/>
210
+
);
211
+
});
188
212
189
-
{interactive && alt && (
190
-
<div class="pointer-events-none absolute bottom-0 right-0 p-2">
191
-
<div class="flex h-4 items-center rounded bg-p-neutral-950/60 px-1 text-[9px] font-bold tracking-wider text-white">
192
-
ALT
193
-
</div>
194
-
</div>
195
-
)}
213
+
return (
214
+
<div
215
+
class={`grid gap-0.5` + (!borderless ? ` overflow-hidden rounded-md border border-outline` : ``)}
216
+
style={{
217
+
'aspect-ratio': totalRatio,
218
+
'grid-template-columns': `minmax(0, ${Math.floor(crA * 100)}fr) minmax(0, ${Math.floor(crB * 100)}fr)`,
219
+
}}
220
+
>
221
+
{nodes}
196
222
</div>
197
223
);
198
-
};
224
+
}
199
225
200
-
return (
201
-
<div class={`` + (!borderless ? ` overflow-hidden rounded-md border border-outline` : ``)}>
202
-
{length === 4 ? (
203
-
<div class="flex gap-0.5">
204
-
<div class="flex grow basis-0 flex-col gap-0.5">
205
-
{/* @once */ render(0, RenderMode.MULTIPLE_SQUARE)}
206
-
{/* @once */ render(2, RenderMode.MULTIPLE_SQUARE)}
207
-
</div>
226
+
if (length === 3) {
227
+
const a = images[0];
228
+
const b = images[1];
229
+
const c = images[2];
208
230
209
-
<div class="flex grow basis-0 flex-col gap-0.5">
210
-
{/* @once */ render(1, RenderMode.MULTIPLE_SQUARE)}
211
-
{/* @once */ render(3, RenderMode.MULTIPLE_SQUARE)}
212
-
</div>
231
+
return (
232
+
<div class={'flex gap-0.5' + (!borderless ? ` overflow-hidden rounded-md border border-outline` : ``)}>
233
+
<div class="flex aspect-square grow-2 basis-0 flex-col gap-0.5">
234
+
<img
235
+
src={/* @once */ a.thumb}
236
+
alt={/* @once */ a.alt}
237
+
class="h-full w-full object-cover text-[0px]"
238
+
/>
213
239
</div>
214
-
) : length === 3 ? (
215
-
<div class="flex gap-0.5">
216
-
<div class="flex aspect-square grow-2 basis-0 flex-col gap-0.5">
217
-
{/* @once */ render(0, RenderMode.MULTIPLE)}
218
-
</div>
219
240
220
-
<div class="flex grow basis-0 flex-col gap-0.5">
221
-
{/* @once */ render(1, RenderMode.MULTIPLE_SQUARE)}
222
-
{/* @once */ render(2, RenderMode.MULTIPLE_SQUARE)}
223
-
</div>
224
-
</div>
225
-
) : length === 2 ? (
226
-
<div class="flex aspect-video gap-0.5">
227
-
<div class="flex grow basis-0 flex-col gap-0.5">{/* @once */ render(0, RenderMode.MULTIPLE)}</div>
228
-
<div class="flex grow basis-0 flex-col gap-0.5">{/* @once */ render(1, RenderMode.MULTIPLE)}</div>
241
+
<div class="flex grow basis-0 flex-col gap-0.5">
242
+
<img
243
+
src={/* @once */ b.thumb}
244
+
alt={/* @once */ b.alt}
245
+
class="h-full w-full object-cover text-[0px]"
246
+
/>
247
+
<img
248
+
src={/* @once */ c.thumb}
249
+
alt={/* @once */ c.alt}
250
+
class="h-full w-full object-cover text-[0px]"
251
+
/>
229
252
</div>
230
-
) : length === 1 ? (
231
-
<>{/* @once */ render(0, RenderMode.STANDALONE)}</>
232
-
) : null}
233
-
</div>
234
-
);
253
+
</div>
254
+
);
255
+
}
256
+
257
+
if (length === 4) {
258
+
const rs = images.map(getAspectRatio);
259
+
const [crA, crB, crC, crD] = rs.map(clampBetween3_4And16_9);
260
+
261
+
const totalWidth = Math.max(crA, crC) + Math.max(crB, crD);
262
+
const totalHeight = Math.max(1 / crA, 1 / crB) + Math.max(1 / crC, 1 / crD);
263
+
const totalAspectRatio = totalWidth / totalHeight;
264
+
265
+
const nodes = images.map((img) => {
266
+
return (
267
+
<img
268
+
src={/* @once */ img.thumb}
269
+
alt={/* @once */ img.alt}
270
+
class="h-full w-full object-cover text-[0px]"
271
+
/>
272
+
);
273
+
});
274
+
275
+
return (
276
+
<div
277
+
class={`grid gap-0.5` + (!borderless ? ` overflow-hidden rounded-md border border-outline` : ``)}
278
+
style={{
279
+
'aspect-ratio': totalAspectRatio,
280
+
'grid-template-columns': `minmax(0, ${Math.floor(crA * 100)}fr) minmax(0, ${Math.floor(crB * 100)}fr)`,
281
+
'grid-template-rows': `minmax(0, ${Math.floor(crC * 100)}fr) minmax(0, ${Math.floor(crD * 100)}fr)`,
282
+
}}
283
+
>
284
+
{nodes}
285
+
</div>
286
+
);
287
+
}
288
+
289
+
return null;
235
290
};
+1
src/components/threads/post-thread-item.tsx
+1
src/components/threads/post-thread-item.tsx
+1
src/components/timeline/post-feed-item.tsx
+1
src/components/timeline/post-feed-item.tsx