Skip to content

Support selecting an Immich album for shared trip photos - #3259

Closed
cleniemeyer-collab wants to merge 4 commits into
Freika:masterfrom
cleniemeyer-collab:feature/immich-share-tags
Closed

cleniemeyer-collab wants to merge 4 commits into
Freika:masterfrom
cleniemeyer-collab:feature/immich-share-tags

Conversation

@cleniemeyer-collab

@cleniemeyer-collab cleniemeyer-collab commented Jul 29, 2026

Copy link
Copy Markdown

Summary

This PR replaces the previous tag-based photo filtering with optional Immich album selection for shared trips.

Features

  • optional Immich album selection when creating a shared link
  • only albums overlapping the trip date are shown
  • albums with no assets are hidden
  • shared photos are filtered by the selected album
  • added a simple fullscreen photo preview for shared trips

Notes

The album filtering is currently implemented in the view to keep the change small and easy to review. It could later be moved into the Immich::Albums service if preferred.

Summary by CodeRabbit

  • New Features
    • Added photo visibility choices for shared links: no photos, public photos, or public and family photos.
    • Shared-link photo searches now respect the selected visibility scope and apply relevant tag filtering.
    • Added tag-based photo retrieval and improved support for multiple selected tags.
  • Improvements
    • Shared trip photos now load when a day is opened, reducing initial page loading.
    • Increased the maximum number of photos displayed from 100 to 2,000.
    • Added sensible default photo visibility settings for shared links.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Shared-link photo settings now support public and family scopes, resolve corresponding Immich tags, apply tag-aware cached searches, and lazy-load trip photos when day sections open.

Changes

Shared-link photo scoping

Layer / File(s) Summary
Photo scope settings
app/models/shared_link.rb, app/controllers/concerns/share_links/managable.rb, app/views/shared_links/*
Defaults, forms, radio controls, and parameter normalization now support public and family photo scopes.
Immich tag resolution
app/services/immich/tags.rb, app/services/shared_links/photo_scope.rb
Immich tags are cached and resolved into required tag IDs, with failures returning no resolved scope.
Tag-scoped photo search
app/services/photos/search.rb, app/services/immich/request_photos.rb, app/services/photos/mappable.rb
Search caching and Immich requests accept tag IDs, aggregate multi-tag results, exclude unfiltered PhotoPrism results, and raise the default mapping limit to 2,000 photos.
Shared photo retrieval and rendering
app/controllers/api/v1/shared/photos_controller.rb, app/services/shared_links/trip_photos.rb, app/views/shared/links/_trip.html.erb
Shared-photo searches pass resolved tags, while trip-day photo grids are injected on details expansion and cleared on collapse.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SharedLink as Shared link
  participant PhotoScope as SharedLinks::PhotoScope
  participant Tags as Immich::Tags
  participant Search as Photos::Search
  participant Immich as Immich::RequestPhotos
  SharedLink->>PhotoScope: Resolve configured photo scope
  PhotoScope->>Tags: Fetch cached Immich tags
  PhotoScope-->>Search: Provide resolved tag IDs
  Search->>Immich: Search with tagIds
  Immich-->>Search: Return tagged assets
  Search-->>SharedLink: Render scoped photos
Loading

Possibly related PRs

Suggested reviewers: freika

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The changes add shared-link photo scope/tag filtering and lazy-loaded trip photos, but nothing about selecting an Immich album. Retitle it to describe the actual change, e.g. "Add shared-link photo scope filtering for trip photos".
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
app/controllers/concerns/share_links/managable.rb (1)

144-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated scope whitelist — single source of truth suggested.

%w[public family] duplicates the valid-scopes list already encapsulated in SharedLinks::PhotoScope (VALID_SCOPES, per the referenced snippet in app/services/shared_links/photo_scope.rb). If a new scope is added there, this literal must be updated too or requests will silently fall back to public.

♻️ Proposed fix referencing the shared constant
-      scope = values['photo_scope'].to_s
-      settings['photo_scope'] = %w[public family].include?(scope) ? scope : 'public'
+      scope = values['photo_scope'].to_s
+      settings['photo_scope'] =
+        SharedLinks::PhotoScope::VALID_SCOPES.include?(scope) ? scope : 'public'
🤖 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 `@app/controllers/concerns/share_links/managable.rb` around lines 144 - 145,
Update the photo_scope validation in the settings assignment to use
SharedLinks::PhotoScope::VALID_SCOPES instead of the duplicated %w[public
family] literal, preserving the existing fallback to "public".
app/views/shared/links/_trip.html.erb (1)

105-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a Stimulus controller instead of a raw inline <script>.

The logic itself is correct (capture-phase listener properly handles the non-bubbling toggle event; content is cloned once and cleared on close). However, the rest of this file and the surrounding Hotwire app rely on Stimulus controllers/data-action bindings rather than ad-hoc global listeners. Wrapping this in a small Stimulus controller (e.g. trip-day-photos_controller.js with a toggle action) would be more consistent with the codebase's conventions, easier to test, and avoids the manual "install once on window" guard.

🤖 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 `@app/views/shared/links/_trip.html.erb` around lines 105 - 137, Replace the
inline script’s global toggle listener and window installation guard with a
Stimulus controller, such as trip-day-photos_controller.js, and bind its toggle
action through the existing data-action conventions. Move the open/close
behavior into the controller while preserving the HTMLDetailsElement check, lazy
template cloning, and clearing the container when closed.
app/services/photos/search.rb (2)

53-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding spec coverage for the new multi-tag aggregation path.

request_immich/request_immich_for_multiple_tags introduce non-trivial branching (single vs. multi-tag, dedupe-by-id, nil short-circuiting on partial failure) flagged as high complexity. A few unit tests covering: single tag, multiple tags with overlap, and one tag request failing (should abort the whole search) would guard this logic going forward.

🤖 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 `@app/services/photos/search.rb` around lines 53 - 79, Add unit specs for the
Immich request flow covering single-tag requests, multi-tag aggregation with
overlapping assets deduplicated by ID, and a multi-tag request where one tag
returns nil and the overall search aborts. Anchor the tests to request_immich
and request_immich_for_multiple_tags, preserving the existing single-tag
behavior and nil short-circuit.

53-79: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Run the per-tag Immich lookups concurrently.

request_immich_for_multiple_tags sends one blocking HTTParty.post request per tag, each with a 10s timeout. For 2+ tags, a cache-miss Immich request can take 20s+ before the per-route 30-minute cache is written. Bound the worst-case to a single request timeout by firing the per-tag searches concurrently.

Don’t change the union strategy to a multi-value tagIds request: Immich’s search metadata filter requires assets to match all supplied tag IDs, so Union-or behavior only works with separate per-tag requests.

🤖 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 `@app/services/photos/search.rb` around lines 53 - 79, Update
request_immich_for_multiple_tags to execute each request_immich_assets([tag_id])
lookup concurrently while preserving the existing union behavior by
concatenating results from separate per-tag searches. Propagate a nil result as
failure, wait for all lookups, and return the combined assets without changing
the single-tag path.
🤖 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.

Inline comments:
In `@app/services/photos/mappable.rb`:
- Line 4: Revert the MAX_PHOTOS increase in Photos::Mappable and decouple the
map-marker limit from the gallery limit. Preserve the existing capped_geotagged
default used by the shared photos controller, while allowing the gallery call
site to use its larger explicit limit through a separate constant or parameter.

In `@app/services/shared_links/trip_photos.rb`:
- Around line 36-44: Update app/services/shared_links/trip_photos.rb lines 36-44
and app/controllers/api/v1/shared/photos_controller.rb lines 71-80 so
PhotoPrism-only configurations still resolve shared photos through
Photos::Search when SharedLinks::PhotoScope#tag_ids returns nil. Replace the
unconditional empty-result path with the appropriate PhotoPrism fallback, while
preserving the existing Immich tag-filtered behavior.

In `@app/views/shared_links/_photo_scope_fields.html.erb`:
- Around line 6-42: Update the photo scope fields partial around the hidden
fields and photo selection radio buttons to accept the current shared-link
settings and use them when rendering. Initialize show_photos, photo_scope, and
the checked radio choice from those settings, preserving the existing defaults
only when no prior values are available so validation rerenders retain public or
family selections.

---

Nitpick comments:
In `@app/controllers/concerns/share_links/managable.rb`:
- Around line 144-145: Update the photo_scope validation in the settings
assignment to use SharedLinks::PhotoScope::VALID_SCOPES instead of the
duplicated %w[public family] literal, preserving the existing fallback to
"public".

In `@app/services/photos/search.rb`:
- Around line 53-79: Add unit specs for the Immich request flow covering
single-tag requests, multi-tag aggregation with overlapping assets deduplicated
by ID, and a multi-tag request where one tag returns nil and the overall search
aborts. Anchor the tests to request_immich and request_immich_for_multiple_tags,
preserving the existing single-tag behavior and nil short-circuit.
- Around line 53-79: Update request_immich_for_multiple_tags to execute each
request_immich_assets([tag_id]) lookup concurrently while preserving the
existing union behavior by concatenating results from separate per-tag searches.
Propagate a nil result as failure, wait for all lookups, and return the combined
assets without changing the single-tag path.

In `@app/views/shared/links/_trip.html.erb`:
- Around line 105-137: Replace the inline script’s global toggle listener and
window installation guard with a Stimulus controller, such as
trip-day-photos_controller.js, and bind its toggle action through the existing
data-action conventions. Move the open/close behavior into the controller while
preserving the HTMLDetailsElement check, lazy template cloning, and clearing the
container when closed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ee01dfc-1083-4453-b46d-4fe98ebabeed

📥 Commits

Reviewing files that changed from the base of the PR and between da551a0 and e331e54.

📒 Files selected for processing (14)
  • app/controllers/api/v1/shared/photos_controller.rb
  • app/controllers/concerns/share_links/managable.rb
  • app/models/shared_link.rb
  • app/services/immich/request_photos.rb
  • app/services/immich/tags.rb
  • app/services/photos/mappable.rb
  • app/services/photos/search.rb
  • app/services/shared_links/photo_scope.rb
  • app/services/shared_links/trip_photos.rb
  • app/views/shared/links/_trip.html.erb
  • app/views/shared_links/_modal_timeline_create_form.html.erb
  • app/views/shared_links/_modal_track_create_form.html.erb
  • app/views/shared_links/_modal_trip_create_form.html.erb
  • app/views/shared_links/_photo_scope_fields.html.erb

Comment thread app/services/photos/mappable.rb
Comment on lines +36 to +44
tag_ids = SharedLinks::PhotoScope.new(@link).tag_ids
return [] if tag_ids.nil?

Photos::Search.cached(
@link.user,
start_date: trip.started_at.iso8601,
end_date: trip.ended_at.iso8601,
tag_ids: tag_ids
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^app/services/shared_links/trip_photos\.rb$|^app/controllers/api/v1/shared/photos_controller\.rb$|SharedLinks|Immich|Photos::Search|PhotoScope)' || true

echo "== trip_photos outline =="
ast-grep outline app/services/shared_links/trip_photos.rb || true
echo

echo "== photos_controller outline =="
ast-grep outline app/controllers/api/v1/shared/photos_controller.rb || true
echo

echo "== relevant lines =="
cat -n app/services/shared_links/trip_photos.rb | sed -n '1,120p'
echo "---------- photos_controller ----------"
cat -n app/controllers/api/v1/shared/photos_controller.rb | sed -n '1,130p'

Repository: Freika/dawarich

Length of output: 7346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== broad searches =="
rg -n "class PhotoScope|module PhotoScope|SharedLinks::PhotoScope|def tag_ids|tag_ids =|Immich::Tags|Photos::Search\.cached|configurate|configured|immich|photo_prism|PhotoPrism" app lib config test spec || true

echo "== locate immich/photo services/controllers =="
git ls-files app lib | rg -i 'immich|photo_prism|photos|search' || true

Repository: Freika/dawarich

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== photo_scope.rb =="
cat -n app/services/shared_links/photo_scope.rb | sed -n '1,80p'
echo

echo "== photos/search.rb relevant =="
cat -n app/services/photos/search.rb | sed -n '1,120p'
echo

echo "== trip_photos_spec relevant =="
cat -n spec/services/shared_links/trip_photos_spec.rb | sed -n '1,120p'
echo

echo "== photo_scope tests if any =="
rg -n "SharedLinks::PhotoScope|SharedLinks\\:\\:PhotoScope|class SharedLinks/PhotoScope|module SharedLinks/PhotoScope|shared_links.*photo_scope|PhotoScope" spec app lib 2>/dev/null | head -200 || true

echo "== focused test files mentioning missing immich in shared/trip =="
grep -RIn "photo_prism\\|photoprism\\|only.*photo\\|no.*immich\\|Immich integration" spec app services/controllers -i 2>/dev/null | rg -i "shared_links|trip_photos|shared/photos|photoprism|unique" | head -150 || true

Repository: Freika/dawarich

Length of output: 25791


Handle PhotoPrism-only sharing before returning an empty result. SharedLinks::PhotoScope#tag_ids resolves tags only through Immich::Tags, falls back to nil on missing Immich config, and both shared photo paths return [] in that case. Since Photos::Search supports PhotoPrism directly, PhotoPrism-only users will see no shared trip/map photos unless Immich is configured; add a PhotoPrism-only fallback or ensure the empty state is intentional and communicated.

  • app/services/shared_links/trip_photos.rb#L36-L44
  • app/controllers/api/v1/shared/photos_controller.rb#L71-L80
📍 Affects 2 files
  • app/services/shared_links/trip_photos.rb#L36-L44 (this comment)
  • app/controllers/api/v1/shared/photos_controller.rb#L71-L80
🤖 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 `@app/services/shared_links/trip_photos.rb` around lines 36 - 44, Update
app/services/shared_links/trip_photos.rb lines 36-44 and
app/controllers/api/v1/shared/photos_controller.rb lines 71-80 so
PhotoPrism-only configurations still resolve shared photos through
Photos::Search when SharedLinks::PhotoScope#tag_ids returns nil. Replace the
unconditional empty-result path with the appropriate PhotoPrism fallback, while
preserving the existing Immich tag-filtered behavior.

Comment on lines +6 to +42
<%= hidden_field_tag 'shared_link[settings][show_photos]',
'0',
data: { photo_scope_show_photos: true } %>

<%= hidden_field_tag 'shared_link[settings][photo_scope]',
'public',
data: { photo_scope_value: true } %>

<label class="label cursor-pointer justify-start gap-3">
<%= radio_button_tag "#{id_prefix}_photo_selection",
'none',
true,
id: "#{id_prefix}_photo_selection_none",
class: 'radio',
data: { photo_scope_choice: true } %>
<span class="label-text">No photos</span>
</label>

<label class="label cursor-pointer justify-start gap-3">
<%= radio_button_tag "#{id_prefix}_photo_selection",
'public',
false,
id: "#{id_prefix}_photo_selection_public",
class: 'radio',
data: { photo_scope_choice: true } %>
<span class="label-text">Public photos only</span>
</label>

<label class="label cursor-pointer justify-start gap-3">
<%= radio_button_tag "#{id_prefix}_photo_selection",
'family',
false,
id: "#{id_prefix}_photo_selection_family",
class: 'radio',
data: { photo_scope_choice: true } %>
<span class="label-text">Public + family photos</span>
</label>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the selected photo scope on validation rerenders.

This always submits show_photos=0/photo_scope=public and selects “No photos.” The timeline form re-renders invalid @shared_link state, so a previously selected family/public scope is silently reset on retry. Pass the current settings into this partial and initialize both hidden values and the checked radio from them.

🤖 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 `@app/views/shared_links/_photo_scope_fields.html.erb` around lines 6 - 42,
Update the photo scope fields partial around the hidden fields and photo
selection radio buttons to accept the current shared-link settings and use them
when rendering. Initialize show_photos, photo_scope, and the checked radio
choice from those settings, preserving the existing defaults only when no prior
values are available so validation rerenders retain public or family selections.

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.

1 participant