diff --git a/mobile/openapi/doc/AlbumResponseDto.md b/mobile/openapi/doc/AlbumResponseDto.md index d257dea2e9..08425de5cb 100644 Binary files a/mobile/openapi/doc/AlbumResponseDto.md and b/mobile/openapi/doc/AlbumResponseDto.md differ diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 4661d5f281..3ef986cd6a 100644 Binary files a/mobile/openapi/lib/model/album_response_dto.dart and b/mobile/openapi/lib/model/album_response_dto.dart differ diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index bc8c153958..b55b0eb219 100644 Binary files a/mobile/openapi/test/album_response_dto_test.dart and b/mobile/openapi/test/album_response_dto_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 12fa55b593..e3de261584 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -4584,6 +4584,10 @@ }, "owner": { "$ref": "#/components/schemas/UserResponseDto" + }, + "lastModifiedAssetTimestamp": { + "format": "date-time", + "type": "string" } }, "required": [ diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts index 2af7810db3..e50c8aa161 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/domain/album/album-response.dto.ts @@ -16,6 +16,7 @@ export class AlbumResponseDto { owner!: UserResponseDto; @ApiProperty({ type: 'integer' }) assetCount!: number; + lastModifiedAssetTimestamp?: Date; } export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 21461fe271..ba350db2ca 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -53,15 +53,19 @@ export class AlbumService { return obj; }, {}); - return albums.map((album) => { - return { - ...album, - assets: album?.assets?.map(mapAsset), - sharedLinks: undefined, // Don't return shared links - shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0, - assetCount: albumsAssetCountObj[album.id], - } as AlbumResponseDto; - }); + return Promise.all( + albums.map(async (album) => { + const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id); + return { + ...album, + assets: album?.assets?.map(mapAsset), + sharedLinks: undefined, // Don't return shared links + shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0, + assetCount: albumsAssetCountObj[album.id], + lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt, + } as AlbumResponseDto; + }), + ); } private async updateInvalidThumbnails(): Promise { diff --git a/server/src/domain/asset/asset.repository.ts b/server/src/domain/asset/asset.repository.ts index 16214931a6..9479d3c120 100644 --- a/server/src/domain/asset/asset.repository.ts +++ b/server/src/domain/asset/asset.repository.ts @@ -47,6 +47,7 @@ export interface IAssetRepository { getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated; getWith(pagination: PaginationOptions, property: WithProperty): Paginated; getFirstAssetForAlbumId(albumId: string): Promise; + getLastUpdatedAssetForAlbumId(albumId: string): Promise; deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; save(asset: Partial): Promise; diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 54b4523e4e..1139dbf11f 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -248,6 +248,13 @@ export class AssetRepository implements IAssetRepository { }); } + getLastUpdatedAssetForAlbumId(albumId: string): Promise { + return this.repository.findOne({ + where: { albums: { id: albumId } }, + order: { updatedAt: 'DESC' }, + }); + } + async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise { const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options; diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 5418176f38..51dbb3a272 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -7,6 +7,7 @@ export const newAssetRepositoryMock = (): jest.Mocked => { getWithout: jest.fn(), getWith: jest.fn(), getFirstAssetForAlbumId: jest.fn(), + getLastUpdatedAssetForAlbumId: jest.fn(), getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index b49965514d..97a52ec8ec 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -284,6 +284,12 @@ export interface AlbumResponseDto { * @memberof AlbumResponseDto */ 'owner': UserResponseDto; + /** + * + * @type {string} + * @memberof AlbumResponseDto + */ + 'lastModifiedAssetTimestamp'?: string; } /** * diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index da7acc74ba..851c37c167 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -15,8 +15,17 @@ export let data: PageData; + const sortByOptions = ['Most recent photo', 'Last modified', 'Album title']; + + let selectedSortBy = sortByOptions[0]; + + const handleChangeSortBy = (e: Event) => { + const target = e.target as HTMLSelectElement; + selectedSortBy = target.value; + }; + const { - albums, + albums: unsortedAlbums, isShowContextMenu, contextMenuPosition, createAlbum, @@ -26,6 +35,28 @@ closeAlbumContextMenu } = useAlbums({ albums: data.albums }); + let albums = unsortedAlbums; + + const sortByDate = (a: string, b: string) => { + const aDate = new Date(a); + const bDate = new Date(b); + return bDate.getTime() - aDate.getTime(); + }; + + $: { + if (selectedSortBy === 'Most recent photo') { + $albums = $unsortedAlbums.sort((a, b) => + a.lastModifiedAssetTimestamp && b.lastModifiedAssetTimestamp + ? sortByDate(a.lastModifiedAssetTimestamp, b.lastModifiedAssetTimestamp) + : sortByDate(a.updatedAt, b.updatedAt) + ); + } else if (selectedSortBy === 'Last modified') { + $albums = $unsortedAlbums.sort((a, b) => sortByDate(a.updatedAt, b.updatedAt)); + } else if (selectedSortBy === 'Album title') { + $albums = $unsortedAlbums.sort((a, b) => a.albumName.localeCompare(b.albumName)); + } + } + const handleCreateAlbum = async () => { const newAlbum = await createAlbum(); if (newAlbum) { @@ -52,7 +83,20 @@ -
+
+ + +