diff --git a/src/components/item.astro b/src/components/item.astro
index d09f30f..068f61b 100644
--- a/src/components/item.astro
+++ b/src/components/item.astro
@@ -110,3 +110,77 @@ const commentsClass = 'ml-[3px] border-l-2 border-line pb-6 pl-[15px] pt-[6px] s
)
}
+
+
diff --git a/src/lib/telegram/index.ts b/src/lib/telegram/index.ts
index ad14220..b6cab27 100644
--- a/src/lib/telegram/index.ts
+++ b/src/lib/telegram/index.ts
@@ -235,59 +235,90 @@ function getImageStickers($: CheerioAPI, message: MessageSelection, options: Ind
function getImages($: CheerioAPI, message: MessageSelection, options: MessageAssetOptions): string {
const { staticProxy = '', id = '', index = 0, title = '' } = options
- const fragments: string[] = []
const loading = getImageLoading(index)
const safeTitle = escapeHtmlAttribute(title || 'Image from post')
- const safePreviewLabel = escapeHtmlAttribute(title ? `Open image preview: ${title}` : 'Open image preview')
const safeCloseLabel = 'Close image preview'
- for (const [photoIndex, photoNode] of message.find('.tgme_widget_message_photo_wrap').toArray().entries()) {
+ interface PhotoData { imageUrl: string, width: number, height: number }
+ const photos: PhotoData[] = []
+
+ for (const photoNode of message.find('.tgme_widget_message_photo_wrap').toArray()) {
const imageUrl = $(photoNode).attr('style')?.match(STYLE_URL_REGEX)?.[1]
-
- if (!imageUrl) {
+ if (!imageUrl)
continue
- }
-
- const popoverId = `modal-${id}-${photoIndex}`
const { width, height } = inferImageDimensions($, photoNode)
- fragments.push(`
-
-
-
-
-
-

-
-
- `)
+ photos.push({ imageUrl, width, height })
}
- if (!fragments.length) {
+ if (!photos.length) {
return ''
}
- const layoutClass = fragments.length % 2 === 0 ? 'image-list-even' : 'image-list-odd'
- return `${fragments.join('')}
`
+ // Single image: keep existing per-image popover behavior
+ if (photos.length === 1) {
+ const { imageUrl, width, height } = photos[0]
+ const popoverId = `modal-${id}-0`
+ const safePreviewLabel = escapeHtmlAttribute(title ? `Open image preview: ${title}` : 'Open image preview')
+ return `
+
+
+
+
+
+
+

+
+
+
+ `
+ }
+
+ // Multiple images: single shared gallery modal with thumbnail grid
+ const galleryId = `gallery-${id}`
+ const count = photos.length
+ // 2 or 4 photos → 2-column; everything else → 3-column (matches Telegram)
+ const colClass = count === 2 || count === 4 ? 'image-gallery-2col' : 'image-gallery-3col'
+
+ const thumbnailButtons = photos.map(({ imageUrl, width, height }, i) => {
+ const safeGalleryLabel = escapeHtmlAttribute(`Open image gallery, image ${i + 1} of ${count}`)
+ return `
+ `
+ }).join('')
+
+ const galleryImages = photos.map(({ imageUrl, width, height }, i) =>
+ `
0 ? ' hidden' : ''} />`,
+ ).join('')
+
+ return `
+ ${thumbnailButtons}
+
+
+
+
+
+
+
${galleryImages}
+
1 / ${count}
+
+ `
}
function getVideo($: CheerioAPI, message: MessageSelection, options: IndexedStaticProxyOptions): string {
diff --git a/src/styles/content.css b/src/styles/content.css
index c94c7cf..258f18f 100644
--- a/src/styles/content.css
+++ b/src/styles/content.css
@@ -100,7 +100,7 @@
line-break: anywhere;
}
- img:not(.tg-emoji):not(.sticker):not(.link_preview_image):not(.modal-img) {
+ img:not(.tg-emoji):not(.sticker):not(.link_preview_image):not(.modal-img):not(.gallery__thumb) {
width: calc(100% - var(--box-margin));
max-width: calc(100% - 1px);
max-height: initial;
@@ -250,6 +250,114 @@
margin-bottom: 0;
}
+ /* Compact Telegram-style photo grid */
+ .image-gallery {
+ display: grid;
+ gap: 2px;
+ border-radius: var(--radius-media);
+ overflow: hidden;
+ width: calc(100% - var(--box-margin));
+ max-width: calc(100% - 1px);
+ margin-bottom: 12px;
+ }
+
+ .image-gallery.image-gallery-2col {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .image-gallery.image-gallery-3col {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .image-gallery__trigger {
+ display: block;
+ overflow: hidden;
+ border-radius: 0;
+ margin-bottom: 0;
+ padding: 0;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ appearance: none;
+ }
+
+ .image-gallery__trigger img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ aspect-ratio: 1;
+ object-fit: cover;
+ transition: transform 0.2s ease;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .image-gallery__trigger img {
+ transition: none;
+ }
+ }
+
+ .image-gallery__trigger:hover img,
+ .image-gallery__trigger:focus-visible img {
+ transform: scale(1.04);
+ }
+
+ .image-gallery__trigger:focus-visible {
+ outline: 2px solid var(--color-heading);
+ outline-offset: -2px;
+ }
+
+ .gallery__img[hidden] {
+ display: none;
+ }
+
+ .gallery__nav {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 20;
+ width: 48px;
+ height: 48px;
+ border-radius: 9999px;
+ background: rgba(0, 0, 0, 0.6);
+ color: #fff;
+ font-size: 36px;
+ line-height: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ cursor: pointer;
+ user-select: none;
+ padding: 0;
+ }
+
+ .gallery__nav:hover {
+ background: rgba(0, 0, 0, 0.8);
+ }
+
+ .gallery__prev {
+ left: max(16px, env(safe-area-inset-left, 16px));
+ }
+
+ .gallery__next {
+ right: max(16px, env(safe-area-inset-right, 16px));
+ }
+
+ .gallery__counter {
+ position: absolute;
+ bottom: max(16px, env(safe-area-inset-bottom, 16px));
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 20;
+ color: #fff;
+ font-size: 13px;
+ background: rgba(0, 0, 0, 0.5);
+ padding: 3px 12px;
+ border-radius: 9999px;
+ margin: 0;
+ white-space: nowrap;
+ }
+
.tgme_widget_message_link_preview {
margin-top: 16px;
display: none;
@@ -548,6 +656,10 @@
cursor: pointer;
}
+ .image-preview-button.image-gallery__trigger {
+ margin-bottom: 0;
+ }
+
.image-preview-button:focus-visible {
outline: 2px solid var(--color-heading);
outline-offset: 4px;