From 02beb85642986d715534ca51c87d7878f5c2515e Mon Sep 17 00:00:00 2001 From: Chaoscontrol Date: Tue, 14 Oct 2025 22:34:20 +0100 Subject: [PATCH] feat(album): show per-user contributions in shared albums (#21740) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: show per-user contribution counts on shared albums Add API support and UI display for per-user asset contribution counts on shared albums: - server: add ContributorCountResponseDto and repository method to aggregate counts per user (excluding deleted assets), expose via album response only when shared and counts > 0 - web: display contributor counts in Album Users modal next to each member’s role This helps users understand participation levels in shared albums. * Add ContributorCountResponseDto and expose contributorCounts on AlbumResponseDto in OpenAPI spec. Regenerate TypeScript SDK and mobile OpenAPI clients to include new types. No breaking changes; fields are additive. * fix: shrink age view to fit and not overflow (#22405) Co-authored-by: Alex * chore: post release tasks (#22587) * chore: clean auth-user entity on reset (#22583) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> * fix: mitigate database lock scenario when running full sync in splash screen page (#22608) * fix: improve sync backup error indicator (#22527) * fix: improve sync indicator error * prefer backup disabled icon before error --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex * fix: bottom navigation bar overlay sheet info (#22610) * fix: respect storage indicator setting (#22596) * fix: respect storage indicator size setting * remove black bar on the bottom of the setting scaffold page --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex * fix: do not run multiple engines on cold startup (#22518) fix: do not run multiple engines on app startup Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex * fix: album selector in favorite view (#22612) * chore(web): update translations (#22486) Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/az/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/el/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/kn/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ml/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/ Translation: Immich/immich Co-authored-by: Arthur Bols Co-authored-by: Ben Kim Co-authored-by: César Gómez Co-authored-by: DR Co-authored-by: DevServs Co-authored-by: Emil Friis Osmann Co-authored-by: Fjuro Co-authored-by: Godwin T Co-authored-by: Hristo T Co-authored-by: Hurricane-32 Co-authored-by: Jozef Gaal Co-authored-by: KecskeTech Co-authored-by: Kiril Panayotov Co-authored-by: Liviu Roman Co-authored-by: Lorenzo Co-authored-by: Marcelo Popper Costa Co-authored-by: Matjaž T Co-authored-by: Miryusif Rahimov Co-authored-by: Msaood Co-authored-by: Mārtiņš Bruņenieks Co-authored-by: Pedro Vendeira Co-authored-by: PontusÖsterlindh Co-authored-by: Rahees Co-authored-by: Sandeep R Co-authored-by: Sylvain Pichon Co-authored-by: TV Box Co-authored-by: Tino Altmann Co-authored-by: User 123456789 Co-authored-by: Vegard Fladby Co-authored-by: anton garcias Co-authored-by: chamdim Co-authored-by: longlarry Co-authored-by: pyccl Co-authored-by: swever Co-authored-by: தமிழ்நேரம் Co-authored-by: 안세훈 * chore: version v2.0.1 * fix(docs): link to immich docs does not lead correctly to docs (#22687) * fix(server): fix chunking Postgres query parameters (#22684) * feat(server): improve checkAlbumAccess query performance (#22467) * Fix slow SQL query in checkAlbumAccess caused by the array overlap operator && * Update access.repository.sql * Rewrite the query to pass assetIds once as a single array parameter * chore: mark VSCode tasks as background tasks (#22631) VSCode expect tasks that aren't marked as background tasks to finish eventually. That's not how a dev-server is supposed to work, we expect it to run for basically infinite time. By marking those tasks as background tasks, VSCode stops showing the infinite loading spinner on those processes. * fix(ml): Resolve IPv6 startup crash and healthcheck failure (#22387) * fix(ml): Resolve IPv6 startup crash and healthcheck failure Fixes #13782 * fix(ml): updated the fix to use the std lib * Apply code formatting to __main__.py * fix(server): override reserved color metadata for video thumbnails (#22348) override reserved metadata * fix(mobile): trash description cut off (#22662) * fix(mobile): empty album description does not save (#22649) * fix(mobile): video player using ref after disposal (#21843) check if disposed * docs: add job order diagram (#22673) * docs: add job order diagram * wording --------- Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> * fix: missing responsive calculation in UserPageLayout (#22455) * fix: use full-size image for non-web-compatible panoramas (#20359) * fix(web): use full-size image for non-web-compatible panoramas * always generate full-size image for panoramas * add unit test * fix formatting --------- Co-authored-by: gergo= * chore: update cli docs to pnpm (#22702) update cli docs to pnpm * chore(web): upgrade ESLint and plugins (#22495) * chore(web): upgrade ESLint and plugins, simplify linting configuration - Update eslint from ^9.18.0 to ^9.36.0 - Update eslint plugins: - eslint-plugin-svelte: ^3.9.0 → ^3.12.4 - eslint-plugin-unicorn: ^60.0.0 → ^61.0.2 - svelte-eslint-parser: ^1.2.0 → ^1.3.3 - typescript-eslint: ^8.28.0 → ^8.45.0 - Remove eslint-p dependency in favor of native eslint concurrency - Add unicorn/no-array-sort rule exception - Update linting scripts to use eslint's native --concurrency flag - Update Makefile and mise.toml to reflect simplified lint commands - Update GitHub Actions workflow to use standard pnpm lint command * pnpm dedupe --------- Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * fix(web): do not notify on patch releases (#22591) * chore: post release tasks (#22616) * fix: hide view in timeline button on local timeline (#22713) * chore(server): support vectorchord 0.5.x (#21602) Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> * fix: Fix issue fail to download iOS live photos (#22708) Co-authored-by: bwees * fix(docs): Remove immich_remove_offline_files as no longer functional (#21774) Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> Co-authored-by: Brandon Wees * fix(mobile): closing editor goes back to main page (#22647) Co-authored-by: bwees * docs: update TrueNAS migration instructions (#22463) Co-authored-by: bo0tzz Co-authored-by: Nicholas Flamy <30300649+NicholasFlamy@users.noreply.github.com> * docs: update Synology install guide (#21996) Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> * fix: improve the selected sidebar item text color in dark mode (#22640) * chore(deps): update redis:6.2-alpine docker digest to 2185e74 (#22718) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore: update devcontainers for trixie, devenv changes (#22194) * fix(deps): update dependency device_info_plus to v12 (#22724) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency flutter to v3.35.5 (#22720) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update github-actions (#22721) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix: --no-git-checks on pnpm publish (#22715) * fix: --no-git-checks on sdk publish * fix: --no-git-checks on cli publish * refactor(web): Clarify property names in Timeline and Scrubber (#22265) refactor(web): Clarify property names in Timeline and Scrubber Renamed properties across Timeline/Scrubber components for clarity: - scrubOverallPercent → timelineScrollPercent - scrubberMonthPercent → viewportTopMonthScrollPercent - scrubberMonth → viewportTopMonth - leadout → isInLeadOutSection Additional changes: - Updated ScrubberListener signature to accept object parameter - Added detailed JSDoc comments for all Scrubber props - Fixed callback invocations to use new object syntax - Aligned Timeline's local state variables with Scrubber prop names * fix: promote to foreground service before starting engine (#22517) fix: show notification from native Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex * refactor(web): extract timeline keyboard actions into separate component (#22266) refactor(web): extract timeline keyboard actions into separate component Extracts keyboard shortcuts and related functionality from Timeline component into a dedicated TimelineKeyboardActions component for better separation of concerns and maintainability. * feat: make skeleton title optional (#22396) feat: skeleton title is optional feat: skeleton title optional * refactor(web): extract asset viewer logic from Timeline into TimelineAssetViewer component (#22268) refactor(web): extract asset viewer logic from Timeline into TimelineAssetViewer component - Extracted asset viewer navigation and action handling logic from Timeline.svelte into a dedicated TimelineAssetViewer component - Reduces Timeline.svelte complexity by ~150 lines and improves separation of concerns - No functional changes - purely a refactoring to improve code organization ## Changes - Created new TimelineAssetViewer.svelte component containing all asset viewer-related logic - Moved handlePrevious, handleNext, handleRandom, handleClose, handlePreAction, and handleAction methods - Timeline.svelte now only passes required props to the new component - Maintained all existing functionality including navigation, asset actions, and stack management * chore: track full actions/cache version in comment (#22359) * fix(ml): ipv6 check (#22735) * chore(deps): cache pnpm dependencies in prod build (#22555) * cache pnpm dependencies use different ids to be safe unnecessary lines * use buildcache folder * chore: use isar immich fork (#22738) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> * fix: bottom sheet blank with local assets that have remote counterparts (#22743) * chore(deps): update dependency @types/node to ^22.18.8 (#22719) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency nodemailer to v7.0.7 [security] (#22740) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update dependency connectivity_plus to v7 (#22723) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex * chore: use hosted isar flutter libs (#22757) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> * fix: skip local only assets in move to lock action (#22728) * fix:prefer trashing to deletions * skip local only assets in move to lock action --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex * fix: brief flashing when swiping from video (#22187) * fix(web): Uniform random distribution during shuffle (#19902) feat: better random distribution * fix: persist search page scroll offset between rebuilds (#22733) fix: persist search scroll between rebuilds Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex * docs: add some external library notes (#22776) * feat(web): seconds and milliseconds in timestamps (#20337) * fix(web): seconds in timestamps * changed date-input step to provide millisecond precision * feat(cli): add debug development config (#22712) * add debug and change ts-node with tsx * update pr changes * update pnpm-lock * remove ts-node from readme * typo * resolve conflicts * remove tsx * launch from dist * add preLaunchTask * update readme * undo main in package.json * remove typo * Apply suggestion from @bwees Co-authored-by: Brandon Wees * revert pnpm-lock changes * @jrasm91 suggestions * chore: run node with source maps --------- Co-authored-by: Jason Rasmussen Co-authored-by: Brandon Wees * docs: add Immich-Stack to community-projects (#21563) docs: add Immich Stack community project Co-authored-by: Jason Rasmussen * feat(web): Add upload to stack action (#19842) * feat(web): Add upload to stack action * Event handling and translation * Update asset viewer instead * lint, improve upload return type * Add suggestions from code review * Resolve merge conflicts * Apply suggestions from code review * feat(server): add `immich.users.total` metric (#21780) * Add immich.users.total metric * Fix tests & one lint error * Lint * Fix SQL Schema checks * Fix nit * Use workers argument in OnEvent hook and remove condition from method body * feat(docs): add zh_TW Traditional Chinese version README (#22703) docs: add zh_TW Traditional Chinese version README * chore: ignore renovate major updates for postgres image (#22764) * fix: remove postgres exclude datasource match (#22811) * chore(deps): update github-actions (major) (#22810) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix: revert terragrunt-action bump (#22812) * chore: don't enforce runes (#22813) * chore(deps): update base-image to v202510092146 (major) (#22818) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update typescript-projects (#22809) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler * fix: only cast to device if the asset is a RemoteAsset (#22805) * feat: (perf) remove scroll compensation (#22837) * fix(deps): update dependency happy-dom to v20 [security] (#22846) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update github-actions (#22793) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix: various typos (#22867) Found via `codespell -q 3 -S "*.svg,./i18n,./docs/package-lock.json,./readme_i18n,./mobile/assets/i18n" -L afterall,devlop,finaly,inout,nd,optin,renderd,sade` * fix: ios skip posting hash response after detached from engine (#22695) * skip posting message after detached from engine * review changes * cancel plugin before destroying engine --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex * chore(deps): update ghcr.io/immich-app/postgres:14-vectorchord0.3.0 docker digest to 6f3e9d2 (#22912) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 docker digest to bcf6335 (#22913) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix: re-add scroll compensation (efficiently) (#22848) * fix: re-add scroll compensation (efficient) * Rename showSkeleton to invisible. Adjust skeleton margins, invisible support. * Fix faulty logic, simplify * Calculate ratios and determine compensation strategy: height comp for above/partiality visible, month-scroll comp within a fully visible month. --------- Co-authored-by: Alex * fix: shared album control permissions (#22435) * fix: shared album control permissions * fix: properly display "add photos" * fix: dont allow modification of album order * fix: album title/description edit from app bar * chore: code review changes * chore: format translations * chore: lintings * fix: show dialog before delete local action (#22280) * fix: show dialog on delete local action # Conflicts: # mobile/lib/repositories/asset_media.repository.dart * button style --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex * fix(deps): update dependency kysely-postgres-js to v3 (#22924) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update redis:6.2-alpine docker digest to 77697a7 (#22915) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update typescript-projects (#22918) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler * feat: local album events notification (#22817) * feat: local album events notification * pr feedback * show number of unread notification * chore: refactor show view in timeline button (#22894) * chore: refactor show view in timeline button This refactor includes changes to notify asset viewer about where an asset was shown from. * chore: realized I could just pull from the timelineProvider instead of storing it in the asset viewer state * chore: rename enum to TimelineOrigin and update members * fix: update isOwner condition --------- Co-authored-by: Alex * chore(web): update translations (#22623) Translate-URL: https://hosted.weblate.org/projects/immich/immich/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/bn/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/el/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ka/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/mr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pa/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/ Translation: Immich/immich Co-authored-by: Abhijeet Bonde Co-authored-by: Adam Uchmanowicz Co-authored-by: Adrian Hermida Co-authored-by: Aleksa Milošević Co-authored-by: Amin Co-authored-by: AndreiP28 Co-authored-by: António Santos Co-authored-by: Asger Mogensen Co-authored-by: Christoph Auer Co-authored-by: Denis Pacquier Co-authored-by: DevServs Co-authored-by: Eetu Mäenpää Co-authored-by: Felipe Garcia Co-authored-by: Filip Joković Co-authored-by: Hurricane-32 Co-authored-by: Indrek Haav Co-authored-by: Jason Song Co-authored-by: Javier Villanueva García Co-authored-by: Jordy H Co-authored-by: Jorge Montejo Co-authored-by: Jozef Gaal Co-authored-by: Konstantinos D Co-authored-by: Leo Bottaro Co-authored-by: Linerly Co-authored-by: Liviu Roman Co-authored-by: Lorenz Baum Co-authored-by: Lukas Konsin Co-authored-by: Mandeep Co-authored-by: Marc Casillas Co-authored-by: Marcelo Popper Costa Co-authored-by: MatijaThe245th Co-authored-by: Matjaž T Co-authored-by: Mees Frensel Co-authored-by: Mirko Co-authored-by: Mārtiņš Bruņenieks Co-authored-by: Oleksandr Yurov Co-authored-by: Orkun Sürel Co-authored-by: Peter Dave Hello Co-authored-by: Philipp Burndorfer Co-authored-by: Prasanth Baskar Co-authored-by: Roman Zhukov Co-authored-by: Sayan Goswami Co-authored-by: Sergey Katsubo Co-authored-by: Simon Bierwald Co-authored-by: Sylvain Pichon Co-authored-by: TV Box Co-authored-by: Taiki M Co-authored-by: Theodore Zhvania Co-authored-by: Tim De Meyer Co-authored-by: User 123456789 Co-authored-by: Valentino Harpa Co-authored-by: Vegard Fladby Co-authored-by: Willem Schipper Co-authored-by: Yago Raña Gayoso Co-authored-by: Zurab Sajaia Co-authored-by: albanobattistella Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: dark&white Co-authored-by: eav5jhl0 Co-authored-by: findussoft Co-authored-by: kiwinho Co-authored-by: millallo Co-authored-by: pyccl Co-authored-by: rokon001 Co-authored-by: vaibhav kumar Co-authored-by: waclaw66 Co-authored-by: Максим Горпиніч Co-authored-by: தமிழ்நேரம் * chore: version v2.1.0 * refactor * question marks are the enemy * refactor count map * update readme * e2e * count of 0 is impossible * useless async --------- Co-authored-by: Chaoscontrol <6642238+Chaoscontrol@users.noreply.github.com> Co-authored-by: Brandon Wees Co-authored-by: Alex Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Weblate (bot) Co-authored-by: Arthur Bols Co-authored-by: Ben Kim Co-authored-by: César Gómez Co-authored-by: DR Co-authored-by: DevServs Co-authored-by: Emil Friis Osmann Co-authored-by: Fjuro Co-authored-by: Godwin T Co-authored-by: Hristo T Co-authored-by: Hurricane-32 Co-authored-by: Jozef Gaal Co-authored-by: KecskeTech Co-authored-by: Kiril Panayotov Co-authored-by: Liviu Roman Co-authored-by: Lorenzo Co-authored-by: Marcelo Popper Costa Co-authored-by: Matjaž T Co-authored-by: Miryusif Rahimov Co-authored-by: Msaood Co-authored-by: Mārtiņš Bruņenieks Co-authored-by: Pedro Vendeira Co-authored-by: PontusÖsterlindh Co-authored-by: Rahees Co-authored-by: Sandeep R Co-authored-by: Sylvain Pichon Co-authored-by: TV Box Co-authored-by: Tino Altmann Co-authored-by: User 123456789 Co-authored-by: Vegard Fladby Co-authored-by: anton garcias Co-authored-by: chamdim Co-authored-by: longlarry Co-authored-by: pyccl Co-authored-by: swever Co-authored-by: தமிழ்நேரம் Co-authored-by: 안세훈 Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Xavier Dupuis Co-authored-by: Sergey Katsubo Co-authored-by: Adrian Jost <22987140+adrianjost@users.noreply.github.com> Co-authored-by: Cokodayo <78474654+CaptainJack2491@users.noreply.github.com> Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> Co-authored-by: Yaros Co-authored-by: USBAkimbo <71508071+USBAkimbo@users.noreply.github.com> Co-authored-by: Min Idzelis Co-authored-by: grgergo Co-authored-by: gergo= Co-authored-by: Jorge Montejo Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Jason Rasmussen Co-authored-by: Diogo Correia Co-authored-by: CuberL Co-authored-by: Xantin <56741168+Xiticks@users.noreply.github.com> Co-authored-by: bo0tzz Co-authored-by: Nicholas Flamy <30300649+NicholasFlamy@users.noreply.github.com> Co-authored-by: TDR001 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Saschl <19493808+Saschl@users.noreply.github.com> Co-authored-by: Pascal Sommer Co-authored-by: kaziu687 Co-authored-by: Qhilm <3350433+Qhilm@users.noreply.github.com> Co-authored-by: Sebastian Schneider Co-authored-by: Tushar Harsora Co-authored-by: Peter Dave Hello Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Co-authored-by: Daniel Dietzler Co-authored-by: luzpaz Co-authored-by: Abhijeet Bonde Co-authored-by: Adam Uchmanowicz Co-authored-by: Adrian Hermida Co-authored-by: Aleksa Milošević Co-authored-by: Amin Co-authored-by: AndreiP28 Co-authored-by: António Santos Co-authored-by: Asger Mogensen Co-authored-by: Christoph Auer Co-authored-by: Denis Pacquier Co-authored-by: Eetu Mäenpää Co-authored-by: Felipe Garcia Co-authored-by: Filip Joković Co-authored-by: Indrek Haav Co-authored-by: Jason Song Co-authored-by: Javier Villanueva García Co-authored-by: Jordy H Co-authored-by: Konstantinos D Co-authored-by: Leo Bottaro Co-authored-by: Linerly Co-authored-by: Lorenz Baum Co-authored-by: Lukas Konsin Co-authored-by: Mandeep Co-authored-by: Marc Casillas Co-authored-by: MatijaThe245th Co-authored-by: Mees Frensel Co-authored-by: Mirko Co-authored-by: Oleksandr Yurov Co-authored-by: Orkun Sürel Co-authored-by: Philipp Burndorfer Co-authored-by: Prasanth Baskar Co-authored-by: Roman Zhukov Co-authored-by: Sayan Goswami Co-authored-by: Simon Bierwald Co-authored-by: Taiki M Co-authored-by: Theodore Zhvania Co-authored-by: Tim De Meyer Co-authored-by: Valentino Harpa Co-authored-by: Willem Schipper Co-authored-by: Yago Raña Gayoso Co-authored-by: Zurab Sajaia Co-authored-by: albanobattistella Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: dark&white Co-authored-by: eav5jhl0 Co-authored-by: findussoft Co-authored-by: kiwinho Co-authored-by: millallo Co-authored-by: rokon001 Co-authored-by: vaibhav kumar Co-authored-by: waclaw66 Co-authored-by: Максим Горпиніч --- e2e/src/api/specs/album.e2e-spec.ts | 5 + mobile/openapi/README.md | 1 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 2 + .../openapi/lib/model/album_response_dto.dart | 9 +- .../model/contributor_count_response_dto.dart | 107 ++++++++++++++++++ open-api/immich-openapi-specs.json | 21 ++++ open-api/typescript-sdk/src/fetch-client.ts | 5 + server/src/dtos/album.dto.ts | 13 +++ server/src/queries/album.repository.sql | 15 +++ server/src/repositories/album.repository.ts | 18 +++ server/src/services/album.service.ts | 5 + web/src/lib/modals/AlbumUsersModal.svelte | 12 ++ 13 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 mobile/openapi/lib/model/contributor_count_response_dto.dart diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 5615a312f2..c4f06edd93 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -136,6 +136,7 @@ describe('/albums', () => { expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining({ isFavorite: false })], + contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), endDate: expect.any(String), @@ -310,6 +311,7 @@ describe('/albums', () => { expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], + contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), endDate: expect.any(String), @@ -345,6 +347,7 @@ describe('/albums', () => { expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], + contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), endDate: expect.any(String), @@ -362,6 +365,7 @@ describe('/albums', () => { expect(body).toEqual({ ...user1Albums[0], assets: [], + contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), endDate: expect.any(String), @@ -382,6 +386,7 @@ describe('/albums', () => { expect(body).toEqual({ ...user2Albums[0], assets: [], + contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), endDate: expect.any(String), diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 4933c2c2b5..698b9774da 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -359,6 +359,7 @@ Class | Method | HTTP request | Description - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - [Colorspace](doc//Colorspace.md) + - [ContributorCountResponseDto](doc//ContributorCountResponseDto.md) - [CreateAlbumDto](doc//CreateAlbumDto.md) - [CreateLibraryDto](doc//CreateLibraryDto.md) - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index df2c2226b1..a3117ede86 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -130,6 +130,7 @@ part 'model/change_password_dto.dart'; part 'model/check_existing_assets_dto.dart'; part 'model/check_existing_assets_response_dto.dart'; part 'model/colorspace.dart'; +part 'model/contributor_count_response_dto.dart'; part 'model/create_album_dto.dart'; part 'model/create_library_dto.dart'; part 'model/create_profile_image_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 06d27593c9..33056cf14e 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -314,6 +314,8 @@ class ApiClient { return CheckExistingAssetsResponseDto.fromJson(value); case 'Colorspace': return ColorspaceTypeTransformer().decode(value); + case 'ContributorCountResponseDto': + return ContributorCountResponseDto.fromJson(value); case 'CreateAlbumDto': return CreateAlbumDto.fromJson(value); case 'CreateLibraryDto': diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 547a6a70fd..2f53706e7a 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -18,6 +18,7 @@ class AlbumResponseDto { this.albumUsers = const [], required this.assetCount, this.assets = const [], + this.contributorCounts = const [], required this.createdAt, required this.description, this.endDate, @@ -43,6 +44,8 @@ class AlbumResponseDto { List assets; + List contributorCounts; + DateTime createdAt; String description; @@ -100,6 +103,7 @@ class AlbumResponseDto { _deepEquality.equals(other.albumUsers, albumUsers) && other.assetCount == assetCount && _deepEquality.equals(other.assets, assets) && + _deepEquality.equals(other.contributorCounts, contributorCounts) && other.createdAt == createdAt && other.description == description && other.endDate == endDate && @@ -122,6 +126,7 @@ class AlbumResponseDto { (albumUsers.hashCode) + (assetCount.hashCode) + (assets.hashCode) + + (contributorCounts.hashCode) + (createdAt.hashCode) + (description.hashCode) + (endDate == null ? 0 : endDate!.hashCode) + @@ -137,7 +142,7 @@ class AlbumResponseDto { (updatedAt.hashCode); @override - String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; + String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -150,6 +155,7 @@ class AlbumResponseDto { json[r'albumUsers'] = this.albumUsers; json[r'assetCount'] = this.assetCount; json[r'assets'] = this.assets; + json[r'contributorCounts'] = this.contributorCounts; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; if (this.endDate != null) { @@ -196,6 +202,7 @@ class AlbumResponseDto { albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), assetCount: mapValueOfType(json, r'assetCount')!, assets: AssetResponseDto.listFromJson(json[r'assets']), + contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']), createdAt: mapDateTime(json, r'createdAt', r'')!, description: mapValueOfType(json, r'description')!, endDate: mapDateTime(json, r'endDate', r''), diff --git a/mobile/openapi/lib/model/contributor_count_response_dto.dart b/mobile/openapi/lib/model/contributor_count_response_dto.dart new file mode 100644 index 0000000000..e0e16ee427 --- /dev/null +++ b/mobile/openapi/lib/model/contributor_count_response_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ContributorCountResponseDto { + /// Returns a new [ContributorCountResponseDto] instance. + ContributorCountResponseDto({ + required this.assetCount, + required this.userId, + }); + + int assetCount; + + String userId; + + @override + bool operator ==(Object other) => identical(this, other) || other is ContributorCountResponseDto && + other.assetCount == assetCount && + other.userId == userId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetCount.hashCode) + + (userId.hashCode); + + @override + String toString() => 'ContributorCountResponseDto[assetCount=$assetCount, userId=$userId]'; + + Map toJson() { + final json = {}; + json[r'assetCount'] = this.assetCount; + json[r'userId'] = this.userId; + return json; + } + + /// Returns a new [ContributorCountResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ContributorCountResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ContributorCountResponseDto"); + if (value is Map) { + final json = value.cast(); + + return ContributorCountResponseDto( + assetCount: mapValueOfType(json, r'assetCount')!, + userId: mapValueOfType(json, r'userId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ContributorCountResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ContributorCountResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ContributorCountResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ContributorCountResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetCount', + 'userId', + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2c045a73f0..b9da330ee5 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10096,6 +10096,12 @@ }, "type": "array" }, + "contributorCounts": { + "items": { + "$ref": "#/components/schemas/ContributorCountResponseDto" + }, + "type": "array" + }, "createdAt": { "format": "date-time", "type": "string" @@ -11471,6 +11477,21 @@ ], "type": "string" }, + "ContributorCountResponseDto": { + "properties": { + "assetCount": { + "type": "integer" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "assetCount", + "userId" + ], + "type": "object" + }, "CreateAlbumDto": { "properties": { "albumName": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 38a57e4b5d..60d72fb32b 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -356,12 +356,17 @@ export type AssetResponseDto = { updatedAt: string; visibility: AssetVisibility; }; +export type ContributorCountResponseDto = { + assetCount: number; + userId: string; +}; export type AlbumResponseDto = { albumName: string; albumThumbnailAssetId: string | null; albumUsers: AlbumUserResponseDto[]; assetCount: number; assets: AssetResponseDto[]; + contributorCounts?: ContributorCountResponseDto[]; createdAt: string; description: string; endDate?: string; diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 00f5759aac..2f3f22099a 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -128,6 +128,14 @@ export class AlbumUserResponseDto { role!: AlbumUserRole; } +export class ContributorCountResponseDto { + @ApiProperty() + userId!: string; + + @ApiProperty({ type: 'integer' }) + assetCount!: number; +} + export class AlbumResponseDto { id!: string; ownerId!: string; @@ -149,6 +157,11 @@ export class AlbumResponseDto { isActivityEnabled!: boolean; @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) order?: AssetOrder; + + // Optional per-user contribution counts for shared albums + @Type(() => ContributorCountResponseDto) + @ApiProperty({ type: [ContributorCountResponseDto], required: false }) + contributorCounts?: ContributorCountResponseDto[]; } export type MapAlbumDto = { diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 36c44414db..0087217738 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -407,3 +407,18 @@ from where "album_asset"."albumsId" = $1 and "album_asset"."assetsId" in ($2) + +-- AlbumRepository.getContributorCounts +select + "asset"."ownerId" as "userId", + count(*) as "assetCount" +from + "album_asset" + inner join "asset" on "asset"."id" = "assetsId" +where + "asset"."deletedAt" is null + and "album_asset"."albumsId" = $1 +group by + "asset"."ownerId" +order by + "assetCount" desc diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index b023068f16..00c1dfda7f 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -379,4 +379,22 @@ export class AlbumRepository { ) .whereRef('album_asset.albumsId', '=', 'album.id'); } + + /** + * Get per-user asset contribution counts for a single album. + * Excludes deleted assets, orders by count desc. + */ + @GenerateSql({ params: [DummyValue.UUID] }) + getContributorCounts(id: string) { + return this.db + .selectFrom('album_asset') + .innerJoin('asset', 'asset.id', 'assetsId') + .where('asset.deletedAt', 'is', sql.lit(null)) + .where('album_asset.albumsId', '=', id) + .select('asset.ownerId as userId') + .select((eb) => eb.fn.countAll().as('assetCount')) + .groupBy('asset.ownerId') + .orderBy('assetCount', 'desc') + .execute(); + } } diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index d7b857d666..dd12e31892 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -79,12 +79,17 @@ export class AlbumService extends BaseService { const album = await this.findOrFail(id, { withAssets }); const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]); + const hasSharedUsers = album.albumUsers && album.albumUsers.length > 0; + const hasSharedLink = album.sharedLinks && album.sharedLinks.length > 0; + const isShared = hasSharedUsers || hasSharedLink; + return { ...mapAlbum(album, withAssets, auth), startDate: albumMetadataForIds?.startDate ?? undefined, endDate: albumMetadataForIds?.endDate ?? undefined, assetCount: albumMetadataForIds?.assetCount ?? 0, lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined, + contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined, }; } diff --git a/web/src/lib/modals/AlbumUsersModal.svelte b/web/src/lib/modals/AlbumUsersModal.svelte index 3cc0f03d5e..7dd7442249 100644 --- a/web/src/lib/modals/AlbumUsersModal.svelte +++ b/web/src/lib/modals/AlbumUsersModal.svelte @@ -31,6 +31,14 @@ let isOwned = $derived(currentUser?.id == album.ownerId); + // Build a map of contributor counts by user id; avoid casts/derived + const contributorCounts: Record = {}; + if (album.contributorCounts) { + for (const { userId, assetCount } of album.contributorCounts) { + contributorCounts[userId] = assetCount; + } + } + onMount(async () => { try { currentUser = await getMyUser(); @@ -111,6 +119,10 @@ {:else} {$t('role_editor')} {/if} + {#if user.id in contributorCounts} + - + {$t('items_count', { values: { count: contributorCounts[user.id] } })} + {/if}