Skip to content

fix(viewer): only show album grid for photos and videos#157

Merged
GeiserX merged 1 commit into
GeiserX:mainfrom
dom1n1nk4s:fix/album-grid-media-type-check
May 20, 2026
Merged

fix(viewer): only show album grid for photos and videos#157
GeiserX merged 1 commit into
GeiserX:mainfrom
dom1n1nk4s:fix/album-grid-media-type-check

Conversation

@dom1n1nk4s
Copy link
Copy Markdown
Contributor

@dom1n1nk4s dom1n1nk4s commented May 20, 2026

Summary

  • Multiple file messages were not displayed correctly, as they were always treated as media files.
  • This PR fixes that.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that causes existing functionality to change)
  • Documentation update
  • Infrastructure/CI change

Database Changes

  • Schema changes (Alembic migration required)
  • Data migration script added in scripts/
  • No database changes

Data Consistency Checklist

  • All chat_id values use marked format (via _get_marked_id())
  • All datetime values pass through _strip_tz() before DB operations
  • INSERT and UPDATE operations handle the same fields identically

Testing

  • Tests pass locally (python -m pytest tests/ -v)
  • Linting passes (ruff check .)
  • Formatting passes (ruff format --check .)
  • Manually tested in development environment

Security Checklist

  • No secrets or credentials committed
  • User input properly validated/sanitized
  • Authentication/authorization properly checked

Here's an example of how it looks like on Telegram:
image

And how it looks like on latest version:
image

After this PR's fix, it looks like this:
image

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced lightbox modal for viewing and navigating media with improved controls
    • Strengthened push notification handling with permission checks
    • Improved "jump to date" functionality with better message loading
  • Bug Fixes

    • Fixed album visibility logic for grouped messages
  • Improvements

    • Better WebSocket reconnection and error handling
    • Optimized chat pagination and infinite-scroll performance

Review Change Stack

@dom1n1nk4s dom1n1nk4s requested a review from GeiserX as a code owner May 20, 2026 19:07
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

Single-file Vue template update introducing lightbox media viewing, refining message/album handling, strengthening WebSocket and push notification infrastructure, improving date-based navigation, and reformatting template markup throughout with functional behavior refinements.

Changes

Chat UI, Lightbox, and Notification Updates

Layer / File(s) Summary
Sidebar and notification template layout
src/web/templates/index.html
Sidebar markup reflow (stats dropdown, folder tabs) and notification banner conditionals rewrapped with whitespace adjustments; no logic changes.
Pinned-message and lightbox modal markup
src/web/templates/index.html
Pinned-message banner and lightbox modal templates restructured with image/video switching, controls, and download links preserved; DOM formatting and line-break changes only.
Lightbox state and media navigation
src/web/templates/index.html
Introduces lightbox Vue state (lightboxOpen, lightboxMedia, lightboxIndex) and computed media navigation (mediaMessages, isLightboxImage, isLightboxVideo); restructures chat loading and infinite-scroll setup with sentinel observed after DOM insertion.
Message loading, scrolling, and album hiding
src/web/templates/index.html
Refines message sorting and album grouping; gates album hiding to photo/video grouped messages only; rewires pinned-message click handlers and refresh/timer logic; updates scroll-to-bottom visibility; includes scroll handler restructuring and date-separator timezone logic.
Lightbox helpers and media transitions
src/web/templates/index.html
Whitespace and structure refactor around lightbox error handling, navigation, and closure without changing state transitions.
WebSocket lifecycle and connection handling
src/web/templates/index.html
Adds explicit onclose reconnect and onerror handlers to WebSocket setup; updates chat data loading and stats UI wiring; initializes timezone configuration.
Push and notification system
src/web/templates/index.html
Reorganizes push/notification control flow with permission guards and early returns; refactors subscription/unsubscription functions with additional checks; gates in-page notification display to skip when push is enabled and subscribed.
Date formatting and timezone handling
src/web/templates/index.html
Enhances formatLastBackupTime with parsed date validation and explicit invalid-format handling; updates date-separator logic to compute day boundaries using viewer timezone conversion.
Date-based chat navigation
src/web/templates/index.html
Improves jumpToDate async correctness with stricter abort checks when switching chats mid-request; strengthens gap-filling loop to load older messages until target message exits extreme end of loaded set.
File formatting
src/web/templates/index.html
Preserves trailing newline.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main functional change: limiting album grid display to photos and videos only, which directly addresses the bug of non-media files being treated as media.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description is complete with summary, type of change selection, all checklists completed, and visual examples provided.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/web/templates/index.html (4)

3325-3345: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Seed the picker with viewerTimezone, not the browser timezone.

openDatePicker() uses moment(initialDate), while the date separator and jumpToDate() are based on viewerTimezone. If those timezones differ, clicking a separator near midnight can preselect the wrong day and jump to the wrong messages.

Suggested fix
-    selectedDate.value = initialDate ? moment(initialDate).format('YYYY-MM-DD') : null
+    selectedDate.value = initialDate
+        ? moment.utc(initialDate).tz(viewerTimezone.value).format('YYYY-MM-DD')
+        : null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/web/templates/index.html` around lines 3325 - 3345, openDatePicker
currently seeds the picker with moment(initialDate) which uses the browser
timezone; change it to parse/format using viewerTimezone so the picker
defaultDate and selectedDate.value use the same timezone as jumpToDate and the
date separators. In the openDatePicker function (and where flatpickr defaultDate
is set), replace moment(initialDate).format('YYYY-MM-DD') / new Date() usage
with moment.tz(initialDate || /* fallback */,
viewerTimezone).format('YYYY-MM-DD') (or construct a timezone-aware Date for
flatpickr) so flatpickrInstance.setDate and the selectedDate.value are based on
viewerTimezone rather than the client's local timezone.

2049-2087: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid reconnect storms when replacing the socket.

initWebSocket() closes the previous socket, but that socket's onclose still queues setTimeout(initWebSocket, 5000). A manual reinit or logout can leave stale reconnect timers behind and eventually create parallel sockets, duplicate subscriptions, and duplicate notifications.

Suggested fix
 const initWebSocket = () => {
-    if (ws) {
-        ws.close()
+    if (wsReconnectTimer) {
+        clearTimeout(wsReconnectTimer)
+        wsReconnectTimer = null
+    }
+    if (ws) {
+        ws.onclose = null
+        ws.close()
+        ws = null
     }
 
     const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
     const wsUrl = `${protocol}//${window.location.host}/ws/updates`
 
     try {
-        ws = new WebSocket(wsUrl)
+        const socket = new WebSocket(wsUrl)
+        ws = socket
 
-        ws.onopen = () => {
+        socket.onopen = () => {
             // ...
         }
 
-        ws.onmessage = (event) => {
+        socket.onmessage = (event) => {
             // ...
         }
 
-        ws.onclose = () => {
+        socket.onclose = () => {
+            if (ws !== socket || !isAuthenticated.value) return
             console.log('[WS] Connection closed, reconnecting in 5s...')
             wsReconnectTimer = setTimeout(initWebSocket, 5000)
         }
 
-        ws.onerror = (e) => {
+        socket.onerror = (e) => {
             console.warn('[WS] Error:', e)
         }
     } catch (e) {
         console.warn('[WS] Failed to connect:', e)
     }
 }
As per coding guidelines, `src/**`: "Check for proper async/await usage and connection cleanup."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/web/templates/index.html` around lines 2049 - 2087, initWebSocket
currently closes an existing ws but leaves its onclose handler active, causing
reconnect storms; to fix, ensure you clear any pending wsReconnectTimer
(clearTimeout on wsReconnectTimer) and suppress the old socket's onclose before
calling close (e.g., remove or overwrite ws.onclose, ws.onmessage, ws.onopen,
ws.onerror to no-ops) so the old socket cannot queue another initWebSocket;
after closing also null out ws to indicate no active socket and only set the
reconnect timer from the new socket's onclose handler (referencing
initWebSocket, ws, wsReconnectTimer, and the ws.onclose handler).

3402-3475: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The gap-fill loop is indexing newest/oldest backwards.

sortedMessages is newest-first, but this block stores sortedMessages[0] as the "oldest" loaded date and breaks when targetIdx > 0. That makes the background fill stop immediately for most jumped-to messages, so the surrounding history never loads.

Suggested fix
-    const oldestLoadedDateBeforeJump = sortedMessages.value.length > 0
-        ? sortedMessages.value[0].date
+    const oldestLoadedDateBeforeJump = sortedMessages.value.length > 0
+        ? sortedMessages.value[sortedMessages.value.length - 1].date
         : null
@@
-    // Check if gap is filled by looking at target's position
-    // in sorted messages (oldest first)
+    // Check if gap is filled by looking at target's position
+    // in sorted messages (newest first)
     const sorted = sortedMessages.value
     const targetIdx = sorted.findIndex(m => m.id === message.id)
 
-    // Gap is filled when target is no longer the oldest
-    if (targetIdx > 0) {
+    // Gap is filled when target is no longer the oldest loaded message
+    if (targetIdx !== -1 && targetIdx < sorted.length - 1) {
         break
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/web/templates/index.html` around lines 3402 - 3475, The code treats
sortedMessages as newest-first but uses index 0 as the oldest and stops filling
when targetIdx > 0; fix by treating the last element as the oldest and reversing
the targetIndex check: set oldestLoadedDateBeforeJump to
sortedMessages.value[sortedMessages.value.length - 1].date (or null) instead of
[0], and inside fillGap compute targetIdx and consider the gap filled when
targetIdx < sorted.length - 1 (i.e., target is no longer the last/oldest),
updating the break condition accordingly; leave other logic (loadMessages,
hasMore, messages, scrollToMessage) unchanged.

2526-2545: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Gate shared captions the same way as album hiding.

Now that grouped documents stay visible, getAlbumCaption(msg) still treats any grouped_id as a shared album. A grouped file batch with one caption will render that same caption under every document card.

Suggested follow-up
 const getAlbumCaption = (msg) => {
     const groupedId = getGroupedId(msg)
     if (!groupedId) return null
+    if (msg.media?.type !== 'photo' && msg.media?.type !== 'video') return null
     const album = getAlbumForMessage(msg)
     if (!album) return null
     const captionMsg = album.find(m => m.text && m.text.trim() !== '')
     return captionMsg ? captionMsg.text : null
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/web/templates/index.html` around lines 2526 - 2545, getAlbumCaption
currently treats any message with a grouped_id as a shared album caption and
shows it for every card; to fix, gate caption rendering the same way as album
hiding by making getAlbumCaption only return the caption for the first item in
the album. Update getAlbumCaption(msg) to either accept an index (e.g.,
getAlbumCaption(msg, index)) or call isFirstInAlbum(msg, index) (using
sortedMessages.value.indexOf(msg) if not passing index) and return the caption
only when isFirstInAlbum(...) is true (and keep existing checks for
msg.media?.type and getGroupedId(msg)). Ensure you reference getAlbumCaption,
isFirstInAlbum, isHiddenAlbumMessage, getGroupedId, and sortedMessages.value in
the change so captions only appear on the first album item.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/web/templates/index.html`:
- Around line 3325-3345: openDatePicker currently seeds the picker with
moment(initialDate) which uses the browser timezone; change it to parse/format
using viewerTimezone so the picker defaultDate and selectedDate.value use the
same timezone as jumpToDate and the date separators. In the openDatePicker
function (and where flatpickr defaultDate is set), replace
moment(initialDate).format('YYYY-MM-DD') / new Date() usage with
moment.tz(initialDate || /* fallback */, viewerTimezone).format('YYYY-MM-DD')
(or construct a timezone-aware Date for flatpickr) so flatpickrInstance.setDate
and the selectedDate.value are based on viewerTimezone rather than the client's
local timezone.
- Around line 2049-2087: initWebSocket currently closes an existing ws but
leaves its onclose handler active, causing reconnect storms; to fix, ensure you
clear any pending wsReconnectTimer (clearTimeout on wsReconnectTimer) and
suppress the old socket's onclose before calling close (e.g., remove or
overwrite ws.onclose, ws.onmessage, ws.onopen, ws.onerror to no-ops) so the old
socket cannot queue another initWebSocket; after closing also null out ws to
indicate no active socket and only set the reconnect timer from the new socket's
onclose handler (referencing initWebSocket, ws, wsReconnectTimer, and the
ws.onclose handler).
- Around line 3402-3475: The code treats sortedMessages as newest-first but uses
index 0 as the oldest and stops filling when targetIdx > 0; fix by treating the
last element as the oldest and reversing the targetIndex check: set
oldestLoadedDateBeforeJump to sortedMessages.value[sortedMessages.value.length -
1].date (or null) instead of [0], and inside fillGap compute targetIdx and
consider the gap filled when targetIdx < sorted.length - 1 (i.e., target is no
longer the last/oldest), updating the break condition accordingly; leave other
logic (loadMessages, hasMore, messages, scrollToMessage) unchanged.
- Around line 2526-2545: getAlbumCaption currently treats any message with a
grouped_id as a shared album caption and shows it for every card; to fix, gate
caption rendering the same way as album hiding by making getAlbumCaption only
return the caption for the first item in the album. Update getAlbumCaption(msg)
to either accept an index (e.g., getAlbumCaption(msg, index)) or call
isFirstInAlbum(msg, index) (using sortedMessages.value.indexOf(msg) if not
passing index) and return the caption only when isFirstInAlbum(...) is true (and
keep existing checks for msg.media?.type and getGroupedId(msg)). Ensure you
reference getAlbumCaption, isFirstInAlbum, isHiddenAlbumMessage, getGroupedId,
and sortedMessages.value in the change so captions only appear on the first
album item.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a362493c-e210-49ac-8620-a7e9580230be

📥 Commits

Reviewing files that changed from the base of the PR and between a890ed0 and 9437781.

📒 Files selected for processing (1)
  • src/web/templates/index.html

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.20%. Comparing base (a890ed0) to head (9437781).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #157      +/-   ##
==========================================
+ Coverage   93.11%   93.20%   +0.09%     
==========================================
  Files          22       22              
  Lines        6300     6300              
==========================================
+ Hits         5866     5872       +6     
+ Misses        434      428       -6     

see 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@GeiserX GeiserX merged commit 429c52e into GeiserX:main May 20, 2026
9 checks passed
@GeiserX
Copy link
Copy Markdown
Owner

GeiserX commented May 20, 2026

Thanks @dom1n1nk4s for the fix! Released in v7.8.4. The album grid now correctly renders only for photos and videos — documents with grouped_id no longer get squeezed into a media grid. Appreciate the clear screenshots showing the before/after!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants